-- Council portal restructure — Phase 1 -- Adds: title/cab_joined_date/slug to users; kind+interested status on attendance; -- pulses, votes, roadmap_items, roadmap_attributions, events, activity tables. -- ── users: new columns ───────────────────────────────────────────── ALTER TABLE users ADD COLUMN title TEXT; ALTER TABLE users ADD COLUMN cab_joined_date TEXT; ALTER TABLE users ADD COLUMN slug TEXT; -- Partial unique index (allows NULL while enforcing uniqueness on populated slugs) CREATE UNIQUE INDEX idx_users_slug_unique ON users(slug) WHERE slug IS NOT NULL; -- Backfill slugs from existing names (seed users have simple ASCII names). -- New users get slugs generated in db.ts (kebab-case + dedupe). UPDATE users SET slug = LOWER(REPLACE(REPLACE(name, ' ', '-'), '.', '')) WHERE slug IS NULL; -- ── attendance: rebuild for kind + widened status ────────────────── -- SQLite can't ALTER a CHECK constraint; rebuild is required. CREATE TABLE attendance_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), meeting_slug TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'meeting' CHECK(kind IN ('meeting','event')), status TEXT NOT NULL CHECK(status IN ('yes','no','interested')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, meeting_slug) ); INSERT INTO attendance_new (id, user_id, meeting_slug, kind, status, updated_at) SELECT id, user_id, meeting_slug, 'meeting', status, updated_at FROM attendance; DROP TABLE attendance; ALTER TABLE attendance_new RENAME TO attendance; CREATE INDEX idx_attendance_meeting ON attendance(meeting_slug); CREATE INDEX idx_attendance_kind ON attendance(kind); -- ── pulses ───────────────────────────────────────────────────────── CREATE TABLE pulses ( id INTEGER PRIMARY KEY AUTOINCREMENT, question TEXT NOT NULL, context TEXT, options TEXT NOT NULL, -- JSON array of strings (length 2–4) opens_at TEXT NOT NULL, closes_at TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','open','closed')), created_at TEXT NOT NULL DEFAULT (datetime('now')), created_by INTEGER NOT NULL REFERENCES users(id) ); CREATE INDEX idx_pulses_status ON pulses(status); CREATE INDEX idx_pulses_dates ON pulses(opens_at, closes_at); -- ── votes ────────────────────────────────────────────────────────── CREATE TABLE votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, pulse_id INTEGER NOT NULL REFERENCES pulses(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, option_index INTEGER NOT NULL CHECK(option_index >= 0), voted_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(pulse_id, user_id) ); CREATE INDEX idx_votes_pulse ON votes(pulse_id); CREATE INDEX idx_votes_user ON votes(user_id); -- ── roadmap_items ────────────────────────────────────────────────── CREATE TABLE roadmap_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'exploring' CHECK(status IN ('shipping','beta','exploring')), target TEXT, display_order INTEGER NOT NULL DEFAULT 0, shipped_at TEXT, -- set when status first transitions to 'shipping'; never reset created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_roadmap_status ON roadmap_items(status, display_order); CREATE INDEX idx_roadmap_shipped ON roadmap_items(shipped_at); -- ── roadmap_attributions ─────────────────────────────────────────── -- Members who shaped this roadmap item. Set explicitly by admin when -- creating/editing. NOT derived from votes, contributions, or any other signal. -- Drives the council mark dot-lighting (a quarter lights iff this user has -- an attribution to an item whose shipped_at falls in that quarter) and the -- "you shaped this" line on /pulse and /roadmap. CREATE TABLE roadmap_attributions ( roadmap_item_id INTEGER NOT NULL REFERENCES roadmap_items(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (roadmap_item_id, user_id) ); CREATE INDEX idx_roadmap_attr_user ON roadmap_attributions(user_id); -- ── events ───────────────────────────────────────────────────────── CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT UNIQUE NOT NULL, title TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'dinner' CHECK(kind IN ('dinner','office_hours','summit','virtual')), description TEXT NOT NULL DEFAULT '', location TEXT NOT NULL DEFAULT '', starts_at TEXT NOT NULL, ends_at TEXT, capacity INTEGER, photo_url TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), created_by INTEGER REFERENCES users(id) ); CREATE INDEX idx_events_starts_at ON events(starts_at); -- ── activity ─────────────────────────────────────────────────────── -- Drives the live ticker on /pulse. Written automatically by the action -- it represents (vote cast, RSVP set, roadmap shipped, pulse opened) — -- never by admin UI. Ticker query: kind != null AND created_at > now - 7d, LIMIT 12. CREATE TABLE activity ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK(kind IN ('voted','rsvped','booked_office_hours','roadmap_shipped','pulse_opened')), subject_type TEXT NOT NULL, subject_id INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_activity_recent ON activity(created_at);