From 2e05bdd73a664ddeec513653ab74e8c696ea1cfd Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Mon, 18 May 2026 19:43:53 +0200 Subject: [PATCH] feat: show job status in favicon on the run page (#9206) * feat: show job status in favicon on the run page * test: cover getJobStatusKind favicon status mapping * chore: remove favicon unit tests Co-authored-by: Diego Imbert Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Diego Imbert Co-authored-by: Claude Opus 4.7 --- frontend/src/lib/favicon.ts | 54 +++++++++++++++++++ .../(root)/(logged)/run/[...run]/+page.svelte | 12 ++++- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/favicon.ts diff --git a/frontend/src/lib/favicon.ts b/frontend/src/lib/favicon.ts new file mode 100644 index 0000000000..ebfd48b76d --- /dev/null +++ b/frontend/src/lib/favicon.ts @@ -0,0 +1,54 @@ +import type { Job } from '$lib/gen' + +export type JobStatusKind = 'running' | 'success' | 'failure' + +const STATUS_COLORS: Record = { + running: '#eab308', + success: '#22c55e', + failure: '#ef4444' +} + +const DEFAULT_FAVICON = '/logo.svg' + +// Inlined windmill logo polygons (fills inlined so the SVG is self-contained as a data URI). +const LOGO_POLYGONS = + '' + + '' + + '' + + '' + + '' + + '' + +function faviconLink(): HTMLLinkElement { + let link = document.querySelector('link[rel="icon"]') + if (!link) { + link = document.createElement('link') + link.rel = 'icon' + document.head.appendChild(link) + } + return link +} + +/** Maps a job to one of the three favicon status colors, following the same + * discrimination as JobStatusIcon (`'success' in job` => completed). */ +export function getJobStatusKind(job: Job | undefined): JobStatusKind | undefined { + if (!job) return undefined + if ('success' in job) return job.success ? 'success' : 'failure' + if (job.canceled) return 'failure' + return 'running' +} + +export function setStatusFavicon(status: JobStatusKind): void { + const color = STATUS_COLORS[status] + const svg = + '' + + `${LOGO_POLYGONS}` + + '' + + `` + + '' + faviconLink().href = `data:image/svg+xml,${encodeURIComponent(svg)}` +} + +export function resetFavicon(): void { + faviconLink().href = DEFAULT_FAVICON +} diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index e31d99f6b3..05b53ece63 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -77,7 +77,8 @@ import ExecutionDuration from '$lib/components/ExecutionDuration.svelte' import { isWindmillTooBigObject } from '$lib/components/job_args' import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte' - import { setContext, untrack } from 'svelte' + import { onDestroy, setContext, untrack } from 'svelte' + import { getJobStatusKind, resetFavicon, setStatusFavicon } from '$lib/favicon' import FlowAssetsHandler, { initFlowGraphAssetsCtx @@ -391,6 +392,15 @@ $effect(() => { job && untrack(() => onJobLoaded()) }) + $effect(() => { + const status = getJobStatusKind(job) + if (status) { + setStatusFavicon(status) + } else { + resetFavicon() + } + }) + onDestroy(resetFavicon)