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:
212
docs/plans/otto-pwa-integration.md
Normal file
212
docs/plans/otto-pwa-integration.md
Normal 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 "2–3 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.
|
||||
Reference in New Issue
Block a user