From d6b655a5cf1a7d643dedf9936d26560670be1318 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 21 Jun 2026 16:11:37 +0200 Subject: [PATCH] docs: complete personal dashboard framework and implementation plan Finish IHUB-WP-0020 design work (status finished, all design tasks done) and add IHUB-WP-0021 with the 12-task implementation workplan plus research, PRs, and FDD deliverables produced during the 2026-06-16 review. --- docs/fdd/personal-dashboard-fdd.md | 609 +++++++++++++++++ docs/prs/personal-dashboard-prs.md | 340 ++++++++++ .../personal-dashboard-current-state.md | 348 ++++++++++ ...UB-WP-0020-personal-dashboard-framework.md | 498 ++++++++------ ...-0021-personal-dashboard-implementation.md | 623 ++++++++++++++++++ 5 files changed, 2227 insertions(+), 191 deletions(-) create mode 100644 docs/fdd/personal-dashboard-fdd.md create mode 100644 docs/prs/personal-dashboard-prs.md create mode 100644 docs/research/personal-dashboard-current-state.md create mode 100644 workplans/IHUB-WP-0021-personal-dashboard-implementation.md diff --git a/docs/fdd/personal-dashboard-fdd.md b/docs/fdd/personal-dashboard-fdd.md new file mode 100644 index 0000000..54f0c09 --- /dev/null +++ b/docs/fdd/personal-dashboard-fdd.md @@ -0,0 +1,609 @@ +# Personal Dashboard Framework FDD + +**Workplan:** IHUB-WP-0020 +**Date:** 2026-06-16 +**Status:** Functional design for follow-on implementation workplan +**Inputs:** `docs/research/personal-dashboard-current-state.md`, +`docs/prs/personal-dashboard-prs.md` + +## 1. Summary + +The personal dashboard is an authenticated, per-user landing surface composed of +server-rendered, governed panels. It reuses existing inter-hub data and links to +existing source dashboards. It does not replace hub dashboards, governance +dashboards, API dashboard, marketplace, or learning dashboard. + +First implementation should ship: + +- one default dashboard per user, with schema ready for multiple dashboards; +- six first-slice panel types; +- persisted panel layout/config; +- stable widget identity for each saved panel; +- `widgetEnvelope` wrapping for every rendered panel; +- simple server-rendered edit forms; +- post-login redirect to the personal dashboard. + +## 2. Design Decisions + +| Topic | Decision | +|---|---| +| Table prefix | Use `personal_dashboards`, `dashboard_panel_types`, and `dashboard_panels` | +| Panel type key field | Use `panel_key`, not `key`, to avoid ambiguous SQL/Haskell naming | +| Dashboard multiplicity | Schema supports multiple dashboards; first UI exposes only the default dashboard | +| Default dashboard | Created idempotently on first dashboard visit | +| Role defaults | No `users.role` column in first slice | +| Watched hubs | Represented in panel config for first slice, no separate watched-hub table | +| Panel widget ownership | Linked panel widgets are owned by the framework hub | +| Panel widget type | Use existing framework-level `panel` widget type | +| Panel removal | Soft-remove panel row with `removed_at`; archive linked widget | +| Rendering model | Controller/helper builds typed panel view models; views render pure HSX | +| Refresh model | Wrap the personal dashboard show action in `autoRefresh` initially | +| Client runtime | No JS framework and no client-side data fetch loop | + +## 3. Schema + +### 3.1 Migration Tables + +```sql +CREATE TABLE personal_dashboards ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + user_id UUID NOT NULL REFERENCES users(id), + name TEXT NOT NULL, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL +); + +CREATE UNIQUE INDEX personal_dashboards_one_default_idx + ON personal_dashboards (user_id) + WHERE is_default = TRUE; + +CREATE INDEX personal_dashboards_user_idx + ON personal_dashboards (user_id); + +CREATE TABLE dashboard_panel_types ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + panel_key TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + description TEXT, + default_config JSONB NOT NULL DEFAULT '{}', + default_col_span INTEGER NOT NULL DEFAULT 4, + default_row_span INTEGER NOT NULL DEFAULT 1, + live_update BOOLEAN NOT NULL DEFAULT FALSE, + status TEXT NOT NULL DEFAULT 'active', + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + CONSTRAINT dashboard_panel_types_span_check CHECK ( + default_col_span BETWEEN 1 AND 12 + AND default_row_span BETWEEN 1 AND 4 + ) +); + +CREATE INDEX dashboard_panel_types_status_idx + ON dashboard_panel_types (status); + +CREATE TABLE dashboard_panels ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + dashboard_id UUID NOT NULL REFERENCES personal_dashboards(id) ON DELETE CASCADE, + panel_type_id UUID NOT NULL REFERENCES dashboard_panel_types(id), + widget_id UUID NOT NULL REFERENCES widgets(id), + title TEXT, + config JSONB NOT NULL DEFAULT '{}', + col INTEGER NOT NULL DEFAULT 0, + row INTEGER NOT NULL DEFAULT 0, + col_span INTEGER NOT NULL DEFAULT 4, + row_span INTEGER NOT NULL DEFAULT 1, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + removed_at TIMESTAMP WITH TIME ZONE, + CONSTRAINT dashboard_panels_layout_check CHECK ( + col BETWEEN 0 AND 11 + AND row >= 0 + AND col_span BETWEEN 1 AND 12 + AND row_span BETWEEN 1 AND 4 + AND col + col_span <= 12 + ) +); + +CREATE INDEX dashboard_panels_dashboard_idx + ON dashboard_panels (dashboard_id, removed_at, row, col, sort_order); + +CREATE UNIQUE INDEX dashboard_panels_widget_idx + ON dashboard_panels (widget_id); +``` + +### 3.2 Seed Data + +The implementation migration or seed helper must ensure: + +- a framework hub exists with `hub_kind = 'framework'`; +- the framework-level `panel` widget type exists and is active; +- six `dashboard_panel_types` exist. + +Seed panel types: + +```sql +INSERT INTO dashboard_panel_types + (panel_key, label, description, default_config, default_col_span, + default_row_span, live_update) +VALUES + ('watched-hubs', 'Watched Hubs', + 'Hub list with latest health hints', + '{"limit":12,"displayMode":"compact"}', 6, 1, FALSE), + ('recent-interactions', 'Recent Activity', + 'Latest interaction events with widget and hub context', + '{"timeRange":"last24h","limit":25,"displayMode":"compact"}', 6, 1, TRUE), + ('triage-queue', 'Triage Queue', + 'Open requirement candidates waiting for triage', + '{"status":"open","limit":10,"sort":"oldest"}', 6, 1, TRUE), + ('recent-decisions', 'Recent Decisions', + 'Recent governance decisions across visible hubs', + '{"timeRange":"last30d","limit":10,"sort":"newest"}', 6, 1, FALSE), + ('hub-health', 'Hub Health', + 'Latest health snapshots and active bottleneck counts', + '{"limit":12,"displayMode":"compact"}', 6, 1, TRUE), + ('learning-digest', 'Learning Digest', + 'Recent learning insights and institutional knowledge highlights', + '{"insightLimit":5,"knowledgeLimit":5}', 6, 1, TRUE) +ON CONFLICT (panel_key) DO NOTHING; +``` + +The exact framework hub seed should use existing hub invariants and avoid +creating a second framework hub. Recommended slug: `inter-hub`. + +## 4. Haskell Types + +### 4.1 Controller Type + +Add to `Web.Types`: + +```haskell +data PersonalDashboardsController + = PersonalDashboardAction + | EditPersonalDashboardAction + | UpdatePersonalDashboardAction + | AddDashboardPanelAction + | UpdateDashboardPanelAction { dashboardPanelId :: !(Id DashboardPanel) } + | RemoveDashboardPanelAction { dashboardPanelId :: !(Id DashboardPanel) } + deriving (Eq, Show, Data) +``` + +Register in: + +- `Web/Routes.hs` with `instance AutoRoute PersonalDashboardsController` +- `Web/FrontController.hs` imports and controller list +- sidebar navigation as `Dashboard` + +### 4.2 Config ADTs + +Store panel config in JSONB. Decode into explicit Haskell types before querying: + +```haskell +data TimeRange + = Last24Hours + | Last7Days + | Last30Days + | AllTimeBounded + deriving (Eq, Show) + +data DisplayMode + = Compact + | Detailed + deriving (Eq, Show) + +data SortMode + = Newest + | Oldest + | HighestRisk + deriving (Eq, Show) + +data DashboardPanelConfig + = WatchedHubsConfig + { hubIds :: !(Maybe [Id Hub]) + , limit :: !Int + , displayMode :: !DisplayMode + } + | RecentInteractionsConfig + { hubIds :: !(Maybe [Id Hub]) + , timeRange :: !TimeRange + , limit :: !Int + , displayMode :: !DisplayMode + } + | TriageQueueConfig + { hubIds :: !(Maybe [Id Hub]) + , status :: !Text + , limit :: !Int + , sortMode :: !SortMode + } + | RecentDecisionsConfig + { hubIds :: !(Maybe [Id Hub]) + , timeRange :: !TimeRange + , limit :: !Int + , sortMode :: !SortMode + } + | HubHealthConfig + { hubIds :: !(Maybe [Id Hub]) + , limit :: !Int + , displayMode :: !DisplayMode + } + | LearningDigestConfig + { hubIds :: !(Maybe [Id Hub]) + , insightLimit :: !Int + , knowledgeLimit :: !Int + } +``` + +Implementation can place these in `Application/Helper/PersonalDashboard.hs`. +Config decoding should return warnings instead of crashing: + +```haskell +data PanelConfigResult + = PanelConfigOk DashboardPanelConfig + | PanelConfigWithWarnings DashboardPanelConfig [Text] +``` + +Clamp all limits server-side. Recommended default min/max: + +- list panel limit: 1 to 50; +- hub list limit: 1 to 50; +- learning insight/knowledge limits: 1 to 20; +- column span: 1 to 12; +- row span: 1 to 4. + +### 4.3 View Model ADT + +Do not query from HSX views. Build typed view models in the controller/helper: + +```haskell +data PersonalDashboardViewModel = PersonalDashboardViewModel + { dashboard :: !PersonalDashboard + , panels :: ![DashboardPanelViewModel] + , panelTypes :: ![DashboardPanelType] + } + +data DashboardPanelViewModel + = WatchedHubsPanel DashboardPanel Widget [WatchedHubRow] [Text] + | RecentInteractionsPanel DashboardPanel Widget [RecentInteractionRow] [Text] + | TriageQueuePanel DashboardPanel Widget [TriageQueueRow] [Text] + | RecentDecisionsPanel DashboardPanel Widget [RecentDecisionRow] [Text] + | HubHealthPanel DashboardPanel Widget [HubHealthRow] [Text] + | LearningDigestPanel DashboardPanel Widget [LearningDigestRow] [Text] + | UnsupportedPanel DashboardPanel Widget Text [Text] +``` + +Each row type should carry exactly the fields the view needs, plus source route +ids for link-outs. + +## 5. Controller Flow + +### 5.1 Show + +```text +GET /PersonalDashboard + -> ensureIsUser + -> ensureDefaultDashboard currentUser + -> fetch active dashboard panels ordered by row, col, sort_order + -> build DashboardPanelViewModel for each panel + -> render ShowView +``` + +The first implementation may wrap `PersonalDashboardAction` in `autoRefresh do`. + +### 5.2 Edit + +```text +GET /PersonalDashboard/Edit + -> ensureIsUser + -> ensureDefaultDashboard currentUser + -> fetch active panels and active panel types + -> render EditView +``` + +Edit view should show: + +- panel title; +- panel type label; +- row, col, col span, row span; +- limit/time range/hub filter where supported; +- remove button; +- add panel form. + +### 5.3 Update Layout/Config + +`UpdatePersonalDashboardAction` should accept a simple form payload for all +active panels. It should: + +- authorize that the dashboard belongs to current user; +- validate layout bounds; +- validate per-panel config; +- update dashboard/panel `updated_at`; +- redirect back to edit or show with success/error messages. + +### 5.4 Add Panel + +`AddDashboardPanelAction` should: + +1. fetch current user's default dashboard; +2. fetch selected active `DashboardPanelType`; +3. find/create the framework hub; +4. create linked `Widget`; +5. create initial `WidgetVersion`; +6. create `DashboardPanel` with default config and next layout slot; +7. redirect to edit. + +### 5.5 Remove Panel + +`RemoveDashboardPanelAction { dashboardPanelId }` should: + +1. verify the panel belongs to current user's dashboard; +2. set `dashboard_panels.removed_at`; +3. set linked widget `is_archived = TRUE` and `status = 'deprecated'`; +4. keep interaction events and annotations intact; +5. redirect to edit. + +## 6. Default Dashboard Seeding + +Recommended helper: + +```haskell +ensureDefaultDashboard :: (?modelContext :: ModelContext) => User -> IO PersonalDashboard +``` + +Behavior: + +1. Query default dashboard for user. +2. If found, return it. +3. If absent, create `personal_dashboards` row with name `My Dashboard`. +4. Fetch active first-slice `DashboardPanelType` rows. +5. Create one linked widget and one panel row for each seed panel. +6. Return the dashboard. + +Default layout: + +| Panel | col | row | col_span | row_span | +|---|---:|---:|---:|---:| +| watched-hubs | 0 | 0 | 6 | 1 | +| recent-interactions | 6 | 0 | 6 | 1 | +| triage-queue | 0 | 1 | 6 | 1 | +| recent-decisions | 6 | 1 | 6 | 1 | +| hub-health | 0 | 2 | 6 | 1 | +| learning-digest | 6 | 2 | 6 | 1 | + +If a user has active stewardship roles matched by email or name, panel config +may include those hub ids. If no match exists, config should stay neutral. + +## 7. Panel Renderer Query Shapes + +All panel queries must be bounded and must not expose secrets. + +### Watched Hubs + +Purpose: show hub names, domains/kinds, latest health score if available, and a +link to `ShowHubAction`. + +Query shape: + +- fetch hubs matching optional hub filter, order by name, limit N; +- fetch latest health snapshots for those hub ids using `DISTINCT ON (hub_id)` + or equivalent bounded query. + +### Recent Activity + +Purpose: show recent interaction events with widget and hub context. + +Query shape: + +- filter by optional hub ids through widget join; +- filter by time range; +- order by `interaction_events.occurred_at DESC`; +- limit N; +- fetch widget/hub names for display. + +### Triage Queue + +Purpose: show open requirement candidates that need attention. + +Query shape: + +- filter `requirement_candidates.status = 'open'`; +- optionally filter by source widget hub; +- order oldest first by default; +- limit N; +- fetch source widget and hub names. + +### Recent Decisions + +Purpose: show governance decisions that changed recently. + +Query shape: + +- filter by time range on `decided_at` or `created_at` fallback; +- optionally filter by hub through requirement candidate source widget lineage; +- order newest first; +- limit N. + +### Hub Health + +Purpose: show latest health score and active bottleneck count. + +Query shape: + +- latest `hub_health_snapshots` per hub; +- active `bottleneck_records` count per hub; +- limit N hubs by health score ascending or hub name depending config. + +When decoding aggregate counts as `Int`, cast `COUNT(*) AS integer` or decode +as `Int64`. + +### Learning Digest + +Purpose: show recent `learning_insights` and +`institutional_knowledge_entries`. + +Query shape: + +- optional hub filter; +- latest insights ordered by `computed_at DESC`, limit N; +- latest knowledge entries ordered by `created_at DESC`, limit N; +- link to source knowledge entries when available. + +## 8. Layout + +Desktop layout: + +- 12-column CSS grid. +- panel spans come from `dashboard_panels.col_span` and `row_span`. +- layout ordering comes from row, col, sort order. +- gap should match existing dashboard spacing. + +Mobile/narrow layout: + +- collapse to a single column. +- ignore column positions visually. +- preserve row/sort ordering. + +Implementation approach: + +- add a small CSS helper in `static/app.css` if inline styles cannot express the + responsive collapse cleanly; +- keep panel headings at compact dashboard scale; +- avoid nested cards; +- keep source link and annotate control visible but quiet. + +## 9. Routing and Navigation + +Add: + +- `PersonalDashboardAction` +- `EditPersonalDashboardAction` +- `UpdatePersonalDashboardAction` +- `AddDashboardPanelAction` +- `UpdateDashboardPanelAction` +- `RemoveDashboardPanelAction` + +Expected user-facing routes with AutoRoute naming are acceptable. If a cleaner +path is desired, add explicit `HasPath`/`CanRoute` later. First implementation +can use AutoRoute for speed and consistency. + +Update login: + +```haskell +login user +redirectTo PersonalDashboardAction +``` + +Do not alter: + +- public `LandingAction`; +- docs/tutorial/extension guide routes; +- existing `HubsAction` route. + +## 10. Governance Lifecycle + +### Panel Add + +- create `DashboardPanel`; +- create linked `Widget`; +- create initial `WidgetVersion` snapshot with panel type and config; +- render through `widgetEnvelope`. + +### Panel Update + +- update `DashboardPanel.config` and layout fields; +- update panel widget name/view context only if needed; +- create a new `WidgetVersion` snapshot when config changes materially. + +### Panel Remove + +- set `dashboard_panels.removed_at`; +- set widget `is_archived = TRUE`; +- set widget `status = 'deprecated'`; +- preserve events and annotations. + +### Annotation + +The existing `WidgetAnnotationsAction` should work because panels have stable +widget ids. + +### Event Capture + +Existing client-side capture can identify panels via `data-widget-id`. If panel +forms submit through normal controller actions, use existing event types where +possible (`viewed`, `clicked`, `submitted`, `commented`). + +## 11. Error Handling + +- Missing dashboard: create default. +- Missing panel type: render `UnsupportedPanel` with warning. +- Invalid config: use defaults and render warning. +- Missing linked widget: repair by creating a replacement widget if possible, + otherwise render unsupported warning. +- Missing framework hub: create the framework hub if absent, honoring unique + framework hub constraint. +- Empty panel data: render a quiet empty state. + +## 12. Tests and Smoke Checks + +Focused automated checks: + +- `ensureDefaultDashboard` is idempotent. +- seeded dashboard contains six active panels. +- each seeded panel has a linked widget. +- config decoder clamps limits and rejects unknown values safely. +- unauthorized user cannot edit another user's dashboard. +- remove action soft-removes panel and archives widget. + +Manual smoke: + +1. Log in as the seeded admin user. +2. Confirm redirect lands on personal dashboard. +3. Confirm all six seeded panels render. +4. Click source links from at least three panels. +5. Open Annotate for one panel and confirm existing annotation flow loads. +6. Edit layout, save, sign out/in, and confirm layout persists. +7. Add a panel and remove a panel. +8. Confirm `HubsAction`, hub show, Ops Review, Learning, API Dashboard, and + Marketplace still load. + +## 13. Implementation File Map + +Expected files for WP-0021: + +- `Application/Migration/-personal-dashboard-framework.sql` +- `Application/Helper/PersonalDashboard.hs` +- `Web/Controller/PersonalDashboards.hs` +- `Web/View/PersonalDashboards/Show.hs` +- `Web/View/PersonalDashboards/Edit.hs` +- `Web/Types.hs` +- `Web/Routes.hs` +- `Web/FrontController.hs` +- `static/app.css` only if needed for responsive grid helpers +- focused tests under `Test/` if the current test harness supports controller or + helper tests + +## 14. Open Questions + +These do not block WP-0021, but should be revisited after the first +implementation: + +1. Should personal dashboards later support team/shared dashboards? +2. Should watched hubs become a first-class table after users start editing + dashboards? +3. Should per-panel refresh be extracted into fragment routes? +4. Should dashboard panel widgets eventually be owned by source hubs instead of + the framework hub? +5. Should dashboard templates become part of the marketplace? + +## 15. Handoff to WP-0021 + +WP-0021 should implement this design in small slices: + +1. schema and seeds; +2. controller/route skeleton and default seeding; +3. first three panel view models/renderers; +4. dashboard show view; +5. remaining panel view models/renderers; +6. edit flow; +7. governance lifecycle; +8. login redirect and navigation; +9. tests and smoke. diff --git a/docs/prs/personal-dashboard-prs.md b/docs/prs/personal-dashboard-prs.md new file mode 100644 index 0000000..7071ce9 --- /dev/null +++ b/docs/prs/personal-dashboard-prs.md @@ -0,0 +1,340 @@ +# Personal Dashboard Framework PRS + +**Workplan:** IHUB-WP-0020 +**Date:** 2026-06-16 +**Status:** Product requirements for follow-on FDD and implementation planning +**Research input:** `docs/research/personal-dashboard-current-state.md` + +## 1. Problem Statement + +Authenticated inter-hub users currently land on the Hubs list after login. That +list is a useful management table, but it does not answer the daily operating +questions users bring to inter-hub: + +- What changed recently? +- Which candidates or governance items need attention? +- Which hubs are unhealthy or blocked? +- Which learning or institutional knowledge signals should I notice today? +- Where should I go next? + +Inter-hub already has many specialized dashboards, but they are scattered across +hub-level and platform-level routes. Users must know which surface to visit and +manually stitch together recent activity, open work, health, governance, and +learning signals. + +The personal dashboard should become the authenticated landing surface that +summarizes the most relevant existing signals and links users to the source +dashboards for detail. It should be persistent, configurable, server-rendered, +and governed by the same IHF widget, annotation, and interaction-event model as +the rest of the application. + +## 2. Users and Personas + +### Hub Operator + +Owns or watches one or more hubs. Needs quick visibility into recent events, +open requirement candidates, hub health, active bottlenecks, and regressions. + +Primary questions: + +- Which hubs need attention now? +- What happened since the last session? +- Which candidates are still open? +- Are any bottlenecks or health drops visible? + +### Governance Reviewer + +Triages candidates, reviews decisions, checks policy coverage, and follows +traceability from observations to implementation outcomes. + +Primary questions: + +- Which candidates are waiting for triage? +- Which decisions or requirements changed recently? +- Which panels need annotation or review? +- Are governance signals visible without visiting every hub? + +### AI Orchestrator + +Monitors agent proposals, review outcomes, learning signals, and institutional +knowledge that may affect future AI-assisted work. + +Primary questions: + +- Which agent proposals or reviews need attention? +- What learning insights were generated recently? +- Which knowledge entries should inform the next work session? +- Are there patterns of repeated friction or successful reuse? + +### Platform Admin + +Watches the inter-hub platform itself: API consumers, hub registry health, +manifests, policy overlays, marketplace activity, and cross-hub propagation. + +Primary questions: + +- Are API consumers active and healthy? +- Are hub manifests and registry views coherent? +- Are cross-hub governance or propagation signals emerging? +- Which operational panels should be promoted into a shared view later? + +## 3. Product Goals + +- Replace the authenticated post-login Hubs table as the daily landing surface. +- Provide a compact, configurable overview of existing inter-hub signals. +- Preserve the existing specialized dashboards as source-of-truth detail views. +- Make every saved panel a governed IHF interaction artifact. +- Keep the first implementation simple enough to deliver without a broad + dashboard refactor. + +## 4. Non-Goals + +- Do not build a drag-and-drop report builder. +- Do not add external datasource connectors. +- Do not introduce a client-side data fetching framework. +- Do not refactor all existing dashboards into reusable panel modules in the + first slice. +- Do not add a full role/permission system. +- Do not make shared/team dashboards part of the first implementation. +- Do not change public root, tutorial, capabilities, or extension-guide routes. + +## 5. Core Requirements + +### Must + +- Provide an authenticated personal dashboard route. +- Redirect successful login to the personal dashboard instead of `HubsAction`. +- Preserve public root behavior for unauthenticated and documentation users. +- Persist at least one dashboard per user. +- Seed a default dashboard on first visit. +- Persist panel instances, layout position, and panel config. +- Render all first-slice panels server-side through IHP/HSX. +- Use a server-side panel catalogue with stable panel keys. +- Bound every panel query by limit and, where relevant, hub/status/time filters. +- Decode JSONB panel config into explicit Haskell config types before querying. +- Create or reference stable `Widget` records for saved panel instances. +- Wrap rendered panels with `widgetEnvelope`. +- Preserve annotation and interaction-event identity across sessions. +- Link each panel to its existing source dashboard or source entity list. +- Provide a simple edit mode for adding, removing, and reordering panels through + normal IHP forms. + +### Should + +- Support hub filters for panels backed by hub-owned data. +- Support simple time-range filters where the underlying table has timestamps. +- Support limit/display-mode settings for panels with list content. +- Refresh recent activity, triage, health, and learning panels using the + existing `autoRefresh` style. +- Keep forms keyboard accessible and understandable without custom JavaScript. +- Render a neutral empty state when a panel has no data. +- Provide safe fallback behavior for invalid panel config. +- Keep first paint sub-second for a seeded dashboard with default panels. +- Use existing Tailwind/card/table visual conventions. + +### Could + +- Add saved watched-hub sets. +- Add multiple named dashboards per user. +- Add dashboard templates. +- Add shared/team dashboards. +- Add more panel types after the first framework slice is proven. +- Add finer-grained panel refresh routes later. +- Add user-selected default landing dashboard later. + +### Won't for First Implementation + +- Drag-and-drop layout editing. +- Mobile-native layout editor. +- Client-side data fetching. +- External dashboard or BI embedding. +- External datasource credentials. +- Role-based access control beyond existing authenticated controller guards. +- Complex visual charting library integration. + +## 6. First-Slice Panel Catalogue + +The first implementation should prove the framework with a small panel set. + +| Panel key | Label | Purpose | Default behavior | +|---|---|---|---| +| `watched-hubs` | Watched Hubs | Show hub list plus latest health hint | All hubs, limit 12 | +| `recent-interactions` | Recent Activity | Show latest interaction events with hub/widget context | Last 24h, limit 25 | +| `triage-queue` | Triage Queue | Show open requirement candidates | Open status, oldest first, limit 10 | +| `recent-decisions` | Recent Decisions | Show recent governance decisions | Last 30 days, newest first, limit 10 | +| `hub-health` | Hub Health | Show latest health snapshot and active blockers | Latest per hub, limit 12 | +| `learning-digest` | Learning Digest | Show latest insights and knowledge highlights | Latest insights and entries, limit 5 each | + +Deferred panel catalogue candidates: + +- `agent-proposals` +- `api-usage` +- `marketplace-trending` +- `my-annotations` +- `policy-compliance` +- `adapter-compatibility` +- `cross-hub-propagations` + +## 7. Functional Requirements + +### Dashboard Route + +- Add a `PersonalDashboardsController` or similarly named controller. +- Add a show action for the current user's default dashboard. +- Add edit/update/add/remove actions for panel management. +- Register routes in `Web.Routes` and `Web.FrontController`. +- Add a sidebar link labelled `Dashboard`. + +### Dashboard Persistence + +- Store dashboard ownership by `users.id`. +- Support at least one default dashboard per user. +- Store panel order and grid layout. +- Store panel config in JSONB. +- Store a linked panel widget id for governance. +- Keep panel deletion non-destructive with respect to historical events and + annotations. + +### Default Seeding + +- On first visit, create a default dashboard for the authenticated user. +- Seed the first-slice panels with safe default config. +- Do not require a `users.role` column. +- Best-effort hub relevance may use active `stewardship_roles.assigned_to` + matching user email or name, but the neutral default must work without it. + +### Panel Rendering + +- Dispatch panels by stable catalogue key. +- Decode and validate config before querying. +- Use bounded queries. +- Render empty states and config warnings without crashing the whole dashboard. +- Wrap every rendered panel in `widgetEnvelope`. +- Link to source dashboards for deeper work. + +### Edit Mode + +- List current panels in layout order. +- Allow adding a panel from active panel types. +- Allow removing a panel from the dashboard. +- Allow editing column, row, span, limit, hub filter, time range, and display + mode where supported. +- Validate layout spans and config values server-side. +- Keep forms usable without custom JavaScript. + +### Governance and Event Capture + +- Saved panels must use stable `Widget` rows. +- Panel widgets should use the existing `panel` widget type. +- Panel widget `view_context` must be non-empty. +- Panel annotations must attach to the panel widget id. +- Panel interaction capture should use existing event types where possible. +- Adding/removing/reconfiguring panels should not mutate historical + interaction events. + +## 8. Non-Functional Requirements + +### Performance + +- Default dashboard first paint target: under 1 second in local development + with seeded fixtures. +- Each panel query should have a default row limit. +- Any aggregate query decoded as `Int` must cast `COUNT(*)` to integer or + decode as `Int64`. +- Avoid N+1 patterns where a simple join or batched fetch is practical. +- Use existing indexes where available; document new index needs in the FDD. + +### Reliability + +- Invalid panel config should not break the whole dashboard. +- Missing source data should render an empty state. +- Missing linked widget should be repaired or reported by the controller before + rendering. +- Dashboard seeding should be idempotent. +- Login redirect should fall back gracefully if dashboard seeding fails. + +### Security and Privacy + +- Dashboard routes require authenticated users. +- Users can view and edit only their own personal dashboard in the first slice. +- No secrets, API keys, or token values may be shown in dashboard panels. +- API usage panels must show only non-secret consumer metadata. +- Panel config must not become an arbitrary SQL or route execution surface. + +### Accessibility and Usability + +- Use semantic headings for panels. +- Use tables/lists for scan-heavy operational data. +- Use form labels for all edit inputs. +- Keep navigation links clear and predictable. +- Do not rely on hover-only controls for essential edits. + +### Maintainability + +- Put renderer dispatch and config decoding in a focused helper/module. +- Keep panel renderer functions small and testable. +- Avoid moving existing dashboard code unless required. +- Prefer additive schema migrations. +- Keep first implementation tasks small enough for separate Codex sessions. + +## 9. Acceptance Criteria + +The product design is acceptable when the FDD can specify: + +- Exact schema tables and fields. +- Exact controller/action names. +- Exact default panel seed set. +- Exact panel config types and defaults. +- Exact first-slice panel query shapes. +- Exact governance identity lifecycle for panel widgets. +- Exact smoke tests for login, dashboard seeding, editing, annotation, and + source dashboard link-outs. + +The implementation will be acceptable when: + +- A new authenticated user lands on a seeded personal dashboard after login. +- The seeded dashboard renders all first-slice panels. +- The user can add, remove, and adjust panels through server-rendered forms. +- Panel layout persists across sessions. +- Each panel is wrapped in `widgetEnvelope`. +- Annotating a panel opens the existing widget annotation flow. +- Existing Hubs and specialized dashboard routes still load. +- Focused tests or smoke checks cover seeding, config validation, route access, + and bounded query behavior. + +## 10. Risks and Mitigations + +| Risk | Mitigation | +|---|---| +| Scope grows into report builder | Limit first slice to six panel types and server-rendered forms | +| Existing dashboards are hard-coded | Extract only needed query/render fragments into new panel renderers | +| Panel config becomes unsafe JSON | Decode into Haskell ADTs and validate before querying | +| Role-aware defaults require missing schema | Use neutral default first; only best-effort stewardship hints | +| AutoRefresh refreshes too much | Bound all queries; defer per-panel refresh unless needed | +| Panel annotations lack stable identity | Require `dashboard_panels.widget_id` and `widgetEnvelope` | +| COUNT decode errors recur | Cast aggregate counts or decode as `Int64` in implementation | + +## 11. Open Questions for FDD + +1. Should table names use `personal_dashboards`/`dashboard_panels`, or a more + explicit `user_dashboards` prefix? +2. Should removed panels archive their linked widgets or mark them deprecated? +3. Should panel widgets be owned by the framework hub, or by the source hub when + a panel is hub-specific? +4. Should the first implementation allow multiple dashboards per user, or only + one default dashboard with schema ready for multiples? +5. Should `autoRefresh` wrap the whole dashboard action initially, or should + live panel fragments get their own actions? +6. Should watched hubs be a separate table in the first slice, or represented + as dashboard panel config only? + +## 12. Recommendation + +Proceed to FDD with a small, governed, server-rendered personal dashboard: + +- One default dashboard per user, schema ready for multiples. +- Six first-slice panel types. +- `dashboard_panels.widget_id` as the governance anchor. +- Existing `panel` widget type for saved panel widgets. +- Whole-page `autoRefresh` initially, with bounded queries. +- Simple edit forms and no custom client runtime. diff --git a/docs/research/personal-dashboard-current-state.md b/docs/research/personal-dashboard-current-state.md new file mode 100644 index 0000000..0f1e45b --- /dev/null +++ b/docs/research/personal-dashboard-current-state.md @@ -0,0 +1,348 @@ +# Personal Dashboard Current-State Research + +**Workplan:** IHUB-WP-0020 +**Date:** 2026-06-16 +**Status:** Research deliverable for T01 + +## Purpose + +This note reviews the current inter-hub implementation before designing a +personal dashboard framework. The main finding is that inter-hub already has a +large set of server-rendered dashboard surfaces, governed widget identity, type +registries, annotations, event capture, hub health, API usage, marketplace, and +learning data. The personal dashboard should compose these capabilities instead +of inventing a separate dashboard product. + +External dashboard products are used here only for pattern extraction. The +implementation direction remains IHP, HSX, Tailwind, server-rendered forms, and +existing IHF governance primitives. + +## Evidence Reviewed + +Repo files inspected for this note: + +- `Web/Controller/Hubs.hs` +- `Web/Controller/Sessions.hs` +- `Web/FrontController.hs` +- `Web/Routes.hs` +- `Web/Types.hs` +- `Application/Schema.sql` +- `Application/Helper/View.hs` +- `Web/Controller/FederatedGovernance.hs` +- `Web/Controller/FederatedPolicyOverlays.hs` +- `Web/Controller/ApiDashboard.hs` +- `Web/Controller/MarketplaceDashboard.hs` +- `Web/Controller/LearningDashboard.hs` +- `docs/phase1-summary.md` through `docs/phase8-summary.md` +- `docs/ihp-ihf-mapping.md` +- `docs/widget-envelope-convention.md` +- Workplans IHUB-WP-0001 through IHUB-WP-0015 where dashboard scope was + introduced. + +## Current Authenticated Entry Point + +The public root and documentation pages already exist through IHUB-WP-0015 and +are registered last in `Web.FrontController`. The authenticated login flow still +redirects to `HubsAction`: + +```haskell +login user +redirectTo HubsAction +``` + +`HubsAction` renders a table of hubs. It is useful as an admin list, but it is +not a personal daily operating surface. + +The sidebar already links to several platform surfaces, including Hubs, +Learning, Ops Review, Federation, API Dashboard, Hub Registry, and Marketplace. +The personal dashboard should therefore become a new authenticated landing +route and a sidebar entry, while public root behavior remains unchanged. + +## Existing Dashboard Inventory + +| Surface | Action | Scope | Live? | Reuse potential | +|---|---|---|---|---| +| Hub list | `HubsAction` | Global hub table | No | Source for watched hubs and hub selector | +| Hub show | `ShowHubAction` | One hub | Yes | Recent events, annotations, widgets, manifest summary | +| Triage dashboard | `TriageDashboardAction` | One hub | Yes | Open candidates, recent escalations, annotation breakdown | +| Governance dashboard | `GovernanceDashboardAction` | One hub | Yes | Accepted candidates, requirements, decisions, traceability | +| Antifragility dashboard | `AntifragilityDashboardAction` | One hub | Yes | Deployments, outcome signals, recurrence leaderboard | +| Agent audit dashboard | `AgentAuditDashboardAction` | One hub context, global data | Yes | Agent proposals and review status | +| Adapter compatibility | `AdapterCompatibilityDashboardAction` | One hub | Yes | Adapter and contract compatibility panels | +| Friction heatmap | `FrictionHeatmapAction` | One hub | Yes | Widget friction summary | +| Bottleneck dashboard | `BottleneckDashboardAction` | One hub | Yes | Open bottlenecks | +| Hub health history | `HubHealthHistoryAction` | One hub | Yes | Health snapshot trend | +| Operational review board | `OperationalReviewBoardAction` | Global | Yes | Hub health, top friction, bottlenecks, propagations | +| Federated governance | `FederatedGovernanceDashboardAction` | Global | Yes | Ownership, routing, policy, stewardship, archive activity | +| Policy compliance | `PolicyComplianceDashboardAction` | Global | Yes | Active overlays and policy reference coverage | +| API dashboard | `ShowApiDashboardAction` | Global API consumers | Yes | Per-consumer request volume, error rate, last seen | +| Marketplace | `MarketplaceDashboardAction` | Global patterns/templates | Yes | Trending patterns, search/filter catalogue | +| Learning dashboard | `LearningDashboardAction` | Global learning memory | Yes | Insights, knowledge highlights, pattern rankings | + +The reusable unit today is not a standalone panel renderer. It is a controller +query plus an HSX view fragment. WP-0021 should extract only the first small set +of renderers needed for the personal dashboard. A broad refactor of existing +dashboards is explicitly unnecessary for the first slice. + +## AutoRefresh and Query Patterns + +Most dashboard actions wrap the whole action with `autoRefresh do`. This is +simple and consistent with the existing IHP style. The current app does not have +a reusable per-panel refresh abstraction. + +Useful bounded patterns already exist: + +- `ShowHubAction` limits recent interaction events to 50 and annotations to 20. +- `GovernanceDashboardAction` limits recent decisions to 20. +- `LearningDashboardAction` limits correlations, rankings, insights, and + knowledge highlights. +- `MarketplaceDashboardAction` limits published patterns/templates and casts the + trending adoption count to integer. + +Risky or broad patterns to avoid copying directly: + +- Some hub dashboards fetch all related records for a hub and filter in memory. + That is acceptable for small scoped screens, but a personal dashboard should + bound every panel query by hub, time, status, and limit. +- Several global dashboards fetch all hubs or all decision records. A personal + view should either limit these or explicitly display only summarized rows. +- Raw `COUNT(*)` queries should cast to `integer` or decode as `Int64`. Recent + production work exposed PostgreSQL/Haskell decode failures when `COUNT(*)` + was decoded as `Int`. + +Recommendation: first implementation can wrap the whole personal dashboard +action in `autoRefresh do`. The FDD should leave finer-grained panel refresh as +a later optimization unless a simple route-level fragment pattern emerges. + +## Governed Widget and Annotation Constraints + +`Application.Helper.View.widgetEnvelope` wraps a `Widget` record and injects +governance metadata: + +- `data-widget-id` +- `data-widget-type` +- `data-hub-id` +- `data-capability-ref` +- `data-view-context` +- `data-policy-scope` +- `data-widget-version` + +It also renders an Annotate link to the widget annotation view. The helper +warns when `view_context` is absent. Therefore, a saved personal dashboard panel +must not be treated as transient markup. It needs stable widget identity. + +The registry seed includes a framework-level widget type named `panel`. That is +the best first choice for saved dashboard panels. A saved panel instance should +create or reference a `widgets` row with: + +- `widget_type = 'panel'` +- `capability_ref = 'personal-dashboard.'` +- `view_context = 'personal-dashboard/'` +- `policy_scope = 'internal'` +- `status = 'active'` + +The FDD should decide the owning hub rule. Recommended first slice: use the +framework hub for personal dashboard panel widgets and store source hub filters +in panel config. This keeps the personal dashboard itself governed by inter-hub +while still letting panels point at hub-specific data. + +## Schema Available for First-Slice Panels + +Existing tables with direct panel value: + +- `hubs` +- `widgets` +- `widget_versions` +- `interaction_events` +- `annotations` +- `annotation_threads` +- `requirement_candidates` +- `triage_states` +- `reviewer_assignments` +- `requirements` +- `decision_records` +- `deployment_records` +- `outcome_signals` +- `friction_scores` +- `bottleneck_records` +- `hub_health_snapshots` +- `cross_hub_propagations` +- `widget_ownerships` +- `hub_routing_rules` +- `federated_policy_overlays` +- `stewardship_roles` +- `archive_records` +- `widget_type_registry` +- `event_type_registry` +- `annotation_category_registry` +- `policy_scope_registry` +- `hub_capability_manifests` +- `api_consumers` +- `api_request_log` +- `widget_patterns` +- `pattern_adoptions` +- `governance_templates` +- `governance_template_clones` +- `outcome_correlations` +- `pattern_performance_records` +- `adaptive_threshold_configs` +- `institutional_knowledge_entries` +- `learning_insights` + +Important gaps: + +- No `personal_dashboards` table. +- No `dashboard_panel_types` table. +- No `dashboard_panels` table. +- No saved watched-hub set or user preference table. +- No user role column. +- No panel config decoder/validator. +- No dedicated panel renderer module. +- No explicit default dashboard seeding helper. + +## User and Role Model Findings + +The `users` table has email, password hash, name, lockout fields, and created +time. It has no role. `stewardship_roles` stores `assigned_to` as text and is +hub-scoped. That can help infer operator relevance, but it is not a reliable +role foreign key. + +Recommendation: do not add `users.role` for the first slice. Seed a neutral +default dashboard for all authenticated users, then allow the user to edit panel +layout and filters. If a default needs hub relevance, match active +`stewardship_roles.assigned_to` against user email or name as a best-effort +hint, not as an authorization rule. + +## First-Slice Panel Candidates + +The following panels are practical without broad refactoring: + +| Panel key | Source | Default filter | Notes | +|---|---|---|---| +| `watched-hubs` | `hubs`, latest `hub_health_snapshots` | all hubs, limit 12 | First panel can be neutral until watched hubs exist | +| `recent-interactions` | `interaction_events`, `widgets`, `hubs` | last 24h, limit 25 | Existing indexes support recent ordering | +| `triage-queue` | `requirement_candidates` | `status = 'open'`, limit 10 | Can join source widget/hub for context | +| `recent-decisions` | `decision_records` | last 30 days, limit 10 | Good governance reviewer entry point | +| `hub-health` | `hub_health_snapshots`, `bottleneck_records` | latest per hub, limit 12 | Needs bounded latest-per-hub query | +| `learning-digest` | `learning_insights`, `institutional_knowledge_entries` | latest, limit 5/5 | Already bounded in existing dashboard | + +Panels to defer until after the framework is proven: + +- `agent-proposals` +- `api-usage` +- `marketplace-trending` +- `my-annotations` +- `adapter-compatibility` +- `policy-compliance` + +The deferred panels are valuable, but they are not needed to prove dashboard +persistence, layout, panel renderer dispatch, and governed panel identity. + +## External Pattern Extraction + +| System | Useful pattern | Translation for inter-hub | +|---|---|---| +| Grafana | Dashboard as saved grid of panels with variables and refresh behavior | Save panel rows plus hub/time filters; keep server-rendered refresh | +| Kibana dashboards | Saved searches and time range awareness | Treat panel query config as explicit, bounded, validated config | +| Retool/Appsmith | Widget catalogue and data binding | Use a server-side panel catalogue; avoid client runtime/data binding | +| Linear home | Personal "my work" aggregation across entities | Make the personal dashboard a daily work queue, not a clone of every dashboard | +| Notion linked databases | Multiple saved views over the same records | Let panels define filter/sort/display options against existing tables | +| Metabase | Question as a governed reusable unit | Treat panel renderer plus validated config as the reusable unit | +| Streamlit | Simple declarative layout vocabulary | Use predictable grid rows/spans and forms rather than drag-and-drop | + +The key pattern across these systems is not visual complexity. It is that a +dashboard is a saved composition of bounded questions/panels with explicit +parameters. For inter-hub, those questions must remain governed IHF widgets. + +## Answers to WP-0020 Research Questions + +### Which existing fragments can become first-slice renderers? + +Good first-slice candidates: + +- Recent activity from `ShowHubAction`. +- Open candidate queue from `TriageDashboardAction`. +- Recent decisions from `GovernanceDashboardAction`. +- Latest hub health from `OperationalReviewBoardAction` and + `HubHealthHistoryAction`. +- Learning digest from `LearningDashboardAction`. +- Watched hubs from `HubsAction` plus latest health snapshots. + +These can be implemented as new renderer functions that reuse the same model +queries and link to existing source dashboards for detail. + +### Which configs are needed on day one? + +Recommended day-one config options: + +- `hubIds :: [Id Hub]` or `hubFilter :: Maybe [Id Hub]` +- `timeRange :: Last24Hours | Last7Days | Last30Days | AllTimeBounded` +- `limit :: Int` +- `sort :: NewestFirst | OldestFirst | HighestRiskFirst` +- `displayMode :: Compact | Detailed` + +Config should be stored as JSONB but decoded into a Haskell ADT before use. +Invalid config should fall back to panel defaults and surface a non-fatal +operator warning. + +### Which panels should live-refresh? + +Live-refresh in the first slice: + +- `recent-interactions` +- `triage-queue` +- `hub-health` +- `learning-digest` + +Static per request in the first slice: + +- `watched-hubs` +- `recent-decisions` + +If the first implementation wraps the entire personal dashboard in +`autoRefresh`, all panels will refresh together. That is acceptable initially if +queries are bounded. + +### How should saved panels map to governed widgets? + +Each saved dashboard panel should own a `widgets` row and store the id on +`dashboard_panels.widget_id`. The panel renderer should call `widgetEnvelope` +with that widget. This gives stable annotation and interaction capture identity. + +Panel lifecycle: + +1. User adds a panel. +2. Controller creates `dashboard_panels` row. +3. Controller creates linked `widgets` row with `widget_type = 'panel'`. +4. Controller creates a `widget_versions` snapshot for the panel widget. +5. Show view renders the panel through `widgetEnvelope`. +6. Removing a panel should mark the widget archived or deprecated, not delete + interaction history. + +### What should be deferred? + +Defer: + +- Drag-and-drop layout. +- Shared dashboards. +- Team dashboards. +- External datasource connectors. +- Client-side data fetching. +- Per-panel WebSocket channels. +- Full refactor of existing dashboard views. +- Complex role model. +- Dashboard marketplace/templates beyond one seeded default. + +## Recommendations for T02/T03 + +1. Define the personal dashboard as the authenticated landing page, not a + replacement for existing source dashboards. +2. Use a small panel catalogue for the first implementation. +3. Persist dashboard/panel rows in relational tables and panel config in JSONB. +4. Decode panel config into explicit Haskell ADTs before querying. +5. Give every saved panel stable `Widget` identity. +6. Use the existing `panel` widget type. +7. Keep the default dashboard neutral and editable. +8. Bound every panel query. +9. Cast SQL aggregate counts to integer when decoding as `Int`. +10. Keep implementation tasks small enough to avoid a cross-dashboard refactor. diff --git a/workplans/IHUB-WP-0020-personal-dashboard-framework.md b/workplans/IHUB-WP-0020-personal-dashboard-framework.md index 2aee3a9..9bee2fd 100644 --- a/workplans/IHUB-WP-0020-personal-dashboard-framework.md +++ b/workplans/IHUB-WP-0020-personal-dashboard-framework.md @@ -4,11 +4,11 @@ type: workplan title: "Personal Dashboard Framework" domain: inter_hub repo: inter-hub -status: backlog +status: finished owner: tegwick topic_slug: inter_hub created: "2026-05-03" -updated: "2026-06-07" +updated: "2026-06-16" phase: 13 state_hub_workstream_id: "72fc022b-0196-492a-aaba-3475f8768f06" --- @@ -17,283 +17,399 @@ state_hub_workstream_id: "72fc022b-0196-492a-aaba-3475f8768f06" ## Goal -Design and implement a personal dashboard framework that allows individual users to -compose, configure, and persist a view of the inter-hub platform tailored to their -role and focus. The dashboard is the post-login landing page and the primary daily -driver surface for hub operators, governance reviewers, and AI orchestrators. +Design the first personal dashboard layer for inter-hub: an authenticated, +per-user landing surface that composes the most important existing hub, +governance, API, marketplace, and learning signals into a configurable daily +operator view. -## Motivation +This workplan is now a design and implementation-planning workplan. It should +produce the current-state audit, product requirements, functional design, and +follow-on implementation workplan needed to build the feature safely. -The current post-login experience drops the user on a raw Hubs list. As inter-hub -grows to 12+ phases of functionality, users need a curated, role-aware entry point -that surfaces the signals that matter to them without requiring manual navigation. -The dashboard is also the natural home for cross-cutting observability (recent -decisions, open candidates, outcome signals) that cuts across the current -controller-per-entity navigation. +## Review Update: 2026-06-15 ---- +This workplan was reviewed against the current repository state and updated +from `backlog` to `ready`. + +The original version assumed inter-hub mainly had a raw Hubs list and needed a +greenfield dashboard framework. That assumption is outdated. The repo now has +many dashboard-like surfaces and governed interaction primitives that should be +reused instead of bypassed: + +- Public root/intro pages exist from IHUB-WP-0015; the authenticated login flow + still redirects to `HubsAction`. +- Hub-level dashboard actions already exist in `Web.Controller.Hubs`, including + hub show, triage, governance, antifragility, agent audit, adapter + compatibility, friction heatmap, bottleneck, hub health history, and the + operational review board. +- Cross-hub and platform dashboards already exist: federated governance, policy + compliance, API usage, marketplace, and learning dashboard. +- The governed interaction substrate is mature: `widgets`, `widget_versions`, + `interaction_events`, `annotations`, type registries, hub manifests, + ownership/routing, API request logs, hub health snapshots, learning insights, + and institutional knowledge are all present. +- There is no personal dashboard schema, controller, saved panel catalogue, + user preference model, or role-aware default layout yet. +- Existing dashboards are mostly hard-coded controller queries plus HSX view + fragments. They are useful source material, but they are not yet reusable + panel renderers. +- The `users` table has no role column. `stewardship_roles.assigned_to` is text + and hub-scoped, so role-aware defaults must be designed carefully instead of + assuming a user-role foreign key exists. + +The updated scope is therefore integration-first: define a personal dashboard +contract that reuses existing data sources and view patterns, then introduce a +small panel renderer abstraction only where it removes real duplication. + +## Scope + +### In Scope + +- Authenticated personal dashboard route and post-login redirect design. +- Per-user saved dashboard record with ordered panel instances. +- A server-rendered panel catalogue backed by existing inter-hub models. +- Simple layout editing through IHP forms; no drag-and-drop in the first slice. +- Hub/time filters for panels where the underlying queries already support + bounded data. +- Panel-level governance: each rendered saved panel must be annotatable and + event-capturable through the existing `widgetEnvelope` convention. +- A migration path that reuses current dashboard queries before attempting broad + refactors. + +### Out of Scope for the First Implementation Workplan + +- Client-side dashboard frameworks or client-side data fetching. +- External datasource connectors. +- Shared/team dashboards. +- Mobile-native layout editing. +- Drag-and-drop layout editing. +- A general purpose report builder. +- Rewriting every existing dashboard into panel renderers. + +## Current Design Constraints + +- Server-rendered IHP views remain the default. `autoRefresh` is acceptable for + panels that already use live refresh patterns. +- Tailwind and existing HSX view conventions should be reused. +- Runtime panel config may be stored as JSONB, but renderer code should decode + into explicit Haskell config types before use. +- Do not create an ungoverned visual component layer. A saved dashboard panel + must either reference or create a `Widget` row, most likely using the existing + framework-level `panel` widget type, so annotations and interaction events + remain first-class IHF artifacts. +- Avoid adding a `users.role` column unless the PRS/FDD proves it is needed. + Prefer defaults derived from current user identity, stewardship assignments, + selected watched hubs, or explicit dashboard template choice. + +## Proposed First-Slice Panel Catalogue + +The initial catalogue should be limited to panels that can be built from +existing tables and controllers: + +| Panel key | Label | Source surface/data | Live? | +|---|---|---|---| +| `watched-hubs` | Watched Hubs | `hubs`, `hub_health_snapshots`, optional saved hub filter | No | +| `recent-interactions` | Recent Activity | `interaction_events` plus `widgets` and `hubs` | Yes | +| `triage-queue` | Triage Queue | open `requirement_candidates` | Yes | +| `recent-decisions` | Recent Decisions | `decision_records`, requirements, candidates | No | +| `hub-health` | Hub Health | latest `hub_health_snapshots`, bottlenecks | Yes | +| `agent-proposals` | Agent Proposals | `agent_proposals`, `agent_review_records` | No | +| `api-usage` | API Usage | `api_consumers`, `api_request_log` | Yes | +| `marketplace-trending` | Marketplace Trending | `widget_patterns`, adoptions, templates | No | +| `learning-digest` | Learning Digest | `learning_insights`, `institutional_knowledge_entries` | Yes | +| `my-annotations` | My Annotations | `annotations` filtered by current user when available | No | + +The implementation workplan should start with a smaller subset if needed: +`watched-hubs`, `recent-interactions`, `triage-queue`, `recent-decisions`, +`hub-health`, and `learning-digest` are enough to prove the framework. ## Tasks -### T01 — Research: Dashboard frameworks and patterns for inspiration +### T01 - Current-state audit and dashboard pattern research ```task id: IHUB-WP-0020-T01 -status: todo +status: done priority: high state_hub_task_id: "6074f195-636b-4517-b6d1-eb3c57394a82" ``` -Survey existing dashboard systems to extract patterns that are re-implementable in -Haskell / IHP / Tailwind under inter-hub's design constraints (server-rendered, -type-safe, governed). +Produce a short research note that starts with the current inter-hub codebase, +then uses external dashboard systems only for secondary inspiration. -**Research targets:** +Required current-state inventory: + +- Existing routes and views that behave like dashboards. +- Existing `autoRefresh` usage and query patterns that are safe to reuse. +- Existing type registry, `Widget`, `widgetEnvelope`, annotation, and event + capture constraints. +- Existing tables that can power first-slice personal panels. +- Gaps: personal dashboard persistence, panel catalogue, saved filters, layout + model, and user preference/defaulting model. + +External systems may still be sampled, but the output should focus on patterns +that are practical in IHP/HSX/Tailwind: | System | What to extract | |---|---| -| **Grafana** | Panel/grid layout model; datasource abstraction; variable-driven filtering | -| **Kibana (dashboards)** | Saved-search panels; time-range awareness; role-based visibility | -| **Retool / Appsmith** | Widget catalogue approach; drag-grid layout; data binding model | -| **Linear (home view)** | Flat "My Work" aggregation across entities; priority surfacing | -| **Notion (linked databases)** | Filter/sort persistence per user; view types (table, board, calendar) | -| **Observable Framework** | Reactive cell model; markdown + code co-location | -| **Metabase** | Question-as-unit; dashboard as ordered collection of questions | -| **Streamlit** | Declarative layout (columns, expanders); pure functional rendering loop | +| Grafana | Panel/grid layout model, dashboard variables, bounded refresh | +| Kibana dashboards | Saved-search panels, time range filters, role visibility | +| Retool/Appsmith | Widget catalogue and data binding concepts, not their client runtime | +| Linear home view | Flat "my work" aggregation across entities | +| Notion linked databases | Saved filters/sorts as user-facing views | +| Metabase | Question-as-unit model and governed saved queries | +| Streamlit | Declarative layout vocabulary suitable for server rendering | -**Questions to answer per system:** -1. How is a dashboard persisted? (JSON blob, relational rows, code-as-config?) -2. How is a widget/panel parameterised? (datasource, filter, display options) -3. How is layout described? (fixed grid, CSS grid, drag-and-drop, responsive breakpoints) -4. How is per-user state separated from shared/team state? -5. What is the update model? (full-page reload, WebSocket push, polling, partial HTMX swap) -6. How are access controls expressed at panel level? +Questions to answer: -**IHP/Haskell-specific constraints to keep in mind:** -- Server-rendered by default; AutoRefresh (WebSocket) available for live data -- No client-side state management library; JS must be minimal -- Type safety from DB schema → view layer is a first-class constraint -- Tailwind CSS; no component library +1. Which existing inter-hub dashboard fragments can become first-slice panel + renderers without broad refactoring? +2. Which panel configs must exist on day one: hub filter, time range, limit, + display mode, or sort order? +3. Which panels need live refresh, and which should stay static per request? +4. How should each saved panel map to a governed `Widget` row? +5. What should be explicitly deferred to avoid building a report builder? -**Exit criteria:** Research notes written in `docs/research/dashboard-frameworks.md`. +Exit criteria: `docs/research/personal-dashboard-current-state.md` exists and +has enough evidence to drive the PRS. + +Completion note (2026-06-16): added +`docs/research/personal-dashboard-current-state.md`, covering the current +dashboard inventory, AutoRefresh/query patterns, governed widget constraints, +first-slice panel candidates, external pattern extraction, and T02/T03 +recommendations. --- -### T02 — Product Requirements Specification (PRS) +### T02 - Product Requirements Specification ```task id: IHUB-WP-0020-T02 -status: todo +status: done priority: high depends_on: T01 state_hub_task_id: "698304bc-b91a-42e2-a617-b3ddbf749174" ``` -Produce a formal PRS for the Personal Dashboard Framework based on T01 findings and -inter-hub's design principles. +Produce a formal PRS based on T01 and the current implementation. -**Required sections:** +Required sections: -1. **Problem statement** — who uses the dashboard, what decisions they make with it, - what pain the current flat-nav approach causes +1. Problem statement: authenticated users currently land on the Hubs list and + must manually navigate to specialized dashboards to answer daily operating + questions. +2. Personas: + - Hub operator: watches hub health, recent events, candidates, and + bottlenecks. + - Governance reviewer: triages candidates, decisions, policy coverage, and + annotations. + - AI orchestrator: watches agent proposals, review outcomes, and learning + signals. + - Platform admin: watches API usage, hub registry health, manifests, and + cross-hub propagation. +3. Core requirements using MoSCoW: + - Must: per-user saved dashboard, seeded default dashboard, panel catalogue, + server-rendered panels, persisted layout, governed panel widget identity, + post-login route design, bounded panel queries. + - Should: hub/time filters, simple edit mode, live refresh on selected + panels, keyboard-accessible forms, link-outs to existing source + dashboards. + - Could: dashboard templates, saved watched-hub sets, shared dashboards, + richer display modes. + - Won't: drag-and-drop, external datasources, client-side fetching, mobile + layout editor, complete refactor of existing dashboards. +4. Non-functional requirements: + - First paint target remains sub-second for seeded dashboards with bounded + panel queries. + - Panel queries must use limits and existing indexes or propose new indexes. + - Dashboard save/load must be simple transactional IHP controller work. + - No new JS framework. +5. Governance fit: + - Saved panel instances are governed IHF widgets or reference governed + widgets. + - Panel views use `widgetEnvelope`. + - Panel interactions emit existing event types where possible. + - Annotations attach to the panel widget identity, not to a transient DOM + block. -2. **User personas** - - Hub Operator: monitors activity within their hub; wants recent events, open candidates - - Governance Reviewer: triages candidates, reviews decisions; needs queue and signal views - - AI Orchestrator: monitors agent proposals, outcome correlations; needs performance panels - - Platform Admin: watches system health, API usage, learning throughput +Exit criteria: `docs/prs/personal-dashboard-prs.md` exists and is ready for +FDD work. -3. **Core requirements (MoSCoW)** - - Must: per-user dashboard persisted in DB; selectable panels from a catalogue; - layout preserved across sessions; role-aware default layout on first login - - Should: panel-level filtering (by hub, by time range); live-update via AutoRefresh - for signal panels; keyboard-navigable - - Could: drag-and-drop layout editing; shared/team dashboards; dashboard templates - - Won't (Phase 13): mobile-native layout; client-side data fetching; external datasources - -4. **Non-functional requirements** - - First paint < 500 ms (server-rendered, no JS data fetching) - - Dashboard save/load < 100 ms - - Each panel query < 200 ms (indexed, bounded result sets) - - Zero JS frameworks; AutoRefresh WebSocket for live panels only - -5. **Governance fit** — dashboard widgets are themselves `Widget` records in the IHF - sense; `InteractionEvent`s recorded on dashboard interactions; annotations possible - on any panel - -**Exit criteria:** `docs/prs/dashboard-framework-prs.md` reviewed and accepted. +Completion note (2026-06-16): added +`docs/prs/personal-dashboard-prs.md`, defining the problem statement, +personas, MoSCoW requirements, first-slice panel catalogue, governance +requirements, acceptance criteria, risks, and FDD open questions. --- -### T03 — Functional Design Document (FDD) +### T03 - Functional Design Document ```task id: IHUB-WP-0020-T03 -status: todo +status: done priority: high depends_on: T02 state_hub_task_id: "438e5771-a043-4f26-a1ce-994ed478a760" ``` -Translate the PRS into a concrete functional design covering schema, component model, -rendering pipeline, and layout system. This is the authoritative reference for implementation. +Translate the PRS into a concrete FDD covering schema, controller actions, +panel renderer contract, layout, seed/default behavior, and migration strategy. -**Required sections:** - -#### 3.1 Data model +The FDD must update the old greenfield schema sketch. A likely shape is: ```sql --- A user's named dashboard -CREATE TABLE dashboards ( +CREATE TABLE personal_dashboards ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, user_id UUID NOT NULL REFERENCES users(id), name TEXT NOT NULL, is_default BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); --- An instance of a panel type on a dashboard, with position -CREATE TABLE dashboard_panels ( +CREATE TABLE dashboard_panel_types ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, - dashboard_id UUID NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE, - panel_type TEXT NOT NULL, -- FK to panel_type_registry - config JSONB NOT NULL DEFAULT '{}', - col INT NOT NULL DEFAULT 0, - row INT NOT NULL DEFAULT 0, - col_span INT NOT NULL DEFAULT 1, - row_span INT NOT NULL DEFAULT 1, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - --- Registry of available panel types -CREATE TABLE panel_type_registry ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, - name TEXT NOT NULL UNIQUE, + key TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, default_config JSONB NOT NULL DEFAULT '{}', - requires_hub BOOLEAN NOT NULL DEFAULT FALSE, - live_update BOOLEAN NOT NULL DEFAULT FALSE + default_col_span INT NOT NULL DEFAULT 4, + default_row_span INT NOT NULL DEFAULT 1, + live_update BOOLEAN NOT NULL DEFAULT FALSE, + status TEXT NOT NULL DEFAULT 'active' +); + +CREATE TABLE dashboard_panels ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + dashboard_id UUID NOT NULL REFERENCES personal_dashboards(id) ON DELETE CASCADE, + panel_type_id UUID NOT NULL REFERENCES dashboard_panel_types(id), + widget_id UUID NOT NULL REFERENCES widgets(id), + config JSONB NOT NULL DEFAULT '{}', + col INT NOT NULL DEFAULT 0, + row INT NOT NULL DEFAULT 0, + col_span INT NOT NULL DEFAULT 4, + row_span INT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` -#### 3.2 Panel catalogue (Phase 13 scope) +The FDD must also resolve: -| Panel type name | Label | Description | Live? | -|---|---|---|---| -| `recent-interactions` | Recent Activity | Latest interaction events across watched hubs | Yes | -| `open-candidates` | Open Candidates | Requirement candidates awaiting triage | No | -| `decision-queue` | Decision Queue | Decisions pending review | No | -| `outcome-signals` | Outcome Signals | Recent outcome signal summary | Yes | -| `hub-health` | Hub Health | Health snapshot per hub | No | -| `agent-proposals` | Agent Proposals | Open AI agent proposals | No | -| `learning-digest` | Learning Digest | Latest institutional knowledge entries | No | -| `my-annotations` | My Annotations | Annotations by the current user | No | +- Naming: whether tables should use `personal_dashboards` or another prefix to + avoid confusing them with existing dashboard actions. +- Panel config: JSONB storage plus explicit Haskell ADT decoding and validation. +- Governance identity: how `dashboard_panels.widget_id` is created, versioned, + and named. +- Renderer contract: -#### 3.3 Layout system - -12-column CSS grid; panels occupy `col_span` columns and `row_span` rows. -Row height fixed at 240px. No drag-and-drop in Phase 13; layout edited via -form fields (col, row, span). Responsive: collapse to single column below 768px. - -``` -┌─────────────────────────────────────────────────────┐ -│ [Recent Activity ×6] [Decision Queue ×3] [Hub ×3]│ -│ │ -├─────────────────────────────────────────────────────┤ -│ [Open Candidates ×4] [Outcome Signals ×4] [My ×4] │ -└─────────────────────────────────────────────────────┘ -``` - -#### 3.4 Rendering pipeline - -``` -GET /Dashboard - → DashboardController#show - → fetch dashboard + panels (ordered by row, col) - → for each panel: dispatch to panel renderer (panelHtml panelType config) - → embed in DashboardView with CSS grid layout - → AutoRefresh wraps live panels only -``` - -Each panel renderer is a Haskell function: ```haskell -type PanelRenderer = PanelConfig -> ModelContext -> IO Html -renderPanel :: Text -> PanelConfig -> ModelContext -> IO Html -renderPanel "recent-interactions" = renderRecentInteractions -renderPanel "open-candidates" = renderOpenCandidates -... +data DashboardPanelConfig + = WatchedHubsConfig WatchedHubsOptions + | RecentInteractionsConfig RecentInteractionsOptions + | TriageQueueConfig TriageQueueOptions + | RecentDecisionsConfig RecentDecisionsOptions + | HubHealthConfig HubHealthOptions + | LearningDigestConfig LearningDigestOptions + +renderDashboardPanel + :: DashboardPanelType + -> DashboardPanel + -> DashboardPanelConfig + -> ModelContext + -> IO Html ``` -#### 3.5 Edit flow +- Layout: 12-column grid on desktop, single-column below the existing Tailwind + breakpoint, stable row/span constraints, no drag-and-drop in the first slice. +- Routes/actions: + - `PersonalDashboardAction` + - `EditPersonalDashboardAction` + - `UpdatePersonalDashboardAction` + - `AddDashboardPanelAction` + - `UpdateDashboardPanelAction` + - `RemoveDashboardPanelAction` +- Login behavior: `CreateSessionAction` should redirect to the personal + dashboard after authentication, while public root pages remain unchanged. +- Defaulting model: seed a default dashboard on first visit without requiring a + `users.role` column. +- Query safety: each panel query must be bounded, indexed, and compatible with + current PostgreSQL type decoding practices such as casting `COUNT(*)` to + integer when read as `Int`. +- Tests and smoke checks needed for the follow-on implementation workplan. -`GET /Dashboard/Edit` → grid with inline forms per panel (col/row/span inputs) + -"Add Panel" dropdown from `panel_type_registry`. `POST /Dashboard` saves layout. -No JavaScript needed for basic edit; optional HTMX for panel preview. +Exit criteria: `docs/fdd/personal-dashboard-fdd.md` exists, schema decisions +are concrete enough to implement, and open questions are explicitly listed. -#### 3.6 Default dashboard on first login - -`afterLoginRedirectPath` (in `SessionsControllerConfig`) redirects to `/Dashboard`. -On first visit, `DashboardController` checks for an existing dashboard; if absent, -creates a default one seeded from the user's role (determined by a `role` field -to be added to `users`, or a simple heuristic based on existing data). - -**Exit criteria:** FDD reviewed; schema migrations drafted; panel catalogue agreed. +Completion note (2026-06-16): added +`docs/fdd/personal-dashboard-fdd.md`, resolving schema names, panel config +typing, renderer/view-model shape, default seeding, governed panel widget +lifecycle, query constraints, routes, layout, tests, and handoff shape for +IHUB-WP-0021. --- -### T04 — Implementation workplan +### T04 - Implementation workplan ```task id: IHUB-WP-0020-T04 -status: todo +status: done priority: medium depends_on: T03 state_hub_task_id: "970aa221-7e17-4500-8b37-9c98676280b1" ``` -Break T03's FDD into a detailed, sequenced task list suitable for execution as a new -workplan (IHUB-WP-0021). Each task must have a clear entry/exit criterion and fit within -the 8k token soft budget. +Create the execution workplan for implementation as `IHUB-WP-0021`. -**Expected task structure of IHUB-WP-0021:** +Expected task structure for `IHUB-WP-0021`: -``` -T01 Schema migration: dashboards, dashboard_panels, panel_type_registry -T02 Seed: default panel types in panel_type_registry -T03 DashboardController — show action (fetch + render) -T04 Panel renderers — first 3 panels (recent-interactions, open-candidates, decision-queue) -T05 DashboardView — CSS grid layout -T06 Panel renderers — remaining 5 panels -T07 Dashboard edit flow (layout form, add/remove panels) -T08 Default dashboard seeding on first login -T09 afterLoginRedirectPath → /Dashboard -T10 AutoRefresh for live panels (recent-interactions, outcome-signals) -T11 Role-aware default layout -T12 Smoke tests -``` +| Task | Focus | +|---|---| +| T01 | Schema migration for personal dashboards, panel types, and panel instances | +| T02 | Seed dashboard panel types and any required framework `panel` widgets/type vocabulary | +| T03 | Add controller/action/route skeleton and default dashboard lookup/seed helper | +| T04 | Implement first three renderers: watched hubs, recent interactions, triage queue | +| T05 | Implement dashboard show view and responsive CSS grid | +| T06 | Implement remaining first-slice renderers: recent decisions, hub health, learning digest | +| T07 | Implement edit flow: reorder/update layout, add/remove panels, validate config | +| T08 | Add governed widget identity creation and `widgetEnvelope` wrapping for panels | +| T09 | Redirect successful login to the personal dashboard | +| T10 | Add `autoRefresh` only around selected live panels or the whole page if finer wrapping is not practical | +| T11 | Add focused tests for seeding, panel config validation, route access, and bounded queries | +| T12 | Manual smoke: login, seeded dashboard, edit layout, annotate a panel, verify source dashboards still load | -**Exit criteria:** IHUB-WP-0021 workplan file committed; T01–T12 each have -entry/exit criteria; ready for execution. +Each task must have entry criteria, exit criteria, rollback notes, and the +smallest reasonable test/smoke requirement. Keep implementation slices small +enough for Codex sessions to finish without broad refactors. ---- +Exit criteria: `workplans/IHUB-WP-0021-personal-dashboard-implementation.md` +exists with all tasks in `todo` state and enough detail to start implementation. + +Completion note (2026-06-16): added +`workplans/IHUB-WP-0021-personal-dashboard-implementation.md` with twelve +sequenced implementation tasks covering schema, seeds, controller skeleton, +panel renderers, show/edit views, governed panel widget lifecycle, login +redirect, AutoRefresh/query hardening, tests, and manual smoke. ## Exit Criteria Summary | Task | Deliverable | Status | |---|---|---| -| T01 | `docs/research/dashboard-frameworks.md` | todo | -| T02 | `docs/prs/dashboard-framework-prs.md` | todo | -| T03 | `docs/fdd/dashboard-framework-fdd.md` | todo | -| T04 | `workplans/IHUB-WP-0021-personal-dashboard-impl.md` | todo | +| T01 | `docs/research/personal-dashboard-current-state.md` | done | +| T02 | `docs/prs/personal-dashboard-prs.md` | done | +| T03 | `docs/fdd/personal-dashboard-fdd.md` | done | +| T04 | `workplans/IHUB-WP-0021-personal-dashboard-implementation.md` | done | -## Design Principles (binding throughout) +## Binding Design Principles -- **Server-first**: every panel renders in a single round-trip. No client-side data fetching. -- **Type-safe config**: `PanelConfig` is a Haskell ADT, not an opaque JSON blob at runtime. -- **IHF governed**: each rendered panel is a `Widget` with a `widgetEnvelope`; interactions - are recorded; annotations can be attached. -- **Tailwind only**: no external component library. Layout via CSS Grid with inline style for - structural properties (lessons learned from sidebar nav). -- **Minimal JS**: AutoRefresh WebSocket for live panels; vanilla JS for any UX enhancement. - No framework, no bundler beyond the existing IHP asset pipeline. +- Server-first: every panel renders on the server in the normal IHP request + lifecycle. +- Integration-first: reuse current dashboard query patterns before extracting + shared abstractions. +- Governed panels: saved panel instances have stable IHF widget identity and + use `widgetEnvelope`. +- Type-safe runtime config: JSONB is storage, not the unchecked runtime API. +- Bounded queries: every panel limits rows and uses existing indexes or proposes + a specific migration. +- Minimal JS: no framework and no client-side data fetch loop. +- Tailwind only: use existing view style and responsive grid conventions. diff --git a/workplans/IHUB-WP-0021-personal-dashboard-implementation.md b/workplans/IHUB-WP-0021-personal-dashboard-implementation.md new file mode 100644 index 0000000..26dd415 --- /dev/null +++ b/workplans/IHUB-WP-0021-personal-dashboard-implementation.md @@ -0,0 +1,623 @@ +--- +id: IHUB-WP-0021 +type: workplan +title: "Personal Dashboard Implementation" +domain: inter_hub +repo: inter-hub +status: ready +owner: codex +topic_slug: inter_hub +created: "2026-06-16" +updated: "2026-06-16" +phase: 13 +depends_on: IHUB-WP-0020 +related_docs: + - docs/research/personal-dashboard-current-state.md + - docs/prs/personal-dashboard-prs.md + - docs/fdd/personal-dashboard-fdd.md +state_hub_workstream_id: "79f72176-fb3f-4d59-9678-d42f5ff1e679" +--- + +# Personal Dashboard Implementation + +## Goal + +Implement the personal dashboard framework designed in IHUB-WP-0020: a +server-rendered authenticated landing page with persisted per-user panels, +governed panel widget identity, default dashboard seeding, simple edit forms, +and six first-slice panel renderers. + +## Inputs + +- `docs/research/personal-dashboard-current-state.md` +- `docs/prs/personal-dashboard-prs.md` +- `docs/fdd/personal-dashboard-fdd.md` +- Existing dashboard surfaces in `Web/Controller/Hubs.hs`, + `Web/Controller/LearningDashboard.hs`, `Web/Controller/ApiDashboard.hs`, + `Web/Controller/MarketplaceDashboard.hs`, and federated governance + controllers. + +## Constraints + +- Keep implementation additive. +- Preserve public root/static routes. +- Do not refactor all existing dashboards. +- No client-side data fetching framework. +- No drag-and-drop layout in this workplan. +- Every saved dashboard panel must have stable `Widget` identity and render + through `widgetEnvelope`. +- Bound every panel query. +- Cast aggregate `COUNT(*)` queries when decoding as `Int`, or decode as + `Int64`. + +## Tasks + +### T01 - Add personal dashboard schema + +```task +id: IHUB-WP-0021-T01 +status: todo +priority: high +state_hub_task_id: "bb7366a3-78ec-42d8-9f16-b7ed4979ec53" +``` + +Add the schema from the FDD: + +- `personal_dashboards` +- `dashboard_panel_types` +- `dashboard_panels` + +Implementation notes: + +- Add an `Application/Migration/-personal-dashboard-framework.sql` + migration. +- Update `Application/Schema.sql` consistently with the migration. +- Use `panel_key`, not `key`, on `dashboard_panel_types`. +- Include `removed_at` on `dashboard_panels`. +- Include indexes and layout CHECK constraints from the FDD. + +Entry criteria: + +- IHUB-WP-0020 is done. +- FDD exists and is reviewed enough for implementation. + +Exit criteria: + +- Schema files contain the three new tables and indexes. +- `rg "personal_dashboards|dashboard_panel_types|dashboard_panels" Application` + finds the expected migration/schema entries. +- No existing table or route behavior is changed. + +Verification: + +- Run `git diff --check`. +- If the IHP dev environment is available, run the repo compile/schema check + used by prior inter-hub workplans. + +Rollback notes: + +- Before production data exists, rollback is removing the migration/schema + additions. +- After production data exists, rollback requires preserving linked `widgets` + and `interaction_events`; do not delete panel widgets casually. + +--- + +### T02 - Seed panel types and framework panel vocabulary + +```task +id: IHUB-WP-0021-T02 +status: todo +priority: high +depends_on: T01 +state_hub_task_id: "d298eab2-736d-48ed-b6d4-84afa1604de9" +``` + +Add idempotent seed data for: + +- framework hub with slug `inter-hub` and `hub_kind = 'framework'` if absent; +- active widget type `panel` if absent; +- six first-slice `dashboard_panel_types`: + - `watched-hubs` + - `recent-interactions` + - `triage-queue` + - `recent-decisions` + - `hub-health` + - `learning-digest` + +Entry criteria: + +- T01 schema exists. + +Exit criteria: + +- Seed SQL or helper is idempotent. +- Re-running seeds does not create duplicate framework hubs, widget types, or + panel types. +- Default configs match the FDD. + +Verification: + +- Inspect seed SQL for `ON CONFLICT DO NOTHING` or equivalent idempotency. +- If DB is available, run a local seed twice and confirm row counts stay stable. + +Rollback notes: + +- Panel type seed rows are additive. If rollback is needed before use, remove + only the new dashboard panel type rows and any framework hub created solely + for this feature. + +--- + +### T03 - Add controller skeleton, routes, and default dashboard helper + +```task +id: IHUB-WP-0021-T03 +status: todo +priority: high +depends_on: T02 +state_hub_task_id: "8a171c71-3762-46c7-88d7-10ffb87fc78a" +``` + +Add: + +- `PersonalDashboardsController` to `Web/Types.hs`; +- `Web/Controller/PersonalDashboards.hs`; +- `Web/View/PersonalDashboards/Show.hs`; +- `Web/View/PersonalDashboards/Edit.hs` placeholder or minimal views; +- route registration in `Web/Routes.hs`; +- controller import and parser registration in `Web/FrontController.hs`; +- helper module `Application/Helper/PersonalDashboard.hs`. + +Implement: + +- `ensureDefaultDashboard :: User -> IO PersonalDashboard`; +- dashboard lookup scoped to current user; +- idempotent default seeding with six panel rows; +- linked `Widget` creation for each seeded panel; +- initial `WidgetVersion` creation for panel widgets. + +Entry criteria: + +- T01/T02 complete. + +Exit criteria: + +- Authenticated user can hit `PersonalDashboardAction`. +- First visit creates a default dashboard with six active panels. +- A second visit does not duplicate panels. +- Controller denies unauthenticated access through `ensureIsUser`. + +Verification: + +- Compile if environment is available. +- Add or run focused helper tests if existing test harness supports it. + +Rollback notes: + +- Remove route/controller/helper additions if skeleton must be reverted. +- Keep seeded widgets/events if any user interactions already happened. + +--- + +### T04 - Implement first three panel view models/renderers + +```task +id: IHUB-WP-0021-T04 +status: todo +priority: high +depends_on: T03 +state_hub_task_id: "012dcd2a-d3e0-48ba-966b-f4c7afa51dad" +``` + +Implement typed config decoding and view-model builders for: + +- `watched-hubs` +- `recent-interactions` +- `triage-queue` + +Requirements: + +- Query from controller/helper, not from HSX views. +- Clamp configured limits. +- Apply optional hub filters. +- Render empty states. +- Include source links: + - watched hub rows link to `ShowHubAction`; + - recent interaction rows link to widget or hub context where available; + - triage rows link to `ShowRequirementCandidateAction`. + +Entry criteria: + +- T03 controller/helper skeleton exists. + +Exit criteria: + +- The three panels render in the dashboard show action. +- Invalid config falls back to defaults and records a warning in the panel view + model. +- Queries are bounded. + +Verification: + +- Compile if environment is available. +- Manual smoke with empty DB and seeded fixture data if possible. + +Rollback notes: + +- Renderer additions are isolated to helper/view modules and can be reverted + without dropping schema. + +--- + +### T05 - Implement dashboard show view and responsive grid + +```task +id: IHUB-WP-0021-T05 +status: todo +priority: high +depends_on: T04 +state_hub_task_id: "b4c4de39-147b-45a0-954d-9bafad4aafa1" +``` + +Build the dashboard show view: + +- title and edit link; +- responsive grid; +- panel cards using row/col/span config; +- panel title fallback to panel type label; +- warning display for invalid config or unsupported panel; +- source link area; +- `widgetEnvelope` around every panel. + +Implementation notes: + +- Use current Tailwind and HSX conventions. +- Add a small CSS helper in `static/app.css` only if needed for responsive + collapse. +- Keep text compact and operational. + +Entry criteria: + +- At least three panel view models exist. + +Exit criteria: + +- Seeded dashboard is usable as an authenticated landing surface. +- Panels do not overlap at desktop or narrow widths. +- Panel layout persists in the order defined by `row`, `col`, and `sort_order`. + +Verification: + +- Compile if environment is available. +- Browser/manual smoke if a dev server is running. + +Rollback notes: + +- Show view changes can be reverted independently of schema/controller work. + +--- + +### T06 - Implement remaining first-slice panel view models/renderers + +```task +id: IHUB-WP-0021-T06 +status: todo +priority: high +depends_on: T05 +state_hub_task_id: "8d0bd046-17b3-48d9-a945-8b2e9c001123" +``` + +Implement: + +- `recent-decisions` +- `hub-health` +- `learning-digest` + +Requirements: + +- Recent decisions: bounded by time range and limit; link to + `ShowDecisionRecordAction`. +- Hub health: latest snapshot per hub plus active bottleneck count; use + aggregate count casting or `Int64`. +- Learning digest: recent `learning_insights` and + `institutional_knowledge_entries`; link to knowledge entries where possible. + +Entry criteria: + +- T05 show view can render panel view models. + +Exit criteria: + +- All six first-slice panels render. +- All panel queries are bounded. +- Empty states are sane for all six panels. + +Verification: + +- Compile if environment is available. +- `git diff --check`. + +Rollback notes: + +- Each renderer should be separable so a single broken panel can be reverted + without removing the framework. + +--- + +### T07 - Implement edit flow + +```task +id: IHUB-WP-0021-T07 +status: todo +priority: high +depends_on: T06 +state_hub_task_id: "51a72b56-5c23-4baa-892f-4ab89fd8495c" +``` + +Implement: + +- `EditPersonalDashboardAction`; +- `UpdatePersonalDashboardAction`; +- `AddDashboardPanelAction`; +- `UpdateDashboardPanelAction`; +- `RemoveDashboardPanelAction`. + +Edit view capabilities: + +- show existing panels in layout order; +- edit row, col, col span, row span, title, and sort order; +- edit supported config fields such as limit, time range, display mode, and + hub filter; +- add active panel type; +- remove panel. + +Entry criteria: + +- All six panels render on show view. + +Exit criteria: + +- User can modify layout and config through server-rendered forms. +- User can add a panel and remove a panel. +- Invalid layout/config re-renders edit view with an error. +- A user cannot edit another user's dashboard/panel. + +Verification: + +- Manual edit smoke. +- Focused authorization/config tests if available. + +Rollback notes: + +- If edit flow is unstable, keep show-only dashboard and disable edit links + until fixed. + +--- + +### T08 - Complete governed panel widget lifecycle + +```task +id: IHUB-WP-0021-T08 +status: todo +priority: high +depends_on: T07 +state_hub_task_id: "ca70b76d-766f-4e6e-84f1-19943c0a347c" +``` + +Harden widget lifecycle behavior: + +- create panel widget on panel add/seed; +- create initial `WidgetVersion` snapshot; +- create a new `WidgetVersion` snapshot when material panel config changes; +- render every panel through `widgetEnvelope`; +- preserve annotations/events when panels are removed; +- archive/deprecate linked widget on panel removal. + +Entry criteria: + +- Edit flow can add/remove/update panels. + +Exit criteria: + +- Every active dashboard panel has a linked active `Widget`. +- Every linked widget has non-empty `view_context`. +- Annotate link opens the existing widget annotation flow. +- Removing a panel does not delete widget history. + +Verification: + +- Manual smoke: add panel, annotate panel, remove panel, confirm widget/event + history is not deleted. +- Inspect generated HTML for expected `data-widget-*` attributes. + +Rollback notes: + +- Do not delete existing `widgets`, `annotations`, or `interaction_events`. + Disable dashboard rendering if needed while preserving history. + +--- + +### T09 - Redirect login and add navigation + +```task +id: IHUB-WP-0021-T09 +status: todo +priority: medium +depends_on: T08 +state_hub_task_id: "2fd1041b-9135-49e4-a9d1-e5d0b67d8fd7" +``` + +Update: + +- `Web/Controller/Sessions.hs` to redirect successful login to + `PersonalDashboardAction`; +- `Web/FrontController.hs` sidebar to include `Dashboard`; +- any relevant public page management links only if they should point to the + dashboard rather than Hubs. + +Do not change: + +- public root route; +- `LandingAction`; +- capabilities/tutorial/extension guide pages; +- `HubsAction` availability. + +Entry criteria: + +- Dashboard show route is stable and governed panel lifecycle is complete. + +Exit criteria: + +- Successful login lands on personal dashboard. +- Hubs remain reachable from sidebar. +- Public pages still render without login. + +Verification: + +- Manual login smoke. +- Route smoke for `/`, Hubs, dashboard, Learning, API Dashboard, Marketplace. + +Rollback notes: + +- If dashboard redirect fails, revert only the login redirect and keep + dashboard accessible from sidebar. + +--- + +### T10 - Add AutoRefresh and query hardening pass + +```task +id: IHUB-WP-0021-T10 +status: todo +priority: medium +depends_on: T09 +state_hub_task_id: "e19c06e7-ec95-40ca-8087-df669b575f86" +``` + +Wrap `PersonalDashboardAction` in `autoRefresh do` and audit all six panel +queries: + +- every query is bounded; +- optional hub filter is applied before broad fetches where practical; +- aggregate counts decode safely; +- no secrets are selected or displayed; +- dashboard refresh remains acceptable with default seed data. + +Entry criteria: + +- Dashboard route, renderers, edit flow, and login redirect exist. + +Exit criteria: + +- Dashboard updates using existing IHP AutoRefresh behavior. +- Query review notes are either captured in code comments or tests where useful. +- No known `COUNT(*)` as `Int` decode hazard remains in dashboard code. + +Verification: + +- Compile if environment is available. +- Manual refresh smoke by adding an interaction/candidate and observing the + dashboard update, when a dev DB is available. + +Rollback notes: + +- Remove `autoRefresh` wrapper if it causes unacceptable behavior; keep static + dashboard route. + +--- + +### T11 - Add focused tests + +```task +id: IHUB-WP-0021-T11 +status: todo +priority: medium +depends_on: T10 +state_hub_task_id: "32b4f55e-ede6-4830-a171-b0785afe88e1" +``` + +Add focused tests where the current harness supports them: + +- default dashboard seeding is idempotent; +- seeded dashboard has six active panels; +- each active panel has linked widget identity; +- config decoder clamps limits and rejects unknown values safely; +- remove action soft-removes panel and archives widget; +- users cannot edit another user's dashboard; +- aggregate counts in dashboard helpers decode safely. + +Entry criteria: + +- T10 implementation is stable enough to test. + +Exit criteria: + +- Relevant test files exist or a documented reason explains why the current + harness cannot cover a case. +- Tests pass where runnable in the local environment. + +Verification: + +- Run available test command. +- If unavailable, record exact blocker in this workplan before closing T11. + +Rollback notes: + +- Do not weaken production behavior to satisfy a brittle test; adjust the test + to match the intended FDD contract. + +--- + +### T12 - Manual smoke and closeout + +```task +id: IHUB-WP-0021-T12 +status: todo +priority: high +depends_on: T11 +state_hub_task_id: "8c6648ae-d33e-48f6-9d56-ee557f367d80" +``` + +Run a manual smoke pass: + +1. Log in as an existing admin user. +2. Confirm redirect lands on personal dashboard. +3. Confirm all six seeded panels render. +4. Click source links from watched hubs, triage queue, and learning digest. +5. Open Annotate for one panel. +6. Edit layout and save. +7. Sign out/in and confirm layout persists. +8. Add and remove a panel. +9. Confirm Hubs, Hub show, Ops Review, Federation, Learning, API Dashboard, + Hub Registry, and Marketplace still load. +10. Run `git diff --check`. + +Entry criteria: + +- T01 through T11 complete or have explicit accepted caveats. + +Exit criteria: + +- Smoke evidence is recorded in this workplan or a short docs/evidence note. +- WP-0021 tasks reflect final status. +- State Hub progress note is logged. +- Operator is reminded to run `make fix-consistency REPO=inter-hub` from + `~/state-hub` after workplan/status changes. + +Rollback notes: + +- If smoke fails after login redirect, first rollback is reverting the login + redirect while keeping dashboard route available for debugging. + +## Workplan Exit Criteria + +- Personal dashboard schema, seeds, controller, views, helper, and route are + implemented. +- Successful login reaches the personal dashboard. +- Default dashboard seeding is idempotent. +- Six first-slice panels render with bounded queries. +- Panel edit flow works. +- Every panel has governed widget identity. +- Existing source dashboards remain functional. +- Checks/smoke evidence is recorded.