- Kotlin
- Jetpack Compose
- Room
- Firebase Auth
- Cloud Storage
- Firestore
- SHA-256
- bcrypt
- PdfDocument + Canvas
Why it exists
- 7 Years filling in these logs by hand, every watch, at sea
- SHA-256 Fingerprint on every exported page, checked against the cloud record
- 0 Third-party PDF libraries — pages are drawn on a raw Canvas
Rows of readings, signed off by the engineer, kept for inspectors who need to trust that no page was altered after the fact. That is the job. The design decisions here come from having done it: why removing a parameter has to be deactivation and never deletion, why the person who collects a reading should not be the one who posts it, and why the log has to keep working with no connection for weeks.
What it does
-
Two-stage sign-off
A watchkeeper collects readings; an engineer posts them. Posting sends nothing anywhere — it inks the record and locks it. Who collected, who posted, and both timestamps travel with every entry and can be read back on a tap.
-
Write-once journal pages
Each day exports to an A4 landscape page with group bands, column headers that wrap rather than truncate, a ruled grid, and a signatures section where unposted rows print as UNSIGNED. A guard refuses to overwrite an existing file, and the file is marked read-only after writing.
-
Integrity verification
Every upload carries a SHA-256 fingerprint in its cloud metadata. A verify action re-hashes local files against the cloud's records, which turns "this page was never altered" from a promise into something checkable.
-
Group-aware pagination
Wide engine rooms do not fit one page. Whole parameter groups are packed onto pages without splitting a group midway, and a group larger than a page gets pages of its own — the way a paper log flows onto continuation sheets.
-
Sea and port modes
Switch to in-port and the main engine parameters disappear while the generators stay. One computed property filters the whole form.
-
Form management without a developer
Engineers add parameters and groups, rename them, move a parameter between groups, change a unit, or retire one. Removing is always deactivation, never deletion, so past records stay exactly as they were signed.
-
Password reset without losing the history
A forgotten password cannot strand a tablet's records. The chief can reset a crew member's credentials, and the account structure prevents a lockout that would leave a vessel's log unreachable.
Offline-first by construction
A ship spends most of its life without connectivity, so nothing waits on a network. Room is the source of truth and the cloud is a mirror. The sync runs one way, ship to office, and there is no conflict resolution by design: the tablet produces, the cloud archives.
Room (source of truth) → Flow → Compose UI | Room + filesystem → sync → Firebase (mirror)
-
The filesystem is the queue
Local PDF files missing from the cloud are the backlog. Deterministic filenames make that comparison trivial, and the sync is idempotent, so it can run on every app start and every network-available callback without duplicating anything. No button, no scheduler.
-
Entries queue on a nullable column
Readings stream to Firestore as they are saved, tracked by
syncedAt. Null, or older thanpostedAt, means pending. The timestamp comparison prefers syncing twice over missing once: at least once, never at most once. -
No blocking when there is no network
Offline, the app never reaches for Firebase at all — no timeouts, no frozen interface. The PDF list shows the last known upload status from a local cache rather than an empty screen.
-
Lazy catch-up instead of scheduled jobs
When the journal opens, any past day that has entries but no PDF is exported on the spot. Nobody reads a journal at midnight; they read it in the morning, so that is when the work happens.
How it is built
MVVM with unidirectional data flow and manual dependency injection. ViewModels read Room through Flow, and the interface never touches the cloud directly. Role and crew identity are carried through navigation as route arguments, so an oiler never sees the buttons an engineer does — the menu is gated at the route, not hidden with a conditional in the layout.
Room migrations are hand-written with exportSchema on. PDFs use Android's own PdfDocument and Canvas with no third-party library. Passwords are hashed with bcrypt; integrity uses MessageDigest.
A fresh install has no seeded test data. It walks through vessel setup, then a chief engineer account with employee-number login, then a realistic starter template the chief can reshape. The app is born configured rather than hard-coded.
Scope and limits
-
No office-side view yet
The sync pipeline has one end built. A web dashboard showing every vessel's live entries and archived journals is the natural other end, and it does not exist.
-
Accounts are created on the tablet
Crew are created locally rather than provisioned centrally from the office. Central provisioning is the right model for a fleet; local creation would remain as the offline fallback.
-
Hardening not finished
Firebase App Check and per-vessel storage rules — a device may write only to its own folder — are specified but not in place.
-
Tests are thin
The sync queue logic, PDF pagination, and migrations are the three places that most need unit tests and do not have them yet.
What I took from it
The hardest parts were not technical. Deciding that a single-engineer watch may sign its own entries, or that retiring a parameter must never touch a record already signed, took knowing what an inspector looks for. Those rules are not in any specification I could have read.