Compare commits

...
Author SHA1 Message Date
Qiufeng de63fcffff feat: show system update history as timeline
Release / release (push) Canceled after 0s
2026-08-19 07:26:48 +08:00
4 changed files with 549 additions and 49 deletions
+9 -9
View File
@@ -83,7 +83,7 @@ PostgreSQL 18 兼容工作继续冻结。
先在宝塔面板停止并删除当前错误的 Java 项目,再执行: 先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.44/install.sh | sudo env KAIDI_APP_PORT=18080 bash curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/install.sh | sudo env KAIDI_APP_PORT=18080 bash
``` ```
该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括 该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括
@@ -96,7 +96,7 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
随后执行一键清理: 随后执行一键清理:
```bash ```bash
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.44/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash
``` ```
上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。 上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。
@@ -117,9 +117,9 @@ Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员
下载地址: 下载地址:
`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.44/kaidi-finance-1.0.0-preview.44.tar.gz` `https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/kaidi-finance-1.0.0-preview.45.tar.gz`
校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.44/SHA256SUMS` 校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/SHA256SUMS`
服务器要求:Linux、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;**systemd 一键安装**还需要 服务器要求:Linux、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;**systemd 一键安装**还需要
systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux 需要宿主机 systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux 需要宿主机
@@ -136,7 +136,7 @@ systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。 必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
```bash ```bash
VERSION=1.0.0-preview.44 VERSION=1.0.0-preview.45
APP_ROOT=/www/wwwroot/kaidi APP_ROOT=/www/wwwroot/kaidi
RELEASE_ROOT="$APP_ROOT/releases/$VERSION" RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
sudo install -d -m 0755 "$RELEASE_ROOT" sudo install -d -m 0755 "$RELEASE_ROOT"
@@ -245,7 +245,7 @@ MySQL 8.4;提前准备外部 MySQL,完成上述向导即可。systemd 在线
`KAIDI_PURGE_DELETE_BACKUP=true`,实现本地受管文件和恢复包一并清除: `KAIDI_PURGE_DELETE_BACKUP=true`,实现本地受管文件和恢复包一并清除:
```bash ```bash
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.44/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash
``` ```
执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用, 执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
@@ -271,8 +271,8 @@ Actions 页面显示 “No matching online runner”,先启动并注册该标
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布: Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
```bash ```bash
git tag v1.0.0-preview.44 git tag v1.0.0-preview.45
git push origin v1.0.0-preview.44 git push origin v1.0.0-preview.45
``` ```
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在 在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
@@ -324,7 +324,7 @@ cat /var/lib/kaidi-update/status.json
```bash ```bash
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \ KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/package-release.sh 1.0.0-preview.44 ./scripts/package-release.sh 1.0.0-preview.45
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/verify-release.sh dist/release ./scripts/verify-release.sh dist/release
``` ```
+1 -1
View File
@@ -15,7 +15,7 @@ FILE_SCANNER_ENABLED=true
FINANCE_BOOTSTRAP_ENABLED=false FINANCE_BOOTSTRAP_ENABLED=false
FINANCE_BOOTSTRAP_PASSWORD= FINANCE_BOOTSTRAP_PASSWORD=
APP_VERSION=1.0.0-preview.44 APP_VERSION=1.0.0-preview.45
UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION
FINANCE_UPDATE_ENABLED=true FINANCE_UPDATE_ENABLED=true
# Use either a stable direct asset base URL or the public Gitea latest-release API. # Use either a stable direct asset base URL or the public Gitea latest-release API.
+12 -3
View File
@@ -138,6 +138,7 @@ async function installFixture(
beforeJson: null, beforeJson: null,
afterJson: JSON.stringify({ afterJson: JSON.stringify({
targetVersion, targetVersion,
...(actionCode === 'SYSTEM_UPDATE_CHECK' ? { releaseNotes: 'Preview update' } : {}),
...(actionCode === 'SYSTEM_UPDATE_SUCCEEDED' ? { state: 'SUCCEEDED' } : {}), ...(actionCode === 'SYSTEM_UPDATE_SUCCEEDED' ? { state: 'SUCCEEDED' } : {}),
}), }),
occurredAt: '2026-08-16T00:02:00Z', occurredAt: '2026-08-16T00:02:00Z',
@@ -415,9 +416,17 @@ test('successful installation starts the ten-second automatic refresh countdown'
timeout: 5_000, timeout: 5_000,
}); });
const historyPanel = page.locator('.history-panel'); const historyPanel = page.locator('.history-panel');
const completedRow = historyPanel.locator('tr').filter({ hasText: '更新完成' }); const timelineItem = historyPanel.locator('.update-timeline__item[data-version="1.0.0-preview.2"]');
await expect(completedRow).toContainText('1.0.0-preview.2'); await expect(timelineItem).toHaveCount(1);
await expect(completedRow).toContainText('新版本已通过健康检查'); await expect(timelineItem).toContainText('更新完成');
await expect(timelineItem).toContainText('新版本已通过健康检查');
await expect(timelineItem).toContainText('Preview update');
await timelineItem.getByRole('button', { name: '查看完整记录', exact: true }).click();
const historyDialog = page.getByRole('dialog', { name: '更新日志详情' });
await expect(historyDialog).toContainText('获取版本');
await expect(historyDialog).toContainText('下载更新包');
await expect(historyDialog).toContainText('立即更新并重启');
await expect(historyDialog).toContainText('更新完成');
}); });
test('reloading during installation resumes polling and starts the refresh countdown', async ({ page }) => { test('reloading during installation resumes polling and starts the refresh countdown', async ({ page }) => {
@@ -111,9 +111,9 @@
<div class="section-heading"> <div class="section-heading">
<div> <div>
<h2>历史更新记录</h2> <h2>历史更新记录</h2>
<p>记录版本获取、下载、重启安装和最终执行结果。</p> <p>按版本汇总获取、下载、重启安装和最终执行结果。</p>
</div> </div>
<span>最近 {{ historyRows.length }} 条</span> <span>最近 {{ historyTimeline.length }} 个版本 · {{ historyRows.length }} 条记录</span>
</div> </div>
<t-alert v-if="historyError" theme="error" :close-btn="false"> <t-alert v-if="historyError" theme="error" :close-btn="false">
@@ -122,23 +122,65 @@
<t-link theme="primary" @click="() => loadHistory()">重新加载</t-link> <t-link theme="primary" @click="() => loadHistory()">重新加载</t-link>
</div> </div>
</t-alert> </t-alert>
<t-table <div v-if="historyLoading" class="history-loading" aria-live="polite">
v-if="historyLoading || historyRows.length" <t-loading text="正在加载历史更新记录" />
:columns="historyColumns" </div>
:data="historyRows" <div v-else-if="historyTimeline.length" class="update-timeline" aria-label="按版本排列的更新时间线">
row-key="publicId" <article
:loading="historyLoading" v-for="item in historyTimeline"
table-layout="fixed" :key="item.key"
:horizontal-scroll-affixed-bottom="true" class="update-timeline__item"
> :data-version="item.version"
<template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template> >
<template #actionCode="{ row }">{{ actionLabel(row.actionCode) }}</template> <div class="update-timeline__rail" aria-hidden="true">
<template #version="{ row }">{{ historyVersion(row) }}</template> <span class="update-timeline__marker" :class="`update-timeline__marker--${resultTheme(item.resultCode)}`">
<template #resultCode="{ row }"> <t-icon :name="item.resultCode === 'SUCCESS' ? 'check' : 'error-circle'" />
<t-tag :theme="resultTheme(row.resultCode)" variant="light">{{ resultLabel(row.resultCode) }}</t-tag> </span>
</template> </div>
<template #reason="{ row }">{{ row.reason || '-' }}</template> <div class="timeline-card">
</t-table> <div class="timeline-card__header">
<div class="timeline-card__identity">
<strong>{{ item.version }}</strong>
<span>{{ formatDateTime(item.occurredAt) }} · {{ item.username || '系统' }}</span>
</div>
<t-tag :theme="resultTheme(item.resultCode)" variant="light">{{ resultLabel(item.resultCode) }}</t-tag>
</div>
<div class="timeline-card__summary">
<span class="timeline-card__label">执行流程</span>
<span>{{ item.operations.join(' · ') }}</span>
<span class="timeline-card__count">{{ item.steps.length }} 条记录</span>
</div>
<p class="timeline-card__reason">{{ item.reason || '已记录本次版本更新操作。' }}</p>
<div v-if="item.releaseNotes" class="timeline-card__notes">
<span>版本说明</span>
<p>{{ item.releaseNotes }}</p>
</div>
<div class="timeline-steps" aria-label="更新步骤">
<div v-for="step in item.steps" :key="step.row.publicId" class="timeline-step">
<span class="timeline-step__dot" :class="`timeline-step__dot--${resultTheme(step.row.resultCode)}`" />
<div class="timeline-step__body">
<div class="timeline-step__heading">
<strong>{{ actionLabel(step.row.actionCode) }}</strong>
<t-tag size="small" :theme="resultTheme(step.row.resultCode)" variant="light">
{{ resultLabel(step.row.resultCode) }}
</t-tag>
<time>{{ formatDateTime(step.row.occurredAt) }}</time>
</div>
<p>{{ step.row.reason || '操作已记录' }} · {{ step.row.username || '系统' }}</p>
</div>
</div>
</div>
<div class="timeline-card__footer">
<span>最后更新:{{ formatDateTime(item.occurredAt) }}</span>
<t-button variant="text" size="small" @click="openHistoryDetail(item)">查看完整记录</t-button>
</div>
</div>
</article>
</div>
<t-empty v-else description="暂无系统更新记录" /> <t-empty v-else description="暂无系统更新记录" />
</section> </section>
@@ -223,11 +265,47 @@
</div> </div>
</div> </div>
</t-dialog> </t-dialog>
<t-dialog
v-model:visible="historyDetailVisible"
header="更新日志详情"
aria-label="更新日志详情"
width="min(680px, calc(100vw - 32px))"
:footer="false"
>
<div v-if="selectedHistory" class="history-detail" role="dialog" aria-label="更新日志详情" aria-modal="true">
<div class="history-detail__header">
<div>
<span>目标版本</span>
<strong>{{ selectedHistory.version }}</strong>
</div>
<t-tag :theme="resultTheme(selectedHistory.resultCode)" variant="light">
{{ resultLabel(selectedHistory.resultCode) }}
</t-tag>
</div>
<p class="history-detail__reason">{{ selectedHistory.reason || '已记录本次版本更新操作。' }}</p>
<div v-if="selectedHistory.releaseNotes" class="history-detail__notes">
<span>版本说明</span>
<p>{{ selectedHistory.releaseNotes }}</p>
</div>
<div class="history-detail__steps" aria-label="更新日志">
<div v-for="step in selectedHistory.steps" :key="step.row.publicId" class="history-detail__step">
<div>
<strong>{{ actionLabel(step.row.actionCode) }}</strong>
<t-tag size="small" :theme="resultTheme(step.row.resultCode)" variant="light">
{{ resultLabel(step.row.resultCode) }}
</t-tag>
</div>
<time>{{ formatDateTime(step.row.occurredAt) }} · {{ step.row.username || '系统' }}</time>
<p>{{ step.row.reason || '操作已记录' }}</p>
</div>
</div>
</div>
</t-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { isAxiosError } from 'axios'; import { isAxiosError } from 'axios';
import type { PrimaryTableCol } from 'tdesign-vue-next';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
@@ -244,6 +322,22 @@ import {
defineOptions({ name: 'SystemUpdatePage' }); defineOptions({ name: 'SystemUpdatePage' });
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger'; type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
interface HistoryTimelineStep {
row: AuditLog;
}
interface HistoryTimelineItem {
key: string;
version: string;
occurredAt: string;
username: string;
resultCode: string;
reason: string;
operations: string[];
releaseNotes: string;
steps: HistoryTimelineStep[];
}
const UPDATE_PENDING_KEY = 'kaidi-system-update-pending'; const UPDATE_PENDING_KEY = 'kaidi-system-update-pending';
const updateStatus = ref<SystemUpdateView | null>(null); const updateStatus = ref<SystemUpdateView | null>(null);
@@ -259,6 +353,8 @@ const historyError = ref('');
const updateRefreshSeconds = ref<number | null>(null); const updateRefreshSeconds = ref<number | null>(null);
const restartCountdown = ref<number | null>(null); const restartCountdown = ref<number | null>(null);
const updateDialogVisible = ref(false); const updateDialogVisible = ref(false);
const historyDetailVisible = ref(false);
const selectedHistory = ref<HistoryTimelineItem | null>(null);
const connectionInterrupted = ref(false); const connectionInterrupted = ref(false);
const localEvents = ref<SystemUpdateEvent[]>([]); const localEvents = ref<SystemUpdateEvent[]>([]);
const awaitingRefresh = ref(false); const awaitingRefresh = ref(false);
@@ -387,14 +483,39 @@ const updateStateTheme = computed<TagTheme>(() => {
return 'default'; return 'default';
}); });
const historyColumns: PrimaryTableCol[] = [ const historyTimeline = computed<HistoryTimelineItem[]>(() => {
{ colKey: 'occurredAt', title: '操作时间', width: 180 }, const grouped = new Map<string, AuditLog[]>();
{ colKey: 'version', title: '版本', width: 170, ellipsis: true }, for (const row of historyRows.value) {
{ colKey: 'actionCode', title: '操作', width: 150 }, const version = historyVersion(row);
{ colKey: 'username', title: '操作人', width: 130, ellipsis: true }, const key = version === '-' ? `unknown:${row.requestId || row.publicId}` : `version:${version}`;
{ colKey: 'resultCode', title: '结果', width: 100 }, const rows = grouped.get(key) || [];
{ colKey: 'reason', title: '说明', minWidth: 220, ellipsis: true }, rows.push(row);
]; grouped.set(key, rows);
}
return [...grouped.entries()]
.map(([key, rows]) => {
const newestFirst = rows.slice().sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
const terminal = newestFirst.find((row) => isTerminalHistoryAction(row.actionCode));
const representative = terminal || newestFirst[0];
const chronological = newestFirst.slice().reverse();
const operations = [...new Set(chronological.map((row) => actionLabel(row.actionCode)).filter(Boolean))];
const releaseNotes = newestFirst.map((row) => historyReleaseNotes(row)).find(Boolean) || '';
return {
key,
version: representative ? historyVersion(representative) : '-',
occurredAt: representative?.occurredAt || '',
username: representative?.username || '',
resultCode: representative?.resultCode || 'UNKNOWN',
reason: representative?.reason || '',
operations,
releaseNotes,
steps: chronological.map((row) => ({ row })),
};
})
.sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
});
async function loadStatus(silent = false) { async function loadStatus(silent = false) {
if (!silent) { if (!silent) {
@@ -705,20 +826,49 @@ function eventStageLabel(stage: string) {
); );
} }
function compareHistoryDates(left: string, right: string) {
const leftTime = Date.parse(left);
const rightTime = Date.parse(right);
if (Number.isNaN(leftTime) || Number.isNaN(rightTime)) return left.localeCompare(right);
return leftTime - rightTime;
}
function isTerminalHistoryAction(action: string) {
return ['SYSTEM_UPDATE_SUCCEEDED', 'SYSTEM_UPDATE_FAILED', 'SYSTEM_UPDATE_RECOVERY_REQUIRED'].includes(action);
}
function parseHistoryPayload(source?: string | null) {
if (!source) return null;
try {
const parsed: unknown = JSON.parse(source);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;
} catch {
return null;
}
}
function historyVersion(row: AuditLog) { function historyVersion(row: AuditLog) {
for (const source of [row.afterJson, row.beforeJson]) { for (const source of [row.afterJson, row.beforeJson]) {
if (!source) continue; const parsed = parseHistoryPayload(source);
try { const version = parsed?.targetVersion || parsed?.latestVersion || parsed?.currentVersion || parsed?.version;
const parsed = JSON.parse(source) as Record<string, unknown>; if (version !== undefined && version !== null && String(version).trim()) return String(version);
const version = parsed.targetVersion || parsed.latestVersion || parsed.currentVersion || parsed.version;
if (version) return String(version);
} catch {
continue;
}
} }
return '-'; return '-';
} }
function historyReleaseNotes(row: AuditLog) {
for (const source of [row.afterJson, row.beforeJson]) {
const parsed = parseHistoryPayload(source);
if (typeof parsed?.releaseNotes === 'string' && parsed.releaseNotes.trim()) return parsed.releaseNotes;
}
return '';
}
function openHistoryDetail(item: HistoryTimelineItem) {
selectedHistory.value = item;
historyDetailVisible.value = true;
}
function actionLabel(action: string) { function actionLabel(action: string) {
return ( return (
{ {
@@ -926,6 +1076,313 @@ onBeforeUnmount(() => {
line-height: 24px; line-height: 24px;
} }
.history-loading {
display: flex;
justify-content: center;
min-height: 160px;
padding: 32px 0;
}
.update-timeline {
display: flex;
flex-direction: column;
gap: 18px;
}
.update-timeline__item {
display: grid;
grid-template-columns: 24px minmax(0, 1fr);
gap: 14px;
min-width: 0;
}
.update-timeline__rail {
position: relative;
display: flex;
justify-content: center;
}
.update-timeline__rail::after {
position: absolute;
top: 30px;
bottom: -18px;
width: 1px;
background: var(--td-component-border);
content: '';
}
.update-timeline__item:last-child .update-timeline__rail::after {
display: none;
}
.update-timeline__marker {
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
color: var(--td-text-color-secondary);
background: var(--td-bg-color-container);
border: 2px solid var(--td-component-border);
border-radius: 50%;
}
.update-timeline__marker--success {
color: #fff;
background: var(--td-success-color);
border-color: var(--td-success-color);
}
.update-timeline__marker--danger {
color: #fff;
background: var(--td-error-color);
border-color: var(--td-error-color);
}
.update-timeline__marker--warning {
color: #fff;
background: var(--td-warning-color);
border-color: var(--td-warning-color);
}
.timeline-card {
min-width: 0;
padding: 16px;
background: var(--td-bg-color-container);
border: 1px solid var(--td-component-border);
border-radius: 6px;
}
.timeline-card__header,
.timeline-card__summary,
.timeline-card__footer,
.timeline-step__heading,
.history-detail__header {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.timeline-card__header,
.timeline-card__footer,
.history-detail__header {
justify-content: space-between;
}
.timeline-card__identity {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.timeline-card__identity strong {
overflow: hidden;
color: var(--td-text-color-primary);
font-size: 16px;
line-height: 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.timeline-card__identity span,
.timeline-card__count,
.timeline-card__footer,
.timeline-step__heading time,
.history-detail__header span,
.history-detail__step time {
color: var(--td-text-color-secondary);
font-size: 12px;
}
.timeline-card__summary {
flex-wrap: wrap;
margin-top: 12px;
padding: 9px 10px;
color: var(--td-text-color-primary);
font-size: 13px;
line-height: 20px;
background: var(--td-bg-color-secondarycontainer);
border-radius: 4px;
}
.timeline-card__label {
color: var(--td-text-color-secondary);
}
.timeline-card__count {
margin-left: auto;
white-space: nowrap;
}
.timeline-card__reason,
.history-detail__reason {
margin: 12px 0 0;
color: var(--td-text-color-primary);
line-height: 22px;
overflow-wrap: anywhere;
}
.timeline-card__notes,
.history-detail__notes {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--td-component-border);
}
.timeline-card__notes > span,
.history-detail__notes > span {
color: var(--td-text-color-secondary);
font-size: 12px;
}
.timeline-card__notes p,
.history-detail__notes p {
margin: 0;
color: var(--td-text-color-primary);
line-height: 22px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.timeline-steps {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 14px;
}
.timeline-step {
display: grid;
grid-template-columns: 10px minmax(0, 1fr);
gap: 10px;
min-width: 0;
}
.timeline-step__dot {
width: 8px;
height: 8px;
margin-top: 7px;
background: var(--td-component-border);
border-radius: 50%;
}
.timeline-step__dot--success {
background: var(--td-success-color);
}
.timeline-step__dot--danger {
background: var(--td-error-color);
}
.timeline-step__dot--warning {
background: var(--td-warning-color);
}
.timeline-step__body {
min-width: 0;
}
.timeline-step__heading {
flex-wrap: wrap;
}
.timeline-step__heading strong {
color: var(--td-text-color-primary);
font-size: 13px;
font-weight: 600;
}
.timeline-step__heading time {
margin-left: auto;
white-space: nowrap;
}
.timeline-step__body p {
margin: 3px 0 0;
color: var(--td-text-color-secondary);
font-size: 12px;
line-height: 20px;
overflow-wrap: anywhere;
}
.timeline-card__footer {
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid var(--td-component-border);
}
.history-detail {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
}
.history-detail__header > div {
display: flex;
flex-direction: column;
gap: 4px;
}
.history-detail__header strong {
color: var(--td-text-color-primary);
font-size: 18px;
line-height: 26px;
}
.history-detail__steps {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 420px;
padding: 12px;
overflow: auto;
background: var(--td-bg-color-secondarycontainer);
border: 1px solid var(--td-component-border);
border-radius: 6px;
}
.history-detail__step {
padding-bottom: 12px;
border-bottom: 1px solid var(--td-component-border);
}
.history-detail__step:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.history-detail__step > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.history-detail__step strong {
color: var(--td-text-color-primary);
font-size: 13px;
}
.history-detail__step time {
display: block;
margin-top: 4px;
}
.history-detail__step p {
margin: 4px 0 0;
color: var(--td-text-color-secondary);
font-size: 13px;
line-height: 20px;
overflow-wrap: anywhere;
}
.alert-content { .alert-content {
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
@@ -1077,6 +1534,40 @@ onBeforeUnmount(() => {
gap: 8px; gap: 8px;
} }
.update-timeline__item {
grid-template-columns: 18px minmax(0, 1fr);
gap: 8px;
}
.update-timeline__marker {
width: 18px;
height: 18px;
}
.update-timeline__rail::after {
top: 24px;
}
.timeline-card {
padding: 12px;
}
.timeline-card__count {
width: 100%;
margin-left: 0;
}
.timeline-step__heading time {
width: 100%;
margin-left: 0;
}
.timeline-card__notes,
.history-detail__notes {
grid-template-columns: 1fr;
gap: 4px;
}
.download-progress__meta, .download-progress__meta,
.dialog-status-line, .dialog-status-line,
.dialog-actions { .dialog-actions {