m's stated use case: home VTODOs (shopping list) shouldn't pollute the
chronological /timeline by default, but they should stay visible on the
home detail page itself. This adds an item-level switch with four kinds
and a URL override to peek at everything when wanted.
## Schema (migration 0015)
- timeline_exclude text[] NOT NULL DEFAULT '{}'
- items_timeline_exclude_idx GIN
- items_unified view rebuilt to surface the new column
- Behaviour-neutral: empty array = unchanged from today. m flips the
toggle himself via /admin/bulk or the detail-page form.
## Aggregation
- web/timeline.go: pre-compute the per-kind keep-list via keepFor(kind)
before fanning out — items with the kind in their exclude array are
dropped entirely (no CalDAV call wasted on excluded sources). Doc and
creation rows check the per-item flag inline. `?include_excluded=1`
(URL) and `include_excluded:true` (MCP arg) override the filter.
- store.Item.ExcludesTimelineKind(kind) helper accepts either singular
("todo") or plural ("todos") to bridge the kind-constant / persisted-
value naming choice — see comment for the why.
## UI
- /i/{path} grows a "Timeline behaviour" collapsible section with four
checkboxes (todos / events / docs / creation) and helper text. Open by
default when any kind is excluded, so m can see at a glance what's
hidden for this item.
- /admin/bulk gains a "timeline todos" select with "Exclude from timeline"
and "Re-include on timeline" — the other three kinds stay editable
per-item only per the task brief (most common use case is just todos).
## MCP
- update_item accepts timeline_exclude as a partial-update field with an
enum-restricted whitelist; unknown values dropped silently.
- itemView always emits timeline_exclude (defaults to []) so consumers
can render the toggle state without a second round-trip.
## Tests
- Migration + GIN index landed
- Item with timeline_exclude=['todos'] hides the VTODO from /timeline
- ?include_excluded=1 brings it back
- Bulk action toggles the array idempotently in both directions
- Detail page renders all 4 checkbox affordances
## docs/design.md
§12 gains a "Per-item exclusion" subsection documenting semantics, the
URL override, the bulk action, and the "detail page still shows everything"
invariant.
## Out of scope (per task brief)
- Per-tag exclusion (per-item is clearer)
- Per-day exclusion (overkill)
- Dashboard exclusion (m only flagged timeline; dashboard's "today" view
should still show shopping today if it's due today)
- Auto-seeding home with timeline_exclude=['todos'] (m runs once himself
via /admin/bulk after the deploy — schema change stays behaviour-neutral)
66 lines
2.0 KiB
SQL
66 lines
2.0 KiB
SQL
-- 0015_items_timeline_exclude.sql
|
|
--
|
|
-- Phase 4f: per-item switch for excluding kinds of content from the
|
|
-- /timeline aggregation. m's stated use case: home VTODOs (shopping list)
|
|
-- shouldn't pollute the chronological spine by default, but they should
|
|
-- stay visible on the home detail page itself.
|
|
--
|
|
-- Values: 'todos' | 'events' | 'docs' | 'creation' — one per timeline
|
|
-- source kind. Empty array = nothing excluded = current behaviour. The
|
|
-- column is behaviour-neutral on write; m flips the toggle himself via
|
|
-- /admin/bulk or the detail-page form after deploy.
|
|
--
|
|
-- GIN index because every timeline aggregation walks every item and
|
|
-- checks the kind against the array; the index covers the same-kind
|
|
-- containment probes the aggregation does.
|
|
|
|
ALTER TABLE projax.items
|
|
ADD COLUMN IF NOT EXISTS timeline_exclude text[] NOT NULL DEFAULT '{}';
|
|
|
|
CREATE INDEX IF NOT EXISTS items_timeline_exclude_idx
|
|
ON projax.items USING gin (timeline_exclude);
|
|
|
|
-- items_unified must surface the new column or store reads will silently
|
|
-- drop it. Same DROP+CREATE dance as 0014 — Postgres can't append cols to
|
|
-- a 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.timeline_exclude,
|
|
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$;
|