Clean up the schema: drop unused tables, rename scopes, publishable awards

Migrations 020–023, with the code that reads each:

- 020 drops people_lists, people_list_members, v_chapters and
  v_person_affiliations. Nothing queried any of them.
- 021 renames event_sections to event_scopes and events.section_id
  to scope_id, finishing what 015 described. The API sends `scopes`
  and `scope_id`, and useEvents, EventListCards and EventCalendar
  take `scope`. It also inserts national/regional/partner, which only
  the retired seed ever created: a database built from migrations
  alone had no scope for the Retreats bands.
- 022 drops events.sort_order and people.sort_order. Events now sort
  by date (upcoming soonest first, past latest first, undated last)
  on /events, org pages and the countdown. People were only ever
  sorted by sort_name on the site. Every other sort_order stays.
- 023 rebuilds teams with created_at and updated_at plus a touch
  trigger, so the teams editor gets the same optimistic concurrency
  as the other entities.

Awards can be drafted: is_published (added in 011) is on both
descriptor halves with a Publishing group, and the award list, award
page, org awards and event awards leave drafts out.

The migration runner now turns foreign keys off around the per-file
transactions and runs foreign_key_check before each commit. PRAGMA
foreign_keys is a no-op inside a transaction, so 009's warning was
right and a rebuild of a referenced table (023) couldn't be written
otherwise. CLAUDE.md is updated to match.

Also removes the stray src/App.tsx.save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-26 17:09:58 -05:00
parent 8790f861fe
commit 25e592bfea
19 changed files with 339 additions and 423 deletions

View file

@ -0,0 +1,23 @@
-- ═══════════════════════════════════════════════════════════════
-- 020 DROP WHAT NOTHING READS
--
-- people_lists and people_list_members were planned for
-- hand-picked rosters and never got a route, a descriptor or a
-- component. v_chapters and v_person_affiliations have no query
-- against them: the routes read organizations and affiliations
-- directly.
--
-- Views first, then the child table before its parent. No trigger
-- references either table, and nothing has a foreign key into
-- them, so no rebuild and no PRAGMA foreign_keys dance.
--
-- PRAGMA user_version; -- reads 19 before this file
-- ═══════════════════════════════════════════════════════════════
DROP VIEW IF EXISTS v_chapters;
DROP VIEW IF EXISTS v_person_affiliations;
DROP TABLE IF EXISTS people_list_members;
DROP TABLE IF EXISTS people_lists;
PRAGMA user_version = 20; -- ← set to this migration's number

View file

@ -0,0 +1,32 @@
-- ═══════════════════════════════════════════════════════════════
-- 021 event_sections → event_scopes
--
-- 015 established that the table is a scope list — whose gathering
-- an event is — and kept the old name to avoid a rename across the
-- codebase. This is that rename: the table, and events.section_id
-- to scope_id. The API field and the useEvents filter follow in
-- the same change.
--
-- Both renames run with legacy_alter_table off (the default), so
-- SQLite carries them into the REFERENCES clause on events, the
-- index on section_id, and v_events, which selects e.*.
--
-- It also inserts the three scopes 015 only relabelled. The
-- retired seed script was what created national, regional and
-- partner, so a database built from migrations alone had no row
-- for the Retreats page's three bands to point at. INSERT OR
-- IGNORE with 015's names and order: a no-op where they exist.
--
-- PRAGMA user_version; -- reads 20 before this file
-- ═══════════════════════════════════════════════════════════════
ALTER TABLE event_sections RENAME TO event_scopes;
ALTER TABLE events RENAME COLUMN section_id TO scope_id;
INSERT OR IGNORE INTO event_scopes (id, name, sort_order) VALUES
('national', 'National', 10),
('regional', 'Regional', 20),
('partner', 'Partner', 50);
PRAGMA user_version = 21; -- ← set to this migration's number

View file

@ -0,0 +1,36 @@
-- ═══════════════════════════════════════════════════════════════
-- 022 DROP events.sort_order AND people.sort_order
--
-- events.sort_order dates from the seed, when event dates were
-- free text ("March/April 2026") and a typed number was the only
-- way to order a list. Events have real dates now, and a new one
-- landed at 0 — ahead of everything — until someone renumbered.
-- The routes order by date instead.
--
-- people.sort_order was never read by the public site: people
-- sort by sort_name everywhere. It only reordered the admin list.
--
-- Every other sort_order stays. On child collections it is the
-- row's position, written by the admin engine from drag order;
-- on organizations, teams, awards, timeline_entries and
-- event_scopes it is an ordering the site actually uses.
--
-- SQLite refuses DROP COLUMN while an index names the column, so
-- the three indexes that do go first and come back without it.
-- No view names either column (v_events selects e.*).
--
-- PRAGMA user_version; -- reads 21 before this file
-- ═══════════════════════════════════════════════════════════════
DROP INDEX IF EXISTS events_section_idx;
DROP INDEX IF EXISTS events_type_idx;
DROP INDEX IF EXISTS people_sort_idx;
ALTER TABLE events DROP COLUMN sort_order;
ALTER TABLE people DROP COLUMN sort_order;
CREATE INDEX events_scope_idx ON events (scope_id, is_published, starts_on);
CREATE INDEX events_type_idx ON events (event_type, is_published, starts_on);
CREATE INDEX people_sort_idx ON people (is_published, sort_name);
PRAGMA user_version = 22; -- ← set to this migration's number

View file

@ -0,0 +1,87 @@
-- ═══════════════════════════════════════════════════════════════
-- 023 teams GETS created_at AND updated_at
--
-- Every other top-level entity has updated_at, which the admin
-- engine compares on save so two people editing the same row
-- can't silently overwrite each other. teams was left out because
-- ADD COLUMN can't take DEFAULT (datetime('now')) on a STRICT
-- table, so this is the rebuild — the recipe from the SQLite docs.
--
-- The runner turns foreign keys off around every migration and
-- runs foreign_key_check before committing (see migrate() in
-- db.js). So DROP TABLE doesn't touch affiliations, whose
-- (team_id, org_id) reference keeps pointing at the name `teams`,
-- which the rename puts back underneath it.
--
-- legacy_alter_table is on for the rename: v_org_leadership,
-- v_timeline and the timeline_entries ref triggers name `teams`.
-- Between the DROP and the RENAME that table doesn't exist, and
-- the modern rename re-parses every view and trigger and fails on
-- them. Legacy mode leaves them alone; they resolve to the new
-- table by name. Turned back off before the end, since the
-- connection outlives this file.
--
-- DROP TABLE takes the two AFTER DELETE triggers with it (and does
-- not fire them), so they're recreated below exactly as 002 and
-- 007 wrote them, along with teams_touch. Triggers go last: the
-- runner may drop statements that follow a BEGIN...END body.
--
-- Verify after:
--
-- PRAGMA user_version; -- 23
-- PRAGMA foreign_key_check; -- no rows
-- SELECT name FROM sqlite_master WHERE tbl_name = 'teams';
-- ═══════════════════════════════════════════════════════════════
PRAGMA legacy_alter_table = ON;
CREATE TABLE teams_new (
id TEXT PRIMARY KEY, -- slug: 'board', 'nw-leadership'
org_id TEXT NOT NULL REFERENCES organizations (id) ON DELETE CASCADE,
name TEXT NOT NULL,
tagline TEXT,
color TEXT,
logo TEXT,
is_published INTEGER NOT NULL DEFAULT 1 CHECK (is_published IN (0, 1)),
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (id, org_id)
) STRICT;
-- Columns listed explicitly rather than SELECT *, so this breaks
-- loudly if the old shape isn't what this file assumes. Existing
-- rows get the migration time for both timestamps.
INSERT INTO teams_new
(id, org_id, name, tagline, color, logo, is_published, sort_order)
SELECT
id, org_id, name, tagline, color, logo, is_published, sort_order
FROM teams;
DROP TABLE teams;
ALTER TABLE teams_new RENAME TO teams;
CREATE INDEX teams_org_idx ON teams (org_id, sort_order);
PRAGMA legacy_alter_table = OFF;
CREATE TRIGGER teams_cleanup
AFTER DELETE ON teams
BEGIN
DELETE FROM content_blocks WHERE owner_kind = 'team' AND owner_id = old.id;
DELETE FROM links WHERE owner_kind = 'team' AND owner_id = old.id;
END;
CREATE TRIGGER timeline_teams_cleanup
AFTER DELETE ON teams
BEGIN
DELETE FROM timeline_entries WHERE ref_kind = 'team' AND ref_id = old.id;
END;
CREATE TRIGGER teams_touch
AFTER UPDATE ON teams
FOR EACH ROW WHEN new.updated_at = old.updated_at
BEGIN
UPDATE teams SET updated_at = datetime('now') WHERE id = new.id;
END;