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

@ -1,25 +1,25 @@
/* ═══════════════════════════════════════════════════════════════
EVENT DATA
One request, filtered per section. The Retreats page has three
One request, filtered per band. The Retreats page has three
bands of events, and all three call this hook — the cache in
api.ts keys on the path, so they share a single fetch and each
narrows the result to what it shows.
useEvents({ section: "national" }) one band
useEvents({ scope: "national" }) one band
useEvents({ host: "northwest" }) one host's events
useEvents({ status: "upcoming" }) a home page strip
useEvents({ type: "workshop" }) one kind, wherever it is
useEvents({ type: ["class", "workshop"] })
useEvents() everything
`section` and `type` are different questions and stack rather
than overlap: the section is which band of the page an event
belongs to, the type is what kind of gathering it is. A regional
class matches both { section: "regional" } and { type: "class" }.
`scope` and `type` are different questions and stack rather
than overlap: the scope is whose gathering an event is, the type
is what kind of gathering it is. A regional class matches both
{ scope: "regional" } and { type: "class" }.
The event_sections rows (the scope list) come back alongside, for
anything that offers a scope filter.
The event_scopes rows come back alongside, for anything that
offers a scope filter.
No fallback. An empty list on a failed request would read as
"nothing scheduled" when the truth is "the server is down", so
@ -35,15 +35,15 @@ import { useMemo } from "react";
import { useResource } from "../lib/useResource.ts";
/* One shared empty list, so a memo keyed on `sections` doesn't
/* One shared empty list, so a memo keyed on `scopes` doesn't
restart on every render before the data arrives. */
const NO_SECTIONS: any[] = [];
const NO_SCOPES: any[] = [];
export function useEvents({ section, host, status, type }: any = {}) {
export function useEvents({ scope, host, status, type }: any = {}) {
const { data, error, loading } = useResource("/events");
const all = data?.events;
const sections = data?.sections ?? NO_SECTIONS;
const scopes = data?.scopes ?? NO_SCOPES;
/* An array prop is a new identity on every render, which would
restart the memo each time. Joining it gives the dependency
@ -52,7 +52,7 @@ export function useEvents({ section, host, status, type }: any = {}) {
const events = useMemo(() => {
let list = all ?? [];
if (section) list = list.filter(e => e.section_id === section);
if (scope) list = list.filter(e => e.scope_id === scope);
// `host` is an organization or a person slug, and an event can
// have several of either — co-hosting puts one event on both
// hosts' lists, which is the point.
@ -63,9 +63,9 @@ export function useEvents({ section, host, status, type }: any = {}) {
list = list.filter(e => wanted.has(e.event_type));
}
return list;
}, [all, section, host, status, typeKey]);
}, [all, scope, host, status, typeKey]);
return { events, sections, loading, error };
return { events, scopes, loading, error };
}
/* Past and upcoming, split. `status` arrives already resolved — the