Build a phone-first local web app that becomes the authoritative record of Daniel's supervised driving history for North Carolina Level 2 provisional license requirements.
The system must:
- Track a 60 hour supervised-driving target.
- Track a separate 10 hour night-driving target.
- Seed itself from the existing records in this repo.
- Allow review, entry, deletion, export, and import of drives.
- Support a live-drive flow with start, end, and cancel.
- Be deployable on this Rocky host as a persistent local service.
The North Carolina DL-4A form requires these fields per log entry:
datetime_of_daytime_of_nightamount_of_driving_timesupervising_driver_printed_namesupervising_driver_dl_number_and_state
It also requires totals for:
- total day hours driven
- total night hours driven
- grand total
The form text also states:
- minimum 60 hours total
- at least 10 hours at night
- no more than 10 hours per week may count toward the 60 hours
The product should track the 10-hours-per-week rule as an advisory. It must continue recording and totaling every valid drive, while clearly flagging the drive that causes a week's total to exceed 10 hours.
Current seed sources:
records/2026-07-02 Daniel driving log.pdfrecords/log.txt
Observed characteristics:
- The PDF contains many historical entries with a start timestamp and either day duration or night duration.
- The PDF already reports aggregate totals:
17h 0mday and1h 8mnight. records/log.txtadds later entries in free-form text.records/log.txtincludes several entries with start/end times, some with duration only, and one final line with no explicit duration.- The sources may contain duplicates or partial overlap.
Implication: import must preserve provenance and warnings. The system must not silently normalize ambiguous records into "clean" data without keeping an audit trail.
Use a small Python application with:
Python 3.13FastAPIfor the HTTP server and JSON/web handlersSQLitefor the authoritative database- server-rendered HTML templates for the UI
- minimal client-side JavaScript for large tap targets, live updates, and the progress gauge
Why this stack:
- Easy to run on Rocky with a user-level
systemdservice - SQLite is sufficient for a single-family log and easy to back up
- Server-rendered pages reduce complexity and make Tailscale deployment simple
- Python has mature timezone and astronomy libraries
The production baseline is:
- Rocky Linux 9.8
- CPython 3.13.14
- Python
sqlite3linked against SQLite 3.34.1 or newer - FastAPI 0.139.2
- Uvicorn 0.51.0
- Jinja2 3.1.6
- python-multipart 0.0.32
- Astral 3.2
- tzdata 2026.3
The implementation must commit separate runtime and test lock files containing
all transitive versions and package hashes. ./driving-log bootstrap creates a
repo-local .venv with python3.13 and installs only those locked artifacts.
Production must not install from an unbounded version range.
The test baseline is pytest 9.1.1, HTTPX 0.28.1, and Playwright 1.61.0, also hash-locked. Browser support is Safari and Chrome on iOS 18 and 19, plus the current and immediately previous desktop Chrome releases. Automated phone coverage uses Playwright WebKit at representative iPhone viewport sizes. A release also requires a smoke test in Chrome on an actual iPhone because all iOS browsers use WebKit but browser chrome and input behavior still differ.
Dependency upgrades are explicit commits that regenerate lock files and run the
full test suite. The application records its release and schema version so
doctor can report the exact deployed combination.
Canonical stored record for a completed drive.
Fields:
idUUID primary key, stable across CSV export/importrequest_idnullable UUID, unique idempotency key for manual creationlive_drive_idnullable UUID with a unique constraintdriver_namesupervisor_namesupervisor_dl_numbersupervisor_dl_statestarted_at_utccanonical UTC instantended_at_utccanonical UTC instanttimezone_name, normallyAmerica/New_Yorkstarted_utc_offset_minutesended_utc_offset_minutesduration_minutesday_minutesnight_minutessolar_calculation_versionsolar_latitudesolar_longitudetzdata_versionroad_typeenum-like text:local,highway,mixed,unknownweatherfree textnotesfree textsourceenum-like text:manual,live_drive,seed_pdf,seed_log_txt,microsoft_formsource_referenceoriginal source locator, such as file name plus row/indeximport_batch_idnullable foreign keycreated_atupdated_atversioninteger incremented on every editdeleted_atnullable soft-delete timestamp
Rules:
duration_minutes = ended_at_utc - started_at_utcduration_minutes = day_minutes + night_minutes- only non-deleted rows count toward totals
- retrying a create with the same
request_idand payload returns the existing drive; reusing it with a different payload returns a conflict - CSV import preserves the original
sourceand usesimport_batch_idto record how the row entered this database - edits preserve
id, source provenance, and creation time; they use the submittedversionfor optimistic concurrency and return a conflict rather than overwriting a newer edit
Stores actionable source-quality findings found during import. They are shown until the drive is reviewed and successfully saved, at which point its source warnings are deleted in the same transaction as the edit.
Fields:
iddrive_idwarning_codewarning_messagecreated_at
Intrinsic warnings such as long duration and crossing midnight, and relational warnings such as overlaps and weekly overage, are derived from the current non-deleted drive set. They must not be persisted as authoritative warning rows because imports and deletions can invalidate them.
Ordinary parsing choices, such as calculating duration from explicit start and
end timestamps, belong only to import_rows provenance. Actionable ambiguity
or source conflict appears initially with a link to review and save the drive.
Exact source duplicates use the derived overlap warning instead of a persisted
source warning.
Represents the durable lifecycle of a live drive.
Fields:
iddriver_namesupervisor_namestarted_at_utctimezone_namestarted_utc_offset_minutesstarted_fromsuch aswebstatus:active,ending,completed, orcancelledprovisional_ended_at_utcnullableprovisional_ended_utc_offset_minutesnullableend_request_idnullable unique UUIDcompleted_drive_idnullable unique foreign keyfinalization_payload_hashnullablefinalized_atnullablecreated_at
Constraints:
- a partial unique index permits only one row whose status is
activeorending - a completed drive's
live_drive_idand the live row'scompleted_drive_idprovide a stable one-to-one identity - completed and cancelled live rows remain as audit history
Tapping End drive is a durable transition performed in one BEGIN IMMEDIATE
transaction:
- Read the live row by its stable UUID.
- If it is active, record the server's current UTC instant and offset as the
provisional end, store the request UUID, and change status to
ending. - If it is already ending with the same request UUID, return the same provisional end as a successful retry.
- If it is completed or cancelled, return that terminal state; conflicting request data returns a conflict.
- Commit the transition before showing the completion form.
If the process or host fails before commit, SQLite rolls back both changes. If it fails after commit but before the phone receives the response, a new session resumes the completion form with the same provisional end time.
Finalizing an ending drive is a second BEGIN IMMEDIATE transaction. It
validates metadata and any corrected end instant, inserts exactly one completed
drive, and marks the live row completed with the drive ID and payload hash. A
retry with the same payload returns that completed drive; different data
returns a conflict. Cancel changes either active or ending to cancelled in
one transaction. A deliberate Resume drive action may change ending back to
active after confirmation and clears the provisional end. Repeated terminal
actions are idempotent. After restart, an active or ending row remains visible
with the appropriate timer or completion interface.
Tracks seed, CSV, and future form imports.
Fields:
idsource_typesource_namecontent_sha256uniqueformat_versionimported_atraw_snapshotcompressed blobstatussummary_json
Required row-level audit history for every seed, CSV, and future Microsoft import.
Fields:
idimport_batch_idsource_typesource_instance_id, identifying the particular form, workbook, or seed filesource_row_keyraw_textparsed_payload_jsonresult_drive_idnullablestatuserror_messagenullable
Constraint:
- unique
(source_type, source_instance_id, source_row_key)across all batches
Stores non-secret application settings required to reproduce calculations, including location, timezone, calendar-week convention, driver name, and optional supervisor defaults. Secrets and Microsoft credentials remain outside the database.
Stores the private export-time mapping from a supervising driver's normalized name to their license number and two-letter state. Profiles are managed from the DMV export page, included in full-state archives, and excluded from CSV backups and audit metadata. Ordinary drive entry continues to store only the supervisor name.
Records security-neutral mutations and operational events such as drive creation/deletion, live-drive finalization/cancellation, imports, archive creation, migrations, and restores. It stores IDs and outcomes but never supervisor license numbers or credentials.
Every browser mutation carries a generated request UUID stored uniquely with its audit event in the same transaction. Repeating the same request and payload returns the prior result; reusing the UUID with a different action or payload returns a conflict. This covers response-loss retries for edits and deletions as well as creation.
The database starts empty and is seeded from the two current record files.
- Import all PDF-derived records.
- Import all
log.txtrecords. - Run duplicate/overlap detection across the combined result.
- Present a seed review page showing ambiguous or suspicious rows.
For the PDF:
- Parse date/time as the drive start time.
- Parse total duration from whichever source day/night column is populated, but treat the source column classification as provenance rather than canonical day/night truth.
- Resolve the start to a UTC instant using
America/New_York, compute the end instant from duration, and recompute canonical day/night minutes from the Apex solar rule. - Preserve the source day/night value in
import_rows; attach a reviewableseed_day_night_mismatchwarning when it differs from the computed split. - Keep environment text as
road_typewhen possible. - Store
Sean Ahernas supervisor where present.
For log.txt:
- Accept both
start only + durationandstart-end + durationformats. - If a row includes start and end but no duration, resolve both to UTC and compute duration.
- If a row includes duration but no end, compute the UTC end from start plus duration.
- Recompute every canonical day/night split from the resulting UTC interval, regardless of any day/night label in the source text.
- If a row lacks enough information to compute duration, import it as a failed
row in
import_rowsand require manual completion in the UI.
The last line in records/log.txt has no separately written duration:
2026-07-24 11:10-11:31: local and highways with wet roads, cloudy conditions
That line has enough information to be complete: its start and end timestamps define a 21 minute duration. The importer computes that duration without a warning and preserves the source row as provenance.
After initial seed, the database is authoritative.
- The original source files are historical inputs, not ongoing truth.
- All later edits happen through the app or explicit import.
- Every imported row keeps provenance so disputes can be traced back.
Night driving must be computed from entered timestamps, not manually toggled.
- Users enter and view local times in
America/New_York. - Store canonical start and end as UTC instants, along with timezone name, entered offsets, and the pinned tzdata version used to resolve them.
- Use the pinned
tzdatapackage rather than host-global timezone files so calculations are reproducible after redeployment. - Reject local times that do not exist during the spring DST transition.
- When a manually entered time is ambiguous during the fall transition, require the user to choose the first or second occurrence and show the corresponding timezone abbreviation and UTC offset.
- Live-drive timestamps come from server UTC and are therefore unambiguous; local values are display conversions.
For every local date intersected by a drive:
- obtain sunrise and sunset for the local area
- define daytime start as
sunrise - 15 minutes - define daytime end as
sunset + 15 minutes - any portion outside that daytime window counts as night minutes
Implications:
- A single drive can contribute to both day and night tallies.
- Intersect the UTC drive interval with every relevant local solar boundary; do not assign a cross-midnight drive entirely to its start date.
- Store the calculation version, Apex coordinates, timezone, and tzdata version on each drive so historical totals remain explainable after configuration or dependency changes.
For version 1, use Apex, North Carolina as the configured home location for astronomy calculations. Recommended configuration:
location_name = Apex, NClatitudelongitudetimezone = America/New_York
Resolve and store fixed coordinates for Apex during implementation rather than calling a geocoding service for every drive. If the family routinely drives far from home, location can become a per-drive field later, but that is likely unnecessary for this use case.
Use a maintained astronomy library such as astral so sunrise/sunset and DST
are delegated to a standard source rather than custom formulas.
Reject save when:
- the resolved end instant is before the resolved start instant
- duration is zero or negative
- a local timestamp falls in a nonexistent DST interval
- an ambiguous fall-DST timestamp has no selected occurrence/offset
- required fields are missing for a completed drive
Allow save, but show prominent warnings when:
- duration exceeds 5 hours
- drive crosses midnight
- drive overlaps another saved drive
- the drive would cause its week's total to exceed 10 hours
- imported data contains an ambiguity or conflicts with its canonical value
The DMV form states that no more than 10 hours per week may count toward the 60.
Recommended behavior:
- keep
total_minutesas the sum of every valid drive - use
total_minutesfor the primary 60-hour progress gauge - intersect each drive's UTC interval with local calendar-week boundaries and allocate minutes on each side; do not assign a cross-boundary drive wholly by its start date
- derive over-cap weeks from all current non-deleted drives whenever totals are read
- after every create, import, or deletion, return the newly computed week total and overage in the mutation response
- show over-cap weeks prominently on the dashboard and drive list
- allow the drive to be saved after the warning is acknowledged
The query returns the week boundaries, total minutes, and
max(0, total_minutes - 600) overage. It also identifies all drives in that
week and the first drive in chronological order that moves the cumulative
weekly total above 600 minutes. Because this state is derived rather than
stored, deleting an earlier drive or importing a historical drive immediately
moves or removes the warning correctly.
The supplied form does not define the week boundary. Use a documented calendar
week convention in version 1 and keep that convention configurable. The default
is Sunday 12:00 a.m. through the following Sunday 12:00 a.m. in
America/New_York. Before final DMV submission, any week over 10 hours should
be reviewed manually.
The main usage mode is phone-first while seated in the passenger seat. The UI should favor large targets, high contrast, and minimal typing.
Use the same Apex solar rule as driving classification for the interface theme:
- light theme from 15 minutes before sunrise through 15 minutes after sunset
- dark theme at all other times
Every server-rendered page includes the theme effective at the response's server
UTC time and the next 48 hours of precomputed theme-boundary UTC instants. Set a
root data-theme value, CSS color-scheme, and the browser theme-color so
native form controls and surrounding browser UI match the active light or dark
palette. Use system conventions for safe areas, text sizing, focus indication,
reduced motion, and touch controls while keeping the solar theme authoritative
over prefers-color-scheme.
JavaScript schedules each supplied boundary and flips the root theme locally. If iOS suspends the page and delays a timer, the visibility handler immediately applies every elapsed boundary before rendering and then refreshes the schedule. Static pages are correct when loaded; the live-drive interface is required to remain open and flip at a boundary without a page reload.
Primary landing page.
Shows:
- circular progress gauge for
total_minutes / 60 hours - separate night progress gauge or clear secondary stat for
night / 10 hours - numeric totals:
total,night total,remaining - advisory card listing any weeks over the 10-hour cap and each overage amount
- active timer or ending-drive completion banner when a live row is in progress
- quick actions:
Start a drive,Add drive,View drives,Import,Export
Gauge behavior:
- speedometer-like arc with a clear
60htarget marker - secondary night stat should be visible without scrolling
- color should indicate
not ready,near goal,goal met
Mobile-friendly chronological list with filters.
Capabilities:
- view recent drives first
- filter by month
- filter rows with warnings only
- tap a drive to open details
- edit an existing drive from its details view
- delete an incorrect drive from the details view
Displayed row summary:
- start date
- start-end
- duration
- day/night split
- road type
- warning badge if present
Must be large and easy to use quickly.
Form design:
- big touch targets
- one-column layout
- large time pickers
- large save button
- defaults for frequent values such as supervisor name
- radio or segmented buttons for road type
- weather as a short optional text field
Fields:
- start date
- start time
- end date, defaulting to the start date
- end time
- large
Ends next daycontrol that advances the end date by one day - supervisor name
- road type
- weather
- notes
Supervisor DL number and state remain nullable and are deliberately omitted from ordinary drive entry. A later DMV export mapping will associate the entered supervisor name with license details and warn if that mapping is missing.
On submit:
- show computed duration
- show computed day/night split
- show both local dates when the drive crosses midnight
- show warnings before final confirm if needed
The edit form reuses the large manual-entry controls and allows correction of:
- date
- end date or
Ends next day - start time
- end time
- supervisor name
- road type
- weather
- notes
Saving an edit occurs in one transaction. It validates the submitted record
version, reruns hard validation, recomputes duration and day/night minutes, and
updates the drive without changing its stable ID or provenance. It also clears
the drive's reviewable import warnings. The response
includes newly derived long-drive, midnight, overlap, and weekly-overage
warnings. An audit_events row records the before and after values with private
license data redacted. If another session edited the drive first, return a
conflict and show both versions for deliberate reconciliation.
Repeating a completed edit with the same request UUID returns the prior result instead of applying the edit twice.
Entry point:
- tap
Start a drive
At start:
- record the current server UTC timestamp and local display offset
- optionally capture default supervisor immediately
- submit a browser-generated UUID so a repeated tap or retried request returns the same live drive
During active drive:
- dashboard prominently shows the fixed local start time, timezone
abbreviation, live duration as
H:MM:SS, andEnd drive/Cancel drive - refreshing the page or restarting the service recovers the stored active or ending drive rather than starting a new timer
- the active drive is household/server state, not browser-session state; its ownership must not depend on a cookie, tab, IP address, Tailscale address, or in-memory process object
- every authorized dashboard request queries SQLite for the single active or
ending row; active elapsed time is reconstructed from
started_at_utc, while ending state resumes the completion form with its provisional end
The live state response includes:
- stable live-drive ID and state
- canonical
started_at_utc - formatted local start time and timezone abbreviation
- current
server_now_utc - current solar theme
- the next 48 hours of solar theme-boundary UTC instants
At response receipt, the client calculates the authoritative elapsed baseline
from server_now_utc - started_at_utc, captures both performance.now() and
Date.now(), and derives a server-to-device wall-clock offset. While visible,
it redraws once per second from the baseline plus monotonic elapsed time; it
never increments a counter that can accumulate timer drift.
On return from iOS suspension, rebase immediately from
Date.now() + server_offset - started_at_utc so elapsed duration and solar
boundaries catch up even before the network is available. Mark that projection
as awaiting server confirmation, then replace the offset after a successful
resynchronization. A material disagreement between monotonic and adjusted wall
time also triggers this rebase and resynchronization path.
No per-second network requests are made. Resynchronize live state and server time only on initial load, return from background/visibility change, explicit reconnect, exhaustion of the supplied boundary schedule, or a start/end/resume/cancel/finalize mutation. If the network is unavailable, the projected timer and supplied theme schedule continue locally and the interface clearly marks server synchronization as pending. Persisted duration and end time always come from server-owned UTC timestamps or an explicitly corrected end time, never from the projected display counter.
End flow:
- tap
End drive - durably record the current server UTC timestamp and transition to
ending - render the completion form only after that transition commits
- prompt only for remaining metadata:
road_type,weather,notes, optional supervisor confirmation - show warnings and computed totals before final save
- submit to the stable live-drive URL; duplicate submissions return the same completed drive
- default the end timestamp to the successful end request time, but allow it to be corrected before confirmation when reconnection or reauthentication delayed the request after the car stopped
- if the browser or network disappears after the tap, a new session resumes this same completion form and provisional end time
Cancel flow:
- tap
Cancel drive - require confirmation
- mark the live-drive row cancelled without creating a completed drive
- permit cancellation from either active or ending state
- repeat submissions return the already-cancelled result
Required reconnect scenario:
- Start a live drive from an iPhone.
- Allow the browser to be reaped and Tailscale to disconnect.
- Reauthenticate to Tailscale from a new network address and open a new browser session.
- The dashboard must show the same active drive, original start time, and
reconstructed elapsed timer with working
End driveandCancel drivecontrols.
The same outcome is required if the web service or Rocky host restarts while the phone is disconnected. A parallel test ends the drive before disconnection and must recover the ending-state completion form and provisional end time.
Web actions:
- export all non-deleted drives to CSV
- import CSV backup
- create and download a full-state archive
- upload and verify a full-state archive; restoration requires a separate, explicit confirmation
- show import summary with created rows, skipped rows, warnings, and failures
CSV is the human-readable, portable backup of the current completed-drive log. It is not a complete application archive and does not include deleted drives, live-drive state, configuration, import audit rows, or audit events.
Recommended columns:
format_versioniddriver_namesupervisor_namesupervisor_dl_numbersupervisor_dl_statestarted_at_utcended_at_utcstarted_at_localended_at_localtimezone_namestarted_utc_offset_minutesended_utc_offset_minutesduration_minutesday_minutesnight_minutessolar_calculation_versionsolar_latitudesolar_longitudetzdata_versionroad_typeweathernotessourcesource_reference
Rules:
- export in UTF-8
- use ISO-like local datetime strings with timezone offset
- use RFC 3339 UTC strings for canonical start/end instants; local strings are human-readable redundant values
- export only non-deleted completed drives
- preserve each drive's stable UUID and original provenance
- validate
format_version, headers, every row, and all duplicate IDs before making any database change - recompute duration and day/night minutes from the canonical UTC interval and recorded calculation context; reject internal inconsistencies rather than trusting redundant CSV totals
- calculate a SHA-256 hash for the exact uploaded file and store it as the
unique
import_batches.content_sha256 - perform the entire import and its audit rows in one transaction
- if the same completed file is imported again, return the original import summary without creating rows
- if a drive UUID already exists with identical canonical content, skip it
- if a drive UUID exists with different content, abort the whole import and report the conflict; never silently overwrite
- if power or process failure interrupts import, the transaction rolls back and retry is safe
CSV import is append/reconcile only. Full replacement belongs to the full-state restore workflow below.
A full-state archive is the disaster-recovery backup. It contains a consistent SQLite snapshot with completed and soft-deleted drives, source warnings, every live-drive lifecycle state, configuration, import raw snapshots and audit rows, schema history, and application audit events. Credentials remain external and are documented separately.
Storage defaults:
- state directory:
/home/ahern/.local/state/driving-log - live database:
/home/ahern/.local/state/driving-log/driving-log.sqlite3 - archive directory:
/home/ahern/.local/state/driving-log/archives - staged restore directory:
/home/ahern/.local/state/driving-log/restore-requests - directory mode
0700; database and archives mode0600
The user-level systemd units express /home/ahern with the %h specifier, but
the application resolves and reports the absolute path shown above.
Archive creation:
- Use Python's SQLite online backup API to write a temporary snapshot without copying the live database or WAL files directly.
- Run
PRAGMA quick_checkagainst the snapshot. - Record archive format, application version, schema version, creation time, database size, and the snapshot's SHA-256 in a manifest.
- Package the snapshot and manifest into one versioned archive bundle.
fsyncthe bundle, atomically rename it into place, thenfsyncthe archive directory.- Mark the archive verified only after reopening the bundle, checking the
snapshot against the manifest hash, and running
quick_check.
driving-log-archive.timer creates and verifies one archive daily. Retain the
most recent 14 daily archives and 8 weekly archives. A verified pre-migration
archive is mandatory and is not removed by ordinary daily retention.
Local archives protect against database corruption but not loss of Rocky's
disk. Configure DRIVING_LOG_EXTERNAL_ARCHIVE_DIR to a mounted external disk,
NAS, or other destination outside the live database filesystem.
After local verification, the archive service copies the bundle to a temporary
name at that destination, fsyncs it, atomically renames it, and independently
verifies its manifest hash and SQLite quick_check. Retain at least 8 verified
weekly external archives. Failed or unavailable replication never deletes a
previous external archive and produces a dashboard and doctor warning.
archive create --out PATH supports an explicit one-off external destination,
and the web download provides a manual off-host copy. Production readiness must
show either a verified configured external archive or an explicitly
acknowledged warning that disaster recovery is limited to the Rocky disk.
CLI restoration runs directly while the web service is stopped. Web restoration uses an external user-systemd helper so the request is not responsible for stopping and restarting its own process:
- The web process uploads an archive into a UUID-named restore request directory and verifies it without changing live state.
- After explicit confirmation, it atomically writes an HMAC-signed request
manifest containing the operation ID, expected archive hash, and bounded
execution window. The signing key is in the mode-
0600service environment file, not the database or archive. - It starts
driving-log-restore@OPERATION_ID.serviceand returns HTTP 202 with the operation ID. The signed request manifest contains a shortnot_beforetime so the helper cannot stop the web service until the response has had time to flush. - The helper re-verifies the staged archive and request manifest, performs the restore procedure below, restarts the web service, and atomically writes a result file outside the restored database.
- After reconnect, the browser reads the result by operation ID. The same
result is available through
./driving-log archive restore-status IDif the web service could not restart.
Restore procedure:
- Stop the web service and acquire the application lock.
- Verify the archive hash, archive format, SQLite integrity, and supported schema before touching the live database.
- Move the current database and any WAL/SHM files to a timestamped quarantine directory; never overwrite the only copy.
- Restore to a temporary file in the state directory, set permissions,
fsync, and atomically rename it to the live database path. - Start the service, run readiness and
quick_check, and retain the quarantine copy until the restored service is manually accepted.
The automated integration suite must prove that a restored archive reproduces all authoritative tables, including an active live drive, deleted rows, configuration, imports, and audits.
Provide simple commands runnable from this repo.
Recommended command surface:
./driving-log bootstrap./driving-log serve./driving-log start./driving-log stop./driving-log restart./driving-log status./driving-log doctor [--json]./driving-log db check./driving-log live status./driving-log imports status./driving-log seed./driving-log csv export --out driving-log.csv./driving-log csv import --in driving-log.csv./driving-log archive create [--out PATH]./driving-log archive list./driving-log archive replicate./driving-log archive verify [ARCHIVE]./driving-log archive restore ARCHIVE --confirm./driving-log archive restore-status OPERATION_ID
Use a small shell entrypoint plus the standard-library argparse module. The
commands are thin wrappers around the application's real service, import, and
archive logic so behavior stays consistent between CLI and web.
doctor is read-only and reports:
- application, Python, SQLite, and schema versions
- configured and resolved database/archive paths and file permissions
- effective SQLite journal, synchronous, foreign-key, and busy-timeout settings
PRAGMA quick_checkresult- active or ending live-drive ID, state, age, and provisional end presence, without private supervisor details
- incomplete or failed import batches
- newest full archive age and latest verification result
- newest external archive age, destination, and verification result
- filesystem free space
- user-service state and recent restart count
- local HTTP liveness/readiness independently from Tailscale Serve status
- Tailscale forwarding configuration and reachability
This separation makes it clear whether an outage is in the process, database, local HTTP service, or Tailscale forwarding layer.
Constraints:
- do not conflict with the existing service on port 80
- primary access will be over Tailscale
Recommended runtime:
- bind the app locally on
127.0.0.1:8766 - expose it through Tailscale Serve or an equivalent forwarding rule
- keep the internal app port configurable
- keep all mutable state outside the Git worktree at
/home/ahern/.local/state/driving-log
Persistence across reboot:
- install a user-level
systemdunit - enable lingering for the user if needed so the service survives logout
- provide CLI helpers that wrap
systemctl --user
Recommended service units:
driving-log-web.servicedriving-log-archive.servicedriving-log-archive.timerdriving-log-restore@.service- optional
driving-log-import.timerlater for scheduled form ingestion
Every database connection must enable and verify:
PRAGMA journal_mode = WALPRAGMA synchronous = FULLPRAGMA foreign_keys = ON- a finite
busy_timeout
All mutations use explicit transactions. Multi-row operations, imports,
live-drive transitions, warning acknowledgements, and audit events commit as
one unit. Write transactions use BEGIN IMMEDIATE when serialization is needed.
The service must never copy a live SQLite file as a backup.
On every startup:
- Resolve and log the exact database path.
- Open SQLite and apply connection pragmas.
- Run
PRAGMA quick_check. - Verify the schema version and migration checksums.
- Apply required migrations only after creating a verified pre-migration archive.
- Run
quick_checkagain before the readiness endpoint succeeds.
If integrity checking, a migration, or post-migration checking fails, the service refuses writes and readiness fails. It logs the recovery command and archive candidates but never silently restores or discards the database.
Maintain an ordered schema_migrations table with version, name, checksum, and
applied timestamp. Each migration runs transactionally; failure rolls back and
prevents service startup. The application refuses to open a schema newer than
it understands. Downgrades are not automatic and require restoring a compatible
pre-migration archive.
Expose localhost health endpoints:
/health/live: process event loop is responsive/health/ready: database opens, schema is supported, integrity check passed, and migrations are complete
Emit one-line structured JSON logs to stdout/stderr for journald. Include event
name, timestamp, severity, request ID, relevant record IDs, duration, and
outcome. Exclude supervisor license numbers, credentials, raw import contents,
and other private form data. ./driving-log status shows concise service state;
doctor performs the deeper read-only checks. Operators can inspect full logs
with journalctl --user-unit driving-log-web.service.
This is an extension point, not a day-1 dependency.
Design now so it plugs in later without changing the core model.
- Wife submits a Microsoft Form.
- Microsoft stores responses in a spreadsheet.
- A scheduled importer reads new rows.
- Imported rows are validated and inserted as
source = microsoft_form. - Suspect rows are visible in the warnings view.
Define an importer abstraction now:
- input: raw row payload
- input identity: source type, stable source instance ID, and stable row key
- output: normalized drive candidate plus warnings/errors
That same interface can be used for:
- seed PDF rows
log.txt- CSV backup import
- future Microsoft rows
The future form should collect:
- start date
- start time
- end date or an
Ends next dayanswer - end time
- supervisor name
- road type
- weather
- notes
Night/day should still be computed by the app.
Use the Microsoft Form response ID as source_row_key; never use spreadsheet
row number because sorting and row insertion can change it. Store a stable form
or workbook ID as source_instance_id.
Each scheduled acquisition may read the whole table. In one transaction:
- a previously unseen source key creates one drive and import row
- an existing key with identical normalized content is a no-op
- an existing key with changed content is reported as a conflict requiring explicit review; it never creates another drive or silently overwrites edits
- the batch records inserted, unchanged, conflicted, and invalid row counts
The whole-snapshot SHA-256 makes retry of an identical acquisition a no-op; the per-row unique source identity makes a later snapshot containing old plus new responses safe. Interrupted batches roll back and can be retried.
Prefer structured spreadsheet export or API access over brittle HTML scraping if Microsoft tooling allows it. The design should keep the importer behind a single adapter so the acquisition method can change without touching validation or storage logic.
Primary audience is the household. The deployment deliberately uses tailnet membership and tailnet policy as its authorization boundary, matching the existing Wordle service. This is an explicit low-threat household trade-off, not a general-purpose internet deployment posture.
Required controls:
- bind only to localhost and expose only through Tailscale Serve
- accept tailnet-only HTTP because Tailscale encrypts node-to-node transport
- do not enable Funnel or bind the application to a LAN or tailnet interface
- do not add an application login, authentication cookie, Origin/Host allowlist, or CSRF layer
- use POST for mutations and never mutate state from GET requests
- use stable request IDs and state/version checks to make duplicate taps, retries, and stale pages safe; these are correctness controls, not authentication
- keep active-drive ownership in SQLite rather than a browser session, address, or identity header
- accept Tailscale identity headers only as optional audit metadata
- require an HMAC-signed, expiring, one-use helper request for archive restore
- return archive and CSV downloads with
Cache-Control: no-storeand attachment disposition - never place operation secrets or supervisor license numbers in URLs or logs
Opening a new browser session displays the dashboard immediately and recovers any active or ending drive from SQLite.
For the Microsoft-form ingestion path, credentials should live outside the repo in environment variables or a local config file excluded from version control.
In addition to CSV drive-log backup and full-state archives, the app should produce a filled, flattened DL-4A PDF:
- chronological table matching the
DL-4Acolumns - total day hours
- total night hours
- grand total
- one row per non-deleted completed drive, with exact
H:MMdurations - supervisor license mapping from
supervisor_profiles, falling back to legacy per-drive license fields - blank supervisor cells when a drive has no supervising driver
- blank customer and certification sections for later handwritten completion
- continuation pages derived from the official packaged template, with totals only on the final page
The review page reports unmapped supervising drivers, blank-supervisor rows, page count, and calendar-week overages. These warnings do not block export or change the authoritative raw totals.
Use deterministic clocks and fixed Apex coordinates. Cover:
- duration validation at zero, negative, 5 hours, and just over 5 hours
- sunrise minus 15 minutes and sunset plus 15 minutes boundary behavior
- drives entirely in day or night and drives split across either boundary
- spring DST gaps, both fall-DST fold occurrences, and UTC round trips in
America/New_York - standard-time and daylight-time dates
- midnight crossing, explicit next-day input, end-before-start behavior, and splitting at local date and calendar-week boundaries
- overlap creation and removal after soft deletion
- calendar-week boundaries, exactly 600 minutes, first minute of overage, multiple over-cap drives, historical import, and deletion-driven recomputation
- CSV format versions, duplicate file hashes, identical UUID retries, conflicting UUIDs, and all-or-nothing row validation
- seed parsing ambiguities, source/computed day-night mismatches, and duplicate candidates
- repeated Microsoft snapshots containing old plus new rows, identical-row no-ops, and changed-row conflicts
Astronomy tests use known expected Apex sunrise/sunset fixtures with a documented tolerance, while business-boundary tests inject exact sunrise/sunset values so library updates cannot obscure a 15-minute-rule regression.
Run against real temporary SQLite files with production pragmas. Cover:
- schema creation and every migration path from each supported schema version
- failed migration rollback and refusal to start on newer or damaged schemas
- WAL recovery after killing a subprocess during manual entry, deletion, live start, active-to-ending transition, ending-state finalization, cancel, CSV import, archive creation, and migration
- live finalization before-commit and after-commit failure points, proving retry creates exactly one completed drive
- ending-state recovery after browser, process, and host restart
- duplicate manual and live requests with same and conflicting idempotency data
- edit retries after response loss and conflicting edits from two browser sessions
- interrupted CSV import rollback and safe retry
quick_checkstartup failure and read-only/unready behavior- archive hash/integrity rejection
- local and external archive retention, interrupted replication, and external hash/integrity rejection
- full restore equality for every authoritative table, including soft-deleted rows, configuration, raw imports, audits, and an active live drive
- quarantine preservation and rollback when post-restore readiness fails
- web restore staging, manifest HMAC and
not_beforeenforcement, helper handoff, result-file recovery, and failed web-service restart
Tests must use subprocess termination and reopen the database rather than merely raising an exception inside one connection.
Playwright WebKit runs at representative iPhone viewport sizes and verifies:
- light theme during the solar day and dark theme during solar night
- day-to-night and night-to-day theme flips at exact supplied boundaries
- native
color-schemeand browsertheme-colorupdate with the page palette - dashboard gauge and night total without horizontal scrolling
- minimum touch-target sizing and readable entry controls
- manual entry validation and warning display
- drive listing, details, editing, concurrent-edit conflict, deletion, CSV download/upload, and archive controls
- tailnet HTTP operation without application login and one-use signed restore requests
- start, elapsed display, durable ending state, resume, finalization, cancel, double-tap, and refresh behavior
- fixed local start-time display and drift-free
H:MM:SSduration updates - no recurring HTTP requests while observing a live timer for at least 65 seconds
- continued timer and theme-boundary projection while network requests fail
- background/suspend return immediately catches up elapsed time and theme before successful server resynchronization
- end-request retry after a response is lost
- active-drive recovery in a fresh browser context with no cookies or local storage
- active-drive recovery after restarting the service between page loads
- ending-state completion recovery in a fresh browser context and after service restart
- offline/Tailscale-unavailable presentation without losing browser form state
- end-time correction after reconnection delay
Before a release is installed as the persistent service, perform an actual iPhone Chrome smoke test through Tailscale: start a drive, close/reap the browser, disconnect Tailscale, reconnect from a changed network, open a new session, verify the active timer, then end and confirm exactly one saved drive.
Required before deployment:
- unit, SQLite integration/recovery, and Playwright suites pass
git diff --checkpasses- a fresh bootstrap works from the committed hash-locked dependencies
- seed import totals and warnings are manually reconciled
doctorreports healthy local service, database, local archive, external archive, and Tailscale layers, or the external-backup limitation is explicitly acknowledged- a full archive is created, verified, restored into a disposable state directory, and compared with the source database
- start/stop/restart and reboot persistence are smoke-tested
These should be handled explicitly during implementation:
- The final
records/log.txtrow has no separately written duration, but its start/end timestamps define 21 minutes. Import that interval normally and preserve the original row as provenance. - The Road Ready PDF includes duplicated-looking timestamps in at least one
place (
08/10/2025 6:27 PMappears twice). Treat as potential duplicates, not automatic deletion. - The Road Ready PDF and
log.txtmay overlap in chronology in future revisions. Provenance plus overlap warnings are required. - The DMV's 10-hours-per-week counting rule should produce a prominent advisory warning without reducing the authoritative logged-hours total.
- database schema
- durability pragmas, migrations, integrity checks, and idempotency
- seed import
- dashboard
- solar-driven light/dark system UI
- manual entry
- drive list, edit, and delete
- CSV export/import
- full-state archive create/verify/restore, external replication, and automated retention
- day/night computation
- warnings engine
- loopback-only serving, retry-safe mutations, and signed restore requests
- unit and SQLite recovery tests
- live-drive start/end/cancel
- durable ending-state recovery and resume
- client-projected live start time, duration, and solar-boundary theme changes
- user-level
systemdservice helpers - Tailscale-forwarded deployment docs
- operational health, doctor, and structured logging
- Playwright mobile-WebKit and actual-iPhone reconnect tests
- print/DMV export view
- Microsoft Form spreadsheet ingestion
- scheduled importer
- Sunrise/sunset location: Apex, North Carolina.
- Timezone:
America/New_York. - Canonical time storage: UTC instants with local timezone, offset, and pinned tzdata context.
- Calendar week: Sunday through Saturday in local time; configurable if DMV provides a different interpretation.
- Supervisor DL number and state: not yet available; keep nullable until supplied and warn before DMV-facing export.
- Dashboard: show all recorded time as progress toward 60 hours. Flag any week over 10 hours and show its overage without reducing the displayed total.
- Theme: light during the Apex solar day and dark during solar night, using the same 15-minute sunrise/sunset offsets as drive classification.
- Live display: show local start time and
H:MM:SSduration, projected client-side between server synchronization events with no per-second requests. - Web access: tailnet-only HTTP through Tailscale Serve, with no application login; tailnet policy is the authorization boundary.
- External archive destination: not yet selected; deployment must configure one or explicitly acknowledge same-disk-only disaster recovery.