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 });