From 511f67b68be98aecb3f54cdecbaee690ae937d1a Mon Sep 17 00:00:00 2001 From: alexma233 Date: Wed, 15 Jul 2026 22:12:44 +0800 Subject: [PATCH] fix(web): announce final task logs and labels --- web/src/lib/i18n/messages/de.ts | 2 ++ web/src/lib/i18n/messages/en-us.ts | 2 ++ web/src/lib/i18n/messages/fr.ts | 2 ++ web/src/lib/i18n/messages/ja.ts | 2 ++ web/src/lib/i18n/messages/zh-hans.ts | 2 ++ web/src/lib/i18n/messages/zh-hant.ts | 2 ++ web/src/lib/log-accessibility.test.mjs | 13 +++++++ web/src/lib/log-accessibility.ts | 9 +++++ web/src/lib/presenters.test.mjs | 36 ++++++++++++++++++++ web/src/lib/presenters.ts | 4 +++ web/src/routes/(app)/tasks/[id]/+page.svelte | 3 +- 11 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 web/src/lib/log-accessibility.test.mjs create mode 100644 web/src/lib/log-accessibility.ts diff --git a/web/src/lib/i18n/messages/de.ts b/web/src/lib/i18n/messages/de.ts index 254bc28..c41b59b 100644 --- a/web/src/lib/i18n/messages/de.ts +++ b/web/src/lib/i18n/messages/de.ts @@ -427,7 +427,9 @@ export const de = { caddySync: "Caddy-Sync", cloudflareTunnelSync: "Cloudflare Tunnel-Sync", caddyReload: "Caddy-Reload", + imageCheck: "Image prüfen", prune: "Docker-Prune", + rusticInit: "Rustic Init", rusticForget: "Rustic Forget", rusticPrune: "Rustic Prune", dockerStart: "Container starten", diff --git a/web/src/lib/i18n/messages/en-us.ts b/web/src/lib/i18n/messages/en-us.ts index 6c1133a..65bcb46 100644 --- a/web/src/lib/i18n/messages/en-us.ts +++ b/web/src/lib/i18n/messages/en-us.ts @@ -427,7 +427,9 @@ export const enUS = { caddySync: "Caddy sync", cloudflareTunnelSync: "Cloudflare Tunnel sync", caddyReload: "Caddy reload", + imageCheck: "Image check", prune: "Docker prune", + rusticInit: "Rustic init", rusticForget: "Rustic forget", rusticPrune: "Rustic prune", dockerStart: "Start container", diff --git a/web/src/lib/i18n/messages/fr.ts b/web/src/lib/i18n/messages/fr.ts index b0ffc99..6be28a7 100644 --- a/web/src/lib/i18n/messages/fr.ts +++ b/web/src/lib/i18n/messages/fr.ts @@ -430,7 +430,9 @@ export const fr = { caddySync: "Synchronisation Caddy", cloudflareTunnelSync: "Synchronisation Cloudflare Tunnel", caddyReload: "Rechargement Caddy", + imageCheck: "Vérification d’image", prune: "Docker prune", + rusticInit: "Rustic init", rusticForget: "Rustic forget", rusticPrune: "Rustic prune", dockerStart: "Démarrer le conteneur", diff --git a/web/src/lib/i18n/messages/ja.ts b/web/src/lib/i18n/messages/ja.ts index e97fd48..d5c2c80 100644 --- a/web/src/lib/i18n/messages/ja.ts +++ b/web/src/lib/i18n/messages/ja.ts @@ -428,7 +428,9 @@ export const ja = { caddySync: "Caddy 同期", cloudflareTunnelSync: "Cloudflare Tunnel 同期", caddyReload: "Caddy リロード", + imageCheck: "イメージ確認", prune: "Docker prune", + rusticInit: "Rustic init", rusticForget: "Rustic forget", rusticPrune: "Rustic prune", dockerStart: "コンテナ起動", diff --git a/web/src/lib/i18n/messages/zh-hans.ts b/web/src/lib/i18n/messages/zh-hans.ts index 8d2ae77..e172e27 100644 --- a/web/src/lib/i18n/messages/zh-hans.ts +++ b/web/src/lib/i18n/messages/zh-hans.ts @@ -409,7 +409,9 @@ export const zhHans = { caddySync: "Caddy 同步", cloudflareTunnelSync: "Cloudflare Tunnel 同步", caddyReload: "Caddy 重载", + imageCheck: "镜像检查", prune: "Docker 清理", + rusticInit: "Rustic init", rusticForget: "Rustic forget", rusticPrune: "Rustic prune", dockerStart: "启动容器", diff --git a/web/src/lib/i18n/messages/zh-hant.ts b/web/src/lib/i18n/messages/zh-hant.ts index 1563230..9c055b3 100644 --- a/web/src/lib/i18n/messages/zh-hant.ts +++ b/web/src/lib/i18n/messages/zh-hant.ts @@ -409,7 +409,9 @@ export const zhHant = { caddySync: "Caddy 同步", cloudflareTunnelSync: "Cloudflare Tunnel 同步", caddyReload: "Caddy 重載", + imageCheck: "映像檢查", prune: "Docker 清理", + rusticInit: "Rustic init", rusticForget: "Rustic forget", rusticPrune: "Rustic prune", dockerStart: "啟動容器", diff --git a/web/src/lib/log-accessibility.test.mjs b/web/src/lib/log-accessibility.test.mjs new file mode 100644 index 0000000..0108034 --- /dev/null +++ b/web/src/lib/log-accessibility.test.mjs @@ -0,0 +1,13 @@ +import { assertEquals } from "jsr:@std/assert@1.0.19/equals"; + +import { logLiveMode } from "./log-accessibility.ts"; + +Deno.test( + "keeps buffered log announcements polite after stream completion", + () => { + assertEquals(logLiveMode("idle", false), "off"); + assertEquals(logLiveMode("streaming", false), "polite"); + assertEquals(logLiveMode("completed", true), "polite"); + assertEquals(logLiveMode("failed", true), "polite"); + }, +); diff --git a/web/src/lib/log-accessibility.ts b/web/src/lib/log-accessibility.ts new file mode 100644 index 0000000..2189d4b --- /dev/null +++ b/web/src/lib/log-accessibility.ts @@ -0,0 +1,9 @@ +export function logLiveMode( + state: string, + hasAnnouncements: boolean, +): "off" | "polite" { + if (state === "streaming" || hasAnnouncements) { + return "polite"; + } + return "off"; +} diff --git a/web/src/lib/presenters.test.mjs b/web/src/lib/presenters.test.mjs index b31881f..2b248c2 100644 --- a/web/src/lib/presenters.test.mjs +++ b/web/src/lib/presenters.test.mjs @@ -1,3 +1,4 @@ +import { assert } from "jsr:@std/assert@1.0.19/assert"; import { assertEquals } from "jsr:@std/assert@1.0.19/equals"; import { enUS } from "./i18n/messages/en-us.ts"; @@ -31,6 +32,41 @@ Deno.test("formats task enums through i18n labels", () => { taskTypeLabel("cloudflare_tunnel_sync", enUS), "Cloudflare Tunnel sync", ); + assertEquals(taskTypeLabel("image_check", enUS), "Image check"); + assertEquals(taskTypeLabel("rustic_init", enUS), "Rustic init"); assertEquals(taskStepNameLabel("compose_down", enUS), "Compose down"); assertEquals(taskStatusLabel("TASK_STATUS_FAILED", enUS), "Unknown"); }); + +Deno.test("maps concrete backend task types", () => { + const backendTaskTypes = [ + "deploy", + "stop", + "restart", + "update", + "backup", + "restore", + "migrate", + "dns_update", + "caddy_sync", + "caddy_reload", + "image_check", + "prune", + "rustic_init", + "rustic_forget", + "rustic_prune", + "docker_start", + "docker_stop", + "docker_restart", + "migrate_rollback", + "docker_remove_container", + "docker_remove_network", + "docker_remove_volume", + "docker_remove_image", + "cloudflare_tunnel_sync", + ]; + + for (const taskType of backendTaskTypes) { + assert(taskTypeLabel(taskType, enUS) !== enUS.status.unknown, taskType); + } +}); diff --git a/web/src/lib/presenters.ts b/web/src/lib/presenters.ts index c61b17b..f3611ff 100644 --- a/web/src/lib/presenters.ts +++ b/web/src/lib/presenters.ts @@ -74,8 +74,12 @@ export function taskTypeLabel(type: string, messages: Dictionary) { return messages.tasks.types.cloudflareTunnelSync; case "caddy_reload": return messages.tasks.types.caddyReload; + case "image_check": + return messages.tasks.types.imageCheck; case "prune": return messages.tasks.types.prune; + case "rustic_init": + return messages.tasks.types.rusticInit; case "rustic_forget": return messages.tasks.types.rusticForget; case "rustic_prune": diff --git a/web/src/routes/(app)/tasks/[id]/+page.svelte b/web/src/routes/(app)/tasks/[id]/+page.svelte index 292fed4..7494677 100644 --- a/web/src/routes/(app)/tasks/[id]/+page.svelte +++ b/web/src/routes/(app)/tasks/[id]/+page.svelte @@ -6,6 +6,7 @@ import type { PageData } from './$types'; import { getMessages } from '$lib/i18n'; + import { logLiveMode } from '$lib/log-accessibility'; const messages = getMessages(); import { actionErrorMessage } from '$lib/capabilities'; @@ -502,7 +503,7 @@ role="log" tabindex="0" aria-label={$messages.tasks.accessibleLogLabel} - aria-live={logState === 'streaming' ? 'polite' : 'off'} + aria-live={logLiveMode(logState, accessibleLogChunks.length > 0)} aria-relevant="additions text" aria-atomic="false" >{#if accessibleLogChunks.length}{#each accessibleLogChunks as chunk}{chunk}{/each}{:else}{$messages.tasks.waitingForOutput}{/if} -- 2.51.2