Skip to content

feat: body types, and Slack meeting reminders for committees (#95) - #100

Merged
pataniaeli merged 2 commits into
devfrom
feat/issue-95-committee-slack-reminders
Sep 10, 2026
Merged

feat: body types, and Slack meeting reminders for committees (#95)#100
pataniaeli merged 2 commits into
devfrom
feat/issue-95-committee-slack-reminders

Conversation

@pataniaeli

Copy link
Copy Markdown
Collaborator

Closes #95.

Body types

bodies.body_type, constrained to the six types in the issue. Backfilled from the names that have been carrying the distinction — checked against the 31 bodies live today:

Type n
Committee 9 Academic Services, Campus Services, Communications & Events, DEI, Global Experience, Student Engagement, Student Org Operations, Sustainability, Wellness
Board 9 Appeals, Elections, Executive, Green Initiatives, Internal Advancement, Student Org Approvals / Compliance / Finance / Governance
Advisory Board 3 AI, Campus Planning, Dining
Team 5 Attendance Manager, Governmental Relations, Student-Designed NU Merch, Website Creation, Website Management
Working Group 3 Co-op, Global Experience Office, Holistic Wellness
Other 2 Senate, SGA General

Advisory Board is tested before Board, or the three advisory boards come out as plain boards. Senate and SGA General are genuinely none of the six, so Other is right for them rather than a failure to match. Management can change any of them in the Bodies tab, which now shows the type on every row.

The reminder

The day before each weekly meeting, the bot posts to the channel named by bodies.slack_channel_id:

📆 Sustainability Committee meets tomorrow — Thursday, September 10, 6:30 PM–8:00 PM
Room: Curry 334
Purpose: Weekly Meeting

Status appears only when it is not ordinary, so a status line always means something needs attention. A week's overrides resolve against its series exactly as My Rooms and the update emails resolve them. A hidden booking is never announced — a channel is a much wider audience than the people who can manage it. Cancelled, Repurposed and Missed are skipped; Pending Cancellation is posted, with its status shown, because the people in the channel are the ones who need to know a meeting is in doubt.

A channel ID, not a name: channels get renamed, and a reminder that quietly stops because someone tidied up a name is worse than no reminder. The API rejects anything that isn't ID-shaped with an explanation rather than storing #committee-chat and never resolving it.

Turning it off

/chambers-reminders off, run in the committee's own channel. The channel is what identifies the committee, so there is no body to pick and no way to reach another committee's settings — which is the gating you asked for. Changing it additionally requires Leadership of that committee, checked against board_memberships rather than anything Slack asserts. With no argument it reports the current state, which anyone in the channel may ask. Management can also toggle it from the Bodies tab.

Scheduling

A GitHub Action, like the keep-warm ping and for the same reason (382c2af): Hobby plans cap Vercel crons at one a day, and this needs to retry because GitHub schedules lag and are occasionally skipped. It fires five times across the morning, and two things make the repeats harmless — a 9am-Eastern gate so nothing posts overnight when the date rolls over, and a recorded row per reminder so later firings find the work done. That row is keyed on (weekly_booking_id, occurrence_date) and not an occurrence id, because the weekly PATCH regenerates its occurrences on every save; a key on their ids would forget every reminder the next time anyone edited the booking and re-post the lot.

Verification

npm run build, tsc and eslint clean (the one bodies-tab.tsx error is pre-existing).

  • The migration was dry-run against production inside an aborting transaction — all DDL parsed and applied, the check constraint was proven to reject an invalid type, the backfill produced the 9/31 above, then it rolled back. Confirmed afterwards that no column or table persisted. It has not been applied; it still needs running on deploy.
  • The reminder rules and message formatting were exercised across 26 cases: every suppression path, override-vs-series inheritance in both directions, DST and year/leap boundaries, and the Eastern-vs-UTC date rollover that would otherwise post a day early. Slack mrkdwn is escaped, so a <!channel> typed into a booking purpose cannot make the bot ping everyone.
  • The nested PostgREST filter path the job depends on was checked against the live API with an existing column, and confirmed to narrow (3 occurrences on a date → 1 after filtering).

Before this works in production

  1. Apply the migration.
  2. SLACK_BOT_TOKEN needs chat:write, and the bot must be invited to each committee's channel — Slack refuses a post to a channel it isn't in with ok: false and HTTP 200, which is why lib/slack.ts checks the body and logs. Or add chat:write.public.
  3. Register /chambers-reminders as a slash command on the Slack app, pointed at the existing /api/slack/command endpoint.
  4. Set the CRON_SECRET repo secret (the keep-warm action already uses it).
  5. Fill in each committee's channel ID in Management → Bodies. Nothing posts for a committee with no channel linked.

I could not click through the Management UI: the dev server is behind a login wall.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chambers Ready Ready Preview Sep 9, 2026 10:39pm UTC

pataniaeli and others added 2 commits September 9, 2026 18:38
Committees were distinguished from boards, teams and working groups by
their name and nothing else, so no query could ask for "the committees"
without pattern-matching a string. bodies.body_type is that distinction,
backfilled from the names that have been carrying it: 9 Committee,
9 Board, 3 Advisory Board, 5 Team, 3 Working Group, and Senate and SGA
General on Other, which is correct for them rather than a failed match.
Advisory Board is tested before Board, or the three advisory boards would
come out as plain boards.

Committees get the reminder: the day before each weekly meeting, the bot
posts the date, time, room, purpose and -- only when it is unusual -- the
status, to the channel named by bodies.slack_channel_id. An id and not a
name, because channels get renamed and a reminder that stops posting
because someone tidied up a channel name is worse than no reminder.

A week's overrides resolve against its series the same way My Rooms and
the update emails resolve them, and a hidden booking is never announced:
a channel is a wider audience than the people who can manage it. Anything
Cancelled, Repurposed or Missed is skipped, but Pending Cancellation is
posted with its status shown -- the people in the channel are exactly the
ones who need to know a meeting is in doubt.

Leadership turns the reminders off from inside their own channel, with
/chambers-reminders off. The channel identifies the committee, so there is
no body to pick and no way to reach another committee's settings; changing
it additionally requires Leadership of that committee, checked against
board_memberships rather than anything Slack asserts. With no argument the
command reports the current state, which anyone in the channel may ask.

Scheduled from a GitHub Action like the keep-warm ping, for the reason
that one is: Hobby plans cap Vercel crons at one a day and this needs to
retry, since GitHub schedules lag and are occasionally skipped. It fires
five times across the morning; a 9am-Eastern gate keeps it from posting
overnight when the date rolls over, and a recorded row per reminder keeps
the later firings from posting twice. That row is keyed on
(weekly_booking_id, occurrence_date) rather than an occurrence id, because
the weekly PATCH regenerates its occurrences on every save and a key on
their ids would forget every reminder the next time anyone made an edit.

Slack answers a post to a channel the bot was never invited to with
ok:false and HTTP 200, so lib/slack.ts checks the body rather than the
status -- a reminder that silently never arrives is the failure this is
most likely to have. A refused post deliberately writes no row, so the
next run of the morning tries again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both pin America/New_York, and dev grew lib/boston-time.ts (#87) while
this branch was open, so the next reader meets two modules that look
interchangeable and are not. APP_TIME_ZONE is about DATE columns -- which
calendar day a booking falls on. bostonWallClockNow is about SGA Spaces
timestamps, which store wall-clock digits labelled Z and need a "now" in
that same shape to compare against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pataniaeli
pataniaeli force-pushed the feat/issue-95-committee-slack-reminders branch from 5838d42 to 0b79754 Compare September 9, 2026 22:39
@pataniaeli pataniaeli self-assigned this Sep 10, 2026
@pataniaeli pataniaeli added the enhancement New feature or request label Sep 10, 2026
@pataniaeli pataniaeli linked an issue Sep 10, 2026 that may be closed by this pull request
@pataniaeli
pataniaeli merged commit bf999e7 into dev Sep 10, 2026
4 checks passed
@pataniaeli
pataniaeli deleted the feat/issue-95-committee-slack-reminders branch September 10, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slack bot gets active

1 participant