diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aaffd44 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# inter-hub environment variables +# Copy to .env and fill in real values before running devenv up. + +# IHP session encryption key — generate with: openssl rand -base64 64 +IHP_SESSION_SECRET=CHANGE_ME_generate_with_openssl_rand_base64_64 + +# PostgreSQL connection (devenv manages this automatically in local dev) +DATABASE_URL=postgresql://localhost/inter-hub?sslmode=disable + +# External base URL for link generation +IHP_BASEURL=http://localhost:8000 + +# Anthropic API key for Phase 5 agent-assisted distillation (Phase 5+) +IHP_ANTHROPIC_API_KEY=sk-ant-CHANGE_ME + +# Default admin credentials (seeded by migration 1744416000-seed-admin-user.sql) +# Email: admin@inter-hub.local +# Password: admin1234! +# IMPORTANT: Change this password immediately after first login. diff --git a/Application/Helper/AgentBridge.hs b/Application/Helper/AgentBridge.hs index 33cc1a4..35547dd 100644 --- a/Application/Helper/AgentBridge.hs +++ b/Application/Helper/AgentBridge.hs @@ -8,10 +8,13 @@ import IHP.Prelude import IHP.ControllerPrelude import Data.Aeson (object, (.=), encode, decode, Value, FromJSON(..), (.:), (.:?)) import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Aeson.Key as AK import qualified Data.ByteString.Lazy as LBS import System.Process (readProcessWithExitCode) import System.Exit (ExitCode(..)) import Generated.Types +import Web.Routes () -- --------------------------------------------------------------------------- -- Request / response types @@ -167,7 +170,7 @@ callBridgeBatch reqs = do readProcessWithExitCode "python3" ["scripts/llm_bridge.py"] (cs payload) let outBytes = LBS.fromStrict (cs stdout) case A.decode @A.Value outBytes of - Just (A.Object o) | Just (A.Array arr) <- A.lookup "results" o -> + Just (A.Object o) | Just (A.Array arr) <- KM.lookup (AK.fromString "results") o -> pure $ map parseResult (toList arr) _ -> pure $ replicate (length reqs) (Left (BridgeError "Unparseable batch output" "ParseError")) diff --git a/Application/Helper/ApiRateLimit.hs b/Application/Helper/ApiRateLimit.hs index 6b65c0f..941570a 100644 --- a/Application/Helper/ApiRateLimit.hs +++ b/Application/Helper/ApiRateLimit.hs @@ -7,6 +7,7 @@ import Generated.Types import IHP.Prelude import IHP.ModelSupport import IHP.ControllerPrelude +import Web.Routes () import Data.Aeson (object, (.=)) import Database.PostgreSQL.Simple (Only(..)) import Web.Controller.Api.V2.Auth (respondWithStatus) diff --git a/Application/Helper/BottleneckDetector.hs b/Application/Helper/BottleneckDetector.hs index c11201e..f1df231 100644 --- a/Application/Helper/BottleneckDetector.hs +++ b/Application/Helper/BottleneckDetector.hs @@ -2,8 +2,12 @@ module Application.Helper.BottleneckDetector where import IHP.Prelude import IHP.ModelSupport +import IHP.QueryBuilder +import IHP.Fetch import Generated.Types +import Web.Routes () import Data.Time.Clock (addUTCTime, getCurrentTime, NominalDiffTime) +import Database.PostgreSQL.Simple (Only(..)) -- | Severity based on how much older than the threshold the record is. staleSeverity :: NominalDiffTime -> NominalDiffTime -> Text @@ -97,5 +101,3 @@ detectBottlenecks hubId hubWidgets candidates requirements decisions deployments pure (r1 <> r2 <> r3 <> r4) -diffUTCTime :: UTCTime -> UTCTime -> NominalDiffTime -diffUTCTime a b = realToFrac (a `Data.Time.Clock.diffUTCTime` b) diff --git a/Application/Helper/Controller.hs b/Application/Helper/Controller.hs index 73555ed..0a242b7 100644 --- a/Application/Helper/Controller.hs +++ b/Application/Helper/Controller.hs @@ -2,6 +2,7 @@ module Application.Helper.Controller where import IHP.ControllerPrelude import Generated.Types +import Web.Routes () import Data.Time.Clock (addUTCTime) import Data.List (sortBy) diff --git a/Application/Helper/CorrelationEngine.hs b/Application/Helper/CorrelationEngine.hs index bd2ce2c..4fc15ad 100644 --- a/Application/Helper/CorrelationEngine.hs +++ b/Application/Helper/CorrelationEngine.hs @@ -3,7 +3,8 @@ module Application.Helper.CorrelationEngine where import IHP.Prelude import Generated.Types import IHP.ModelSupport (sqlQuery) -import Database.PostgreSQL.Simple (Only(..)) +import Web.Routes () +import Database.PostgreSQL.Simple (Only(..), (:.)(..)) -- | For a hub, compute the correlation score per annotation category: -- fraction of traceability chains ending in a positive outcome signal @@ -28,4 +29,4 @@ computeAnnotationCorrelations hubId = \ WHERE w.hub_id = ? \ \ GROUP BY a.category \ \ ORDER BY score DESC" - [hubId] + (Only hubId) diff --git a/Application/Helper/CrossHubPropagation.hs b/Application/Helper/CrossHubPropagation.hs index 6aff9cc..a94a526 100644 --- a/Application/Helper/CrossHubPropagation.hs +++ b/Application/Helper/CrossHubPropagation.hs @@ -2,7 +2,10 @@ module Application.Helper.CrossHubPropagation where import IHP.Prelude import IHP.ModelSupport +import IHP.QueryBuilder +import IHP.Fetch import Generated.Types +import Web.Routes () import Data.Time.Clock (addUTCTime, getCurrentTime) import Data.Aeson (toJSON) import qualified Data.List as List diff --git a/Application/Helper/FrictionScore.hs b/Application/Helper/FrictionScore.hs index b7f1e71..94d87f7 100644 --- a/Application/Helper/FrictionScore.hs +++ b/Application/Helper/FrictionScore.hs @@ -2,7 +2,11 @@ module Application.Helper.FrictionScore where import IHP.Prelude import IHP.ModelSupport +import IHP.QueryBuilder +import IHP.Fetch import Generated.Types +import Web.Routes () +import Database.PostgreSQL.Simple (Only(..)) import Data.Time.Clock (addUTCTime, getCurrentTime) import qualified Data.Aeson as A import qualified Data.HashMap.Strict as H diff --git a/Application/Helper/HubHealth.hs b/Application/Helper/HubHealth.hs index 2e8a836..8b9325d 100644 --- a/Application/Helper/HubHealth.hs +++ b/Application/Helper/HubHealth.hs @@ -3,6 +3,7 @@ module Application.Helper.HubHealth where import IHP.Prelude import IHP.ModelSupport import Generated.Types +import Web.Routes () import Data.Time.Clock (addUTCTime, getCurrentTime) -- | Health score deduction table (documented): @@ -50,7 +51,7 @@ computeHubHealth hubId widgets candidates decisions deployments signals annotati score = max 0 (100 - deductions) newRecord @HubHealthSnapshot - |> set #hubId hubId + |> set #hubId (toUUID hubId) |> set #healthScore score |> set #openCandidates openCount |> set #regressedWidgets regCount diff --git a/Application/Helper/ModelRouter.hs b/Application/Helper/ModelRouter.hs index bf14eea..c9d6403 100644 --- a/Application/Helper/ModelRouter.hs +++ b/Application/Helper/ModelRouter.hs @@ -6,6 +6,7 @@ module Application.Helper.ModelRouter where import IHP.Prelude import IHP.ControllerPrelude import Generated.Types +import Web.Routes () import Database.PostgreSQL.Simple (Only(..)) -- | Resolve the highest-priority active AgentRegistration for the given hub diff --git a/Application/Helper/RoutingEngine.hs b/Application/Helper/RoutingEngine.hs index 18757a9..f84720e 100644 --- a/Application/Helper/RoutingEngine.hs +++ b/Application/Helper/RoutingEngine.hs @@ -2,7 +2,10 @@ module Application.Helper.RoutingEngine where import IHP.Prelude import IHP.ModelSupport +import IHP.QueryBuilder +import IHP.Fetch import Generated.Types +import Web.Routes () -- | Apply active routing rules to a RequirementCandidate. -- Finds the highest-priority matching active rule for the candidate's hub diff --git a/Application/Helper/TypeRegistry.hs b/Application/Helper/TypeRegistry.hs index 29302a3..221d95b 100644 --- a/Application/Helper/TypeRegistry.hs +++ b/Application/Helper/TypeRegistry.hs @@ -3,6 +3,7 @@ module Application.Helper.TypeRegistry where import IHP.Prelude import IHP.ModelSupport import Generated.Types +import Web.Routes () import Database.PostgreSQL.Simple (Only(..)) -- | Validate that a type name exists in widget_type_registry with status='active'. diff --git a/Application/Helper/View.hs b/Application/Helper/View.hs index ef5ad3e..8368bcf 100644 --- a/Application/Helper/View.hs +++ b/Application/Helper/View.hs @@ -3,6 +3,15 @@ module Application.Helper.View where import IHP.ViewPrelude import Generated.Types import Web.Types +import Web.Routes () +import IHP.View.Form.Select (CanSelect(..)) + +-- | CanSelect instance for (Text, Text) tuples where fst is the label +-- and snd is the value. Used by selectField when options are plain text pairs. +instance CanSelect (Text, Text) where + type SelectValue (Text, Text) = Text + selectLabel = fst + selectValue = snd -- | Widget Envelope — wraps any widget's rendered content with IHF governance metadata. -- @@ -44,7 +53,7 @@ widgetEnvelope widget inner = {renderEnvelopeWarnings warnings} {inner}
- {case mParentProposal of - Nothing -> mempty - Just p -> [hsx| -Parent Proposal
-{p.proposalType} — {p.status}
-{show r}
- Parent Proposal
+{p.proposalType} — {p.status}
+{show r}
+ No proposals found.
|] - else renderTable proposals widgets} + {if null proposals then noProposalsMsg else renderTable proposals widgets} |] +noProposalsMsg :: Html +noProposalsMsg = [hsx|No proposals found.
|] + +renderTypeTab :: Maybe Text -> Maybe Text -> Text -> Html +renderTypeTab mStatusFilter mTypeFilter t = [hsx| + {t} +|] + +renderStatusTab :: Maybe Text -> Maybe Text -> Text -> Html +renderStatusTab mTypeFilter mStatusFilter s = [hsx| + {s} +|] + agentProposalsUrl :: Maybe Text -> Maybe Text -> Text agentProposalsUrl mt ms = let parts = catMaybes @@ -83,7 +91,7 @@ renderRow :: [Widget] -> AgentProposal -> Html renderRow widgets p = [hsx|{e}
|]) c.explanation} + {maybe mempty renderConfExplanation c.explanation}{n}
|]) review.notes} + {maybe mempty renderReviewNote review.notes} |] @@ -119,7 +124,7 @@ renderReviewForm pid status class="w-full border border-gray-300 rounded px-3 py-2 text-sm">Mean confidence: —
|] +renderMeanConfidence (Just c) = [hsx|Mean confidence: {printf "%.2f" c :: String}
|] + +renderPolicyRow :: ModelRoutingPolicy -> Html +renderPolicyRow p = [hsx| +No routing policies. Add one.
|] + +noProposalsMsg :: Html +noProposalsMsg = [hsx|No proposals yet.
|] + +renderProposalRow :: AgentProposal -> Html +renderProposalRow p = [hsx| +Acceptance rate
Mean confidence: —
|] - Just c -> [hsx|Mean confidence: {printf "%.2f" c :: String}
|] - } + {renderMeanConfidence p.meanConfidence} |] diff --git a/Web/View/AiGovernancePolicies/Index.hs b/Web/View/AiGovernancePolicies/Index.hs index 936270a..655486a 100644 --- a/Web/View/AiGovernancePolicies/Index.hs +++ b/Web/View/AiGovernancePolicies/Index.hs @@ -1,6 +1,6 @@ module Web.View.AiGovernancePolicies.Index where -import Web.View.Prelude +import IHP.ViewPrelude data IndexView = IndexView { policies :: ![AiGovernancePolicy] @@ -48,9 +48,7 @@ instance View IndexView whereNo threads yet.
|] - else [hsx| -No threads yet.
|] +renderThreadsSection threads allAnnotations = [hsx| +{d}
|]) t.description} + {maybe mempty renderThreadDesc t.description}{d}
|] + buildSeverityBreakdown :: [Annotation] -> [(Text, Int)] buildSeverityBreakdown annotations = [ ("low", length $ filter (\a -> a.severity == "low") annotations) diff --git a/Web/View/AnnotationThreads/New.hs b/Web/View/AnnotationThreads/New.hs index 56a9cd3..468617c 100644 --- a/Web/View/AnnotationThreads/New.hs +++ b/Web/View/AnnotationThreads/New.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () data NewView = NewView { widget :: !Widget @@ -13,9 +14,9 @@ data NewView = NewView instance View NewView where html NewView { .. } = [hsx| diff --git a/Web/View/AnnotationThreads/Show.hs b/Web/View/AnnotationThreads/Show.hs index 757516f..5b2f4fc 100644 --- a/Web/View/AnnotationThreads/Show.hs +++ b/Web/View/AnnotationThreads/Show.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () data ShowView = ShowView { widget :: !Widget @@ -14,9 +15,9 @@ data ShowView = ShowView instance View ShowView where html ShowView { .. } = [hsx| @@ -24,7 +25,7 @@ instance View ShowView where{d}
|]) thread.description} + {maybe mempty renderThreadDesc thread.description}{d}
|] + barColor :: Text -> Text barColor "low" = "bg-gray-300" barColor "medium" = "bg-blue-400" diff --git a/Web/View/Annotations/Index.hs b/Web/View/Annotations/Index.hs index 1a96fca..06bb45f 100644 --- a/Web/View/Annotations/Index.hs +++ b/Web/View/Annotations/Index.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () data IndexView = IndexView { widget :: !Widget @@ -11,18 +12,21 @@ data IndexView = IndexView } instance View IndexView where - html IndexView { .. } = [hsx| + html IndexView { .. } = + let rootAnnotations = filter (\a -> isNothing a.parentId) annotations + childrenOf parent = filter (\a -> a.parentId == Just parent.id) annotations + in [hsx|{a.body}
{annotation.body}
@@ -50,8 +49,7 @@ instance View ShowView where renderEscalation :: Annotation -> Maybe RequirementCandidate -> Html renderEscalation annotation Nothing = [hsx|This annotation has not been escalated yet.
-No pending proposals.
|] +renderPendingQueue pending = [hsx| +All decisions with impl refs have deployments.
|] - else [hsx| -No deployments yet.
|] - else [hsx| -| Version | -Decision | -Signals | -Eval | -Deployed | -
|---|
No recurring widgets detected.
|] - else [hsx| -| Widget | -Cycles | -
|---|
All decisions with impl refs have deployments.
|] +renderOpenGaps gaps = [hsx| +No deployments yet.
|] +renderRecentDeploysSection deploys decisions signals evals = [hsx| +| Version | +Decision | +Signals | +Eval | +Deployed | +
|---|
No recurring widgets detected.
|] +renderRecurrenceSection leaderboard widgets = [hsx| +| Widget | +Cycles | +
|---|
{hub.name}
No active bottlenecks detected.
|] - else mempty} + {if null bottlenecks then noBottlenecksMsg else mempty} |] where stages = ["candidate", "requirement", "decision", "observation"] :: [Text] @@ -83,12 +82,15 @@ instance View BottleneckDashboardView whereNo active bottlenecks detected.
|] + severityBadge :: Text -> Text severityBadge s = case s of "critical" -> "bg-red-100 text-red-800" diff --git a/Web/View/Hubs/Edit.hs b/Web/View/Hubs/Edit.hs index e78cdf0..4c42e8b 100644 --- a/Web/View/Hubs/Edit.hs +++ b/Web/View/Hubs/Edit.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () data EditView = EditView { hub :: !Hub } @@ -13,7 +14,7 @@ instance View EditView where diff --git a/Web/View/Hubs/FrictionHeatmap.hs b/Web/View/Hubs/FrictionHeatmap.hs index f320f1e..720fca3 100644 --- a/Web/View/Hubs/FrictionHeatmap.hs +++ b/Web/View/Hubs/FrictionHeatmap.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () import Application.Helper.FrictionScore (scoreBand) data FrictionHeatmapView = FrictionHeatmapView @@ -20,11 +21,11 @@ instance View FrictionHeatmapView where{hub.name}
No widgets in this hub.
|] - else [hsx| -No widgets in this hub.
|] + renderHeatmapGrid ws = [hsx| +{w.widgetType}
All requirements have linked decisions.
|] - else forEach awaitingDecision renderAwaitingReq} + {renderAwaitingSection awaitingDecision}No decisions recorded yet.
|] - else [hsx| -| Title | -Outcome | -Source Widget | -Decided At | -
|---|
All requirements have linked decisions.
|] +renderAwaitingSection reqs = [hsx|{forEach reqs renderAwaitingReq}|] + +renderRecentDecisionsSection :: [DecisionRecord] -> [Requirement] -> [RequirementCandidate] -> [Widget] -> Html +renderRecentDecisionsSection [] _ _ _ = [hsx|No decisions recorded yet.
|] +renderRecentDecisionsSection decisions reqs candidates ws = [hsx| +| Title | +Outcome | +Source Widget | +Decided At | +
|---|
{hub.name}
No snapshots yet. Take the first one.
|] - (latest : _) -> [hsx| -Current Health Score
- healthScoreBadge latest.healthScore}> - {show latest.healthScore} - -| Score | -Open Cand. | -Regressed | -Stale Dec. | -Bottlenecks | -Taken At | -
|---|
No snapshots yet. Take the first one.
|] +renderLatestPanel (latest : _) = [hsx| +Current Health Score
+ healthScoreBadge latest.healthScore}> + {show latest.healthScore} + +| Score | +Open Cand. | +Regressed | +Stale Dec. | +Bottlenecks | +Taken At | +
|---|
No hubs registered.
|] - else [hsx| -| Hub | -Health | -Snapshot | -- |
|---|
No friction scores computed yet.
|] - else [hsx| -| Widget | -Score | -Type | -
|---|
No active bottlenecks.
|] - else [hsx| -No open propagation events.
|] - else [hsx| -{show p.detectedAt}
No hubs registered.
|] + renderHubHealthTable hs = [hsx| +| Hub | +Health | +Snapshot | ++ |
|---|
No friction scores computed yet.
|] + renderFrictionTable scores ws = [hsx| +| Widget | +Score | +Type | +
|---|
No active bottlenecks.
|] + renderBottlenecksPanel _ = [hsx| +No open propagation events.
|] + renderPropagationsPanel ps = [hsx| +Contact: {c}
|])} + {maybe mempty renderManifestContactLine m.contact} |] @@ -225,3 +226,9 @@ kindBadge _ = [hsx|— {d}|] + +renderManifestContactLine :: Text -> Html +renderManifestContactLine c = [hsx|Contact: {c}
|] diff --git a/Web/View/Hubs/TriageDashboard.hs b/Web/View/Hubs/TriageDashboard.hs index b5b5310..02ccc99 100644 --- a/Web/View/Hubs/TriageDashboard.hs +++ b/Web/View/Hubs/TriageDashboard.hs @@ -4,6 +4,7 @@ import Web.Types import Generated.Types import IHP.Prelude import IHP.ViewPrelude +import Web.Routes () data TriageDashboardView = TriageDashboardView { hub :: !Hub @@ -20,7 +21,7 @@ instance View TriageDashboardView where @@ -46,25 +47,13 @@ instance View TriageDashboardView whereQueue empty.
|] - else [hsx| -No escalations yet.
|] - else [hsx| -Queue empty.
|] +renderTriageQueue items ws = [hsx| +No escalations yet.
|] +renderEscalationsSection items ws = [hsx| +