Files
inter-hub/Web/Controller/Hubs.hs
Bernd Worsch 840b0e5c7b feat(P2+P3): IHF Phase 2 complete; register Phase 3 workplan
Phase 2 — Structured Feedback and Triage (IHUB-WP-0002):
- Schema: annotation_threads, requirement_candidates, triage_states,
  reviewer_assignments; annotations extended with severity + thread_id
- AnnotationThreadsController: create threads, assign annotations
- RequirementCandidatesController: CRUD, escalation, triage lifecycle,
  reviewer assignment, my-queue
- Annotation severity (low/medium/high/critical) with Tailwind color cues
- TriageDashboardAction on HubsController with autoRefresh
- Integration tests (T01–T09), SCOPE.md updated, docs/phase2-summary.md

Phase 3 — Governance and Decision Linkage (IHUB-WP-0003):
- Workplan registered: 9 tasks, State Hub workstream 5f201ee3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:42:56 +00:00

113 lines
3.7 KiB
Haskell

module Web.Controller.Hubs where
import Web.Types
import Web.View.Hubs.Index
import Web.View.Hubs.Show
import Web.View.Hubs.New
import Web.View.Hubs.Edit
import Web.View.Hubs.TriageDashboard
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
instance Controller HubsController where
beforeAction = ensureIsUser
action HubsAction = do
hubs <- query @Hub |> orderByAsc #createdAt |> fetch
render IndexView { hubs }
action NewHubAction = do
let hub = newRecord @Hub
render NewView { hub }
action ShowHubAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget
|> filterWhere (#hubId, hubId)
|> orderByAsc #name
|> fetch
widgetIds <- pure (map (.id) widgets)
recentEvents <- sqlQuery
"SELECT * FROM interaction_events WHERE widget_id = ANY(?) ORDER BY occurred_at DESC LIMIT 50"
(Only (PGArray widgetIds))
recentAnnotations <- sqlQuery
"SELECT * FROM annotations WHERE widget_id = ANY(?) ORDER BY created_at DESC LIMIT 20"
(Only (PGArray widgetIds))
render ShowView { hub, widgets, recentEvents, recentAnnotations }
action CreateHubAction = do
let hub = newRecord @Hub
hub
|> fill @'["slug", "name", "domain"]
|> validateField #slug nonEmpty
|> validateField #name nonEmpty
|> validateField #domain nonEmpty
|> ifValid \case
Left hub -> render NewView { hub }
Right hub -> do
hub <- createRecord hub
setSuccessMessage "Hub created"
redirectTo ShowHubAction { hubId = hub.id }
action EditHubAction { hubId } = do
hub <- fetch hubId
render EditView { hub }
action UpdateHubAction { hubId } = do
hub <- fetch hubId
hub
|> fill @'["slug", "name", "domain"]
|> validateField #slug nonEmpty
|> validateField #name nonEmpty
|> validateField #domain nonEmpty
|> ifValid \case
Left hub -> render EditView { hub }
Right hub -> do
updateRecord hub
setSuccessMessage "Hub updated"
redirectTo ShowHubAction { hubId = hub.id }
action DeleteHubAction { hubId } = do
hub <- fetch hubId
deleteRecord hub
setSuccessMessage "Hub deleted"
redirectTo HubsAction
action TriageDashboardAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget
|> filterWhere (#hubId, hubId)
|> fetch
let widgetIds = map (.id) widgets
-- All candidates for this hub's widgets
allCandidates <- query @RequirementCandidate
|> filterWhereIn (#sourceWidgetId, widgetIds)
|> orderByAsc #createdAt
|> fetch
-- Triage queue: open candidates, oldest first
let triageQueue = filter (\c -> c.status == "open") allCandidates
-- Recent escalations: last 20
recentEscalations <- query @RequirementCandidate
|> filterWhereIn (#sourceWidgetId, widgetIds)
|> orderByDesc #createdAt
|> limit 20
|> fetch
-- All annotations for category breakdown
allAnnotations <- query @Annotation
|> filterWhereIn (#widgetId, widgetIds)
|> fetch
render TriageDashboardView
{ hub
, widgets
, allCandidates
, triageQueue
, recentEscalations
, allAnnotations
}