Field Notes: Eight Modules in Thirty-One Days
What changed in StationClipboard between August 7 and September 7, 2026: eight new modules, 46 database migrations, a rebuilt Assets workspace, per-user notification preferences, and the long Wednesday that took the whole portal mobile.
Field Notes is where we write down what actually changed in StationClipboard — in the order it happened, without the marketing pass. This one covers August 7 through September 7, 2026. Thirty-one days.
It was a big one.
| 546 | commits |
| 46 | database migrations |
| 1,535 | files touched |
| 318,418 | lines added |
| 8 | new modules |
Here's the whole month, in order.
The short version
- Eight new modules shipped: Incident Command, Workflows, Payroll, Corrections, Elections, Checklists, Meet, and Budget.
- Assets was rebuilt from an inspection list into a real equipment workspace — spreadsheet-grade tables, barcode capture, recurring maintenance, retirement, bulk everything.
- Checklists arrived with SCBA, including per-seat pack tracking and air fills.
- Notifications got preferences and Do Not Disturb, per user, per event.
- Dark mode became real — and stays real, because a script now generates the compatibility layer instead of a human maintaining it.
- The portal went mobile, in one very long Wednesday.
- Elections ship with genuinely secret ballots. Not "we promise not to look." The server does not hold the key.
August 7 — The fireground gets a board
The month opened by finishing Incident Command: not incident reporting (that's NERIS, and it happens after the call), but the board an IC actually stands in front of while the call is running.
What landed:
- The Command tab — ICS positions, hazards, and resource requests, tracked live.
- Units & personnel on scene, with assignments drawn from an org-configurable assignment and benchmark catalog. Every department names things differently; the catalog is yours, not ours.
- A PAR/accountability clock with per-unit rounds, plus automatic PAR triggers — the board prompts for a roll call on the conditions you configure, rather than waiting for someone to remember.
- Tactical checklist templates, so a structure fire board and a water rescue board aren't the same board.
- Staffing configuration validation, so a misconfigured board fails at setup instead of at 3 a.m.
- An incident map with preplans, on Leaflet and OpenStreetMap.
We also wrote down the settings we deliberately refused to add. That document is now part of the module. A board with sixty knobs on it is not a command board.
Same day, smaller: Applications Flow was renamed to Recruiting everywhere (nobody outside the codebase ever called it "applications flow"), avatar upload landed with proper profile-photo handling, the availability page got a responsive day editor with real touch handling, MyPolicies moved onto the shared DataTable with sorting, and iOS got a useKeyboardInset hook so the on-screen keyboard stops covering the field you're typing into.
Time Clock also gained "block clock-in on pending acknowledgements" — if a member has an unread policy waiting on their signature, the department can decide they don't start a shift until it's signed.
August 8–10 — NERIS grows up
Incident Reporting spent three days becoming something you'd trust with a state submission.
- The report lifecycle was fixed to use
reviewStatus, and legacy status inconsistencies were reconciled in a migration. A report's state is now one thing, not two things that disagree. - An Auditor Panel — the reviewer's side of the workflow, with send-back handled as its own explicit act rather than a generic "done."
- A validation engine with normalized NERIS error messages, advisory checks separated from blocking ones, selective re-testing, and a force re-run. A validation failure now tells you what to fix in words, instead of echoing a schema path.
- Incident schema generation scripts and real NERIS payload work — power generation hazards, CSST, medical oxygen.
- NERIS enrollment was fixed end to end, with the flow documented and the confirmation logged.
- An integration audit and readiness check, so a department can see whether it's actually wired up before it needs to be.
A SQL migration runner also landed this week (lib/db/run-sql.mjs), because Drizzle's push wants to drop the sessions table and hand-written migrations were the honest answer.
Alongside that: impersonation for site admins ("view organization as user"), with every action logged and a clean exit path; role-based visibility and edit permissions on personnel custom fields, so a custom field can be officer-only; data-visualization tokens so charts look right in both themes; and the first chart primitives, built in Tickets.
Push notifications got two real fixes: they now survive logout and moved or site-admin accounts, and they show the actual notification text on the lock screen instead of a generic banner.
August 11–12 — Workflows
The department's own automation surface. Not a purchasable module — every organization has it, sitting under Settings → Automations.
Shipped: the builder (with step duplication and sane node placement), run history, an approvals queue, run detail, installable templates, schedule preview, and settings.
Then the parts that make automation survivable:
- Retry logic and real error handling.
- Auto-pause after consecutive failures, with throttled failure email. A broken workflow stops on its own instead of failing two hundred times overnight.
- Trigger health monitoring.
- Informative messaging for skipped runs — "nothing matched" is a result, not an error.
- A resource input type, and visibility logic for inputs.
- Grouped queries for step and approval counts, because the list page was doing N+1 work.
The permission model is the part we're proudest of, and it's deliberately finer-grained than the rest of the portal:
Authoring an automation and letting it loose on the department are genuinely different acts. Someone can be trusted to draft and test a workflow without being the person who decides it starts mailing two hundred members at 6 a.m.
So workflows.publish is separate from workflows.create. Calling an outside service and managing inbound webhooks are separate again — an outbound request leaves the building with department data in it. And there's workflows.manage_execution: one switch that pauses every workflow in the organization at once, for when an automation misbehaves.
The rule underneath all of it: a workflow can never do something its author could not do by hand. Every action declares the permission its author must hold, and it's checked when the workflow is saved. Automation is not a way around the permission system.
August 11–12, 25 — Payroll
A payroll platform built for the way fire and EMS actually pay people.
- Payroll employee records with effective-dated compensation history.
- Pay groups configured around either a standard workweek or an FLSA §7(k) work period of 7–28 days — the exemption most departments live under, and the one general-purpose payroll software gets wrong.
- An automatically generated pay calendar with configurable pay-day rules, plus holiday calendar management and work-period presets.
- A customizable pay-code catalog carrying tax, overtime, and regular-rate treatment, with treatment descriptions and review notes.
- Pay rule management — UI and schema.
- Regular rate and overtime calculations, and a tax calculation engine.
- Payroll runs with creation and exception handling, plus time normalization so imported hours land in a consistent shape.
- Compensation and termination dialogs.
- Pay statements — a
StatementsPanelfor administrators and aMyStatementpage for members. - Later in the month, time off balance management and tracking.
Payroll depends only on Core. A department can run it next to the Time Clock, next to Scheduling, next to an outside time system, or with hours typed in by hand. Its contracts live in their own workspace package (@workspace/payroll-contracts) with tests, because payroll math is the last place you want a surprise.
August 13 — Corrections, and making everything clickable
Corrections is a small module solving a specific, annoying problem: an officer needs to send an official item to a member — a report returned for correction, or a notice about something that happened on shift — and needs to know it was received and answered.
The member answers through a link in their email. No login. Token-based access, scoped to the one item. Departments define the kinds of items, the reason templates, the extra fields, and how hard the system chases a response. The department sees who was told, who answered, and what's still outstanding.
The same day, at 1:25 a.m., a commit landed whose message we won't reproduce in full. The gist was: make everything clickable.
Every stat card in the portal is now a link, backed by an API that filters to exactly what the number counted. No shared component, no generic drill-down — each one custom, in its own module's color. If a card says "7 overdue," clicking it shows you those seven.
Also that day: credits gained multiple category assignments (with a follow-up migration to clear dangling pointers), the issue board got public-listing rules with unit tests for isPubliclyListed, issue events and notes got visibility controls, interview availability landed for applicants, and Sparky — our AI provider layer — shipped with a response schema, a testing framework, dynamic model listing, and Corrections as its first operation surface.
August 15–16 — Onboarding, notifications, and mobile navigation
- Communication template attachments.
- Onboarding resource tasks, settings components, and onboarding email with tests.
- The notification system was refactored for mobile.
- Availability status for members — a lightweight "am I available" signal, separate from the scheduling engine.
- Mobile navigation gained a back control and sub-item expansion, and the unsaved-changes bar learned when to stay out of the way.
August 17 — Elections, reports, and the dark mode reckoning
Elections
Officer elections with genuinely secret ballots.
Define the ranks the department elects and what each requires of its holder, decide who may vote and who may stand, then run it: nominations, an approved ballot, one vote per member per position, and a count supporting plurality, approval, or ranked choice.
The ballot box is the interesting part, and it's worth being precise about:
- At open, the election gets an X25519 keypair. The public half is stored in the clear and is all the voting endpoint ever touches. Every ballot is sealed to it with a fresh ephemeral key, so no two ballots share a symmetric key.
- The private half is encrypted at rest and decrypted once, at tally time, by someone holding
elections:certify. While voting is open, the running server cannot read a single ballot — not because it declines to, but because it does not have the key. - Run the election with a tally passphrase and the private half is encrypted under that instead of under the server secret. Then the box genuinely cannot be opened by anyone but the officer holding it. Lose it and the ballots are unrecoverable. There is no back door, because a back door is a second way in.
Ballots carry no voter, no timestamp, and no identifier of any kind. Everything around the box is recorded instead: a tamper-evident hash chain covering every act, a frozen fingerprint of the sealed box, and a receipt each voter can use to prove they took part.
What this doesn't defend against is someone modifying the running application to record ballots on the way in. Nothing at the storage layer defends against that, and implying otherwise is the dangerous part. What it does defend against is every form of after-the-fact access — a backup, a replica, a restored snapshot, a curious administrator with psql, an operator asked to "just peek at the totals." Which is what actually happens.
Reports and analytics
Chart primitives for analytics visualization landed properly. Tickets got refactored analytics and reports pages plus saved reports. The Report Builder and Report Workspace gained advanced settings, and summaryDefaultFor decides when a report shows a summary by default.
We also removed the cross-module Reports hub. Reports live in the module they're about. A separate destination that lists every report in the department sounds tidy and is, in practice, a place nobody goes.
Dark mode
Most module pages predate our semantic color tokens and style themselves with literal white and slate utilities. Rather than hand-write a dark variant on every one, index.css remaps those utilities to a neutral surface ladder whenever they render inside .theme-scope or a portaled Radix surface.
That block used to be maintained by hand — which meant a new module could ship a color family nobody had listed. Corrections (lime) and Payroll (fuchsia and sky) both rendered as bright light-mode chips on a charcoal page for exactly that reason.
So we wrote gen-dark-compat.mjs. It scans the portal source for every color utility actually in use and emits a rule for each. Coverage now follows the code. It runs with --check in CI and fails if the block is stale. Utilities already written with an explicit dark: variant are left alone — the author opted out.
The rest of the day was spent applying it: the AppShell, the secondary navigation rail and header, the HeaderClock, user avatars, and the Incident Command board (which also got its missing migration).
And also
- iCalendar integration — members can subscribe to their schedule in Apple Calendar, Google Calendar, or Outlook. It moved from a profile tab to a proper Settings section.
- Workflow auto-pause (above).
- Integration detail and settings pages were refactored.
August 18 — Notification preferences, Do Not Disturb, and Assets
Notifications become a choice
Notifications have three channels, and only two of them are choices.
In-app is always delivered — the notification bar is the record of what happened, and a record with gaps in it isn't a record. Email and push are per-user, per-event, stored sparsely in notification_preferences: a missing row means "never expressed an opinion," and the catalog default applies.
notify() resolves recipients, writes in-app rows for all of them, then filters email and push through partitionRecipientsByPreference. Email and push sends now run in parallel.
Preference lookup fails open. If we can't read someone's preferences, we notify them:
Muting someone who wanted a notification is worse than notifying someone who muted it.
A caller's own email: false is not a preference — it's the system saying this particular notification has no email form — and it always wins.
The settings page shows each user only the events they can actually receive, derived from their permissions. Do Not Disturb landed the same day, along with time-sensitive alerts and customizable push sounds (a checklist alert and a shift reminder should not sound identical).
All of it is documented in NOTIFICATION_AUDIT.md, and adding a notification means adding a catalog entry — the settings page, eligibility filtering, and preference storage are all generic over it.
Assets, part one
Inspections were removed from the Assets frontend. They were the wrong shape: an inspection is a checklist, and Checklists was two days away.
What replaced them:
- Recurring asset maintenance — schema, API, a task editor, and a reminder scheduler.
- A "Due Work" route that answers the only question that matters: what needs doing.
- A corporate layout and module kit, then the workspaces list and workspace detail rebuilt on it, with asset profiles and quick-assign.
August 19 — Assets, part two
The asset table stopped being a table and became a workspace.
- Column visibility toggles, fit-to-screen, fullscreen, and a context menu.
- Keyboard navigation for row selection.
- Bulk actions, bulk assignment, bulk row operations, and CSV export.
- Saved views.
- Row accents — visual cues for status, so an out-of-service row looks out of service.
- A status bar with attention counts.
BulkAddDrawerbecameBulkAddSheet, which is a nicer way to enter forty helmets.- Barcode scanning, gated by an
allowScanningflag per table. - Asset retirement — an
allowRetirementproperty, endpoints, dialogs, context-menu actions, and a retired status badge. Retiring an asset is not deleting it. AssetsRecordProfilePageand an improved profile drawer.AssetAttachments— files and previews on an asset.- Asset history (migration
0050_asset_row_history), and an assigned-assets tab on the personnel record, so "what does this firefighter have" is one click from their profile.
We also wrote up the visual direction, built an EquipmentDetailCorporate mockup to argue about before committing, and loaded real PPE gear lists to test against.
August 20–23 — Checklists, and SCBA
Checklists is our answer to PSTRAX-style truck checks, and it's the module with the most opinions in it.
Admins build checklists with categories, questions, readings, and imports from asset workspaces. Schedules can be daily, every-N-days, weekly, or monthly, each with a due-by time, and each optionally runnable as-needed as well. Checks can be per-apparatus or per-member (PPE).
The central design decision:
A check is shared work the whole crew logs into in batches, and only completes once every item is recorded.
Not one person's form. Three people can be logging the same rig at once; the check finishes when the rig is finished. Failed items raise alerts to configured roles.
Built over four days:
- The run page, with offline support, item tracking, photo previews, and orphaned-photo handling.
- Alerts with resolution, extra fields, and denormalized context so an alert still makes sense a month later.
- Eligibility logic, with tests — who is allowed to log what.
- Overdue warnings and as-needed checkoffs in settings.
missedBeforetracking, so the board can say "this was also missed on the 14th" rather than just "overdue."- Asset service status driven by checklist alerts — a failed check can take a truck out of service.
- Issued PPE as a checklist target.
- Inventory events (migration
0057), compartments (0052), shared logging (0053), a board strip and standalone alerts (0054), operational trust (0055), and ownership, review, and versions (0056). - A Help page with real documentation, and the routes monolith split into something maintainable.
- Checklist alerts surfaced on the Asset Profile and Workspace Detail pages, so the alert appears where the equipment is.
SCBA
Then SCBA, which needed its own workflow because a pack is not a line item:
- Per-seat slot tracking (
0060_scba_seat_slots) — which pack is in which seat. - Seat history, component inspections, and air fills as first-class events.
- A task set editor, a gear picker, and seat-scoped logging.
- A development seed script, so we could test against a realistic apparatus.
The color handling across all of it got centralized into a shared surface utility and unified panel components — one theme, not eleven.
August 22 — Real windows
Personnel stopped using modals on desktop and started using real windows — movable, stackable, dragged on the compositor so they don't stutter. Certifications got preview tiles in the hub.
Officer Hub also had its vocabulary corrected: what we'd been calling a "case" is now a record, everywhere. The case detail and cases pages were removed and rebuilt. A LIKE wildcard escaping bug in case search was fixed. Module access control was applied across notifications and routes.
August 24–25 — Assignment, scanning, and imports
- Multi-user assignment for tickets — more than one person can own a request.
- Barcode scanning improvements and better input handling.
- Site admin mass import, for departments arriving with a roster in a spreadsheet.
- Training attendance event data export.
OnboardingRichText, so onboarding instructions can be formatted instead of being one long paragraph.- Time off balances.
- Shift reminders for Scheduling (migration
0062). - A dashboard fix for module labels with long, unbreakable words.
August 26 — The long Wednesday
103 commits in one day. This was the day the portal went mobile, and the day Settings got rebuilt. They happened together because they're the same problem: every page had grown its own layout.
Settings, rebuilt
A shared kit — SettingsList, SettingsListRow, SettingsToggle, SettingsRowLink, SettingsStatStrip, PageLoader — applied across General Settings, Modules, Roles, Logs, Station Clipboard settings, and the organization form. Plus:
- A settings canvas background to give the rebuilt pages visual hierarchy.
- Icon rendering on organization settings tabs.
- Branding — logo upload with adjustment.
- Organization contacts, full CRUD.
- Definitions — the department's own names for things — with
SLOT_NAMESreplacing the old defaults. - Custom fields integrated into General Settings via a variant of the existing panel.
- Two-factor authentication role scoping — require 2FA of the roles that need it, not everyone.
- The font moved to Open Sans.
Mobile, everywhere
Phone layouts, action sheets, responsive components, and back navigation landed across Quartermaster (browse, my inventory, members, requests, event reviews, member gear), Onboarding (overview, people, program library, person detail, reports, settings), Tickets (list and detail, with auto-refresh), Training (events, records, requirements, compliance, my training, event forms), Corrections (dashboard, case detail, my corrections, types, reports, templates), Personnel, and the Profile page.
Navigation links gained a from query parameter so "back" goes where you came from rather than where we guessed.
And in the middle of all that
- Certifications gained a review process, organization-specific certification types, certification hours counted in training totals, date validation to stop impossible entries, and PDF certificate thumbnails.
- Quartermaster got inventory event handling, low-stock alert settings, member request approval and rejection, an
ActionMenu, and an event reviews section gated on Checklists. - Training got bulk event updates and a reworked dropdown menu.
- A chart palette — categorical, ordinal, and status themes.
- Analytics endpoints and a stats-heavy dashboard, with icons across analytics, onboarding, settings, training, and inventory.
- Station Board learned to refuse editing on narrow screens (you cannot lay out a wall display on a phone) and moved board actions into a dropdown.
- Personnel got a management tile on the module launcher.
- One
fix(onboarding): drop JSX generic that Babel cannot parse, which is the kind of commit that ends a 103-commit day.
August 27 — Meet
Department video meetings, held in the portal.
Start a room on the spot or put one on the calendar; repeat it daily, weekly, or monthly. Invite members by name or by role — so "every officer" means whoever holds that role on the day, not a list that went stale in March.
Cameras, microphones, and screen sharing run browser-to-browser. No recording, and no video passes through StationClipboard.
The host gets the controls a meeting actually needs: mute one person or the whole room, turn a camera off, a lobby, a lock, and a live view of who was expected, who came, who came late, and who has the meeting sitting in a background tab.
Shipped over one day: the module and schema, a pre-join page, the room, guest access (with a signalling cursor so a guest joining mid-call catches up), rejoin and attendance tracking, meeting filtering, session locking, sidebar sub-items with visibility controls, settings, and a help page.
Two fixes from that day are worth quoting, because they name the bugs exactly:
fix(meet): video that actually reaches people, rooms that reopen, and a much cheaper pollfix(meet): a camera that is off is off, and a peer that blinks is not gone
Every default is set once for the department and can be overridden on a single meeting.
September 2–4 — Certifications, the kiosk, and the Support Hub
- Certification type categories — a way to group thirty certification types into something a human can scan.
- Certification file rules and download naming, so an exported certificate arrives named after the person and the credential instead of
download(3).pdf. Plus a dashboard widget. - Kiosk pending items improved, with better certification handling.
- Mobile navigation components reworked for larger touch targets and consistency.
Then the Support Hub. IssueReportDialog and SupportDialog were both removed — two dialogs doing one job badly — and replaced with a real destination: a navigation shell, ticket management, an improved issue board with categories and labels, a ticket count hook, proper tab handling, and a customizable ticket intake form (migration 0069), so a department can ask the questions it needs on the request rather than in the first three replies.
Support ticket badges now use the same state-pill vocabulary as the console. Two different words for the same state is a bug.
User role loading and formatting were also reworked for performance and structure.
September 6 — Announcements, training self-logging, tickets, and Budget
The last big day of the window, and it was four things at once.
Announcements
Attachments (0070) and cover images (0071). An announcement about the new SOP should be able to carry the SOP.
Training self-logging
Members can now log their own training, and the department can trust the result:
- A submission queue for self-logged entries, with approvals and comprehensive filtering.
- Crew crediting — a member logging a drill can credit the crew who were there with them, up to a configured attendee limit, with instructor selection on the entry.
- Pending submissions on the dashboard, with user-specific notifications.
- Self-logged entries shown as awaiting approval in training records, so nobody double-counts.
- Reopen and archive for training events.
- Pagination on events and records, and clearer compliance status.
- Training period utilities for requirement management.
Tickets
- Read marks — tracking which messages a participant has actually seen.
- Reply audience logic, so a reply notifies the people it concerns and not the whole thread.
- An "action required" notification and better status handling.
- Ticket status breakdown in analytics, with tests.
- Maximum attachments raised from 10 to 15 across the platform.
Budget
The department's budget as a spreadsheet — not an accounting package.
Set the fiscal year the department actually runs on (October, July, January, whatever the municipality says) and the module works out how far through it you are, how much of each line is gone, and which categories are spending faster than the calendar.
Build your own GL codes — each one is a line of the budget — and edit figures in place the way you would in Excel. The summary answers the five questions a chief opens a budget to ask: what did we budget, what have we spent, what's left, how far through the year are we, and what needs attention.
Receipt tracking is not part of it. Spending is a number somebody types. We'd rather ship the thing a department will actually keep up to date.
Shipped with settings, categories, a lines schema, date handling, unique-line constraints, a year lifecycle (0074), a NativeSelect for the sheet, accessibility work, and inline hints for GL code usage.
September 7 — Today
- Announcement reminders — a nudge for the people who haven't read it yet.
- Dismiss all notifications, because a notification bar you can't clear is a notification bar you stop looking at.
Smaller things, in no particular order
Everything else that landed this month and didn't have a section of its own:
- Station Clipboard settings management, and a member setup checklist with a skip option
- Modal broadcasts, with a block structure and targeting options
- HR Info promoted to a toggleable module, independent of Recruiting
- A maintenance gate for planned downtime
- Unsaved-changes bar on every settings surface
- Compact badge option for the editable avatar
SecondaryNavigationShellreplacing per-module navigation components- Sticky filters in Corrections reports and templates
- Filter and date-range controls on workflows and runs
- Bulk delete and bulk update for tickets (status, priority, category, assignee)
- Unique-violation error handling, with tests
- Credits landing-tab management, to stop a redirect loop
- Onboarding added to Officer Hub sidebar visibility
- Officer Hub participant uniqueness (
0058) - Saved reports (
0042) - Personnel custom field visibility (
0033) - Certification document formats (
0068) - Demo seed data, and an SCBA development seed
- Credits simplified — officer tracking removed, category resolution rewritten
- Session handling hardened
- Time Clock availability (
0040), calendar integration (0044), multi-assignee tickets (0061), time off balances (0063), certification type settings (0064) - The workspace now carries 129 test files, and the number keeps going up
What we didn't do
Being honest about the gaps:
- Budget has no receipt tracking. Deliberate, for now.
- Meet doesn't record. Also deliberate — recording department meetings is a records-retention decision, not a feature decision, and we're not making it for you.
- Elections can't recover a lost tally passphrase. By design. It will not change.
- Some TypeScript project references are still stale, and we've documented which. Running
typecheck:libsbefore leaf checks is the workaround until it isn't. - The dark-compat layer is a compatibility layer. The real fix is semantic tokens on every page, and that's a long tail we're still walking.
What's next
Payroll runs need to meet real time data end to end. Checklists needs the reporting story that Assets already has. And the mobile pass got us to usable on a phone — the next pass is about making the things a firefighter does on a phone (log a check, answer a correction, clock in, read an announcement) feel like they were designed for it, rather than adapted to it.
More next month.
Field Notes is published from the StationClipboard changelog. If your department wants something on this list, or wants something that isn't — tell us.