Skip to content

Normalize ALL timestamps to epoch-ms + consolidate baseline (closes #226) - #40

Merged
important-new merged 7 commits into
mainfrom
feat/schema-timestamp-ms
Jul 13, 2026
Merged

important-new merged 7 commits into
mainfrom
feat/schema-timestamp-ms

Conversation

@important-new

@important-new important-new commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Completes InspectorHub#226: every time-semantic Drizzle column now stores integer epoch-milliseconds with explicit { mode: 'timestamp_ms' }; the only TEXT time columns left are documented calendar/clock exceptions (due_date, closing_date, start_time, end_time, date).

Because OpenInspection is pre-launch, this follows the repo policy (drizzle.config.ts) of regenerating the single migrations/0000_baseline.sql rather than stacking forward migrations — so the prior incremental timestamp work is collapsed into one clean baseline with no dead columns.

What changed

  • mode:'timestamp' (seconds) → timestamp_ms with an idempotent ×1000 conversion; bare integer('*_at') → explicit timestamp_ms; text datetime('now') and text-ISO '*_at' columns → integer timestamp_ms.
  • 11 text-ISO _at columns converted (automation_logs, inspections, inspection_requests, discount_codes, marketplace_templates/libraries, tenant_marketplace/library_imports).
  • lint:timestamps gate (scripts/check-timestamps.mjs) with 4 rules incl. Rule D: text('*_at') is a violation; calendar/clock text columns documented as intentional exceptions.
  • Single consolidated 0000_baseline.sql.

Verification

  • lint clean (timestamps gate 0 violations), db:check no drift (81 tables), type-check clean, suite green (unit 3085 / web 1188 / workers 87).
  • Data preserved on the deployed DB via export → ISO→ms transform → reimport: 2841 rows, zero loss, all timestamps land in epoch-ms.

🤖 Generated with Claude Code

important-new and others added 7 commits July 13, 2026 16:05
Annotates all 35 bare integer('*_at') columns (18 always-ms + 17
formerly-seconds columns migrated to ms by the prior task) with
{ mode: 'timestamp_ms' }, and exempts the one dead frozen column
(users.expires_at, guest removal) with a ts-lint-ok comment so the
gate skips it.

Fixes every consumer broken by the resulting number->Date type change:
insert/update sites across server/services and server/portal switch
from Date.now()/Math.floor(.../1000) to new Date(...), and read sites
that expose a public number contract (unix-ms or unix-seconds, e.g.
observer-link exp, QBO connection status, report verification
publishedAt) convert back via .getTime()/1000 at the boundary so
external consumers (settings UI, public verify page) are unaffected.

Also fixes two call sites that only broke at runtime because they
route through untyped `any` seams (ScopedDB.insert, the dual-driver
erasure orchestrator) that tsc cannot see through, and updates all
affected test fixtures (unit + workers) accordingly.

lint:timestamps now reports only the 4 remaining text datetime('now')
columns (Task 5 scope). db:check shows no drift (mode-only change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hkbz8jKCeiQigeqLuBNvhH
…stamp_ms

Numbered the drizzle-generated retype 0026 (not 0025) to avoid colliding with
the hand-authored 0025 seconds->ms data migration, which is not tracked in the
drizzle journal (mirrors the portal convention: pure data migrations occupy a
file number without a journal entry, leaving an idx gap).
Adds scripts/check-ts-range.mjs (npm run check:ts-range) — samples
MIN/MAX per column across a representative set of migrated tables
and fails if any non-null value falls outside epoch-ms ~2015-2035.

Refreshes the tenant-scoping baseline (line-shift only, no new
findings) — prior timestamp-migration commits on this branch moved
lines in observer-link.service.ts and qbo/api-base.ts past two
pre-existing, verified-safe update-by-id queries.
…to timestamp_ms via baseline consolidation

Converts the remaining 11 text('*_at') columns (automation_logs.send_at/
delivered_at, inspections.confirmed_at, inspection_requests.scheduled_at,
discount_codes.expires_at, marketplace_templates/libraries.created_at/updated_at,
tenant_marketplace/library_imports.imported_at) from ISO-string TEXT to
integer { mode: 'timestamp_ms' }. Documents the calendar/clock exceptions
(due_date, closing_date, start_time, end_time, date) that intentionally stay TEXT.

Per the repo pre-launch policy (drizzle.config.ts), schema changes regenerate the
single migrations/0000_baseline.sql — so this collapses the prior forward
migrations (incl. the B1 timestamp work) into one clean baseline with no dead
columns. Existing data is preserved by an export -> ISO->ms transform -> reimport
of the deployed DB (validated: 2841 rows, zero loss).

Widens lint:timestamps with Rule D (text('*_at') is a violation). All time-semantic
Drizzle columns now use integer timestamp_ms; closes the acceptance criteria of InspectorHub#226.
@important-new important-new changed the title Normalize all timestamp columns to epoch-milliseconds (timestamp_ms) Normalize ALL timestamps to epoch-ms + consolidate baseline (closes #226) Jul 13, 2026
@important-new
important-new merged commit 5f56679 into main Jul 13, 2026
5 checks passed
@important-new
important-new deleted the feat/schema-timestamp-ms branch July 13, 2026 23:37
important-new added a commit that referenced this pull request Aug 23, 2026
…ges (PDF TOC degrade + Word-export memory) (InspectorHub#242)

* fix(pca): degrade TOC pass-2 render gracefully on Browser Rendering 429

The two-pass real-TOC-page-number renderer (generatePdfWithTocPages, InspectorHub#240)
fires pass 2 immediately after pass 1. On the Workers Free plan, Browser
Rendering allows only 1 Quick Action every 10 seconds, so pass 2 is rejected
with HTTP 429 (code 2001) before it loads — which threw and 500'd EVERY
full-tier PCA PDF download on free-tier BR.

Pass 2 is an enhancement, not a requirement: on 429, back off 11s (clears the
free-tier 10s window; wall-time only, no browser-hours) and retry once, then
degrade to the un-numbered pass-1 PDF instead of throwing. Paid plans
(10 Quick Actions/second) never hit this path. Mirrors the existing
extractAnchorPages graceful-degrade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YErf73G9tqjUmMSB2y1RUB

* feat(pca): consistent Browser Rendering rate-limit UX across all PDF surfaces

Every surface that triggers a live Browser Rendering render on click now shares
usePdfExport(): it fetches the PDF, and on a rate-limit / hard failure shows the
reason and counts down a cooldown ('Retry in Ns') so repeated clicks don't
compound the free-tier limit (1 render / 10s). Copy is centralized in the hook so
the reminder reads identically everywhere.

- ReportView 'Download PDF' FAB (owner + token) — refactored onto the hook
- Editor 'Preview PDF' — was window.open (raw JSON 500 on failure) -> hook (view)
- Verify page 'Download signed vN PDF' — was <a download> -> hook (download)
- Repair-request 'View as PDF' — was <a target=_blank> -> hook (view)

view-mode pre-opens the tab inside the click gesture to dodge the popup blocker.
DS-conformant (ih-* tokens only). buttons source-scan test follows the logic into
the hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YErf73G9tqjUmMSB2y1RUB

* fix(pca): bound Word-export photo memory + declare optional IMAGES binding

The Word (.docx) export embeds Appendix B photos. When the IMAGES binding is
present each is downscaled to a ~960px JPEG (~200KB) and 50-100 photos fit under
the isolate memory limit. When IMAGES is unset (an account without Images
Transformations enabled), photos embed at full resolution (2-4MB) and a large
report would OOM — docx's Packer.toBuffer materializes the whole document (all
media) in memory, so peak is ~2x the embedded bytes.

- buildAppendixPhotoInputs now embeds photos in order until a 32MiB byte budget
  is reached, then omits the rest (logged), keeping >=1 photo. Peak stays well
  under 128MiB regardless of the IMAGES binding or a transform-failure fallback.
- wrangler.jsonc declares the optional IMAGES binding (account-scoped name-only,
  like STREAM): accounts with Images Transformations enabled — Free plan included,
  5,000/mo — get downscaled exports; accounts without still deploy fine with
  env.IMAGES unset and fall back to the byte budget.
- Deploy doc notes the binding is optional and how the fallback behaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YErf73G9tqjUmMSB2y1RUB

* test(pca): real-isolate memory profile — 40x1MiB no-IMAGES export stays bounded

Adds a workerd stress case exercising the dangerous path the byte budget guards:
env.IMAGES unset (full-resolution embed), 40 genuinely-distinct 1 MiB photos.
Proves in real workerd that the export completes (no OOM/retry) and
APPENDIX_PHOTO_TOTAL_BYTE_BUDGET caps embedding (imageRelCount < 40; the tail
photo #40 is omitted). Parameterizes seedLargeCommercialInspection with a
photo-bytes fn so the existing 60-photo (tiny-PNG) case is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YErf73G9tqjUmMSB2y1RUB

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant