Rates as of a past date (optional)
Trips
Trucks
Range is tank size × mileage. Edit any row; add a row for each unit. A fleet-wide CSV upload (unit, model year, make, tank, mpg) will replace this table later.
| Unit / name | Tank (gal) | Miles per gal | Full range |
|---|
Planning rules
If no station within that width lets the truck cross a stretch, the search widens across that stretch only (to at most 150 mi) and says so. Road factor is used only for estimates: the demo route when the map service is unavailable, and station detours that could not be measured (shown with ≈). A stop must save at least the amount above, compared with the cheapest plan, to be worth an extra stop. The production build will use a routing API (Google, HERE or Mapbox) and this setting disappears.
Company fueling policies
Stations there are excluded from every plan, even if one would otherwise be cheapest. The plan buys enough fuel at the cheapest pumps before the route enters one of these to cross the whole excluded stretch and still arrive with the reserve — it does not force a full tank at the last pump. If one tank genuinely can't cross it, the plan says so instead of guessing.
Exchange rate & maps
The fixed rate is used when the live rate is switched off, and as a fallback if the Bank of Canada can't be reached. The live rate is the Bank's official daily average, published once per business day (not a second-by-second quote). All optimisation runs in USD per US gallon.
Recent daily rates
Price files waiting for your approval
These arrived by email but look different from the last published file, so they were not published. Check them against what BVD actually sent: Approve publishes it for everyone, Reject throws it away.
Shared database & sign-in
This is the one Supabase project everyone signs into and every price file is shared through — it's baked into this page by default, so nobody else needs to touch this card. Only repoint it here if you're moving the whole company to a different Supabase project; every driver and planner's device picks up whatever is saved here the next time they open the page.
Run the setup SQL (auth-schema.sql) in this project's SQL editor first if it hasn't been run yet. Both values are on the project's API settings page — the anon key is meant to be public-readable, this is normal; the real protection is the row-level security policies that SQL sets up, not keeping this key hidden. There's deliberately no "disconnect" here any more — clearing this would sign everyone on this browser out with nothing to sign back into, since the login screen itself needs this connection to work.
US price file (BVD / PCN, USD per gallon)
Columns read: SITE, NAME, CITY, STATE, RETAIL PRICE (column M) and YOUR PRICE (column N — the price offered to HMF, used for all planning), plus the effective date in the header row. Coordinates come from the site list; a new site that is not in the list shows up as "needs location" until it is geocoded.
Canada price file
Reads the "YOUR PRICE" column (Q in your BVD file) the same way the US file reads column N. Accepts the PCN layout (SITE, NAME, CITY, PROV, YOUR PRICE) or a simple list (name, city, prov, price). LAT/LON columns are used when present; otherwise the town name is geocoded.
| Station | City | CAD / L | ≈ USD / gal |
|---|
Compliance
Checks where drivers actually filled up against what this tool suggested for their trip. Needs the shared database connected (Fleet & fuel data → Shared prices) and two things logged there: a trip record for every trip sheet generated, and imported fuel-card transactions to compare against.
Savings
Reports what the planner actually saved, from real fills rather than from the plan. Needs the shared database connected (Fleet & fuel data → Shared prices), a trip record for every trip sheet, and imported BVD statements.
Audit
A standing list of every fuel-card transaction that didn't cleanly match a planned trip — no trip on file for that card, or filled somewhere other than a suggested stop. Review these against the driver. Needs the shared database connected (Fleet & fuel data → Shared prices).
Import a BVD fuel-card statement
Upload the statement PDF straight from BVD — it's read automatically, no setup needed. A CSV export also works: the first row is read as column headers, and you map each one to the right field once (the mapping is remembered by column name for next time).
Same trip, priced with each day's own price file — see how the cost of this exact route moved day to day.
Add someone
Create their login first in the Supabase dashboard (Authentication → Users → Add user) — they'll show up below automatically the moment that login exists. Then just set their role here.
Everyone with access
| Name | Role | Status |
|---|
How the planner decides where to fuel
- Route. Start → (via stops) → border crossing → delivery → (next load). The downloadable file routes on real highways with OpenStreetMap data (OSRM) and shows the route on a map; the claude.ai-hosted copy cannot reach outside services, so it draws straight legs × a road factor instead.
- Next load. When the next stop after unloading is entered, the whole run is optimised together, so the last fill before delivery is sized with the onward stations in mind — small if cheap fuel is coming, larger if the next leg is expensive or empty.
- Stations on the route. Every station in the price file within the corridor width (default 15 miles either side) is placed at its mile marker along the trip. For each one the map service measures the real road detour (round trip by road, off the highway and back), and the fuel burned on that detour is paid for in the plan.
- Common currency. US prices are your contracted price in USD per gallon. Canadian prices are CAD per litre × 3.785 ÷ today's CAD/USD rate, so both sides compare on the same scale.
- Cheapest plan. The planner tests every sequence of stops that keeps the truck above its reserve. At each stop it buys just enough to reach the next stop if that one is cheaper, and fills the tank if the next one is dearer. Auto picks the fewest stops whose fuel cost is within your minimum saving (Settings, default $10) of the cheapest sequence, so a stop that would save less than that is dropped; a fixed number of stops picks the cheapest sequence with exactly that many.
- Exchange rate. The USD/CAD rate is the Bank of Canada's official daily average, fetched automatically and stored one row per business day in
fx_rates(created byfx-rates.sql). Planning for today uses the newest rate; planning for a past date, the Savings tab and invoice matching use the rate of that day; each trip sheet records the rate it was planned with (planned_trips.fx_rate,fx_date) and prints it in the PDF header. Settings → Exchange rate can switch to a fixed rate instead, which is also the fallback if the Bank of Canada can't be reached. - Savings tab (planners and admins). Reports savings from real invoice fills instead of from the plan. Planner savings = gallons bought × (average price along the trip's route on the day the sheet was made, each station counted by the road it covers − price paid); the average is saved with each trip sheet (two columns on
planned_trips, added by the savings SQL), and the BVD contract discount is left out on purpose. Leakage = gallons × (price paid − price the plan expected at its stop), split into price drift (the planned station's price moved) and driver choice (the driver fueled elsewhere). Negative values are shown, never clamped to zero. - Savings shown on the plan page. Three comparisons for the same gallons: (a) buying everything at the dearest station on the route, (b) the average price along the route, each station counted by the road it covers — the same benchmark the Savings tab uses, so what you see here is what the tab later checks against real fills (it goes negative if the plan costs more than the average; fewer than 5 priced pumps shows no average), and (c) pump retail at the chosen stops, which is what the BVD contract saves.
- “Why is this above the average?” note. Planners and admins only. When the savings tile is red by $1 or more, a table shows how much each stop cost against the average (its rows add up to the tile), why each pump was chosen (for example the only pump within reach), the likely reasons behind the gap, and what can be tried, such as leaving with a full tank. The reasons are rules the planner checks, so they say what is likely, not what is certain.
- Two price files, two fixed columns. US prices always come from column N ("YOUR PRICE") of the PCN file; Canadian prices always come from column Q ("YOUR PRICE") of the BVD Canada file. Everything else in each file (cost, taxes, retail) is ignored for planning, except retail, which is used only for the retail-savings comparison.
- Company policies override the price optimiser, not negotiate with it. Settings → Company fueling policies apply before any cost-minimising happens: leaving Winnipeg always fills the tank completely at a fixed pump chosen by direction, regardless of whether it's the cheapest option that day; any province/state checked under "Never fuel in these provinces/states" (British Columbia, Washington, Oregon and California by default) is removed from consideration entirely, so the truck never needs to (and can't) fuel inside: the plan buys enough at the cheapest pumps before it to cross the whole stretch and still arrive with the reserve (it does not force a full tank at the last pump — it fills there only when that is the cheapest way, or when a next-stop leg after unloading needs the fuel). If a no-fuel zone is simply too long for the truck's range to cross on one tank, the plan says so up front rather than searching endlessly for a station that was never going to exist. The normal cost-minimising logic only ever runs on what's left after these policies are applied.
- Mid-trip re-planning. A trip logged from "Plan a trip" shows up in My Trips with a status (planned / in progress / completed) anyone with visibility into that trip can update. While it's not completed, "Re-plan remaining route" jumps back to the planner with the destination and original via stops pre-filled — just enter the truck's current location, and it builds a fresh plan using today's prices and current policies. It doesn't alter the original trip sheet; the route geometry and suggested stops from the first plan aren't reused, only the destination and via-city list, so this starts the remaining leg over with fresh numbers rather than literally resuming the old plan.
- Proactive stale-pricing flag. Right after login, and every time My Trips is opened, any trip still "In progress" is compared against the newest fuel price file on file for either country. If a newer file has landed since that trip was logged, it's highlighted in the table, named in a banner at the top of My Trips, and counted in a badge on the "My trips" tab itself — visible from any screen, before the tab is even opened — as a nudge to re-plan rather than keep driving on stale numbers.
- How fills are matched to trip sheets. A fuel-card fill is matched on the card number (spaces, dashes and capitals are ignored, so "7083 1234" and "70831234" are the same card) to the newest trip sheet made on that card up to about 8 hours after the fill's time on the statement — the statement's clock has no time zone, so a sheet made the same evening can still be the right one. A fill on a card that has no sheet at all in the period is a truck that isn't using the planner yet, so Audit and the import screen hide those behind a tick-box ("Also show fills on cards that haven't made any trip sheet"); fills on cards that do use the planner are always listed. Sheets made without a fuel card number can't be matched to anything, and Compliance and Audit say how many there are. Reports read every row in the period, not just the first thousand.
- Re-plans stay linked for compliance. A re-plan's new trip sheet is tagged with the trip it replaced, and My Trips shows that link both ways ("Replanned →" on the original, "Re-plan of" on the new one), with a comparison right on the Plan screen showing what changed — stops and total cost — the moment the fresh plan computes. Compliance and Audit check a fuel-card fill against every stop suggested anywhere across that whole re-plan chain, not just the latest version, so a fill made under an earlier plan doesn't get wrongly flagged just because the trip was re-planned afterward.
Assumptions in this demo
- Truck, reserve and minimum fill come from Fleet & fuel data (default fleet truck 180 gal × 5 mpg, reserve 100 miles, minimum 30 gal per stop). The plan always arrives at every pump and the destination with at least the reserve you set. The minimum fill keeps it from stopping for a few gallons, but a smaller purchase is used when it saves more than your minimum-saving amount. When a trip comes back along the road it went out on (a next stop after unloading), a pump on that road can be used on the way back too.
- Map tiles are from Esri's free public basemap, not OpenStreetMap's own tile servers, which block apps like this one that have no web referrer (CARTO's equivalent free tile service now requires a paid-tier API key as of September 2026). A real rollout should still move to a paid tile plan or self-hosted tiles for guaranteed uptime.
- Distances are estimates (straight line × 1.18). Add a via city if the route looks wrong.
- Canadian stations come from the BVD Canada file (95 sites, column Q = "YOUR PRICE"), same as the 606 US sites from the PCN file (column N).
- Settings, trucks, and planning rules are kept in this browser only. Price files can optionally be shared — see below.
- Leaving Winnipeg, the starting point is treated as 136 Wheatfield Rd (Rosser/CentrePort, postal code R4B) rather than downtown Winnipeg — coordinates (49.9584688, -97.2383306) are baked directly into the code, not geocoded live, so this never depends on a working connection to a geocoding service.
- The Winnipeg direction rule is deliberately simplified to two buckets: Canada and west of Winnipeg → Headingley; anything else, including every US-bound trip → Oak Bluff. If a real trip needs a different pump than this resolves to, it can be turned off for that one plan in Settings.
Signing in — driver, planner and admin access
Everything in this app sits behind a real login (Supabase Auth). There's no self-signup — an admin creates each person's login in the Supabase dashboard, and a matching profile with a role appears automatically:
- Driver — only the Plan a trip page. Generating a trip sheet asks for a trip number, then logs the trip (driver, truck, trailer, fuel card, trip number, every suggested stop) for later compliance checks. A driver can only ever plan and see their own trips, always gets the Auto stop plan (there is no stop-count choice for drivers), and never sees prices or costs. No trip sheet PDF prints any price or cost, whoever generates it — planners and admins still see every price on screen.
- Planner — everything a driver has, plus: can plan a trip on behalf of any driver (picked from a dropdown instead of typing a name), can see every driver's trips, and has the Compliance and Price Variation tabs.
- Admin — everything, plus: uploading price files, the Users tab (setting everyone's role, disabling an account), and the Shared prices connection settings (the only place the database URL and key are visible).
- Master admin — the one admin flag that controls the others. Only the master admin can edit an existing admin's account, promote someone to admin, or move the master-admin flag itself; a regular admin can freely manage driver and planner accounts but the app and the database both refuse anything that touches an admin account from anyone else.
This is enforced twice, deliberately: the app hides tabs and controls a role shouldn't need, but the real boundary is Postgres row-level security (RLS) on every table, checked against the signed-in user's own session — hiding a button in the browser is never, by itself, a security control.
One-time setup, in order
- Create a free account and a new project at supabase.com — no credit card needed. Free projects pause automatically after 7 days with no traffic; opening the site again wakes it, or reopen the project in the Supabase dashboard to wake it manually.
- In that project's SQL Editor, run the script below once. It creates every table this app uses and locks every one of them to signed-in users only, with the role rules described above.
-- 1. Shared, dated price snapshots create table public.fuel_price_history ( id bigint generated always as identity primary key, country text not null check (country in ('us','ca')), effective_date date not null, data jsonb not null, uploaded_at timestamptz not null default now() ); create index fuel_price_history_lookup on public.fuel_price_history (country, effective_date desc, uploaded_at desc); alter table public.fuel_price_history enable row level security; -- 2. One row per generated trip sheet create table public.planned_trips ( id bigint generated always as identity primary key, created_at timestamptz not null default now(), trip_number text, driver_user_id uuid references auth.users(id), created_by_user_id uuid references auth.users(id), driver_name text, truck_number text, trailer_number text, fuel_card_number text, origin text, destination text, trip_miles numeric, via_cities jsonb not null default '[]'::jsonb, replanned_from bigint references public.planned_trips(id), suggested_stops jsonb not null, total_cost_usd numeric, corridor_avg_usd_gal numeric, corridor_stations integer, fx_rate numeric, fx_date date, status text not null default 'planned' check (status in ('planned','in_progress','completed')) ); alter table public.planned_trips enable row level security; -- 3. Actual fuel-card fills, imported from a BVD invoice create table public.actual_fills ( id bigint generated always as identity primary key, invoice_number text, card_number text not null, auth_code text unique, driver_name text, unit_number text, fill_time timestamptz not null, site_number text, site_name text, site_city text, site_prov_st text, product text, qty numeric, price numeric, final_amount numeric, currency text, imported_at timestamptz not null default now() ); create index actual_fills_card_time on public.actual_fills (card_number, fill_time); alter table public.actual_fills enable row level security; -- 4. Optional email-drop queue (see "Getting price files in automatically" below) create table public.raw_price_uploads ( id bigint generated always as identity primary key, country text not null check (country in ('us','ca')), filename text, raw_text text not null, uploaded_at timestamptz not null default now(), processed boolean not null default false, processed_at timestamptz ); alter table public.raw_price_uploads enable row level security; -- 5. Who's allowed in, and what role they have. A profile is created -- automatically, as the lowest-privilege role, the instant a login is -- added in Authentication → Users — there's no separate "register" step. create table public.profiles ( id uuid primary key references auth.users(id) on delete cascade, full_name text not null, role text not null check (role in ('driver','planner','admin')), is_master_admin boolean not null default false, active boolean not null default true, created_at timestamptz not null default now() ); alter table public.profiles enable row level security; create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, full_name, role) values (new.id, coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email, '@', 1)), 'driver'); return new; end $$; create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user(); create or replace function public.my_role() returns text language sql stable as $$ select role from public.profiles where id = auth.uid() $$; create or replace function public.i_am_master_admin() returns boolean language sql stable as $$ select coalesce((select is_master_admin from public.profiles where id = auth.uid()), false) $$; create policy "signed-in users can read profiles" on public.profiles for select to authenticated using (true); create policy "only admins update profiles" on public.profiles for update to authenticated using (public.my_role() = 'admin') with check (public.my_role() = 'admin'); -- Only the master admin can touch an existing admin account, promote someone -- to admin, or change the master-admin flag itself. create or replace function public.protect_admin_accounts() returns trigger language plpgsql as $$ begin if public.i_am_master_admin() then return new; end if; if old.role = 'admin' then raise exception 'Only the master admin can modify an existing admin account'; end if; if new.role = 'admin' then raise exception 'Only the master admin can promote someone to admin'; end if; if new.is_master_admin is distinct from old.is_master_admin then raise exception 'Only the master admin can change this flag'; end if; return new; end $$; create trigger protect_admin_accounts before update on public.profiles for each row execute function public.protect_admin_accounts(); -- Prices: everyone signed in reads; only an admin adds a new dated file create policy "signed-in users read prices" on public.fuel_price_history for select to authenticated using (true); create policy "admins upload prices" on public.fuel_price_history for insert to authenticated with check (public.my_role() = 'admin'); -- Trips: a driver sees only their own; planners and admins see everyone's create policy "drivers see own trips, planners/admins see all" on public.planned_trips for select to authenticated using (driver_user_id = auth.uid() or public.my_role() in ('planner','admin')); create policy "drivers log their own, planners log on behalf of anyone" on public.planned_trips for insert to authenticated with check (created_by_user_id = auth.uid() and (driver_user_id = auth.uid() or public.my_role() in ('planner','admin'))); -- Same audience as the select policy above gets to update a trip (in practice, just its -- status — planned/in_progress/completed — for the mid-trip re-plan workflow). create policy "drivers update own trip status, planners update any" on public.planned_trips for update to authenticated using (driver_user_id = auth.uid() or public.my_role() in ('planner','admin')) with check (driver_user_id = auth.uid() or public.my_role() in ('planner','admin')); -- Actual fills: planners and admins read; only an admin imports a BVD invoice create policy "planners and admins read fills" on public.actual_fills for select to authenticated using (public.my_role() in ('planner','admin')); create policy "admins import fills" on public.actual_fills for insert to authenticated with check (public.my_role() = 'admin'); -- Email-drop queue: nothing can insert into it directly any more. Files get in -- only through intake_price_file(), which needs a secret token (see the price -- intake hardening section in auth-schema.sql). Signed-in users read the queue; -- only admins mark rows processed or rejected directly. create policy "signed-in users process the queue" on public.raw_price_uploads for select to authenticated using (true); create policy "admins mark it processed" on public.raw_price_uploads for update to authenticated using (public.my_role() = 'admin') with check (public.my_role() = 'admin'); -- Anonymous access is retired everywhere. revoke all on public.fuel_price_history, public.planned_trips, public.actual_fills, public.profiles from anon; grant select, insert on public.fuel_price_history to authenticated; grant select, insert, update on public.planned_trips to authenticated; grant select, insert on public.actual_fills to authenticated; grant select, update on public.profiles to authenticated; revoke all on public.raw_price_uploads from anon; grant select, update on public.raw_price_uploads to authenticated; -- 7. Fleet-wide settings (trucks, planning rules) shared by every user create table if not exists public.app_settings ( key text primary key, value jsonb not null, updated_at timestamptz not null default now(), updated_by uuid references auth.users(id) ); alter table public.app_settings enable row level security; create policy "signed-in users read settings" on public.app_settings for select to authenticated using (true); create policy "admins add settings" on public.app_settings for insert to authenticated with check (public.my_role() = 'admin'); create policy "admins change settings" on public.app_settings for update to authenticated using (public.my_role() = 'admin') with check (public.my_role() = 'admin'); revoke all on public.app_settings from anon; grant select, insert, update on public.app_settings to authenticated; - In Project Settings → API, copy the Project URL and the anon public key — never the service_role key, which must never go in this app or any browser — and paste both into Fleet & fuel data → Shared prices (admin only). For a single-company deployment like this one, an admin can instead bake those same two values into the page's own code as defaults, so every driver and planner's device is connected the moment they open the link, with nothing to configure on their end.
- Create your own login first, in Authentication → Users → Add user. Find its id on that same page and run, once:
update public.profiles set role = 'admin', is_master_admin = true where id = '<your-uuid-here>';
That row is the only placeis_master_adminis ever set — never through the app itself. - Everyone else: create their login the same way in the dashboard (they'll appear in the app's Users tab automatically, defaulted to driver), then set their real role and full name there. Nobody needs a password reset flow to get started — just tell them the email and temporary password you set for them.
The anon key is meant to be public — every Supabase app ships it inside the page the browser downloads, this one included. It cannot read or write anything on its own; every table above refuses it outright — the one thing it can call without signing in is the token-guarded price-intake function, which does nothing without the secret token — so the actual protection is the row-level security policies, not keeping that key hidden.
Trip records & compliance matching
Every Trip Sheet PDF generated anywhere logs the trip number, the driver, truck, trailer and fuel card, and every suggested stop, into planned_trips — the record of what each driver was told to do. This never blocks the PDF. If the database refuses the write, the trip is kept on that device, a red banner shows the database's own reason, and it is retried automatically (every 45 seconds, when the connection returns, and on the Retry button) until it is saved. The Compliance tab (planner and admin only) then matches those records against actual_fills, imported from a BVD invoice by the same fuel card number the trip sheet used. The auth_code unique constraint on actual_fills makes re-importing the same invoice safe — a transaction already on file is skipped rather than duplicated. Only "TA" (tractor) fills are matched against suggested stops; "TF" (trailer/reefer) and "DF" (DEF) are logged but not compared, since the route planner only optimises the tractor's diesel.
Getting price files in automatically (any email provider)
An uploaded price file only updates the browser that uploaded it, unless it goes through the shared database — the Shared prices card covers uploading by hand, and that stays available. To have BVD's emails update the site with nobody touching a keyboard, let something in your email system relay each CSV attachment into the raw_price_uploads table created above. The website does the parsing — the exact same code as a manual upload — the next time any signed-in person (planner, driver or admin) opens it, then shows a notice that the new prices loaded. This needs the publish_queued_price_upload function from auth-schema.sql; without it, only an admin opening the site will drain the queue.
Microsoft 365 / Outlook, using Power Automate (make.powerautomate.com). BVD sends four emails a day — a PDF and a CSV for each of the US and Canada. Only the CSVs are price data; PDFs that reach the queue are quietly discarded, but it's cleaner not to send them:
- Create an Automated cloud flow, trigger When a new email arrives (V3) (Office 365 Outlook). Set From to BVD's sending address and Has Attachment to Yes. Turn on Include Attachments.
- Add Apply to each over the trigger's Attachments, with a Condition inside it: attachment Name ends with
.csv. - Inside the Yes branch, add a second Condition that decides the country from something that always differs between the two emails — the subject or the file name (e.g. contains "Canada"). Yes → country
"ca", No →"us". - In each branch add an HTTP action (this is a Premium connector — if it shows a diamond icon or asks you to upgrade, see below):
- Method:
POST; URI:<your Project URL>/rest/v1/raw_price_uploads - Headers:
apikeyandAuthorization: Bearer <anon key>(same two values as the Shared prices card), plusContent-Type: application/json - Body:
{"country": "us", "filename": "@{items('Apply_to_each')?['Name']}", "raw_text": "@{base64ToString(items('Apply_to_each')?['ContentBytes'])}"}— use"ca"in the Canada branch.
- Method:
- Save, turn the flow on, and check Run history after the next BVD email. Manual upload in Settings keeps working the same as before.
No Premium licence — what is actually running here. The HTTP action is the only Premium piece, so this site uses a flow with only standard actions: it forwards each BVD email to a private address on a subdomain (in.) handled by a Cloudflare Email Worker. The Worker (1) drops mail that isn't from an allowed sender (ALLOWED_SENDERS, default the company domain), (2) keeps only the CSV attachments, deciding the country from -cad- / -usd- in the file name, and (3) calls intake_price_file() in Supabase with a secret INTAKE_TOKEN. Same queue, same parsing as before.
Held files. Before a queued file is published, the database compares it with the last published one for that country. If any price is outside a plausible range, the average moved more than 15%, or there are under 60% as many stations, the file is held: nothing is published, and an admin sees Approve / Reject buttons at the top of Fleet & fuel data. Manual uploads in Settings are not checked, since an admin is doing them on purpose.
Further hardening worth considering
- Session length. Supabase's default access-token lifetime (1 hour, silently refreshed) applies here; a shorter forced re-login can be set in Authentication → Settings if idle sessions on shared devices are a concern.
- Login protections. Confirm rate-limiting and leaked-password protection are turned on in Authentication → Settings — both are built into Supabase Auth and off by default on some older projects.
- Audit trail. Role changes currently aren't logged anywhere beyond Postgres's own row history; a simple trigger that writes old/new role to a small
role_changestable is a cheap addition if that history ever matters. - Fuel card numbers. They're currently stored and shown in full wherever a trip or fill appears; masking all but the last 4 digits in the UI (while keeping the full number for matching) is a reasonable step if this data is considered sensitive enough to warrant it.
Modules (for the build-out)
The script is split into independent parts — Store (settings), Data (price files), Sync (the shared database), Auth (sign-in, session, roles), Places (geocoding), Routing (distance & corridor), Optimizer (fuel-stop maths), Planner (screen), TripSheet (PDF), Users (role management). Each can be swapped or moved to a server without touching the others.