Public Access
Claude Context
This commit is contained in:
1 parent
d998b6731e
commit
5cf885d683
3 files changed
+286
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
# visitor-signin — overview
|
||||||
|
|
||||||
|
Self-hosted visitor sign-in/out kiosk for school campuses. Visitors sign in at an iPad kiosk
|
||||||
|
(photo, WWCC/VIT, host), recurring visitors use mobile number + 4-digit PIN, and a label badge
|
||||||
|
prints automatically from a networked Brother QL-820NWB. Staff manage everything from an admin
|
||||||
|
console. A Google Sheet mirrors who is currently on site for evacuation roll calls.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Node.js 22, Express, SQLite (`better-sqlite3`), vanilla JS front end — no build step.
|
||||||
|
- Single Docker container on an Ubuntu docker host; authoring happens on Windows.
|
||||||
|
- Server-side badge rendering with `@napi-rs/canvas`; printing via `brother_ql` (Python) over
|
||||||
|
TCP 9100.
|
||||||
|
- Self-signed TLS: local CA + auto-renewing server certificate, distributed to iPads via Jamf Pro.
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
- `/` — kiosk (guest sign in, PIN sign in, sign out). No admin link on purpose.
|
||||||
|
- `/admin/login` — standalone login (email + password + TOTP).
|
||||||
|
- `/admin` — console: on site, visit log, recurring visitors, people to visit, sites, admins, system.
|
||||||
|
- HTTP helper port — serves the CA (`/ca.crt`, `/ca.cer`, `/ca.zip`) and redirects to HTTPS.
|
||||||
|
|
||||||
|
## Running it
|
||||||
|
- Deploy branch: **`deploy`**. Host update:
|
||||||
|
`git fetch origin && git reset --hard origin/deploy && docker compose up -d --build`
|
||||||
|
- After changing `.env`: `docker compose up -d` (never `restart`).
|
||||||
|
- Check what's deployed: `docker compose exec visitor-signin npm run version`.
|
||||||
|
- Printing diagnostics: `docker compose exec visitor-signin node scripts/print-test.mjs`.
|
||||||
|
|
||||||
|
## Where to look
|
||||||
|
- `01-architecture.md` — current design and the reasoning behind it.
|
||||||
|
- `sessions/` — dated session records.
|
||||||
|
- `README.md` in the repo root — operator documentation.
|
||||||
|
|
||||||
|
## Session Log
|
||||||
|
- 2026-09-23 — Visitor sign-in kiosk: build, HTTPS, and server-side label printing — Built the app end to end; added multi-site, 2FA admin accounts, self-signed CA for Jamf-managed iPads, server-side QL-820NWB printing (found `--red` is mandatory on DK-22251), single-tab evacuation sheet, branding, and photo halftoning.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# visitor-signin — architecture
|
||||||
|
|
||||||
|
Current state of the design. Established 2026-09-23 (first recorded session). When a later
|
||||||
|
session changes something here, edit the relevant section and note the date and reason.
|
||||||
|
|
||||||
|
## Process layout
|
||||||
|
One Node process in one container:
|
||||||
|
- HTTPS listener on container port 3000 (kiosk, admin, APIs).
|
||||||
|
- Plain-HTTP helper on 3001: serves the CA certificate and redirects everything else to HTTPS.
|
||||||
|
- Background jobs in-process: photo retention purge, optional nightly auto sign-out, daily
|
||||||
|
certificate renewal check, Google Sheet sync retries.
|
||||||
|
|
||||||
|
## Data
|
||||||
|
- SQLite at `/data/visitors.db` (bind-mounted `./data`), WAL mode.
|
||||||
|
- Photos on disk under `/data/photos/YYYY/MM/`; banners under `/data/branding/`; certs under
|
||||||
|
`/data/certs/`.
|
||||||
|
- Migrations are additive and idempotent (`addColumn` in `src/db.js`) — never destructive.
|
||||||
|
- `visits` denormalises name, contact, host and site name, so history survives deletion of the
|
||||||
|
related records.
|
||||||
|
|
||||||
|
## Identity and access
|
||||||
|
- Admin accounts: email + scrypt password + TOTP (RFC 6238), eight one-shot recovery codes.
|
||||||
|
Roles `owner` (manages admins and sites) and `admin` (optionally scoped to one site).
|
||||||
|
Optional email-domain restriction. First account created once from `ADMIN_BOOTSTRAP_*`.
|
||||||
|
- Recurring visitors: mobile number is the username; PIN is 4 digits, stored AES-256-GCM
|
||||||
|
encrypted with a keyed HMAC `pin_lookup` for uniqueness. Phone, email and PIN are each unique.
|
||||||
|
- `APP_SECRET` keys sessions, PIN encryption and `pin_lookup`. Changing it breaks every PIN.
|
||||||
|
|
||||||
|
## Multi-site
|
||||||
|
- `sites` owns: people to visit (`hosts`), badge settings, printer settings, branding.
|
||||||
|
- A kiosk is bound to a site by `/?site=<slug>` (remembered in the tablet's local storage) or a
|
||||||
|
first-run picker; a single-site install never asks.
|
||||||
|
- Sign in, sign out and duplicate detection are scoped per site.
|
||||||
|
|
||||||
|
## TLS
|
||||||
|
- Local CA (10 years) + server certificate (398 days), generated by the container with openssl.
|
||||||
|
- Server cert renews inside 30 days and is hot-swapped with `setSecureContext`; the CA is never
|
||||||
|
replaced except by an explicit `--force`, so devices trust it once.
|
||||||
|
- Reissue is triggered by a change in `HTTPS_HOSTNAMES` (tracked in `certs/.hostnames.json`),
|
||||||
|
not by the full SAN list, because the container's own bridge IP changes between restarts.
|
||||||
|
- CA distributed as PEM `.crt`, DER `.cer` (required by Jamf Pro) and a zip bundle (browsers
|
||||||
|
block bare certificate downloads).
|
||||||
|
|
||||||
|
## Printing
|
||||||
|
- The server renders the badge (`src/printer.js`, `@napi-rs/canvas`) at 300 dpi, 696 dots across
|
||||||
|
(QL-820NWB head width on a 62 mm roll), and sends it with `brother_ql` over TCP 9100.
|
||||||
|
- Photos are halftoned in the renderer (Floyd–Steinberg by default) so preview equals print.
|
||||||
|
- The loaded roll is an explicit per-site setting (`printer_label`: `62` or `62red`). `62red`
|
||||||
|
always adds `--red`; without it the job is monochrome and the printer refuses it.
|
||||||
|
- Sign in waits up to `PRINT_SIGNIN_WAIT_MS` for the print; on failure the kiosk falls back to a
|
||||||
|
browser print of the HTML badge (`src/sites.js`).
|
||||||
|
- `brother_ql`'s network backend never reads the socket and the QL-820NWB does not answer status
|
||||||
|
requests on 9100, so the server cannot confirm a label came out. Brother Status Monitor on a
|
||||||
|
connected PC is the source of truth for refusals.
|
||||||
|
|
||||||
|
## Google Sheet
|
||||||
|
- Evacuation mirror only: one tab ("On site now"), rewritten in full on every sign in/out and
|
||||||
|
every 15 minutes. No history is sent to Google; history lives in the app (Visit log, CSV).
|
||||||
|
- A failed write marks the tab stale and is retried; it never blocks the kiosk.
|
||||||
|
|
||||||
|
## Front end
|
||||||
|
- Three separate documents: kiosk (`index.html`), login (`login.html`), console (`admin.html`).
|
||||||
|
- Kiosk theming is per site: four chosen colours, the rest derived with WCAG contrast rules.
|
||||||
|
- HTML/JS/CSS served with `Cache-Control: no-cache` so an upgrade is never masked by a cached
|
||||||
|
console.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- Author on Windows, deploy on an Ubuntu docker host from the `deploy` branch with
|
||||||
|
`git reset --hard origin/deploy` (the host is a mirror, never merges).
|
||||||
|
- `docker-entrypoint.sh` fixes ownership of the bind-mounted `/data`, then drops to `node`.
|
||||||
|
- `pull_policy: build` — the image is always built locally.
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# Visitor sign-in kiosk: build, HTTPS, and server-side label printing
|
||||||
|
|
||||||
|
**Date:** 2026-09-23
|
||||||
|
**Project:** visitor-signin
|
||||||
|
**Status:** ongoing
|
||||||
|
**Tags:** node, express, sqlite, docker, kiosk, tls, jamf, brother-ql, ql-820nwb, label-printing, google-sheets, multi-site, 2fa, dithering
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Built an internally hosted visitor sign-in/out kiosk for school campuses from scratch: guest and
|
||||||
|
recurring-visitor sign in with photo capture and WWCC/VIT recording, an admin console, and a
|
||||||
|
Google Sheet mirror for evacuation use. The session then iterated through multi-site support,
|
||||||
|
per-admin accounts with 2FA, self-signed HTTPS for iPad kiosks deployed via Jamf Pro, and
|
||||||
|
moving badge printing off the iPad onto the server, driving a Brother QL-820NWB directly. It
|
||||||
|
closed on getting two-colour (DK-22251) printing accepted by the printer and on photo halftoning.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- **Node/Express + SQLite + vanilla JS in one container, no front-end build step.** Simple to
|
||||||
|
run on a single docker host; no database server to manage. Rejected a React/SPA build as
|
||||||
|
unnecessary weight for a kiosk.
|
||||||
|
- **Recurring-visitor PINs are encrypted (AES-256-GCM keyed from `APP_SECRET`), not hashed.**
|
||||||
|
A 4-digit PIN has 10,000 values, so hashing gives no protection against someone holding the
|
||||||
|
DB file; encryption lets admins reprint a lost card. Real protection is the lockout (5 wrong
|
||||||
|
PINs = 15 min). Consequence: changing `APP_SECRET` makes every PIN unreadable.
|
||||||
|
- **Phone, email and PIN must each be unique.** PINs use a keyed HMAC column (`pin_lookup`)
|
||||||
|
because the ciphertext has a random IV and cannot be compared. Enforced in app code *and* by
|
||||||
|
partial unique indexes; migration backfills `pin_lookup` and names existing duplicates rather
|
||||||
|
than failing startup.
|
||||||
|
- **A stored recurring-visitor photo is copied onto each visit, not referenced.** Keeps the visit
|
||||||
|
log a true snapshot, and stops photo-retention purging from deleting a live profile photo.
|
||||||
|
- **"Remove" on a recurring visitor is a hard delete that keeps visit history.** Visits
|
||||||
|
denormalise name/contact/host, so history survives; identifiers are freed for reuse. "Active"
|
||||||
|
untick is offered as the reversible alternative.
|
||||||
|
- **TLS: a 10-year local CA plus a 398-day server certificate, auto-renewed.** Apple/Chrome reject
|
||||||
|
server certs over ~398 days, so a single self-signed cert would need reinstalling on every
|
||||||
|
tablet yearly. The server cert renews inside 30 days and hot-swaps via `setSecureContext`;
|
||||||
|
the CA never changes, so devices are configured once. Rejected: single long-lived self-signed
|
||||||
|
cert; reverse proxy (still supported as an option).
|
||||||
|
- **Cert change detection tracks configured names via a sidecar file, not the full SAN list.**
|
||||||
|
The container's docker bridge IP is included automatically and changes on restart; comparing
|
||||||
|
full SANs would reissue the cert on every boot.
|
||||||
|
- **Plain-HTTP helper port serves the CA (`/ca.crt`, `/ca.cer`, `/ca.zip`) and redirects the
|
||||||
|
rest to HTTPS.** Solves the chicken-and-egg of downloading a cert you don't yet trust.
|
||||||
|
- **CA offered as DER `.cer` and as a zip bundle.** Jamf's certificate payload rejects PEM `.crt`
|
||||||
|
on extension; browsers block bare `.crt/.cer` downloads. The zip (all encodings + README with
|
||||||
|
fingerprint and install steps) is written by an in-repo ZIP writer rather than a dependency,
|
||||||
|
validated against Python's `zipfile`.
|
||||||
|
- **Google Sheet holds only who is currently on site.** One tab, rewritten in full on every sign
|
||||||
|
in/out (never patched, so it can't hold stale names). The earlier append-only history tab was
|
||||||
|
dropped at the user's request; full history stays in the app (Visit log + CSV). Keeps visitor
|
||||||
|
contact/movement history off the cloud.
|
||||||
|
- **Server-side printing: render the badge to a bitmap with `@napi-rs/canvas`, send via
|
||||||
|
`brother_ql` over TCP 9100.** Takes the iPad out of the print path (it was mangling label
|
||||||
|
format). Chose `brother_ql` over hand-rolling Brother's raster protocol for hardware
|
||||||
|
reliability. Sign-in awaits the print briefly; on failure the kiosk falls back to browser print.
|
||||||
|
- **Roll type is an explicit per-site setting (`printer_label`), decoupled from the red styling
|
||||||
|
option.** Originally the media id was inferred from the "print in red" checkbox, which caused
|
||||||
|
unexplainable "Wrong Roll Type" refusals. See *Technical Details* for the `--red` finding.
|
||||||
|
- **Photo halftoning happens in the renderer (Floyd–Steinberg), not in the printer driver.** The
|
||||||
|
printer is 1-bit; `brother_ql`'s default threshold turned faces into solid blocks. Doing it
|
||||||
|
server-side makes the preview and the printed label the same image.
|
||||||
|
- **Admin login is a separate document at `/admin/login`.** Replaced an embedded panel that
|
||||||
|
"unfolded" inside the console. Routes are declared before `express.static` so the redirect
|
||||||
|
guard can't be bypassed.
|
||||||
|
- **Branding: four settable colours; everything else derived.** Text on coloured backgrounds is
|
||||||
|
chosen by WCAG luminance; the muted label colour is mixed toward the background only as far as
|
||||||
|
it can go while staying ≥4.5:1. A fixed grey was the cause of "text blending in". SVG banners
|
||||||
|
rejected (can carry script, served to every kiosk).
|
||||||
|
- **Kiosk has no admin link and no author credit.** A kiosk is a public terminal. Credit line
|
||||||
|
remains on staff-facing pages only.
|
||||||
|
- **Company is recorded, not printed.** Adding it to the badge overflowed 62×90 mm labels.
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
**Ports / env:** container `3000` (HTTPS app) and `3001` (HTTP helper); host `HOST_PORT=8443`,
|
||||||
|
`HOST_HTTP_PORT=8080`. Key env: `HTTPS_ENABLED`, `HTTPS_HOSTNAMES` (comma list of names/IPs
|
||||||
|
the kiosks use), `HTTPS_PUBLIC_PORT` (must equal `HOST_PORT`), `APP_SECRET`,
|
||||||
|
`ADMIN_BOOTSTRAP_EMAIL`/`ADMIN_BOOTSTRAP_PASSWORD` (first account only; `ADMIN_PASSWORD` is
|
||||||
|
dead), `ADMIN_REQUIRE_2FA`, `ADMIN_ALLOWED_DOMAINS`, `EXPIRY_WARNING_DAYS`,
|
||||||
|
`SHEETS_ONSITE_TAB`, `PRINT_TIMEOUT_MS`, `PRINT_SIGNIN_WAIT_MS`.
|
||||||
|
|
||||||
|
**Operational gotchas (all hit during this session):**
|
||||||
|
- After editing `.env`, use `docker compose up -d`, **not** `docker compose restart` — restart
|
||||||
|
keeps the old environment. Symptom: cert covers only `localhost`, `visitors.local` and a
|
||||||
|
`172.x` bridge IP. Server now warns at boot when `HTTPS_HOSTNAMES` is at its default.
|
||||||
|
- Host `.env` predates many settings; compare with `diff <(grep -oE '^[A-Z_]+=' .env | tr -d '=' | sort -u) <(grep -oE '^[A-Z_]+=' .env.example | tr -d '=' | sort -u)`.
|
||||||
|
- Bind-mounted `./data` is created root-owned; `docker-entrypoint.sh` starts as root, chowns
|
||||||
|
`/data` only if ownership is wrong, then drops to `node` via `setpriv` (`util-linux` installed
|
||||||
|
explicitly).
|
||||||
|
- `pull_policy: build` in compose — the image is never pulled from a registry.
|
||||||
|
- `.gitattributes` forces LF (CRLF only for `.ps1`/`.bat`) so shell scripts survive Windows.
|
||||||
|
- `data/`, `.env`, `secrets/` are gitignored and survive `git reset --hard` (verified).
|
||||||
|
|
||||||
|
**Deploy flow (host):** branch is **`deploy`**.
|
||||||
|
`git fetch origin && git reset --hard origin/deploy && docker compose up -d --build`
|
||||||
|
then `docker compose exec visitor-signin npm run version` (reports which features are present).
|
||||||
|
|
||||||
|
**Printer — QL-820NWB findings (hard-won):**
|
||||||
|
- Head width on a 62 mm roll is **696 dots** at 300 dpi; bitmap is always 696 wide. 90 mm = 1063
|
||||||
|
raster lines. Rotation is applied to the finished image, not the layout.
|
||||||
|
- **DK-22251 (black/red) requires `brother_ql ... --label 62red --red` — even for an all-black
|
||||||
|
badge.** Proven on the wire: `--label 62red` alone is byte-identical to `--label 62` (1063
|
||||||
|
mono `g` lines, `ESC i K` mode byte `0x08`); adding `--red` produces 2126 two-plane `w` lines
|
||||||
|
and mode byte `0x09`. Printer message when missing: *"Black/Red on White paper is installed
|
||||||
|
now. Change it to Monochrome media."* Mismatch fails in both directions.
|
||||||
|
- `brother_ql`'s network backend **only writes; it never reads**, so it reports success even when
|
||||||
|
the printer refuses the job.
|
||||||
|
- The QL-820NWB **does not answer status requests on port 9100** (write-only print server).
|
||||||
|
**Brother Status Monitor on a connected Windows PC** gives the real refusal reason — use it
|
||||||
|
first.
|
||||||
|
- `62` = continuous roll; `62x100` etc. = die-cut. Sending one to the other is "Wrong Roll Type".
|
||||||
|
|
||||||
|
**Schema (SQLite, `src/db.js`, idempotent `addColumn` migrations):** tables `sites`, `hosts`,
|
||||||
|
`frequent_visitors`, `visits`, `admin_users`, `login_attempts`, `pin_attempts`, `settings`,
|
||||||
|
`sheet_queue` (now unused). Notable additions this session: `sites.printer_{enabled,host,port,
|
||||||
|
model,rotate,label}`, `sites.photo_{mode,threshold,contrast}`, `sites.badge_*`,
|
||||||
|
`sites.banner_{path,height,align}`, `sites.colour_{brand,signout,page,text}`,
|
||||||
|
`frequent_visitors.{pin_lookup,photo_path,company,site_id}`, `visits.{company,site_id,site_name}`.
|
||||||
|
|
||||||
|
**Diagnostic scripts (run inside the container):** `scripts/version.mjs` (`npm run version`),
|
||||||
|
`scripts/print-test.mjs [--dry] [--label ID] [--rotate DEG]`, `scripts/printer-status.mjs`
|
||||||
|
(won't work on the QL-820NWB — see above), `scripts/print-probe.mjs` (interactive, needs
|
||||||
|
`docker compose exec -it`), `scripts/make-cert.mjs [--force]`, `scripts/gen-cert.sh`.
|
||||||
|
|
||||||
|
**Push scripts:** `push-to-gitea.ps1/.bat/.sh`. Bugs fixed in-session: PowerShell functions
|
||||||
|
return their whole output stream (a wrapper made every git call look failed); branch was
|
||||||
|
hard-coded to `main` while work was on `deploy`; now detects unfinished rebases, checks git
|
||||||
|
identity, uses the checked-out branch, and verifies `$LASTEXITCODE` after each call.
|
||||||
|
|
||||||
|
## Code Changes
|
||||||
|
Whole project created this session. Principal files:
|
||||||
|
- `src/server.js` — Express app, HTTPS listener with cert hot-swap, HTTP helper (CA downloads +
|
||||||
|
redirect), admin page routing before static, `Cache-Control: no-cache` on html/js/css, jobs.
|
||||||
|
- `src/db.js` — schema, migrations, default site, `pin_lookup` backfill, duplicate reporting.
|
||||||
|
- `src/config.js` — env parsing incl. `https`, `sheets`, `printing`, `admin` groups.
|
||||||
|
- `src/auth.js` — scrypt passwords, RFC 6238 TOTP (verified against RFC vectors), recovery codes.
|
||||||
|
- `src/users.js` — admin accounts, domain restriction, lockout, bootstrap.
|
||||||
|
- `src/pins.js` — PIN encrypt/decrypt, keyed `pinLookup`, collision-aware `generatePin`.
|
||||||
|
- `src/photos.js` — save/copy/purge visit and profile photos.
|
||||||
|
- `src/sites.js` — site helpers, HTML badge renderer (browser fallback).
|
||||||
|
- `src/branding.js` — theme derivation, WCAG contrast, banner storage.
|
||||||
|
- `src/tls.js` — CA + server cert lifecycle, DER export, zip bundle.
|
||||||
|
- `src/zip.js` — dependency-free ZIP writer.
|
||||||
|
- `src/sheets.js` — single-tab "On site now" rewrite, human-readable Google error mapping.
|
||||||
|
- `src/printer.js` — canvas badge render, Floyd–Steinberg halftone, `brother_ql` invocation with
|
||||||
|
`--red` for `62red`, photo preview sheet, error translation.
|
||||||
|
- `src/routes/kiosk.js` — sign in/out, PIN auth, site scoping, server-print with fallback.
|
||||||
|
- `src/routes/admin.js` — auth/2FA, users, sites, hosts, recurring visitors, visits, printer
|
||||||
|
diagnostics/test/preview, TLS endpoints, status.
|
||||||
|
- `public/index.html`, `public/js/kiosk.js`, `public/css/kiosk.css` — kiosk flow (filter input
|
||||||
|
above host dropdown, square photo capture, badge fallback).
|
||||||
|
- `public/login.html`, `public/js/login.js`, `public/css/login.css` — standalone paged login.
|
||||||
|
- `public/admin.html`, `public/js/admin.js`, `public/css/admin.css` — console.
|
||||||
|
- `Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `.gitattributes`, `.env.example`.
|
||||||
|
- `scripts/*` — diagnostics and cert tooling listed above.
|
||||||
|
|
||||||
|
## Open Questions / Next Steps
|
||||||
|
- **Confirm the `--red` fix prints on real hardware.** Pending: deploy, then **Test print**. The
|
||||||
|
site's roll setting is already `62red` with red on.
|
||||||
|
- **Latest changes exist only in the handed-over tarball, not in git:** CA zip bundle, printer
|
||||||
|
diagnostics/probe scripts, `--red` flag, photo halftoning. Next: extract over the Windows
|
||||||
|
working copy → commit → push `deploy` → host `reset --hard origin/deploy` → rebuild.
|
||||||
|
- **Tune photo halftoning from a printed label** (defaults: error diffusion, threshold 50,
|
||||||
|
contrast 20). 300 dpi dithering reads differently in the hand than on screen.
|
||||||
|
- Physical print output not yet verified: margins, cut position, rotation on the roll, red plane.
|
||||||
|
- Host `.env` is stale; recommend rebuilding from `.env.example`, preserving `APP_SECRET` exactly.
|
||||||
|
- Google Sheet returned 403 "caller does not have permission" — fix is sharing the sheet with the
|
||||||
|
service account (shown in Admin → System). Outcome not confirmed.
|
||||||
|
- Recommend a DNS name for the kiosk instead of a bare IP (IP-only certs are unreliable on
|
||||||
|
Android; DHCP changes break everything).
|
||||||
|
- Consider making `deploy` the default branch in Gitea / retiring the tangled `main`.
|
||||||
|
- Offered, not built: die-cut roll options in the roll dropdown; an admin field for
|
||||||
|
`HTTPS_HOSTNAMES`; banner/logo on the printed pass; theming the admin console.
|
||||||
|
- Housekeeping: `sheet_queue` table is unused; `brother_ql==0.9.4` is unmaintained (fork
|
||||||
|
`brother_ql_next` exists).
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
- Repository: `<redacted-gitea-host>/<redacted-owner>/visitor-signin`, working branch `deploy`.
|
||||||
|
- Commits on `deploy`: `8bc7179` Server-side printing, roll type setting, cache headers;
|
||||||
|
`eb98658` Printing Debug; `b1a7933` Printer Debug 2.
|
||||||
|
- `brother_ql` 0.9.4 (PyPI) — `print --help` documents the `--red` requirement.
|
||||||
|
- Brother QL-820NWB specification: 12–62 mm media, 60.96 mm max print width, 300×300 dpi.
|
||||||
|
- RFC 6238 (TOTP) — test vectors used to verify `src/auth.js`.
|
||||||
Reference in new issue
Block a user