Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ NEXT_PUBLIC_SITE_URL=https://www.getvindicated.org
# Postgres service and reference it from the website service).
DATABASE_URL=
# Passwords for /board. Everyone on the board gets BOARD_PASSWORD (view and
# submit). Rana gets BOARD_ADMIN_PASSWORD (edit everything). Pierce gets
# submit). Rana gets BOARD_ADMIN_PASSWORD (edit everything). Chloe gets
# BOARD_SOCIALS_PASSWORD (edit socials). Changing one signs out everyone
# who used it.
BOARD_PASSWORD=
Expand Down
2 changes: 1 addition & 1 deletion BOARD_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ need a password.
| Railway variable | Who gets it | What it unlocks |
| ------------------------ | -------------------- | --------------------------------------- |
| `BOARD_PASSWORD` | everyone on the board | view everything, submit assignments |
| `BOARD_SOCIALS_PASSWORD` | Pierce | also add, edit and delete socials |
| `BOARD_SOCIALS_PASSWORD` | Chloe | also add, edit and delete socials |
| `BOARD_ADMIN_PASSWORD` | Rana | edit everything |

Use three different passwords. People stay signed in on a device for 30
Expand Down
4 changes: 2 additions & 2 deletions components/board/SocialsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function SocialsTab() {
title="Add a social"
table="socials"
fields={SOCIAL_FIELDS}
initial={{ title: "", date: "", time: "", location: "", kind: "b", status: "Idea", lead: "Pierce", cost: "", note: "" }}
initial={{ title: "", date: "", time: "", location: "", kind: "b", status: "Idea", lead: "Chloe", cost: "", note: "" }}
onClose={() => setEditing(null)}
onSaved={(v) => jumpTo(v.date)}
/>
Expand Down Expand Up @@ -233,7 +233,7 @@ export function SocialsTab() {
);
})}
</ul>
<p style={{ marginTop: 12 }}>Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.</p>
<p style={{ marginTop: 12 }}>Status runs Idea → Planning → Confirmed → Completed. Chloe leads socials unless noted.</p>
</section>
);
}
27 changes: 12 additions & 15 deletions docs/board-preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ <h3 id="m-label" aria-live="polite"></h3>

<h3 style="margin-top:28px">All socials</h3>
<ul class="agenda" id="agenda"></ul>
<p class="muted small" style="margin-top:12px">Status runs Idea → Planning → Confirmed → Completed. Pierce leads socials unless noted.</p>
<p class="muted small" style="margin-top:12px">Status runs Idea → Planning → Confirmed → Completed. Chloe leads socials unless noted.</p>
</section>

<!-- MEETINGS -->
Expand Down Expand Up @@ -351,7 +351,6 @@ <h3>Board meeting · Sep 21, 2026</h3>
P("Yunchuan Wang","Research Analyst","Research & Legal","member17@example.com","(555) 555-0117"),
P("Maria Miranda Nadsady","Legal Research","Research & Legal","member18@example.com","(555) 555-0118"),
P("Chloe Yuching Lin","Outreach Lead · Social Media","Outreach","member19@example.com","(555) 555-0119",{ lead: true }),
P("Pierce Matthew Kosobayashi","Socials","Outreach","member20@example.com","(555) 555-0120"),
P("Rami K Dakak","Research Analyst","Research & Legal","member21@example.com","(555) 555-0121")
];
const openSeats = {};
Expand Down Expand Up @@ -419,22 +418,21 @@ <h3>Board meeting · Sep 21, 2026</h3>
{ name:"Chloe", role:"Outreach Lead · Social Media", items:[
T("Post twice on @ucb_vindicated before Monday night","Link to each post"),
T("2-week content calendar: what gets posted, which days, who makes each one","Doc link"),
T("Follow up with the high schools and YMCA programs you emailed in round 1 that haven't replied","Screenshot of each follow-up, plus a list of who replied")] },
T("Follow up with the high schools and YMCA programs you emailed in round 1 that haven't replied","Screenshot of each follow-up, plus a list of who replied"),
T("RSVP form for VINdiBasketball (Sat Oct 17), posted on @ucb_vindicated","Form link and post link"),
T("Send invites for every social on the Socials calendar to the full member email list","Screenshot of the sent email showing the recipients"),
T("Keep the Socials calendar current: confirm each location and update it on the page","The updated calendar")] },
{ name:"Noirit", role:"IVP · Price Is Right Lead", items:[
T("Price Is Right week 2 check: pipeline started, database set up, game screens running with placeholder data","GitHub commit links and a screenshot of the screens"),
T("Sunday night: message every board member who hasn't submitted yet","Screenshot of the messages")] },
{ name:"Pierce", role:"Socials", items:[
T("VINdiBasketball: set the date and book an RSF court","Booking confirmation"),
T("RSVP form for basketball, sent to Chloe to post","Form link"),
T("Send invites for every social on the Socials calendar to the full member email list","Screenshot of the sent email showing the recipients"),
T("Keep the Socials calendar current: confirm each location and update it on the page","The updated calendar")] },
{ name:"Ian", role:"Software Lead", items:[
T("Get access to the website repo from Rizwaan or Ray","Screenshot showing access"),
T("Assign 3 website-fix pull requests to Ryan and Ethan: write each fix as a GitHub issue with what's broken and what done looks like","Links to the 3 issues, each assigned to Ryan or Ethan")] },
{ name:"Halima", role:"EVP · Partnerships", items:[
T("Reach out to 10 Berkeley student orgs that make videos, starting with Cal TV, about filming or collaborating with us","Screenshot of each sent email or DM, plus a list of the 10 orgs and who replied")] },
{ name:"Daryen", role:"Communications Lead", items:[
T("Call 5 local mechanics about recording a short video with us (carried over from round 1)","Call log: shop name, number, time called, what they said")] },
T("Call 5 local mechanics about recording a short video with us (carried over from round 1)","Call log: shop name, number, time called, what they said"),
T("Book an RSF court for VINdiBasketball, Sat Oct 17, 2–4pm","Booking confirmation")] },
{ name:"Jordan", role:"Data Lead", items:[
T("Project spec, including how many people you need (carried over)","Doc link"),
T("Message James Cheng about the project (carried over)","Screenshot of the message"),
Expand All @@ -447,7 +445,6 @@ <h3>Board meeting · Sep 21, 2026</h3>
["Ian","Follow up with Rizwaan, project spec","Marked completed"],
["Chloe","Event ideas, 5 mechanic calls, 10 high schools + YMCA","Marked completed"],
["Daryen","Help Halima, 5 mechanic calls","Not done"],
["Pierce","Event ideas doc, workshop timeline, connect with Halima","Not done"],
["Jordan","Project spec, connect with James, answer Ray","Not done"]
];
document.getElementById("r1-count").textContent = `${round1.filter(r => r[2] !== "Not done").length} of ${round1.length} done`;
Expand Down Expand Up @@ -522,13 +519,13 @@ <h3>Board meeting · Sep 21, 2026</h3>
/* ---------- socials ---------- */
// kind: b = Berkeley, x = UCSC x UCB, o = online, org = org date (not a social)
const socials = [
{ date:"2026-10-17", time:"2:00–4:00pm", title:"VINdiBasketball", where:"RSF", kind:"b", lead:"Pierce", status:"Planning", cost:"Free. Optional boba after, self-pay.", note:"First Berkeley social. Court booking and RSVP form due Mon, Oct 12." },
{ date:"2026-10-17", time:"2:00–4:00pm", title:"VINdiBasketball", where:"RSF", kind:"b", lead:"Chloe", status:"Planning", cost:"Free. Optional boba after, self-pay.", note:"First Berkeley social. Court booking and RSVP form due Mon, Oct 12." },
{ date:"2026-10-21", time:"9:00–10:00pm", title:"Skribbl night", where:"Discord", kind:"o", lead:"Chloe", status:"Planning", cost:"Free", note:"Open to all three chapters. Chloe posts the link that afternoon." },
{ date:"2026-10-24", time:"Afternoon", title:"UCSC × UCB social", where:"San Francisco · spot TBD", kind:"x", lead:"Rana + Ashwin", status:"Idea", cost:"Self-pay", note:"Combined social with UCSC. Location still being decided." },
{ date:"2026-10-30", time:"5:00–8:00pm", title:"Glade Night + Chipotle", where:"Memorial Glade", kind:"b", lead:"Pierce", status:"Planning", cost:"Chipotle catering from the $240, if the Oct 13 budget allows", note:"Halloween eve, costumes optional. Last week of daylight before clocks change." },
{ date:"2026-10-30", time:"5:00–8:00pm", title:"Glade Night + Chipotle", where:"Memorial Glade", kind:"b", lead:"Chloe", status:"Planning", cost:"Chipotle catering from the $240, if the Oct 13 budget allows", note:"Halloween eve, costumes optional. Last week of daylight before clocks change." },
{ date:"2026-11-06", time:"", title:"Price Is Right demo", where:"", kind:"org", lead:"Noirit", status:"", cost:"", note:"Not a social. No socials Nov 5–15." },
{ date:"2026-11-18", time:"8:00–10:30pm", title:"Movie Night + Menchie's", where:"TBD", kind:"b", lead:"Daryen (movie), Chloe (Menchie's)", status:"Idea", cost:"Self-pay froyo", note:"" },
{ date:"2026-11-21", time:"1:00–4:00pm", title:"UCSC × UCB basketball", where:"Irvington Park, 41825 Blacow Rd, Fremont", kind:"x", lead:"Pierce + UCSC lead", status:"Idea", cost:"Free", note:"Meet-in-the-middle game, separate from VINdiBasketball. Outdoor court, first come. Backup: Karl Nordvik Park." },
{ date:"2026-11-21", time:"1:00–4:00pm", title:"UCSC × UCB basketball", where:"Irvington Park, 41825 Blacow Rd, Fremont", kind:"x", lead:"Chloe + UCSC lead", status:"Idea", cost:"Free", note:"Meet-in-the-middle game, separate from VINdiBasketball. Outdoor court, first come. Backup: Karl Nordvik Park." },
];
const SOCIALS_KEY = "vin-socials-v1";
try { const saved = JSON.parse(localStorage.getItem(SOCIALS_KEY) || "null"); if (Array.isArray(saved) && saved.length) socials.splice(0, socials.length, ...saved); } catch (_) {}
Expand Down Expand Up @@ -567,7 +564,7 @@ <h3>Board meeting · Sep 21, 2026</h3>

const card = document.getElementById("ev-card");
if (editEv !== null) {
const s = editEv === "new" ? { date: "", time: "", title: "", where: "", kind: "b", lead: "Pierce", status: "Idea", cost: "", note: "" } : socials[editEv];
const s = editEv === "new" ? { date: "", time: "", title: "", where: "", kind: "b", lead: "Chloe", status: "Idea", cost: "", note: "" } : socials[editEv];
const opt = (v, cur, label) => `<option value="${v}" ${v === cur ? "selected" : ""}>${label}</option>`;
card.hidden = false;
card.innerHTML = `<h3>${editEv === "new" ? "Add a social" : "Edit " + esc(s.title)}</h3>
Expand All @@ -591,7 +588,7 @@ <h3>Board meeting · Sep 21, 2026</h3>
${editEv !== "new" ? `<button class="chip" type="button" data-edelete="1" style="margin-left:auto">${confirmDel ? "Click again to delete" : "Delete"}</button>` : ""}
</div>
<p id="e-err" hidden style="font-weight:400"></p>
<p class="small muted" style="margin:0">Preview: edits save in this browser only. On the live site, Pierce and Rana can edit and everyone sees the change.</p>
<p class="small muted" style="margin:0">Preview: edits save in this browser only. On the live site, Chloe and Rana can edit and everyone sees the change.</p>
</form>`;
}
else if (openEv === null) card.hidden = true;
Expand Down
2 changes: 1 addition & 1 deletion lib/board/roles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Who can do what on /board. People sign in with one of three passwords,
// all set as environment variables in Railway:
// BOARD_PASSWORD everyone on the board: view and submit
// BOARD_SOCIALS_PASSWORD socials editor (Pierce): also add/edit/delete socials
// BOARD_SOCIALS_PASSWORD socials editor (Chloe): also add/edit/delete socials
// BOARD_ADMIN_PASSWORD admin (Rana): edit everything

// Where assignment submissions are emailed.
Expand Down
43 changes: 43 additions & 0 deletions lib/board/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ async function setup() {
if (await empty("board_meetings")) {
for (const m of starter.meetings) await sql`INSERT INTO board_meetings ${sql(m)}`;
}
await removePierce(sql);
}

const OLD_IAN = "Start the /board page on getvindicated.org from this preview";
Expand Down Expand Up @@ -82,3 +83,45 @@ async function retaskRound2(sql: ReturnType<typeof db>) {
}
});
}

type Round2 = [string, string, [string, string][]][];

// Pierce left the board on Oct 11. Removes him, his assignments and
// submissions, gives his socials work to Chloe and the court booking to
// Daryen, and makes Chloe the lead on his socials. Runs once per database.
async function removePierce(sql: ReturnType<typeof db>) {
const round2 = starter.round2 as Round2;
const added: Record<string, string[]> = {
Chloe: [
"RSVP form for VINdiBasketball (Sat Oct 17), posted on @ucb_vindicated",
"Send invites for every social on the Socials calendar to the full member email list",
"Keep the Socials calendar current: confirm each location and update it on the page",
],
Daryen: ["Book an RSF court for VINdiBasketball, Sat Oct 17, 2–4pm"],
};
await sql.begin(async (tx) => {
const [first] = await tx`INSERT INTO board_changes (name) VALUES ('2026-10-11-remove-pierce')
ON CONFLICT DO NOTHING RETURNING name`;
if (!first) return;

await tx`DELETE FROM board_members
WHERE lower(email) = 'pierce.kosobayashi@berkeley.edu' OR name ILIKE '%Kosobayashi%'`;
await tx`DELETE FROM board_submissions WHERE person = 'Pierce'`;
await tx`DELETE FROM board_assignments WHERE person = 'Pierce'`;
await tx`UPDATE board_socials SET lead = replace(lead, 'Pierce', 'Chloe') WHERE lead LIKE '%Pierce%'`;

for (const [person, tasks] of Object.entries(added)) {
const [, role, items] = round2.find(([p]) => p === person)!;
const [last] = await tx`SELECT max(sort) AS sort, max(due_date) AS due FROM board_assignments
WHERE round = 2 AND person = ${person}`;
let sort = (last?.sort ?? 0) + 1;
for (const task of tasks) {
const [, proof] = items.find(([t]) => t === task)!;
const [exists] = await tx`SELECT 1 FROM board_assignments WHERE round = 2 AND person = ${person} AND task = ${task}`;
if (exists) continue;
await tx`INSERT INTO board_assignments (round, person, person_role, task, proof_required, due_date, sort)
VALUES (2, ${person}, ${role}, ${task}, ${proof}, ${last?.due ?? starter.dueRound2}, ${sort++})`;
}
}
});
}
50 changes: 19 additions & 31 deletions lib/board/starter-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
[
"Follow up with the high schools and YMCA programs you emailed in round 1 that haven't replied",
"Screenshot of each follow-up, plus a list of who replied"
],
[
"RSVP form for VINdiBasketball (Sat Oct 17), posted on @ucb_vindicated",
"Form link and post link"
],
[
"Send invites for every social on the Socials calendar to the full member email list",
"Screenshot of the sent email showing the recipients"
],
[
"Keep the Socials calendar current: confirm each location and update it on the page",
"The updated calendar"
]
]
],
Expand All @@ -33,28 +45,6 @@
]
]
],
[
"Pierce",
"Socials",
[
[
"VINdiBasketball: set the date and book an RSF court",
"Booking confirmation"
],
[
"RSVP form for basketball, sent to Chloe to post",
"Form link"
],
[
"Send invites for every social on the Socials calendar to the full member email list",
"Screenshot of the sent email showing the recipients"
],
[
"Keep the Socials calendar current: confirm each location and update it on the page",
"The updated calendar"
]
]
],
[
"Ian",
"Software Lead",
Expand Down Expand Up @@ -86,6 +76,10 @@
[
"Call 5 local mechanics about recording a short video with us (carried over from round 1)",
"Call log: shop name, number, time called, what they said"
],
[
"Book an RSF court for VINdiBasketball, Sat Oct 17, 2–4pm",
"Booking confirmation"
]
]
],
Expand Down Expand Up @@ -139,12 +133,6 @@
"missing",
"Not done"
],
[
"Pierce",
"Event ideas doc, workshop timeline, connect with Halima",
"missing",
"Not done"
],
[
"Jordan",
"Project spec, connect with James, answer Ray",
Expand All @@ -159,7 +147,7 @@
"title": "VINdiBasketball",
"location": "RSF",
"kind": "b",
"lead": "Pierce",
"lead": "Chloe",
"status": "Planning",
"cost": "Free. Optional boba after, self-pay.",
"note": "First Berkeley social. Court booking and RSVP form due Mon, Oct 12."
Expand Down Expand Up @@ -192,7 +180,7 @@
"title": "Glade Night + Chipotle",
"location": "Memorial Glade",
"kind": "b",
"lead": "Pierce",
"lead": "Chloe",
"status": "Planning",
"cost": "Chipotle catering from the $240, if the Oct 13 budget allows",
"note": "Halloween eve, costumes optional. Last week of daylight before clocks change."
Expand Down Expand Up @@ -225,7 +213,7 @@
"title": "UCSC × UCB basketball",
"location": "Irvington Park, 41825 Blacow Rd, Fremont",
"kind": "x",
"lead": "Pierce + UCSC lead",
"lead": "Chloe + UCSC lead",
"status": "Idea",
"cost": "Free",
"note": "Meet-in-the-middle game, separate from VINdiBasketball. Outdoor court, first come. Backup: Karl Nordvik Park."
Expand Down
9 changes: 1 addition & 8 deletions scripts/board-members.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,11 @@
"phone": "(555) 555-0118",
"lead": true
},
{
"name": "Pierce Matthew Kosobayashi",
"role": "Socials",
"team": "Outreach",
"email": "member20@example.com",
"phone": "(555) 555-0119"
},
{
"name": "Rami K Dakak",
"role": "Research Analyst",
"team": "Research & Legal",
"email": "member21@example.com",
"phone": "(555) 555-0120"
}
]
]
6 changes: 6 additions & 0 deletions scripts/board-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,9 @@ CREATE TABLE IF NOT EXISTS board_meetings (
summary text NOT NULL DEFAULT '',
recording_url text NOT NULL DEFAULT ''
);

-- One-time data changes already applied to this database (see lib/board/setup.ts).
CREATE TABLE IF NOT EXISTS board_changes (
name text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
);