feat(board): v2 header, splash screen, logo route
This commit is contained in:
parent
4c46e361a9
commit
e5071f8750
60
src/server/board/chrome.ts
Normal file
60
src/server/board/chrome.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { escapeHtml } from '../ui-shared.js';
|
||||
|
||||
const LOGO = '<img src="/logo.svg" alt="" width="24" height="24" class="b2-logo-img">';
|
||||
|
||||
/**
|
||||
* 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 `
|
||||
<header class="b2-hdr">
|
||||
<span class="b2-brand">${LOGO}<b>agenthub</b></span>
|
||||
<span class="b2-proj" title="Projektwechsel kommt mit v3">/ ${name} <span class="b2-chev">▾</span></span>
|
||||
<nav class="b2-nav" aria-label="Primary">
|
||||
<b aria-current="page">Board</b><a href="/team">Team</a><a href="/activity">Activity</a><a href="/decisions">Decisions</a>
|
||||
</nav>
|
||||
<span class="b2-hdr-right">
|
||||
<button type="button" class="b2-btn" id="newTaskBtn" aria-haspopup="dialog" aria-expanded="false">+ New task</button>
|
||||
<span class="sse-status stale" id="sseStatus">
|
||||
<span class="b2-live-dot" aria-hidden="true"></span>
|
||||
<span id="sseLabel">connecting</span>
|
||||
</span>
|
||||
</span>
|
||||
</header>`;
|
||||
}
|
||||
|
||||
/** Fullscreen splash overlay, shown until first data render (hard cap 1.5s). */
|
||||
export function splashHtml(): string {
|
||||
return `
|
||||
<div id="b2-splash" class="b2-splash" aria-hidden="true">
|
||||
<div class="b2-splash-inner">
|
||||
<img src="/logo.svg" alt="agenthub" width="72" height="72" class="b2-splash-logo">
|
||||
<div class="b2-splash-word">agenthub</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
})();`;
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
35
tests/boardV2-chrome.test.ts
Normal file
35
tests/boardV2-chrome.test.ts
Normal file
@ -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('<b>x</b>')).not.toContain('<b>x</b>');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user