Files
inter-hub/docs/ihp-realtime.md
tegwick 8b6ce5bbc8 docs: add specification, reference docs, workplan, and agent guidance
Adds all Phase 0 content that was created but never committed:
- CLAUDE.md and SCOPE.md — agent and developer orientation
- specs/TailwindForInteractionHubs_v0.2.md — IHF Tailwind coding guide
- docs/ — five IHP v1.5 reference guides (overview, data, controllers, realtime, ihf-mapping)
- workplans/IHUB-WP-0001 — Phase 1 implementation plan (12 tasks, state-hub synced)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 02:07:13 +01:00

267 lines
9.0 KiB
Markdown

# IHP: Realtime Capabilities
IHP offers four distinct realtime mechanisms. Choosing the right one for a given IHF surface is important — they have very different trade-offs.
---
## Summary Comparison
| Mechanism | Best For | Client Requirement | State Lives |
|-----------|---------|-------------------|-------------|
| AutoRefresh | Read-heavy live dashboards | Vanilla JS (morphdom) | Server / DB |
| DataSync | Reactive JS components embedded in server pages | JS SDK + bundler | DB (via RLS) |
| Server-Side Components | Rich interactive UI with Haskell state machine | Vanilla JS | Server (Haskell) |
| HTMX | Partial page updates triggered by user actions | htmx.js | Server |
---
## 1. AutoRefresh
Enables server-side views to **automatically re-render** when the underlying database changes. No client-side framework or state management required.
### How It Works
1. Wrap an action with `autoRefresh do ...`
2. IHP tracks which DB tables the action's queries touched (via PostgreSQL LISTEN/NOTIFY)
3. A WebSocket connection is established when the page loads
4. On any INSERT/UPDATE/DELETE to tracked tables, the server re-runs the action
5. If the generated HTML differs, the delta is sent over WebSocket; `morphdom` applies it to the DOM with minimal DOM mutations
```haskell
action HubDashboardAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
recentEvents <- query @InteractionEvent
|> filterWhere (#hubId, hubId)
|> orderByDesc #occurredAt
|> limit 20
|> fetch
render HubDashboardView { .. }
```
### Layout Requirements
```haskell
defaultLayout inner = [hsx|
<head>
{autoRefreshMeta}
<script src="/vendor/morphdom.js"></script>
<script src="/vendor/ihp-auto-refresh.js"></script>
</head>
...
|]
```
### Custom SQL Tracking
For `sqlQuery` calls that bypass the query builder, manually register table reads:
```haskell
trackTableRead "interaction_events"
```
### IHF Use Cases
- **Hub operator dashboard** — live widget counts, recent interaction signals, annotation feed
- **Governance ledger view** — live requirement candidate list
- **Triage board** — live annotation queue
AutoRefresh is the right choice for all of these: server-rendered, no JS complexity, updates arrive automatically.
---
## 2. IHP DataSync
A WebSocket-based API that allows **JavaScript to query the database directly**, mirroring the Haskell query builder in JS. For reactive JS components embedded in otherwise server-rendered pages.
### JavaScript API
```javascript
// One-time fetch
const annotations = await query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
.fetch();
// Realtime subscription (React hook)
const { records: annotations } = useQuery(
query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
);
// Mutations
await createRecord('annotations', { widgetId, body, category });
await updateRecord('annotations', id, { body });
await deleteRecord('annotations', id);
```
When `useQuery()` establishes a subscription, all connected clients automatically re-render when the query result changes.
### Security: Row Level Security
DataSync relies on **PostgreSQL Row Level Security (RLS)**. IHP runs with two DB roles:
- Privileged owner role — used by Haskell server code
- `ihp_authenticated` — used by DataSync (what JS clients see)
Example RLS policy:
```sql
-- Users can only see their own annotations
CREATE POLICY "Users can view own annotations" ON annotations
FOR SELECT
USING (actor_id = ihp_user_id());
-- Anyone can view annotations on public widgets
CREATE POLICY "Public widget annotations are readable" ON annotations
FOR SELECT
USING (EXISTS (
SELECT 1 FROM widgets w
WHERE w.id = annotations.widget_id
AND w.policy_scope = 'public'
));
```
### Setup Requirements
Node.js/npm, a frontend bundler (esbuild recommended), DataSync JS SDK, WebSocket + REST API controllers enabled in `FrontController`.
### IHF Use Cases
- **Annotation composer** — reactive annotation entry form with live thread updates
- **Widget feedback inbox** — live feed of all annotations on a specific widget for the widget owner
- **Cross-tenant widget embeds** — widget UIs embedded in third-party pages; RLS isolates each widget's data
---
## 3. Server-Side Components
React-like components that run entirely on the server. Rich interactive UI with a Haskell state machine — no JS state management needed.
### Structure
A component has:
- **State** — a Haskell data type
- **Actions** — a sum type
- **Render** — HSX function from state to HTML
- **`componentDidMount`** — hook for async data loading on connect
```haskell
-- Web/Component/AnnotationComposer.hs
data AnnotationComposer = AnnotationComposer
{ widgetId :: !(Id Widget)
, body :: !Text
, category :: !Text
, submitted :: !Bool
}
data AnnotationComposerController
= UpdateBodyAction { body :: !Text }
| UpdateCategoryAction { category :: !Text }
| SubmitAnnotationAction
deriving (Eq, Show, Data)
instance Component AnnotationComposer AnnotationComposerController where
initialState = AnnotationComposer
{ widgetId = error "set by mount"
, body = ""
, category = "friction"
, submitted = False
}
action state UpdateBodyAction { body } = pure state { body }
action state UpdateCategoryAction { category } = pure state { category }
action state SubmitAnnotationAction = do
createRecord (newRecord @Annotation
|> set #widgetId state.widgetId
|> set #body state.body
|> set #category state.category)
pure state { submitted = True, body = "" }
render state = [hsx|
<div class="annotation-composer">
<textarea
value={state.body}
onInput={callServerAction' UpdateBodyAction { body = inputValue }}
/>
<select onInput={callServerAction' UpdateCategoryAction { category = selectValue }}>
<option value="friction">Friction</option>
<option value="wish">Wish</option>
<option value="defect">Defect</option>
</select>
<button onclick={callServerAction SubmitAnnotationAction}>Submit</button>
</div>
|]
```
### Lifecycle
1. Client triggers action: `callServerAction('SubmitAnnotationAction')`
2. Server evaluates action handler → produces new state
3. Server re-renders → diffs HTML → sends granular DOM patch over WebSocket
4. `morphdom` applies patch (preserves third-party JS library DOM state)
### Registration
```haskell
-- Web/FrontController.hs
routeComponent @AnnotationComposer
```
```haskell
-- In a view:
{component @AnnotationComposer}
```
### Current Status
Described as "early development stage — expect bugs and API changes." Suitable for experimental use but not for core production paths yet.
### IHF Use Cases
- **Annotation entry UI** with live category validation and preview
- **Widget triage panel** with drag-to-sort and inline editing
- **Governance decision form** with multi-step workflow state
---
## 4. HTMX Integration
IHP pairs cleanly with htmx (both are hypermedia-first). Use `respondHtml` (partial renders without layout) in controller actions:
```haskell
action AppendAnnotationAction { widgetId } = do
let annotation = newRecord @Annotation
annotation
|> fill @'["body", "category"]
|> validateField #body nonEmpty
|> ifValid \case
Left _ -> respondHtml [hsx|<div class="error">Body required</div>|]
Right annotation -> do
annotation <- createRecord annotation
respondHtml (renderAnnotation annotation)
```
Reinitialise htmx after Turbolinks page transitions or AutoRefresh updates:
```javascript
document.addEventListener('turbolinks:load', () => htmx.process(document.body));
```
### IHF Use Cases
- **Governance ledger append** — POST an approval/rejection action → server appends to ledger and returns updated fragment
- **Annotation triage status update** — inline status toggle without page reload
- **Requirement candidate promotion** — single-button action that updates status and returns the updated card
---
## Decision Guide for IHF Phase 1
| Surface | Recommended Mechanism | Rationale |
|---------|----------------------|-----------|
| Hub dashboard (live signal counts, recent events) | AutoRefresh | Server-rendered, zero JS complexity |
| Widget detail page (event history) | AutoRefresh | Same — read-heavy, aggregate view |
| Annotation feed (live thread) | AutoRefresh or DataSync | AutoRefresh if all users see the same view; DataSync if per-user filtered |
| Annotation entry form | Server-Side Components (experimental) or HTMX | SSC for rich state machine; HTMX for simple append |
| Widget registration flow | Standard IHP forms | No realtime needed |
| Triage board (drag, inline edit) | Server-Side Components | Rich interaction state |