Files
inter-hub/Web/View/ApiConsumers/Index.hs
Bernd Worsch 3cac021213
Some checks failed
Test / test (push) Has been cancelled
feat(WP-0010): IHF Phase 9 — External API Surface and Consumer SDKs
Delivers the full Phase 9 external API layer:

- Versioned REST API (/api/v2/) with OpenAPI 3.1 spec; enum arrays for
  widget_type, event_type, annotation category drawn live from registry tables
- OAuth 2.0 client credentials flow (/api/v2/token); hub:*:write scopes
  gated on active HubCapabilityManifest FK
- API key management: SHA256-hashed tokens, key_prefix for display,
  one-time reveal on creation, revocation support
- TypeScript and Python consumer SDKs generated from registry tables
  (/api/v2/sdk/ihf-client.ts, /api/v2/sdk/ihf-client.py)
- Webhook delivery: HMAC-SHA256 signing, append-only webhook_deliveries,
  fire-and-forget dispatch via forkIO, 3-retry logic
- Admin API dashboard with 24h stats (request count, error rate, last seen)
- Rate limiting (per-minute) and daily quota enforcement via api_request_log
- Schema migration: api_consumers, api_keys, webhook_subscriptions (CHECK
  constraint on 6 framework lifecycle topics), webhook_deliveries
  (append-only trigger), api_request_log
- ARCHITECTURE-LAYERS.md scorecard: 3.34 → 3.41 (approaching Strong)
- contracts/functional/interaction-reporting-v1.md extended with Phase 9
  endpoint catalogue and 422 validation error format

GAAF: no bare TEXT discriminators; webhook event_type uses CHECK constraint
over 6 allowed framework lifecycle topic strings (not widget event types).

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

70 lines
3.5 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module Web.View.ApiConsumers.Index where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data IndexView = IndexView { consumers :: ![ApiConsumer] }
instance View IndexView where
html IndexView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">API Consumers</h1>
<p class="text-sm text-gray-500 mt-1">External systems authenticated against /api/v2/</p>
</div>
<a href={NewApiConsumerAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
New Consumer
</a>
</div>
<div class="mb-4 flex gap-3 text-sm">
<a href={ApiV2OpenApiJsonAction} target="_blank" class="text-indigo-600 hover:underline">openapi.json</a>
<a href={ApiV2DocsAction} target="_blank" class="text-indigo-600 hover:underline">API Docs</a>
<a href={ApiV2SdkIndexAction} class="text-indigo-600 hover:underline">SDKs</a>
<a href={ShowApiDashboardAction} class="text-indigo-600 hover:underline">Dashboard</a>
</div>
<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-600">Name</th>
<th class="text-left px-4 py-3 font-medium text-gray-600">Manifest</th>
<th class="text-left px-4 py-3 font-medium text-gray-600">Rate Limit</th>
<th class="text-left px-4 py-3 font-medium text-gray-600">Quota/day</th>
<th class="text-left px-4 py-3 font-medium text-gray-600">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{forEach consumers renderRow}
</tbody>
</table>
</div>
|]
where
renderRow consumer = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium">
<a href={ShowApiConsumerAction consumer.id} class="text-indigo-600 hover:underline">
{consumer.name}
</a>
</td>
<td class="px-4 py-3 text-gray-500">
{if isJust consumer.hubCapabilityManifestId then "" else "" :: Text}
</td>
<td class="px-4 py-3 text-gray-600">{show consumer.rateLimitPerMinute}/min</td>
<td class="px-4 py-3 text-gray-600">{show consumer.quotaPerDay}</td>
<td class="px-4 py-3">
{if consumer.isActive
then [hsx|<span class="bg-green-100 text-green-700 text-xs px-2 py-0.5 rounded-full">active</span>|]
else [hsx|<span class="bg-gray-100 text-gray-500 text-xs px-2 py-0.5 rounded-full">inactive</span>|]}
</td>
<td class="px-4 py-3 text-right">
<a href={EditApiConsumerAction consumer.id} class="text-gray-400 hover:text-gray-700 text-sm mr-3">Edit</a>
<a href={ApiKeysAction consumer.id} class="text-gray-400 hover:text-gray-700 text-sm">Keys</a>
</td>
</tr>
|]