feat(phase 4d): public-listing fields so projax becomes the portfolio source of truth
Adds five additive columns on projax.items and propagates them through
every read/write path. flexsiebels.de (and any future portfolio renderer)
can now pull the public set via the MCP `list_items(public=true)` filter
and stop hard-coding project lists.
## Schema (migration 0014)
- public boolean default false (partial index when true)
- public_description text default ''
- public_live_url text default ''
- public_source_url text default ''
- public_screenshots text[] default '{}'
- items_unified view rebuilt to include the five new columns
- items_public_idx PARTIAL INDEX WHERE public = true (5% of rows)
## Store
- Item struct + scan/scanItems extended (5 cols)
- UpdateInput accepts the new fields with full-replace semantics
- new SetPublic(ids, bool) for bulk write
- SearchFilters gains Public *bool — nil = no filter
## MCP
- list_items: new `public` boolean filter (input schema + handler)
- update_item: 5 new partial-update fields (nil pointer = leave alone)
- itemView always emits the 5 fields (even when public=false) so consumers
can preview "what would publish" without a second round-trip
- 2 new integration tests against the DB
## Web
- /i/{path} grows a "Public listing" fieldset: toggle + textarea + 2 URL
inputs + screenshot list editor with add/remove rows + inline JS for
the editor. Values persist when public is off so toggling never
destroys typed-in content.
- /admin/bulk action bar gains "Make public" / "Make private" via a new
select; SQL update is a single statement per action.
- /?public=1 and /?public=0 chip parameters narrow the tree page.
Active() + QueryString() + TogglePublic() round-trip the state.
- parseScreenshotList helper trims + drops empties + preserves order
- 5 integration tests: migration landed, form round-trip, bulk action
round-trip, detail-page affordances, tree-filter narrowing
## docs/design.md §15
Documents the schema, MCP contract, UI surfaces, flexsiebels consumption
pattern, and what's NOT in scope (flexsiebels-side render, asset hosting,
approval workflows).
## Out of scope (per task brief)
- Flexsiebels rendering — separate task in m/flexsiebels.de after this ships
- Asset hosting (projax stores URLs, never bytes — same PER discipline)
- Multi-stage publish workflow (boolean is enough)
This commit is contained in:
@@ -630,6 +630,44 @@ Times stringify into either YYYY-MM-DD (date-only) or full RFC 3339 UTC (timed),
|
||||
|
||||
**Auth:** same `Authorization: Bearer ${PROJAX_MCP_TOKEN}` as the rest of `/mcp/rpc`. No CORS allowlist needed — consumers (PWA backend, future agents) call projax server-to-server.
|
||||
|
||||
## 15. Public listing (Phase 4d)
|
||||
|
||||
projax becomes the source of truth for which items go on m's public portfolio (flexsiebels.de today; any future renderer via MCP). Five new columns on `projax.items`, all default-safe — 95% of items stay private and the partial index keeps the "show me everything public" query cheap.
|
||||
|
||||
**Schema (migration 0014):**
|
||||
|
||||
| Column | Type | Default | Meaning |
|
||||
|---|---|---|---|
|
||||
| `public` | `boolean` | `false` | The toggle |
|
||||
| `public_description` | `text` | `''` | Public-facing prose (markdown); written separately from `content_md` so internal notes never leak |
|
||||
| `public_live_url` | `text` | `''` | Production / demo URL |
|
||||
| `public_source_url` | `text` | `''` | Repo URL when m wants to expose source |
|
||||
| `public_screenshots` | `text[]` | `'{}'` | Ordered list of image URLs (projax stores pointers, never bytes — same PER discipline as §3c) |
|
||||
|
||||
Partial index `items_public_idx ON projax.items (public) WHERE public = true` covers the public-only query. items_unified flows the columns through automatically.
|
||||
|
||||
**MCP contract:**
|
||||
|
||||
- `update_item` accepts any subset of the five new fields as a partial-update patch (nil pointers leave the existing value alone).
|
||||
- `list_items` gains a `public: boolean` filter. `public=true` returns only public items, `public=false` only private, absent returns all (current behaviour).
|
||||
- `get_item` returns all five fields automatically — itemView always includes them, even when `public=false`, so consumers can preview "what would publish" without a second round-trip.
|
||||
|
||||
**UI surfaces:**
|
||||
|
||||
- `/i/{path}` detail page grows a "Public listing" fieldset: toggle + description textarea + live/source URL inputs + screenshot list editor (one row per URL, add/remove buttons, server-side empty-row drop). Values persist when public is off so toggling never destroys typed-in content.
|
||||
- `/admin/bulk` action bar gains a `public-listing` select with "Make public" / "Make private" — bulk apply uses a single UPDATE per action.
|
||||
- `/?public=1` and `/?public=0` chip parameters on the tree page narrow to public/private respectively. `Active()` and `QueryString()` round-trip the state; `TogglePublic()` cycles nil → true → false → nil.
|
||||
|
||||
**Intended flexsiebels consumption pattern:**
|
||||
|
||||
flexsiebels' Go (or Deno) backend POSTs to `https://projax.msbls.de/mcp/rpc` with `Authorization: Bearer ${PROJAX_MCP_TOKEN}`, calls `list_items({public: true})`, renders the response server-side. The PROJAX_MCP_TOKEN is the server-to-server credential — m never sees it. No CORS work needed; calls stay server-to-server like the existing PWA bridge (mAi#228).
|
||||
|
||||
**What does NOT happen here:**
|
||||
|
||||
- Flexsiebels-side rendering — separate task in `m/flexsiebels.de`.
|
||||
- Asset hosting for screenshots — projax stores URLs; m hosts images wherever already-deployed (Imgur, S3, static-asset endpoint, …).
|
||||
- A publish workflow with approval stages — single boolean is enough.
|
||||
|
||||
## 9. Phase-1 deliverable checklist
|
||||
|
||||
- [ ] `projax.items` + `projax.item_links` migrations in `db/migrations/`
|
||||
|
||||
Reference in New Issue
Block a user