diff --git a/dashboard/app/src/app/App.tsx b/dashboard/app/src/app/App.tsx index dde5759..4349fa9 100644 --- a/dashboard/app/src/app/App.tsx +++ b/dashboard/app/src/app/App.tsx @@ -20,6 +20,7 @@ export function App() { const [route, setRoute] = useState(routeFromHash); const [commandOpen, setCommandOpen] = useState(false); const dashboard = useQuery({ queryKey: ['dashboard'], queryFn: ({ signal }) => api.dashboard(signal) }); + const releaseHealth = useQuery({ queryKey: ['release-health'], queryFn: ({ signal }) => api.releaseHealth(signal) }); useEffect(() => { const onHash = () => setRoute(routeFromHash()); @@ -39,7 +40,7 @@ export function App() { let content = ; if (dashboard.isError) content = void dashboard.refetch()} />; if (dashboard.data) { - content = route === 'overview' ? + content = route === 'overview' ? : route === 'forensics' ? : route === 'benchmarks' ? : route === 'replay' ? diff --git a/dashboard/app/src/features/overview/OverviewPage.tsx b/dashboard/app/src/features/overview/OverviewPage.tsx index 1c72a65..343d083 100644 --- a/dashboard/app/src/features/overview/OverviewPage.tsx +++ b/dashboard/app/src/features/overview/OverviewPage.tsx @@ -1,17 +1,86 @@ -import { Activity, AlertTriangle, Gauge, ShieldCheck } from 'lucide-react'; +import { Activity, AlertTriangle, FileWarning, Gauge, ShieldCheck } from 'lucide-react'; import { BarChart } from '../../components/charts/BarChart'; import { compactNumber, ms, percent, ratio } from '../../lib/format'; -import type { DashboardPayload, ServiceHealth } from '../../types/domain'; +import { RELEASE_HEALTH_SUMMARY_PATH } from '../../lib/releaseHealth'; +import type { ReleaseHealthState } from '../../lib/releaseHealth'; +import type { DashboardPayload, ReleaseHealthCheck, ReleaseHealthStatus, ServiceHealth } from '../../types/domain'; function MetricCard({ title, value, description, icon: Icon, tone = 'nominal' }: { title: string; value: string; description: string; icon: typeof Activity; tone?: string }) { return

{title}

{description}

{value}
; } + +function healthTone(status?: ReleaseHealthStatus) { + const normalized = String(status ?? 'unknown').toLowerCase(); + if (['green', 'pass', 'passed', 'present', 'nominal', 'ok'].includes(normalized)) return 'nominal'; + if (['yellow', 'warning', 'degraded', 'missing_optional'].includes(normalized)) return 'warning'; + if (['red', 'fail', 'failed', 'critical', 'missing', 'unavailable'].includes(normalized)) return 'critical'; + return 'unknown'; +} + +function statusLabel(status?: ReleaseHealthStatus) { + return String(status ?? 'unknown').replaceAll('_', ' '); +} + +function checkStatus(check?: ReleaseHealthCheck) { + if (!check) return 'unknown'; + if (check.status) return check.status; + return check.present ? 'present' : 'missing'; +} + +function artifactList(artifacts?: string[]) { + return artifacts && artifacts.length ? artifacts : ['None reported']; +} + +function ReleaseHealthSummaryCard({ state }: { state?: ReleaseHealthState }) { + const summary = state?.summary; + const missingRequired = summary?.missing_artifacts?.required_local ?? []; + const missingOptional = summary?.missing_artifacts?.optional_cross_repo ?? []; + const missingArtifacts = [...missingRequired, ...missingOptional]; + const contractStatus = checkStatus(summary?.checks?.contract_validation_report); + const apiStatus = checkStatus(summary?.checks?.api_export_validation_report); + const projectStatus = checkStatus(summary?.checks?.project_health_report); + const overall = summary?.overall_status ?? 'unknown'; + + return ( +
+
+
+

Release health summary

+

Static release-readiness artifact for sanitized dashboard review.

+
+ {statusLabel(overall)} +
+ {state?.unavailable ?
{state.message ?? `Health summary unavailable. Expected ${RELEASE_HEALTH_SUMMARY_PATH}.`}
: null} + {!summary ?
Health summary unavailable. Expected {RELEASE_HEALTH_SUMMARY_PATH}.
: null} +
+
Contract validation{statusLabel(contractStatus)}
+
API/export validation{statusLabel(apiStatus)}
+
Project health report{statusLabel(projectStatus)}
+
+
+
+

Missing artifacts

+
    {artifactList(missingArtifacts).map((artifact) =>
  • {artifact}
  • )}
+
+
+

Next actions

+
    {artifactList(summary?.next_recommended_actions).map((action) =>
  • {action}
  • )}
+
+
+

Safety notes

+
    {artifactList(summary?.safety_notes).map((note) =>
  • {note}
  • )}
+
+
+
+ ); +} + function ServiceList({ services }: { services: ServiceHealth[] }) { return
{services.map((service) =>
{service.name}

{service.owner} · {service.domain} · queue {service.queue_depth}

{service.status}
)}
; } -export function OverviewPage({ payload }: { payload: DashboardPayload }) { +export function OverviewPage({ payload, releaseHealth }: { payload: DashboardPayload; releaseHealth?: ReleaseHealthState }) { const compressionData = payload.benchmarks.map((row) => ({ label: row.name, value: row.reduction_percent, auxiliary: row.top_family_coverage })); return (
@@ -21,6 +90,7 @@ export function OverviewPage({ payload }: { payload: DashboardPayload }) { 900 ? 'warning' : 'nominal'} /> +

Compression quality lanes

Bars show token reduction; amber dots show top-family coverage.

live
percent(value)} />

Service dependency health

Operational ownership, SLO posture, and queue pressure.

{payload.audit_summary.degraded_services} degraded
diff --git a/dashboard/app/src/lib/api.ts b/dashboard/app/src/lib/api.ts index 44df7e6..7c8e540 100644 --- a/dashboard/app/src/lib/api.ts +++ b/dashboard/app/src/lib/api.ts @@ -1,4 +1,5 @@ import { fallbackPayload } from '../mocks/fallbackPayload'; +import { fetchReleaseHealthSummary } from './releaseHealth'; import type { DashboardPayload } from '../types/domain'; const API_BASE = import.meta.env.VITE_COMP_TEXT_API_BASE ?? ''; @@ -20,6 +21,7 @@ export async function fetchDashboard(signal?: AbortSignal): Promise fetchReleaseHealthSummary(API_BASE, signal), exportJsonUrl: `${API_BASE}/export.json`, exportCsvUrl: `${API_BASE}/export.csv`, }; diff --git a/dashboard/app/src/lib/releaseHealth.ts b/dashboard/app/src/lib/releaseHealth.ts new file mode 100644 index 0000000..e44264f --- /dev/null +++ b/dashboard/app/src/lib/releaseHealth.ts @@ -0,0 +1,78 @@ +import { releaseHealthSummaryFallback } from '../mocks/releaseHealthSummary'; +import type { ReleaseHealthCheck, ReleaseHealthSummary } from '../types/domain'; + +export const RELEASE_HEALTH_SUMMARY_PATH = 'docs/reports/dashboard-health-summary.json'; + +export interface ReleaseHealthState { + summary: ReleaseHealthSummary; + unavailable: boolean; + message?: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function stringArray(value: unknown): string[] { + return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []; +} + +function normalizeCheck(value: unknown, key: string): ReleaseHealthCheck { + if (!isRecord(value)) return { key, status: 'unknown', present: false }; + return { + description: typeof value.description === 'string' ? value.description : undefined, + key: typeof value.key === 'string' ? value.key : key, + path: typeof value.path === 'string' ? value.path : undefined, + present: typeof value.present === 'boolean' ? value.present : undefined, + required: typeof value.required === 'boolean' ? value.required : undefined, + size_bytes: typeof value.size_bytes === 'number' ? value.size_bytes : undefined, + status: typeof value.status === 'string' ? value.status : undefined, + }; +} + +export function normalizeReleaseHealthSummary(value: unknown): ReleaseHealthSummary | null { + if (!isRecord(value)) return null; + const checks = isRecord(value.checks) + ? Object.fromEntries(Object.entries(value.checks).map(([key, check]) => [key, normalizeCheck(check, key)])) + : undefined; + const missing = isRecord(value.missing_artifacts) + ? Object.fromEntries(Object.entries(value.missing_artifacts).map(([key, artifacts]) => [key, stringArray(artifacts)])) + : undefined; + + return { + overall_status: typeof value.overall_status === 'string' ? value.overall_status : 'unknown', + checks, + required_artifacts_present: stringArray(value.required_artifacts_present), + missing_artifacts: missing, + next_recommended_actions: stringArray(value.next_recommended_actions), + safety_notes: stringArray(value.safety_notes), + generated_at: typeof value.generated_at === 'string' ? value.generated_at : undefined, + synthetic: typeof value.synthetic === 'boolean' ? value.synthetic : undefined, + summary_type: typeof value.summary_type === 'string' ? value.summary_type : undefined, + }; +} + +function releaseHealthUrl(apiBase: string) { + return `${apiBase.replace(/\/$/, '')}/dashboard-health-summary.json`; +} + +async function fetchSummary(path: string, signal?: AbortSignal): Promise { + const response = await fetch(path, { signal, headers: { Accept: 'application/json' } }); + if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); + const summary = normalizeReleaseHealthSummary(await response.json()); + if (!summary) throw new Error('Invalid release health summary payload'); + return summary; +} + +export async function fetchReleaseHealthSummary(apiBase = '', signal?: AbortSignal): Promise { + try { + return { summary: await fetchSummary(releaseHealthUrl(apiBase), signal), unavailable: false }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown summary load error'; + return { + summary: releaseHealthSummaryFallback, + unavailable: true, + message: `Health summary unavailable. Expected ${RELEASE_HEALTH_SUMMARY_PATH}. ${message}`, + }; + } +} diff --git a/dashboard/app/src/mocks/releaseHealthSummary.ts b/dashboard/app/src/mocks/releaseHealthSummary.ts new file mode 100644 index 0000000..7ca18de --- /dev/null +++ b/dashboard/app/src/mocks/releaseHealthSummary.ts @@ -0,0 +1,45 @@ +import type { ReleaseHealthSummary } from '../types/domain'; + +export const releaseHealthSummaryFallback: ReleaseHealthSummary = { + overall_status: 'unknown', + checks: { + contract_validation_report: { + description: 'Contract schema validation report.', + key: 'contract_validation_report', + path: 'docs/reports/contract-validation-report.md', + present: false, + required: true, + status: 'unavailable', + }, + api_export_validation_report: { + description: 'Synthetic API/export contract validation report.', + key: 'api_export_validation_report', + path: 'docs/reports/api-export-validation-report.md', + present: false, + required: true, + status: 'unavailable', + }, + project_health_report: { + description: 'Generated project health and release status report.', + key: 'project_health_report', + path: 'docs/reports/project-health-report.md', + present: false, + required: true, + status: 'unavailable', + }, + }, + missing_artifacts: { + required_local: ['docs/reports/dashboard-health-summary.json'], + optional_cross_repo: [], + }, + next_recommended_actions: [ + 'Generate docs/reports/dashboard-health-summary.json before release review.', + 'Run the dashboard with a built static bundle or local backend route that can serve the summary artifact.', + ], + safety_notes: [ + 'Health summary unavailable; showing synthetic fallback metadata only.', + 'No real Daimler data, customer payloads, secrets, cookies, tokens, raw production logs, or proprietary documents are included.', + ], + synthetic: true, + summary_type: 'dashboard_release_health_fallback', +}; diff --git a/dashboard/app/src/styles/app.css b/dashboard/app/src/styles/app.css index 5e0e743..5eda027 100644 --- a/dashboard/app/src/styles/app.css +++ b/dashboard/app/src/styles/app.css @@ -55,3 +55,15 @@ .empty, .error, .loading { padding: var(--space-8); text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: var(--radius-lg); } .skeleton { display: block; height: 1rem; border-radius: 999px; background: linear-gradient(90deg, rgba(148,163,184,.12), rgba(148,163,184,.25), rgba(148,163,184,.12)); background-size: 240% 100%; animation: pulse 1.4s infinite; } @keyframes pulse { to { background-position: -240% 0; } } +.release-health-card { display: grid; gap: var(--space-4); } +.release-health-checks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); } +.release-health-checks > div { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-3); border: 1px solid rgba(148, 163, 184, 0.12); border-radius: var(--radius-md); background: rgba(7, 17, 31, 0.46); } +.release-health-checks span { color: var(--muted); font-size: 0.88rem; } +.release-health-columns { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-4); } +.release-health-columns h4 { margin: 0 0 var(--space-2); font-size: 0.82rem; color: var(--subtle); text-transform: uppercase; letter-spacing: 0.08em; } +.release-health-columns ul { margin: 0; padding-left: 1.1rem; color: var(--muted); } +.release-health-columns li + li { margin-top: var(--space-2); } +.notice { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--border); color: var(--muted); background: rgba(7, 17, 31, 0.46); } +.notice.warning { color: var(--warning); background: rgba(251, 191, 36, 0.08); } +.badge.unknown { color: var(--subtle); background: rgba(148, 163, 184, 0.1); } +@media (max-width: 980px) { .release-health-checks, .release-health-columns { grid-template-columns: 1fr; } } diff --git a/dashboard/app/src/types/domain.ts b/dashboard/app/src/types/domain.ts index 6f082ef..de696d0 100644 --- a/dashboard/app/src/types/domain.ts +++ b/dashboard/app/src/types/domain.ts @@ -94,6 +94,37 @@ export interface ServiceHealth { dependencies: string[]; } + +export type ReleaseHealthStatus = 'green' | 'yellow' | 'red' | 'unknown' | string; + +export interface ReleaseHealthCheck { + description?: string; + key?: string; + path?: string; + present?: boolean; + required?: boolean; + size_bytes?: number; + status?: string; +} + +export interface ReleaseHealthMissingArtifacts { + optional_cross_repo?: string[]; + required_local?: string[]; + [key: string]: string[] | undefined; +} + +export interface ReleaseHealthSummary { + overall_status?: ReleaseHealthStatus; + checks?: Record; + required_artifacts_present?: string[]; + missing_artifacts?: ReleaseHealthMissingArtifacts; + next_recommended_actions?: string[]; + safety_notes?: string[]; + generated_at?: string; + synthetic?: boolean; + summary_type?: string; +} + export interface DashboardPayload { audit_summary: AuditSummary; benchmarks: BenchmarkResult[]; diff --git a/dashboard/app/vite.config.ts b/dashboard/app/vite.config.ts index 37738a2..b322c2a 100644 --- a/dashboard/app/vite.config.ts +++ b/dashboard/app/vite.config.ts @@ -3,11 +3,13 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], + publicDir: '../../docs/reports', server: { proxy: { '/api': 'http://127.0.0.1:8765', '/export.json': 'http://127.0.0.1:8765', '/export.csv': 'http://127.0.0.1:8765', + '/dashboard-health-summary.json': 'http://127.0.0.1:8765', }, }, build: { diff --git a/dashboard/industrial_dashboard.py b/dashboard/industrial_dashboard.py index 8135746..141db78 100644 --- a/dashboard/industrial_dashboard.py +++ b/dashboard/industrial_dashboard.py @@ -29,6 +29,7 @@ from src.validation.token_telemetry import drift_fingerprint, tokenizer_version APP_DIST = Path(__file__).resolve().parent / "app" / "dist" +RELEASE_HEALTH_SUMMARY = ROOT / "docs" / "reports" / "dashboard-health-summary.json" def _now() -> datetime: @@ -249,6 +250,12 @@ def _serve_static(self, path: str) -> bool: def do_GET(self) -> None: parsed = urlparse(self.path) + if parsed.path == "/dashboard-health-summary.json": + if RELEASE_HEALTH_SUMMARY.exists(): + self._send(200, RELEASE_HEALTH_SUMMARY.read_bytes(), "application/json") + return + self._send(404, b'{"error":"dashboard health summary not found"}', "application/json") + return if parsed.path.startswith("/api/") or parsed.path in {"/export.json", "/export.csv", "/replay"}: data = dashboard_data() if parsed.path in {"/api/dashboard", "/export.json", "/replay"}: diff --git a/docs/API_SURFACE.md b/docs/API_SURFACE.md index f2da431..4dc3f8b 100644 --- a/docs/API_SURFACE.md +++ b/docs/API_SURFACE.md @@ -47,6 +47,7 @@ surfaces: | `GET /export.json` | JSON | Stable | Machine-readable export of the same dashboard evidence for CI or air-gapped review. | | `GET /export.csv` | CSV | Stable | Spreadsheet-friendly review artifact with benchmark and forensic rows. | | `GET /replay` | JSON | Stable | Replay-oriented evidence view using the dashboard payload shape. | +| `GET /dashboard-health-summary.json` | JSON | Stable | Static release health summary artifact loaded from `docs/reports/dashboard-health-summary.json` for dashboard release-readiness badges and fallback-safe UI rendering. | | `GET /` | HTML or React app | Stable | Human dashboard entry point; falls back to a stdlib HTML view when the React build is unavailable. | All stable routes should preserve no-store cache headers, avoid exposing raw @@ -69,6 +70,7 @@ summaries include: - Benchmark tables or charts for p50, p95, p99, RPS, error rate, and payload size. - Export links for JSON and CSV evidence handoff. +- Release health summary panel sourced from `docs/reports/dashboard-health-summary.json`, with fallback messaging when that artifact is unavailable. ## Export/report endpoints @@ -78,10 +80,11 @@ PR artifacts, and explicit about any schema changes. `docs/reports/dashboard-health-summary.json` is the dashboard-facing static release-readiness artifact generated by -`python scripts/generate_dashboard_health_summary.py`. Future dashboard/UI work -can load that JSON directly from committed reports or CI artifacts to render -status badges, missing-artifact callouts, and next-action lists without starting -a live server or using real Daimler data. The companion Markdown report, +`python scripts/generate_dashboard_health_summary.py`. The React dashboard copies +`docs/reports/` into the Vite static bundle and also accepts the stdlib backend +route at `/dashboard-health-summary.json`, allowing the UI to render status +badges, missing-artifact callouts, and next-action lists without an external +service or real Daimler data. The companion Markdown report, `docs/reports/dashboard-health-summary.md`, provides the human-readable review summary.