MilesPost
Installed it from an older address? It won't update from there — see
Moving to the current address.
Team-driver tools. Two questions, two tabs:
ETA — when do I get there? One panel, both answers side by side. Predicted is
dispatch's flat miles ÷ 50; Live is a truck-legal, traffic-aware arrival from real
routing, with your team stops layered on — fuel, the fixed-clock driver swap, and one DOT
break per shift. The board between them is the gap: how far ahead of or behind dispatch you
actually are.
34 RESET — when am I legal? Set the moment you shut down, get the moment your 70 comes
back, and hand a real alarm to your phone's Calendar.
Resolves timezones from a town name. Day and night modes. The app shell runs 100% offline —
no server, no signal required to open it, read the 34 reset, or get a Predicted arrival. The
live half is the one part that genuinely needs signal: it asks HERE for a traffic-aware truck
route, and with no fresh quote that slot alone goes quiet rather than showing an arrival the
road can't back up. Predicted holds its place either way.
Current version: v5.3
Files
| File | What it is |
|---|
index.html | The app — markup, styles, and all the DOM wiring. Loads lib/logic.js. |
lib/logic.js | The pure logic — timezone resolver, ETA solver, 34-reset and ICS math. No DOM, no dependencies. Imported by the app and covered by the test suite. |
sw.js | Service worker. Caches everything so it works with zero bars. |
manifest.webmanifest | Makes it installable to the home screen. |
icon-192.png icon-512.png icon-1024.png apple-touch-icon.png | Home-screen and install icons — the green MILE 34 marker. Full-bleed and opaque, as iOS requires. |
marker.png | The MILE 34 marker with a transparent background, for the app's masthead. 70×132 — three times the 44px it's shown at. |
.nojekyll | Tells GitHub Pages to serve the files as-is. Don't delete it. |
CNAME | Points GitHub Pages at the custom domain, milespost.figari.dev. Don't delete it — the app's address depends on it. |
fonts/ | Vendored Overpass + Overpass Mono (woff2, SIL OFL — licenses included). No fonts CDN: the shell stays fully offline. |
design/highway-reference.html | The driver-approved Highway mockup — design source of truth for v4.0. Not shipped to the app (not in sw.js ASSETS). |
Install it to the home screen
iPhone: open https://milespost.figari.dev/ in Safari → Share → Add to Home Screen.
Android: open it in Chrome → menu → Install app / Add to Home Screen.
Own icon, fullscreen, no browser bars, and it works in a dead zone because the service
worker cached it on first load.
Updates. MilesPost checks for a new version by itself whenever you open it or come back
to it. When one is ready, the version plate at the bottom lights up amber and says
UPDATE AVAILABLE (v…) — TAP TO RELOAD. Tap it when it suits you; it never reloads on
its own, so a load you're halfway through typing is never wiped. Tapping the plate at any
other time checks right then. Close the app instead and, when you reopen it, it's either
already on the new version or the plate is lit and waiting.
Moving to the current address
MilesPost has lived at three addresses, and a copy installed from either of the first two is
stranded:
| Address | Now |
|---|
https://5w311.github.io/milespost/ | 404. The repo was renamed MilesPost, and Pages paths are case-sensitive. |
https://5w311.github.io/MilesPost/ | Redirects to the custom domain below. |
https://milespost.figari.dev/ | Current. |
A stranded copy won't look broken — that's the trap. The service worker serves the whole
app from its cache, so it keeps launching and working normally while it silently never
updates again, frozen at whatever version it last cached.
Nothing in this repo can repoint it: a home-screen app's start URL is captured at install
time, on the device. Remove it from the home screen and add it again from
https://milespost.figari.dev/. Do that once and it updates normally from then on.
Saved settings don't come with it. A phone keeps each site's saved data separately by
domain, and the custom domain is a new one — swap schedule, theme, Running, Override and
tuning all start at their defaults. Note your swap times before removing the old copy.
The custom domain is decoupled from the repo's name, so renaming the repo again can't strand
installed copies the way the first rename did. Nothing in the app hardcodes a URL — every
path is relative — and the HERE key is locked to trusted domains that include
milespost.figari.dev, so live mileage and ETAs work there with no code change.
Notes
- Settings persist per-device via
localStorage.
- Timezone resolution is a lookup table covering all 50 states, with per-city handling for
the split states (TX, TN, KY, FL, IN, ND, SD, NE, KS, ID, OR, MI). Unknown towns fall back
to a manual zone picker.
- Swaps run on a fixed clock, not on mileage — 06:00 / 18:00 Eastern by default, anchored
to a zone so the schedule doesn't drift west as the truck moves. Fuel is on the odometer.
They're separate stops because they're separate things.
- The 34 counts 34 real elapsed hours, so it stays correct across a DST change. Naive
wall-clock math would cost you an hour in the fall and hand you an illegal 33 in the spring.
- No web app can fire a notification while it's closed. That's why the reset hands the alarm
to your Calendar instead of pretending to have a timer.
- HOS is not modeled. No 10-hour reset is built into the ETA — the math assumes the truck
keeps rolling through driver swaps. The 11/14 and the 70-hour cycle are still on you.
Version history
v5.3
- Fixed: Live now rolls out when you told it to. Live used to start the run the moment
you tapped GET ETA, while Predicted used your Rolling Out time. Price a run ahead of time —
Dalton, GA at 1:30 AM — and the two started hours apart: stops landed outside the run
you'd entered (a 19:00 swap on a run that's over by early afternoon), and the board could
say "59m ahead" next to a Live clock half a day later. Now both arrivals depart from
Rolling Out, so the stops sit inside your run and "ahead/behind" is the gap between the
two clocks. Change Rolling Out and Live follows without a new fetch. The traffic in Live
is still right-now conditions, as the board says.
- The stop list reads in the order things happen: the groups are ordered by their first
stop, so a 19:00 swap sits above a 23:00 DOT break, matching the strip above it.
v5.2
- The fuel gauge looks like FuelPost's now. A blue needle on a flat track, a tick at every
eighth and a finer one at every half, the reserve painted on the low end — red over the
last eighth, amber over the backup band — and "How far can you run now?" with the
reading (F — 900 mi) beside it. Tap or drag anywhere on the track, or use the arrow keys;
it snaps to the nearest eighth, ⅛ through F. The low-tank note only shows when there's
something to say — at or under the quarter-tank floor.
v5.1
-
One button: GET ETA. UPDATE ETA is gone. GET ETA used to hide as soon as miles had a
number in it, which is the only reason a second button existed — typed dispatch miles, a
quote gone stale, or wanting a fresh traffic read had nowhere else to go. Now GET ETA
stays up whenever there's a destination, and tapping it always pulls a fresh quote.
-
The gauge reads like FuelPost's: the miles you can plan on — F — 900 mi on a full
tank, down 150 an eighth, 0 at the bottom. It used to lead with the fuel physically
aboard (1200 at F), including the reserve that's never planned.
-
Tank and Loaded mpg are gone from tuning — one "Fuel every … mi max" instead: the most
miles you'll run between fuel stops, 900 by default. That's what a full tank plans; the
quarter-tank reserve and the limp eighth still sit underneath it. PRESET_VERSION goes to
7, so saved tuning resets to the new default; swap schedule and theme stay.
-
The stop list is grouped: one row each for DOT breaks, fuel and swaps — with how many
(×2, ×3) and the time they add up to — and every stop's time and mile listed under it. The
run strip above still draws them in the order they happen.
-
The tank replaces "fuel every N miles." That one number was doing two jobs — how far
the tank goes (a truck fact) and how far you'll run before stopping (a decision) — so
there was nowhere to say you're rolling out on a half tank, and every plan assumed a full
one. Now range comes off the truck (gallons × loaded mpg, 200 × 6 = 1200 mi), and a
gauge says what's aboard right now.
-
Two floors, because "don't plan on it" and "can't use it" are different claims. A
plan never spends the bottom quarter; nothing ever spends the last eighth — that's limp
fuel and it stays the driver's. Adapted from FuelPost's lib/gauge.js, which worked this
out first.
-
On the gauge — eighths, E to F, in the two bands. Tapping it re-solves from the quote
already in hand, so it costs nothing and needs no signal. Deliberately not remembered
between sessions: what's in the tank goes stale the moment the truck moves, and a
remembered level would be confidently wrong more often than right.
-
Roll out low enough and the first stop lands at mile 0 — fuel before you roll.
Knowing that at the yard beats any arrival time on the screen, and the old model could
never say it.
-
The stop schedule. The run now lists its stops — what, when, and at what mile. The
app always computed those positions to draw the run strip and then threw them away; a
driver plans against the stop, not the count. The strip and the list read the same solved
schedule, so a notch and a row can never disagree.
v4.8
- Updates work like FuelPost's now. MilesPost checks for a new version on its own —
every time you open it or switch back to it — instead of only when you tapped the version
number. When one has downloaded, the version plate at the bottom turns amber and names
it: UPDATE AVAILABLE (v4.8.1) — TAP TO RELOAD.
- It never reloads by itself anymore. Before, a new version took over the moment it
arrived and the app reloaded under you — wiping miles, destination and departure if you
were mid-entry. Now nothing happens until you tap the plate.
- Still works with zero bars. The update downloads in the background and waits; the
offline copy is untouched until you tap.
- Fixed: two deploys close together could deliver the old version. The phone's web
cache keeps files for ten minutes, and a new version could pick up the previous
version's page from it — then show the old version number and believe it was current.
Updates now always check each file with the server.
- One last automatic switch. Your phone is running the old updater, which can't show
the new plate — so getting v4.8 onto it happens the old way, once (tap the version
number, or close and reopen the app). Every update after that waits for your tap.
v4.7.2
- Fixed: the device-timezone line showed the wrong arrival. It sits under the big
Predicted number, but once a live quote landed it quietly switched to the Live arrival
converted to your clock. It now always shows Predicted in your device's timezone.
- The Live slot names its day when it lands on a different day than Predicted. The
date line and the day tab belong to Predicted, so a Live "01:10" next to "Sun, Sep 27"
read as the same Sunday. It now reads e.g.
MON · truck route. Same day, nothing added.
v4.7.1
- The buttons drop "LIVE": GET LIVE ETA is now GET ETA, and UPDATE LIVE ETA is now
UPDATE ETA (GETTING ETA… / UPDATING… while they work). The Live slot in the panel
already says where the number comes from.
- Help text caught up with v4.7: no more pointers to a "Live tab", "either tab" or the old
GET MILEAGE button.
v4.7
- The two ETA sub-tabs are gone — both arrivals now share one panel. Predicted and
Live sat behind a tab switch, so the one thing worth knowing — the gap between them —
could only ever be read one half at a time. They're side by side now: Predicted anchors
the panel as the big number, Live rides alongside it in the same amber as the message
board below.
- Predicted anchors it because it never needs the network. It's arithmetic on numbers
already in hand, so with no signal, a denied location, a failed fetch or a stale quote,
the big number keeps its place and its size — only the live slot goes quiet, saying
"needs signal" or "unavailable". The panel holds its shape instead of emptying out,
which is what the old strictly-live tab couldn't do.
- The LIVE board now carries the gap —
LIVE 1h 08m ahead · traffic +56m. "Truck
route" came off it: the slot above already says that. As before, the gap is measured
between the two run times, never the two arrival clocks, so a departure set far in the
future can't leak into the answer.
- "day shift driving @ arrival." On a run with swaps, the old wording read as who's
driving right now, which was never what the line reported.
- The contextual button reads GET LIVE ETA and delivers one; the run panel and the
live CTA are no longer tab-gated, since there are no ETA tabs left to gate them.
v4.6.5
- The help guides open on frosted glass. Tapping any "?" now blurs the page behind
the guide instead of just dimming it, so the white sign stands out and the screen
underneath reads as out of focus. The day theme gets a lighter frost so the light road
stays light. Tapping outside or pressing Escape still closes it, exactly as before.
- With the phone's Reduce Transparency setting on, or on a browser that can't blur,
it falls back to the plain dark dim.
- Removed the "Assumes nonstop team running. Doesn't track HOS — check your clocks."
note from the bottom of the ETA tab.
v4.6.4
- Fixed: the masthead looked blurred on iPhone. As a home-screen app, MilesPost draws
under the status bar, and iOS softens and dims a band reaching about 35pt below the bar
so the clock stays readable. The page started only 16pt below the bar, so the top of the
masthead sat inside that band even scrolled all the way up — "MILESPOST" and the marker
rendered soft while the status bar above them was sharp.
- The page now starts 24pt further down whenever there's a status bar to clear, putting
the masthead below the band. In a regular Safari tab nothing moves. Content scrolling up
under the bar still softens as it passes — that's iOS working as intended.
v4.6.3
- The MILE 34 marker goes green, matching the app's green guide-sign panels. All four
icon files replaced in place — 180, 192, 512 and 1024 — full-bleed and opaque, as iOS
requires.
- The masthead now matches the home-screen icon: the same green marker, replacing the
MILES/POST interstate shield. It has a transparent background, so it sits cleanly on both
the day and night themes rather than in a box. Shown at 44px tall, a little larger than
the old shield, so "MILE 34" stays readable beside the name; stored at three times that
for sharp screens — 12 KB, where the original artwork was 284 KB for every phone to cache.
- To see the new home-screen icon you have to re-add the app — iOS freezes a home-screen
icon at install. If you're moving to
milespost.figari.dev anyway, that re-add covers it.
The masthead updates on its own.
v4.6.2
- New app icon: the MILE 34 marker — a pale-blue mile-marker sign on a navy field. All
three icon files replaced in place, plus a new 1024×1024 for installs that ask for a large
one. The in-app masthead keeps the MILES/POST interstate shield for now.
- To see it on your home screen you have to re-add the app — iOS freezes a home-screen
icon when you install it. If you're moving to
milespost.figari.dev anyway (see Moving to
the current address), that re-add picks up the new icon too.
v4.6.1
- Changing the city now refreshes the mileage for GET MILEAGE users too. v4.6 only
refreshed if you'd used UPDATE LIVE ETA at least once, so anyone working the Predicted tab
with GET MILEAGE got the stale mileage cleared but not replaced. It now refreshes using
whichever fetch you've already used — and a GET MILEAGE refresh stays a mileage fetch, it
doesn't conjure a live quote.
- Still nothing fetched behind your back: having used neither button, a city change fetches
nothing, so it can never be the thing that first asks for your location. There's no
mileage to update in that state anyway.
v4.6
- Fixed: changing the city left the old route's numbers on screen wearing the new city's
name. Quote Nashville, then type Laredo, and the chip and the arrival label said Laredo
while the mileage and the LIVE board were still Nashville's — 400 miles and a Nashville
arrival, relabelled. The same stale-quote inheritance CLEAR was fixed for, reached by
changing the destination directly instead of clearing first.
- Changing either end of the route now refreshes it. Destination or "Rolling out" — both
define the route since v4.5. The old quote is dropped, and re-fetched automatically if
you've used live at least once, on the same terms the Live tab's own auto-refresh already
used. If you haven't, nothing is fetched behind your back; the stale mileage still clears,
which puts GET MILEAGE / GET LIVE ETA back on screen — the same refresh, one tap.
- A mileage you typed still survives it. Only a number the app filled in itself is
cleared. Re-entering the same city is not a change and costs no fetch.
- The origin and 34-reset town fields now read "City, State" instead of "Town, ST".
v4.5
- "Rolling out" can now set where the route starts, not just what clock it's in. Tap
change, set a town, and live mileage is measured from there instead of from your phone's
GPS. Leave it on Your device and nothing changes — that's still the default and still
the GPS fix. The chip tells you which you're on.
- Two things that buys you: pricing a run before you're on it (sitting at home, quote
Carson → Coppell rather than home → Coppell), and a live ETA with location turned off,
which previously wasn't possible at all — no GPS fix, no route.
- A town that can't be placed is now an error rather than a quiet fall back to the GPS fix.
Starting the route somewhere else without saying so would put the mileage out by however
far apart the two are, with nothing on screen to show it.
- Both ends of the route share one bounded geocode cache, so an origin you keep re-quoting
from is looked up once, same as the destination.
v4.4.1
- Fixed: a correctly typed Kentucky or Tennessee town could be rejected as unplaceable.
Those two are the only split states with no default zone — deliberately, since both are cut
in half and guessing would put an appointment an hour out — so any town outside the
built-in list came back the same as gibberish. Type "Independence, KY" (which the
suggestion dropdown itself offers) and the app told you to add the state you had just
typed, then handed you all seven zones.
- It now says what's actually wrong — "Independence, KY — Kentucky runs on two clocks and
this town isn't in the built-in list. Pick the one you're headed for:" — and offers only
the two zones that state spans. Applies to the destination, origin, and 34-reset fields.
- The zone picker leads with a "Pick a zone…" placeholder, so choosing either option
registers. Narrowed to two, the right one was already selected half the time, and
re-selecting it fired nothing.
v4.4
- The Running switch is on both ETA tabs now, moved out of the run panel and into the
load card directly under "Rolling out" — next to the clock it takes over, and the reason
that field and NOW are greyed out. Parked in the run panel it was Live-only, so a driver
on Predicted met a disabled departure with no visible way to free it. It also stays put
while + TUNE TO YOUR TRUCK is open, for the same reason.
- The clock runs on Predicted too. Predicted solves from the departure just as Live
does, so it has exactly as much use for a current one. Still no network calls, and it
still stands down on the 34 RESET tab and while the app is in the background.
v4.3.2
- Fixed: entering a shutdown time whose 34 had already finished silently wiped itself.
The value landed in the field and then blanked a second later with Legal at still
reading
--:-- and nothing said — the auto-clear that retires a finished reset couldn't
tell a stale entry from a running one that had aged out, and ate it on the next tick.
Such an entry is now refused up front with a message naming when that 34 finished, and
what you typed stays in the field so it can be corrected rather than retyped.
- The ordinary case is untouched: a shutdown in the past whose 34 is still running —
"I shut down last night" — commits exactly as before. Only entries already past the
auto-clear window are refused, and a running reset still retires itself on its own.
v4.3.1
- The Predicted tab's comparison board drops "of dispatch" — it now reads
LIVE 08:02 CDT · 0h 12m ahead. The line it sits under is dispatch's own ÷ 50 math, so
naming what the gap was measured against was saying it twice.
v4.3
- On the Live tab, the contextual button reads GET LIVE ETA and delivers one. On
Predicted it's still GET MILEAGE and still fetches distance only. The label change came
with the behaviour: the mileage-only fetch never produces a live arrival, so a button
promising one had to run the full live fetch — which fills the mileage on its way past
anyway. The label follows the tab live, and the busy state tracks whichever fetch is
actually running.
- UPDATE LIVE ETA moved into the load card, directly above CLEAR — with the load it
acts on, instead of buried under the run panel. It now also stays put while
+ TUNE TO YOUR TRUCK is open; it used to hide, because it lived inside the panel
those fields expand into.
- Running is on by default, and remembered — switch it off and it stays off. Safe to
default on because the tick is purely local: it re-solves the quote you already have and
makes no network calls at all. While it's on it owns the departure clock, so "Rolling
out" and NOW start greyed out; switch Running off to set a departure by hand.
v4.2.3
- Fixed: an update could install without the app ever switching to it. The check that
suppresses the pointless reload on a first install was a snapshot taken when the page
loaded, so it also swallowed every real update afterwards on that same page. If the
service worker installed during this load — a first launch, or after iOS had evicted it
— the new version would download and activate while the app carried on showing the old
one, footer stuck on "UPDATING…". It now tracks being controlled as live state: the first
handover is skipped, every update after it reloads.
- Fixed: "CHECKING FOR UPDATES…" could hang forever. The fallback that reports the
result was scheduled after awaiting the check, so a check that never came back meant
the fallback was never created either. The check is now raced against an 8-second
deadline, and a check that fails or times out says "COULDN'T CHECK — NO SIGNAL?"
instead of hanging — or, worse, claiming you're up to date when it never reached the
server. Tapping again mid-check no longer stacks a second one.
- Both are now covered by a real service-worker test — an actual install, an actual deploy,
an actual reload — rather than a stand-in whose update call always succeeded instantly,
which is why neither bug was caught before.
v4.2.2
- The Predicted tab's live comparison is now a changeable-message board, same black
panel and amber dot-matrix as the Live tab's. It's a live-sourced number, so it should
look like one. Dispatch's own
÷ 50 math stays plain note text directly above it, since
that half isn't live-sourced. The wording drops "Live says" — the LIVE badge on the board
already says it.
v4.2.1
- Live readout tidy-up. Every item removed here was already on screen within an inch of
itself. The Live tab's label reads LIVE ARRIVAL (Predicted still reads ARRIVAL); the
CMS board is down to
LIVE truck route · traffic +Nm, having dropped the arrival clock,
its date and zone, the road mileage and the device-clock tail — the big number above, the
miles field below, and the device-timezone line directly above the board all still carry
those. The stat line's average speed is now TOTAL, so rolling and stopped visibly add
up to it.
v4.2
- RUNNING switch on the Live ETA tab. Flip it on and the departure clock tracks now
while the arrival re-solves against it every second, so the tab stays current between
refreshes instead of quietly describing a moment that's passed. Departure and NOW are
disabled while it runs (a clock that rewrites itself can't also be hand-set) and hand
back frozen at their last value when you switch it off. It's a session mode — always off
when you open the app.
- RUNNING never uses data. Not one network call while it ticks. The arrival is
re-solved from the quote you already pulled — same drive time, same traffic, but the
swap/DOT/fuel overlay re-run from the current moment, which is the part that actually
moves as the clock does. UPDATE LIVE ETA is still the only thing that fetches, by
design: an auto-refresh was considered and rejected on API budget. The tick also pauses
on the Predicted tab and while the app is backgrounded.
- It doesn't extend a quote's life, either. Ten minutes after the fetch the quote goes
stale and the arrival clears exactly as it would have — RUNNING keeps the arrival honest,
it doesn't keep it alive.
- Destination geocoding is cached, so re-quoting the same city stops re-looking-up
coordinates that haven't moved. A repeat refresh now costs one routing call instead of a
lookup plus a route — roughly half the transactions. GET MILEAGE benefits too. Changing
the destination always re-geocodes, so a new city can never route against the last one's
position.
- Net effect: the Live tab stays current on its own, and the app makes fewer API calls
than it did before.
v4.1
- The two ETA tabs are now Predicted ETA and Live ETA. Predicted is unchanged —
dispatch's flat
miles ÷ 50, same math as ever, just a name that says what it is.
- Live ETA is strictly live. It no longer computes an offline arrival from miles and a
cruise speed. It shows a number only while there's a fresh HERE quote; with no signal, no
GPS permission, a failed fetch, or a quote older than ten minutes, the arrival clears and
the tab tells you to tap UPDATE LIVE ETA instead of showing a guess dressed as a read.
- The Conservative / Realistic / Push presets are gone. They were a shortcut for setting
six numbers at once, and live routing now supplies the only one of the six it replaced —
cruise speed. The other five are the team-driver stop model the router knows nothing about,
so they stay: fuel interval and stop length, swap length, DOT break length and timing, all
editable individually under + TUNE TO YOUR TRUCK, along with the swap schedule.
- Cruise speed is removed from tuning, because after this nothing reads it — an editable
field that silently affects nothing is worse than no field.
- Saved tuning resets to the new defaults (the old Realistic values, minus mph) —
PRESET_VERSION bumped, since the stored shape changed. Your swap schedule, theme, origin,
and override setting are preserved as always.
- The Predicted tab's cross-model comparison line is now live-sourced: "Live says HH:MM TZ
(Nh Nm ahead of dispatch)", shown only when a fresh quote backs it. Comparing dispatch's
÷50 against a real truck-legal read is the comparison worth having; comparing it against
another offline model wasn't.
v4.0.3
- GET MILEAGE now shows on both ETA tabs, not just Simple — same conditions
(destination set, miles still blank). On Tuned it sits alongside UPDATE LIVE ETA as the
quicker, narrower action: road distance only, no traffic quote.
v4.0.2
- New mark: the interstate MilesPost shield — red crest reading MILES over a blue
field reading POST, replacing v4.0.1's green MP shield everywhere it appears. All
three home-screen/favicon files swapped in place, and the masthead now uses the same
mark (
shield.png, a transparent cutout of the identical shield, cached offline like
everything else) instead of the old inline green SVG — home screen, favicon, and
in-app brand are one identity.
- Already installed? Re-add to the home screen — again. Same iOS limitation as
v4.0.1: the home-screen icon is cached at install time, so anyone who re-added after
v4.0.1 still sees the green MP shield until they remove and re-add MilesPost once
more. In-browser and fresh installs update immediately.
v4.0.1
- New app icon: the MP route shield — the same green shield with white border and MP
monogram as the v4.0 masthead, on the road-dark background with a muted amber lane-dash
line (an echo of the old roadside-marker icon's amber). All three icon files replaced
in place, full-bleed opaque as iOS requires.
- Already installed? Re-add to the home screen to see it. iOS caches the home-screen
icon at install time and nothing can force a refresh — remove MilesPost from the home
screen and add it again from Safari. In-browser tabs and fresh installs pick the new
icon up immediately.
- Shipped as its own patch because v4.0 had already deployed by the time the icon landed:
the icons live in the service worker's cache-first ASSETS, so without this deploy's own
CACHE bump an installed phone would keep serving the old files forever.
v4.0 — "Highway"
- Full visual redesign in interstate guide-sign language, replacing Liquid Glass.
Matches the driver-approved mockup (committed at
design/highway-reference.html).
Panel color carries meaning, like real signage: green guide panels for the two
arrival readouts (with exit tabs showing day + timezone), white regulatory panels
for every input card and the help sign, amber for the primary CTAs and the 34-rule
warning (now a proper warning-diamond sign), and red reserved for BROKE THE 34
alone. The LIVE line is now an amber dot-matrix changeable-message board with its
caveat folded into the board; milepost markers run between cards; the masthead carries
an MP route shield; the version footer is a small tappable sign plate.
- Typefaces: Overpass / Overpass Mono, vendored — not linked. The app is an offline
PWA with zero external requests in its shell, so the woff2 files are committed under
fonts/ (SIL OFL, licenses included), declared with font-display:swap over the old
system stacks, and added to the service worker's ASSETS so they cache like everything
else. Airplane mode still shows signage type, not a fallback.
- Both themes survive. Signs read the same day and night — only the road changes:
night keeps the dark asphalt (white panels dimmed slightly so a 2 AM cab doesn't take
a flashlight to the face), day swaps in a light concrete surface with the panels at
full brightness. Body-text contrast re-verified ≥ 4.5:1 in both.
- Zero logic changes. Every id, function, state variable, and behavior is exactly
v3.1.6 — two-tap guards, override persistence, GET MILEAGE visibility, live freshness
gating, tap-to-update, all of it. The readouts' old amber/green top-accent border is
retired; the same
.done toggle that drove it now turns the whole Legal-at sign's
border and exit tab green. The full test suite passed with zero assertion changes.
v3.1.6
- Both readouts re-render when the app comes back to the foreground, so a LIVE quote
that went stale while backgrounded is gone on the first frame instead of still showing
this morning's arrival until something else happens to trigger a render (iOS suspends
timers in the background, and firing overdue ones on resume isn't guaranteed).
- The "Your Load" help now mentions GET MILEAGE (Simple tab) alongside UPDATE LIVE ETA
(Tuned tab) as ways to fill in mileage from the real route.
v3.1.5
- Tap the version number in the footer to check for an update right now, instead of
waiting on the browser's own update-check schedule (iOS Safari especially can lag well
behind an actual deploy). Taps through "CHECKING FOR UPDATES…" → either "YOU'RE UP TO
DATE" (reverts to the version stamp after a couple seconds) or "UPDATING…" if a new
version is found — which stays up while the page finishes swapping over and reloads on
its own, exactly as it already did before this change. Nothing about how an update
installs changed; this only asks the browser to check sooner.
- The version footer is now a real
<button> instead of a plain <div> (proper
accessibility for something now genuinely interactive), styled identically to how it
always looked. The service worker registration is also kept around and told never to
serve sw.js itself from HTTP cache (updateViaCache: "none") — a client-side
guarantee that doesn't depend on assuming anything about GitHub Pages' own cache
headers, and what makes an on-demand check actually meaningful.
v3.1.4
- "GET MILEAGE" button on the Simple ETA tab. If a destination is set but miles is
still blank, a small button appears next to the field and fetches the real road
distance (the same HERE truck-routing call UPDATE LIVE ETA already uses) to fill it in
— for a driver who typed the destination before knowing dispatch's mileage figure. This
does not produce a live/traffic-aware ETA — Simple ETA stays flat miles÷50 on
purpose, and this button never touches
LIVE.res/at/trafficS/note. It only fills
the field, same as picking dispatch's own number would. Never shows on the Tuned tab —
UPDATE LIVE ETA already covers this there, and more.
- The fetch itself is now
fetchTruckRoute(), a small function factored out of
updateLive() so both buttons share one path instead of two copies of the same
geocode-then-route call. updateLive()'s own behavior is unchanged — confirmed via the
full existing test suite before adding anything new on top.
lib/logic.js gains metersToMiles(), the meters→miles conversion that used to be a
literal 1609.344 inlined once inside solveEtaLive. Now there's one source of truth
for a constant two different features both need.
v3.1.3
- Dropped the timezone off the Arrival header — "Arrival · Gresham, OR" instead of
"Arrival · Gresham, OR · PDT". The big clock right below it already carries its own tz
suffix, so the header no longer repeats it.
- "your clock" is now "current device timezone" everywhere it appeared — the main
readout's secondary time and the LIVE line's own device-clock addendum.
v3.1.2
- Removed the redundant SET button on Destination Town. Resolving already worked two
other ways — pressing Enter (the field's mobile keyboard now labels its return key
"done" via
enterkeyhint) and tapping a suggestion — so the button next to it was
dead weight. The field now fills the whole row.
- Dropped the stray "· " before the LIVE line's time — "LIVE 23:14 ..." instead of
"LIVE · 23:14 ...", now that "LIVE" is its own badge and doesn't need a separator to
read as a distinct label.
- Clear ON/OFF text next to the override switch. The knob is always the same amber
regardless of state — only its position changes — so at a glance there was no way to
tell which side meant what. Now reads "OFF" (muted) or "ON" (green, distinct from the
knob's own always-amber fill so it doesn't look like it's describing the knob's color).
- The LIVE line now shows the driver's own clock too, when it differs from the
destination's. The leading time was already destination-local — same tz the main
arrival number uses — verified by an actual timezone-mismatch test rather than assumed
correct from reading the code. What was missing was LIVE's own equivalent of the main
readout's "· your clock" line; it now appends one (muted, using LIVE's own
liveEta,
not the tuned/quick model's eta — the two can genuinely differ, since LIVE solves
from right now rather than the typed departure time).
- Every clock in the app now carries its own timezone marking, not just implied by a
header label elsewhere on the card. Covers the two big readout numbers (a smaller,
muted suffix so it doesn't compete with the 46px digits), the Simple ETA tab's "Tuned
model says..." comparison, the LIVE line's leading time, and the 34-Reset "Clock
starts in..." pending preview.
v3.1.1
- The OVERRIDE row now hides along with UPDATE LIVE ETA while tuning is open — it used
to stay behind, sitting oddly between the tuning toggle and the now-hidden button.
Hiding it doesn't touch
E.liveOverride or the switch's own on/off state; it reappears
exactly as it was left.
- Moved the OVERRIDE explanation into a "?" help button, next to the label — matching
every other help entry in the app — instead of always-on caption text under the switch.
v3.1
- Small "×" clear buttons on Miles Remaining and Destination Town — visible only once
a field has content, tucked next to the "mi" suffix and the SET button respectively.
Factored into two shared functions (
clearMilesField() / clearDestField()) that the
big CLEAR button now uses too, instead of three near-duplicate copies of the same logic.
- Fix: CLEAR no longer leaves a stale LIVE quote behind.
clearDestField() now nulls
LIVE.res/at/note/trafficS along with the destination — a live quote is tied to
the destination it was solved for, so clearing that destination invalidates the quote.
The bug wasn't that the LIVE line stayed visible right after CLEAR (miles going to 0
already hides it) — it's that the stale route data survived, ready to reattach itself
as a fresh-looking "LIVE" line the moment a new, never-quoted destination was
entered afterwards. Tuning (preset, tuned values, swap schedule) is untouched by CLEAR,
exactly as before.
- OVERRIDE toggle, next to UPDATE LIVE ETA: when on, a live quote always overwrites
the mileage field, even a number you typed yourself. Off by default — with it off,
behavior is byte-for-byte what it's always been (autofill only, never touch a typed
number). Persisted like tuning, so it's remembered across sessions. Same
.switch pill
as the day/night toggle, driven by its own on/off state rather than the page theme.
- Fixed a real bug surfaced while building this: the day-theme CSS rule that positions
the day/night switch's knob was written against
.switch generally, not that one
switch specifically — so under day theme, every .switch pill's knob would've been
forced to the right regardless of its own state. Caught before shipping by inspecting
the override toggle's actual computed style, not just eyeballing a screenshot (a
screenshot misread at a glance in the other direction, too — worth a second look
rather than trusting either the code or the image alone).
- The LIVE line finally reads as something that matters. It used to be identical, tiny
muted text to every other secondary readout line — easy to miss entirely. "LIVE" is now
its own small reefer-filled badge, and the rest of the line (time, miles, traffic delta)
is bumped up from the old baseline (12px/400/muted → 13px/600/ink) without turning into
a second arrival number competing with the real one above it. Its own CSS class, not an
extension of
.sub, so etaYours/etaShift stay exactly as plain as they've always been.
v3.0.4
- Fix: re-quoting a live ETA for a different city left the old city's mileage in the
field. The autofill guard was blank-only, which can't tell a number the driver typed
from one the app filled in itself — so once a mileage was there for any reason, every
later quote refused to touch it. The first city worked; the second silently kept the
first one's distance.
- The app now remembers the exact mileage it last wrote. If the field still reads exactly
that, it's the app's own number and a new quote replaces it. The moment it reads
anything else, the driver has taken the field over and it's never touched again — so
dispatch's contractual number is still safe, which is the whole reason the guard
existed. CLEAR drops the record along with the value.
v3.0.3
- Actually fix the live mileage autofill not appearing until the field is tapped.
v3.0.2's fix was aimed at the wrong thing: it dispatched a synthetic
input event,
but an event is a JS-level notification to listeners and never touches rendering, so
it could not have fixed a paint bug. Confirmed still broken on-device at v3.0.2.
- The screen recording pinned it down: the arrival clock, LIVE line and run strip all
updated in the same pass — and
renderEta() only computes an arrival at all when
Number($("miles").value) > 0. So the value was genuinely in the DOM; only the
<input>'s own rendered text was stale. That rules out a general compositing problem
and points squarely at WebKit's input renderer.
- New
repaintField() forces the element's renderer to be torn down and rebuilt: drop
it out of layout, read a layout property to force a synchronous reflow, restore it.
All in one task, so the browser never paints the intermediate state — no flicker.
It skips focused fields (display:none would blur them and drop the keyboard
mid-type) and restores scroll position (removing the field shortens the document for
the duration of the reflow, which could otherwise clamp a scroll near the bottom).
- The repaint itself can't be observed from Chromium, so it needs on-device
confirmation. What is covered by the smoke suite: the field is left visible
afterwards — if the restore is ever dropped, the field vanishes outright, which would
be far worse than the original bug.
v3.0.2
- Fix: the live mileage autofill didn't visibly appear until you tapped the field.
Mobile Safari doesn't always repaint an input's text after a bare JS
.value
assignment while it's unfocused — the number was correct underneath the whole time,
tapping in just forced a repaint that revealed it. updateLive() now dispatches a
real input event right after setting the value, which forces WebKit to reconcile
the display. Applied the same fix preventively to the city-suggestion tap-to-select
handler (same shape, never reported broken).
- That second fix uncovered a real bug while it was being verified: dispatching the
event re-triggers the field's own debounced-suggestion listener, which was about to
silently reopen the dropdown ~300ms after picking, showing fresh results for the
city name you'd just chosen. The picker now cancels that scheduled fetch and
invalidates its in-flight token in the same handler, so a pick closes the dropdown
and keeps it closed.
- This is a WebKit rendering-timing quirk that Chromium (what the automated suite runs
in) won't reliably reproduce either way — verify the mileage fix on an actual iPhone.
The suggestion regression, being a real DOM/timing bug rather than a paint quirk, has
smoke coverage instead.
v3.0.1
- The tier-1 glass chrome now actually reads as glass. Two changes, and the second one
was the real cause:
- Pushed the material further: fill
.55 → .38, blur 18px → 22px, saturate 160% →
180%, and the pane border up to .14 so its edge stays defined as the fill thins out.
The specular hairline came up a touch to match (.42 night / .24 day).
- Gave the blur something to bend. The canvas was a single flat colour, and
backdrop-filter over a flat surface has nothing to diffract — it just reads as a
tint no matter how transparent it gets. There's now a barely-perceptible radial
gradient behind everything, built from each theme's own --panel / --line, so the
chrome picks up real gradation as you scroll past it.
- The opaque base layer is unchanged, so the app still can't show a host page through:
body::before keeps painting solid --asphalt, and the gradient sits on top of it,
where every transparent stop reveals only that base.
- Tab and preset labels were re-measured against the thinner glass from real rendered
pixels, not arithmetic: 5.35:1 night and 5.80:1 day, both clear of 4.5:1.
- UPDATE LIVE ETA is now a full-width amber pill button instead of small underlined
link text sharing a row with the tuning toggle. It reuses the same
.primary class as
ADD ALARM TO CALENDAR, so the pill shape, sheen and dimmed disabled state all come for
free. "+ TUNE TO YOUR TRUCK" moved to its own line, centered, just above the button —
still a plain link. The button's id, its click handler, and the "UPDATING LIVE…" pending
swap are untouched.
- Opening the tuning panel stands the LIVE button down until you close it again, so the
tuning fields sit directly under the toggle that revealed them instead of being split off
below a full-width button. A live fetch that finishes while the button is hidden still
comes back in the right state.
v3.0
- New visual direction: Liquid Glass, applied in three deliberate tiers. Floating
translucent chrome, pill shapes, and a soft specular hairline on surface edges — but
not everywhere. This app gets read one-handed, at a glance, in a moving truck and
sometimes in direct sun, so the glass is tiered by what a surface is for:
- Chrome (tab rows, sub-tabs, presets, the day/night switch, the help modal's
backdrop) gets the real material: blur, saturation, translucency. It's navigation,
not data.
- Solid pill buttons (SET, CLEAR, BROKE THE 34, UPDATE LIVE ETA, the "?" circles)
get the pill shape and a subtle sheen but stay fully opaque. A translucent SET
button floating over other content is exactly the ambiguity a high-stakes one-handed
tap can't afford.
- Data surfaces (every card, the help sheet, the city dropdown) stay near-opaque
with no blur. Contrast beats material on anything carrying a number.
- Inputs and selects are untouched apart from the new corner radius — no blur, no
translucency, ever, where real numbers get typed.
- One radius scale (
--r-sm / --r-card / --r-sheet / --r-pill) replaces the old
scattered values, and new RGB-triplet tokens let a translucent background be written
once and stay correct in both themes.
- Restrained motion: 200ms tab/preset transitions, a small scale on button press, a
quick fade-and-rise on the help sheet. All of it disabled under
prefers-reduced-motion, and the glass falls back to near-opaque with no blur under
prefers-reduced-transparency.
- Status signals are untouched: the Arrival/Legal-at accent border still switches amber →
green exactly as before, and DOT / Fuel / Swap keep their existing colours (their
swatches just became rounded pill chips instead of squares).
- Contrast was re-checked in both themes after the tint change — body text 15:1 (night) /
18:1 (day), the amber readout 9.7:1 / 5.8:1, the green "legal" state 5.0:1 in day. The
day theme's amber gradient tops out at 5.05:1 on white, still clear of 4.5:1 in glare.
- "Estimated ETA" is now "Simple ETA" — the old name expanded to "Estimated Estimated
Time of Arrival." Label only; nothing under the hood changed.
v2.4.4
- Same-named cities in other states now show up. Suggestions switched from HERE
Autosuggest to HERE Autocomplete restricted to cities (
types=city) — every
result slot is a city, so typing "cypress" now offers Cypress, TX and Cypress, CA
instead of only the nearby one (autosuggest mixed streets and businesses into the
ranking and localized hard). The dropdown now shows up to 8 suggestions (it scrolls).
If the autocomplete call ever fails, the previous autosuggest request silently takes
over, so suggestions degrade rather than vanish.
v2.4.3
- City suggestions now find far more cities — including small towns. The app was
asking HERE for only 5 autosuggest results, and cities compete with streets and
businesses in that ranking, so all but the biggest cities got crowded out (a typical
query returned just 1–2 usable cities). It now requests 20 results and shows the top
5 cities from them. Labels that carry a ZIP code (e.g. "Chattanooga, TN 37402") are
also parsed now, so postal-code localities resolve to their city instead of being
dropped.
v2.4.2
- Fix: city suggestions (v2.4.1) weren't showing any results. HERE's Autosuggest
response, on this account's plan, returns locality (city) results with only a flat
address.label (e.g. "Chattanooga, TN, United States") — not the structured
address.city/address.stateCode fields the original filter required. It now falls
back to parsing the city and state straight out of the label (or title) when those
fields are missing, while still preferring them if a response ever does include them.
v2.4.1
- City suggestions while typing a destination, an origin change, or a 34-reset
shutdown location — a small dropdown of "City, ST" matches from HERE Autosuggest,
debounced (300ms, 3+ characters). Tap one and it fills the field and resolves it
immediately, same as typing the full thing and hitting SET. No signal, no results, or
a failed request and the dropdown just doesn't appear — typing and SET/Enter keep
working exactly as before, fully offline.
v2.4
- "?" help buttons on the four main cards (Arrival, Your Load, How You Run, 34-reset
readout) open a short plain-English explainer with a couple of practical tips — the
Arrival one is mode-aware (Estimated vs Tuned). One shared bottom-sheet panel, no close
button — dismiss by tapping outside it. Purely informational: no logic, state, or math
touched.
v2.3.1
- UPDATE LIVE ETA now fills in miles when the field is blank, using the real road
distance from HERE's route — type a destination, tap the button, done. It never
overwrites a mileage you've already entered (dispatch's number is often contractual and
differs from the real road distance on purpose), and typing mid-fetch still wins.
v2.3
- LIVE ETA line on the Tuned Model tab, alongside Estimated and Tuned. Tap UPDATE LIVE
ETA and the app asks HERE Routing (truck profile — 13'6", 8'6" wide, ~72 ft, 80,000 lb,
5 axles, 1 trailer) for a traffic-aware drive time from your GPS position to the
destination you already typed, then runs the same team overlay (swaps, DOT break,
fuel) on top. Shows the arrival, real road miles, and the current traffic cost.
- Honest caveat, in the UI too: live traffic reflects conditions right now — on a
multi-day team run it mainly shapes the next few hours. The truck-legal routing (real
road speeds, truck-legal path) is what helps the whole run.
- Fails soft, always: no GPS permission, no signal, a HERE error, or a quote older
than 10 minutes and the LIVE line simply isn't shown — the app behaves exactly as
before, with a small "live unavailable — showing tuned" note. Nothing blocks, nothing
errors. Refreshes on demand only (button or reopening the tab), never on a timer.
- The service worker now handles only same-origin requests, so API calls always hit the
live network and can never be served a stale cached quote.
- The HERE key is domain-locked to this app's origin and lives in one named const
(
HERE_API_KEY) for easy rotation. Presets unchanged — saved tuning is preserved.
v2.2.1
- Added a small colour key under the run strip (DOT / Fuel / Swap) so the notch colours are
labelled, and recoloured the DOT notch green so it no longer reads like the brown fuel notch.
v2.2
- DOT break replaces the "stretch" stop in the Tuned Model. Instead of a recurring
every-N-hours stretch, the model now takes the federal 30-minute break once per driving
shift, a set number of hours into that shift. A run gets one DOT break per shift it drives
through and reaches the mark of — a run that ends before the mark takes none, mirroring how
swaps are counted. DOT duration is editable but never drops below 30 min; DOT timing (hours
into shift) is editable per preset (Conservative 4h, Realistic 5h, Push 6h).
- Stop durations retuned: swap is now a flat 30 min on every preset; fuel is
25 / 20 / 15 min (Conservative / Realistic / Push). Cruise speeds and fuel intervals are
unchanged.
- Bumping the preset version means saved custom tuning resets to the new defaults — your
swap schedule and theme are preserved.
v2.1.9
- Centered the amber "It has to be 34 consecutive hours off duty…" warning on the 34 Reset
tab, to match the now-centered footers and calendar note.
v2.1.8
- Follow-up to v2.1.7 centering: the swap-timezone select now also centers on Safari/WebKit
(which ignores
text-align on a <select> — text-align-last fixes it), and the stacked
footer/note lines are centered on the paragraph itself rather than by inheritance.
v2.1.7
- Presentational tidy-up: centered the ETA and 34 Reset footers, the calendar note, and the
swap-timezone select, and split each two-sentence footer/note into two stacked lines.
v2.1.6
- Reworded the ETA tab footer to "Assumes nonstop team running. Doesn't track HOS — check
your clocks."
v2.1.5
- Fix: the 34 Reset date picker no longer locks you out. Tapping the shutdown date box
used to hide the input the instant the picker opened — the native picker's default "now"
value fired the "reset started" rule before you'd chosen anything, so only the NOW button
worked. A reset now starts only on an explicit commit: NOW, or a new SET button
next to the field. Opening or scrolling the picker never starts the reset, so the field
stays up the whole time you're choosing a date.
v2.1.4
- 34 Reset tab tidied while a reset runs. Once a shutdown time is set, the input card
collapses so only the countdown shows; it returns only when you CLEAR TIMER. (BROKE THE 34
restarts to now, so it stays running and the input stays hidden.)
- Auto-clear 10 minutes after completion. A finished 34 shows "Complete … clearing in
M:SS" and clears itself to the empty state at zero. The check keys off the completion
timestamp (
autoClearElapsed in lib/logic.js, with tests), so reopening the app past the
window lands on the clean empty state — never a negative countdown.
v2.1.3
- Simplified the no-destination arrival label to "Arrival · set a destination below",
dropping the confusing "your clock" phrasing and the timezone tag in that state.
v2.1.2
- The ETA tab now opens with miles empty instead of a pre-filled 1200, so first load
shows the clean "Enter miles and a departure time." state — identical to post-CLEAR.
v2.1.1
- CLEAR button on the ETA tab, mirroring the 34 Reset tab's. Clears the load —
miles, destination, and appointment — and resets departure to now; it leaves the truck
setup (presets, tuning, swap schedule, origin timezone, theme) alone. Same two-tap
arm/confirm guard as the reset tab, and inert until there's a load to clear.
v2.1
- "Who's driving on arrival." The Tuned Model view now shows day shift driving or
night shift driving under the arrival time — which team driver is behind the wheel when
you roll in, read off the fixed-clock swap schedule. Display-only, no new inputs. Not shown
on Estimated, which has no swap concept. The
shiftAtArrival logic lives in lib/logic.js
with its own tests, including exact-swap boundaries and custom (non-06/18) schedules.
v2.0.2
- No user-facing changes. Internal only: the DOM-free logic (timezone resolver, ETA solver,
swap schedule, 34-reset and ICS math) moved out of
index.html into lib/logic.js, and a
test suite now covers it. Behavior is unchanged and verified — see Tests.
v2.0.1
- The 34-reset calendar event is now a single point in time (zero-length) instead of a
15-minute block. The two alarms — 30 minutes out and the moment itself — are unchanged;
they were always the part that pings you. The 15-minute span was only there to give the
event a visible length, which isn't needed.
v2.0 — MilesPost
Renamed from Team ETA. The old name only described half of what the app does.
- 34-hour reset timer, merged in as a second tab. Shutdown time (now, or planned ahead),
live countdown, one bar per hour, and a calendar handoff with two alarms — 30 minutes out
and the moment itself.
- Day / night modes. Night is the Cascadia dash. Day is a cool ground with a darkened
amber (
#A8490A) that clears 4.5:1 contrast in glare — raw amber on white is 1.8:1 and
vanishes in exactly the sunlight you'd need it in. Follows your phone, then remembers your
choice.
- Background is painted on a layer the app owns, so a host page can't force it transparent
and show its own dark canvas through.
- Nav restructured: ETA and 34 RESET are two tools; Estimated and Tuned Model are
two views of one calculation, so they nest under ETA.
- New icon and identity.
v1.1.1
- Tabs: Simple ETA (fixed
miles ÷ 50) and Tuned Model ETA. The ÷50 is hardcoded.
- Swaps and fuel stops modeled separately. They were lumped into one mileage-based stop,
which was wrong: fuel is on the odometer, a swap is on the clock. Arrival is now solved
iteratively, because the number of swaps depends on when you arrive and when you arrive
depends on how many swaps you took.
- Short runs that finish inside one shift correctly take zero swaps.
- Run strip colour-codes stops: blue = swap, dark amber = fuel, slate = stretch.
v1.1
Presets retuned to real team numbers:
| Preset | Cruise | Fuel | Swap | Stretch |
|---|
| Conservative | 64 mph | every 500 mi, 15 min | 40 min | every 4 h, 20 min |
| Realistic | 65 mph | every 650 mi, 15 min | 35 min | every 5 h, 20 min |
| Push | 68 mph | every 800 mi, 20 min | 30 min | every 6 h, 15 min |
- Saved settings are versioned (
PRESET_VERSION), so new defaults actually reach phones that
already had the app instead of being overridden by localStorage.
- New versions are checked for on launch and on every return to the app, then offered on the
version footer — never applied without a tap (v4.8; before that the app auto-reloaded).
v1.0
Initial release. Two models, offline timezone resolver, appointment cushion, installable PWA.
Tests
The correctness-critical logic — timezone math, the iterative ETA solver, the swap
schedule, and the DST-proof 34-hour reset — lives in lib/logic.js with no DOM
dependencies, so it runs under Vitest in plain Node.
npm install
npm test # one-shot unit run (Vitest)
npm run test:watch
npm run test:browser # end-to-end smoke test in a real browser (Playwright)
Tests live in test/. The Vitest suite covers the (start, end] swap boundaries and DST
edge cases, the fixed-point ETA (including the zero-swap short run and the fuel
off-by-one), fromWall/toWall round-trips, the full split-state city table, and the
claim that the 34 counts 34 real elapsed hours across both DST transitions. The browser
smoke test (test/smoke.browser.mjs) serves the app over HTTP and confirms index.html
loads lib/logic.js as a module and renders a computed arrival. CI runs both on every push
(.github/workflows/test.yml).
Updating the app
Three things have to happen or the update won't reach phones that already installed it:
- Bump
CACHE in sw.js. It's a build marker, not a version number — change it on
every single deploy, even if the app version stays the same. Skip it and nothing changes
for anyone, and you'll waste an hour wondering why.
- Bump
PRESET_VERSION in index.html — but only if you changed the presets.
Otherwise saved settings will override your new defaults.
- Update the version stamp at the bottom of
index.html.
How a deploy reaches a phone (v4.8): the page runs registration.update() on launch and on
every visibilitychange to visible, and when the footer is tapped. A changed sw.js
installs in the background, re-fetching every file in ASSETS with cache: "no-cache" so
GitHub Pages' max-age=600 can't hand it the previous build's files, then waits. The
page asks the waiting worker its version (a "version" message, answered with CACHE
minus the milespost- prefix — so keep CACHE as milespost-v<version>) and shows
"UPDATE AVAILABLE (v…) — TAP TO RELOAD". The tap posts "skipWaiting"; the
controllerchange that follows reloads that page. A controllerchange the driver didn't
ask for raises the offer instead of reloading. TAP_FLAG in sw.js (an empty cache named
milespost-tap, created when a tap-aware build takes over) tells a new build the phone
already works this way; without it (a pre-v4.8 page can't send the tap) the new build takes
over on its own that one time.