diff --git a/README.md b/README.md index da8294d..5dbab38 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,7 @@ Operational and recovery behavior is documented in [docs/OPERATIONS.md](docs/OPERATIONS.md), while user and administrator tasks are covered by [docs/USER_GUIDE.md](docs/USER_GUIDE.md) and [docs/ADMIN_GUIDE.md](docs/ADMIN_GUIDE.md). + +The Reporting route, workspace, state, consequence, and accessibility mapping +is recorded in +[docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md). diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..9622ffd --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,29 @@ +# Reporting Interface Pattern Migration + +Reporting is a list-detail analytical workspace. It owns report semantics, +policy-aware result shaping, drill-through presentation, rendering, and +publication. Dataflow owns generic validation, typed expressions, schema +propagation, SQL compilation, and execution; Datasources and provider modules +own source access. + +| Surface | Task and archetype | Consequence and state contract | +| --- | --- | --- | +| `/reports` | Report catalogue and list-detail workspace | The catalogue keeps selected report context while parameters, results, history, and provenance change. `/reporting` is a compatibility route only. | +| Semantic report controls | Parameterized analytical query | Dimensions, measures, parameters, and pivot shape become a validated Dataflow-backed plan. Reporting does not execute unchecked presentation SQL. | +| Provider report controls | Governed cross-module report | Purpose, audience, permission, availability, privacy transforms, source revisions, and retention remain visible. A blocked Run action stays present with a keyboard-focusable reason. | +| Results, history, and export | Monitoring/reporting evidence | Loading, empty, failed, truncated, successful, historical, and provider-partial states remain distinguishable. Tables are the accessible fallback for visual results. | +| Save and schedule dialogs | Create/edit and asynchronous setup | Core dialogs retain focus and stable actions. Scheduling records a durable report definition/revision and does not imply immediate publication. | + +Run, schedule, export, and publication are consequential actions. Backend +permissions remain authoritative; the WebUI mirrors them and explains disabled +actions without exposing protected rows. Optional Dataflow, Policy, Files, +Mail, Templates, Search, and Notifications integrations are capability-based. +The full-height three-region workspace collapses at narrow widths while keeping +the catalogue, result, and inspector in task order. + +Verification: + +- `npm run test:interface-pattern` +- Reporting service, provider-report, migration, manifest, and module-permutation tests +- the Core TypeScript graph, structural localization audit, theme check, module + permutations, and full-product bundle budget diff --git a/src/govoplan_reporting/backend/manifest.py b/src/govoplan_reporting/backend/manifest.py index 368b343..2607d58 100644 --- a/src/govoplan_reporting/backend/manifest.py +++ b/src/govoplan_reporting/backend/manifest.py @@ -455,6 +455,11 @@ manifest = ModuleManifest( href="govoplan-reporting/docs/ADMIN_GUIDE.md", kind="repository", ), + DocumentationLink( + label="Reporting interface pattern audit", + href="govoplan-reporting/docs/INTERFACE_PATTERN_MIGRATION.md", + kind="repository", + ), ), ), ), diff --git a/webui/package.json b/webui/package.json index 2f9b4b3..7b65934 100644 --- a/webui/package.json +++ b/webui/package.json @@ -13,6 +13,9 @@ }, "./styles/reporting.css": "./src/styles/reporting.css" }, + "scripts": { + "test:interface-pattern": "node scripts/test-interface-pattern.mjs" + }, "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", diff --git a/webui/scripts/test-interface-pattern.mjs b/webui/scripts/test-interface-pattern.mjs new file mode 100644 index 0000000..a92f33f --- /dev/null +++ b/webui/scripts/test-interface-pattern.mjs @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; + +const page = fs.readFileSync("src/features/reporting/ReportingPage.tsx", "utf8"); +const provider = fs.readFileSync("src/features/reporting/ProviderReportWorkspace.tsx", "utf8"); +const styles = fs.readFileSync("src/styles/reporting.css", "utf8"); + +assert.ok(page.includes("DocumentationHelpLink"), "Reporting exposes configured-system help"); +assert.ok(page.includes("PageScrollViewport"), "Reporting owns bounded catalogue and inspector scrolling"); +assert.ok(page.includes("DataGrid"), "Tabular report results use the shared grid"); +assert.ok(page.includes(" ({ scope_type: "tenant", scope_id: tenant.id, @@ -101,6 +103,15 @@ export function ProviderReportWorkspace({ settings, auth, report }: { const missingRequired = report.parameters.some((item) => item.required && (parameters[item.key] === undefined || parameters[item.key] === "") ); + const runDisabledReason = !canRun + ? "Report run permission is required." + : !report.available + ? report.unavailable_reason ?? "Policy does not allow this report." + : missingRequired + ? "Complete the required report parameters." + : !purpose.trim() + ? "Record the purpose for this governed report run." + : undefined; return ( <>
@@ -113,7 +124,8 @@ export function ProviderReportWorkspace({ settings, auth, report }: { diff --git a/webui/src/features/reporting/ReportingPage.tsx b/webui/src/features/reporting/ReportingPage.tsx index 3ac4d93..4acb6aa 100644 --- a/webui/src/features/reporting/ReportingPage.tsx +++ b/webui/src/features/reporting/ReportingPage.tsx @@ -21,6 +21,7 @@ import { Button, DataGrid, Dialog, + DocumentationHelpLink, DismissibleAlert, IconButton, LoadingIndicator, @@ -202,6 +203,10 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext) /> {reports.length + providerReports.length} reports + } @@ -265,7 +270,11 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext) } variant="ghost" onClick={() => setScheduleDialogOpen(true)} /> } } variant="ghost" onClick={() => setSaveDialogOpen(true)} /> -