Files
senior-kiosk/CLAUDE.md
T

7.2 KiB

SeniorNet Kiosk — Project Context for AI Assistants

What this project is

A locked-down, zero-maintenance browser workstation for elderly users. The senior never sees a desktop — only a full-screen Chromium window with a slim toolbar on top. No configuration, no maintenance required from the user.

Architecture

┌─────────────────────────────────────────────────────┐
│  PyQt5 toolbar (72px, always-on-top, FramelessHint) │
│  [SeniorNet]  [◀] [▶] [⌂] [☰ bookmarks]            │
├─────────────────────────────────────────────────────┤
│                                                     │
│           Chromium --app mode                       │
│       (CDP port 9222 for navigation)                │
│                                                     │
└─────────────────────────────────────────────────────┘
  • browser.py — main app. Launches both the PyQt5 toolbar and Chromium as a subprocess.
  • Toolbar talks to Chromium via Chrome DevTools Protocol (CDP) over WebSocket on port 9222.
  • Bookmarks stored in bookmarks.json (nested JSON, supports folders).
  • Home screen: /home/jgitta/kiosk-home/index.html

Machines

kiosk-dev — 192.168.88.48 (SSH alias: kiosk-dev) ← ACTIVE DEV MACHINE

  • Debian 13 Trixie, Proxmox VM 130 on jg-hud
  • System Chromium: /usr/lib/chromium/chromium — NOT snap
  • Repo at: /home/jgitta/kiosk-browser/
  • This is the machine being actively developed and tested on

Production kiosk — 192.168.88.44 (SSH alias: kiosk)

  • Ubuntu 24.04 + snap Chromium — needs to be migrated to Debian to match kiosk-dev
  • Currently offline / pending migration

Deploy workflow

bash /home/jgitta/kiosk-browser/deploy.sh "describe your change"

This: commits + pushes to Gitea, restarts kiosk-dev immediately, and deploys to production if reachable.

CRITICAL: kiosk-restart must kill Chromium (pkill -f "remote-debugging-port=9222") BEFORE killing browser.py. If only Python is killed, Chromium holds the SingletonLock and the new instance silently exits — deploys appear to work but nothing changes on screen.

/usr/local/bin/kiosk-restart on kiosk-dev:

pkill -f "remote-debugging-port=9222" || true
pkill -f "python3.*browser" || true
sleep 1

Key files

File Purpose
browser.py Main application — toolbar + CDP + Chromium launcher
bookmarks.json Nested bookmark structure
home-screen-index.html Home screen source (deploy copies to /home/jgitta/kiosk-home/index.html)
deploy.sh One-command deploy
/home/jgitta/kiosk-home/index.html Senior home screen (live copy, outside repo)
~/.config/openbox/autostart Auto-restart loop on crash
/usr/local/bin/kiosk-restart Safe restart (kills Chromium + Python)
/etc/X11/xorg.conf.d/50-kiosk-hardening.conf Blocks TTY switching

Gitea repo (see also jgitta/homelab-configs)

Source of truth for infra: https://gitea.jgitta.com/jgitta/homelab-configs

Piece Detail
Gitea app Docker on siklos 192.168.88.27 — HTTP 3002, SSH 2222 (not 22)
Public URL gitea.jgitta.comCaddy 192.168.88.110 → proxies to Gitea :3002
DNS MikroTik 192.168.88.1 / siklos; public DNS may resolve to WAN IP (still reaches Caddy :443)

This repo (senior-kiosk):

  • HTTPS (usual — what worked before): https://gitea.jgitta.com/jgitta/senior-kiosk.git — through Caddy. Auth via Gitea username + access token (git config credential.helper store), or token in URL (legacy on kiosk-dev).
  • SSH: Gitea SSH is on port 2222, not 22. Plain git@gitea.jgitta.com:... hits port 22 on the WAN and will not accept your Gitea password. Use either:
    • ssh://git@192.168.88.27:2222/jgitta/senior-kiosk.git (LAN), or
    • ~/.ssh/config: Host gitea.jgitta.comPort 2222 (requires WAN/LAN port-forward 2222→siklos:2222 if using the hostname)
  • Branch: master
  • Deploy: bash deploy.sh "message" — push, sync home screen, kiosk-restart
  • Secrets: credentials.md gitignored

Roundcube webmail

  • URL: https://mail.jgitta.com/roundcube/ (proxied via Caddy at 192.168.88.110)
  • LXC container 203 on jg-hud, IP: 192.168.88.45
  • Ubuntu 24.04, Apache2, PHP, MariaDB, Roundcube 1.6.6
  • Gmail IMAP: ssl://imap.gmail.com:993
  • Autologin plugin: /var/lib/roundcube/plugins/autologin/autologin.php
  • Logs: /var/lib/roundcube/logs/autologin.log and errors.log
  • Run commands on container: ssh jg-hud "pct exec 203 -- bash -c '...'" (jg-hud is the Proxmox host)
  • browser.py clears the Roundcube session cookie on every startup (forces fresh autologin)

Known hard-won lessons

Chromium

  • --no-sandbox causes "unsupported flag" infobar — never use it on Debian system Chromium
  • --test-type triggers Google CAPTCHA bot detection — never use it
  • --disable-blink-features=AutomationControlled causes "unsupported flag" infobar — removed
  • --disable-infobars, --disable-session-crashed-bubble, --disable-save-password-bubble, --suppress-message-center-popups, --disable-translate are all deprecated/removed in current Chromium — do not use
  • Profile dir: /home/jgitta/chromium-kiosk (not hidden — snap couldn't write hidden dirs; kept this convention)
  • Chromium adds its own flags via the Debian wrapper (--show-component-extension-options etc.) — ignore these, they're harmless

PyQt5 / Openbox

  • QMenu does NOT work under Openbox/X11 with Qt.Tool | WindowStaysOnTopHint parent — use a custom QWidget with the same flags instead (BookmarkPanel)
  • Qt event filters only see clicks on Qt widgets — clicks that land on Chromium (separate X11 process) are invisible. Use a QTimer polling QApplication.mouseButtons() + QCursor.pos() to detect outside clicks.
  • To prevent toolbar minimize: override changeEvent, detect Qt.WindowMinimized, call QTimer.singleShot(50, self._self_restore)
  • To restore minimized Chromium: periodic QTimer + xprop -id <wid> _NET_WM_STATE to check _NET_WM_STATE_HIDDEN, then xdotool windowmap <wid>

Roundcube autologin

  • Plugin MUST call both session->regenerate_id(false) AND session->set_auth_cookie() after login
  • Logs are in /var/lib/roundcube/logs/ NOT /var/log/roundcube/

Deployment

  • pkill -f browser.py in SSH kills its own session (shell cmdline contains "browser.py") — use kiosk-restart
  • Chromium must be killed before Python or SingletonLock prevents new instance from starting

Infrastructure overview

  • Proxmox host: jg-hud (main), siklos (192.168.88.27)
  • TrueNAS: 192.168.88.24
  • Thinkstation (dev desktop): 192.168.88.41
  • Caddy reverse proxy: 192.168.88.110 — terminates HTTPS for *.jgitta.com (including gitea.jgitta.com → Gitea :3002)
  • Gitea: behind Caddy at gitea.jgitta.com; app listens on port 3002 on the Gitea host
  • Headscale: self-hosted Tailscale on siklos