7 Commits

Author SHA1 Message Date
mAi
dc4863faca chore(mgmt teardown step 5+6): drop stale dokploy comment + append DONE log
Per docs/plans/mgmt-teardown.md §4 steps 5 + 6.

Step 5: deploy/dokploy.yaml — stale "federated with mgmt.msbls.de" line
in the header comment replaced with the current host-scoped /login cookie
model. The mgmt federation never happened in projax anyway (projax
cookies are host-scoped, no Domain attribute).

Step 6: append a "DONE 2026-05-16" section to docs/plans/mgmt-teardown.md
recording every step's commit hash across both repos, the head-approved
deviation from §4 step 1 (SvelteKit-side redirect instead of Dokploy
Traefik labels — Dokploy config is UI-only), verification curls, and the
post-teardown janitorial that's out of scope for the worker (env-var
cleanup in Dokploy, DNS at m's leisure).

m/msbls.de side merged separately (86bfa61) — three commits:
2941dc4 (redirect), <previous step's commit covers the rest>.
2026-05-16 01:06:28 +02:00
mAi
dc50823860 feat(phase 3a mcp): MCP surface so mai/otto/Claude can read+write projax
mcp package (new): minimal JSON-RPC 2.0 + MCP-protocol server, tools
delegate to *store.Store (no business-logic duplication).

- handler.go: handleRPC routes initialize / tools/list / tools/call /
  ping / notifications/initialized; Bearer-token middleware; results
  flow through the standard MCP content[].text envelope; tool errors
  surface as isError: true (transport errors stay JSON-RPC errors).
- tools.go: 10 tools — list_items / get_item / create_item /
  update_item / delete_item / list_links / add_link / remove_link /
  search / tree. Multi-parent in/out — parent_paths[] string array,
  resolved per call. itemView/linkView keep the wire shape snake_case
  and stable.
- mcp_test.go + tools_test.go: protocol primitives (no DB) plus a
  full create → get → search → delete round-trip skipping cleanly
  when the DB env is absent. Multi-parent assertion discovers the
  test pair from the live DB rather than hard-coding a row.

store extensions:
- ListByFilters(SearchFilters) with parent_path/tags/management/kind/
  status/q/has_repo/has_caldav predicates.
- Search(q, limit) ranked across title/slug/aliases/content_md.
- GetByPathOrSlug for callers that don't know the full path.
- SoftDeleteCascade refuses on live descendants unless cascade=true.

web:
- New optional Server.MCP http.Handler. main.go mounts an mcp.Server
  when PROJAX_MCP_TOKEN is set; /mcp/* gets a StripPrefix and bypasses
  the Supabase-cookie auth middleware (its own Bearer auth applies).
- Off cleanly when the token is unset.

ops:
- ~/.claude/mcp/projax.sh stdio→HTTP bridge (NDJSON in, NDJSON out,
  Bearer header).
- .mcp.json adds an http-transport entry for clients that speak
  HTTP+MCP natively.
- deploy/dokploy.yaml advertises PROJAX_MCP_TOKEN as a secret.
- docs/design.md §7 added: tool list, multi-parent semantics, env
  contract, transport + bridge.
2026-05-15 17:59:03 +02:00
mAi
1ffbfc6e69 feat(phase 2.d gitea): read-only issue ingest on items with gitea-repo links
gitea package (new): minimal client mirroring caldav's structure
- client.go: token auth, 5s timeout, ErrNotFound
- issues.go: ListIssues(owner, repo, opts) hitting
  /repos/{o}/{r}/issues?type=issues&state=…&since=…, ParseRepoRef,
  RepoHTMLURL. PullRequest-flagged rows dropped server- and client-side.
- httptest stubs covering parse, 404, ParseRepoRef variants.

web wiring:
- Server.Gitea optional GiteaDeps (Client + in-memory 3-min TTL cache
  keyed by owner/repo|state).
- detailIssues iterates every gitea-repo link, sums open issues, captures
  last-30d closed (≤20) into a disclosure. Per-repo failures surface as
  banner; one missing repo never blanks the section.
- relativeTime renders "Nm/h/d ago" / "yesterday" / fallback date.

Templates:
- issues_section.tmpl: per-repo block, header "Issues (n) + ↗ Gitea repo",
  rows with #N · title · labels · milestone · assignees · updated.
  Titles open in new tab.
- detail.tmpl: include the partial when Gitea is on and issues != nil.
- CSS: matches the Tasks section visual language.

main.go: GITEA_URL gates the integration (off when unset). GITEA_URL set
but GITEA_TOKEN missing → refuse to start.

deploy/dokploy.yaml: GITEA_URL env + GITEA_TOKEN secret added.

docs/design.md: new §6 mirroring §5's structure (link model, listing
semantics, caching, env contract, parked items).
2026-05-15 17:27:01 +02:00
mAi
96b61f7ed4 feat(phase 2 caldav): list + link + create CalDAV calendars
m's CalDAV server (dav.msbls.de, SabreDAV) now feeds projax via a thin
read-only-plus-create-on-demand integration. No background sync; tasks
fetched live on detail-page render.

New caldav/ package
- ListCalendars (PROPFIND Depth: 1, filters non-calendar collections)
- ListTodos (REPORT calendar-query for VTODO; hand-rolled iCalendar
  parser for UID/SUMMARY/STATUS/DUE/PRIORITY/LAST-MODIFIED — RFC 5545
  line-folding aware)
- CreateCalendar (MKCALENDAR, 405 → ErrCalendarExists for the "link
  instead" branch)
- httptest-stubbed tests cover all four paths.

Store
- ItemLink shape + LinksByType / LinksByRefType / AddLink / DeleteLink.
  AddLink upserts on (item_id, ref_type, ref_id, rel) so re-linking the
  same calendar is idempotent.

Web
- GET /admin/caldav — discovery + auto-suggested matches + manual
  linker. Suggestion = lowercased displayname == projax slug or title.
- POST /admin/caldav/link — insert item_links row.
- POST /admin/caldav/unlink — delete by link id.
- POST /i/{path}/caldav/create — MKCALENDAR at <base>/<slug>/, then
  AddLink. On 405 (already exists), fall back to link-only.
- Detail page Tasks section: per-calendar block with open VTODOs +
  collapsed completed (30d window). Errors per calendar logged and
  skipped, so one bad calendar does not blank the page.
- nav adds /admin/caldav link.

main.go
- DAV_URL + DAV_USER + DAV_PASSWORD optional. Missing DAV_URL → CalDAV
  off (admin page renders "not configured" notice). DAV_URL set but
  user/pass missing → fail fast at boot.

docs/design.md gains §5 documenting the integration shape.
deploy/dokploy.yaml lists the two new secrets + the env var.

Phase 2.b (writeback / two-way / background sync) is parked.
2026-05-15 16:57:43 +02:00
mAi
840c1760c9 feat(auth): federate with mgmt.msbls.de via Supabase cookies
projax was deployed publicly through Dokploy/Traefik with a Let's
Encrypt cert; the earlier "Tailscale-only" claim was never true. Gate
every request at the application layer using the same Supabase JWT
cookie pair that mgmt.msbls.de issues, so projax inherits SSO without
running its own login.

Middleware (web/auth.go):
- GET <SUPABASE_URL>/auth/v1/user with the access_token cookie or a
  Bearer header. On 2xx → pass through.
- On expiry, swap the refresh_token via /auth/v1/token?grant_type=
  refresh_token and rotate both cookies (Domain=msbls.de, HttpOnly,
  Secure, SameSite=Lax, Path=/, Max-Age=1y). Cookie attributes match
  mgmt/auth.ts verbatim — refreshed sessions stay drop-in compatible
  with the rest of the .msbls.de fleet.
- Anything still invalid → 302 to <PROJAX_LOGIN_URL>?redirectTo=
  <original-absolute-url>. mgmt's safeRedirect() rejects absolute URLs
  and falls back to /, so after login the user lands on mgmt; manual
  click back to projax then succeeds with the fresh cookie. UX is
  rough but functional; broadening mgmt's safeRedirect is parked for a
  separate PR.
- /healthz remains ungated so Dokploy/Traefik probes don't hit the
  redirect.

main.go: enable the middleware only when SUPABASE_URL is set; require
SUPABASE_ANON_KEY when it is (refuse to start otherwise). New env
overrides: PROJAX_LOGIN_URL (default https://mgmt.msbls.de/login),
PROJAX_COOKIE_DOMAIN (default msbls.de). Local dev with no env stays
fully anonymous.

Tests (7 cases, no DB needed): stub Supabase via httptest covers
healthz-open, anonymous-redirect, bad-cookie-redirect, good-cookie
pass-through, Bearer-pass-through, stale-but-refreshable rotation
(verifies cookie Domain/HttpOnly/Secure/SameSite), final fail
redirect.

DB-backed integration tests now honour PROJAX_SKIP_MIGRATE=1 so they
don't deadlock against the live container's auto-migrate during a
deploy window.

README + dokploy.yaml: kill the Tailscale-only claim, document the
federated-auth trust model and the new SUPABASE_* env contract.
2026-05-15 14:58:43 +02:00
mAi
092a56cf24 feat(db): pivot to dedicated projax_admin role
Mirrors the mbrian_admin pattern: the binary connects as a role bounded
to the projax schema, so even a compromised projax process cannot read
mai.workers, otto.*, vault.*, etc.

- 0001: switch grants block from postgres → projax_admin (conditional
  on the role existing — bootstrap still works as superuser before the
  role is created). Wrap `create schema` in a guard so the migration
  is idempotent when re-run as a non-superuser app role that lacks
  database-level CREATE.
- 0005_reown_to_projax_admin.sql: enumerate every projax-namespaced
  object via pg_namespace + pg_class / pg_proc and ALTER OWNER to
  projax_admin. Explicitly scoped — no global REASSIGN OWNED that
  would yank ownership from other projects sharing the postgres role.
  Strips residual postgres grants. No-ops with a NOTICE when the role
  is missing.
- README: new "Manual prerequisite" deploy section. Documents the
  CREATE ROLE statement, the cross-schema USAGE + SELECT grants, AND
  the RLS policy `projax_read ON mai.projects` that's required because
  mai.projects has row-level security with policies scoped to `mai`
  and `anon` only. Without the policy, items_unified silently returns
  zero mai-source rows.
- deploy/dokploy.yaml: DSN comment now reflects projax_admin and
  points at the README prereq.

Verified locally against msupabase with a throwaway projax_admin role:
- 13/13 tests green
- mai.workers SELECT → permission denied
- mai.sessions SELECT → permission denied
- mai.projects SELECT → 59 rows (RLS policy in effect)
- projax.items_unified SELECT → 66 rows (7 projax + 59 mai)
2026-05-15 13:32:56 +02:00
mAi
9466759aeb build: Dockerfile + Dokploy manifest + README
- Multi-stage Dockerfile: golang:1.25-alpine builder → distroless static
  runtime as nonroot. Image weighs ~15 MB. Embeds templates, static
  assets and migrations into the single binary.
- deploy/dokploy.yaml documents the Dokploy app for projax.msbls.de:
  Tailscale-only, healthz path, single replica, secret PROJAX_DB_URL.
  Translates to the Dokploy UI; not auto-applied.
- README rewritten as runbook: env vars, route table, test command,
  deploy notes, trust model (Tailscale + no auth in v1, defer to
  Supabase auth if it ever outgrows the fence), schema summary.
- .dockerignore strips .git, .m, .claude, docs, tests from build ctx.
- .gitignore covers ad-hoc binary and dist artefacts.

Verified locally: docker build succeeds, container responds to /healthz
and / against msupabase via --network host.
2026-05-15 13:26:53 +02:00