- OPAC_URL / STAFF_URL let Quick Add build correct links behind a reverse proxy
- restore the Catalogue/Staff header links (lost when the init block was
rewritten) and verify them in the browser test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
Quick Add:
- barcode choices: automatic (zero-padded, >= 7 digits), scan a pre-printed
sticker per item, or none for now; bulk 'give barcodes to items without one';
reserve a block of numbers and print the sheet ahead of time (link opens
Koha's range printer prefilled)
- default shelf saved on the server (shared by every device)
- item types follow Koha; CSP allows cover redirects; aria attrs fixed
Koha bootstrap:
- default lending rule (3 weeks, 25 items, 3 renewals, no fines) - a fresh
Koha refuses every checkout without one
- automatic library-card numbers (start at 1000001), fines off
- Koha's own auto-barcode off (one numbering scheme)
- patron card template (Avery 5371) and layout with a scannable barcode
- LIBRARY_CODE, SHELVES and ITEM_TYPES honoured at first install
Why 7 digits: decoded printed sheets - 7-digit Code 39 read back 40/40, 6-digit
all-numeric symbols were reinterpreted as Code 32 for ~1 in 10 values.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- Quick Add: choose the shelf/room (Koha LOC values) once; every scanned item
gets it and the choice is remembered; invalid values are ignored
- install.sh --status shows localhost in loopback mode; port probing works
without ss
- library/README.md; kiosk Readme.md add-on section and licenses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- BIND_ADDR (default 0.0.0.0) on every published port; 127.0.0.1 keeps the
stack reachable from the host only (Docker bypasses ufw, so this is the
actual control the kiosk addon's 'this kiosk only' choice uses)
- Koha serves its web-installer page with HTTP 200 before the database
exists, so 'port answers' was not 'ready': the entrypoint now writes
/run/koha-ready, the healthcheck requires it, and install.sh waits for
healthy + the quick-add health endpoint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- lib/docker.sh: shared Docker/Compose tooling (on-demand install via apt or
get.docker.com, compose wrapper, container state/health, HTTP wait)
- menus/addon_library.sh: install/update/uninstall the Koha + Quick Add stack
from library/ to /opt/kiosk-library, add the catalogue as a kiosk page
(duration 0), choose kiosk-only vs LAN access, show addresses and login
- wire into install.sh menu, complete_uninstall and clone_settings
- tests/addon_library.test.sh: 41 checks against a stub sudo/docker and a
fixture stack (no root, Docker or network needed)
Note: the 'network access' choice relies on a BIND_ADDR setting in the
library stack, added in a follow-up commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
A small standard-library Python service next to Koha:
- looks the code up via Z39.50 (yaz) then Open Library over HTTPS
- checks Koha for an existing copy (ISBN-10/13 and UPC/EAN aware)
- adds the record + an item with the next barcode via Koha's REST API
- scanner-friendly UI: auto-focus, audible feedback, duplicate card with
'add another copy', manual entry when nothing is found, undo, and the
barcode range to print labels for
Also: enable RESTBasicAuth, map 024$a to the ean column, wire the service,
port and credentials into compose/install.sh.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- Seed Avery 5160/5167 and Demco spine templates plus Spine, Spine+barcode
and Barcode-only layouts (Dewey call number split across lines)
- Default 942$c (item type) to BOOK so a Z39.50 import saves without edits
- Enable Plack includes in addition to the plain ones (static files/rewrites)
- Swap Apache from mpm_itk to prefork after koha-create (itk can't switch
users in a container and returned HTTP 500 on every page)
- Drop unneeded SETUID/SETGID caps
Verified live: staff login, Z39.50 search/import against a local server,
save + auto barcode, label PDF with Dewey spine + barcode.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- koha-shell -c wraps the command in single quotes, which broke the env
assignments; use runuser + env instead and verify tables were created
- koha-create requires mpm_itk, so keep it and grant the caps it needs
- stop any Apache started by koha-create before running it in foreground
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
- Run Apache as the instance user instead of mpm_itk AssignUserID
- Add Listen for the staff port; start zebra/indexer/worker/plack explicitly
- Tolerate koha-plack --enable returning non-zero under set -e
- install.sh: only fail on a crash loop, not a single restart
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
Compose stack (MariaDB + Koha built from the Koha project's Debian
packages), headless bootstrap replacing the web installer, seeded item
types and LoC Z39.50 server, and an install.sh that installs Docker,
generates passwords and waits for the site. End-to-end testing in progress.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
Unattended installs on an --offline ISO cap the online attempt (default 40 min)
and finish from the bundle on timeout or failure. Interactive retry menu gains
[O] to install from the bundle. NodeSource curl gets timeouts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4
kiosk-wifi-helper gains region-status/region-set. The country is persisted as
a cfg80211 regdom module option and in the WiFi netplan file; locale and
keyboard go through localectl, validated against what's installed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4
Without a country Linux uses the restrictive world domain: networks show up
in scans but the adapter can't join them. The menu asks (default from the
existing config or system locale); the web helper uses the same default.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4
- Menu asks which adapter when several exist (shows USB/PCI); the web UI
helper prefers the adapter already configured.
- Stale-entry cleanup also marks remaining installer wifis optional so a
dead WiFi entry can't hold up boot.
- After a failed connect, read wpa_supplicant's log to say wrong password /
network not found / rejected.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4
netplan apply waits ~90s for every wifis: interface in every file and then
fails if one doesn't exist (e.g. installer config naming wlan0 while the
adapter is wlp1s0). Remove such entries (backup kept as *.kiosk-bak) before
applying, in both the menu and kiosk-wifi-helper.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4
The WiFi screen now opens with Ctrl+Alt+Super+W, which is easier to
press than the five-key chord. The Shift+W+S+F+H chord and
Ctrl+Alt+Shift+Super+W are removed. Fn isn't part of the combo: most
keyboards never send it to the computer.
With password protection on, the screen asks for the unlock code: the
PIN pad for a PIN, a password field for a password. It shares the
lock screen's wrong-code slowdown. With protection off, it falls back
to the hidden-sites PIN as before. The lock screen's check now goes
through the same checkCode() helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
.jitsi-pin now holds "sha256:<hex>" (or NOPIN) instead of the digits.
The menu writes the hashed form. A plain-digits file still works and
is converted by the kiosk app at startup.
The PIN is now checked in the main process instead of the PIN pad's
renderer, using the lock screen's slowdown, now factored out
(createCodeThrottle): 5 wrong in a row refuse entry for 30s, doubling
up to 5 minutes. The F10 PIN pad and the WiFi screen share one count.
The WiFi screen's close-after-5-tries is replaced by this. The PIN pad
no longer shows the default PIN on screen, and the menu no longer
echoes the PIN as it's typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
The unlock code and the boot lock code can each be a password or a
4-8 digit PIN. A PIN gets an on-screen number pad, so the lock works on
a touch-only kiosk. The boot lock can use the unlock code or its own.
These are the kiosk's own codes, set in Core Settings -> Password
Protection & Lockout or on the Web UI's Lockout page, never the admin
(sudo) password.
The lock screen moves from an inline data: URL to lockscreen.html. The
code is now hashed and compared in the main process, which also slows
down guessing: after every 5 wrong codes in a row, input is refused for
30s, doubling up to 5 minutes. Existing configs behave as before:
missing fields mean a password, and the same code at boot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
With the virtual consoles turned off there was no way to join a WiFi
network at the kiosk itself. Holding Shift+W+S+F+H together (or
Ctrl+Alt+Shift+Super+W) now opens a PIN-protected WiFi screen: scan,
pick or type a network, enter the password, connect.
The root side is a small helper (kiosk-wifi-helper: status, scan,
connect only) that the kiosk user may run through a validated
sudoers.d rule. It writes the same netplan file as Core Settings ->
WiFi and puts the previous one back if the new network gives no
address. Installed by provisioning and Advanced -> Upgrade, removed by
Complete Uninstall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Easy2Boot's UEFI menu returned straight to the menu for the new kiosk ISO
(a 3.5 GB copy on a well-used drive). Some multiboot modes need the ISO in
one unbroken piece, and Linux has no defragmenter for NTFS/FAT32/exFAT -
Easy2Boot's own Linux helpers are a 32-bit udefrag needing i386 libraries
and a FAT32-only perl script.
e2b-contig.sh rewrites just the one file, using only ordinary file
operations - never filesystem structures - so the worst case is "still
not contiguous", never a damaged drive:
1. fill the drive's free space with 4 MB filler files (plugs every gap;
a plain copy, or fallocate where it exists at all, just fills the
gaps again - measured on ntfs-3g: 7-8 pieces either way);
2. map the fillers (filefrag -e, which also works on ntfs-3g via FIBMAP)
and find the longest physically unbroken run of single-piece ones;
3. free just enough of that run and write the copy - the only free space
left - then delete the fillers;
4. verify the copy's SHA-256 against the original and rename it into
place (same directory, so the data doesn't move).
If there's no run big enough while the original is on the drive,
--move-out parks it on this computer (checksummed), retries, and puts it
back verified if that still fails. --check only reports. Interrupts and
failures remove fillers and temp copies.
Tested on NTFS images through ntfs-3g: a 60 MB file in 7 pieces became 1
piece with identical content, no leftovers, free space unchanged (27 s for
~900 MB free); drives with no big enough free run (with and without
--move-out) report so and leave the original byte-identical, parked copy
cleaned up. FAT32/exFAT through their FUSE drivers can't report layout,
which the tool detects and stops on, naming the kernel driver to remount
with (vfat/exfat/ntfs3); the kernel drivers aren't available in the test
environment, so those paths are untested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Reported: Core Settings -> WiFi "just returns to the core settings menu".
run_menu clears the screen the moment a handler returns, and every early
exit in wifi_menu returned without a pause - so whatever went wrong
(no adapter found, interface wouldn't come up, ...) was wiped before it
could be read.
- wifi_menu now runs the wizard (renamed wifi_configure) and always
pauses afterwards, success or failure.
- Adapter detection by type (/sys/class/net/*/wireless or phy80211),
falling back to wl* - not every adapter is named wl*. When none is
found, lists the interfaces that do exist.
- Clears a software rfkill block via sysfs before `ip link set up`
(rfkill isn't installed on a server); warns on a hard block; shows
ip's actual error if bringing the link up still fails.
- Writes its own $NETPLAN_DIR/60-kiosk-wifi.yaml instead of overwriting
the first *.yaml found - on a fresh install that's the installer's
50-cloud-init.yaml with the wired config, which it silently dropped.
- SSID/password written as YAML single-quoted scalars: a password with
" or \ produced invalid YAML before.
- Success = the WiFi interface itself has an IPv4 address (waits up to
30s), not "the machine has some IP". The SSH revert watchdog uses the
same test instead of pinging 8.8.8.8, which on a network without
internet reverted every working connection; with no previous WiFi file
it reverts by removing ours.
Tested with ip/netplan/sudo stubbed and netplan files in a scratch dir:
installer's wired file untouched, own file written, SSID
`Bob's "Café" #2` and password `p@ss"wo'rd\\:#x` round-trip through a
YAML parser exactly, no-address case reports instead of claiming
success, early errors stay on screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Reported on a 26.04 offline install, first-boot step 4:
chown: cannot dereference '/home/kiosk/kiosk-app/*.js': No such file or directory
(same for *.mjs, *.html). `sudo chown ... "$KIOSK_DIR"/*.js` has the glob
expanded by the calling (admin) shell before sudo runs; the kiosk user's
home is created mode 750, so the admin can't list it, the pattern stays
literal, and chown fails - stopping the install under set -e.
- provision_install_app: chown the copied top-level files with
`sudo find "$KIOSK_DIR" -maxdepth 1 -type f -exec chown ...` (root does
the listing). Top level only, so chrome-sandbox inside node_modules
keeps its root ownership.
- Swept lib/ and menus/ for the same class of bug: Asterisk Intercom's
baresip_is_installed and status read $KIOSK_HOME/.baresip/accounts
without sudo, so they always reported "not configured". Now sudo test
/ sudo head. Everything else that touches the kiosk home (config.json,
kiosk-app checks) already goes through sudo -u kiosk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Reported on a 26.04 offline install, first-boot step 2:
useradd: group 'netdev' does not exist
useradd refuses to create the user at all when any -G group is missing,
which stopped provisioning. Ubuntu 26.04 Server's base system has audio,
video, input and plugdev but no netdev (checked against the 26.04.1
ISO's /etc/group).
provision_create_kiosk_user now `groupadd -f -r`s each supplementary
group first (a no-op when it exists), then passes the same list to
useradd.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
A default Ubuntu Server 26.04 install only gets wpasupplicant/iw when
WiFi is configured in the installer's network screen (they're not in the
server manifest otherwise). Without them netplan can't join a WiFi
network and Core Settings -> WiFi stops at "No WiFi tools found" - which
is exactly the offline-first case: install with no network, then use the
kiosk's WiFi menu to connect.
- Add wpasupplicant and iw to PROVISION_APT_PACKAGES, so first boot
installs them in step 1 (from the offline bundle when there's no
internet) - before Core Settings -> WiFi is offered. +1.6 MB in the
bundle.
- wifi_menu installs them itself (via run_with_offline_fallback) if
they're missing, instead of erroring with an apt command to run.
Bundle rebuilt: contains wpasupplicant and iw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Reported: installed from the ISO, files present, but the kiosk setup
never appeared on first boot.
Cause: kiosk-firstboot.service had After=cloud-final.service, and on 26.04
cloud-final.service is After=multi-user.target - while multi-user.target,
which wants kiosk-firstboot, is implicitly After= it. An ordering cycle;
systemd breaks it at boot by deleting a job, and kiosk-firstboot's was
the one to go. systemd-analyze verify against the 26.04.1 ISO's own units
reproduces it:
Found ordering cycle: kiosk-firstboot.service/start after
cloud-final.service/start after multi-user.target/start - after
kiosk-firstboot.service
- Drop After=cloud-final.service. The script's `cloud-init status --wait`
goes too: cloud-final waits for multi-user.target, which waits for this
service - it would deadlock. DPkg::Lock::Timeout already covers what it
was guarding against (dpkg lock contention).
- Drop Conflicts=getty@tty1.service: getty.target pulls getty@tty1 into
the same boot transaction, and a conflict between two wanted jobs is
also resolved by dropping one. Before=getty@tty1 alone keeps tty1's
login prompt waiting until this oneshot finishes.
Verified: systemd-analyze verify of kiosk-firstboot + multi-user.target +
cloud-final + cloud-init.target + getty@tty1 + graphical.target in the
26.04.1 server filesystem - old unit reports the cycle, fixed unit is
clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
Offline bundle now carries every addon that works on a network without
internet: LMS (Lyrion's own .deb, from the addon's LMS_DEB_URL) +
Squeezelite, Asterisk Intercom (baresip), VNC, WireGuard and the
Emergency Hotspot packages - each list in a variable next to the code
that installs it (SQUEEZELITE_APT_PACKAGES, ASTERISK_INTERCOM_APT_PACKAGES,
VNC_APT_PACKAGES, WIREGUARD_APT_PACKAGES, EMERGENCY_HOTSPOT_APT_PACKAGES),
read by build-bundle.sh. Each install goes through
run_with_offline_fallback like CUPS, so they install offline from the
normal menus / Web UI. LMS installs as a bundle package when offline
(lms_offline_install_wanted); its online repo/direct-download paths are
unchanged. Tailscale/Netbird deliberately not bundled (vendor scripts,
vendor servers). Bundle: 524 MB (+87 MB).
Emergency hotspot: the boot check pinged only 8.8.8.8, so on a LAN with
no internet the hotspot started on every boot. Now chosen at setup:
"network" (default - router or internet must answer; a router that
ignores ping still counts if it answered ARP), "internet" (old check),
or "host" (a specific address). Three tries 10s apart. Existing scripts
keep the old check until Reconfigure.
New Core Settings -> Time Server (NTP) (menus/time_sync.sh): point
systemd-timesyncd at LAN time server(s) via a timesyncd.conf.d drop-in,
back to Ubuntu's defaults, or set the clock by hand (and the RTC) for a
network with no time server. Complete Uninstall resets it.
Also: lms_service_name used `systemctl list-unit-files | grep -q`, the
same pipefail/SIGPIPE pattern as the earlier cups_is_installed bug; now
`systemctl cat`.
Tested in a network-less chroot of the 26.04.1 server filesystem: kiosk
base, then action_wireguard_install and action_install_lms (real
functions) and the Squeezelite/Intercom/VNC/hotspot package sets all
installed from the bundle, apt left on normal sources after each.
Hotspot script generated for all three modes (plus an invalid address,
which falls back to "network") and its check run against simulated
networks: LAN-only with router answering ping or only ARP -> no hotspot
in "network" mode; nothing reachable -> hotspot. Time Server actions
driven with timedatectl stubbed: drop-in contents, input validation,
manual set-time sequence, reset removes the drop-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
PDFs in a slideshow folder now play alongside images and Word documents:
a one-page PDF is shown whole, a longer one fits the width and scrolls
through its pages during docInterval (first 30 pages - rendering a huge
PDF for a few seconds on screen isn't worth the time and memory).
- kiosk-app/pdf-render.mjs renders pages to canvases with pdf.js
(pdfjs-dist ^6.3.289, new kiosk-app dependency): no viewer toolbar, and
the slideshow's existing auto-scroll works on it. pdf.js >= 4.2.67 is
required (CVE-2024-4367: older versions could run script embedded in a
crafted PDF); isEvalSupported:false too. It's a module because pdf.js
only ships as one - confirmed to load (with its worker) from file:// in
Electron 44.
- Hands pdf.js a copy of the bytes: it transfers the buffer it's given,
which would break the cached copy the next time round.
- slideshow.js lists .pdf as a document type; the title-request handler
serves any listed non-image item (still only from that folder).
- provision_install_app now copies kiosk-app/*.mjs as well - without it
pdf-render.mjs would never reach an installed kiosk.
Tested with the real main.js on Electron 44.5.1 under Xvfb + Openbox,
app files copied with provision_install_app's own glob: a photo, a
one-page PDF, a three-page PDF and a .docx played in order, twice round;
the one-page PDF shown whole, the three-page one scrolled through all
pages. Offline bundle rebuilt with pdfjs-dist (437 MB total, +22 MB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm