From 930e404545b6c50074c7d6059a137374531de616 Mon Sep 17 00:00:00 2001 From: vitya Date: Fri, 22 May 2026 15:42:59 +0300 Subject: [PATCH] feat(render): card owner pill + short ISO date span closes board-viewer-card-meta-fix Review found two acceptance fields missing from cards: - project_owner was in JSON but never displayed - last_commit_iso was only consumed for relative age; no absolute date Now both rendered (server-side src/render.ts + client-side static/board.js in sync), with 4 new TDD tests: - renders owner pill when project_owner is set - omits owner pill when empty - renders short YYYY-MM-DD date when last_commit_iso is set - omits date span when null CSS: .badge.owner = transparent + border (visually distinct from solid .badge.project), .date = small muted tabular-nums. Smoke-verified in browser. Co-Authored-By: Claude Opus 4.7 (1M context) --- .tasks/STATUS.md | 8 +++---- .tasks/board-viewer-card-meta-fix.md | 6 ++++- src/render.ts | 12 ++++++++++ static/board.css | 12 ++++++++++ static/board.js | 12 ++++++++-- tests/render.test.ts | 36 ++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index aeabd3e..2d1d578 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -49,10 +49,10 @@ _Updated: 2026-05-22_ --- -## ⚪ [board-viewer-card-meta-fix] — add owner pill + last-commit-marker to cards -**Status:** ready -**Where I stopped:** review нашёл acceptance gap — `project_owner` есть в JSON но не выводится; `last_commit_iso` только в relative age, не абсолютной дате. -**Next action:** TDD render + симметрично client-side board.js. См. `board-viewer-card-meta-fix.md`. +## 🟢 [board-viewer-card-meta-fix] — add owner pill + last-commit-marker to cards +**Status:** done +**Where I stopped:** owner pill + short ISO date (`YYYY-MM-DD` + tooltip с full ISO) рендерятся в обеих ветках (`render.ts` + `board.js`). 4 новых TDD-теста; smoke-verified. +**Next action:** — **Branch:** master --- diff --git a/.tasks/board-viewer-card-meta-fix.md b/.tasks/board-viewer-card-meta-fix.md index dbeb4ac..5dceab2 100644 --- a/.tasks/board-viewer-card-meta-fix.md +++ b/.tasks/board-viewer-card-meta-fix.md @@ -36,7 +36,11 @@ html-render таска закрылась без этих полей — gap. Н ## Completed steps -- [ ] (фиксируется при выполнении) +- [x] RED+GREEN: 4 теста для owner pill + date span (renders/omits) +- [x] `src/render.ts` renderCard: owner pill + date span (short YYYY-MM-DD + tooltip с full ISO) +- [x] `static/board.js` renderCard синхронно +- [x] `static/board.css`: `.badge.owner` (transparent + border) + `.date` (small muted tabular-nums) +- [x] smoke-verified в браузере: top cards показывают `board-viewer` + `OpeItcLoc03` + `2026-05-22` + `25m` ## Notes diff --git a/src/render.ts b/src/render.ts index 6ed4ece..11b4798 100644 --- a/src/render.ts +++ b/src/render.ts @@ -117,15 +117,27 @@ function renderCard(r: TaskRecord, now: Date, archiveDays: number): string { const age = formatAge(r.last_commit_iso, now); const archived = r.status === 'done' && isOlderThanDays(r.last_commit_iso, now, archiveDays); const truncatedTitle = truncate(r.title, TITLE_MAX); + const ownerPill = r.project_owner + ? `${escapeHtml(r.project_owner)}` + : ''; + const dateSpan = r.last_commit_iso + ? `${escapeHtml(shortDate(r.last_commit_iso))}` + : ''; return `
  • ${escapeHtml(truncatedTitle)}
    ${escapeHtml(r.project)} + ${ownerPill} + ${dateSpan} ${age === null ? '' : `${escapeHtml(age)}`}
  • `; } +function shortDate(iso: string): string { + return iso.slice(0, 10); +} + function isOlderThanDays(iso: string | null, now: Date, days: number): boolean { if (iso === null) return false; const diffMs = now.getTime() - new Date(iso).getTime(); diff --git a/static/board.css b/static/board.css index 59e9f28..683665a 100644 --- a/static/board.css +++ b/static/board.css @@ -245,3 +245,15 @@ aside.drawer[hidden] { .badge.status { background: var(--badge-bg); } + +.badge.owner { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); +} + +.date { + font-variant-numeric: tabular-nums; + color: var(--muted); + font-size: 0.7rem; +} diff --git a/static/board.js b/static/board.js index 5d84e12..8300652 100644 --- a/static/board.js +++ b/static/board.js @@ -76,13 +76,21 @@ function truncate(s, max) { function renderCard(rec, now, mode) { const age = formatAge(rec.last_commit_iso, now); const archived = isArchived(rec, now); - const meta = mode === 'by-project' + const primary = mode === 'by-project' ? `${STATUS_EMOJI[rec.status]} ${STATUS_LABEL[rec.status]}` : `${escapeHtml(rec.project)}`; + const ownerPill = rec.project_owner + ? `${escapeHtml(rec.project_owner)}` + : ''; + const dateSpan = rec.last_commit_iso + ? `${escapeHtml(rec.last_commit_iso.slice(0, 10))}` + : ''; return `
  • ${escapeHtml(truncate(rec.title, 80))}
    - ${meta} + ${primary} + ${ownerPill} + ${dateSpan} ${age === null ? '' : `${escapeHtml(age)}`}
  • `; diff --git a/tests/render.test.ts b/tests/render.test.ts index 81311d5..12f5bb9 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -148,6 +148,42 @@ describe('renderBoard', () => { expect(html).toContain('"embed-test"'); }); + test('renders owner as a pill when project_owner is non-empty', () => { + const html = renderBoard( + [rec({ slug: 'x', project_owner: 'OpeItcLoc03' })], + { generatedAt: now }, + ); + + expect(html).toMatch(/OpeItcLoc03<\/span>/); + }); + + test('omits owner pill when project_owner is empty string', () => { + const html = renderBoard( + [rec({ slug: 'x', project_owner: '' })], + { generatedAt: now }, + ); + + expect(html).not.toContain('badge owner'); + }); + + test('renders last_commit_iso as a short YYYY-MM-DD date span', () => { + const html = renderBoard( + [rec({ slug: 'x', last_commit_iso: '2026-05-19T08:30:00Z' })], + { generatedAt: now }, + ); + + expect(html).toMatch(/]*>2026-05-19<\/span>/); + }); + + test('omits date span when last_commit_iso is null', () => { + const html = renderBoard( + [rec({ slug: 'x', last_commit_iso: null })], + { generatedAt: now }, + ); + + expect(html).not.toMatch(/ { const html = renderBoard([], { generatedAt: now });