# Personal Dashboard Current-State Research **Workplan:** IHUB-WP-0020 **Date:** 2026-06-16 **Status:** Research deliverable for T01 ## Purpose This note reviews the current inter-hub implementation before designing a personal dashboard framework. The main finding is that inter-hub already has a large set of server-rendered dashboard surfaces, governed widget identity, type registries, annotations, event capture, hub health, API usage, marketplace, and learning data. The personal dashboard should compose these capabilities instead of inventing a separate dashboard product. External dashboard products are used here only for pattern extraction. The implementation direction remains IHP, HSX, Tailwind, server-rendered forms, and existing IHF governance primitives. ## Evidence Reviewed Repo files inspected for this note: - `Web/Controller/Hubs.hs` - `Web/Controller/Sessions.hs` - `Web/FrontController.hs` - `Web/Routes.hs` - `Web/Types.hs` - `Application/Schema.sql` - `Application/Helper/View.hs` - `Web/Controller/FederatedGovernance.hs` - `Web/Controller/FederatedPolicyOverlays.hs` - `Web/Controller/ApiDashboard.hs` - `Web/Controller/MarketplaceDashboard.hs` - `Web/Controller/LearningDashboard.hs` - `docs/phase1-summary.md` through `docs/phase8-summary.md` - `docs/ihp-ihf-mapping.md` - `docs/widget-envelope-convention.md` - Workplans IHUB-WP-0001 through IHUB-WP-0015 where dashboard scope was introduced. ## Current Authenticated Entry Point The public root and documentation pages already exist through IHUB-WP-0015 and are registered last in `Web.FrontController`. The authenticated login flow still redirects to `HubsAction`: ```haskell login user redirectTo HubsAction ``` `HubsAction` renders a table of hubs. It is useful as an admin list, but it is not a personal daily operating surface. The sidebar already links to several platform surfaces, including Hubs, Learning, Ops Review, Federation, API Dashboard, Hub Registry, and Marketplace. The personal dashboard should therefore become a new authenticated landing route and a sidebar entry, while public root behavior remains unchanged. ## Existing Dashboard Inventory | Surface | Action | Scope | Live? | Reuse potential | |---|---|---|---|---| | Hub list | `HubsAction` | Global hub table | No | Source for watched hubs and hub selector | | Hub show | `ShowHubAction` | One hub | Yes | Recent events, annotations, widgets, manifest summary | | Triage dashboard | `TriageDashboardAction` | One hub | Yes | Open candidates, recent escalations, annotation breakdown | | Governance dashboard | `GovernanceDashboardAction` | One hub | Yes | Accepted candidates, requirements, decisions, traceability | | Antifragility dashboard | `AntifragilityDashboardAction` | One hub | Yes | Deployments, outcome signals, recurrence leaderboard | | Agent audit dashboard | `AgentAuditDashboardAction` | One hub context, global data | Yes | Agent proposals and review status | | Adapter compatibility | `AdapterCompatibilityDashboardAction` | One hub | Yes | Adapter and contract compatibility panels | | Friction heatmap | `FrictionHeatmapAction` | One hub | Yes | Widget friction summary | | Bottleneck dashboard | `BottleneckDashboardAction` | One hub | Yes | Open bottlenecks | | Hub health history | `HubHealthHistoryAction` | One hub | Yes | Health snapshot trend | | Operational review board | `OperationalReviewBoardAction` | Global | Yes | Hub health, top friction, bottlenecks, propagations | | Federated governance | `FederatedGovernanceDashboardAction` | Global | Yes | Ownership, routing, policy, stewardship, archive activity | | Policy compliance | `PolicyComplianceDashboardAction` | Global | Yes | Active overlays and policy reference coverage | | API dashboard | `ShowApiDashboardAction` | Global API consumers | Yes | Per-consumer request volume, error rate, last seen | | Marketplace | `MarketplaceDashboardAction` | Global patterns/templates | Yes | Trending patterns, search/filter catalogue | | Learning dashboard | `LearningDashboardAction` | Global learning memory | Yes | Insights, knowledge highlights, pattern rankings | The reusable unit today is not a standalone panel renderer. It is a controller query plus an HSX view fragment. WP-0021 should extract only the first small set of renderers needed for the personal dashboard. A broad refactor of existing dashboards is explicitly unnecessary for the first slice. ## AutoRefresh and Query Patterns Most dashboard actions wrap the whole action with `autoRefresh do`. This is simple and consistent with the existing IHP style. The current app does not have a reusable per-panel refresh abstraction. Useful bounded patterns already exist: - `ShowHubAction` limits recent interaction events to 50 and annotations to 20. - `GovernanceDashboardAction` limits recent decisions to 20. - `LearningDashboardAction` limits correlations, rankings, insights, and knowledge highlights. - `MarketplaceDashboardAction` limits published patterns/templates and casts the trending adoption count to integer. Risky or broad patterns to avoid copying directly: - Some hub dashboards fetch all related records for a hub and filter in memory. That is acceptable for small scoped screens, but a personal dashboard should bound every panel query by hub, time, status, and limit. - Several global dashboards fetch all hubs or all decision records. A personal view should either limit these or explicitly display only summarized rows. - Raw `COUNT(*)` queries should cast to `integer` or decode as `Int64`. Recent production work exposed PostgreSQL/Haskell decode failures when `COUNT(*)` was decoded as `Int`. Recommendation: first implementation can wrap the whole personal dashboard action in `autoRefresh do`. The FDD should leave finer-grained panel refresh as a later optimization unless a simple route-level fragment pattern emerges. ## Governed Widget and Annotation Constraints `Application.Helper.View.widgetEnvelope` wraps a `Widget` record and injects governance metadata: - `data-widget-id` - `data-widget-type` - `data-hub-id` - `data-capability-ref` - `data-view-context` - `data-policy-scope` - `data-widget-version` It also renders an Annotate link to the widget annotation view. The helper warns when `view_context` is absent. Therefore, a saved personal dashboard panel must not be treated as transient markup. It needs stable widget identity. The registry seed includes a framework-level widget type named `panel`. That is the best first choice for saved dashboard panels. A saved panel instance should create or reference a `widgets` row with: - `widget_type = 'panel'` - `capability_ref = 'personal-dashboard.'` - `view_context = 'personal-dashboard/'` - `policy_scope = 'internal'` - `status = 'active'` The FDD should decide the owning hub rule. Recommended first slice: use the framework hub for personal dashboard panel widgets and store source hub filters in panel config. This keeps the personal dashboard itself governed by inter-hub while still letting panels point at hub-specific data. ## Schema Available for First-Slice Panels Existing tables with direct panel value: - `hubs` - `widgets` - `widget_versions` - `interaction_events` - `annotations` - `annotation_threads` - `requirement_candidates` - `triage_states` - `reviewer_assignments` - `requirements` - `decision_records` - `deployment_records` - `outcome_signals` - `friction_scores` - `bottleneck_records` - `hub_health_snapshots` - `cross_hub_propagations` - `widget_ownerships` - `hub_routing_rules` - `federated_policy_overlays` - `stewardship_roles` - `archive_records` - `widget_type_registry` - `event_type_registry` - `annotation_category_registry` - `policy_scope_registry` - `hub_capability_manifests` - `api_consumers` - `api_request_log` - `widget_patterns` - `pattern_adoptions` - `governance_templates` - `governance_template_clones` - `outcome_correlations` - `pattern_performance_records` - `adaptive_threshold_configs` - `institutional_knowledge_entries` - `learning_insights` Important gaps: - No `personal_dashboards` table. - No `dashboard_panel_types` table. - No `dashboard_panels` table. - No saved watched-hub set or user preference table. - No user role column. - No panel config decoder/validator. - No dedicated panel renderer module. - No explicit default dashboard seeding helper. ## User and Role Model Findings The `users` table has email, password hash, name, lockout fields, and created time. It has no role. `stewardship_roles` stores `assigned_to` as text and is hub-scoped. That can help infer operator relevance, but it is not a reliable role foreign key. Recommendation: do not add `users.role` for the first slice. Seed a neutral default dashboard for all authenticated users, then allow the user to edit panel layout and filters. If a default needs hub relevance, match active `stewardship_roles.assigned_to` against user email or name as a best-effort hint, not as an authorization rule. ## First-Slice Panel Candidates The following panels are practical without broad refactoring: | Panel key | Source | Default filter | Notes | |---|---|---|---| | `watched-hubs` | `hubs`, latest `hub_health_snapshots` | all hubs, limit 12 | First panel can be neutral until watched hubs exist | | `recent-interactions` | `interaction_events`, `widgets`, `hubs` | last 24h, limit 25 | Existing indexes support recent ordering | | `triage-queue` | `requirement_candidates` | `status = 'open'`, limit 10 | Can join source widget/hub for context | | `recent-decisions` | `decision_records` | last 30 days, limit 10 | Good governance reviewer entry point | | `hub-health` | `hub_health_snapshots`, `bottleneck_records` | latest per hub, limit 12 | Needs bounded latest-per-hub query | | `learning-digest` | `learning_insights`, `institutional_knowledge_entries` | latest, limit 5/5 | Already bounded in existing dashboard | Panels to defer until after the framework is proven: - `agent-proposals` - `api-usage` - `marketplace-trending` - `my-annotations` - `adapter-compatibility` - `policy-compliance` The deferred panels are valuable, but they are not needed to prove dashboard persistence, layout, panel renderer dispatch, and governed panel identity. ## External Pattern Extraction | System | Useful pattern | Translation for inter-hub | |---|---|---| | Grafana | Dashboard as saved grid of panels with variables and refresh behavior | Save panel rows plus hub/time filters; keep server-rendered refresh | | Kibana dashboards | Saved searches and time range awareness | Treat panel query config as explicit, bounded, validated config | | Retool/Appsmith | Widget catalogue and data binding | Use a server-side panel catalogue; avoid client runtime/data binding | | Linear home | Personal "my work" aggregation across entities | Make the personal dashboard a daily work queue, not a clone of every dashboard | | Notion linked databases | Multiple saved views over the same records | Let panels define filter/sort/display options against existing tables | | Metabase | Question as a governed reusable unit | Treat panel renderer plus validated config as the reusable unit | | Streamlit | Simple declarative layout vocabulary | Use predictable grid rows/spans and forms rather than drag-and-drop | The key pattern across these systems is not visual complexity. It is that a dashboard is a saved composition of bounded questions/panels with explicit parameters. For inter-hub, those questions must remain governed IHF widgets. ## Answers to WP-0020 Research Questions ### Which existing fragments can become first-slice renderers? Good first-slice candidates: - Recent activity from `ShowHubAction`. - Open candidate queue from `TriageDashboardAction`. - Recent decisions from `GovernanceDashboardAction`. - Latest hub health from `OperationalReviewBoardAction` and `HubHealthHistoryAction`. - Learning digest from `LearningDashboardAction`. - Watched hubs from `HubsAction` plus latest health snapshots. These can be implemented as new renderer functions that reuse the same model queries and link to existing source dashboards for detail. ### Which configs are needed on day one? Recommended day-one config options: - `hubIds :: [Id Hub]` or `hubFilter :: Maybe [Id Hub]` - `timeRange :: Last24Hours | Last7Days | Last30Days | AllTimeBounded` - `limit :: Int` - `sort :: NewestFirst | OldestFirst | HighestRiskFirst` - `displayMode :: Compact | Detailed` Config should be stored as JSONB but decoded into a Haskell ADT before use. Invalid config should fall back to panel defaults and surface a non-fatal operator warning. ### Which panels should live-refresh? Live-refresh in the first slice: - `recent-interactions` - `triage-queue` - `hub-health` - `learning-digest` Static per request in the first slice: - `watched-hubs` - `recent-decisions` If the first implementation wraps the entire personal dashboard in `autoRefresh`, all panels will refresh together. That is acceptable initially if queries are bounded. ### How should saved panels map to governed widgets? Each saved dashboard panel should own a `widgets` row and store the id on `dashboard_panels.widget_id`. The panel renderer should call `widgetEnvelope` with that widget. This gives stable annotation and interaction capture identity. Panel lifecycle: 1. User adds a panel. 2. Controller creates `dashboard_panels` row. 3. Controller creates linked `widgets` row with `widget_type = 'panel'`. 4. Controller creates a `widget_versions` snapshot for the panel widget. 5. Show view renders the panel through `widgetEnvelope`. 6. Removing a panel should mark the widget archived or deprecated, not delete interaction history. ### What should be deferred? Defer: - Drag-and-drop layout. - Shared dashboards. - Team dashboards. - External datasource connectors. - Client-side data fetching. - Per-panel WebSocket channels. - Full refactor of existing dashboard views. - Complex role model. - Dashboard marketplace/templates beyond one seeded default. ## Recommendations for T02/T03 1. Define the personal dashboard as the authenticated landing page, not a replacement for existing source dashboards. 2. Use a small panel catalogue for the first implementation. 3. Persist dashboard/panel rows in relational tables and panel config in JSONB. 4. Decode panel config into explicit Haskell ADTs before querying. 5. Give every saved panel stable `Widget` identity. 6. Use the existing `panel` widget type. 7. Keep the default dashboard neutral and editable. 8. Bound every panel query. 9. Cast SQL aggregate counts to integer when decoding as `Int`. 10. Keep implementation tasks small enough to avoid a cross-dashboard refactor.