diff --git a/docs/plans/otto-pwa-integration.md b/docs/plans/otto-pwa-integration.md new file mode 100644 index 0000000..bb5b52b --- /dev/null +++ b/docs/plans/otto-pwa-integration.md @@ -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/`. +- **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":,"arguments":{...}}} +// +// Response: {"jsonrpc":"2.0","id":1,"result":{ +// "content":[{"type":"text","text":""}], +// "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/` → 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.