Files
projax/docs/plans/otto-pwa-integration.md
mAi 081784479d 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.
2026-05-17 18:34:49 +02:00

15 KiB
Raw Permalink Blame History

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):

// 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:

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.tsxdist/projax/index.html (overview shell)
  • pwa/web/src/projax-detail.tsxdist/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:
    {
      "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:
    {
      "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.