Files
board-viewer/.tasks/board-viewer-polish.md
vitya 469ff112cc feat(polish): close all 6 review nitpicks in one sweep
closes board-viewer-polish

- M1 src/parser.ts: comment explaining why **Status:** text field is intentionally
  ignored (emoji is canonical, prevents future-reader head-scratch)
- M2 src/render.ts + tests: clamp future ISO to '0m' (server clock drift safety)
  + inline comment + test
- M3 .tasks/board-viewer-gitea-reader.md: decisions-log clarifying orphan
  per-task files are out of scope by design (STATUS.md = index of record)
- M4 static/board.{js,css} + src/render.ts: filter rebuilt to query records
  (not DOM badge text); 5 status-filter pill buttons in header with active
  toggle state; smoke-verified blocked filter
- M5 deploy/Dockerfile.build: HEALTHCHECK (file fresher than 2×tick) and
  stderr redirect for failures with timestamp
- M6 src/reader.ts: parallelize getLatestCommitIso per-repo via Promise.all
  (preserves block order)

49 tests pass (was 43 pre-polish), typecheck clean, smoke against real Gitea
returns 81 records from 4 repos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:46:47 +03:00

61 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# board-viewer-polish
## Goal
Bundle of minor / nitpick findings from `board-viewer-review` checkpoint. Each — small, individually below the task-creation threshold; collectively worth doing in one sweep.
## Items
### M1. Document parser's `**Status:**` field neglect
- **Location:** `src/parser.ts` (in `parseOneBlock`)
- **Problem:** `**Status:**` text field is matched by `FIELD_RE` but never stored in `StatusBlock` (emoji is SoT). Future maintainer reads parser and wonders why.
- **Fix:** comment `// **Status:** text field intentionally ignored — emoji is canonical` near where it's matched, OR `delete fields['Status']` after parse to avoid the orphan field in local var. Choose the comment.
### M2. `formatAge` future-timestamp behavior is undocumented
- **Location:** `src/render.ts:31` + `static/board.js:49`
- **Problem:** `Math.max(0, …)` silently clamps future ISO to "0m". With server-clock drift на VDS vs Gitea это маскирует ошибку синхронизации.
- **Fix:** add one test asserting `formatAge(futureIso, now) === '0m'` to lock behavior, and add inline comment "// clamps to '0m' on future iso — see test".
### M3. `gitea-reader` acceptance "per-task без записи в STATUS.md (legacy)" is impossible by design
- **Location:** `.tasks/board-viewer-gitea-reader.md` Decisions log
- **Problem:** Acceptance line 42 promises tests for "per-task без записи в STATUS.md (legacy)" case, but the reader iterates STATUS.md blocks only — orphan per-task files are invisible by design (matches spec: STATUS.md is the index).
- **Fix:** append a decisions-log entry to `board-viewer-gitea-reader.md` clarifying this is intentional; STATUS.md is the entry point, orphan per-task files are out of scope.
### M4. Client filter doesn't match status in by-status mode; "multi-select по статусам" not implemented
- **Location:** `static/board.js:139-147` (`applyFilter`)
- **Problem:** Filter reads `.badge.status` / `.badge.project` text from DOM. In by-status mode there's no status badge → typing "blocked" matches nothing. Также acceptance в `board-viewer-html-render.md:27` обещает multi-select по статусам — не реализован.
- **Fix:** rebuild `applyFilter` to query the underlying `records` array (status always available regardless of view mode). Decide on multi-select: either ship a status-pill filter row, or amend acceptance criteria. Recommendation: add multi-select pills below the filter input — simple `<button data-status="…" class="status-filter">` toggle.
### M5. Build container failures show only `echo` output, no Docker healthcheck
- **Location:** `deploy/Dockerfile.build:14`
- **Problem:** `... || echo 'render failed; will retry next tick'` swallows non-zero exit. Operator must `docker compose logs build` to notice. No Docker healthcheck signals unhealthy.
- **Fix:** emit failure to stderr (`>&2 echo`), and add a `HEALTHCHECK` to Dockerfile that checks `[ -f /output/index.html ]` AND `[ $(date +%s) - $(stat -c %Y /output/index.html) -lt 600 ]` (file exists AND fresher than 10 min). Compose `web` service can `depends_on` health.
### M6. `getLatestCommitIso` called serially, N=80+ per tick
- **Location:** `src/reader.ts:36-37`
- **Problem:** `for...of await` serializes 80+ HTTP calls. Within budget for now but a Gitea hiccup serializes through all of them.
- **Fix:** parallelize per-repo or globally with `Promise.all(blocks.map(...))`. Cap concurrency at ~10 if politeness is a concern.
## Decisions log
- 2026-05-22: bundle создан промоушеном из review-чекпойнта; severity варьируется minor → nitpick; bundled, чтобы не плодить 6 микро-тасок.
## Completed steps
- [x] M1: comment в `parser.ts` объясняющий что `**Status:**` text field intentionally ignored (emoji canonical)
- [x] M2: тест clamps future timestamps to "0m" + inline comment в formatAge
- [x] M3: decisions-log entry в gitea-reader.md: orphan per-task без блока в STATUS.md — out of scope by design
- [x] M4: filter rebuilt to query underlying records (status always known); 5 status-filter pill buttons добавлены в header; click toggles active set; CSS `.status-filter.active` filled. Smoke-verified: click "blocked" → только 1 blocked задача видна.
- [x] M5: HEALTHCHECK в Dockerfile.build (index.html exists AND fresher than 2×tick); failures redirected to stderr с timestamp
- [x] M6: `getLatestCommitIso` parallelized via Promise.all per-repo (preserves block order)
## Notes
Каждый item — TDD по тому, что есть testable. M1/M3 — comment/docs only ([skip-tdd: visual]). M2 — pure test. M4/M5/M6 — TDD logic.