generated from coulomb/repo-seed
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.
610 lines
18 KiB
Markdown
610 lines
18 KiB
Markdown
# 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.
|