Public Access
Visitor sign in kiosk: multi-site, badge printing, WWCC expiry warnings, admin accounts with 2FA
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
# Visitor sign in
|
||||
|
||||
A self-hosted visitor kiosk for sites that need a WWCC/VIT record and a photo at the door.
|
||||
Runs in one Docker container, stores everything locally in SQLite, and mirrors every sign in
|
||||
and sign out to a Google Sheet so someone outside the building can see who is on site during
|
||||
an evacuation.
|
||||
|
||||
- **Kiosk** at `/` — a stepped sign in, a PIN sign in for regulars, and sign out. No link to
|
||||
the admin console: a kiosk is a public terminal and administration does not belong on it.
|
||||
Reach the console from a staff machine at `/admin`.
|
||||
- **Admin** at `/admin` — who's on site now, the visit log, recurring visitors, the people a
|
||||
visitor can ask for, sites, admin accounts, and system status.
|
||||
|
||||
Handles several sites from one container, prints a badge after sign in if you want one,
|
||||
warns admins before a WWCC or VIT lapses, and gives each admin their own account with
|
||||
two factor.
|
||||
|
||||
## What it collects
|
||||
|
||||
| | Guest sign in | Recurring visitor |
|
||||
|---|---|---|
|
||||
| First and last name | typed each visit | on file |
|
||||
| Company or organisation | optional, typed each visit | on file |
|
||||
| Person being visited | picked from the list | picked each visit |
|
||||
| Photo | taken at the kiosk | on file if saved, otherwise taken at the kiosk |
|
||||
| WWCC / VIT / none | typed each visit | on file |
|
||||
| Mobile and/or email | at least one required | on file |
|
||||
|
||||
Sign out only needs a **last name** plus a **mobile number or email**, which works for both.
|
||||
|
||||
The company field is optional and clearly marked as such — plenty of visitors are not from
|
||||
anywhere in particular. When it is filled in it appears in the on-site list, on the evacuation
|
||||
sheet, and in the visit log, and the log search matches on it, so you can pull up every visit from
|
||||
one contractor. It is **not** printed on the badge — the label stays name, host, time and check
|
||||
status.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone https://gitea.hideawaygaming.com.au/jessikitty/visitor-signin.git
|
||||
cd visitor-signin
|
||||
cp .env.example .env
|
||||
|
||||
# Generate a secret and set a real admin password before you start.
|
||||
openssl rand -hex 32 # paste into APP_SECRET
|
||||
$EDITOR .env
|
||||
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
The kiosk is then on `https://<docker-host>:8443` and the admin console on
|
||||
`https://<docker-host>:8443/admin`. Expect a browser warning until you install the
|
||||
authority certificate — see below.
|
||||
|
||||
Set `ADMIN_BOOTSTRAP_EMAIL` and `ADMIN_BOOTSTRAP_PASSWORD` in `.env` before the first start —
|
||||
they create the first admin account, once. You'll be asked to enrol two factor and set a real
|
||||
password at first sign in.
|
||||
|
||||
Then, in the admin console:
|
||||
|
||||
1. **Sites** → rename the first site, add more if you have them, and turn badge printing on.
|
||||
2. **People to visit** → pick a site, then paste or upload your staff CSV
|
||||
(see `docs/hosts-sample.csv`).
|
||||
3. **Recurring visitors** → add anyone who comes regularly. A PIN is generated and a printable
|
||||
card opens straight away.
|
||||
4. **Admins** → invite the rest of the front office.
|
||||
|
||||
## Working from Windows, deploying to Ubuntu
|
||||
|
||||
Develop on Windows, run the container on the Ubuntu host. Two scripts are included:
|
||||
|
||||
```powershell
|
||||
# in PowerShell, inside the visitor-signin folder
|
||||
.\push-to-gitea.ps1
|
||||
```
|
||||
|
||||
Or double-click `push-to-gitea.bat`. If PowerShell blocks the script, run
|
||||
`powershell -ExecutionPolicy Bypass -File .\push-to-gitea.ps1`. On the Ubuntu host,
|
||||
`./push-to-gitea.sh` does the same thing.
|
||||
|
||||
`.gitattributes` forces LF endings for everything except the `.ps1` and `.bat` helpers, so the
|
||||
shell scripts and the Dockerfile still work after a round trip through a Windows checkout —
|
||||
otherwise `gen-cert.sh` fails on the host with a confusing `\r: command not found`.
|
||||
|
||||
Deploy on the Ubuntu host with:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.hideawaygaming.com.au/jessikitty/visitor-signin.git
|
||||
cd visitor-signin && cp .env.example .env && nano .env
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
`docker compose` on Windows works too if you have Docker Desktop, but the camera, printing and
|
||||
the `data/` permissions all behave more predictably on the Ubuntu host.
|
||||
|
||||
## Several sites, one container
|
||||
|
||||
Each site has its own name, its own list of people to visit, and its own badge settings.
|
||||
Recurring visitors are either tied to one site or welcome at all of them.
|
||||
|
||||
Point a kiosk at a site once, during setup, with `http://host:8088/?site=front-gate`. The tablet
|
||||
remembers the choice, so the address bar no longer matters. If you skip that, the kiosk asks
|
||||
which site it is on first use — and with only one site set up, it never asks at all.
|
||||
|
||||
Sign in, sign out and the "already signed in" check are all scoped to the kiosk's site, so the
|
||||
same person can be signed in at two sites at once without the system arguing about it.
|
||||
|
||||
An admin account can be limited to a single site. Those admins see only that site's visitors,
|
||||
staff list and log, and cannot touch the others.
|
||||
|
||||
## Branding the kiosk
|
||||
|
||||
Each site can carry its own banner and colours, set under **Sites → Edit**.
|
||||
|
||||
**Banner.** Upload a PNG, JPEG or WebP up to 2 MB. A PNG with a transparent background is the
|
||||
one to use — it sits straight on the bar colour with nothing painted behind it, so it works
|
||||
whatever colour you pick. The admin preview shows it on a checkerboard so you can see the
|
||||
transparency. When a banner is set it replaces the site name in the kiosk header. Set the on-screen height in
|
||||
pixels to suit the shape of your logo, and choose whether it sits **left** or **centred** — the
|
||||
header is a three-column layout, so a centred logo is centred on the page rather than centred in
|
||||
whatever space the clock leaves over. The setting applies to the site name too, when no banner is
|
||||
uploaded. SVG is deliberately not accepted:
|
||||
it can carry script, and this file is served to every kiosk.
|
||||
|
||||
**Colours.** Four are settable:
|
||||
|
||||
| Setting | Where it shows |
|
||||
|---|---|
|
||||
| Bar and buttons | The top bar, the Sign in door, primary buttons, the confirmation mark |
|
||||
| Sign out | The Sign out door and the signed-out confirmation |
|
||||
| Page background | Behind everything, with card and rule colours derived from it |
|
||||
| Body text | Headings, answers, and the source for the softer label colour |
|
||||
|
||||
Two things are still worked out rather than set. **Text on a coloured background** is chosen by
|
||||
contrast, so a pale yellow bar gets dark text instead of unreadable white. And the **muted colour**
|
||||
used for field labels and hints is your body text mixed towards the background only as far as it
|
||||
can go while still clearing WCAG AA at 4.5:1 — a fixed grey looks fine on the default background
|
||||
and vanishes on a custom one, which is the usual cause of text that blends in.
|
||||
|
||||
The site editor shows the contrast ratio as you type and warns below 4.5:1. Aim for 7:1 or better
|
||||
on a kiosk people read standing up.
|
||||
|
||||
Leave a colour box empty to fall back to the default. Anything that is not a six-digit hex value
|
||||
is ignored rather than applied.
|
||||
|
||||
The admin console keeps its own neutral look, so it stays recognisable when you are switching
|
||||
between sites. Printed badges stay black on white — a label printer has no colours to give.
|
||||
|
||||
## Badge printing
|
||||
|
||||
Turn it on per site under **Sites → Edit**. After a visitor signs in, the kiosk loads the badge
|
||||
into a hidden frame and prints it — one label, no dialog on most kiosk setups. The badge shows
|
||||
the site, the visitor's name, who they are visiting, the time in, their WWCC/VIT number or a
|
||||
boxed **No WWCC / VIT**, the photo if you want it, and an optional line of your own text.
|
||||
|
||||
The photo is square, matching the crop taken at the kiosk, and sits vertically centred.
|
||||
|
||||
Pick your stock from the **Label stock** list and the dimensions fill themselves in. A label
|
||||
noticeably taller than it is wide gets a stacked layout — photo on top, name beneath — which is
|
||||
what you want on a roll printer. Wider stock gets the photo alongside the text instead. Type
|
||||
scales with the constraining dimension, so small labels stay readable.
|
||||
|
||||
Admins can reprint from the **On site** list, and the visitor gets a "Print the badge again"
|
||||
button on the confirmation screen if the first one jams.
|
||||
|
||||
### Brother QL-820NWB
|
||||
|
||||
The default for a new site is 62 × 100 mm, which matches the DK-11202 die-cut label. For a
|
||||
continuous roll, 62 × 90 mm is a good visitor badge.
|
||||
|
||||
It takes media 12 to 62 mm wide and prints up to 60.96 mm across at 300 × 300 dpi, so 62 mm is
|
||||
the widest roll it will accept — the console warns you if you enter anything wider. Useful rolls:
|
||||
|
||||
| Roll | Size | Good for |
|
||||
|---|---|---|
|
||||
| DK-22205 | 62 mm continuous | The default. Cut to any length; 90 mm suits a visitor badge |
|
||||
| DK-11202 | 62 × 100 mm die-cut | Pre-cut, no length to choose |
|
||||
| DK-22251 | 62 mm continuous, black/red | Same as DK-22205 but supports the red option below |
|
||||
| DK-11208 | 38 × 90 mm die-cut | Narrower; turn the photo off |
|
||||
| DK-11209 | 29 × 62 mm die-cut | Name and host only |
|
||||
|
||||
**The red option.** Tick *Print the heading and the no-check warning in red* and the site name
|
||||
and the **No WWCC / VIT** box print red instead of black, which makes a visitor without a check
|
||||
obvious across a room. It only works on a DK-22251 roll — on any other roll the printer renders
|
||||
it as grey. Two-colour printing is also far slower than black alone (Brother rate it at roughly
|
||||
15 labels a minute against 110), which is irrelevant for one badge at a time but worth knowing.
|
||||
|
||||
### Printing from the server
|
||||
|
||||
Set the printer's IP address under **Sites → Edit → Printer** and tick *Print from the server*.
|
||||
The server then renders the badge itself and pushes it to the printer over the network, so:
|
||||
|
||||
- a kiosk tablet needs no printer driver, no default printer and no print dialog
|
||||
- adding a second kiosk means plugging in a tablet, nothing else
|
||||
- the badge prints automatically the moment someone completes their sign in
|
||||
|
||||
The QL-820NWB has Ethernet and Wi-Fi, so it lives on the network rather than tethered to a
|
||||
tablet. Give it a **fixed IP** — a DHCP lease change would silently stop badges printing.
|
||||
|
||||
Rendering happens at 300 dpi and 696 dots across, which is the printer's fixed head width on a
|
||||
62 mm roll. **Bitmap preview** on the site card shows the exact image that will be sent, and
|
||||
**Test print** sends a sample badge. Use both before committing a roll.
|
||||
|
||||
**Rotation.** At 0° the badge is laid out across the 62 mm width and runs down the label. At 90°
|
||||
it is laid out along the length and turned before printing, which is what you want when the label
|
||||
hangs from its short edge. Set it per site and check the bitmap preview — the two look very
|
||||
different and only one will suit how you hang them.
|
||||
|
||||
**If the printer cannot be reached**, sign in still completes. The kiosk falls back to its own
|
||||
browser print dialog, and the failure is shown against the site in **Admin → Sites** with the
|
||||
reason. Admins can reprint any badge from the **On site** list.
|
||||
|
||||
Leave *Print from the server* off and the kiosk prints through the browser as before: set the
|
||||
QL-820NWB as the browser's default printer, margins to none, scaling 100%.
|
||||
|
||||
## WWCC and VIT expiry warnings
|
||||
|
||||
Give a recurring visitor an expiry date and the console watches it. Inside
|
||||
`EXPIRY_WARNING_DAYS` (28 by default) their row turns amber; past the date it turns red, a count
|
||||
appears on the **Recurring visitors** tab, and a banner sits across the top of every screen.
|
||||
|
||||
Nothing is blocked automatically — an expired check is a conversation to have at the desk, not
|
||||
a door the software should slam. Site-scoped admins only see warnings for their own site.
|
||||
|
||||
## Admin accounts and two factor
|
||||
|
||||
Each admin signs in with their own email address and password.
|
||||
|
||||
- **Roles.** *Owner* manages admins and sites. *Admin* handles day to day work, optionally
|
||||
limited to one site.
|
||||
- **Two factor.** With `ADMIN_REQUIRE_2FA=true` (the default) every admin enrols an
|
||||
authenticator app at first sign in — a QR code appears, they scan it, and eight one-shot
|
||||
recovery codes are issued. Standard TOTP, so Google Authenticator, Authy, 1Password,
|
||||
Bitwarden and the rest all work.
|
||||
- **Domain limits.** Set `ADMIN_ALLOWED_DOMAINS=yourschool.vic.edu.au` and both invitations and
|
||||
sign in refuse anything else. Subdomains of a listed domain are accepted.
|
||||
- **Recovery.** An owner can reset another admin's password (a temporary one is shown on
|
||||
screen, and they must change it at next sign in) or clear their two factor so they can
|
||||
re-enrol on a new phone.
|
||||
- **Lockout.** Six wrong passwords locks that email address for 15 minutes.
|
||||
|
||||
If every owner loses access, stop the container, clear the `admin_users` table with
|
||||
`sqlite3 data/visitors.db "DELETE FROM admin_users;"`, and start it again — the bootstrap
|
||||
account is recreated from `.env`.
|
||||
|
||||
## HTTPS and the certificate
|
||||
|
||||
The kiosk needs HTTPS: browsers block camera access on plain http unless the address is
|
||||
`localhost`. Since this never faces the internet, it runs its own certificate authority.
|
||||
|
||||
With `HTTPS_ENABLED=true` (the default) the container creates two things at first start:
|
||||
|
||||
- **A certificate authority**, valid for ten years. Install this on each kiosk device, once.
|
||||
- **A server certificate**, valid for about 13 months, signed by that authority.
|
||||
|
||||
The server certificate renews itself before it lapses and reloads without a restart. Because
|
||||
the authority is what the devices trust, renewal never means touching the tablets again. This is
|
||||
why it isn't one plain self-signed certificate: Apple and Chrome reject server certificates
|
||||
valid for much more than a year, so a single self-signed file would have to be reinstalled
|
||||
everywhere every year.
|
||||
|
||||
### Set it up
|
||||
|
||||
List every name and address staff might type, in `.env`:
|
||||
|
||||
```
|
||||
HTTPS_ENABLED=true
|
||||
SECURE_COOKIES=true
|
||||
HTTPS_HOSTNAMES=visitors.local,visitors.hideaway.lan,192.168.1.50
|
||||
HOST_PORT=8443
|
||||
HTTPS_PUBLIC_PORT=8443
|
||||
```
|
||||
|
||||
An address that isn't listed produces a browser warning. Change the list and restart; the
|
||||
certificate reissues itself automatically, and devices that already trust the authority accept
|
||||
it without any further work.
|
||||
|
||||
The kiosk is then at `https://visitors.local:8443`, admin at `https://visitors.local:8443/admin`.
|
||||
|
||||
### Trusting the authority on each device
|
||||
|
||||
Port 8080 runs a small plain-http helper that does exactly two things: hands out the authority
|
||||
certificate, and redirects everything else to https. It exists to solve the chicken-and-egg
|
||||
problem of fetching the certificate you don't yet trust.
|
||||
|
||||
On each tablet, browse to `http://visitors.local:8080/ca.crt` and install the downloaded file:
|
||||
|
||||
| Device | Where |
|
||||
|---|---|
|
||||
| Windows | Double-click → Install Certificate → Local Machine → *Trusted Root Certification Authorities* |
|
||||
| Android | Settings → Security → Encryption & credentials → Install a certificate → **CA certificate** |
|
||||
| iPad / iPhone | Open in Safari → install the profile → then Settings → General → About → **Certificate Trust Settings** and switch it on. The second step is separate and easy to miss |
|
||||
| macOS | Double-click → Keychain Access → System → set to *Always Trust* |
|
||||
| Ubuntu | `sudo cp ca.crt /usr/local/share/ca-certificates/visitor-signin.crt && sudo update-ca-certificates` |
|
||||
|
||||
Admins can also download it from **Admin → System → Certificate**, which shows the expiry dates
|
||||
and the authority's fingerprint — check that fingerprint matches what the tablet shows during
|
||||
installation.
|
||||
|
||||
### Managing it later
|
||||
|
||||
**Admin → System → Certificate** has *Renew the server certificate* (safe, no device changes)
|
||||
and *Start a new authority* (every device must trust the new one, so only for a suspected key
|
||||
leak). From a shell on the docker host, `./scripts/gen-cert.sh` and
|
||||
`./scripts/gen-cert.sh --force` do the same two jobs.
|
||||
|
||||
The private keys live in `data/certs/` with `0600` permissions. They are in `.gitignore` and
|
||||
must never be committed.
|
||||
|
||||
### If you'd rather use a reverse proxy
|
||||
|
||||
Set `HTTPS_ENABLED=false` and `TRUST_PROXY=true`, terminate TLS at Nginx Proxy Manager, Traefik
|
||||
or Caddy, and drop the port 8080 mapping from `docker-compose.yml`.
|
||||
|
||||
## Google Sheet mirroring
|
||||
|
||||
Every sign in and sign out appends a row. If Google is unreachable the row is queued in the
|
||||
database and retried every minute, so a dropped internet connection never blocks the front desk.
|
||||
|
||||
1. In [Google Cloud Console](https://console.cloud.google.com/), create a project and enable
|
||||
the **Google Sheets API**.
|
||||
2. Create a **service account**, then create a **JSON key** for it and download the file.
|
||||
3. Create the spreadsheet you want to use. **Share it with the service account's email address**
|
||||
(it ends in `.iam.gserviceaccount.com`) with **Editor** access. This step is the one people
|
||||
forget — without it every write returns a permission error.
|
||||
4. Copy the spreadsheet id out of the URL:
|
||||
`docs.google.com/spreadsheets/d/`**`THIS_PART`**`/edit`.
|
||||
5. Put the key file at `./secrets/google-service-account.json` (compose mounts `./secrets`
|
||||
read-only), then in `.env`:
|
||||
|
||||
```
|
||||
SHEETS_ENABLED=true
|
||||
SHEETS_SPREADSHEET_ID=THIS_PART
|
||||
SHEETS_LOG_TAB=Visitor log
|
||||
SHEETS_ONSITE_TAB=On site now
|
||||
GOOGLE_CREDENTIALS_PATH=/secrets/google-service-account.json
|
||||
```
|
||||
|
||||
If you would rather not mount a file, base64 the key instead —
|
||||
`base64 -w0 key.json` — and put the result in `GOOGLE_CREDENTIALS_B64`.
|
||||
6. Restart, then **Admin → System → Test the sheet connection**. Both tabs and their headers
|
||||
are created the first time.
|
||||
|
||||
### What the sheet holds
|
||||
|
||||
**Only the people currently on site.** One tab, rewritten in full whenever anyone signs in or
|
||||
out. Nothing is appended, so there is no history to scroll past while you are standing in a car
|
||||
park counting heads — the top row says `On site now — 3 people — updated 31/08/26, 14:12`, and
|
||||
everything under it is someone still in the building.
|
||||
|
||||
Rewriting rather than patching is deliberate: a failed update can never leave a stale name on the
|
||||
evacuation list, because whatever is on the tab is what the database said at the time shown. If a
|
||||
write fails the tab is marked stale and rewritten on the next pass, once a minute. It also
|
||||
refreshes every 15 minutes on its own to keep the "on site for" column honest.
|
||||
|
||||
The **full visit history stays in the application** — searchable under **Visit log** in the admin
|
||||
console, and downloadable as CSV. It is not sent to Google, which keeps visitor contact details
|
||||
and movement history off a cloud service that only exists here for the evacuation case.
|
||||
|
||||
Every row carries the site name, so one spreadsheet covers every site.
|
||||
|
||||
**Bookmark the sheet on the phones that would actually be used in an evacuation, and check it
|
||||
after setup.** A sheet nobody can find is not a safety measure.
|
||||
|
||||
*Upgrading from an earlier version:* the old "Visitor log" tab is left alone but no longer
|
||||
written to. Delete it by hand when you are ready.
|
||||
|
||||
## Recurring visitors and PINs
|
||||
|
||||
Each saved person is one record: **mobile number, email address and PIN are all unique**, checked
|
||||
when a record is added or edited and enforced by the database. If an existing database already
|
||||
contains duplicates, the startup log names who collides and the checks stay at the application
|
||||
level until you fix them.
|
||||
|
||||
### A photo on file
|
||||
|
||||
Give a recurring visitor a photo in the admin console — from the machine's camera or an uploaded
|
||||
file — and the kiosk stops asking them to pose. They enter their PIN, pick who they are visiting,
|
||||
and the sign in completes with their pass printing immediately.
|
||||
|
||||
The stored photo is *copied* onto each visit rather than referenced, so the visit log stays a
|
||||
true snapshot: replacing someone's photo later does not change what past visits show, and photo
|
||||
retention cleaning up old visits can never delete a live profile photo.
|
||||
|
||||
Leave the photo blank and they are asked at the kiosk as before.
|
||||
|
||||
### Removing someone
|
||||
|
||||
**Remove** on the recurring visitors list deletes the saved record for good — the PIN stops
|
||||
working, the stored photo is deleted, and their mobile number, email and PIN become available
|
||||
for someone else.
|
||||
|
||||
Their **visit history is kept**. Visits store the name, contact details and host as their own
|
||||
columns, so the log remains a complete record of who was in the building regardless of whether
|
||||
the person is still on file. Removing someone who is currently signed in does not sign them out;
|
||||
the confirmation says so, and their visit can still be closed with their last name and mobile
|
||||
number at the kiosk.
|
||||
|
||||
If they might come back, untick **Active** in Edit instead. That keeps the record, the PIN and
|
||||
the history, but stops the PIN working at the kiosk.
|
||||
|
||||
### PINs
|
||||
|
||||
The mobile number is the username, and the PIN is four digits. PINs are stored encrypted with
|
||||
`APP_SECRET` rather than hashed, so an admin can reprint a lost card without resetting it. Four
|
||||
digits is only 10,000 combinations, so hashing would add nothing against anyone holding a copy
|
||||
of the database — the real protection is the lockout: five wrong PINs on a number locks it for
|
||||
15 minutes.
|
||||
|
||||
Four digits gives 10,000 combinations and each must be unique, so that is the ceiling on
|
||||
simultaneous recurring visitors. Deactivating someone frees theirs.
|
||||
|
||||
**Changing `APP_SECRET` makes every stored PIN unreadable.** If you have to change it, reissue
|
||||
PINs from the admin console afterwards.
|
||||
|
||||
## Where the data lives
|
||||
|
||||
Everything is under `./data` on the docker host:
|
||||
|
||||
```
|
||||
data/
|
||||
├── visitors.db SQLite: sites, visits, recurring visitors, hosts, admins, retry queue
|
||||
├── visitors.db-wal write-ahead log — back this up alongside the .db
|
||||
├── certs/ the local authority and the server certificate (keys are 0600)
|
||||
└── photos/2026/08/ JPEGs, foldered by year and month
|
||||
```
|
||||
|
||||
Photos never leave the host. The sheet records only whether a photo exists. They are deleted
|
||||
automatically after `PHOTO_RETENTION_DAYS` (90 by default), and only the admin console can view
|
||||
them.
|
||||
|
||||
To back up: `docker compose stop && tar czf visitor-backup-$(date +%F).tar.gz data/ && docker compose start`.
|
||||
|
||||
## Settings worth knowing
|
||||
|
||||
| Variable | Does what |
|
||||
|---|---|
|
||||
| `REQUIRE_PHOTO` | `false` lets a visitor continue if the camera fails |
|
||||
| `AUTO_SIGNOUT_TIME` | e.g. `18:30` — closes off anyone still shown as on site. Blank to disable |
|
||||
| `PHOTO_RETENTION_DAYS` | `0` keeps photos forever |
|
||||
| `HOST_PORT` | https port on the docker host, default `8443` |
|
||||
| `HOST_HTTP_PORT` | http helper port, default `8080` — serves the CA and redirects |
|
||||
| `HTTPS_HOSTNAMES` | every name and IP the certificate should cover |
|
||||
| `EXPIRY_WARNING_DAYS` | how far ahead to warn about a WWCC or VIT, default `28` |
|
||||
| `ADMIN_REQUIRE_2FA` | `false` makes two factor optional per admin |
|
||||
| `ADMIN_ALLOWED_DOMAINS` | comma separated; blank allows any email domain |
|
||||
|
||||
The kiosk returns to the home screen after two minutes of inactivity so the next visitor never
|
||||
sees the last one's details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**`pull access denied for visitor-signin`** — something ran `docker compose pull`. The image is
|
||||
built here, not fetched from a registry. Use `docker compose up -d --build`. The compose file
|
||||
sets `pull_policy: build` so this should not come back.
|
||||
|
||||
**`EACCES: permission denied, mkdir '/data/photos'`** — the bind-mounted `./data` on the host is
|
||||
owned by root, and the app runs as an unprivileged user. The container's entrypoint fixes this
|
||||
itself on start; if you are on an older build, do it by hand:
|
||||
|
||||
```bash
|
||||
sudo chown -R 1000:1000 data secrets
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
**Changes to the code do nothing** — Compose reuses the existing image. Always
|
||||
`docker compose up -d --build` after a `git pull`.
|
||||
|
||||
**Google Sheet says "The caller does not have permission"** — the app authenticated fine and
|
||||
Google refused the spreadsheet. Work through these in order:
|
||||
|
||||
1. **Admin → System** shows the service account address. Open the sheet, press Share, paste that
|
||||
address, set it to **Editor**, and untick "Notify people". This is the cause about nine times
|
||||
in ten.
|
||||
2. If your Google Workspace blocks sharing outside the organisation, the share will silently fail
|
||||
or be refused — a service account address is external. Ask your Workspace admin to allow it,
|
||||
or create the sheet in an account that permits external sharing.
|
||||
3. If the sheet lives in a **Shared drive**, share the drive with the service account, not just
|
||||
the file.
|
||||
4. Check the spreadsheet ID matches the one in the sheet's URL. A wrong ID usually gives a 404,
|
||||
but a valid ID for someone else's sheet gives this same 403.
|
||||
5. Confirm the **Google Sheets API** is enabled on the project the key belongs to. A key from
|
||||
project A cannot use an API enabled only on project B.
|
||||
|
||||
Press **Test the sheet connection** after each step.
|
||||
|
||||
**Browser still warns about the certificate** — the authority is installed but not trusted. On
|
||||
iOS that is a second, separate step under Settings → General → About → Certificate Trust
|
||||
Settings. On Android, use a hostname rather than a bare IP.
|
||||
|
||||
## Running without Docker
|
||||
|
||||
```bash
|
||||
npm install
|
||||
DATA_DIR=./data APP_SECRET=$(openssl rand -hex 32) \
|
||||
ADMIN_BOOTSTRAP_EMAIL=you@example.com ADMIN_BOOTSTRAP_PASSWORD=ChangeMe12345 npm start
|
||||
```
|
||||
|
||||
Node 20 or newer, and `openssl` on PATH if you want the container to issue its own certificate.
|
||||
|
||||
## A note on evacuation use
|
||||
|
||||
The **On site now** tab is the offsite copy, and it only helps if someone can open it on a phone
|
||||
while standing in a car park. Bookmark it on the relevant phones and check it actually loads for
|
||||
them, not just for you.
|
||||
|
||||
Check it again occasionally. A revoked service account key, or a sheet whose sharing was changed,
|
||||
will queue rows silently — the head count and *Updated* time in the first row are the giveaway,
|
||||
and **Admin → System** shows the last successful write and any error.
|
||||
|
||||
Worth deciding now: the kiosk is on your internal network, so if the network or the container is
|
||||
down, the sheet stops updating while people keep walking in. `AUTO_SIGNOUT_TIME` limits how stale
|
||||
the list can get overnight, but a printed fallback at the front desk is still worth having.
|
||||
|
||||
---
|
||||
|
||||
Created by: Jess Rogerson (yelling commands at Claude.AI)
|
||||
Reference in New Issue
Block a user