+
+ ${kpiSkeletonHtml()}
+ ${columnsHtml(ACTIVE_COLUMNS)}
+
+ ${sidebarHtml()}
+
+${modalsHtml()}
+
+
+
+
+`;
+}
diff --git a/src/server/board/kpis.ts b/src/server/board/kpis.ts
index e65e7f0..6d3581f 100644
--- a/src/server/board/kpis.ts
+++ b/src/server/board/kpis.ts
@@ -46,6 +46,14 @@ ${backlogSeries.toString()}
${areaPath.toString()}
${laneChips.toString()}
window.__b2UpdateKpis = function (tasks, agentColor) {
+ var kpiPrev = window.__b2KpiPrev || (window.__b2KpiPrev = {});
+ function flashKpi(card, value) {
+ if (kpiPrev[card] !== undefined && kpiPrev[card] !== value) {
+ var cardEl = document.getElementById(card);
+ if (cardEl) { cardEl.classList.remove('metric-flash'); void cardEl.offsetWidth; cardEl.classList.add('metric-flash'); }
+ }
+ kpiPrev[card] = value;
+ }
function setK(card, key, fn) {
var el = document.querySelector('#' + card + ' [data-k="' + key + '"]');
if (el) fn(el);
@@ -53,6 +61,7 @@ window.__b2UpdateKpis = function (tasks, agentColor) {
// Open
var open = tasks.filter(function (t) { return t.status === 'open'; });
setK('kpiOpen', 'count', function (el) { el.textContent = String(open.length); });
+ flashKpi('kpiOpen', String(open.length));
setK('kpiOpen', 'total', function (el) {
el.textContent = '/ ' + tasks.filter(function (t) { return t.status !== 'cancelled'; }).length;
});
@@ -68,6 +77,7 @@ window.__b2UpdateKpis = function (tasks, agentColor) {
var chips = laneChips(tasks, status);
var capped = capChips(chips, 3);
setK(card, 'count', function (el) { el.textContent = String(chips.length); });
+ flashKpi(card, String(chips.length));
setK(card, 'chips', function (el) {
el.innerHTML = capped.visible.map(function (c) {
var color = agentColor ? agentColor(c.name) : '#a5b4fc';
@@ -81,6 +91,7 @@ window.__b2UpdateKpis = function (tasks, agentColor) {
// Done
var ds = doneStats(tasks);
setK('kpiDone', 'count', function (el) { el.textContent = String(ds.done); });
+ flashKpi('kpiDone', String(ds.done));
setK('kpiDone', 'total', function (el) { el.textContent = '/ ' + ds.total; });
setK('kpiDone', 'cap', function (el) {
el.textContent = ds.pct + '% · +' + ds.doneThisWeek + ' diese Woche';
diff --git a/src/server/routes.ts b/src/server/routes.ts
index efe0560..15a2433 100644
--- a/src/server/routes.ts
+++ b/src/server/routes.ts
@@ -13,7 +13,7 @@ import { computeBudget } from '../core/services/budgetService.js';
import { getRoster } from '../core/services/rosterService.js';
import { loadConfig, saveConfig } from '../core/config.js';
import { renderActivityHtml } from './activity.js';
-import { renderBoardHtml } from './board.js';
+import { renderBoardHtml } from './board/index.js';
import { renderTeamHtml } from './team.js';
import { renderArchiveHtml } from './archive.js';
import { renderDecisionsHtml } from './decisions.js';
diff --git a/tests/boardV2.test.ts b/tests/boardV2.test.ts
new file mode 100644
index 0000000..de419cb
--- /dev/null
+++ b/tests/boardV2.test.ts
@@ -0,0 +1,36 @@
+import { describe, it, expect } from 'vitest';
+import { renderBoardHtml } from '../src/server/board/index.js';
+
+describe('renderBoardHtml (v2)', () => {
+ const html = renderBoardHtml('demo-project');
+
+ it('is a full html document with favicon and splash', () => {
+ expect(html).toMatch(/^/i);
+ expect(html).toContain('rel="icon" href="/logo.svg"');
+ expect(html).toContain('id="b2-splash"');
+ });
+ it('contains header, kpis, columns and sidebar mount points', () => {
+ for (const s of ['b2-hdr', 'demo-project', 'id="kpiOpen"', 'id="b2Budget"', 'id="b2Feed"']) {
+ expect(html).toContain(s);
+ }
+ });
+ it('keeps the v1 interaction surface (dnd, sse, modals, budget reset)', () => {
+ for (const s of ['draggable', "new EventSource('/events')", 'task-log', 'budgetReset', 'taskModal']) {
+ expect(html).toContain(s);
+ }
+ });
+ it('renders in-progress cards with a progress ring and working glow', () => {
+ expect(html).toContain('b2-ring');
+ expect(html).toContain('b2-working');
+ });
+ it('does not ship the old v1 metric cards or lottie kpi icons', () => {
+ expect(html).not.toContain('metric-card');
+ expect(html).not.toContain('lottie');
+ });
+ it('runs the init sequence after all module definitions', () => {
+ const defIdx = html.indexOf('window.__b2UpdateKpis =');
+ const initIdx = html.indexOf('loadStatusMeta();');
+ expect(defIdx).toBeGreaterThan(-1);
+ expect(initIdx).toBeGreaterThan(defIdx);
+ });
+});
diff --git a/tests/server.test.ts b/tests/server.test.ts
index 9fcb5d7..426d904 100644
--- a/tests/server.test.ts
+++ b/tests/server.test.ts
@@ -143,9 +143,9 @@ describe('server routes', () => {
expect(res.headers['content-type']).toContain('text/html');
const html = res.payload;
- expect(html).toContain('