Files
inter-hub/Web/Controller/Api/V2/HubRegistry.hs
Bernd Worsch 6e8972f828
Some checks failed
Test / test (push) Has been cancelled
feat(WP-0011): IHF Phase 10 — Hub Registry and Widget Marketplace
Delivers the hub registry discovery UI, widget pattern library,
governance template library, and marketplace dashboard.

Key changes:
- Schema: widget_patterns (widget_type FK to registry), widget_pattern_versions,
  pattern_adoptions, governance_templates (categories JSONB, validated at
  controller), governance_template_clones — all GAAF-compliant, no bare TEXT
  type discriminators
- Migration: 1743897600-ihf-phase10-hub-registry.sql
- HubRegistry controller + views: browsable view over hub_capability_manifests,
  hub_health_snapshots, hubs with per-hub GAAF compliance indicator
- WidgetPatterns controller + views: publish, version, adopt; adoption
  triggers manifest amendment draft when new types are introduced
- GovernanceTemplates controller + views: CRUD, clone with category
  validation against annotation_category_registry
- MarketplaceDashboard controller + view: full-text search, widget-type
  filter, sort, trending panel, autoRefresh
- API v2: /api/v2/hub-registry, /api/v2/widget-patterns (+ adopt endpoint)
- OpenAPI spec updated with Phase 10 paths
- GAAF scorecard: Customization 2.5 → 3.2; overall 3.41 → 3.56 (Strong)
- CLAUDE.md: Phase 10 complete; active workplan → Phase 11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 20:14:43 +00:00

80 lines
3.1 KiB
Haskell

module Web.Controller.Api.V2.HubRegistry where
-- GET /api/v2/hub-registry — list hubs with active manifest summary + GAAF indicator
-- GET /api/v2/hub-registry/:hubId — single hub detail
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (object, (.=), Value)
import Web.Controller.Api.V2.Auth (requireApiConsumer, checkRateLimitAndLog)
instance Controller ApiV2HubRegistryController where
action ApiV2IndexHubRegistryAction = do
consumer <- requireApiConsumer
checkRateLimitAndLog consumer "GET" "/api/v2/hub-registry"
hubs <- query @Hub |> orderByAsc #name |> fetch
rows <- mapM buildHubJson hubs
renderJson rows
action ApiV2ShowHubRegistryAction { hubId } = do
consumer <- requireApiConsumer
checkRateLimitAndLog consumer "GET" ("/api/v2/hub-registry/" <> tshow hubId)
hub <- fetch hubId
mManifest <- query @HubCapabilityManifest
|> filterWhere (#hubId, hubId)
|> filterWhere (#status, "active")
|> fetchOneOrNothing
mSnapshot <- query @HubHealthSnapshot
|> filterWhere (#hubId, hubId)
|> orderByDesc #computedAt
|> limit 1
|> fetchOneOrNothing
renderJson (hubDetailJson hub mManifest mSnapshot)
-- | Build a summary JSON object for a hub including manifest and health data.
buildHubJson :: (?modelContext :: ModelContext) => Hub -> IO Value
buildHubJson hub = do
mManifest <- query @HubCapabilityManifest
|> filterWhere (#hubId, hub.id)
|> filterWhere (#status, "active")
|> fetchOneOrNothing
mSnapshot <- query @HubHealthSnapshot
|> filterWhere (#hubId, hub.id)
|> orderByDesc #computedAt
|> limit 1
|> fetchOneOrNothing
pure $ hubDetailJson hub mManifest mSnapshot
hubDetailJson :: Hub -> Maybe HubCapabilityManifest -> Maybe HubHealthSnapshot -> Value
hubDetailJson hub mManifest mSnapshot =
let gaafIndicator = case mManifest of
Nothing -> "no_manifest" :: Text
Just m | m.status == "active" -> "compliant"
| otherwise -> "draft_only"
in object
[ "id" .= hub.id
, "name" .= hub.name
, "slug" .= hub.slug
, "domain" .= hub.domain
, "hubKind" .= hub.hubKind
, "gaafStatus" .= gaafIndicator
, "manifest" .= fmap manifestSummary mManifest
, "healthScore" .= fmap (.healthScore) mSnapshot
, "healthAt" .= fmap (.computedAt) mSnapshot
]
manifestSummary :: HubCapabilityManifest -> Value
manifestSummary m = object
[ "id" .= m.id
, "manifestVersion" .= m.manifestVersion
, "status" .= m.status
, "declaredWidgetTypes" .= m.declaredWidgetTypes
, "declaredEventTypes" .= m.declaredEventTypes
, "declaredAnnotationCategories" .= m.declaredAnnotationCategories
, "declaredPolicyScopes" .= m.declaredPolicyScopes
, "activatedAt" .= m.activatedAt
]