Bread Run

How the app works — staff & developer overview · March 2026
Live URL
mvebread.dblo.net
GitHub Pages · Push to main → live in seconds · No server, no build step, no install
What it does
Mobile packing checklist for bread deliveries
Staff select their route, see all customer orders grouped and sorted in van-loading order, tick items as they pack. Changes sync between phones in real time via Firebase.
🗺️
System Map
What talks to what and how
📊
Google Sheets
Published as CSV · Read-only from app
Source of truth for all order data. Office staff manage orders here. Published as a public CSV feed. The app reads it but never writes back to it.
PUBFile → Share → Publish to web → CSV
GETFetched on load, tab focus, and Refresh tap
GETCache-busted with ?_=timestamp every time
🔥
Firebase RTDB
mve-bread-default-rtdb.europe-west1
Stores which items are ticked so multiple phones stay in sync. No authentication — open access validated by Firebase rules that check the data shape.
GET/statuses.json — all checked states on load
PUT/statuses/{key} — one item ticked
DEL/statuses/{key} — one item un-ticked
PAT/statuses — batch null on route reset
PUT/lastModified — timestamp on every write
GET/lastModified — polled every 15 s
📱
Driver's Phone
Any browser · mvebread.dblo.net
Just a browser — no app to install. Can be saved to home screen for an app-like experience. All state lives in memory; a page refresh re-fetches everything from scratch.
HOSTStatic files served from GitHub Pages
GETSheet CSV on load + Refresh
GETFirebase statuses alongside sheet load
PUTFirebase on every checkbox change
/statuses/{key}
Read + Write
// key = encodeURIComponent(itemKey)
// itemKey = "orderNum|wareName"
"1000620628|Barnehagebrødet": {
  status: "checked",
  route: "3",
  customer: "KUEHNE + NAGEL AS"
}
// Sorting Stage entries use SUMMARY| prefix:
"SUMMARY|3|Barnehagebrødet": {
  status: "checked",
  route: "3",
  customer: ""
}
Order items and Sorting Stage items share the same bucket, distinguished by the SUMMARY| prefix. Keys are URL-encoded so product names with special characters are safe.
/lastModified
Read + Write
// A single Unix timestamp (ms)
1709650834291

// Written after every PUT, DELETE, PATCH

// App polls this every 15 seconds.
// Compares to lastKnownModified in memory.
// Only fetches full /statuses if changed.
Efficient multi-device sync. Instead of fetching all statuses every 15 s (heavy), the app reads this single tiny number. A full sync only fires when something actually changed on another device.
🃏
Order Card Anatomy
What each part of a card means
Unchecked
Barnehagebrødet 750g QTY: 12
ORDER 1000620628 SUPPLIER bakehuset
Checked — sinks to bottom
Barnehagebrødet 750g QTY: 12
ORDER 1000620628
With "Accepts alternatives" flag
Grovbrød 750g QTY: 4
ORDER 1000620631
⇌ Accepts alternatives
Bread name
Full product name from sheet col 3. The main thing you're packing. Struck through and greyed when checked.
QTY badge
How many units for this order line. Yellow when pending, grey when checked.
Supplier logo
Bakehuset (assets/logo.svg) or Sandnes Bakeri (assets/sandnes-bakeri.png). Shown as an image on the card. Fades to 35% opacity when checked.
Order number + Supplier text
Reference info shown in small text. Not needed for packing — just useful if there's a query about a specific order.
Accepts alternatives
Only appears if sheet col 13 = TRUE. Means you can swap to an equivalent product if the exact one isn't in stock.
Checked state
Green left border + dark green background + strikethrough. Card moves to bottom of customer section. Saved to Firebase immediately on tap.
📦
Sorting & Display Logic
How the app decides what goes where
Customer order — van loading (LIFO)
Customers sorted by routeOrdering (sheet col 12), highest first. Highest = last delivery stop, packed first, deepest in van. Lowest = first stop, packed last, by the doors. Customer positions never move while you're working — only their styling changes when done.
CUSTOMERORDERING
SVAFAS STAVANGER…4100 ← top
OCAB AS3700
E PROPERTY MGMT3000
MOTIVE AS2000
AS BLOMSTERRINGEN200 ← bottom
Within a customer — unchecked float up
Inside each customer section, unchecked orders always sit above checked ones. Ticking an item immediately sends it to the bottom. Remaining work is always visible at the top without scrolling.
ORDERSSTATE
Grovbrød 750gpending
Loffbrød 500gpending
Barnehagebrødet✓ done
Knekkebrød 200g✓ done
Department sub-grouping
If a customer has orders across multiple departments (sheet col 8), their orders are split with a labelled divider. If only one department (or none), no divider is shown. Handles customers like hospitals or schools with multiple delivery points.
AVDELING KJØKKEN
Loffbrød 500gpending
DAGSENTER
Barnehagebrødetpending
Sorting Stage — bread type totals
Shows each unique bread type with total units across all customers on the route. Sorted by quantity (toggle with QTY button). Ticked types sink to the bottom. Used for the collecting phase, before packing individual customer orders.
BREAD TYPETOTAL
Barnehagebrødet 750g47 stk
Grovbrød 750g31 stk
Loffbrød 500g18 stk
Knekkebrød 200g✓ 12 stk
🔄
Sync Behaviour
When data is read from and written to Firebase
Trigger
Firebase call
What happens & why
Page loads
GET sheetGET /statuses
Sheet CSV fetched and parsed, dropdown built. Then Firebase statuses applied on top. Page renders with correct checked states immediately.
Checkbox tapped (check)
PUT /statuses/{key}
Local state flips immediately — no wait. Firebase PUT fires in background. /lastModified timestamp updated, other phones notice within 15 seconds.
Checkbox tapped (uncheck)
DELETE /statuses/{key}
Entry removed from Firebase entirely rather than writing "unchecked". Keeps the database clean and small. Other phones see the removal on next poll.
Last item in a customer ticked
PUTGET /statuses
Sync overlay blocks the UI. App awaits the PUT, then immediately does a full GET to pick up any other driver's changes, before re-rendering the completion state.
Every 15 seconds (background)
GET /lastModified
Reads just the single timestamp number. If unchanged, nothing happens. If changed, triggers a full status fetch. Very cheap — avoids polling all data continuously.
Tab comes back into focus
GET /statuses
Full status sync runs immediately when the driver switches back from another app, catching up any missed changes.
Refresh button tapped
GET sheetGET /statuses
Re-fetches the Google Sheet from scratch in case orders changed. Preserves all checked state in memory. Then fetches Firebase on top.
Reset confirmed
PATCH /statuses
Single PATCH sets all items for the route to null (bulk delete). Clears both order cards and Sorting Stage entries for that route only, in one request.
📁
Files in the Repo
What each file does and when you'd touch it
index.html
Page shell
HTML structure only — no logic, no styles. Header, dropdown, stats bar, Sorting Stage panel, content area, sync overlay, reset dialog. Links to style.css and script.js. Rarely needs changing.
style.css
All visual styling
Dark olive theme, yellow-gold accents, green checks. CSS variables at the very top control the whole palette — change --accent to change yellow, --check for green. All card states, touch targets, responsive breakpoints.
script.js
All app logic
The entire brain. Fetching, parsing, Firebase sync, rendering, event handling, polling. The two constants at the top — SHEET_CSV_URL and FIREBASE_URL — are the main things to update if backends change.
assets/logo.svg
Bakehuset logo
The MVE / Bakehuset brand logo. Shown as a small image on order cards when supplier = "bakehuset". If this file is missing, the card still works — supplier name shows as text.
assets/sandnes-bakeri.png
Sandnes Bakeri logo
Shown on cards when supplier = "sandnes bakeri". Both logos use a white drop-shadow filter and fade to 35% opacity when the card is checked.
config/firebase-rules.json
Firebase security rules
/statuses is public read, per-key write. Writes are validated: must have status, route, customer fields; status must be exactly "checked" or "unchecked". Deploy via Firebase console if rules change.
CNAME
Custom domain
One line: mvebread.dblo.net. Tells GitHub Pages which domain to serve the site on. Do not edit or delete — breaks the URL.
legacy/Code.gs
Archived · Apps Script
The original write-back server, superseded by Firebase. Not connected to anything active. Kept for reference. Bound to a separate Google Sheet via .clasp.json.