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)
77 lines
2.8 KiB
SQL
77 lines
2.8 KiB
SQL
-- 0014_items_public_fields.sql
|
|
--
|
|
-- Phase 4d: public-listing fields so flexsiebels.de (and any future public
|
|
-- consumer) can render m's portfolio from projax directly via the MCP
|
|
-- surface. projax becomes the single source of truth for "which projects
|
|
-- are public, and what should the public copy say about them?"
|
|
--
|
|
-- Five additive, default-safe columns:
|
|
-- public — single boolean toggle; 95% of items stay false
|
|
-- public_description — public-facing prose (markdown), free of internal
|
|
-- notes that live in content_md
|
|
-- public_live_url — production/demo URL if the project has one
|
|
-- public_source_url — repo URL if m wants to expose the source
|
|
-- public_screenshots — ordered list of image URLs; projax stores
|
|
-- pointers, never bytes (PER discipline §"Out of
|
|
-- scope (permanent — 2026-05-17)")
|
|
--
|
|
-- The partial index speeds up the "give me everything public" query that
|
|
-- flexsiebels and the optional /tree filter chip will issue. We keep it
|
|
-- partial because the predicate is highly selective (only public rows).
|
|
-- items_unified picks the new columns up automatically via SELECT *.
|
|
|
|
ALTER TABLE projax.items
|
|
ADD COLUMN IF NOT EXISTS public boolean NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS public_description text NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS public_live_url text NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS public_source_url text NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS public_screenshots text[] NOT NULL DEFAULT '{}';
|
|
|
|
CREATE INDEX IF NOT EXISTS items_public_idx
|
|
ON projax.items (public)
|
|
WHERE public = true;
|
|
|
|
-- items_unified is the canonical read shape every web/MCP code path uses.
|
|
-- The five new columns have to flow through it, otherwise Item.* writes
|
|
-- silently drop on read. DROP+CREATE because Postgres won't append columns
|
|
-- to an existing view via CREATE OR REPLACE.
|
|
|
|
DROP VIEW IF EXISTS projax.items_unified;
|
|
CREATE VIEW projax.items_unified AS
|
|
SELECT
|
|
i.id,
|
|
i.kind,
|
|
i.title,
|
|
i.slug,
|
|
i.paths,
|
|
i.parent_ids,
|
|
i.content_md,
|
|
i.aliases,
|
|
i.metadata,
|
|
i.status,
|
|
i.pinned,
|
|
i.archived,
|
|
i.start_time,
|
|
i.end_time,
|
|
'projax'::text AS source,
|
|
(SELECT l.ref_id FROM projax.item_links l
|
|
WHERE l.item_id = i.id AND l.ref_type = 'mai-project' LIMIT 1) AS source_ref_id,
|
|
i.tags,
|
|
i.management,
|
|
i.public,
|
|
i.public_description,
|
|
i.public_live_url,
|
|
i.public_source_url,
|
|
i.public_screenshots,
|
|
i.created_at,
|
|
i.updated_at
|
|
FROM projax.items i
|
|
WHERE i.deleted_at IS NULL;
|
|
|
|
DO $own$ BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'projax_admin') THEN
|
|
EXECUTE 'ALTER VIEW projax.items_unified OWNER TO projax_admin';
|
|
EXECUTE 'GRANT SELECT ON projax.items_unified TO projax_admin';
|
|
END IF;
|
|
END $own$;
|