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

@@ -1,10 +1,11 @@
module Web.View.HubRegistry.Index where
import Web.Types
import Web.Controller.HubRegistry (HubRegistryRow(..), GaafStatus(..), gaafStatus)
import Web.Types (HubRegistryRow(..), GaafStatus(..), gaafStatus)
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Data.Aeson (Value(..))
import qualified Data.Vector as V
@@ -29,12 +30,13 @@ instance View IndexView where
<div class="space-y-3">
{forEach registryRows renderRow}
{if null registryRows
then [hsx|<p class="text-sm text-gray-400">No hubs registered yet.</p>|]
else mempty}
{if null registryRows then noHubsMsg else mempty}
</div>
|]
noHubsMsg :: Html
noHubsMsg = [hsx|<p class="text-sm text-gray-400">No hubs registered yet.</p>|]
renderRow :: HubRegistryRow -> Html
renderRow row@HubRegistryRow { hub, mManifest, mLatestSnapshot } =
let gs = gaafStatus mManifest
@@ -46,7 +48,7 @@ renderRow row@HubRegistryRow { hub, mManifest, mLatestSnapshot } =
<div class="bg-white rounded-lg border border-gray-200 p-4 hover:border-indigo-200">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<a href={ShowHubRegistryAction { hubId = hub.id }}
<a href={ShowHubRegistryAction (hub.id)}
class="font-medium text-indigo-700 hover:underline">
{hub.name}
</a>
@@ -74,7 +76,8 @@ gaafBadge GaafNoManifest =
healthScoreBadge :: Int -> Html
healthScoreBadge s =
let cls = if s >= 80 then "bg-green-100 text-green-800"
let cls :: Text
cls = if s >= 80 then "bg-green-100 text-green-800"
else if s >= 50 then "bg-amber-100 text-amber-800"
else "bg-red-100 text-red-700"
in [hsx|<span class={"px-2 py-0.5 rounded text-xs " <> cls}>health {tshow s}</span>|]

View File

@@ -1,10 +1,10 @@
module Web.View.HubRegistry.Show where
import Web.Types
import Web.Controller.HubRegistry (GaafStatus(..), gaafStatus)
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Data.Aeson (Value(..), encode)
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy.Char8 as BL
@@ -47,54 +47,64 @@ instance View ShowView where
</div>
</div>
{case mManifest of
Nothing -> [hsx|
<div class="bg-amber-50 border border-amber-200 rounded p-3 mb-6 text-sm text-amber-800">
No active manifest. <a href={NewHubCapabilityManifestAction} class="underline">Create one</a> to register hub-owned types.
</div>
|]
Just m -> [hsx|
<div class="grid grid-cols-2 gap-4 mb-6">
{jsonArraySection "Widget Types" m.declaredWidgetTypes}
{jsonArraySection "Event Types" m.declaredEventTypes}
{jsonArraySection "Annotation Categories" m.declaredAnnotationCategories}
{jsonArraySection "Policy Scopes" m.declaredPolicyScopes}
</div>
|]}
{manifestSection mManifest}
<h2 class="text-lg font-semibold mb-3">Health History</h2>
{if null healthHistory
then [hsx|<p class="text-sm text-gray-400 mb-6">No snapshots recorded yet.</p>|]
else [hsx|
<div class="overflow-x-auto mb-6">
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-gray-500 border-b border-gray-200">
<th class="text-left py-2">Score</th>
<th class="text-left py-2">Open Candidates</th>
<th class="text-left py-2">Regressed Widgets</th>
<th class="text-left py-2">Stale Decisions</th>
<th class="text-left py-2">Active Bottlenecks</th>
<th class="text-left py-2">Computed At</th>
</tr>
</thead>
<tbody>
{forEach healthHistory renderSnapshotRow}
</tbody>
</table>
</div>
|]}
{renderHealthHistory healthHistory}
<h2 class="text-lg font-semibold mb-3">Adopted Patterns</h2>
{if null adoptedPatterns
then [hsx|<p class="text-sm text-gray-400">No patterns adopted yet. <a href={WidgetPatternsAction} class="text-indigo-600 hover:underline">Browse patterns </a></p>|]
else [hsx|
<div class="space-y-2">
{forEach adoptedPatterns renderAdoptedPattern}
</div>
|]}
{renderAdoptedPatternsSection adoptedPatterns}
|]
manifestSection :: Maybe HubCapabilityManifest -> Html
manifestSection Nothing = [hsx|
<div class="bg-amber-50 border border-amber-200 rounded p-3 mb-6 text-sm text-amber-800">
No active manifest. <a href={NewHubCapabilityManifestAction} class="underline">Create one</a> to register hub-owned types.
</div>
|]
manifestSection (Just m) = [hsx|
<div class="grid grid-cols-2 gap-4 mb-6">
{jsonArraySection "Widget Types" m.declaredWidgetTypes}
{jsonArraySection "Event Types" m.declaredEventTypes}
{jsonArraySection "Annotation Categories" m.declaredAnnotationCategories}
{jsonArraySection "Policy Scopes" m.declaredPolicyScopes}
</div>
|]
renderAdoptedPatternsSection :: [AdoptedPatternRow] -> Html
renderAdoptedPatternsSection [] = [hsx|<p class="text-sm text-gray-400">No patterns adopted yet. <a href={WidgetPatternsAction} class="text-indigo-600 hover:underline">Browse patterns </a></p>|]
renderAdoptedPatternsSection ps = [hsx|
<div class="space-y-2">
{forEach ps renderAdoptedPattern}
</div>
|]
renderPinnedBadge :: Bool -> Html
renderPinnedBadge True = [hsx|<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-700">pinned</span>|]
renderPinnedBadge False = [hsx|<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-500">follow latest</span>|]
renderHealthHistory :: [HubHealthSnapshot] -> Html
renderHealthHistory [] = [hsx|<p class="text-sm text-gray-400 mb-6">No snapshots recorded yet.</p>|]
renderHealthHistory history = [hsx|
<div class="overflow-x-auto mb-6">
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-gray-500 border-b border-gray-200">
<th class="text-left py-2">Score</th>
<th class="text-left py-2">Open Candidates</th>
<th class="text-left py-2">Regressed Widgets</th>
<th class="text-left py-2">Stale Decisions</th>
<th class="text-left py-2">Active Bottlenecks</th>
<th class="text-left py-2">Computed At</th>
</tr>
</thead>
<tbody>
{forEach history renderSnapshotRow}
</tbody>
</table>
</div>
|]
manifestCell :: Maybe HubCapabilityManifest -> Id Hub -> Html
manifestCell Nothing hubId = [hsx|
<div class="mt-1">
@@ -106,7 +116,7 @@ manifestCell Nothing hubId = [hsx|
manifestCell (Just m) _ = [hsx|
<div class="mt-1 flex items-center gap-2">
<span class="font-mono text-sm">{m.manifestVersion}</span>
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
<a href={ShowHubCapabilityManifestAction (m.id)}
class="text-xs text-indigo-600 hover:underline">View</a>
</div>
|]
@@ -163,16 +173,14 @@ renderAdoptedPattern :: AdoptedPatternRow -> Html
renderAdoptedPattern (patternId, patternName, widgetType, _, _, isPinned, adoptedAt) = [hsx|
<div class="bg-white rounded border border-gray-200 p-3 flex items-center justify-between">
<div>
<a href={ShowWidgetPatternAction { widgetPatternId = patternId }}
<a href={ShowWidgetPatternAction (patternId)}
class="font-medium text-sm text-indigo-700 hover:underline">
{patternName}
</a>
<span class="ml-2 font-mono text-xs text-gray-400">{widgetType}</span>
</div>
<div class="flex items-center gap-2 text-xs text-gray-500">
{if isPinned
then [hsx|<span class="px-2 py-0.5 rounded bg-blue-100 text-blue-700">pinned</span>|]
else [hsx|<span class="px-2 py-0.5 rounded bg-gray-100 text-gray-500">follow latest</span>|]}
{renderPinnedBadge isPinned}
<span>{tshow adoptedAt}</span>
</div>
</div>