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>
This commit is contained in:
2026-03-28 23:37:34 +00:00
parent cfcf4c81f7
commit 840b0e5c7b
25 changed files with 2136 additions and 29 deletions

View File

@@ -0,0 +1,65 @@
module Web.Controller.AnnotationThreads where
import Web.Types
import Web.View.AnnotationThreads.Index
import Web.View.AnnotationThreads.New
import Web.View.AnnotationThreads.Show
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
instance Controller AnnotationThreadsController where
beforeAction = ensureIsUser
action WidgetAnnotationThreadsAction { widgetId } = do
widget <- fetch widgetId
threads <- query @AnnotationThread
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #createdAt
|> fetch
-- Fetch annotation counts per thread
allAnnotations <- query @Annotation
|> filterWhere (#widgetId, widgetId)
|> fetch
render IndexView { widget, threads, allAnnotations }
action ShowAnnotationThreadAction { annotationThreadId } = do
thread <- fetch annotationThreadId
widget <- fetch thread.widgetId
annotations <- query @Annotation
|> filterWhere (#threadId, Just annotationThreadId)
|> orderByAsc #createdAt
|> fetch
render ShowView { widget, thread, annotations }
action NewAnnotationThreadAction { widgetId } = do
widget <- fetch widgetId
let thread = newRecord @AnnotationThread
render NewView { widget, thread }
action CreateAnnotationThreadAction { widgetId } = do
widget <- fetch widgetId
mUser <- currentUserOrNothing
let createdBy = fmap (.id) mUser
let thread = newRecord @AnnotationThread
thread
|> fill @'["title", "description"]
|> set #widgetId widgetId
|> set #createdBy (fmap (Id . unId) createdBy)
|> validateField #title nonEmpty
|> ifValid \case
Left thread -> render NewView { widget, thread }
Right thread -> do
createRecord thread
setSuccessMessage "Thread created"
redirectTo WidgetAnnotationThreadsAction { widgetId }
action AssignAnnotationToThreadAction { annotationId } = do
annotation <- fetch annotationId
threadId <- param @(Id AnnotationThread) "threadId"
annotation
|> set #threadId (Just threadId)
|> updateRecord
setSuccessMessage "Annotation added to thread"
redirectTo ShowAnnotationThreadAction { annotationThreadId = threadId }