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
10 changes: 10 additions & 0 deletions pages/dashboard/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [x] Slice: `DLS-PAGE-014` conservative built-in data-state exposure validation via implementation-local declarative markers.
- [x] Slice: `DLS-PAGE-006` conservative run-link coverage validation for the `runs` built-in page.
- [x] Slice: `DLS-PAGE-002` conservative `overview` linked-findings and operational-value timeline coverage validation.
- [x] Slice: `DLS-PAGE-006` and `DLS-PAGE-014` presenter render for the `runs` built-in page status counts, outcome counts, scope/model/time columns, run links, and independent data-state summaries.
- [ ] **Security, privacy, accessibility** — Section 13 including escaping, redaction, and keyboard and screen-reader behavior verified with Playwright.
- [ ] **Compliance suite** — Section 14 test suite, the compliance checklist, Appendix A as a passing fixture, and Appendix C as failing fixtures.
- [ ] **Parity** — inventory the features of the existing dashboard in `.github/scripts/pages-report/report.mjs`, record them in `PLAN.md` as a parity checklist, then express each one as YAML configuration plus data fixtures, closing the checklist incrementally.
Expand All @@ -38,6 +39,15 @@

## Run log

### 2026-08-29 (built-in runs render slice)

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-006` and `DLS-PAGE-014` presenter increment for the `runs` built-in page, rendering run status counts, terminal conclusions, downstream outcome counts, scope, rollout mode, engine, requested model, resolved model, started time, run links, and independent data-state summaries.
- Updated `src/presenter.js` so declarative built-in `runs` definitions now render a concrete table and summary lists from `runs` and `outcomes` logical sources, reusing the keyed-list DOM primitive for deterministic row reconciliation.
- Replaced the previous browser smoke test with a Playwright browser test in `test/e2e/smoke.spec.js` that renders a built-in `runs` page and verifies counts, rows, links, provenance, and independent `availability`, `completeness`, and `freshness` text.
- Added `test/unit/presenter.test.js` to cover deterministic presenter output for the same `runs` slice in jsdom.
- Verified `npm install`, `npm run typecheck`, `npm run lint`, and `npm test`; `npm run test:e2e` remains blocked in this environment because the Playwright Chromium executable is not provisioned (`browserType.launch: Executable doesn't exist`).
- Next milestone: Built-in pages, next slice for rendering one additional Section 10 built-in page from the declarative definitions or extracting the first reusable presentation component needed by that rendering.

### 2026-08-29 (built-in overview provenance-and-freshness render slice)

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-002` and `DLS-PAGE-014` presenter increment for `overview`, rendering independent availability, completeness, and freshness summaries plus per-source provenance from runtime source metadata.
Expand Down
2 changes: 1 addition & 1 deletion pages/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ The current built-in-pages slice adds a conservative implementation-local built-

The latest built-in-pages increment also adds a conservative implementation-local `definition.data-state` marker for `DLS-PAGE-014`, requiring declarative independent coverage of `availability`, `completeness`, and `freshness` on built-in pages.

The current built-in-pages slice extends conservative Section 10 coverage for `overview`, requiring declarative built-in definitions to expose recent linked findings through relation-specific finding links, to expose operational-value timelines with definition-aware series coverage, and to render page-level provenance plus independent freshness/completeness/availability summaries in the browser prototype.
The current built-in-pages slice extends Section 10 rendering for `runs`, adding a visible browser prototype for status and conclusion counts, downstream outcome counts, scope/model/time columns, run links, and independent freshness/completeness/availability summaries derived from runtime source metadata.
207 changes: 203 additions & 4 deletions pages/dashboard/src/presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Tiny presenter prototype for built-in and custom dashboard pages.
*/

import { h } from './dom.js';
import { h, keyed } from './dom.js';

/**
* @typedef {{ availability: 'available'|'empty'|'unavailable', completeness: 'complete'|'partial'|'unknown', freshness: 'fresh'|'stale'|'unknown' }} DataState
Expand All @@ -17,7 +17,19 @@ import { h } from './dom.js';
*/

/**
* @typedef {{ document: import('./validator.js').DashboardDocument, sources: Record<string, LogicalSourceInput> }} PresentationInput
* @typedef {{ id: string, kind: 'built-in', page: string, title?: string, description?: string, definition?: { views?: Array<unknown>, ['data-state']?: Record<string, boolean> } }} PresentableBuiltInPage
*/

/**
* @typedef {{ id: string, kind: 'custom', title?: string, description?: string, views: unknown[] }} PresentableCustomPage
*/

/**
* @typedef {{ languageVersion: string, dashboard: { id: string, title: string, description?: string, defaults?: Record<string, unknown>, pages: Array<PresentableBuiltInPage | PresentableCustomPage> } }} PresentationDocument
*/

/**
* @typedef {{ document: PresentationDocument, sources: Record<string, LogicalSourceInput> }} PresentationInput
*/

/**
Expand All @@ -41,7 +53,7 @@ export function renderDashboard(input) {
}

/**
* @param {import('./validator.js').BuiltInPage | import('./validator.js').CustomPage} page
* @param {PresentableBuiltInPage | PresentableCustomPage} page
* @param {Record<string, LogicalSourceInput>} sources
* @returns {HTMLElement}
*/
Expand All @@ -63,7 +75,7 @@ function renderPage(page, sources) {
}

/**
* @param {import('./validator.js').BuiltInPage & { definition?: { views?: Array<unknown>, ['data-state']?: Record<string, boolean> } }} page
* @param {PresentableBuiltInPage} page
* @param {string} title
* @param {Record<string, LogicalSourceInput>} sources
* @returns {HTMLElement}
Expand Down Expand Up @@ -94,6 +106,8 @@ function renderBuiltInPage(page, title, sources) {
);
});

const builtInBody = renderBuiltInPageBody(page, pageSources);

return h(
'section',
{ className: 'dashboard-page', 'data-page-kind': 'built-in', 'data-page-name': page.page, 'data-page-id': page.id },
Expand All @@ -108,6 +122,7 @@ function renderBuiltInPage(page, title, sources) {
h('dt', null, 'Freshness'),
h('dd', { 'data-state-axis': 'freshness' }, effectiveState.freshness)
),
builtInBody,
h('h3', null, 'Provenance'),
h(
'ul',
Expand All @@ -119,6 +134,121 @@ function renderBuiltInPage(page, title, sources) {
);
}

/**
* @param {PresentableBuiltInPage} page
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {HTMLElement}
*/
function renderBuiltInPageBody(page, pageSources) {
if (page.page === 'runs') {
return renderRunsPage(pageSources);
}

return h('p', { className: 'page-placeholder' }, `Built-in page ${page.page} is not rendered in this increment.`);
}

/**
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {HTMLElement}
*/
function renderRunsPage(pageSources) {
const runsSource = pageSources.get('runs');
const outcomeSource = pageSources.get('outcomes');
const runs = Array.isArray(runsSource?.rows) ? runsSource.rows : [];
const outcomes = Array.isArray(outcomeSource?.rows) ? outcomeSource.rows : [];

const statusCounts = countBy(runs, 'run-status');
const conclusionCounts = countBy(runs, 'run-conclusion');
const outcomeCounts = countBy(outcomes, 'outcome-state');

const items = runs.map((run, index) => ({
key: getRunKey(run, index),
run,
outcomeCount: countMatchingOutcomes(outcomes, run)
}));

return h(
'div',
{ className: 'runs-page' },
h('h3', null, 'Run Status Counts'),
renderSummaryList('run-status-counts', statusCounts),
h('h3', null, 'Run Conclusion Counts'),
renderSummaryList('run-conclusion-counts', conclusionCounts),
h('h3', null, 'Outcome Counts'),
renderSummaryList('run-outcome-counts', outcomeCounts),
h('h3', null, 'Runs'),
h(
'table',
{ className: 'runs-table' },
h(
'thead',
null,
h(
'tr',
null,
h('th', null, 'Run'),
h('th', null, 'Status'),
h('th', null, 'Conclusion'),
h('th', null, 'Organization'),
h('th', null, 'Repository'),
h('th', null, 'Workflow'),
h('th', null, 'Rollout Mode'),
h('th', null, 'Engine'),
h('th', null, 'Requested Model'),
h('th', null, 'Resolved Model'),
h('th', null, 'Started At'),
h('th', null, 'Outcome Count'),
h('th', null, 'Run Link')
)
),
h(
'tbody',
null,
items.length > 0
? keyed(
items,
(item) => renderRunRow(/** @type {{ key: string, run: Record<string, unknown>, outcomeCount: number }} */ (item)),
(item) => /** @type {{ key: string }} */ (item).key
)
: h('tr', null, h('td', { colSpan: 13 }, 'No runs available.'))
)
)
);
}

/**
* @param {{ key: string, run: Record<string, unknown>, outcomeCount: number }} item
* @returns {HTMLElement}
*/
function renderRunRow(item) {
const run = item.run;
const runLink = findRunLink(run);

return h(
'tr',
{ 'data-run-id': String(run.run ?? item.key) },
h('td', null, toText(run.run)),
h('td', null, toText(run['run-status'])),
h('td', null, toText(run['run-conclusion'])),
h('td', null, toText(run.organization)),
h('td', null, toText(run.repository)),
h('td', null, toText(run.workflow)),
h('td', null, toText(run['rollout-mode'])),
h('td', null, toText(run.engine)),
h('td', null, toText(run['requested-model'])),
h('td', null, toText(run['resolved-model'])),
h('td', null, toText(run['started-at'])),
h('td', null, String(item.outcomeCount)),
h(
'td',
null,
runLink
? h('a', { href: runLink.href }, runLink.label)
: 'Unavailable'
)
);
}

/**
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {DataState}
Expand Down Expand Up @@ -206,6 +336,75 @@ function getViewSource(view) {
return view.data.source;
}

/**
* @param {Map<string, number>} counts
* @param {string} className
* @returns {HTMLElement}
*/
function renderSummaryList(className, counts) {
const entries = [...counts.entries()];
return h(
'ul',
{ className },
entries.length > 0
? entries.map(([name, count]) => h('li', null, `${name}: ${count}`))
: [h('li', null, 'No data available.')]
);
}

/**
* @param {Array<Record<string, unknown>>} rows
* @param {string} field
* @returns {Map<string, number>}
*/
function countBy(rows, field) {
/** @type {Map<string, number>} */
const counts = new Map();
for (const row of rows) {
const key = toText(row[field]);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return counts;
}

/**
* @param {Array<Record<string, unknown>>} outcomes
* @param {Record<string, unknown>} run
* @returns {number}
*/
function countMatchingOutcomes(outcomes, run) {
return outcomes.filter((outcome) => outcome.run === run.run).length;
}

/**
* @param {Record<string, unknown>} run
* @param {number} index
* @returns {string}
*/
function getRunKey(run, index) {
return typeof run.run === 'string' && run.run.length > 0 ? run.run : `run-${index}`;
}

/**
* @param {Record<string, unknown>} row
* @returns {{ href: string, label: string } | null}
*/
function findRunLink(row) {
const candidate = row['run-link'];
if (!isPlainObject(candidate) || typeof candidate.href !== 'string' || typeof candidate.label !== 'string') {
return null;
}
return { href: candidate.href, label: candidate.label };
}

/**
* @param {unknown} value
* @returns {string}
*/
function toText(value) {
return value == null || value === '' ? 'unknown' : String(value);
}

/**
* @param {string} value
* @returns {string}
Expand Down
Loading