Adds title/cab_joined_date/slug to users, extends attendance with kind enum and 'interested' status, and creates pulses, votes, roadmap_items, roadmap_attributions, events, and activity tables. Slug backfill covers the three seed users; new-user slug generation will live in db.ts. roadmap_items has shipped_at to drive the council mark (simpler than an audit table). roadmap_attributions is admin-curated only. Also logs the pre-existing /api/contributions/[id]/edit 302-only bug in KNOWN_ISSUES.md so it isn't lost; out of scope for this work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
6.6 KiB
SQL
125 lines
6.6 KiB
SQL
-- 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);
|