From 8553705e47a558bd664d4de0b1a70677f4fb7f2d Mon Sep 17 00:00:00 2001 From: Joe Gitta Date: Fri, 22 May 2026 21:39:07 +0000 Subject: [PATCH] Fix bookmarks menu dismiss, home Email URL, and deploy hygiene. Close the hamburger panel on outside click/focus via X11 (xinput/xdotool) without a fullscreen overlay; point home Email to Roundcube; sync home-screen-index.html in deploy/install; remove credentials.md from git and document private LAN-only Gitea usage. Co-authored-by: Cursor --- .cursorrules | 8 ++- .gitignore | 1 + CLAUDE.md | 12 +++-- browser.py | 114 +++++++++++++++++++++++++++++++++++------ credentials.md | 11 ---- credentials.md.example | 10 ++++ deploy.sh | 12 +++++ home-screen-index.html | 3 +- install.sh | 7 ++- 9 files changed, 143 insertions(+), 35 deletions(-) delete mode 100644 credentials.md create mode 100644 credentials.md.example diff --git a/.cursorrules b/.cursorrules index a40bda4..acec1e6 100644 --- a/.cursorrules +++ b/.cursorrules @@ -64,9 +64,13 @@ sleep 1 | `/usr/local/bin/kiosk-restart` | Safe restart (kills Chromium + Python) | | `/etc/X11/xorg.conf.d/50-kiosk-hardening.conf` | Blocks TTY switching | -## Gitea repo +## Gitea repo (private LAN only) -`https://gitea.jgitta.com/jgitta/senior-kiosk` — master branch +- **URL**: `git@gitea.jgitta.com:jgitta/senior-kiosk.git` (SSH) or `https://gitea.jgitta.com/jgitta/senior-kiosk.git` (HTTPS) +- **Branch**: `master` +- **Exposure**: Internal only — LAN/VPN, not public internet. Never embed tokens in `git remote` URLs. +- **Deploy**: `bash deploy.sh "message"` — push to Gitea, sync home screen, `kiosk-restart`. +- **Secrets**: `credentials.md` is gitignored. ## Roundcube webmail diff --git a/.gitignore b/.gitignore index 9460c99..6157bf4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc *.pyo .env +credentials.md diff --git a/CLAUDE.md b/CLAUDE.md index a40bda4..ece012a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,15 +58,21 @@ sleep 1 |------|---------| | `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 | +| `/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 +## Gitea repo (private LAN only) -`https://gitea.jgitta.com/jgitta/senior-kiosk` — master branch +- **URL**: `git@gitea.jgitta.com:jgitta/senior-kiosk.git` (SSH) or `https://gitea.jgitta.com/jgitta/senior-kiosk.git` (HTTPS) +- **Branch**: `master` +- **Exposure**: Internal only — `gitea.jgitta.com` resolves on LAN/VPN (Caddy at 192.168.88.110). Not public internet. +- **Auth**: SSH key preferred (`git@gitea.jgitta.com:jgitta/senior-kiosk.git`) once the machine’s public key is in Gitea → Settings → SSH Keys. Until then use HTTPS without a token in the URL and `git config credential.helper store`. Rotate any token that was ever in a remote URL. +- **Deploy**: `bash deploy.sh "message"` — push to Gitea, sync home screen, `kiosk-restart`; production via SSH on 192.168.88.x / Tailscale. +- **Secrets**: `credentials.md` is gitignored; Roundcube passwords live on the mail server, not in this repo. ## Roundcube webmail diff --git a/browser.py b/browser.py index e2db327..e6e2aaf 100755 --- a/browser.py +++ b/browser.py @@ -5,7 +5,7 @@ Navigation: ◀ ▶ ⌂ icon buttons + ☰ hamburger menu for bookmarks. Chrome is controlled via Chrome DevTools Protocol (CDP) over WebSocket. """ import sys, json, os, subprocess, time, threading, urllib.request -from PyQt5.QtCore import Qt, QTimer, QPoint +from PyQt5.QtCore import Qt, QTimer, QPoint, QRect from PyQt5.QtGui import QFont, QColor, QPalette from PyQt5.QtWidgets import ( QApplication, QWidget, QHBoxLayout, QVBoxLayout, @@ -377,6 +377,43 @@ class ManagerDialog(QDialog): self.tree.setCurrentItem(sel); self._commit() +# ── X11 helpers (clicks/focus on Chromium never reach Qt) ─────────── +def _x11_env(): + env = os.environ.copy() + env.setdefault("DISPLAY", ":0") + return env + + +def _x11_run(cmd, timeout=0.4): + try: + r = subprocess.run( + cmd, capture_output=True, text=True, + timeout=timeout, env=_x11_env()) + return r.stdout.strip() if r.returncode == 0 else "" + except Exception: + return "" + + +def _x11_active_window_id(): + out = _x11_run(["xdotool", "getactivewindow"]) + return int(out) if out.isdigit() else 0 + + +def _x11_left_button_down(): + """True when the physical left mouse button is held (works across all apps).""" + try: + r = subprocess.run( + ["bash", "-c", + "DISPLAY=:0 dev=$(xinput --list --short 2>/dev/null | " + "awk '/slave.*pointer/ {print $2; exit}'); " + "[ -n \"$dev\" ] && xinput query-state \"$dev\" 2>/dev/null | " + "grep -q 'button\\[1\\]=down'"], + timeout=0.3, env=_x11_env()) + return r.returncode == 0 + except Exception: + return False + + # ── Bookmark dropdown panel ─────────────────────────────────────── class BookmarkPanel(QWidget): """ @@ -384,10 +421,15 @@ class BookmarkPanel(QWidget): Uses the same Qt.Tool | WindowStaysOnTopHint flags as the toolbar so it always appears above Chrome, unlike QMenu which has z-order issues. """ + _OPEN_GRACE_SEC = 0.35 + def __init__(self, cdp, overlay): super().__init__() self.cdp = cdp self.overlay = overlay + self._shown_at = 0.0 + self._focus_at_open = 0 + self._our_window_ids = set() self.setWindowFlags( Qt.Tool | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_ShowWithoutActivating) @@ -403,10 +445,8 @@ class BookmarkPanel(QWidget): """) QApplication.instance().installEventFilter(self) - # Timer: hide panel when user clicks anywhere outside it - # (clicks on Chromium never reach Qt's eventFilter) - self._click_guard = QTimer() - self._click_guard.timeout.connect(self._check_outside_click) + self._dismiss_timer = QTimer() + self._dismiss_timer.timeout.connect(self._check_dismiss) def rebuild(self, items): # Remove old layout @@ -459,6 +499,19 @@ class BookmarkPanel(QWidget): self.resize(pw, min(ph, max_h)) self.move(SCREEN_W - pw - 4, TOOLBAR_H) + def _refresh_our_window_ids(self): + ids = set() + for w in (self, self.overlay): + wid = int(w.winId()) + if wid: + ids.add(wid) + self._our_window_ids = ids + + def _burger_global_rect(self): + btn = self.overlay.burger_btn + origin = btn.mapToGlobal(QPoint(0, 0)) + return QRect(origin, btn.size()) + def _add_items(self, vbox, items, depth): indent = " " * depth for item in items: @@ -484,28 +537,53 @@ class BookmarkPanel(QWidget): self.hide() self.overlay.open_manager() + def hide(self): + self._dismiss_timer.stop() + super().hide() + def setVisible(self, visible): super().setVisible(visible) if visible: - self._click_guard.start(100) + self._shown_at = time.time() + self._refresh_our_window_ids() + self._focus_at_open = _x11_active_window_id() + self.raise_() + self.overlay.raise_() + self._dismiss_timer.start(100) else: - self._click_guard.stop() + self._dismiss_timer.stop() - def _check_outside_click(self): - """Hide panel when a click lands outside it — even on the Chromium window.""" + def _cursor_outside_panel(self): from PyQt5.QtGui import QCursor + pos = QCursor.pos() + if self.frameGeometry().contains(pos): + return False + if self._burger_global_rect().contains(pos): + return False + return True + + def _check_dismiss(self): + """Close when focus leaves or user clicks outside the panel.""" if not self.isVisible(): - self._click_guard.stop() + self._dismiss_timer.stop() return - if QApplication.mouseButtons() & Qt.LeftButton: - if not self.geometry().contains(QCursor.pos()): - self.hide() + if time.time() - self._shown_at < self._OPEN_GRACE_SEC: + return + + # Click outside (X11 — works even when Chromium has focus) + if _x11_left_button_down() and self._cursor_outside_panel(): + self.hide() + return + + # Focus moved away from toolbar / menu (e.g. clicked the web page) + active = _x11_active_window_id() + if active and active != self._focus_at_open and active not in self._our_window_ids: + self.hide() def eventFilter(self, obj, event): from PyQt5.QtCore import QEvent if event.type() == QEvent.MouseButtonPress and self.isVisible(): - gp = event.globalPos() - if not self.geometry().contains(gp): + if not self.frameGeometry().contains(event.globalPos()): self.hide() return False @@ -549,6 +627,9 @@ class KioskOverlay(QWidget): SCREEN_W, SCREEN_H = w, h self.setGeometry(0, 0, SCREEN_W, TOP_H) self.resize(SCREEN_W, TOP_H) + if self.bm_panel.isVisible(): + pw = self.bm_panel.width() + self.bm_panel.move(SCREEN_W - pw - 4, TOOLBAR_H) # Resize Chrome after brief delay so X has settled QTimer.singleShot(500, self._resize_chrome) @@ -634,8 +715,7 @@ class KioskOverlay(QWidget): self.bm_panel.hide() else: self.bm_panel.rebuild(load_bm()) - self.bm_panel.show() - self.bm_panel.raise_() + self.bm_panel.setVisible(True) def open_manager(self): d = ManagerDialog(self) diff --git a/credentials.md b/credentials.md deleted file mode 100644 index 6cf0b5c..0000000 --- a/credentials.md +++ /dev/null @@ -1,11 +0,0 @@ -# Senior Kiosk — Credentials - -> **Keep this file private.** Do not share publicly. - -## Gmail App Password (for Roundcube IMAP) - -- **Account**: Gmail account used on the kiosk -- **Purpose**: Roundcube webmail IMAP/SMTP access -- **App Password**: `zrhe ncfw zvgb qqfs` -- **Created**: 2026-05-21 -- **Note**: Spaces are part of the display only — enter without spaces when configuring Roundcube: `zrhencfwzvgbqqfs` diff --git a/credentials.md.example b/credentials.md.example new file mode 100644 index 0000000..b1b4acc --- /dev/null +++ b/credentials.md.example @@ -0,0 +1,10 @@ +# Senior Kiosk — Credentials (template) + +> Copy to `credentials.md` (gitignored). Never commit real secrets. + +## Gmail App Password (for Roundcube IMAP) + +- **Account**: your-gmail@example.com +- **Purpose**: Roundcube webmail IMAP/SMTP access +- **App Password**: (create in Google Account → Security → App passwords) +- **Note**: Configure on the Roundcube server only; do not commit to git. diff --git a/deploy.sh b/deploy.sh index 79cae63..20fc9d2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,8 +7,19 @@ KIOSK_LOCAL="jgitta@192.168.88.44" KIOSK_REMOTE="jgitta@100.64.0.1" REPO_DIR="$(cd "$(dirname "$0")" && pwd)" +HOME_SRC="$REPO_DIR/home-screen-index.html" +HOME_DST="/home/jgitta/kiosk-home/index.html" COMMIT_MSG="${1:-deploy: update from kiosk-dev $(date +%Y-%m-%d)}" +echo "==> Syncing home screen to $HOME_DST..." +if [ -f "$HOME_SRC" ]; then + mkdir -p "$(dirname "$HOME_DST")" + cp "$HOME_SRC" "$HOME_DST" + chown jgitta:jgitta "$HOME_DST" 2>/dev/null || true +else + echo " WARNING: $HOME_SRC missing — home screen not updated" +fi + echo "==> Committing and pushing to Gitea..." git -C "$REPO_DIR" add -A git -C "$REPO_DIR" commit -m "$COMMIT_MSG" 2>/dev/null || echo " (nothing new to commit)" @@ -33,6 +44,7 @@ if [ -n "$KIOSK" ]; then ssh "$KIOSK" " cd /home/jgitta/kiosk-browser git pull + cp home-screen-index.html /home/jgitta/kiosk-home/index.html /usr/local/bin/kiosk-restart " echo " Production updated." diff --git a/home-screen-index.html b/home-screen-index.html index d5c3432..1fcd9cb 100644 --- a/home-screen-index.html +++ b/home-screen-index.html @@ -1,4 +1,5 @@ + @@ -96,7 +97,7 @@
- +
✉️
Email
diff --git a/install.sh b/install.sh index 653468a..ae93f74 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -e echo "=== Installing packages ===" apt-get update apt-get install -y python3-pyqt5 unclutter xorg openbox lightdm \ - fonts-noto-color-emoji python3-pip + fonts-noto-color-emoji python3-pip xinput xdotool # Install websocket-client pip3 install websocket-client --break-system-packages @@ -38,6 +38,11 @@ mkdir -p /etc/X11/xorg.conf.d cp "$SCRIPT_DIR/system-config/xorg-50-kiosk-hardening.conf" \ /etc/X11/xorg.conf.d/50-kiosk-hardening.conf +# Home screen (source in repo → served outside repo at file:///home/jgitta/kiosk-home/) +mkdir -p /home/jgitta/kiosk-home +cp "$SCRIPT_DIR/home-screen-index.html" /home/jgitta/kiosk-home/index.html +chown jgitta:jgitta /home/jgitta/kiosk-home/index.html + # Disable unused virtual consoles for i in 2 3 4 5 6; do systemctl disable getty@tty$i 2>/dev/null || true