generated from coulomb/repo-seed
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.
This commit is contained in:
609
docs/fdd/personal-dashboard-fdd.md
Normal file
609
docs/fdd/personal-dashboard-fdd.md
Normal file
@@ -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/<timestamp>-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.
|
||||
340
docs/prs/personal-dashboard-prs.md
Normal file
340
docs/prs/personal-dashboard-prs.md
Normal file
@@ -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.
|
||||
348
docs/research/personal-dashboard-current-state.md
Normal file
348
docs/research/personal-dashboard-current-state.md
Normal file
@@ -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.<panel-key>'`
|
||||
- `view_context = 'personal-dashboard/<panel-key>'`
|
||||
- `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.
|
||||
Reference in New Issue
Block a user