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
11 changes: 11 additions & 0 deletions pages/dashboard/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@

## Run log

### 2026-08-29 (GitHub Primer brand styling & presentation component slice)

- Updated the dashboard presenter to clone the style of the current JavaScript dashboard implemented in CAO (`.github/scripts/pages-report/report.mjs`), generating dashboards that are GitHub brand-aligned using GitHub Primer CSS tokens and elements.
- Added GitHub Primer design tokens and stylesheet module (`src/styles.js`) supporting dark, light, contrast, and high-contrast color schemes.
- Added GitHub Octicons and CAO brand mark SVG helpers (`src/octicons.js`) with SVG namespace support in the DOM builder (`src/dom.js`).
- Created reusable presentation components for Primer status/mode badges (`src/components/badge.js`) and data-state metrics card grids (`src/components/data-state.js`).
- Updated `src/presenter.js` to render the Primer `.app-shell` layout with `.org-sidebar`, brand mark, `.primary-nav` with Octicons, breadcrumbs, `.overview-header`, `.table-region` data tables, and `.report-footer`.
- Configured Playwright runner to use the system Chromium binary and expanded unit and E2E test suites to verify Primer styling, brand elements, sidebar navigation, and data badges.
- Verified all quality gates pass: `npm run typecheck`, `npm run lint`, `npm test`, and `npm run test:e2e`.
- Next milestone: Built-in pages, next slice for rendering remaining Section 10 built-in pages (such as overview or tasks) or custom page views.

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

- Extended the Built-in pages milestone with a narrow `DLS-PAGE-013` and `DLS-PAGE-014` presenter increment for the `findings` built-in page, rendering finding summary, severity, status, scope, observed time, available issue/pull-request/run links, and independent data-state summaries.
Expand Down
3 changes: 3 additions & 0 deletions pages/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ 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 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.

The latest presenter slice updates the presentation layer to clone the style of the current JavaScript dashboard in CAO (`.github/scripts/pages-report/report.mjs`), rendering dashboards that are GitHub brand-aligned using GitHub Primer CSS tokens, Octicons, sidebar navigation, responsive layout, and status/mode badges.

7 changes: 6 additions & 1 deletion pages/dashboard/playwright.config.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { existsSync } from 'node:fs';
import { defineConfig } from '@playwright/test';

const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| (existsSync('/usr/bin/chromium') ? '/usr/bin/chromium' : undefined);

export default defineConfig({
testMatch: ['**/*.spec.js'],
testDir: './test/e2e',
timeout: 30000,
use: {
headless: true,
launchOptions: {
args: ['--no-sandbox']
args: ['--no-sandbox'],
...(executablePath ? { executablePath } : {})
}
}
});
49 changes: 49 additions & 0 deletions pages/dashboard/src/components/badge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Reusable GitHub Primer status and mode badges.
*/

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

/**
* @param {unknown} status
* @returns {HTMLElement}
*/
export function renderStatusBadge(status) {
const text = status == null || status === '' ? 'unknown' : String(status);
const normalized = text.toLowerCase();
let statusClass = 'status-muted';

if (['success', 'completed', 'active', 'true', 'fresh', 'available', 'complete', 'accepted'].includes(normalized)) {
statusClass = 'status-success';
} else if (['in-progress', 'running', 'pending', 'review', 'partial', 'stale', 'attention', 'warning'].includes(normalized)) {
statusClass = 'status-attention';
} else if (['failure', 'failed', 'rejected', 'danger', 'unavailable', 'critical'].includes(normalized)) {
statusClass = 'status-danger';
}

return h('span', { className: `status ${statusClass}` }, text);
}

/**
* @param {unknown} mode
* @returns {HTMLElement}
*/
export function renderModeBadge(mode) {
const text = mode == null || mode === '' ? 'unknown' : String(mode);
const normalized = text.toLowerCase();
const modeClass = normalized === 'live' ? 'mode-live' : normalized === 'review' ? 'mode-review' : '';

return h('span', { className: `mode-badge ${modeClass}`.trim() }, text);
}

/**
* @param {unknown} active
* @returns {HTMLElement}
*/
export function renderActiveStateBadge(active) {
const text = String(active);
const isActive = text === 'true' || text === 'active';
const statusClass = isActive ? 'status-success' : 'status-muted';

return h('span', { className: `status ${statusClass}` }, text);
}
55 changes: 55 additions & 0 deletions pages/dashboard/src/components/data-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* GitHub Primer data-state metrics card grid component.
*/

import { h } from '../dom.js';
import { renderStatusBadge } from './badge.js';

/**
* @typedef {import("../presenter.js").DataState} EffectiveDataState
*/

/**
* @param {EffectiveDataState | undefined} effectiveState
* @returns {HTMLElement}
*/
export function renderDataStateMetrics(effectiveState) {
const availability = effectiveState?.availability ?? 'available';
const completeness = effectiveState?.completeness ?? 'complete';
const freshness = effectiveState?.freshness ?? 'fresh';

return h(
'dl',
{ className: 'data-state-summary metrics' },
h(
'div',
{ className: 'metric-card' },
h('dt', { className: 'metric-label' }, 'Availability'),
h(
'dd',
{ className: 'metric-value', 'data-state-axis': 'availability' },
renderStatusBadge(availability),
),
),
h(
'div',
{ className: 'metric-card' },
h('dt', { className: 'metric-label' }, 'Completeness'),
h(
'dd',
{ className: 'metric-value', 'data-state-axis': 'completeness' },
renderStatusBadge(completeness),
),
),
h(
'div',
{ className: 'metric-card' },
h('dt', { className: 'metric-label' }, 'Freshness'),
h(
'dd',
{ className: 'metric-value', 'data-state-axis': 'freshness' },
renderStatusBadge(freshness),
),
),
);
}
20 changes: 19 additions & 1 deletion pages/dashboard/src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,32 @@ export function keyed(items, renderItem, key) {
return descriptor;
}

const SVG_TAGS = new Set([
'svg',
'path',
'symbol',
'use',
'g',
'defs',
'line',
'circle',
'rect',
'polyline',
'polygon',
'text',
'tspan'
]);

/**
* @param {string} name
* @param {Record<string, unknown> | null | undefined} [props]
* @param {...unknown} children
* @returns {HTMLElement}
*/
export function h(name, props, ...children) {
const element = document.createElement(name);
const element = SVG_TAGS.has(name)
? /** @type {HTMLElement} */ (/** @type {unknown} */ (document.createElementNS('http://www.w3.org/2000/svg', name)))
: document.createElement(name);
applyProps(element, props ?? {});
appendChildren(element, flattenChildren(children));
return element;
Expand Down
124 changes: 124 additions & 0 deletions pages/dashboard/src/octicons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* GitHub Octicon SVG icons and brand elements.
*/

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

/** @type {Record<string, { viewBox?: string, paths: Array<{ d: string, fill?: string, stroke?: string, strokeWidth?: string }> }>} */
const OCTICON_DATA = {
'mark-github': {
paths: [
{ d: 'M8 0C3.58 0 0 3.64 0 8.13c0 3.59 2.29 6.64 5.47 7.71.4.08.55-.18.55-.39 0-.19-.01-.82-.01-1.49-2.01.44-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.59 1.23.83.72 1.22 1.87.88 2.33.67.07-.53.28-.88.51-1.08-1.78-.21-3.64-.91-3.64-4.02 0-.89.31-1.62.82-2.19-.08-.2-.36-1.04.08-2.16 0 0 .67-.22 2.2.84A7.5 7.5 0 0 1 8 3.85a7.5 7.5 0 0 1 2 .27c1.53-1.06 2.2-.84 2.2-.84.44 1.12.16 1.96.08 2.16.51.57.82 1.3.82 2.19 0 3.12-1.87 3.81-3.65 4.02.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.47.55.39A8.01 8.01 0 0 0 16 8.13C16 3.64 12.42 0 8 0Z' }
]
},
server: {
paths: [
{ d: 'M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v4c0 .372-.116.717-.314 1 .198.283.314.628.314 1v4a1.75 1.75 0 0 1-1.75 1.75H1.75A1.75 1.75 0 0 1 0 12.75v-4c0-.358.109-.707.314-1a1.739 1.739 0 0 1-.314-1v-4C0 1.784.784 1 1.75 1ZM1.5 2.75v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Zm.25 5.75a.25.25 0 0 0-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-4a.25.25 0 0 0-.25-.25ZM7 4.75A.75.75 0 0 1 7.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 7 4.75ZM7.75 10h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM3 4.75A.75.75 0 0 1 3.75 4h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 4.75ZM3.75 10h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z' }
]
},
workflow: {
paths: [
{ d: 'M0 1.75C0 .784.784 0 1.75 0h3.5C6.216 0 7 .784 7 1.75v3.5A1.75 1.75 0 0 1 5.25 7H4v4a1 1 0 0 0 1 1h4v-1.25C9 9.784 9.784 9 10.75 9h3.5c.966 0 1.75.784 1.75 1.75v3.5A1.75 1.75 0 0 1 14.25 16h-3.5A1.75 1.75 0 0 1 9 14.25v-.75H5A2.5 2.5 0 0 1 2.5 11V7h-.75A1.75 1.75 0 0 1 0 5.25Zm1.75-.25a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-3.5a.25.25 0 0 0-.25-.25Zm9 9a.25.25 0 0 0-.25.25v3.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-3.5a.25.25 0 0 0-.25-.25Z' }
]
},
play: {
paths: [
{ d: 'M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm6.25-2.11a.75.75 0 0 1 1.14-.64l3 1.86a.75.75 0 0 1 0 1.28l-3 1.86a.75.75 0 0 1-1.14-.64Z' }
]
},
repo: {
paths: [
{ d: 'M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z' }
]
},
package: {
paths: [
{ d: 'm8.88.49 5.75 2.88c.23.11.37.34.37.59v8.08c0 .25-.14.48-.37.59l-5.75 2.88a1.97 1.97 0 0 1-1.76 0l-5.75-2.88A.66.66 0 0 1 1 12.04V3.96c0-.25.14-.48.37-.59L7.12.49a1.97 1.97 0 0 1 1.76 0ZM8 1.83 3.02 4.32 8 6.81l4.98-2.49L8 1.83Zm-5.5 3.7v6.11l4.75 2.38V7.91L2.5 5.53Zm6.25 8.49 4.75-2.38V5.53L8.75 7.91v6.11Z' }
]
},
issue: {
paths: [
{ d: 'M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm0 12.5a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11Zm-.75-9.25a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-1.5 0ZM8 9.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z' }
]
},
'pull-request': {
paths: [
{ d: 'M3.25 1.75a1.75 1.75 0 1 0 0 3.5 1.75 1.75 0 0 0 0-3.5ZM2.5 6.75v5.19a1.75 1.75 0 1 0 1.5 0V6.75a.75.75 0 0 0-1.5 0Zm10.25 4a1.75 1.75 0 1 0 0 3.5 1.75 1.75 0 0 0 0-3.5ZM8.5 2.5a.75.75 0 0 0 0 1.5h1.75A1.75 1.75 0 0 1 12 5.75v3a.75.75 0 0 0 1.5 0v-3a3.25 3.25 0 0 0-3.25-3.25Z' }
]
},
'check-circle': {
paths: [
{ d: 'M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm0 1.5a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm3.03 2.97a.75.75 0 0 1 0 1.06l-3.5 3.5a.75.75 0 0 1-1.06 0l-1.5-1.5a.75.75 0 0 1 1.06-1.06L7 8.44l2.97-2.97a.75.75 0 0 1 1.06 0Z' }
]
},
meter: {
paths: [
{ d: 'M8 1.5a6.5 6.5 0 1 0 6.016 4.035.75.75 0 0 1 1.388-.57 8 8 0 1 1-4.37-4.37.75.75 0 1 1-.569 1.389A6.473 6.473 0 0 0 8 1.5Zm6.28.22a.75.75 0 0 1 0 1.06l-4.063 4.064a2.5 2.5 0 1 1-1.06-1.06L13.22 1.72a.75.75 0 0 1 1.06 0ZM7 8a1 1 0 1 0 2 0 1 1 0 0 0-2 0Z' }
]
},
graph: {
paths: [
{ d: 'M1.5 1.75V13.5h13.75a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75V1.75a.75.75 0 0 1 1.5 0Zm14.28 2.53-5.25 5.25a.75.75 0 0 1-1.06 0L7 7.06 4.28 9.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.25-3.25a.75.75 0 0 1 1.06 0L10 7.94l4.72-4.72a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z' }
]
},
eye: {
paths: [
{ d: 'M8 2c3.7 0 6.5 3.2 7.5 5.3a1.6 1.6 0 0 1 0 1.4C14.5 10.8 11.7 14 8 14S1.5 10.8.5 8.7a1.6 1.6 0 0 1 0-1.4C1.5 5.2 4.3 2 8 2Zm0 1.5c-2.9 0-5.3 2.6-6.1 4.4a.2.2 0 0 0 0 .2c.8 1.8 3.2 4.4 6.1 4.4s5.3-2.6 6.1-4.4a.2.2 0 0 0 0-.2C13.3 6.1 10.9 3.5 8 3.5Zm0 1.75a2.75 2.75 0 1 1 0 5.5 2.75 2.75 0 0 1 0-5.5Zm0 1.5a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z' }
]
},
'external-link': {
paths: [
{ d: 'M3.75 2h3a.75.75 0 0 1 0 1.5h-3a.25.25 0 0 0-.25.25v8.5c0 .14.11.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3a.75.75 0 0 1 1.5 0v3A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.78 2.78 2 3.75 2Zm5.5-.75A.75.75 0 0 1 10 0h5.25c.41 0 .75.34.75.75V6a.75.75 0 0 1-1.5 0V2.56L8.78 8.28a.75.75 0 0 1-1.06-1.06l5.72-5.72H10a.75.75 0 0 1-.75-.75Z' }
]
}
};

/**
* @param {string} name
* @param {string} [className]
* @returns {SVGElement}
*/
export function octicon(name, className = '') {
const data = OCTICON_DATA[name];
const paths = data ? data.paths : [];

return /** @type {SVGElement} */ (/** @type {unknown} */ (h(
'svg',
{
className: `octicon octicon-${name}${className ? ` ${className}` : ''}`,
viewBox: data?.viewBox ?? '0 0 16 16',
'aria-hidden': 'true',
focusable: 'false'
},
...paths.map((p) => h('path', {
d: p.d,
fill: p.fill ?? 'currentColor',
...(p.stroke ? { stroke: p.stroke, 'stroke-width': p.strokeWidth ?? '1' } : {})
}))
)));
}

/**
* @returns {SVGElement}
*/
export function agenticWorkflowMark() {
return /** @type {SVGElement} */ (/** @type {unknown} */ (h(
'svg',
{
className: 'sidebar-brand-mark',
viewBox: '0 0 24 24',
'aria-hidden': 'true',
focusable: 'false'
},
h('path', {
d: 'M1 3a2 2 0 0 1 2-2h6.5a2 2 0 0 1 2 2v6.5a2 2 0 0 1-2 2H7v4.063C7 16.355 7.644 17 8.438 17H12.5v-2.5a2 2 0 0 1 2-2H21a2 2 0 0 1 2 2V21a2 2 0 0 1-2 2h-6.5a2 2 0 0 1-2-2v-2.5H8.437A2.939 2.939 0 0 1 5.5 15.562V11.5H3a2 2 0 0 1-2-2Zm2-.5a.5.5 0 0 0-.5.5v6.5a.5.5 0 0 0 .5.5h6.5a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5Zm11.5 11.5a.5.5 0 0 0-.5.5V21a.5.5 0 0 0 .5.5H21a.5.5 0 0 0 .5-.5v-6.5a.5.5 0 0 0-.5-.5Z',
fill: 'currentColor'
}),
h('path', {
d: 'm17.143 3.15c.083-.222.406-.222.49 0l.58 1.545c.18.48.565.855 1.049 1.023l1.584.566c.228.081.228.396 0 .477l-1.584.566a1.763 1.719 0 0 0-1.05 1.023l-.58 1.545c-.083.223-.406.223-.489 0l-.58-1.545a1.763 1.719 0 0 0-1.049-1.023l-1.584-.566c-.228-.081-.228-.396 0-.477l1.584-.566a1.763 1.719 0 0 0 1.05-1.023Z',
fill: '#c06eff',
stroke: '#c06eff',
'stroke-width': '.717'
})
)));
}
Loading
Loading