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:
2026-06-21 16:11:37 +02:00
parent a8292b639b
commit d6b655a5cf
5 changed files with 2227 additions and 191 deletions

View 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.

View 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.

View 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.

View File

@@ -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; T01T12 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.

View File

@@ -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/<timestamp>-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.