Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/specs/2026-08-01-html-usage-activity-date-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Align HTML usage activity dates with their cells

## Traceability

- Spec ID: 2026-08-01-html-usage-activity-date-alignment
- Status: Implemented
- Issue: None; focused report-layout maintenance from visual review of the
generated AI coding workflow evidence report.

## Intent

Make the self-contained HTML report project-usage activity chart preserve the
one-to-one relationship between each UTC date and its activity cell. Dates must
read from left to right, use the same grid columns as the cells they describe,
and keep any overflow inside the chart instead of expanding the page.

## Acceptance Scenarios

- HUA-AC-1: The HTML renderer emits exactly one activity cell per retained UTC
date in source order, with strictly increasing horizontal grid columns and
the existing heat level, tooltip, and accessible label derived from that
date's value.
- HUA-AC-2: The activity axis shares the chart's `--heat-days`, column width,
and gap. Ranges of four days or fewer label every date in its matching
column. Longer ranges label the first and last dates plus every seventh date,
excluding periodic ticks within the last three days to avoid a duplicate
end cluster.
- HUA-AC-3: At 1180 px, 800 px, and 320 px viewport widths, the activity panel
remains vertically centered, the page has no horizontal overflow, and long
date ranges scroll only inside the chart container.
- HUA-AC-4: Empty activity data, English and Chinese output, tooltip and
`aria-label` content, self-contained HTML behavior, artifact names, and the
`summary.usageActivity` schema, UTC ordering, values, scoring, findings,
Markdown, and Canvas behavior remain unchanged.
- HUA-AC-5: The existing findings in
`.codex/better-harness/2026-08-01-ai-coding-workflow-evidence` re-render to
the same canonical artifact set, and renderer plus HTML validation pass.
- HUA-AC-6: Focused renderer tests, the documentation graph and link checks,
the full package test suite, package verification, preview health and Canvas
runtime smoke checks, browser console/page-error inspection, and whitespace
validation are executed and recorded. Feature-related checks pass; unavailable
external runtime or platform privileges remain explicit exceptions.

## Non-goals

- Changing activity collection, date normalization, truncation, aggregation,
heat calculation, scoring, finding eligibility, or report conclusions.
- Adding report fields, JavaScript, external resources, or generated artifacts.
- Changing the Markdown or Canvas renderer, the activity schema, or the
generated report's evidence source.
- Hand-editing the generated `report.html` or changing unrelated root CLI,
test, spec, documentation-graph, or diagnostic-output worktree changes.

## Plan and Tasks

1. Add renderer-level fixtures for four-day, thirty-day, empty, and localized
activity output, with explicit date-to-column assertions. (HUA-AC-1,
HUA-AC-2, HUA-AC-4)
2. Replace the seven-row column flow with one shared horizontal cell-and-axis
grid, deterministic tick selection, and chart-local overflow. (HUA-AC-1,
HUA-AC-2, HUA-AC-3)
3. Document the portable HTML activity-axis contract without changing report
data ownership or runtime boundaries. (HUA-AC-2, HUA-AC-4)
4. Re-render the current report from its existing `findings.json`, then run
automated, visual, preview, packaging, and traceability checks before
changing this spec to Implemented. (HUA-AC-3, HUA-AC-5, HUA-AC-6)

## Test and Review Evidence

- The focused activity-chart regressions passed 3/3:
`node --test --test-name-pattern="HTML activity chart" test/harness-report-render-cli.test.mjs`.
They cover four-day column binding, thirty-day tick selection, empty data,
Chinese output, tooltip/`aria-label` parity, and self-contained HTML.
- Re-rendering the current report from its existing `findings.json` passed all
four canonical checks (`output-location`, `run-directory-artifacts`,
`findings-json`, and `html-report`). The pre-existing fix-result sidecar was
moved outside the run directory only for validation and restored unchanged.
SHA-256 checks confirmed `findings.json`, `report.md`, and the sidecar were
unchanged; only `report.html` changed.
- Playwright geometry checks at 1180 px, 800 px, and 320 px found four strictly
increasing cells, matching source-date/grid-column pairs, zero pixel
cell-to-label center deltas, and zero page-level horizontal overflow. A
thirty-day browser sequence at 320 px produced 256 px of chart-local
overflow with `overflow-x:auto`; the page still had zero horizontal overflow.
The final browser console check reported zero errors and zero warnings.
- Documentation routing regenerated to 35 files and 51 links, and the doc-link
suite passed 6/6:
`node scripts/doc-link-graph/cli.mjs skills/better-harness` and
`node --test test/doc-link-graph.test.mjs`.
- Package verification passed with 369 npm entries and 392 runtime-zip entries:
`npm run pack:verify`.
- The full suite completed with 1044 passes, 6 platform skips, and 4 failures:
`npm test`. Every failure was an existing Windows fixture that could not
create a symbolic link because the current process lacks symlink authority
(`EPERM`); no HTML or activity-chart assertion failed. The direct renderer
suite likewise passed 29/30 with only its symlink fixture blocked.
- `npm run preview` was executed but intentionally failed fast because this
machine has no Canvas SDK runtime and neither `CANVAS_SDK_MEDIA_DIR` nor
`CANVAS_SDK_ROOT` is configured. Consequently `/health` and
`/canvas-module.js` could not be smoke-tested in this environment; no fake
runtime was substituted for that missing external prerequisite.
29 changes: 25 additions & 4 deletions scripts/harness-analysis/renderers/html.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@ function heatLevel(value, max) {
return Math.max(1, Math.min(4, Math.ceil((value / max) * 4)));
}

function activityTickIndexes(dayCount) {
if (dayCount <= 4) return Array.from({ length: dayCount }, (_, index) => index);

const lastIndex = dayCount - 1;
const indexes = [0];
for (let index = 7; index < lastIndex; index += 7) {
if (lastIndex - index > 3) indexes.push(index);
}
indexes.push(lastIndex);
return indexes;
}

function renderActivity(summary, language) {
const activity = summary?.usageActivity;
const usage = summary?.usageEfficiency;
Expand All @@ -232,8 +244,15 @@ function renderActivity(summary, language) {
const label = activeMinutes.length > 0
? copy(language, `${date}: ${formatNumber(active, language)} active minutes`, `${date}:${formatNumber(active, language)} 活跃分钟`)
: copy(language, `${date}: ${formatNumber(sessions, language)} sessions`, `${date}:${formatNumber(sessions, language)} 个会话`);
return `<span class="heat-cell l${heatLevel(values[index], max)}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}"></span>`;
return `<span class="heat-cell l${heatLevel(values[index], max)}" data-date="${escapeHtml(date)}" style="grid-column:${index + 1}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}"></span>`;
}).join("");
const ticks = activityTickIndexes(dates.length).map((index) => {
const position = dates.length > 4
? index === 0 ? " first" : index === dates.length - 1 ? " last" : ""
: "";
return `<span class="heat-tick${position}" data-date="${escapeHtml(dates[index])}" style="grid-column:${index + 1}">${escapeHtml(dates[index])}</span>`;
}).join("");
const heatWidth = Math.max(13, (dates.length * 17) - 4);
const coverage = summary?.atAGlance?.coverage ?? {};
const selection = usage?.selection ?? coverage?.selection ?? {};
const analyzed = selection.analyzedSessionCount ?? selection.analyzedCount ?? 0;
Expand All @@ -243,8 +262,10 @@ function renderActivity(summary, language) {

return `<div class="activity-layout">
<div class="activity-panel">
<div class="heatmap" role="img" aria-label="${copy(language, "Session activity heatmap", "会话活跃度热力图")}">${cells || `<span class="empty">${renderVisibleText(copy(language, "No dated activity was retained.", "没有保留按日期聚合的活动数据。"), language)}</span>`}</div>
${dates.length > 0 ? `<div class="heat-legend"><span>${escapeHtml(dates[0])}</span><span>${escapeHtml(dates.at(-1))}</span></div>` : ""}
${dates.length > 0 ? `<div class="heat-scroll" style="--heat-days:${dates.length};--heat-min-width:${heatWidth}px">
<div class="heatmap" role="img" aria-label="${copy(language, "Session activity heatmap", "会话活跃度热力图")}">${cells}</div>
<div class="heat-axis" aria-hidden="true">${ticks}</div>
</div>` : `<div class="heatmap-empty" role="img" aria-label="${copy(language, "Session activity heatmap", "会话活跃度热力图")}"><span class="empty">${renderVisibleText(copy(language, "No dated activity was retained.", "没有保留按日期聚合的活动数据。"), language)}</span></div>`}
</div>
<div class="stacked-metrics">
${metric(copy(language, "Sessions reviewed", "已分析会话"), `${formatNumber(analyzed, language)} / ${formatNumber(eligible, language)}`, copy(language, "all-eligible usage census", "全量合格用量普查"), language)}
Expand Down Expand Up @@ -457,7 +478,7 @@ export function renderHtml(reportData, actionContext) {
.metric > span,.metric > small { display:block; color:var(--muted); }.metric > strong { display:block; margin:5px 0 2px; font-size:34px; line-height:1.1; }.metric > small { font-size:12px; }
.section { margin-top:64px; scroll-margin-top:24px; }.section-heading { display:flex; justify-content:space-between; gap:30px; align-items:end; margin-bottom:20px; }.section-heading h2 { margin:5px 0 0; font-size:30px; letter-spacing:-.025em; }.section-heading > p { max-width:420px; margin:0; color:var(--muted); text-align:right; font-size:14px; }
.dimension-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:12px; }.dimension-card { min-height:230px; padding:19px; border:1px solid var(--line); border-radius:19px; background:linear-gradient(180deg,var(--panel-2),var(--panel)); }.dimension-top { display:flex; min-height:52px; justify-content:space-between; gap:8px; align-items:flex-start; font-weight:700; }.score-line { display:flex; align-items:baseline; gap:4px; margin-top:14px; }.score-line strong { font-size:34px; }.score-line span { color:var(--muted); font-size:12px; }.track { height:8px; margin:8px 0 16px; overflow:hidden; border-radius:999px; background:#30353a; }.track i { display:block; height:100%; border-radius:inherit; background:linear-gradient(90deg,var(--blue-2),var(--blue)); }.dimension-card p { margin:0; color:var(--muted); font-size:13px; }
.activity-layout { display:grid; grid-template-columns:minmax(0,1.8fr) minmax(230px,.7fr); gap:14px; }.activity-panel,.subpanel,.finding,.custom-grid article,.evidence-grid,.evidence-note { border:1px solid var(--line); border-radius:19px; background:var(--panel); }.activity-panel { padding:22px; overflow:hidden; }.heatmap { display:grid; grid-template-rows:repeat(7,13px); grid-auto-flow:column; grid-auto-columns:13px; gap:4px; min-height:115px; overflow-x:auto; padding-bottom:8px; }.heat-cell { border-radius:3px; background:#2b3035; }.heat-cell.l1{background:#174d70}.heat-cell.l2{background:#206f9e}.heat-cell.l3{background:#319bd1}.heat-cell.l4{background:#70c9ff}.heat-legend { display:flex; justify-content:space-between; color:var(--muted); font-size:11px; }.stacked-metrics { display:grid; gap:14px; }.stacked-metrics .metric { min-height:0; }
.activity-layout { display:grid; grid-template-columns:minmax(0,1.8fr) minmax(230px,.7fr); gap:14px; }.activity-panel,.subpanel,.finding,.custom-grid article,.evidence-grid,.evidence-note { border:1px solid var(--line); border-radius:19px; background:var(--panel); }.activity-panel { display:flex; min-width:0; flex-direction:column; justify-content:center; padding:22px; overflow:hidden; }.heat-scroll { width:100%; min-width:0; overflow-x:auto; padding-bottom:8px; }.heatmap,.heat-axis { display:grid; grid-template-columns:repeat(var(--heat-days),minmax(13px,1fr)); column-gap:4px; min-width:var(--heat-min-width); }.heatmap { align-items:center; }.heat-cell { width:13px; height:13px; justify-self:center; border-radius:3px; background:#2b3035; }.heat-cell.l1{background:#174d70}.heat-cell.l2{background:#206f9e}.heat-cell.l3{background:#319bd1}.heat-cell.l4{background:#70c9ff}.heat-axis { margin-top:10px; color:var(--muted); font-size:10px; }.heat-tick { justify-self:center; white-space:nowrap; }.heat-tick.first { justify-self:start; }.heat-tick.last { justify-self:end; }.heatmap-empty { color:var(--muted); }.stacked-metrics { display:grid; gap:14px; }.stacked-metrics .metric { min-height:0; }
.two-column { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-top:14px; }.subpanel { padding:22px; }.subpanel h3 { font-size:15px; }.usage-list { display:grid; gap:12px; }.usage-row { display:grid; grid-template-columns:minmax(100px,.75fr) 1fr 52px; gap:12px; align-items:center; font-size:12px; }.usage-row > span { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.usage-row i { height:7px; overflow:hidden; border-radius:999px; background:#30353a; }.usage-row b { display:block; height:100%; background:linear-gradient(90deg,var(--blue-2),var(--blue)); }.usage-row strong { text-align:right; }
.finding-list { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }.finding-card { min-width:0; min-height:220px; display:flex; flex-direction:column; padding:18px; border:1px solid var(--line); border-radius:19px; background:linear-gradient(180deg,#1c2329,var(--panel)); }.finding-card-main { display:grid; gap:10px; }.finding-meta { display:flex; flex-wrap:wrap; gap:7px; }.finding-card h3 { min-height:2.8em; margin:0; display:-webkit-box; overflow:hidden; font-size:17px; line-height:1.4; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.finding-preview { margin:0; color:#c7ced6; display:-webkit-box; overflow:hidden; font-size:13px; line-height:1.55; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.finding-actions,.dialog-actions { display:flex; flex-wrap:wrap; justify-content:space-between; gap:10px; margin-top:auto; padding-top:16px; }.action-button { min-height:38px; padding:8px 13px; border:1px solid transparent; border-radius:10px; color:var(--text); background:#30363d; font:inherit; font-size:13px; font-weight:700; cursor:pointer; }.action-button:hover { filter:brightness(1.12); }.action-button:focus-visible { outline:2px solid var(--blue); outline-offset:2px; }.action-button.secondary { color:#cceaff; border-color:#286d9b; background:#164e75; }.action-button.ghost { color:#d4dae0; border-color:var(--line); background:transparent; }.finding-dialog,.manual-copy-dialog { width:min(880px,calc(100vw - 32px)); max-height:calc(100vh - 32px); overflow:auto; padding:24px; border:1px solid var(--line); border-radius:20px; color:var(--text); background:var(--panel); box-shadow:0 30px 90px rgba(0,0,0,.55); }.finding-dialog::backdrop,.manual-copy-dialog::backdrop { background:rgba(4,8,12,.72); }.dialog-heading { display:flex; justify-content:space-between; gap:18px; align-items:flex-start; }.dialog-heading h3 { margin:6px 0 0; font-size:24px; }.dialog-section { margin-top:18px; }.dialog-section p { margin:7px 0 0; color:#d1d7de; }.dialog-section ol,.dialog-section ul { margin:8px 0 0; padding-left:22px; color:#d1d7de; }.manual-copy-dialog textarea { width:100%; min-height:260px; margin-top:14px; padding:14px; resize:vertical; border:1px solid var(--line); border-radius:12px; color:var(--text); background:#11161b; font:13px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace; }.no-js .finding-actions,.no-js .manual-copy-dialog { display:none!important; }.no-js .finding-dialog:not([open]) { display:block; position:static; width:auto; max-height:none; margin-top:16px; box-shadow:none; }
.suggestion-block { margin-top:28px; padding-top:24px; border-top:1px solid var(--line); }.suggestion-heading { display:flex; justify-content:space-between; gap:20px; align-items:end; margin-bottom:14px; }.suggestion-heading h3 { margin:4px 0 0; font-size:20px; }.suggestion-heading p { margin:0; color:var(--muted); font-size:12px; }.suggestion-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; }.suggestion { padding:18px; border:1px solid var(--line); border-radius:17px; background:linear-gradient(180deg,#1b252d,var(--panel)); }.suggestion-top { display:flex; justify-content:space-between; gap:8px; }.suggestion h3 { margin:13px 0 7px; font-size:16px; }.suggestion > p { color:#c7ced6; font-size:13px; }.suggestion dl { display:grid; gap:9px; margin:14px 0 0; }.suggestion dl div { display:grid; gap:2px; }.suggestion dt { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }.suggestion dd { margin:0; color:#d7dde4; font-size:12px; }
Expand Down
2 changes: 1 addition & 1 deletion templates/reporting/html-visual.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Mirror the Markdown report and the parity rules in
conclusions.

Keep first-screen labels short. Put long paths, command output, screenshots, and
raw evidence anchors in an evidence table or appendix.
raw evidence anchors in an evidence table or appendix. Render retained UTC activity dates left-to-right; use shared cell/tick grid columns and gaps, and keep long-range scrolling inside the chart.

HTML cannot assume a host chat API, native prompt injection, or host-specific
deep links. For each shared finding/action row, render compact `Copy AI Fix`
Expand Down
Loading