Migrate database times to standard timestamp formats + track events in a log - #144
Draft
xalbd wants to merge 1 commit into
Draft
Migrate database times to standard timestamp formats + track events in a log#144xalbd wants to merge 1 commit into
xalbd wants to merge 1 commit into
Conversation
Times were spread across four ad-hoc string encodings that every reader had
to re-parse: section.time as "9:00-9:50" or "1:00pm-1:50pm", section.day as
"Monday", availability.week as TEXT, and feedback.submitted_at written with
datetime('now') -- which is not ISO-8601 and which JS Date parses as local
time, so the admin panel patched the string by hand before reading it.
Standardize on three formats, defined once in lib/time.ts:
instant TEXT, ISO-8601 UTC with ms '2026-07-29T04:12:33.123Z'
time of day TEXT, 24h 'HH:MM' LA wall time
day of week INTEGER, ISO weekday, 1 = Monday
section keeps a recurring pattern (day_of_week + start_time/end_time) since
it repeats weekly. availability now stores real instants, computed on write
from QUARTER_START + week + the section's weekday, so queries filter on
start_at instead of rebuilding dates from three columns. Messy Airtable times
are normalized once at ingest in init-sections; nothing downstream re-parses.
Also add observability. Every app table gets created_at/updated_at, with
updated_at maintained by AFTER UPDATE triggers so plain UPDATEs stay recorded
without each call site remembering. availability gets status_changed_at,
stamped by trigger on any open/hidden/taken flip including bulk resets.
Observation sign-up time was previously not recorded anywhere.
These columns are nullable: rows predating the migration have no known
creation time and carry NULL rather than claiming they were created today.
For actions that delete the row they describe -- cancellations, admin
removals, withdraws -- a column cannot survive, so those go to a new
append-only event_log with flat indexed columns and a JSON details blob.
Actions are domain.verb so a prefix match gets a whole domain. actor_email
is denormalized because process-withdraws deletes users.
BetterAuth's tables are left alone; it already writes ISO-8601 UTC.
Verified by applying the full migration chain against SQLite with legacy-shaped
data covering each old time format: backfill produces correct 24h times
(including the noon am/pm edge), unparseable rows keep their identity with NULL
times rather than being dropped, and the triggers fire as intended.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
laprogram | 976816c | Commit Preview URL Branch Preview URL |
Jul 29 2026, 11:24 PM |
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.
Fixes #137 and also fixes #142.
Do not merge yet. This PR has not undergone human review.
Claude summary:
Times were spread across four ad-hoc string encodings that every reader had to re-parse: section.time as "9:00-9:50" or "1:00pm-1:50pm", section.day as "Monday", availability.week as TEXT, and feedback.submitted_at written with datetime('now') -- which is not ISO-8601 and which JS Date parses as local time, so the admin panel patched the string by hand before reading it.
Standardize on three formats, defined once in lib/time.ts:
instant TEXT, ISO-8601 UTC with ms '2026-07-29T04:12:33.123Z'
time of day TEXT, 24h 'HH:MM' LA wall time
day of week INTEGER, ISO weekday, 1 = Monday
section keeps a recurring pattern (day_of_week + start_time/end_time) since it repeats weekly. availability now stores real instants, computed on write from QUARTER_START + week + the section's weekday, so queries filter on start_at instead of rebuilding dates from three columns. Messy Airtable times are normalized once at ingest in init-sections; nothing downstream re-parses.
Also add observability. Every app table gets created_at/updated_at, with updated_at maintained by AFTER UPDATE triggers so plain UPDATEs stay recorded without each call site remembering. availability gets status_changed_at, stamped by trigger on any open/hidden/taken flip including bulk resets. Observation sign-up time was previously not recorded anywhere.
These columns are nullable: rows predating the migration have no known creation time and carry NULL rather than claiming they were created today.
For actions that delete the row they describe -- cancellations, admin removals, withdraws -- a column cannot survive, so those go to a new append-only event_log with flat indexed columns and a JSON details blob. Actions are domain.verb so a prefix match gets a whole domain. actor_email is denormalized because process-withdraws deletes users.
BetterAuth's tables are left alone; it already writes ISO-8601 UTC.
Verified by applying the full migration chain against SQLite with legacy-shaped data covering each old time format: backfill produces correct 24h times (including the noon am/pm edge), unparseable rows keep their identity with NULL times rather than being dropped, and the triggers fire as intended.