docs(phase 4c-A): otto-PWA integration survey + recommendation

Phase A of the 4c task brief. Survey found the integration already
exists and ships (mAi#228, 2026-05-15) — the question is which slice
deepens it next.

Plan covers:
- Otto-PWA structural notes (Go backend + Bun/TS frontend in m/mAi, not
  m/otto — ADR-006 moved PWA code into mAi)
- Existing MCP-consumption pattern (Bearer-token JSON-RPC bridge,
  graceful 501 degradation, 4 endpoints registered, frontend shells +
  client TS, live at https://otto.msbls.de/projax/)
- 3 deepening slices: (S1) timeline surface, (S2) CalDAV writeback,
  (S3) dated docs quick-add
- Recommendation: ship (S1) first — Phase 4a just landed /timeline
  in projax web, the data + aggregation logic exist, exposing via MCP
  is a clean wrap with no schema or auth model change
- Impl plan if greenlit: 3 slices across projax + mAi with cross-repo
  deploy verification

Out of scope until head greenlights: writing any code in m/mAi.
This commit is contained in:
mAi
2026-05-17 18:34:49 +02:00
parent 54d2720f91
commit 081784479d

View File

@@ -0,0 +1,212 @@
# Otto-PWA ↔ projax integration
**Status:** Phase A survey, 2026-05-17 (task t-projax-4c-otto-pwa)
**Author:** knuth (coder, projax)
**Scope:** survey + recommend next slice. No code in m/mAi until head greenlights.
## TL;DR — the integration already exists; the open question is which deepening slice ships next
The Phase A task brief asks for "23 candidate integration shapes" and a recommendation. The honest finding from the survey is that **the shape was decided and shipped in mAi#228 (2026-05-15)** — option (b): an Otto-PWA Go-backend proxy in front of projax's MCP-RPC endpoint, with a top-level "Projax" tab in the bottom nav and a per-item detail page. It is live at `https://otto.msbls.de/projax/`.
So the right framing for §3 is not "which integration shape" but "which next slice deepens the existing integration." Three candidates below, recommendation in §4.
If head reads this and wants the original three-shapes-from-scratch framing anyway, say so — but pretending the base shape is an open question would waste a deploy and risk breaking the live tab.
## §1 Otto-PWA structural notes
Even though the task said "m/otto," the PWA frontend code does **not** live in `/home/m/dev/otto` — that repo is otto's persona / orchestration content. Per `otto/CLAUDE.md` ADR-006 (2026-05-15), the PWA itself moved into `m/mAi`.
- **Code root:** `/home/m/dev/mAi/pwa/` (Go backend) + `/home/m/dev/mAi/pwa/web/` (frontend).
- **Backend:** Go single binary. Routes registered in `pwa/main.go`. Per-integration files (`gitea.go`, `caldav.go`, `mvoice_proxy.go`, `supabase.go`, `projax.go`) keep wire-format details out of the main file.
- **Frontend:** TypeScript + a hand-rolled JSX runtime (`pwa/web/src/jsx.ts`), built with Bun (`pwa/web/bunfig.toml`, `build.ts`). Server-rendered shells emit static HTML at build time; per-page client JS lives under `pwa/web/src/client/`. No SvelteKit, no React. Pages are addressable HTML files (`/projax/index.html`, `/projax/i/index.html`) — the directory-fallback in `handleStatic` serves the shared shell for arbitrary `/projax/i/<path>`.
- **Auth model:** every browser→backend request carries a bearer token (`OTTO_PWA_TOKEN`, stored in `localStorage`, prompted on first visit). The backend is *not* Supabase-cookie-gated like projax itself — it's a single-tenant token. Cross-app integrations (projax, gitea, supabase) keep their own server-side credentials; the user never sees them.
- **Routes / nav:** bottom-nav has 5 slots: `Chat`, `Lists`, `[capture]`, `Today`, `Projax`. The Projax slot is the integration's surface; `Today` is the daily-driver overview.
- **Mobile UX conventions:** pull-to-refresh (`pull-to-refresh.ts`), visibility-change re-fetch, sessionStorage filter persistence, dialog-based settings, dark-by-default palette. Card-based layout — every panel is a `.projax-card` rounded rectangle. No animations on state transitions.
## §2 Existing MCP-consumption pattern
The PWA already talks to projax's MCP-RPC end-to-end. The pattern is documented in `m/mAi/docs/plans/pwa-projax-surface.md`; the implementation lives in `mAi/pwa/projax.go` (505 LOC) + `projax_test.go` (280 LOC).
Wire format (mirrored verbatim for any future MCP-backed integration):
```go
// POST https://projax.msbls.de/mcp/rpc
// Authorization: Bearer ${PROJAX_MCP_TOKEN}
// Body: {"jsonrpc":"2.0","id":1,"method":"tools/call",
// "params":{"name":<tool>,"arguments":{...}}}
//
// Response: {"jsonrpc":"2.0","id":1,"result":{
// "content":[{"type":"text","text":"<json string>"}],
// "isError":false}}
//
// Double-unmarshal: response.result.content[0].text is a JSON string
// that needs to be parsed into the typed result struct.
```
Configuration (env, never committed):
- `PROJAX_MCP_URL` (default `https://projax.msbls.de/mcp`)
- `PROJAX_MCP_TOKEN` (no default; missing ⇒ every `/otto/projax/*` returns 501)
Graceful degradation pattern: `projaxClient.configured()` is checked at handler entry; when false the route returns 501 with `"projax disabled — no PROJAX_MCP_TOKEN"` and the rest of the PWA keeps working. Same pattern used by `sweep.go` / `push.go`.
Currently surfaced MCP tools (from projax's `mcp/tools.go` set of 10): `tree`, `list_items`, `get_item`, `update_item`. Not yet surfaced: `create_item`, `delete_item`, `list_links`, `add_link`, `remove_link`, `search`.
Routes registered in `pwa/main.go`:
```go
register("GET /otto/projax/tree", s.requireAuth(s.handleProjaxTree))
register("GET /otto/projax/items", s.requireAuth(s.handleProjaxListItems))
register("GET /otto/projax/items/{path...}", s.requireAuth(s.handleProjaxGetItem))
register("PATCH /otto/projax/items/{path...}", s.requireAuth(s.handleProjaxUpdateItem))
```
Frontend entrypoints (build.ts):
- `pwa/web/src/projax.tsx``dist/projax/index.html` (overview shell)
- `pwa/web/src/projax-detail.tsx``dist/projax/i/index.html` (detail shell)
- `pwa/web/src/client/projax.ts` (overview logic, 232 LOC) — fetches `/mai-pwa/projax/tree?depth=2`, applies status+management filters in-memory, renders DAG-root cards.
- `pwa/web/src/client/projax-detail.ts` (detail logic, 285 LOC) — fetches `/mai-pwa/projax/items/{path}`, renders item card + content_md + quick-actions (status/pin/archive) + links table.
Production state (probed 2026-05-17):
- `GET https://otto.msbls.de/projax/` → 200 OK (shell loads).
- `GET https://otto.msbls.de/mai-pwa/projax/tree` → 401 (auth gate fires; route registered).
- `GET https://otto.msbls.de/projax/i/<path>` → 200 (directory-fallback serves the shared shell).
## §3 Candidate deepening slices
What's NOT surfaced today, ordered by user value / scope:
### (S1) Surface projax `/timeline` in the PWA — chronological "what's happening by date"
Phase 4a just shipped a `/timeline` view in projax web: dated VTODOs + VEVENTs + dated item_links + creation markers, braided into a single chronological spine with filter chips. It is the new "what's happening across all my projects, by day" surface.
The PWA cannot show this today because:
- projax MCP exposes `tree` / `list_items` / `get_item` but **no** `timeline` tool.
- PWA backend has no `/otto/projax/timeline` endpoint.
- PWA frontend has no `/timeline/` shell or client JS.
**Pros:** highest user-visible win; the data already exists in projax; the survey logic is centralised in `web/timeline.go` (~700 LOC) and only needs an MCP wrapper; uses the same Bearer-token bridge already in place; complements Today (which is otto-centric) by adding a project-centric chronological view; m can finally see "what's due today on house1 and what's due next week on paliad" on his phone.
**Cons:** widest scope — touches projax (new MCP tool + tests + redeploy), the PWA backend (new endpoint + handler + tests), the PWA frontend (new TSX page + client TS + Bun-build wire-up + nav slot decision: replace existing tab or add 6th? probably add a "Timeline" sub-link on the Projax overview rather than a new bottom-nav slot to keep nav from sprawling).
**Where in projax:** add `timeline` MCP tool in `mcp/tools.go` returning `{days: [{date, label, sticky, rows: [{kind, item, ...}]}], from, to, total_rows}`. Reuse `Server.buildTimeline()` internally. Schema mirrors the existing `timelinePayload` JSON shape.
### (S2) CalDAV todo writeback from the PWA — complete / edit / delete
Right now the PWA can read VTODOs only indirectly (via item detail's Links section). Phase 4a's dashboard + timeline grew per-row complete / edit / delete affordances in the projax web UI. Putting the same three actions in the PWA detail page would let m tick a task off from his phone.
**Pros:** highest "I actually use this every day" value. The CalDAV writeback logic is centralised in `web/caldav.go::handleCalDAVTodoAction`; wrapping it as an MCP tool is a clear refactor. No new frontend page — just three buttons on the existing detail card.
**Cons:** new MCP write-tool requires careful auth + the `(item, calendar)` ownership guard already in the web handler; the PWA needs to know each task's `(calendar_url, uid)` which today is fetched via projax web only. We'd need to either surface VTODO rows via a new MCP tool or have the PWA fetch them via a separate CalDAV path.
**Where in projax:** new MCP tools `list_caldav_todos(item_path)` and `caldav_todo_action(item_path, calendar_url, uid, action, summary?, due?)`. Both wrap existing `caldav.go` logic.
### (S3) Dated documents quick-add on PWA detail
Item detail shows links read-only today. Phase 3c shipped dated `item_links` (event_date) with quick-add via `/i/{path}/links/add`. MCP `add_link` already supports event_date. The PWA could add a small "Add dated link" form to the detail page using existing MCP without any projax-side work.
**Pros:** smallest scope — no projax change, just PWA frontend + a new endpoint that proxies the existing `add_link` MCP tool. Reuses the read-mostly aesthetic.
**Cons:** narrow audience — m mostly creates dated links from his desktop next to the actual document path. The mobile use case ("here's a URL I want to anchor to today") is real but infrequent. Hard to justify ahead of (S1) or (S2).
## §4 Recommendation
**Ship (S1) timeline next.**
Three reasons in order:
1. **Phase 4a just landed a self-contained chronological surface in projax web.** It is the highest-value new view m has gotten this month, and the asymmetry between "available on desktop" and "missing on phone" is acute — the timeline answers "what's today / tomorrow / this week" which is exactly the question one asks on a phone.
2. **The data + aggregation logic exist** in `web/timeline.go`. Exposing it via MCP is a clean wrap: take the same `buildTimeline` call, emit it as JSON over RPC. No new business logic, no new caching, no schema work.
3. **It's an MCP-only extension on the projax side** — no migration, no auth model change, no Dokploy rewiring. The PROJAX_MCP_TOKEN is already provisioned per mAi#228's deployment.
(S2) CalDAV writeback is the higher day-to-day value but has wider blast radius (write paths through a new MCP tool, ownership guard, ETag flow). It's the natural slice *after* (S1).
(S3) dated documents quick-add is the smallest, but the use case isn't pressing enough to leapfrog (S1) or (S2).
## §5 What needs to land in projax for (S1)
If head greenlights timeline first:
1. **New MCP tool `timeline`** in `mcp/tools.go`. Signature:
```json
{
"name": "timeline",
"description": "Chronological spine of dated content (VTODOs, VEVENTs, dated item_links, creation markers).",
"arguments": {
"from": "YYYY-MM-DD (optional, default now-30d)",
"to": "YYYY-MM-DD (optional, default now+90d)",
"order": "asc|desc (optional, default desc)",
"kinds": "[todo|event|doc|creation] (optional, default all)",
"tag": "string (optional)",
"mgmt": "string (optional)",
"has": "string (optional)",
"q": "string (optional)"
}
}
```
2. **Return shape** — JSON mirror of `timelinePayload`:
```json
{
"days": [{
"date": "2026-05-17",
"label": "Today",
"sticky": "today",
"rows": [{
"kind": "todo|event|doc|creation",
"item_path": "work.paliad",
"summary": "…",
"todo": {"uid", "calendar_url", "due", "status", "summary"},
"event": {"start", "all_day", "duration_hint", "summary", "location", "recurring"},
"link": {"id", "ref_type", "ref_id", "note", "per"},
"far_future": false
}]
}],
"from": "2026-04-17", "to": "2026-08-15", "total_rows": 42
}
```
3. **Reuse** `Server.buildTimeline()` — extract the assembly path into a `Store`-or-server helper that the MCP tool calls. The existing handler stays unchanged; both paths converge on the helper.
4. **Tests**: `mcp/tools_test.go` with a unit test per source (empty, with todos, with events, with docs, with filters, with order toggle). Same fixtures as the existing timeline tests can be reused.
5. **No schema change. No migration. No Dokploy env change** — the PROJAX_MCP_TOKEN already gates the surface.
CORS allowlist on `/mcp/rpc`: not needed. The PWA backend calls projax server-to-server.
## §6 Implementation plan if greenlit (S1)
### Slice 1 — projax MCP `timeline` tool
- `mcp/tools.go`: register the tool, add the args struct, call into `Server.buildTimeline()` (refactor needed: factor the `buildTimeline` body out of `Server` if it's not already callable from MCP context — currently it's a server method that takes a `TreeFilter` and emits a `*timelinePayload`. Should be cleanly callable.
- `mcp/timeline_view.go`: typed JSON shape that mirrors `timelinePayload` 1:1 for the MCP response.
- `mcp/tools_test.go`: 6 unit tests (empty/todos/events/docs/filter/order).
- Branch `mai/knuth/4c-mcp-timeline`. Standard projax deploy + verify.
### Slice 2 — PWA backend `/otto/projax/timeline`
*In `m/mAi`, after Slice 1 ships:*
- `pwa/projax.go`: new `Timeline(ctx, args) (timelineResult, error)` typed wrapper using the existing `callTool` plumbing.
- `pwa/main.go`: `register("GET /otto/projax/timeline", s.requireAuth(s.handleProjaxTimeline))`. Same auth + 501 graceful-degradation pattern.
- `pwa/projax_test.go`: new test cases for the wrapper + handler.
### Slice 3 — PWA frontend `/projax/timeline/`
*Same PR as Slice 2:*
- `pwa/web/src/projax-timeline.tsx`: shell page with date-header spine layout.
- `pwa/web/src/client/projax-timeline.ts`: fetch `/mai-pwa/projax/timeline`, render days, wire filter chips (kinds, order toggle).
- `pwa/web/build.ts`: wire `dist/projax/timeline/index.html` + client bundle.
- `pwa/web/src/projax.tsx`: add a "Timeline" link in the overview topbar (NOT a new bottom-nav slot — nav already has 5).
### Cross-cutting
- Test plan: probe projax MCP with a `tools/list` call after Slice 1 deploy to confirm `timeline` shows up; live curl with the Bearer token to confirm the tool runs and returns the expected shape.
- Standard cross-repo deploy verification: projax container task-ID delta after Slice 1; mAi (otto-pwa) container task-ID delta after Slices 2+3; live probe of `https://otto.msbls.de/projax/timeline/` returning the new shell HTML.
### Out of scope for 4c
- CalDAV writeback from PWA (S2 — separate task).
- Documents quick-add on PWA detail (S3 — separate task).
- Push notifications on dated-item triggers.
- Real-time updates / SSE — visibility-change re-fetch + pull-to-refresh stays the v1 pattern.
- New bottom-nav slot — keep 5.
## References
- `/home/m/dev/mAi/docs/plans/pwa-projax-surface.md` — the original mAi#228 design (planck, 2026-05-15). Cites `concept-promotion-demotion-projects` from mBrian for vocabulary alignment.
- `/home/m/dev/mAi/pwa/projax.go` — backend MCP proxy (shipping).
- `/home/m/dev/mAi/pwa/web/src/{projax,projax-detail}.tsx` — frontend shells (shipping).
- `/home/m/dev/projax/mcp/tools.go` — current 10-tool MCP surface (no `timeline` yet).
- `/home/m/dev/projax/web/timeline.go` — buildTimeline + payload shapes shipped in 4a.
- projax `docs/design.md` §7 (MCP surface), §12 (Timeline view), §13 (Theming).
- otto `CLAUDE.md` ADR-006 — PWA code lives in m/mAi, not m/otto.