From e5071f8750e3900f644361c710c0226600fe1050 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 20 Jul 2026 17:26:25 +0200 Subject: [PATCH] feat(board): v2 header, splash screen, logo route --- src/server/board/chrome.ts | 60 ++++++++++++++++++++++++++++++++++++ src/server/routes.ts | 10 ++++++ tests/boardV2-chrome.test.ts | 35 +++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/server/board/chrome.ts create mode 100644 tests/boardV2-chrome.test.ts diff --git a/src/server/board/chrome.ts b/src/server/board/chrome.ts new file mode 100644 index 0000000..22b2430 --- /dev/null +++ b/src/server/board/chrome.ts @@ -0,0 +1,60 @@ +import { escapeHtml } from '../ui-shared.js'; + +const LOGO = ''; + +/** + * Fixed app header for board v2. The project cell is visual only — the real + * project switcher dropdown arrives with the multi-project v3 spec. + * The SSE status span keeps the v1 ids (`sseStatus`, `sseLabel`) so the ported + * connection-status JS keeps working unchanged. + */ +export function headerHtml(projectName: string): string { + const name = escapeHtml(projectName); + return ` +
+ ${LOGO}agenthub + / ${name}  + + + + + + connecting + + +
`; +} + +/** Fullscreen splash overlay, shown until first data render (hard cap 1.5s). */ +export function splashHtml(): string { + return ` +`; +} + +/** + * Inline script: hides the splash after the first successful refresh() or after + * a hard 1500ms cap, whichever comes first. Exposes window.__b2SplashDone() so + * the data layer can signal completion. + */ +export function splashJs(): string { + return ` +(function () { + var done = false; + window.__b2SplashDone = function () { + if (done) return; + done = true; + var s = document.getElementById('b2-splash'); + if (!s) return; + s.classList.add('b2-splash-hide'); + setTimeout(function () { if (s.parentNode) s.parentNode.removeChild(s); }, 450); + }; + setTimeout(window.__b2SplashDone, 1500); // hard cap — splash must never block +})();`; +} diff --git a/src/server/routes.ts b/src/server/routes.ts index 0efd463..efe0560 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -59,6 +59,16 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise const boardHtml = renderBoardHtml(loadConfig(cwd).projectName); app.get('/board', async (_request, reply) => reply.type('text/html; charset=utf-8').send(boardHtml)); + // AgentHub logo (header, splash, favicon). Read-only, cacheable. + app.get('/logo.svg', async (_request, reply) => { + try { + const buf = await readFile(join(CARD_ASSETS_DIR, 'logo.svg')); + return reply.type('image/svg+xml').header('Cache-Control', 'public, max-age=3600').send(buf); + } catch { + return reply.status(404).send('not found'); + } + }); + // Serve the KPI-card Lottie animations + the vendored lottie-web player from the // repo `assets/` dir. Read-only, path-traversal-guarded, cacheable. app.get('/card-assets/*', async (request, reply) => { diff --git a/tests/boardV2-chrome.test.ts b/tests/boardV2-chrome.test.ts new file mode 100644 index 0000000..7d7ad9e --- /dev/null +++ b/tests/boardV2-chrome.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; +import { headerHtml, splashHtml, splashJs } from '../src/server/board/chrome.js'; + +describe('headerHtml', () => { + it('renders logo, project cell with chevron, nav and new-task button', () => { + const h = headerHtml('my-project'); + expect(h).toContain('/logo.svg'); + expect(h).toContain('my-project'); + expect(h).toContain('b2-proj'); // project cell (visual only, dropdown comes with v3) + expect(h).toContain('▾'); + expect(h).toContain('+ New task'); + expect(h).toContain('id="newTaskBtn"'); + // SSE status mount compatible with the ported v1 connection JS + expect(h).toContain('id="sseStatus"'); + expect(h).toContain('id="sseLabel"'); + }); + it('escapes the project name', () => { + expect(headerHtml('x')).not.toContain('x'); + }); +}); + +describe('splash', () => { + it('renders overlay with logo and wordmark', () => { + const s = splashHtml(); + expect(s).toContain('id="b2-splash"'); + expect(s).toContain('/logo.svg'); + expect(s).toContain('agenthub'); + }); + it('fade-out script has a hard timeout and never blocks', () => { + const js = splashJs(); + expect(js).toContain('b2-splash'); + expect(js).toContain('1500'); // hard cap in ms + expect(js).toContain('__b2SplashDone'); + }); +});