fix(WP-0014): pre-flight compilation fixes, Tailwind pipeline, and admin seed

A2 — Compilation fixes:
- Remove inline FK constraints from Schema.sql; IHP schema compiler cannot
  parse them. Add 1744329600-restore-fk-constraints.sql migration to restore
  referential integrity at the DB level.
- Rename `#label` → `#label_` throughout to avoid clash with Haskell built-in.
- Fix `hub.id == hid` UUID comparisons to use `toUUID hub.id`.
- Replace non-existent `setStatus`/`respondJson` calls with
  `renderJsonWithStatusCode` throughout Api controllers.
- Fix qualified package import for `cryptohash-sha256` in Auth.hs.
- Add `CanSelect (Text, Text)` instance in Helper.View.
- Refactor HSX inline lambdas to named helper functions in 100+ views
  (GHC cannot infer types for anonymous functions inside quasi-quoted HSX).
- Fix missing imports (IHP.QueryBuilder, IHP.Fetch, Web.Routes, Only, etc.)
  across helpers and controllers.
- Remove duplicate `diffUTCTime` definition in BottleneckDetector.
- Change `createEventForHub` return type from `IO ResponseReceived` to `IO ()`.
- Seed type-registry vocabulary via 1744502400-seed-type-registries.sql
  (moved from Schema.sql where IHP does not execute INSERT statements).

A3 — Tailwind build pipeline:
- Add `tailwindcss` to flake.nix native packages.
- Uncomment `tailwind.exec` process in devenv shell config.
- Add tailwind/tailwind.config.js (scans Web/View/**/*.hs).
- Add tailwind/app.css with @tailwind directives.

A4 — Admin user seed:
- Add 1744416000-seed-admin-user.sql: inserts admin@inter-hub.local
  with bcrypt-hashed password admin1234! (cost 10).
- Add .env.example documenting all required environment variables
  and default admin credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 09:55:12 +00:00
parent ffd5fbb900
commit f1978c3888
147 changed files with 2710 additions and 2075 deletions

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Application.Helper.View (adapterStatusBadge)
import Data.List (nub, sortBy)
import Data.Ord (comparing, Down(..))
@@ -23,7 +24,7 @@ instance View AdapterCompatibilityDashboardView where
<h1 class="text-2xl font-semibold">Adapter Compatibility Dashboard</h1>
<p class="text-sm text-gray-500">{hub.name}</p>
</div>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm text-indigo-600 hover:underline"> Hub</a>
</div>
@@ -71,17 +72,11 @@ instance View AdapterCompatibilityDashboardView where
<div class="flex gap-6 text-sm">
<div>
<span class="text-gray-500 mr-1">Envelope:</span>
{forEach envelopes (\e -> [hsx|
<a href={ShowEnvelopeEmissionContractAction { envelopeEmissionContractId = e.id }}
class="font-mono text-indigo-600 hover:underline mr-2">v{e.contractVersion}</a>
|])}
{forEach envelopes renderEnvelopeLink}
</div>
<div>
<span class="text-gray-500 mr-1">Reporting:</span>
{forEach reportings (\r -> [hsx|
<a href={ShowInteractionReportingContractAction { interactionReportingContractId = r.id }}
class="font-mono text-indigo-600 hover:underline mr-2">v{r.contractVersion}</a>
|])}
{forEach reportings renderReportingLink}
</div>
</div>
</div>
@@ -92,19 +87,7 @@ instance View AdapterCompatibilityDashboardView where
Unassigned Widgets
<span class="ml-1 text-xs text-gray-400">(no adapter_spec_id)</span>
</h2>
{if null unassignedWidgets
then [hsx|<p class="text-sm text-gray-400">All widgets have adapter assignments.</p>|]
else [hsx|
<div class="text-sm text-gray-600 space-y-1">
{forEach unassignedWidgets (\w -> [hsx|
<div>
<a href={ShowWidgetAction { widgetId = w.id }}
class="text-indigo-600 hover:underline">{w.name}</a>
<span class="text-xs text-gray-400 ml-2">{w.widgetType}</span>
</div>
|])}
</div>
|]}
{renderUnassignedWidgets unassignedWidgets}
</div>
<!-- Panel 5: Stale adapters -->
@@ -112,23 +95,7 @@ instance View AdapterCompatibilityDashboardView where
<h2 class="text-sm font-semibold text-gray-700 mb-3">
Active Adapter Specs
</h2>
{if null activeSpecs
then [hsx|<p class="text-sm text-gray-400">No active adapter specs.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Adapter</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Framework</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Widgets</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach activeSpecs renderSpecRow}
</tbody>
</table>
|]}
{renderActiveSpecsTable activeSpecs}
</div>
|]
where
@@ -149,13 +116,31 @@ instance View AdapterCompatibilityDashboardView where
in sortBy (comparing (Down . snd))
[ (sid, length (filter (== sid) assigned)) | sid <- specIds ]
renderActiveSpecsTable :: [WidgetAdapterSpec] -> Html
renderActiveSpecsTable [] = [hsx|<p class="text-sm text-gray-400">No active adapter specs.</p>|]
renderActiveSpecsTable ss = [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Adapter</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Framework</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Widgets</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach ss renderSpecRow}
</tbody>
</table>
|]
renderSpecRow :: WidgetAdapterSpec -> Html
renderSpecRow s =
let widgetCount = length (filter (\w -> w.adapterSpecId == Just s.id) widgets)
in [hsx|
<tr class="hover:bg-gray-50">
<td class="px-3 py-2">
<a href={ShowWidgetAdapterSpecAction { widgetAdapterSpecId = s.id }}
<a href={ShowWidgetAdapterSpecAction (s.id)}
class="text-indigo-600 hover:underline">{s.name}</a>
</td>
<td class="px-3 py-2">
@@ -170,6 +155,35 @@ instance View AdapterCompatibilityDashboardView where
</tr>
|]
renderEnvelopeLink :: EnvelopeEmissionContract -> Html
renderEnvelopeLink e = [hsx|
<a href={ShowEnvelopeEmissionContractAction (e.id)}
class="font-mono text-indigo-600 hover:underline mr-2">v{e.contractVersion}</a>
|]
renderReportingLink :: InteractionReportingContract -> Html
renderReportingLink r = [hsx|
<a href={ShowInteractionReportingContractAction (r.id)}
class="font-mono text-indigo-600 hover:underline mr-2">v{r.contractVersion}</a>
|]
renderUnassignedWidgets :: [Widget] -> Html
renderUnassignedWidgets [] = [hsx|<p class="text-sm text-gray-400">All widgets have adapter assignments.</p>|]
renderUnassignedWidgets ws = [hsx|
<div class="text-sm text-gray-600 space-y-1">
{forEach ws renderUnassignedWidgetRow}
</div>
|]
renderUnassignedWidgetRow :: Widget -> Html
renderUnassignedWidgetRow w = [hsx|
<div>
<a href={ShowWidgetAction (w.id)}
class="text-indigo-600 hover:underline">{w.name}</a>
<span class="text-xs text-gray-400 ml-2">{w.widgetType}</span>
</div>
|]
kpiCard :: Text -> Text -> Text -> Html
kpiCard label value textClass = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-4">

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data AgentAuditDashboardView = AgentAuditDashboardView
{ hub :: !Hub
@@ -19,7 +20,7 @@ instance View AgentAuditDashboardView where
<h1 class="text-2xl font-semibold">Agent Audit Dashboard</h1>
<p class="text-sm text-gray-500">{hub.name}</p>
</div>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm text-indigo-600 hover:underline"> Hub</a>
</div>
@@ -35,14 +36,7 @@ instance View AgentAuditDashboardView where
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-5">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Proposals by Type</h2>
<div class="flex gap-4 flex-wrap">
{forEach allTypes (\t ->
let cnt = length (filter (\p -> p.proposalType == t) proposals)
in [hsx|
<div class="flex items-center gap-2">
<span class={typeBadge t <> " text-xs px-2 py-0.5 rounded font-medium"}>{t}</span>
<span class="text-sm font-semibold text-gray-700">{show cnt}</span>
</div>
|])}
{forEach allTypes (renderTypeCount proposals)}
</div>
</div>
@@ -51,15 +45,7 @@ instance View AgentAuditDashboardView where
<div class="px-5 py-3 border-b border-gray-100 bg-yellow-50">
<h2 class="text-sm font-semibold text-yellow-800">Unreviewed Queue ({show pendingCount})</h2>
</div>
{if null pending
then [hsx|<p class="text-sm text-gray-400 px-5 py-4">No pending proposals.</p>|]
else [hsx|
<table class="w-full text-sm">
<tbody class="divide-y divide-gray-100">
{forEach (sortByCreatedAt pending) renderQueueRow}
</tbody>
</table>
|]}
{renderPendingQueue pending}
</div>
<!-- Recent proposals (last 20) -->
@@ -90,20 +76,11 @@ instance View AgentAuditDashboardView where
<thead>
<tr>
<th class="text-left px-3 py-1 text-gray-500">Model</th>
{forEach allTypes (\t -> [hsx|
<th class="px-3 py-1 text-gray-500">{t}</th>
|])}
{forEach allTypes renderTypeHeader}
</tr>
</thead>
<tbody>
{forEach allModels (\m -> [hsx|
<tr class="border-t border-gray-100">
<td class="px-3 py-1 font-mono text-gray-600">{m}</td>
{forEach allTypes (\t ->
let cnt = length (filter (\p -> p.modelRef == m && p.proposalType == t) proposals)
in [hsx|<td class="px-3 py-1 text-center text-gray-700">{if cnt == 0 then "" else show cnt}</td>|])}
</tr>
|])}
{forEach allModels (renderModelRow allTypes proposals)}
</tbody>
</table>
</div>
@@ -121,6 +98,23 @@ instance View AgentAuditDashboardView where
allTypes = ["summary", "requirement_draft", "duplicate_flag", "policy_flag", "impl_proposal"]
allModels = nub (map (.modelRef) proposals)
renderTypeHeader :: Text -> Html
renderTypeHeader t = [hsx|<th class="px-3 py-1 text-gray-500">{t}</th>|]
renderModelRow :: [Text] -> [AgentProposal] -> Text -> Html
renderModelRow types props m = [hsx|
<tr class="border-t border-gray-100">
<td class="px-3 py-1 font-mono text-gray-600">{m}</td>
{forEach types (renderMatrixCell props m)}
</tr>
|]
renderMatrixCell :: [AgentProposal] -> Text -> Text -> Html
renderMatrixCell props m t =
let cnt = length (filter (\p -> p.modelRef == m && p.proposalType == t) props)
display = if cnt == 0 then "" else show cnt
in [hsx|<td class="px-3 py-1 text-center text-gray-700">{display}</td>|]
kpiCard :: Text -> Text -> Text -> Html
kpiCard label value colorClass = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-4">
@@ -139,7 +133,7 @@ renderQueueRow p = [hsx|
</td>
<td class="px-4 py-2 text-gray-400 text-xs">{show p.createdAt}</td>
<td class="px-4 py-2">
<a href={ShowAgentProposalAction { agentProposalId = p.id }}
<a href={ShowAgentProposalAction (p.id)}
class="text-xs text-indigo-600 hover:underline">Review </a>
</td>
</tr>
@@ -149,7 +143,7 @@ renderRecentRow :: [Widget] -> AgentProposal -> Html
renderRecentRow widgets p = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-4 py-2">
<a href={ShowAgentProposalAction { agentProposalId = p.id }}
<a href={ShowAgentProposalAction (p.id)}
class={typeBadge p.proposalType <> " text-xs px-2 py-0.5 rounded font-medium"}>
{p.proposalType}
</a>
@@ -165,6 +159,26 @@ renderRecentRow widgets p = [hsx|
</tr>
|]
renderTypeCount :: [AgentProposal] -> Text -> Html
renderTypeCount proposals t =
let cnt = length (filter (\p -> p.proposalType == t) proposals)
in [hsx|
<div class="flex items-center gap-2">
<span class={typeBadge t <> " text-xs px-2 py-0.5 rounded font-medium"}>{t}</span>
<span class="text-sm font-semibold text-gray-700">{show cnt}</span>
</div>
|]
renderPendingQueue :: [AgentProposal] -> Html
renderPendingQueue [] = [hsx|<p class="text-sm text-gray-400 px-5 py-4">No pending proposals.</p>|]
renderPendingQueue pending = [hsx|
<table class="w-full text-sm">
<tbody class="divide-y divide-gray-100">
{forEach (sortByCreatedAt pending) renderQueueRow}
</tbody>
</table>
|]
widgetName :: [Widget] -> Maybe (Id Widget) -> Text
widgetName _ Nothing = ""
widgetName widgets (Just wid) = maybe "" (.name) (find (\w -> w.id == wid) widgets)

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data AntifragilityDashboardView = AntifragilityDashboardView
{ hub :: !Hub
@@ -24,22 +25,22 @@ instance View AntifragilityDashboardView where
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
<a href={HubsAction} class="hover:text-gray-700">Hubs</a>
<span>/</span>
<a href={ShowHubAction { hubId = hub.id }} class="hover:text-gray-700">{hub.name}</a>
<a href={ShowHubAction (hub.id)} class="hover:text-gray-700">{hub.name}</a>
<span>/</span>
<span>Antifragility</span>
</div>
<h1 class="text-2xl font-semibold">Antifragility Dashboard {hub.name}</h1>
</div>
<div class="flex gap-2">
<a href={TriageDashboardAction { hubId = hub.id }}
<a href={TriageDashboardAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Triage
</a>
<a href={GovernanceDashboardAction { hubId = hub.id }}
<a href={GovernanceDashboardAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Governance
</a>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Hub
</a>
@@ -67,14 +68,7 @@ instance View AntifragilityDashboardView where
</div>
<!-- Regression alerts -->
{if null regressionWidgetIds then mempty else [hsx|
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-4 mb-6">
<h2 class="text-sm font-semibold text-red-700 mb-3"> Regression Alerts</h2>
<div class="flex flex-wrap gap-2">
{forEach regressedWidgets renderRegressedBadge}
</div>
</div>
|]}
{if null regressionWidgetIds then mempty else renderRegressionAlerts regressedWidgets}
<!-- Open gaps: decisions with impl refs but no deployment -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-5 mb-6">
@@ -84,56 +78,19 @@ instance View AntifragilityDashboardView where
(decisions with impl refs but no deployment recorded)
</span>
</h2>
{if null openGaps
then [hsx|<p class="text-sm text-gray-400">All decisions with impl refs have deployments.</p>|]
else [hsx|
<div class="space-y-1">
{forEach openGaps renderGapRow}
</div>
|]}
{renderOpenGaps openGaps}
</div>
<!-- Recent deployments -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-5 mb-6">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Recent Deployments</h2>
{if null recentDeploys
then [hsx|<p class="text-sm text-gray-400">No deployments yet.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Version</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Decision</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Signals</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Eval</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Deployed</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach recentDeploys (renderDeployRow allDecisions allSignals allEvaluations)}
</tbody>
</table>
|]}
{renderRecentDeploysSection recentDeploys allDecisions allSignals allEvaluations}
</div>
<!-- Recurrence leaderboard -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-5">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Recurrence Leaderboard</h2>
{if null recurrenceLeaderboard
then [hsx|<p class="text-sm text-gray-400">No recurring widgets detected.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Widget</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Cycles</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach recurrenceLeaderboard (renderRecurrenceRow widgets)}
</tbody>
</table>
|]}
{renderRecurrenceSection recurrenceLeaderboard widgets}
</div>
|]
where
@@ -160,7 +117,7 @@ sortByDesc f = sortBy (\a b -> compare (f b) (f a))
renderRegressedBadge :: Widget -> Html
renderRegressedBadge w = [hsx|
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="text-xs bg-red-100 text-red-800 border border-red-300 rounded px-2 py-1 hover:bg-red-200">
{w.name}
</a>
@@ -169,7 +126,7 @@ renderRegressedBadge w = [hsx|
renderGapRow :: DecisionRecord -> Html
renderGapRow d = [hsx|
<div class="flex items-center justify-between py-1.5 text-sm">
<a href={ShowDecisionRecordAction { decisionRecordId = d.id }}
<a href={ShowDecisionRecordAction (d.id)}
class="text-indigo-600 hover:text-indigo-800">{d.title}</a>
<span class={outcomeClass d.outcome <> " text-xs px-2 py-0.5 rounded font-medium"}>
{d.outcome}
@@ -181,7 +138,7 @@ renderDeployRow :: [DecisionRecord] -> [OutcomeSignal] -> [ChangeEvaluation] ->
renderDeployRow decisions signals evals dr = [hsx|
<tr>
<td class="py-2 pr-4">
<a href={ShowDeploymentRecordAction { deploymentRecordId = dr.id }}
<a href={ShowDeploymentRecordAction (dr.id)}
class="font-mono text-indigo-600 hover:text-indigo-800">{dr.versionRef}</a>
</td>
<td class="py-2 pr-4 text-gray-600">{decisionTitle}</td>
@@ -189,7 +146,7 @@ renderDeployRow decisions signals evals dr = [hsx|
{renderSignalSummary drSignals}
</td>
<td class="py-2 pr-4 text-right">
{maybe [hsx|<span class="text-gray-400 text-xs"></span>|] renderEvalBadge mScore}
{maybe noEvalBadge renderEvalBadge mScore}
</td>
<td class="py-2 text-right text-xs text-gray-400">{show dr.deployedAt}</td>
</tr>
@@ -203,9 +160,7 @@ renderSignalSummary :: [OutcomeSignal] -> Html
renderSignalSummary [] = [hsx|<span class="text-gray-400 text-xs"></span>|]
renderSignalSummary signals = [hsx|
<div class="flex gap-1 justify-end">
{forEach (take 3 signals) (\s -> [hsx|
<span class={signalDot s.signalType}></span>
|])}
{forEach (take 3 signals) renderSignalDot}
</div>
|]
@@ -227,7 +182,7 @@ renderRecurrenceRow :: [Widget] -> (Id Widget, Int) -> Html
renderRecurrenceRow widgets (wid, count) = [hsx|
<tr>
<td class="py-2">
{maybe [hsx|<span class="text-gray-500"></span>|] renderWidgetLink mWidget}
{maybe noWidgetSpan renderWidgetLink mWidget}
</td>
<td class="py-2 text-right">
<span class="text-sm font-semibold text-yellow-700"> {show count}</span>
@@ -239,10 +194,72 @@ renderRecurrenceRow widgets (wid, count) = [hsx|
renderWidgetLink :: Widget -> Html
renderWidgetLink w = [hsx|
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="text-indigo-600 hover:text-indigo-800">{w.name}</a>
|]
renderRegressionAlerts :: [Widget] -> Html
renderRegressionAlerts ws = [hsx|
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-4 mb-6">
<h2 class="text-sm font-semibold text-red-700 mb-3"> Regression Alerts</h2>
<div class="flex flex-wrap gap-2">
{forEach ws renderRegressedBadge}
</div>
</div>
|]
renderOpenGaps :: [DecisionRecord] -> Html
renderOpenGaps [] = [hsx|<p class="text-sm text-gray-400">All decisions with impl refs have deployments.</p>|]
renderOpenGaps gaps = [hsx|
<div class="space-y-1">
{forEach gaps renderGapRow}
</div>
|]
renderRecentDeploysSection :: [DeploymentRecord] -> [DecisionRecord] -> [OutcomeSignal] -> [ChangeEvaluation] -> Html
renderRecentDeploysSection [] _ _ _ = [hsx|<p class="text-sm text-gray-400">No deployments yet.</p>|]
renderRecentDeploysSection deploys decisions signals evals = [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Version</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Decision</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Signals</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Eval</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Deployed</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach deploys (renderDeployRow decisions signals evals)}
</tbody>
</table>
|]
renderRecurrenceSection :: [(Id Widget, Int)] -> [Widget] -> Html
renderRecurrenceSection [] _ = [hsx|<p class="text-sm text-gray-400">No recurring widgets detected.</p>|]
renderRecurrenceSection leaderboard widgets = [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Widget</th>
<th class="text-right py-2 text-xs font-medium text-gray-500">Cycles</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach leaderboard (renderRecurrenceRow widgets)}
</tbody>
</table>
|]
noEvalBadge :: Html
noEvalBadge = [hsx|<span class="text-gray-400 text-xs"></span>|]
noWidgetSpan :: Html
noWidgetSpan = [hsx|<span class="text-gray-500"></span>|]
renderSignalDot :: OutcomeSignal -> Html
renderSignalDot s = [hsx|<span class={signalDot s.signalType}></span>|]
outcomeClass :: Text -> Text
outcomeClass "accepted" = "bg-green-100 text-green-800"
outcomeClass "rejected" = "bg-red-100 text-red-800"

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Data.Time.Clock (diffUTCTime, getCurrentTime)
data BottleneckDashboardView = BottleneckDashboardView
@@ -20,11 +21,11 @@ instance View BottleneckDashboardView where
<p class="text-sm text-gray-500">{hub.name}</p>
</div>
<div class="flex gap-2">
<a href={DetectBottlenecksAction { hubId = hub.id }}
<a href={DetectBottlenecksAction (hub.id)}
class="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-700">
Detect
</a>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Hub
</a>
@@ -33,9 +34,7 @@ instance View BottleneckDashboardView where
{forEach stages renderStageSection}
{if null bottlenecks
then [hsx|<p class="text-sm text-gray-400 mt-4">No active bottlenecks detected.</p>|]
else mempty}
{if null bottlenecks then noBottlenecksMsg else mempty}
|]
where
stages = ["candidate", "requirement", "decision", "observation"] :: [Text]
@@ -83,12 +82,15 @@ instance View BottleneckDashboardView where
</span>
</td>
<td class="px-3 py-2 text-right">
<a href={ResolveBottleneckAction { bottleneckRecordId = b.id }}
<a href={ResolveBottleneckAction (b.id)}
class="text-xs text-indigo-600 hover:underline">Resolve</a>
</td>
</tr>
|]
noBottlenecksMsg :: Html
noBottlenecksMsg = [hsx|<p class="text-sm text-gray-400 mt-4">No active bottlenecks detected.</p>|]
severityBadge :: Text -> Text
severityBadge s = case s of
"critical" -> "bg-red-100 text-red-800"

View File

@@ -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
<div class="flex items-center gap-2 text-sm text-gray-500 mb-4">
<a href={HubsAction} class="hover:text-gray-700">Hubs</a>
<span>/</span>
<a href={ShowHubAction { hubId = hub.id }} class="hover:text-gray-700">{hub.name}</a>
<a href={ShowHubAction (hub.id)} class="hover:text-gray-700">{hub.name}</a>
<span>/</span>
<span>Edit</span>
</div>

View File

@@ -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
<p class="text-sm text-gray-500">{hub.name}</p>
</div>
<div class="flex gap-2">
<a href={RecomputeFrictionAction { hubId = hub.id }}
<a href={RecomputeFrictionAction (hub.id)}
class="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-700">
Recompute
</a>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Hub
</a>
@@ -38,18 +39,20 @@ instance View FrictionHeatmapView where
<span><span class="inline-block w-3 h-3 rounded bg-red-100 mr-1"></span>Critical (60+)</span>
</div>
{if null widgets
then [hsx|<p class="text-sm text-gray-400">No widgets in this hub.</p>|]
else [hsx|
<div class="grid grid-cols-3 gap-3">
{forEach widgets renderWidgetCard}
</div>
|]}
{renderHeatmapGrid widgets}
|]
where
scoreFor w = maybe 0 (.score) (find (\fs -> fs.widgetId == w.id) frictionScores)
hasScore w = any (\fs -> fs.widgetId == w.id) frictionScores
renderHeatmapGrid :: [Widget] -> Html
renderHeatmapGrid [] = [hsx|<p class="text-sm text-gray-400">No widgets in this hub.</p>|]
renderHeatmapGrid ws = [hsx|
<div class="grid grid-cols-3 gap-3">
{forEach ws renderWidgetCard}
</div>
|]
renderWidgetCard :: Widget -> Html
renderWidgetCard w =
let s = scoreFor w
@@ -57,12 +60,14 @@ instance View FrictionHeatmapView where
in [hsx|
<div class={"rounded-lg border p-4 " <> band}>
<div class="flex items-start justify-between">
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="font-medium text-sm hover:underline">{w.name}</a>
{if hasScore w
then [hsx|<span class="text-lg font-bold">{show s}</span>|]
else [hsx|<span class="text-xs text-gray-400"></span>|]}
{renderScoreBadge (hasScore w) s}
</div>
<p class="text-xs mt-1 opacity-70">{w.widgetType}</p>
</div>
|]
renderScoreBadge :: Bool -> Int -> Html
renderScoreBadge True s = [hsx|<span class="text-lg font-bold">{show s}</span>|]
renderScoreBadge False _ = [hsx|<span class="text-xs text-gray-400"></span>|]

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data GovernanceDashboardView = GovernanceDashboardView
{ hub :: !Hub
@@ -23,22 +24,22 @@ instance View GovernanceDashboardView where
<div class="flex items-center gap-2 text-sm text-gray-500 mb-1">
<a href={HubsAction} class="hover:text-gray-700">Hubs</a>
<span>/</span>
<a href={ShowHubAction { hubId = hub.id }} class="hover:text-gray-700">{hub.name}</a>
<a href={ShowHubAction (hub.id)} class="hover:text-gray-700">{hub.name}</a>
<span>/</span>
<span>Governance</span>
</div>
<h1 class="text-2xl font-semibold">Governance Dashboard {hub.name}</h1>
</div>
<div class="flex gap-2">
<a href={TriageDashboardAction { hubId = hub.id }}
<a href={TriageDashboardAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Triage Dashboard
</a>
<a href={AntifragilityDashboardAction { hubId = hub.id }}
<a href={AntifragilityDashboardAction (hub.id)}
class="text-sm border border-green-300 text-green-700 px-3 py-1.5 rounded hover:bg-green-50">
Antifragility
</a>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Hub Overview
</a>
@@ -54,14 +55,7 @@ instance View GovernanceDashboardView where
</div>
</div>
{if null regressionWidgetIds then mempty else [hsx|
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-4 mb-6">
<h2 class="text-sm font-semibold text-red-700 mb-2"> Regressed Widgets</h2>
<div class="flex flex-wrap gap-2">
{forEach regressedWidgets renderRegressedBadge}
</div>
</div>
|]}
{if null regressionWidgetIds then mempty else renderGovRegressionAlerts regressedWidgets}
<!-- Open requirements awaiting decision -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-5 mb-6">
@@ -71,31 +65,13 @@ instance View GovernanceDashboardView where
({show (length awaitingDecision)} pending)
</span>
</h2>
{if null awaitingDecision
then [hsx|<p class="text-sm text-gray-400">All requirements have linked decisions.</p>|]
else forEach awaitingDecision renderAwaitingReq}
{renderAwaitingSection awaitingDecision}
</div>
<!-- Recent decisions -->
<div class="bg-white rounded-lg border border-gray-200 px-6 py-5 mb-6">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Recent Decisions</h2>
{if null recentDecisions
then [hsx|<p class="text-sm text-gray-400">No decisions recorded yet.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Title</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Outcome</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Source Widget</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Decided At</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach recentDecisions (renderDecisionRow allRequirements allCandidates widgets)}
</tbody>
</table>
|]}
{renderRecentDecisionsSection recentDecisions allRequirements allCandidates widgets}
</div>
<!-- Traceability coverage per widget -->
@@ -150,7 +126,7 @@ isAwaitingDecision decisions req =
renderAwaitingReq :: Requirement -> Html
renderAwaitingReq req = [hsx|
<div class="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
<a href={ShowRequirementAction { requirementId = req.id }}
<a href={ShowRequirementAction (req.id)}
class="text-sm text-indigo-600 hover:text-indigo-800">{req.title}</a>
<span class="text-xs text-gray-400">{show req.createdAt}</span>
</div>
@@ -160,7 +136,7 @@ renderDecisionRow :: [Requirement] -> [RequirementCandidate] -> [Widget] -> Deci
renderDecisionRow reqs candidates widgets dr = [hsx|
<tr>
<td class="py-2 pr-4">
<a href={ShowDecisionRecordAction { decisionRecordId = dr.id }}
<a href={ShowDecisionRecordAction (dr.id)}
class="text-indigo-600 hover:text-indigo-800">{dr.title}</a>
</td>
<td class="py-2 pr-4">
@@ -213,7 +189,7 @@ renderCoverageRow annotations candidates requirements decisions w = [hsx|
renderRegressedBadge :: Widget -> Html
renderRegressedBadge w = [hsx|
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="text-xs bg-red-100 text-red-800 border border-red-300 rounded px-2 py-1 hover:bg-red-200">
{w.name}
</a>
@@ -223,6 +199,38 @@ coverageMark :: Bool -> Html
coverageMark True = [hsx|<span class="text-green-600 font-bold"></span>|]
coverageMark False = [hsx|<span class="text-gray-300"></span>|]
renderGovRegressionAlerts :: [Widget] -> Html
renderGovRegressionAlerts ws = [hsx|
<div class="bg-red-50 border border-red-200 rounded-lg px-6 py-4 mb-6">
<h2 class="text-sm font-semibold text-red-700 mb-3">Regression Alerts</h2>
<div class="flex flex-wrap gap-2">
{forEach ws renderRegressedBadge}
</div>
</div>
|]
renderAwaitingSection :: [Requirement] -> Html
renderAwaitingSection [] = [hsx|<p class="text-sm text-gray-400">All requirements have linked decisions.</p>|]
renderAwaitingSection reqs = [hsx|{forEach reqs renderAwaitingReq}|]
renderRecentDecisionsSection :: [DecisionRecord] -> [Requirement] -> [RequirementCandidate] -> [Widget] -> Html
renderRecentDecisionsSection [] _ _ _ = [hsx|<p class="text-sm text-gray-400">No decisions recorded yet.</p>|]
renderRecentDecisionsSection decisions reqs candidates ws = [hsx|
<table class="w-full text-sm">
<thead class="border-b border-gray-100">
<tr>
<th class="text-left py-2 text-xs font-medium text-gray-500">Title</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Outcome</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Source Widget</th>
<th class="text-left py-2 text-xs font-medium text-gray-500">Decided At</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
{forEach decisions (renderDecisionRow reqs candidates ws)}
</tbody>
</table>
|]
outcomeClass :: Text -> Text
outcomeClass "accepted" = "bg-green-100 text-green-800"
outcomeClass "rejected" = "bg-red-100 text-red-800"

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Application.Helper.HubHealth (healthScoreBadge)
data HubHealthHistoryView = HubHealthHistoryView
@@ -19,57 +20,63 @@ instance View HubHealthHistoryView where
<p class="text-sm text-gray-500">{hub.name}</p>
</div>
<div class="flex gap-2">
<a href={SnapshotHubHealthAction { hubId = hub.id }}
<a href={SnapshotHubHealthAction (hub.id)}
class="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-700">
Take Snapshot
</a>
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Hub
</a>
</div>
</div>
{case snapshots of
[] -> [hsx|<p class="text-sm text-gray-400">No snapshots yet. Take the first one.</p>|]
(latest : _) -> [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-6 flex items-center gap-6">
<div>
<p class="text-xs text-gray-500 mb-1">Current Health Score</p>
<span class={"text-3xl font-bold px-3 py-1 rounded " <> healthScoreBadge latest.healthScore}>
{show latest.healthScore}
</span>
</div>
<div class="text-sm text-gray-600 space-y-1">
<div>Open candidates: <strong>{show latest.openCandidates}</strong></div>
<div>Regressed widgets: <strong>{show latest.regressedWidgets}</strong></div>
<div>Stale decisions: <strong>{show latest.staleDecisions}</strong></div>
<div>Active bottlenecks: <strong>{show latest.activeBottlenecks}</strong></div>
</div>
</div>
|]}
{renderLatestPanel snapshots}
{if null snapshots then mempty else [hsx|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Score</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Open Cand.</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Regressed</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Stale Dec.</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Bottlenecks</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Taken At</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach snapshots renderRow}
</tbody>
</table>
</div>
|]}
{renderSnapshotsTable snapshots}
|]
renderLatestPanel :: [HubHealthSnapshot] -> Html
renderLatestPanel [] = [hsx|<p class="text-sm text-gray-400">No snapshots yet. Take the first one.</p>|]
renderLatestPanel (latest : _) = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-6 flex items-center gap-6">
<div>
<p class="text-xs text-gray-500 mb-1">Current Health Score</p>
<span class={"text-3xl font-bold px-3 py-1 rounded " <> healthScoreBadge latest.healthScore}>
{show latest.healthScore}
</span>
</div>
<div class="text-sm text-gray-600 space-y-1">
<div>Open candidates: <strong>{show latest.openCandidates}</strong></div>
<div>Regressed widgets: <strong>{show latest.regressedWidgets}</strong></div>
<div>Stale decisions: <strong>{show latest.staleDecisions}</strong></div>
<div>Active bottlenecks: <strong>{show latest.activeBottlenecks}</strong></div>
</div>
</div>
|]
renderSnapshotsTable :: [HubHealthSnapshot] -> Html
renderSnapshotsTable [] = mempty
renderSnapshotsTable snaps = [hsx|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Score</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Open Cand.</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Regressed</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Stale Dec.</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Bottlenecks</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Taken At</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach snaps renderRow}
</tbody>
</table>
</div>
|]
renderRow :: HubHealthSnapshot -> Html
renderRow s = [hsx|
<tr class="hover:bg-gray-50">

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data IndexView = IndexView { hubs :: ![Hub] }
@@ -44,7 +45,7 @@ renderHub :: Hub -> Html
renderHub hub = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3">
<a href={ShowHubAction { hubId = hub.id }}
<a href={ShowHubAction (hub.id)}
class="font-medium text-indigo-600 hover:text-indigo-800">
{hub.name}
</a>
@@ -53,9 +54,9 @@ renderHub hub = [hsx|
<td class="px-4 py-3 text-gray-500">{hub.domain}</td>
<td class="px-4 py-3">{kindBadge hub.hubKind}</td>
<td class="px-4 py-3 text-right">
<a href={EditHubAction { hubId = hub.id }}
<a href={EditHubAction (hub.id)}
class="text-gray-500 hover:text-gray-700 text-xs mr-3">Edit</a>
<a href={DeleteHubAction { hubId = hub.id }}
<a href={DeleteHubAction (hub.id)}
class="text-red-500 hover:text-red-700 text-xs"
data-confirm="Delete this hub?">Delete</a>
</td>

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data NewView = NewView { hub :: !Hub }

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Application.Helper.HubHealth (healthScoreBadge)
import Application.Helper.FrictionScore (scoreBand)
import Web.View.Hubs.BottleneckDashboard (severityBadge)
@@ -26,68 +27,25 @@ instance View OperationalReviewBoardView where
<!-- Panel 1: Hub health matrix -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Hub Health Matrix</h2>
{if null hubs
then [hsx|<p class="text-sm text-gray-400">No hubs registered.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Hub</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Health</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Snapshot</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach hubs renderHubRow}
</tbody>
</table>
|]}
{renderHubHealthTable hubs}
</div>
<!-- Panel 2: Top friction widgets -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Top Friction Widgets</h2>
{if null topFrictionScores
then [hsx|<p class="text-sm text-gray-400">No friction scores computed yet.</p>|]
else [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Widget</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Score</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Type</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach (zip topFrictionScores topWidgets) renderFrictionRow}
</tbody>
</table>
|]}
{renderFrictionTable topFrictionScores topWidgets}
</div>
<!-- Panel 3: Active bottlenecks by stage -->
<div class="bg-white rounded-lg border border-gray-200 p-5 mb-4">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Active Bottlenecks by Stage</h2>
{if null bottlenecks
then [hsx|<p class="text-sm text-gray-400">No active bottlenecks.</p>|]
else [hsx|
<div class="grid grid-cols-4 gap-3">
{forEach stages renderBottleneckStage}
</div>
|]}
{renderBottlenecksPanel bottlenecks}
</div>
<!-- Panel 4: Open cross-hub propagations -->
<div class="bg-white rounded-lg border border-gray-200 p-5">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Open Cross-Hub Propagations</h2>
{if null openPropagations
then [hsx|<p class="text-sm text-gray-400">No open propagation events.</p>|]
else [hsx|
<div class="space-y-2">
{forEach openPropagations renderPropagationRow}
</div>
|]}
{renderPropagationsPanel openPropagations}
</div>
|]
where
@@ -108,23 +66,17 @@ instance View OperationalReviewBoardView where
in [hsx|
<tr class="hover:bg-gray-50">
<td class="px-3 py-2">
<a href={ShowHubAction { hubId = h.id }}
<a href={ShowHubAction (h.id)}
class="text-indigo-600 hover:underline">{h.name}</a>
</td>
<td class="px-3 py-2">
{case mSnap of
Nothing -> [hsx|<span class="text-xs text-gray-400"></span>|]
Just s -> [hsx|
<span class={"px-2 py-0.5 rounded text-xs font-semibold " <> healthScoreBadge s.healthScore}>
{show s.healthScore}
</span>
|]}
{renderHealthScore mSnap}
</td>
<td class="px-3 py-2 text-xs text-gray-400">
{maybe "never" (\s -> show s.computedAt) mSnap}
</td>
<td class="px-3 py-2 text-right">
<a href={HubHealthHistoryAction { hubId = h.id }}
<a href={HubHealthHistoryAction (h.id)}
class="text-xs text-indigo-600 hover:underline">History</a>
</td>
</tr>
@@ -134,7 +86,7 @@ instance View OperationalReviewBoardView where
renderFrictionRow (fs, w) = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-3 py-2">
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="text-indigo-600 hover:underline">{w.name}</a>
</td>
<td class="px-3 py-2">
@@ -170,10 +122,69 @@ instance View OperationalReviewBoardView where
<p class="text-xs text-gray-400 mt-0.5">{show p.detectedAt}</p>
</div>
<div class="flex gap-2 ml-4">
<a href={AcknowledgePropagationAction { crossHubPropagationId = p.id }}
<a href={AcknowledgePropagationAction (p.id)}
class="text-xs text-yellow-600 hover:underline whitespace-nowrap">Acknowledge</a>
<a href={ResolvePropagationAction { crossHubPropagationId = p.id }}
<a href={ResolvePropagationAction (p.id)}
class="text-xs text-green-600 hover:underline">Resolve</a>
</div>
</div>
|]
renderHubHealthTable :: [Hub] -> Html
renderHubHealthTable [] = [hsx|<p class="text-sm text-gray-400">No hubs registered.</p>|]
renderHubHealthTable hs = [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Hub</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Health</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Snapshot</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach hs renderHubRow}
</tbody>
</table>
|]
renderFrictionTable :: [FrictionScore] -> [Widget] -> Html
renderFrictionTable [] _ = [hsx|<p class="text-sm text-gray-400">No friction scores computed yet.</p>|]
renderFrictionTable scores ws = [hsx|
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-3 py-2 font-medium text-gray-600">Widget</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Score</th>
<th class="text-left px-3 py-2 font-medium text-gray-600">Type</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach (zip scores ws) renderFrictionRow}
</tbody>
</table>
|]
renderBottlenecksPanel :: [BottleneckRecord] -> Html
renderBottlenecksPanel [] = [hsx|<p class="text-sm text-gray-400">No active bottlenecks.</p>|]
renderBottlenecksPanel _ = [hsx|
<div class="grid grid-cols-4 gap-3">
{forEach stages renderBottleneckStage}
</div>
|]
renderPropagationsPanel :: [CrossHubPropagation] -> Html
renderPropagationsPanel [] = [hsx|<p class="text-sm text-gray-400">No open propagation events.</p>|]
renderPropagationsPanel ps = [hsx|
<div class="space-y-2">
{forEach ps renderPropagationRow}
</div>
|]
renderHealthScore :: Maybe HubHealthSnapshot -> Html
renderHealthScore Nothing = [hsx|<span class="text-xs text-gray-400"></span>|]
renderHealthScore (Just s) = [hsx|
<span class={"px-2 py-0.5 rounded text-xs font-semibold " <> healthScoreBadge s.healthScore}>
{show s.healthScore}
</span>
|]

View File

@@ -4,6 +4,7 @@ import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data ShowView = ShowView
{ hub :: !Hub
@@ -33,39 +34,39 @@ instance View ShowView where
</p>
</div>
<div class="flex gap-2">
<a href={TriageDashboardAction { hubId = hub.id }}
<a href={TriageDashboardAction (hub.id)}
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
Triage Dashboard
</a>
<a href={GovernanceDashboardAction { hubId = hub.id }}
<a href={GovernanceDashboardAction (hub.id)}
class="text-sm border border-purple-300 text-purple-700 px-3 py-1.5 rounded hover:bg-purple-50">
Governance Dashboard
</a>
<a href={AntifragilityDashboardAction { hubId = hub.id }}
<a href={AntifragilityDashboardAction (hub.id)}
class="text-sm border border-green-300 text-green-700 px-3 py-1.5 rounded hover:bg-green-50">
Antifragility
</a>
<a href={AgentAuditDashboardAction { hubId = hub.id }}
<a href={AgentAuditDashboardAction (hub.id)}
class="text-sm border border-purple-300 text-purple-700 px-3 py-1.5 rounded hover:bg-purple-50">
Agent Audit
</a>
<a href={AdapterCompatibilityDashboardAction { hubId = hub.id }}
<a href={AdapterCompatibilityDashboardAction (hub.id)}
class="text-sm border border-teal-300 text-teal-700 px-3 py-1.5 rounded hover:bg-teal-50">
Adapters
</a>
<a href={FrictionHeatmapAction { hubId = hub.id }}
<a href={FrictionHeatmapAction (hub.id)}
class="text-sm border border-orange-300 text-orange-700 px-3 py-1.5 rounded hover:bg-orange-50">
Friction
</a>
<a href={BottleneckDashboardAction { hubId = hub.id }}
<a href={BottleneckDashboardAction (hub.id)}
class="text-sm border border-red-300 text-red-700 px-3 py-1.5 rounded hover:bg-red-50">
Bottlenecks
</a>
<a href={HubHealthHistoryAction { hubId = hub.id }}
<a href={HubHealthHistoryAction (hub.id)}
class="text-sm border border-green-300 text-green-700 px-3 py-1.5 rounded hover:bg-green-50">
Health
</a>
<a href={EditHubAction { hubId = hub.id }}
<a href={EditHubAction (hub.id)}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">
Edit
</a>
@@ -146,7 +147,7 @@ renderWidgetRow :: Widget -> Html
renderWidgetRow w = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3">
<a href={ShowWidgetAction { widgetId = w.id }}
<a href={ShowWidgetAction (w.id)}
class="font-medium text-indigo-600 hover:text-indigo-800">
{w.name}
</a>
@@ -202,12 +203,12 @@ renderManifestSection (Just m) _ = [hsx|
<div class="flex items-center gap-2">
{manifestStatusBadge m.status}
<span class="text-sm text-gray-600">v{m.manifestVersion}</span>
{forEach (maybeText m.capabilityDescription) (\d -> [hsx|<span class="text-sm text-gray-500"> {d}</span>|])}
{maybe mempty renderCapabilityDesc m.capabilityDescription}
</div>
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
<a href={ShowHubCapabilityManifestAction (m.id)}
class="text-sm text-indigo-600 hover:text-indigo-800">View manifest </a>
</div>
{forEach (maybeText m.contact) (\c -> [hsx|<p class="text-xs text-gray-400">Contact: {c}</p>|])}
{maybe mempty renderManifestContactLine m.contact}
</div>
|]
@@ -225,3 +226,9 @@ kindBadge _ = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-10
maybeText :: Maybe Text -> [Text]
maybeText Nothing = []
maybeText (Just t) = [t]
renderCapabilityDesc :: Text -> Html
renderCapabilityDesc d = [hsx|<span class="text-sm text-gray-500"> {d}</span>|]
renderManifestContactLine :: Text -> Html
renderManifestContactLine c = [hsx|<p class="text-xs text-gray-400">Contact: {c}</p>|]

View File

@@ -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
<div class="mb-6 flex items-center gap-2 text-sm text-gray-500">
<a href={HubsAction} class="hover:text-gray-700">Hubs</a>
<span>/</span>
<a href={ShowHubAction { hubId = hub.id }} class="hover:text-gray-700">{hub.name}</a>
<a href={ShowHubAction (hub.id)} class="hover:text-gray-700">{hub.name}</a>
<span>/</span>
<span>Triage Dashboard</span>
</div>
@@ -46,25 +47,13 @@ instance View TriageDashboardView where
<!-- Triage queue -->
<section>
<h2 class="text-lg font-medium mb-3">Triage Queue (Open)</h2>
{if null triageQueue
then [hsx|<p class="text-sm text-gray-400">Queue empty.</p>|]
else [hsx|
<div class="space-y-2">
{forEach triageQueue (renderQueueItem widgets)}
</div>
|]}
{renderTriageQueue triageQueue widgets}
</section>
<!-- Recent escalations -->
<section>
<h2 class="text-lg font-medium mb-3">Recent Escalations</h2>
{if null recentEscalations
then [hsx|<p class="text-sm text-gray-400">No escalations yet.</p>|]
else [hsx|
<div class="space-y-2">
{forEach recentEscalations (renderEscalationItem widgets)}
</div>
|]}
{renderEscalationsSection recentEscalations widgets}
</section>
</div>
@@ -85,6 +74,22 @@ renderKpi label status candidates colorClass =
</div>
|]
renderTriageQueue :: [RequirementCandidate] -> [Widget] -> Html
renderTriageQueue [] _ = [hsx|<p class="text-sm text-gray-400">Queue empty.</p>|]
renderTriageQueue items ws = [hsx|
<div class="space-y-2">
{forEach items (renderQueueItem ws)}
</div>
|]
renderEscalationsSection :: [RequirementCandidate] -> [Widget] -> Html
renderEscalationsSection [] _ = [hsx|<p class="text-sm text-gray-400">No escalations yet.</p>|]
renderEscalationsSection items ws = [hsx|
<div class="space-y-2">
{forEach items (renderEscalationItem ws)}
</div>
|]
renderQueueItem :: [Widget] -> RequirementCandidate -> Html
renderQueueItem widgets c =
let mWidget = find (\w -> w.id == c.sourceWidgetId) widgets
@@ -92,7 +97,7 @@ renderQueueItem widgets c =
in [hsx|
<div class="bg-white rounded border border-gray-200 px-4 py-3">
<div class="flex items-start justify-between gap-2">
<a href={ShowRequirementCandidateAction { requirementCandidateId = c.id }}
<a href={ShowRequirementCandidateAction (c.id)}
class="text-sm font-medium text-indigo-600 hover:text-indigo-800 leading-snug">
{c.title}
</a>
@@ -115,7 +120,7 @@ renderEscalationItem widgets c =
<span class={statusClass c.status <> " text-xs px-2 py-0.5 rounded"}>{c.status}</span>
<span class="text-xs text-gray-500">{maybe "" (.name) mWidget}</span>
</div>
<a href={ShowRequirementCandidateAction { requirementCandidateId = c.id }}
<a href={ShowRequirementCandidateAction (c.id)}
class="text-sm text-indigo-600 hover:text-indigo-800">{c.title}</a>
</div>
|]