v1.14.0 - #90
Merged
Merged
Conversation
An admin opens Cancellations, presses Auto-Cancel, optionally narrows by booking type and date range, reviews the list, and sends CSC a single email asking for each reservation to be released. Every booking marked Pending Cancellation is collected, and each line carries the date, the time and the reservation code. The weekly half is the part worth reading twice. weekly_room_occurrences.status is an override where NULL means "inherit from the series" -- 340 of the 385 occurrence rows carry a NULL status. A series marked Pending Cancellation therefore has occurrences that are pending without any occurrence row saying so, and matching on the occurrence's own column would silently skip every series-level cancellation: the case with the most dates behind it. Room, times and reservation code inherit the same way, so each is resolved against the series before it reaches the email. That resolution is a pure exported function because it is the piece most likely to be wrong and the hardest to observe -- no series is currently pending, so there is no live data to exercise it. Nothing changes status. The email asks CSC to release the rooms; CSC is the authority on whether that happened, and marking anything 'Cancelled' here would assert an outcome Chambers has not been told. The existing "Mark as Done" control stays the step that closes the loop. Two guards, because this is the only mail Chambers sends outside SGA and it cannot be recalled. The admin approves the actual list rather than a count, and the send re-runs the query server-side rather than trusting the posted body -- otherwise a client could mail any set of dates and codes it liked to an external address. The approved count travels with the request, and a mismatch answers 409 rather than sending a list nobody agreed to. The send is awaited rather than deferred like the member-facing emails: it is the entire point of the request, and an admin told it went needs that to be true. Reservations with no code on file are still listed, flagged in the preview and marked in the email, rather than dropped -- CSC can find those by date and room, and silently omitting them would lose a cancellation. The recipient defaults to cscreservations@northeastern.edu and is overridable via CSC_EMAIL so a staging deployment can point somewhere harmless. Replies go to OPS_EMAIL rather than the no-reply sender. Verified against the live database with Resend intercepted, so nothing was sent: the collector returns exactly the 6 rows an independent SQL query finds, with the same codes, rooms and bodies, ordered by date then time. Type filters give 0/0/6 for tabling/one-time/weekly, date bounds give 2, 4 and 1 rows, and a malformed date is rejected rather than passed through. The inheritance helper resolves an all-NULL occurrence to the series' pending status, lets an occurrence override beat it, and falls back safely with no series at all. The email addresses cscreservations@northeastern.edu with the reply-to set, and its table fits the 600px frame with no cell overflow. Through the real modal: filters update the count live, the send button disables on an empty list, and the POST carries the reviewed count with the filters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes to the behaviour agreed on review. Sending now cancels. The reservations included in the request are marked 'Cancelled' once CSC has been asked, rather than being left Pending Cancellation for a human to close out. Order matters and is deliberate: the email goes first, and statuses move only after it has been accepted. Marking first and then failing to send would leave a booking cancelled in Chambers that CSC still holds a room for -- the one outcome worth designing against, because nobody would go looking for it. If the mail fails, nothing is touched and the response says so. An occurrence whose status was inherited gets 'Cancelled' written onto the occurrence itself, so only the dates actually sent stop being pending and the rest of the series is untouched. Each affected booking also gets an audit_logs row, so the change appears in the Audit tab beside every other status change rather than looking like it happened by itself. Best effort: the mail is out and the statuses have moved, so failing the request over a missing log would only invite a resend. Codeless reservations are dropped rather than listed. A reservation code is the only handle CSC has on a booking, so one without it can be neither requested nor -- now that sending cancels -- cancelled, since doing so would put Chambers and CSC out of step over a request CSC could not act on. They are set aside and shown in the preview and on the confirmation, because they are still outstanding and someone has to chase them by hand. Whitespace counts as absent: a code column holding " " is a blank someone tabbed through. Ops is copied on every request, from OPS_EMAIL, so the division holds the record rather than only the sender's mailbox. This also closes the duplicate-send gap raised on the PR. Marked-Cancelled rows no longer match the query, so a second run cannot re-send them, and no extra column or send log is needed. Verified without sending mail or writing to the database. The code rule rejects null, undefined, empty and whitespace and accepts a real code padded or not. The send addresses CSC with OPS_EMAIL on cc and reply-to. The rows the updates would target were checked against the database read-only: all six ids exist as occurrences, all six parent booking ids are correct for the audit rows, all six are currently pending, and no pending row is missed -- so the statement would hit exactly the intended rows and nothing else. Through the real modal with skipped rows present: the coded two are listed, the two without codes appear in their own block marked not sent and not cancelled, the footer names both recipients, and the confirmation reports what was cancelled and what was left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atched Auto-Cancel took a booking type and a date range and acted on everything that matched. That made the filter -- something you set to look around with -- decide what got cancelled, and after the change that sending also marks bookings Cancelled, a mis-set filter was no longer a bad email but a batch of bookings cancelled without anyone choosing them. The button now returns every reservation currently marked Pending Cancellation, and the admin ticks the ones to include. Nothing is selected when the modal opens: including a reservation is an act, not a default, because the send is both an email to a university office that will act on it and a status change in Chambers, and neither can be taken back. The filters are gone rather than kept alongside the checkboxes. Two mechanisms for choosing what gets cancelled, one of which quietly hides rows, is how this went wrong in the first place. If the list grows enough to need finding rather than reading, a filter that narrows only what is displayed can come back -- selection would stay explicit. The selection is a choice among what the server finds, never the source of truth. Each row is named by a key scoped to its table, and every submitted key is matched back against a freshly collected set before anything is sent: a client cannot introduce a date, a code, or a booking that is not currently pending with a code on file, and the list that reaches CSC is built from the server's own rows rather than from anything posted. A key that no longer resolves fails the whole request rather than the row. Sending the remainder would cancel a different set from the one the admin reviewed, and they would have no way to tell which. Verified without sending mail or writing to the database. Keys are unique and table-scoped; a valid subset resolves exactly; an unknown key resolves to nothing and is reported; the same uuid under the wrong table prefix resolves to nothing, which is the reason the key carries its table; a mixed selection reports the bad one, so the route refuses the batch. Both handlers answer 401 unauthenticated. Through the real modal: three rows, none ticked, send disabled and the recipient line hidden; ticking one enables it and names both recipients; select all and clear all move between 3 and 0; and ticking the first and third posts exactly those two keys with the middle row excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cancellation request records a cancellation_type: 'Cancellation' when the meeting is off, 'Virtual' when it is moving online. Auto-Cancel was marking everything 'Cancelled', which is wrong for the second -- a virtual meeting still happens, it just does not need the room -- and 'Virtual' is already a status the rest of Chambers understands. Each pending reservation now resolves its own outcome. A request naming the exact week wins over one covering the series; one-time and tabling rows match on their booking. Requests are read once and indexed rather than looked up per row, and ordered so a still-Pending request beats a resolved one and the most recent beats an older one. Where nothing says which, it falls back to 'Cancelled'. That is the common case rather than an edge: of the seven reservations pending right now, four have no cancellation request behind them at all, because a booking can be set to Pending Cancellation directly by an admin. 'Cancelled' is the plain reading of that, and going virtual is a specific thing a requester asks for -- defaulting the other way would quietly leave rooms marked as still meeting. Those rows say so in the list. A reservation taking a default rather than a stated intent is labelled, because that is the one an admin might want to look at before approving it. Updates are grouped by table and by status, so a mixed batch writes each its own value in one statement per pair. Audit rows are keyed the same way: one booking can contribute both a cancelled week and a virtual one, and a single row saying 'Cancelled' would misreport the other. CSC's side is unchanged. Either way the reservation is released, so the email still asks for exactly that. The send button stops claiming to mark things cancelled once a Virtual is in the selection, and the confirmation reports the split. Verified against the live database with nothing written and no mail sent. outcomeOf maps 'Virtual' and 'Cancellation' to their statuses as stated, and null, undefined and an unrecognised value to Cancelled as a default. Resolved across the seven pending reservations it gives six Cancelled and one Virtual with four defaulted, matching an independent SQL join row for row. Through the real modal: each row carries its outcome, the defaulted one is labelled, the button reads "mark cancelled" for a Cancelled-only selection and "apply statuses" once the Virtual is added, and the confirmation reads "2 now marked Cancelled, and 1 marked Virtual". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updated cancellation request email content for clarity and consistency.
package.json, the lockfile's two root entries, and the sidebar label. Only lines 3 and 9 of package-lock.json belong to the project; a find-and-replace across the file would bump a dependency to a version that does not exist and break npm ci. A minor rather than a patch: this release adds things rather than only fixing them. Since 1.13.8 it carries the calendar filling the page (#70), booking cards that lead with the purpose and open their details (#78), deniable revision requests (#77), expected attendance on room requests (#76), creation emails and update emails that say what changed (#79), and Auto-Cancel. The FAQ's roadmap had v1.14.0 as the next unknown release. That entry now describes what actually shipped, and the placeholder moves to v1.15.0 so the roadmap keeps pointing forward. Written for members rather than for this repo: what changed for someone using Chambers, with the administrator-only parts named as such. Also escapes an apostrophe in the v2.0.0 paragraph that has been failing react/no-unescaped-entities since it was written. It is in the file either way, and leaving a known lint error in a file being edited invites the next person to assume it is meant to be there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bfb4b34 rewrote the plain-text body of the CSC request but left the HTML one saying the old thing, so the two halves of the same email disagreed -- and since almost every mail client renders the HTML part, the revision was invisible to nearly everyone who received it. The HTML now carries the same words as the text: the shorter opening, "Requested by <name>." without the trailing division, the reply-all sentence, and the sign-off, which the HTML had never had at all. Wording is mirrored rather than reinterpreted -- these are the sentences from bfb4b34, only marked up. The <strong> on the count and on the requester's name is kept from the existing markup. Verified by rendering both bodies from the real template with Resend intercepted: they now read the same, line for line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A test run went to CSC. The recipient fell back to the real address whenever CSC_EMAIL was unset, so a safe test depended on an environment variable being set, in the right Vercel scope, on a deployment created after it was added -- with no sign when any of that was untrue. Auto-Cancel exists only on this branch, so the run could only have been a preview deployment or a local server, and both took the default. Outside production the default is now refused rather than used. A preview or local deployment must name its recipient explicitly, and if it has not the request fails with an explanation instead of reaching a university office. The failure mode becomes "your test did not send", which costs a minute, rather than "CSC received a real cancellation request", which costs a retraction. Production is untouched: VERCEL_ENV is 'production' there and the default applies, so nothing needs configuring for the feature to work in earnest. Refused before anything happens, so a misconfigured environment can neither send nor cancel. The preview reports the block too, so the modal says sending is disabled and why, rather than letting the button be pressed and fail. Also: the destination was a grey footnote beside the button, and a real send went out with the real address on screen the whole time. It is now a bordered block above the button, red and reading "This goes to CSC for real" when it does, and stating the substitute when redirected. A footnote earned what it got. Carries the cancellation-request closing that prompted this session: sending now marks the requests it fully covered as Done, so nobody closes by hand what Auto-Cancel did. Only when every pending reservation a request covers went out -- an occurrence-scoped request covers one row and closes when selected, but sending three weeks of a five-week series request does not finish it, and closing it would drop the remaining two off the Cancellations tab with nothing done. Verified with nothing sent. The guard refuses local-dev with no override, preview with no override, preview with the variable empty or whitespace, and accepts an explicit override in preview or production; production with no override still resolves to CSC. The coverage rule is a pure exported function because no series-scoped request exists in the data to exercise it. All three modal states render: blocked disables the button and explains why, the real address shows red as "This goes to CSC for real", and a redirect shows the substitute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The template existed but was swallowed by the `.env*` rule, so it lived on one machine and nobody else could see it. It also did not mention CSC_EMAIL at all. Between them that meant the only record of the correct spelling was the source, which is how the variable came to be misnamed in Vercel's preview environment. A misnamed variable is indistinguishable from an unset one, the recipient fell back to the real address, and CSC received a test cancellation request. So the template is tracked, and it now lists every variable the code reads. CSC_EMAIL carries a note on what an empty value means in each environment -- production falls back to CSC on purpose, everywhere else refuses -- and on spelling it exactly. Safe to commit: every value is empty. It is a list of names and comments, which is precisely what was missing. CRON_SECRET was absent too, and UPSTASH_REDIS_REST_URL/TOKEN stay even though they never appear as process.env references -- Redis.fromEnv() in lib/rate-limit reads them implicitly, so a checklist that dropped them would be wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat: Auto-Cancel — email CSC one request for every pending cancellation
The advance-notice requirement rejected bookings that were far enough out. With it set to two hours it behaved like six in summer and seven in winter, which is why it is currently set to 0 in app_settings -- the only way to stop it blocking legitimate bookings. SGA Spaces stores times as Boston wall-clock digits with a Z on the end: a 6 PM booking is T18:00:00.000Z whatever the offset is that month. slotToIso sets UTC hours straight from the slot index, the modal reads them back with getUTCHours, and the confirmation email depends on it too -- there is a comment in space-booking-confirmed.ts saying UTC fields give the right local-time digits to pair with TZID=America/New_York. That is self-consistent until something compares one of those values against a real instant. The check did exactly that: start_time against Date.now(). Every booking therefore looked one UTC offset earlier than it really was, and the requirement rejected anything inside minHours + 4, or + 5 once the clocks go back. Which is why the calendar disagreed with the server. The shading uses wallClockNow, already in the stored domain, so it correctly showed the slot as available -- and then submitting it failed. "Now" is therefore taken as Boston wall clock, in the same domain as the value it is compared against. Pinned to America/New_York rather than the server's clock, because Vercel runs in UTC and reading local fields there is the same bug with an extra step. Intl resolves the offset for the instant given, so EDT and EST both work without a table. Also fixes the same root cause in remaining-hours, which derived its Sun-Sat window from real UTC. UTC is already Sunday by 8 PM on a Saturday in EDT, so for the last four hours of the week a user's hours were counted against the next one and the remaining figure jumped. Half a domain bug is worse than none. Verified by running the check both ways against fixed instants in each season. With a two-hour requirement in EDT, the old comparison rejected every booking up to five hours out and the new one rejects only those under two; in EST the old rejected up to six. Wall clock resolves to 11:46 from 15:46Z in September and to 11:00 from 16:00Z in January, so a hardcoded offset would have been wrong for half the year. min_hours_advance_spaces can go back to a real value once this ships. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The kiosk displays themselves were already right, and deliberately so: the page compares local minutes-from-midnight against the stored value's UTC hours and minutes, which are the wall-clock digits, so both sides are in the same domain. There is a comment saying as much, and the date-flip trap is handled in the page as well -- local date components rather than toISOString, which would roll over at 8 PM EDT. What was still armed is the server's fallback for a missing `date` parameter, which took the UTC date. After 8 PM EDT that is already tomorrow, so a caller without the parameter would be served the wrong day's bookings. The display page always sends it, so nothing reaches this today -- but it is the same trap as the booking buffer, and leaving it set for the next caller is how the buffer bug got written in the first place. Verified at 9:30 PM EDT: the kiosk still shows a 9-10 PM booking as in use, and the two fallbacks disagree by a day exactly as expected -- UTC says the 9th, Boston says the 8th. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix: measure the SGA Spaces buffer in the timezone it is stored in (#87)
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.