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 <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 21:39:07 +00:00
parent b99f058bfb
commit 8553705e47
9 changed files with 143 additions and 35 deletions
+6 -2
View File
@@ -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
+1
View File
@@ -2,3 +2,4 @@ __pycache__/
*.pyc
*.pyo
.env
credentials.md
+9 -3
View File
@@ -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 machines 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
+96 -16
View File
@@ -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()):
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)
-11
View File
@@ -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`
+10
View File
@@ -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.
+12
View File
@@ -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."
+2 -1
View File
@@ -1,4 +1,5 @@
<!DOCTYPE html>
<!-- Source for /home/jgitta/kiosk-home/index.html — copied by deploy.sh and install.sh -->
<html lang="en">
<head>
<meta charset="UTF-8">
@@ -96,7 +97,7 @@
<div class="grid">
<a class="btn btn-gmail" href="https://mail.google.com">
<a class="btn btn-gmail" href="https://mail.jgitta.com/roundcube/">
<div class="icon">✉️</div>
Email
</a>
+6 -1
View File
@@ -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