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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
? `<span class="badge owner">${escapeHtml(r.project_owner)}</span>`
|
||||
: '';
|
||||
const dateSpan = r.last_commit_iso
|
||||
? `<span class="date" title="${attr(r.last_commit_iso)}">${escapeHtml(shortDate(r.last_commit_iso))}</span>`
|
||||
: '';
|
||||
return ` <li class="card" data-slug="${attr(r.slug)}" data-raw-url="${attr(r.raw_url)}" data-archived="${archived ? 'true' : 'false'}">
|
||||
<div class="card-title">${escapeHtml(truncatedTitle)}</div>
|
||||
<div class="card-meta">
|
||||
<span class="badge project">${escapeHtml(r.project)}</span>
|
||||
${ownerPill}
|
||||
${dateSpan}
|
||||
${age === null ? '' : `<span class="age">${escapeHtml(age)}</span>`}
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
? `<span class="badge status">${STATUS_EMOJI[rec.status]} ${STATUS_LABEL[rec.status]}</span>`
|
||||
: `<span class="badge project">${escapeHtml(rec.project)}</span>`;
|
||||
const ownerPill = rec.project_owner
|
||||
? `<span class="badge owner">${escapeHtml(rec.project_owner)}</span>`
|
||||
: '';
|
||||
const dateSpan = rec.last_commit_iso
|
||||
? `<span class="date" title="${escapeHtml(rec.last_commit_iso)}">${escapeHtml(rec.last_commit_iso.slice(0, 10))}</span>`
|
||||
: '';
|
||||
return `<li class="card" data-slug="${escapeHtml(rec.slug)}" data-raw-url="${escapeHtml(rec.raw_url)}" data-archived="${archived ? 'true' : 'false'}">
|
||||
<div class="card-title">${escapeHtml(truncate(rec.title, 80))}</div>
|
||||
<div class="card-meta">
|
||||
${meta}
|
||||
${primary}
|
||||
${ownerPill}
|
||||
${dateSpan}
|
||||
${age === null ? '' : `<span class="age">${escapeHtml(age)}</span>`}
|
||||
</div>
|
||||
</li>`;
|
||||
|
||||
@@ -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(/<span class="badge owner">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(/<span class="date"[^>]*>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(/<span class="date"/);
|
||||
});
|
||||
|
||||
test('writes generation timestamp as data-generated attribute on the header', () => {
|
||||
const html = renderBoard([], { generatedAt: now });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user