From 46247b773319d56d252eda163215be2c087110ee Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Thu, 23 Apr 2026 08:34:47 -0500 Subject: [PATCH] Add a banner to indicate updates in progress --- server/src/live-reload.ts | 19 +++++++++++ server/src/queue/edit-queue.ts | 10 ++++++ src/layouts/BaseLayout.astro | 58 +++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/server/src/live-reload.ts b/server/src/live-reload.ts index 755ddd4..e7c2e86 100644 --- a/server/src/live-reload.ts +++ b/server/src/live-reload.ts @@ -43,3 +43,22 @@ export function broadcastReload(reason: string, data?: Record): } } +export type UpdateStatusPhase = 'request_received' | 'update_started' | 'update_done'; + +export function broadcastUpdateStatus(phase: UpdateStatusPhase, data?: Record): void { + if (!wss) return; + + const payload = JSON.stringify({ + type: 'update_status', + ts: Date.now(), + phase, + ...data, + }); + + for (const client of wss.clients) { + if (client.readyState === client.OPEN) { + client.send(payload); + } + } +} + diff --git a/server/src/queue/edit-queue.ts b/server/src/queue/edit-queue.ts index 066a974..fad5413 100644 --- a/server/src/queue/edit-queue.ts +++ b/server/src/queue/edit-queue.ts @@ -1,5 +1,6 @@ import type { EditJobPayload } from '@dynamic-sites/shared'; import { logger } from '../logger.js'; +import { broadcastUpdateStatus } from '../live-reload.js'; export interface EditQueue { enqueue(payload: EditJobPayload): void; @@ -24,11 +25,19 @@ export function createEditQueue(): EditQueue { while (jobs.length > 0 && !shuttingDown) { const job = jobs.shift()!; logger.info({ event: 'job.started', kind: job.kind, id: job.id }, 'Processing job'); + broadcastUpdateStatus('update_started', { jobKind: job.kind, jobId: job.id }); try { await processor!(job); logger.info({ event: 'job.completed', kind: job.kind, id: job.id }, 'Job completed'); + broadcastUpdateStatus('update_done', { jobKind: job.kind, jobId: job.id, ok: true }); } catch (err) { logger.error({ event: 'job.failed', kind: job.kind, id: job.id, error: (err as Error).message }, 'Job failed'); + broadcastUpdateStatus('update_done', { + jobKind: job.kind, + jobId: job.id, + ok: false, + error: (err as Error).message, + }); } } @@ -51,6 +60,7 @@ export function createEditQueue(): EditQueue { } jobs.push(payload); logger.info({ event: 'job.enqueued', kind: payload.kind, id: payload.id, depth: jobs.length }, 'Job enqueued'); + broadcastUpdateStatus('request_received', { jobKind: payload.kind, jobId: payload.id, depth: jobs.length }); // Start draining on next tick if (processor) setImmediate(drain); }, diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 18a5915..ad14bc0 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -102,9 +102,45 @@ const wsUrl = (process.env.PUBLIC_LIVE_RELOAD_WS_URL ?? import.meta.env.PUBLIC_L font-size: 0.85rem; color: var(--color-text-muted); } + + .update-banner { + position: sticky; + top: 0; + z-index: 50; + width: 100%; + height: 32px; + display: none; + align-items: center; + gap: 0.5rem; + padding: 0 1rem; + background: color-mix(in srgb, var(--color-primary), white 92%); + border-bottom: 1px solid var(--color-border); + color: var(--color-text); + font-size: 0.85rem; + } + .update-banner[data-visible='true'] { + display: flex; + } + .update-banner__spinner { + width: 14px; + height: 14px; + border-radius: 999px; + border: 2px solid color-mix(in srgb, var(--color-primary), white 70%); + border-top-color: var(--color-primary); + animation: updateBannerSpin 0.9s linear infinite; + flex: 0 0 auto; + } + @keyframes updateBannerSpin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } +
+ + Updating… +