10 KiB
Health Routine – Package Scanner
ALWAYS UPDATE THIS FILE AFTER MAKING CHANGES
What this is
A single-page web app (index.html) used internally by Health Routine staff to scan package barcodes.
A user selects their name, scans/types a code, and the result is looked up via a webhook and appended to a table.
Runs in any browser — designed for phones, tablets, and desktop.
Files
index.html — the entire app (HTML + CSS + JS, no build step)
logo.png — Health Routine brand logo (used in the header)
agent.md — this file
start — bash script that launches live-server on localhost:8080
Design
Colors are pulled directly from healthroutine.de's CSS variables:
| Token | Hex | Usage |
|---|---|---|
--primary |
#005f5a |
Controls bar, table header, buttons |
--primary-dark |
#004845 |
Hover states |
--primary-light |
#e0f0ef |
Row hover, badge backgrounds |
--bg |
#f7f7fa |
Page background |
--card |
#eaeaf2 |
Alternating table rows |
--sage |
#82917d |
(brand footer green, available) |
--error |
#c71b1b |
Error states |
--success |
#15803d |
Success toasts |
Font: system font stack (-apple-system, Segoe UI, etc.)
Webhook
Base URL (scan): https://brandize.app.n8n.cloud/webhook/transfer
Base URL (batches): https://brandize.app.n8n.cloud/webhook/batch
GET — load users
Called once on page load to populate the user dropdown.
GET <base_url>
Authorization: Basic <base64>
Actual response shape (confirmed via console):
{ "Name": ["Oliver", "Luka", "Benni"] }
buildDropdown() handles all three shapes n8n may return:
{ Name: ["Oliver", "Luka", "Benni"] }— single object, Name is an array (current)[ { Name: "Oliver" }, ... ]— array of objects{ Name: "Oliver" }— single object, Name is a string
POST — submit scan
Called automatically when the user types/stops typing for 100ms (debounced).
POST <base_url>
Authorization: Basic <base64>
Content-Type: application/json
{ "user": "Oliver", "code": "SCANNED_BARCODE_STRING" }
Success response:
{
"name": "Christa Gierdahl, Christa Gierdahl",
"producttag": "0-HR-Nail-Care-5",
"batch": "BATCH/25-11-14/06173",
"country": "DE"
}
Error response (non-2xx or 200 with error field):
{ "error": "Human-readable error description" }
Fields mapped to table columns:
country→ Country (teal pill badge)name→ Nameproducttag→ Tag (monospace pill, truncated with tooltip on hover)batch→ received but not displayed in the table
Authentication
The webhook requires HTTP Basic Auth.
- On
401(GET or POST) a modal appears asking for username + password. - Credentials are saved to a cookie (
hr_auth, 30-day expiry,SameSite=Strict) so the user is not prompted again. - On page load, the cookie is read and credentials restored before the first network call.
- If the modal is triggered again (e.g. wrong password), the fields are pre-filled from the cookie and focus lands on the password field for quick confirmation.
saveCreds(user, pass)— JSON-stringifies, URI-encodes, base64-encodes, writes cookie.loadCredsFromCookie()— reverses that; returns{ user, pass }ornull.clearCredsCookie()— available but not exposed in UI (call from browser console if needed).
Error handling (POST)
Two cases:
- Non-2xx response — body is parsed; if
errData.errorexists it is shown verbatim; otherwise a genericHTTP <status>message is shown. - 200 response with
errorfield — treated as failure:item.erroris displayed, row is not added to the table.
Errors appear in the persistent status banner (red, no auto-dismiss) and the scan input is selected so the user can re-scan or correct the code.
GET — load batches
Called on page load and when the Batches tab becomes visible.
GET <batch_url>
Authorization: Basic <base64>
Response shape:
[
{
"data": [
{ "id": 18599, "name": "BATCH/26-06-10/18599", "description": "mix dpd", "carrier": "DPD/AT", "state": "done" },
...
]
}
]
The Batches tab shows a searchable, filterable table with German headers (Chargenname, Transport, Status). Default filter resets to state != "done" ("Active") each time the tab opens. Columns are sortable — click Chargenname or Status to toggle ascending/descending. Sorting resets to name-asc when switching tabs.
NDJSON / response format
n8n sometimes returns multiple items as NDJSON (one JSON object per line) instead of a proper JSON array. parseJson(res) handles all three formats:
- Standard JSON array
[...] - Single JSON object
{...}— passed through as-is - NDJSON — split on
\n, each line parsed individually, returned as an array
UI layout
┌─────────────────────────────────────────────────────┐
│ [HR logo] Health Routine | Package Scanner │ ← white brand bar
├─────────────────────────────────────────────────────┤
│ [▥ SCAN HERE_____________] [User ▾] [✕ Clear] │ ← teal controls bar
├─────────────────────────────────────────────────────┤
│ SCAN LOG 3 scans ● │
│ ┌───────────────────────────────────────────────┐ │
│ │ COUNTRY │ NAME │ TAG │ │ ← sticky header
│ ├──────────┼───────────────────────┼─────────────┤ │
│ │ DE │ Christa Gierdahl │ 0-HR-Nail… │ │ ← newest row on top
│ │ AT │ ... │ ... │ │
│ │ scrollable area │ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
On mobile (< 620 px): scan input goes full-width on its own row; user dropdown and clear button share the row below.
Key behaviours
- On load: cookie credentials restored → GET webhooks fire (users + batches) → dropdown populated → scan input auto-focused
- Scan auto-submit: scanner input is debounced at 100ms — typing a barcode and pausing auto-submits without needing Enter
- Scan auto-switches tab: submitting a scan automatically switches to the Scan Log tab if the Batches tab is active
- Clear button: empties the table, clears the scan input, resets the scan counter
- No user selected: red toast + highlighted dropdown; does not submit
- 401 on any request: modal prompts for credentials (pre-filled from cookie), saves to cookie, retries original call
- Webhook error:
errorfield shown verbatim in persistent red banner; input selected for re-scan - Network error: dismissing error banner; input selected so user can retry
- Dropdown error state: tapping/focusing the dropdown while it shows an error re-triggers
loadUsers() - NDJSON/array/object:
parseJson()normalises all response formats from n8n - Duplicate scan:
isDuplicate(code)queries.col-codecells (present on pending, resolved, and error rows) before enqueuing - Scan queue:
scanQueue[]is a FIFO array of{ code, user, rowEl }.drainQueue()runs sequentially in the background — oneprocessItem()at a time. Scans can be added faster than the webhook responds; each gets its own pending row immediately - Pending row: shows a spinner +
Processing… <code>while the item is in-flight; replaced in-place by the resolved row or an error row - Error row: red background, shows the error message and the raw code; code is still in
.col-codeso it cannot be re-scanned - Queue pill: teal badge next to the scan counter showing
N pending; hidden when queue is empty - Auth mid-queue: if
processItemgets 401, it returns"auth"todrainQueue, which pauses (queueBusy = false), shows the auth modal withdrainQueueas the retry callback — queue resumes from the same item after login - Clear: also drains
scanQueueand resetsqueueBusy
Scan flow (sequence)
Enter key
└─ enqueue(code)
├─ validation (user selected? duplicate?)
├─ scanInput.value = "" ← immediate clear
├─ addPendingRow(code) ← spinner row in table
└─ scanQueue.push(...) ← add to FIFO
└─ drainQueue() if idle
└─ processItem() loop
├─ fetch POST webhook
├─ resolveRow() ← replace spinner with result
└─ rowError() ← replace spinner with error
Table columns (DOM)
| Visible | Header | CSS class | Content |
|---|---|---|---|
| ✓ | Country | — | item.country wrapped in teal badge |
| ✓ | Name | name-cell |
item.name |
| ✓ | Tag | — | item.producttag in monospace pill |
| ✗ | Code | col-code |
Raw scanned code (display:none), used for duplicate detection |
How to run
Open index.html directly in a browser (file://) — no server needed for local use.
Or run the start script for live-reload during development:
./start # requires Node.js; installs live-server globally if missing