diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md new file mode 100644 index 0000000..8087b49 --- /dev/null +++ b/.claude/ralph-loop.local.md @@ -0,0 +1,419 @@ +--- +active: true +iteration: 1 +session_id: +max_iterations: 20 +completion_promise: "HEUREKA" +workplan_id: IHUB-WP-0006 +workplan_file: workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md +started_at: "2026-03-29T21:00:29Z" +--- + +## Workplan Status Check — Do This First, Every Iteration + +Read the workplan file at: `workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md` + +Count the task blocks (fenced code blocks with language tag `task`): +- How many tasks exist in total? +- How many have `status: done`? + +If **every task** has `status: done` AND the frontmatter `status` is `done`: + The workplan is complete. Output exactly: HEUREKA + Do nothing else. Stop here. + +Otherwise: continue with the implementation below. + +--- + +## Workplan: IHUB-WP-0006 — IHF Phase 6 — Cross-Framework UI Adaptation Layer +**File:** `workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md` + + +# IHF Phase 6 — Cross-Framework UI Adaptation Layer + +## Goal + +Ensure semantic continuity while the UI stack diversifies. Phase 5 established +AI-assisted distillation within the IHP server-rendered surface. Phase 6 ensures +that widget identity, interaction capture, and annotation capability are +preserved when UI components are authored outside of IHP HSX — React, Vue, or +any JS-based component — without bypassing the IHF core. + +All Phase 6 artifacts are formal contracts rather than free-form conventions. +A widget that participates via an adapter must honour the same identity, +traceability, and event-capture obligations as a native IHP widget. + +## Background + +Phases 1–5 are complete. The IHF core (widget registry, interaction events, +annotations, requirements, decisions, outcomes, agent assistance) is stable. + +The spec (§Phase 6) calls for: +- widget protocol adapters +- metadata emission standards +- client-side SDKs or thin adapters +- cross-framework annotation launcher +- standardized interaction reporting interface + +Artifacts introduced: `WidgetAdapterSpec`, `InteractionReportingContract`, +`EnvelopeEmissionContract`. + +Reference: `specs/InteractionHubFrameworkSpecification_v0.1.md` §Phase 6, +`docs/ihp-overview.md`, `docs/ihp-controllers-views-forms.md`. + +## Phase 6 Exit Criteria (from IHF spec §Phase 6) + +- New UI technologies can participate without bypassing the IHF core +- Widget identity remains stable across frontend evolution +- Annotations and interaction events remain compatible + +## Data Artifacts Introduced (Phase 6) + +`WidgetAdapterSpec`, `InteractionReportingContract`, `EnvelopeEmissionContract` + + +## Tasks + +### T01 — Schema: WidgetAdapterSpec, InteractionReportingContract, EnvelopeEmissionContract + +```task +id: IHUB-WP-0006-T01 +status: todo +priority: high +state_hub_task_id: "8d92f9d5-ec3c-4d9b-b16c-26f938a306e7" +``` + +Add Phase 6 tables to `Application/Schema.sql` and write migration: + +```sql +-- Describes how a specific UI technology (React, Vue, etc.) maps to IHF widget +-- protocol obligations — identity, envelope emission, event reporting. +CREATE TABLE widget_adapter_specs ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + name TEXT NOT NULL UNIQUE, -- e.g. "react-18", "vue-3", "web-component" + framework TEXT NOT NULL, -- e.g. "react", "vue", "vanilla" + version TEXT NOT NULL, -- adapter spec version, e.g. "1.0" + envelope_contract_id UUID REFERENCES envelope_emission_contracts(id), + reporting_contract_id UUID REFERENCES interaction_reporting_contracts(id), + status TEXT NOT NULL DEFAULT 'draft', + -- status values: draft | active | deprecated + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL +); + +CREATE INDEX widget_adapter_specs_framework_idx ON widget_adapter_specs (framework); +CREATE INDEX widget_adapter_specs_status_idx ON widget_adapter_specs (status); + +-- Formalises the rules for how a widget envelope must be emitted: +-- which attributes are required, their format, and version. +CREATE TABLE envelope_emission_contracts ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0", "1.1" + required_attributes JSONB NOT NULL, + -- e.g. ["data-widget-id", "data-view-context", "data-hub-id"] + optional_attributes JSONB NOT NULL DEFAULT '[]', + validation_rules JSONB NOT NULL DEFAULT '{}', + -- machine-readable rules: format checks, presence guards + description TEXT, + status TEXT NOT NULL DEFAULT 'active', + -- status values: draft | active | superseded + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL +); + +-- Standardised REST interface contract for external event and annotation +-- submission — used by non-IHP adapters. +CREATE TABLE interaction_reporting_contracts ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0" + endpoint_path TEXT NOT NULL, -- e.g. "/api/v1/interaction-events" + accepted_event_types JSONB NOT NULL, -- e.g. ["clicked","viewed","submitted"] + required_fields JSONB NOT NULL, + -- minimum payload: widget_id, hub_id, event_type, occurred_at + auth_scheme TEXT NOT NULL DEFAULT 'bearer', + description TEXT, + status TEXT NOT NULL DEFAULT 'active', + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL +); + +-- Link widgets to their adapter spec (null = native IHP widget). +ALTER TABLE widgets + ADD COLUMN adapter_spec_id UUID REFERENCES widget_adapter_specs(id); + +CREATE INDEX widgets_adapter_spec_id_idx ON widgets (adapter_spec_id); +``` + +**Exit criteria:** `migrate` runs cleanly; all Phase 6 types available in GHCi. + + +### T02 — EnvelopeEmissionContract: formalise widgetEnvelope as a versioned contract + +```task +id: IHUB-WP-0006-T02 +status: todo +priority: high +state_hub_task_id: "298af675-550b-480b-bed6-05efc79cd0c9" +``` + +1. Seed the canonical v1.0 `EnvelopeEmissionContract` record in a migration: + - `required_attributes: ["data-widget-id", "data-view-context", "data-hub-id"]` + - `optional_attributes: ["data-policy-scope", "data-widget-version"]` + - `validation_rules: {data-widget-id: "uuid", data-hub-id: "uuid"}` +2. Update the `widgetEnvelope` helper (`Web/View/Helpers.hs` or equivalent) to + read the active contract version from DB (or config) and assert required + attributes at render time — log a warning (not crash) if any are missing. +3. Add `EnvelopeEmissionContractsController`: + - `index`: table of contract versions with status badges + - `show`: full required/optional attributes and validation rules as formatted + JSON panels + - Read-only (contracts are immutable once active; a new version supersedes) +4. Link from global nav under "Contracts" + +**Exit criteria:** Active contract record exists in DB; widgetEnvelope validates +against it; contract index/show pages render correctly. + + +### T03 — InteractionReportingContract: REST endpoint for external event submission + +```task +id: IHUB-WP-0006-T03 +status: todo +priority: high +state_hub_task_id: "f2767465-ff00-48be-b2dc-5bf3b179cca9" +``` + +1. Seed the canonical v1.0 `InteractionReportingContract`: + - `endpoint_path: "/api/v1/interaction-events"` + - `accepted_event_types: ["clicked","viewed","submitted","dismissed","errored"]` + - `required_fields: ["widget_id","hub_id","event_type","occurred_at"]` +2. Add `Api.InteractionEventsController` (separate from the web controller): + - `POST /api/v1/interaction-events` — JSON body, Bearer token auth + - Validate payload against the active `InteractionReportingContract` + - Create `InteractionEvent` record + - Return `201 Created` with `{id, widget_id, event_type}` or `422` with + validation errors +3. Register the API route in `FrontController.hs` +4. Add `InteractionReportingContractsController` (read-only, same pattern as T02) + +**Exit criteria:** `POST /api/v1/interaction-events` with a valid payload creates +an `InteractionEvent`; invalid payloads return `422`; contract show page renders. + + +### T04 — WidgetAdapterSpecsController and registry dashboard + +```task +id: IHUB-WP-0006-T04 +status: todo +priority: high +state_hub_task_id: "e84016d0-60c0-48cb-ad70-2c054d2530db" +``` + +1. Scaffold `WidgetAdapterSpecsController`: + - `index`: table of adapters — framework badge, version, status, envelope + contract version, reporting contract version + - `new` / `create`: register a new adapter spec + - `show`: full detail — framework, version, linked contracts, notes, status + - `edit` / `update`: update notes and status only (contracts are immutable + once linked) + - No delete — adapter specs are audit artifacts +2. Validation: + - `name`, `framework`, `version` required + - `status` must be `draft | active | deprecated` +3. On widget `new`/`edit` forms: optional `adapter_spec_id` select (null = native) +4. On widget show page: if `adapter_spec_id` present, show adapter badge with + link to the spec + +**Exit criteria:** Adapter specs can be registered, listed, and viewed; widget +form allows adapter assignment; widget show page renders adapter badge. + + +### T05 — Cross-framework annotation launcher (lightweight JS widget) + +```task +id: IHUB-WP-0006-T05 +status: todo +priority: medium +state_hub_task_id: "fea86955-d5e6-4623-b5cc-f422c266c9cf" +``` + +1. Create `static/js/ihf-annotation-launcher.js` — a self-contained vanilla JS + module (no framework dependency): + - On `DOMContentLoaded`, scan for elements with `data-widget-id` attribute + - Inject a small "annotate" trigger (button or icon) adjacent to each + enrolled element + - On trigger click: open a lightweight inline form (textarea + category + select) and POST to `/annotations` (existing IHP endpoint) via `fetch` + - On success: show a brief confirmation; on error: show inline error message + - Reads `data-hub-id` from the element (or nearest ancestor) for the hub + context +2. The launcher must work in React-rendered pages where IHP does not own the + DOM — it relies solely on `data-widget-id` presence. +3. Include as an optional script tag in the IHP layout (`Web/View/Layout.hs`) + with a feature flag (`IHP_ANNOTATION_LAUNCHER=true`) +4. Document usage in `docs/annotation-launcher.md` + +**Exit criteria:** Launcher script injects annotation triggers on a page with +`data-widget-id` elements; annotation POST succeeds; works from a static HTML +test page (not IHP-rendered). + + +### T06 — React adapter specification and reference example + +```task +id: IHUB-WP-0006-T06 +status: todo +priority: medium +state_hub_task_id: "023269d8-9835-40b4-a394-478a0f36eee0" +``` + +1. Register a `react-18` `WidgetAdapterSpec` record (via migration seed or + admin UI): + - links to envelope v1.0 contract and reporting v1.0 contract + - `status = active` +2. Create `static/js/ihf-react-adapter.js` — a thin React hook + HOC: + - `useWidgetEnvelope(widgetId, hubId, viewContext)` — returns a `ref` and + `data-*` props object conforming to the envelope contract + - `withWidgetEnvelope(WrappedComponent, widgetId, hubId, viewContext)` — HOC + that applies the envelope to the root DOM element + - `useInteractionReporter(widgetId, hubId)` — returns a `reportEvent(type)` + function that POSTs to `/api/v1/interaction-events` +3. Create `docs/react-adapter.md` with usage examples for all three exports +4. Add a test fixture page in `static/` demonstrating a React widget using the + adapter alongside an IHP-rendered widget on the same page + +**Exit criteria:** `useWidgetEnvelope` emits correct `data-*` attributes; +`reportEvent` reaches `/api/v1/interaction-events`; annotation launcher script +picks up the React widget's `data-widget-id`; docs written. + + +### T07 — Adapter compatibility validation dashboard + +```task +id: IHUB-WP-0006-T07 +status: todo +priority: medium +state_hub_task_id: "dc8fa48a-7195-4410-a77e-717b53127c2e" +``` + +1. Add `AdapterCompatibilityDashboardAction { hubId }` to `HubsController` + (AutoRefresh): + - **Adapter summary**: count of registered adapters by status + (draft / active / deprecated) + - **Widget coverage**: total widgets / native IHP / adapter-backed (per + adapter spec), with percentage bars + - **Contract versions in use**: which envelope and reporting contract + versions are active + - **Unassigned widgets**: widgets with no `adapter_spec_id` that have + received events from external origins (heuristic: `user_agent` not + matching IHP server) + - **Stale adapters**: adapter specs with `status=active` but no widgets + assigned in the last 30 days +2. Link from hub Show page alongside Triage / Governance / Antifragility / + Agent dashboards +3. Add "Adapters" link to global nav + +**Exit criteria:** Dashboard renders all five panels; live-updates on widget or +adapter changes; stale adapter detection works. + + +### T08 — Phase 6 gate: tests, consistency, docs + +```task +id: IHUB-WP-0006-T08 +status: todo +priority: high +state_hub_task_id: "90ea4814-7603-4016-be34-d41ae091f7e1" +``` + +1. **Integration tests** (`Test/`): + - EnvelopeEmissionContract create + fetch (required_attributes, validation_rules) + - InteractionReportingContract create + fetch + - `POST /api/v1/interaction-events` — valid payload creates InteractionEvent + - `POST /api/v1/interaction-events` — missing required field returns 422 + - WidgetAdapterSpec create + status transition (draft → active → deprecated) + - Widget with adapter_spec_id: fetch + show renders adapter badge + - Adapter compatibility dashboard: compiles and returns correct widget counts +2. **Consistency sync** via State Hub MCP: + `check_repo_consistency(repo_slug="inter-hub", fix=True)` +3. **Documentation updates:** + - Update `SCOPE.md` current state section: Phase 6 complete + - Write `docs/phase6-summary.md`: what was built, contract model, adapter + pattern, known limitations, Phase 7 readiness +4. **Smoke test checklist:** + - Register a `react-18` adapter spec via UI + - Assign a widget to the adapter + - POST a test interaction event via `curl` to `/api/v1/interaction-events` + - Verify event appears in widget show page + - Open annotation launcher on a page with a React-backed widget + - Confirm adapter compatibility dashboard shows correct coverage + +**Exit criteria:** All tests pass; consistency sync reports no errors; smoke +test completed; SCOPE.md updated. + + +## Phase 6 Dependencies + +- Phases 1–5 schema stable (widget registry, interaction events, and annotation + model required for adapter integration) +- `envelope_emission_contracts` and `interaction_reporting_contracts` must exist + before `widget_adapter_specs` (foreign key; T01 handles both in one migration) +- Contracts (T01–T03) before adapter spec controller (T04) +- Adapter spec controller (T04) before annotation launcher (T05) and React + adapter (T06) — widget assignment UI depends on T04 +- All feature tasks (T01–T07) before gate (T08) + +## Notes + +- **Contracts are immutable once active.** A new version supersedes the old; + old versions remain readable for audit. No in-place edits after status=active. +- **Native IHP widgets are unaffected.** `adapter_spec_id` is nullable. Existing + widgets continue to function exactly as before. +- **The JS adapter is a thin client.** It does not embed a framework build + pipeline. `ihf-react-adapter.js` is a plain ESM module; consumers bundle it + themselves. +- **Auth for the reporting API.** Bearer token scheme. In Phase 6 the token + is a per-hub API key stored in `hubs.api_key` (add column in T01 migration). + Phase 8 (federated) can layer on OAuth. +- **No local JS build toolchain added.** Static JS files are served as-is. + Phase 6 does not introduce npm, webpack, or esbuild into the IHP project. + +--- + +## How to Work + +- Stay strictly within the scope of the workplan above +- Work through tasks in priority order (high → medium → low) +- Use TDD where applicable: write a failing test, make it pass, then refactor +- Use whatever test runner, linter, and build tools this repository already uses +- Consult existing documentation (README, docs/, wiki/, specs/) for context +- Document significant architecture decisions as ADRs if the project uses them + +## Updating Task Status + +As you complete each task, edit the workplan file to update its status: + +``` +status: todo → status: in_progress (when you start it) +status: in_progress → status: done (when it is verified complete) +``` + +When **every task** is `done`, also update the workplan frontmatter: + +``` +status: active → status: done +``` + +## Success Criteria + +Before marking the workplan done and outputting `HEUREKA`, +verify all of the following are true: + +1. Every task block in `workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md` has `status: done` +2. The workplan frontmatter `status` is `done` +3. The full test suite passes with no failures +4. The codebase passes the project's standard code-quality checks + (linting, type checking, formatting — whatever applies to this project) +5. Documentation reflects the implemented behaviour + +Output `HEUREKA` only when all five are genuinely true. + diff --git a/Application/Helper/View.hs b/Application/Helper/View.hs index 8ea6a62..ef5ad3e 100644 --- a/Application/Helper/View.hs +++ b/Application/Helper/View.hs @@ -11,6 +11,15 @@ import Web.Types -- client-side event capture script reads to identify the widget without coupling -- to implementation details. -- +-- The envelope is validated against the v1.0 EnvelopeEmissionContract at render +-- time. Missing required attributes are surfaced as an inline warning banner +-- (development) rather than a hard failure, so layout is preserved in production. +-- +-- Required attributes (contract v1.0): +-- data-widget-id — stable UUID from the widget registry +-- data-view-context — logical UI location +-- data-hub-id — owning hub UUID +-- -- Usage: -- -- @ @@ -18,10 +27,10 @@ import Web.Types -- -- |] -- @ --- --- See docs/widget-envelope-convention.md for the full convention. widgetEnvelope :: Widget -> Html -> Html -widgetEnvelope widget inner = [hsx| +widgetEnvelope widget inner = + let warnings = envelopeContractWarnings widget + in [hsx|
+ {renderEnvelopeWarnings warnings} {inner}
|] + +-- | Validate a Widget record against EnvelopeEmissionContract v1.0 required +-- attributes. Returns a list of human-readable warning messages for any +-- attribute that is missing or empty. An empty list means the widget is +-- contract-compliant. +envelopeContractWarnings :: Widget -> [Text] +envelopeContractWarnings widget = catMaybes + [ if isNothing widget.viewContext || widget.viewContext == Just "" + then Just "envelope:v1.0 — data-view-context is missing (set widget.viewContext)" + else Nothing + -- data-widget-id and data-hub-id are always present (non-nullable fields) + ] + +renderEnvelopeWarnings :: [Text] -> Html +renderEnvelopeWarnings [] = mempty +renderEnvelopeWarnings ws = [hsx| +
+ Envelope contract warning: + {forEach ws (\w -> [hsx|
{w}
|])} +
+|] + +-- | Status badge colour for WidgetAdapterSpec and contract status values. +adapterStatusBadge :: Text -> Text +adapterStatusBadge "active" = "bg-green-100 text-green-800" +adapterStatusBadge "draft" = "bg-yellow-100 text-yellow-800" +adapterStatusBadge "deprecated" = "bg-gray-100 text-gray-500" +adapterStatusBadge "superseded" = "bg-gray-100 text-gray-400" +adapterStatusBadge _ = "bg-gray-100 text-gray-600" diff --git a/Web/Controller/ApiInteractionEvents.hs b/Web/Controller/ApiInteractionEvents.hs new file mode 100644 index 0000000..1481ea2 --- /dev/null +++ b/Web/Controller/ApiInteractionEvents.hs @@ -0,0 +1,104 @@ +module Web.Controller.ApiInteractionEvents where + +import Web.Types +import Generated.Types +import IHP.Prelude +import IHP.ControllerPrelude +import Data.Aeson (object, (.=)) +import qualified Data.Text as T +import Network.Wai (requestMethod, requestHeaders) + +-- | Accepted event types per InteractionReportingContract v1.0 +apiAcceptedEventTypes :: [Text] +apiAcceptedEventTypes = ["clicked", "viewed", "submitted", "dismissed", "errored"] + +instance Controller ApiInteractionEventsController where + + action CreateApiInteractionEventAction = do + -- Method guard — only POST accepted. + when (requestMethod ?request /= "POST") do + setStatus 405 + respondJson (object ["error" .= ("Method not allowed" :: Text)]) + + -- Bearer token auth — validate against hub.api_key. + let authHeader = lookup "Authorization" (requestHeaders ?request) + let mApiKey = authHeader >>= \h -> + let t = cs h :: Text + in if "Bearer " `T.isPrefixOf` t + then Just (T.drop 7 t) + else Nothing + + case mApiKey of + Nothing -> do + setStatus 401 + respondJson (object ["error" .= ("Authorization: Bearer required" :: Text)]) + Just apiKey -> do + mHub <- query @Hub + |> filterWhere (#apiKey, Just apiKey) + |> fetchOneOrNothing + case mHub of + Nothing -> do + setStatus 401 + respondJson (object ["error" .= ("Invalid or unknown API key" :: Text)]) + Just hub -> createEventForHub hub + +createEventForHub :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?respond :: Respond, ?request :: Request) => Hub -> IO ResponseReceived +createEventForHub hub = do + -- Validate required fields per contract v1.0 + widgetIdText <- paramOrNothing @Text "widget_id" + eventType <- paramOrNothing @Text "event_type" + _occurredAt <- paramOrNothing @Text "occurred_at" + + let missing = catMaybes + [ if isNothing widgetIdText then Just ("widget_id" :: Text) else Nothing + , if isNothing eventType then Just "event_type" else Nothing + , if isNothing _occurredAt then Just "occurred_at" else Nothing + ] + + unless (null missing) do + setStatus 422 + respondJson (object + [ "error" .= ("Missing required fields" :: Text) + , "missing" .= missing + ]) + + let Just wIdText = widgetIdText + Just evType = eventType + + unless (evType `elem` apiAcceptedEventTypes) do + setStatus 422 + respondJson (object + [ "error" .= ("Unacceptable event_type" :: Text) + , "accepted" .= apiAcceptedEventTypes + ]) + + -- Resolve widget — must belong to this hub. + case readMay wIdText of + Nothing -> do + setStatus 422 + respondJson (object ["error" .= ("widget_id must be a valid UUID" :: Text)]) + Just rawId -> do + let wId = Id rawId :: Id Widget + mWidget <- fetchOneOrNothing wId + case mWidget of + Nothing -> do + setStatus 422 + respondJson (object ["error" .= ("Widget not found" :: Text)]) + Just widget -> do + when (widget.hubId /= hub.id) do + setStatus 403 + respondJson (object ["error" .= ("Widget does not belong to this hub" :: Text)]) + + event <- newRecord @InteractionEvent + |> set #widgetId widget.id + |> set #eventType evType + |> set #actorType "external_adapter" + |> createRecord + + setStatus 201 + respondJson (object + [ "id" .= event.id + , "widget_id" .= event.widgetId + , "event_type" .= event.eventType + , "occurred_at" .= event.occurredAt + ]) diff --git a/Web/Controller/EnvelopeEmissionContracts.hs b/Web/Controller/EnvelopeEmissionContracts.hs new file mode 100644 index 0000000..afa2fe5 --- /dev/null +++ b/Web/Controller/EnvelopeEmissionContracts.hs @@ -0,0 +1,21 @@ +module Web.Controller.EnvelopeEmissionContracts where + +import Web.Types +import Web.View.EnvelopeEmissionContracts.Index +import Web.View.EnvelopeEmissionContracts.Show +import Generated.Types +import IHP.Prelude +import IHP.ControllerPrelude + +instance Controller EnvelopeEmissionContractsController where + beforeAction = ensureIsUser + + action EnvelopeEmissionContractsAction = do + contracts <- query @EnvelopeEmissionContract + |> orderByDesc #createdAt + |> fetch + render IndexView { contracts } + + action ShowEnvelopeEmissionContractAction { envelopeEmissionContractId } = do + contract <- fetch envelopeEmissionContractId + render ShowView { contract } diff --git a/Web/Controller/InteractionReportingContracts.hs b/Web/Controller/InteractionReportingContracts.hs new file mode 100644 index 0000000..69103e8 --- /dev/null +++ b/Web/Controller/InteractionReportingContracts.hs @@ -0,0 +1,21 @@ +module Web.Controller.InteractionReportingContracts where + +import Web.Types +import Web.View.InteractionReportingContracts.Index +import Web.View.InteractionReportingContracts.Show +import Generated.Types +import IHP.Prelude +import IHP.ControllerPrelude + +instance Controller InteractionReportingContractsController where + beforeAction = ensureIsUser + + action InteractionReportingContractsAction = do + contracts <- query @InteractionReportingContract + |> orderByDesc #createdAt + |> fetch + render IndexView { contracts } + + action ShowInteractionReportingContractAction { interactionReportingContractId } = do + contract <- fetch interactionReportingContractId + render ShowView { contract } diff --git a/Web/FrontController.hs b/Web/FrontController.hs index 6520910..c28dc81 100644 --- a/Web/FrontController.hs +++ b/Web/FrontController.hs @@ -17,6 +17,7 @@ import Web.Controller.Requirements () import Web.Controller.DecisionRecords () import Web.Controller.DeploymentRecords () import Web.Controller.AgentProposals () +import Web.Controller.ApiInteractionEvents () import Web.Controller.EnvelopeEmissionContracts () import Web.Controller.InteractionReportingContracts () import Web.Controller.WidgetAdapterSpecs () @@ -35,6 +36,7 @@ instance FrontController WebApplication where , parseRoute @DecisionRecordsController , parseRoute @DeploymentRecordsController , parseRoute @AgentProposalsController + , parseRoute @ApiInteractionEventsController , parseRoute @EnvelopeEmissionContractsController , parseRoute @InteractionReportingContractsController , parseRoute @WidgetAdapterSpecsController diff --git a/Web/Routes.hs b/Web/Routes.hs index 052b660..c46d3a2 100644 --- a/Web/Routes.hs +++ b/Web/Routes.hs @@ -35,6 +35,19 @@ instance AutoRoute DeploymentRecordsController instance AutoRoute AgentProposalsController -- Phase 6 — Cross-Framework UI Adaptation + +-- API endpoint: POST /api/v1/interaction-events +instance CanRoute ApiInteractionEventsController where + parseRoute' = do + _ <- string "/api" + _ <- string "/v1" + _ <- string "/interaction-events" + endOfInput + pure CreateApiInteractionEventAction + +instance HasPath ApiInteractionEventsController where + pathTo CreateApiInteractionEventAction = "/api/v1/interaction-events" + instance AutoRoute EnvelopeEmissionContractsController instance AutoRoute InteractionReportingContractsController instance AutoRoute WidgetAdapterSpecsController diff --git a/Web/Types.hs b/Web/Types.hs index 3ff6ec6..59485d1 100644 --- a/Web/Types.hs +++ b/Web/Types.hs @@ -112,6 +112,10 @@ data AgentProposalsController | RejectProposalAction { agentProposalId :: !(Id AgentProposal) } deriving (Eq, Show, Data) +data ApiInteractionEventsController + = CreateApiInteractionEventAction + deriving (Eq, Show, Data) + data EnvelopeEmissionContractsController = EnvelopeEmissionContractsAction | ShowEnvelopeEmissionContractAction { envelopeEmissionContractId :: !(Id EnvelopeEmissionContract) } diff --git a/Web/View/EnvelopeEmissionContracts/Index.hs b/Web/View/EnvelopeEmissionContracts/Index.hs new file mode 100644 index 0000000..838aabc --- /dev/null +++ b/Web/View/EnvelopeEmissionContracts/Index.hs @@ -0,0 +1,67 @@ +module Web.View.EnvelopeEmissionContracts.Index where + +import Web.Types +import Generated.Types +import IHP.Prelude +import IHP.ViewPrelude +import Application.Helper.View (adapterStatusBadge) + +data IndexView = IndexView + { contracts :: ![EnvelopeEmissionContract] + } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+
+

Envelope Emission Contracts

+

+ Formalises which data-* attributes every widget envelope must emit. +

+
+
+ → Reporting Contracts + +
+ + {if null contracts + then [hsx|

No contracts found.

|] + else renderTable contracts} + |] + +renderTable :: [EnvelopeEmissionContract] -> Html +renderTable contracts = [hsx| +
+ + + + + + + + + + + {forEach contracts renderRow} + +
VersionRequired AttributesStatusCreated
+
+|] + +renderRow :: EnvelopeEmissionContract -> Html +renderRow c = [hsx| + + + v{c.contractVersion} + + {tshow c.requiredAttributes} + + " text-xs px-2 py-0.5 rounded font-medium"}> + {c.status} + + + {show c.createdAt} + +|] diff --git a/Web/View/EnvelopeEmissionContracts/Show.hs b/Web/View/EnvelopeEmissionContracts/Show.hs new file mode 100644 index 0000000..9cde29c --- /dev/null +++ b/Web/View/EnvelopeEmissionContracts/Show.hs @@ -0,0 +1,59 @@ +module Web.View.EnvelopeEmissionContracts.Show where + +import Web.Types +import Generated.Types +import IHP.Prelude +import IHP.ViewPrelude +import Application.Helper.View (adapterStatusBadge) + +data ShowView = ShowView + { contract :: !EnvelopeEmissionContract + } + +instance View ShowView where + html ShowView { .. } = [hsx| +
+ + ← Envelope Contracts + +
+ +
+

+ Envelope Contract v{contract.contractVersion} +

+ " text-xs px-2 py-0.5 rounded font-medium"}> + {contract.status} + +
+ + {forEach (contractDescription contract) (\d -> [hsx| +

{d}

+ |])} + +
+
+

Required Attributes

+
{tshow contract.requiredAttributes}
+
+ +
+

Optional Attributes

+
{tshow contract.optionalAttributes}
+
+ +
+

Validation Rules

+
{tshow contract.validationRules}
+
+
+ +
+ Created: {show contract.createdAt} +
+ |] + +contractDescription :: EnvelopeEmissionContract -> [Text] +contractDescription c = case c.description of + Just d -> [d] + Nothing -> [] diff --git a/Web/View/InteractionReportingContracts/Index.hs b/Web/View/InteractionReportingContracts/Index.hs new file mode 100644 index 0000000..8142f04 --- /dev/null +++ b/Web/View/InteractionReportingContracts/Index.hs @@ -0,0 +1,69 @@ +module Web.View.InteractionReportingContracts.Index where + +import Web.Types +import Generated.Types +import IHP.Prelude +import IHP.ViewPrelude +import Application.Helper.View (adapterStatusBadge) + +data IndexView = IndexView + { contracts :: ![InteractionReportingContract] + } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+
+

Interaction Reporting Contracts

+

+ Defines the REST endpoint and payload schema for external adapter event submission. +

+
+ + ← Envelope Contracts + +
+ + {if null contracts + then [hsx|

No contracts found.

|] + else renderTable contracts} + |] + +renderTable :: [InteractionReportingContract] -> Html +renderTable contracts = [hsx| +
+ + + + + + + + + + + + {forEach contracts renderRow} + +
VersionEndpointAuthStatusCreated
+
+|] + +renderRow :: InteractionReportingContract -> Html +renderRow c = [hsx| + + + v{c.contractVersion} + + {c.endpointPath} + {c.authScheme} + + " text-xs px-2 py-0.5 rounded font-medium"}> + {c.status} + + + {show c.createdAt} + +|] diff --git a/Web/View/InteractionReportingContracts/Show.hs b/Web/View/InteractionReportingContracts/Show.hs new file mode 100644 index 0000000..049783b --- /dev/null +++ b/Web/View/InteractionReportingContracts/Show.hs @@ -0,0 +1,76 @@ +module Web.View.InteractionReportingContracts.Show where + +import Web.Types +import Generated.Types +import IHP.Prelude +import IHP.ViewPrelude +import Application.Helper.View (adapterStatusBadge) + +data ShowView = ShowView + { contract :: !InteractionReportingContract + } + +instance View ShowView where + html ShowView { .. } = [hsx| +
+ + ← Reporting Contracts + +
+ +
+

+ Reporting Contract v{contract.contractVersion} +

+ " text-xs px-2 py-0.5 rounded font-medium"}> + {contract.status} + +
+ + {forEach (contractDescription contract) (\d -> [hsx| +

{d}

+ |])} + +
+
+

Endpoint

+
+ POST + {contract.endpointPath} +
+
Auth: {contract.authScheme}
+
+ +
+

Required Fields

+
{tshow contract.requiredFields}
+
+ +
+

Accepted Event Types

+
{tshow contract.acceptedEventTypes}
+
+
+ +
+

Example Request

+
curl -X POST {contract.endpointPath} \
+  -H "Authorization: Bearer <hub-api-key>" \
+  -H "Content-Type: application/json" \
+  -d '{"{"}
+    "widget_id": "<uuid>",
+    "hub_id": "<uuid>",
+    "event_type": "clicked",
+    "occurred_at": "2026-03-29T12:00:00Z"
+{"}"}'
+
+ +
+ Created: {show contract.createdAt} +
+ |] + +contractDescription :: InteractionReportingContract -> [Text] +contractDescription c = case c.description of + Just d -> [d] + Nothing -> [] diff --git a/workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md b/workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md index e76bde2..20f6e30 100644 --- a/workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md +++ b/workplans/IHUB-WP-0006-ihf-phase6-cross-framework-ui-adaptation.md @@ -62,7 +62,7 @@ Reference: `specs/InteractionHubFrameworkSpecification_v0.1.md` §Phase 6, ```task id: IHUB-WP-0006-T01 -status: todo +status: done priority: high state_hub_task_id: "8d92f9d5-ec3c-4d9b-b16c-26f938a306e7" ```