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:
+6
-2
@@ -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
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
credentials.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
|
||||
|
||||
|
||||
+96
-16
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user