generated from coulomb/repo-seed
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>
This commit is contained in:
266
docs/ihp-realtime.md
Normal file
266
docs/ihp-realtime.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user