feat(db): add migration 0003 for council portal schema
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>
This commit is contained in:
parent
d054b56bf7
commit
53cb9a7e49
2 changed files with 128 additions and 0 deletions
3
KNOWN_ISSUES.md
Normal file
3
KNOWN_ISSUES.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Known issues
|
||||||
|
|
||||||
|
- `src/pages/api/contributions/[id]/edit.ts` — both `POST` and `GET` handlers return `302 → /contribute` unconditionally. The route accepts the request but never edits anything; the linked "Edit" affordance on `/contribute` therefore does nothing within the 10-minute window. Out of scope for the council portal restructure; leave untouched.
|
||||||
125
migrations/0003_council_portal.sql
Normal file
125
migrations/0003_council_portal.sql
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
-- 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);
|
||||||
Loading…
Add table
Reference in a new issue