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
9 changes: 9 additions & 0 deletions pages/dashboard/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- [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.
- [x] Slice: `DLS-PAGE-005` and `DLS-PAGE-014` presenter render for the `workflows` built-in page inventory, active state, rollout mode, run conclusions, downstream outcomes, available usage, findings, operational value counts, 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 @@ -39,6 +40,14 @@

## Run log

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

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-005` and `DLS-PAGE-014` presenter increment for the `workflows` built-in page, rendering workflow inventory with active state, rollout mode, run counts, run conclusion summaries, downstream outcome counts, available AIC totals, finding counts, operational value counts, and independent data-state summaries.
- Updated `src/presenter.js` so declarative built-in `workflows` definitions now render a concrete table from `workflows`, `runs`, `outcomes`, `usage`, `findings`, and `operational-values` logical sources, reusing the keyed-list DOM primitive for deterministic row reconciliation.
- Expanded `test/unit/presenter.test.js` with a jsdom presenter contract for the `workflows` slice and replaced the browser smoke coverage in `test/e2e/smoke.spec.js` with a Playwright browser test that verifies the rendered workflow rows, aggregated counts, provenance, and independent `availability`, `completeness`, and `freshness` text.
- 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 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.
Expand Down
172 changes: 172 additions & 0 deletions pages/dashboard/src/presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ function renderBuiltInPageBody(page, pageSources) {
return renderRunsPage(pageSources);
}

if (page.page === 'workflows') {
return renderWorkflowsPage(pageSources);
}

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

Expand Down Expand Up @@ -249,6 +253,101 @@ function renderRunRow(item) {
);
}

/**
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {HTMLElement}
*/
function renderWorkflowsPage(pageSources) {
const workflowsSource = pageSources.get('workflows');
const runsSource = pageSources.get('runs');
const outcomesSource = pageSources.get('outcomes');
const usageSource = pageSources.get('usage');
const findingsSource = pageSources.get('findings');
const operationalValuesSource = pageSources.get('operational-values');

const workflows = Array.isArray(workflowsSource?.rows) ? workflowsSource.rows : [];
const runs = Array.isArray(runsSource?.rows) ? runsSource.rows : [];
const outcomes = Array.isArray(outcomesSource?.rows) ? outcomesSource.rows : [];
const usage = Array.isArray(usageSource?.rows) ? usageSource.rows : [];
const findings = Array.isArray(findingsSource?.rows) ? findingsSource.rows : [];
const operationalValues = Array.isArray(operationalValuesSource?.rows) ? operationalValuesSource.rows : [];

const workflowItems = workflows.map((workflow, index) => ({
key: getWorkflowKey(workflow, index),
workflow,
runCount: countMatchingRows(runs, workflow, 'workflow'),
conclusionCounts: countByMatchingRows(runs, workflow, 'workflow', 'run-conclusion'),
outcomeCount: countMatchingRows(outcomes, workflow, 'workflow'),
aicTotal: sumMatchingNumericRows(usage, workflow, 'workflow', 'aic'),
findingCount: countMatchingRows(findings, workflow, 'workflow'),
operationalValueCount: countMatchingRows(operationalValues, workflow, 'workflow')
}));

return h(
'div',
{ className: 'workflows-page' },
h('h3', null, 'Workflow Inventory'),
h(
'table',
{ className: 'workflows-table' },
h(
'thead',
null,
h(
'tr',
null,
h('th', null, 'Workflow'),
h('th', null, 'Organization'),
h('th', null, 'Repository'),
h('th', null, 'Active State'),
h('th', null, 'Rollout Mode'),
h('th', null, 'Run Count'),
h('th', null, 'Run Conclusions'),
h('th', null, 'Outcome Count'),
h('th', null, 'Available AIC'),
h('th', null, 'Finding Count'),
h('th', null, 'Operational Value Count')
)
),
h(
'tbody',
null,
workflowItems.length > 0
? keyed(
workflowItems,
(item) => renderWorkflowRow(/** @type {{ key: string, workflow: Record<string, unknown>, runCount: number, conclusionCounts: Map<string, number>, outcomeCount: number, aicTotal: number, findingCount: number, operationalValueCount: number }} */ (item)),
(item) => /** @type {{ key: string }} */ (item).key
)
: h('tr', null, h('td', { colSpan: 11 }, 'No workflows available.'))
)
)
);
}

/**
* @param {{ key: string, workflow: Record<string, unknown>, runCount: number, conclusionCounts: Map<string, number>, outcomeCount: number, aicTotal: number, findingCount: number, operationalValueCount: number }} item
* @returns {HTMLElement}
*/
function renderWorkflowRow(item) {
const workflow = item.workflow;

return h(
'tr',
{ 'data-workflow-id': String(workflow.workflow ?? item.key) },
h('td', null, toText(workflow.workflow)),
h('td', null, toText(workflow.organization)),
h('td', null, toText(workflow.repository)),
h('td', null, toText(workflow['workflow-active'])),
h('td', null, toText(workflow['rollout-mode'])),
h('td', null, String(item.runCount)),
h('td', null, formatCounts(item.conclusionCounts)),
h('td', null, String(item.outcomeCount)),
h('td', null, formatNumber(item.aicTotal)),
h('td', null, String(item.findingCount)),
h('td', null, String(item.operationalValueCount))
);
}

/**
* @param {Map<string, LogicalSourceInput>} pageSources
* @returns {DataState}
Expand Down Expand Up @@ -376,6 +475,43 @@ function countMatchingOutcomes(outcomes, run) {
return outcomes.filter((outcome) => outcome.run === run.run).length;
}

/**
* @param {Array<Record<string, unknown>>} rows
* @param {Record<string, unknown>} matchRow
* @param {string} field
* @returns {number}
*/
function countMatchingRows(rows, matchRow, field) {
return rows.filter((row) => row[field] === matchRow[field]).length;
}

/**
* @param {Array<Record<string, unknown>>} rows
* @param {Record<string, unknown>} matchRow
* @param {string} matchField
* @param {string} countField
* @returns {Map<string, number>}
*/
function countByMatchingRows(rows, matchRow, matchField, countField) {
return countBy(rows.filter((row) => row[matchField] === matchRow[matchField]), countField);
}

/**
* @param {Array<Record<string, unknown>>} rows
* @param {Record<string, unknown>} matchRow
* @param {string} matchField
* @param {string} numericField
* @returns {number}
*/
function sumMatchingNumericRows(rows, matchRow, matchField, numericField) {
return rows.reduce((total, row) => {
if (row[matchField] !== matchRow[matchField]) {
return total;
}
return total + toNumber(row[numericField]);
}, 0);
}

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

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

/**
* @param {Record<string, unknown>} row
* @returns {{ href: string, label: string } | null}
Expand All @@ -405,6 +550,33 @@ function toText(value) {
return value == null || value === '' ? 'unknown' : String(value);
}

/**
* @param {unknown} value
* @returns {number}
*/
function toNumber(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
}

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

/**
* @param {number} value
* @returns {string}
*/
function formatNumber(value) {
return Number.isInteger(value) ? String(value) : value.toFixed(2);
}

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