Skip to content

Migrate database times to standard timestamp formats + track events in a log - #144

Draft
xalbd wants to merge 1 commit into
mainfrom
database-timestamps
Draft

Migrate database times to standard timestamp formats + track events in a log#144
xalbd wants to merge 1 commit into
mainfrom
database-timestamps

Conversation

@xalbd

@xalbd xalbd commented Jul 29, 2026

Copy link
Copy Markdown
Member

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.

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>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate database times to standard timestamp formats instead of string parsing Store times for everything in the database

1 participant