Compare commits

..

43 Commits

Author SHA1 Message Date
jgitta 2969648aa1 deploy: fix scroll jump when closing previous story 2026-05-27 14:31:24 +00:00
jgitta e727ed35a9 deploy: adjust news story scrolling to headline top 2026-05-27 14:29:31 +00:00
jgitta 999dda22df deploy: smooth scroll newspaper focus 2026-05-27 14:27:15 +00:00
jgitta 4c474c0916 deploy: secure 100% paywall-free direct-link news 2026-05-27 14:24:49 +00:00
jgitta 585b7f2639 deploy: replace paywalled NYT with AP and CNBC 2026-05-27 14:24:16 +00:00
jgitta f02ac40b91 deploy: randomize and shuffle top RSS feeds 2026-05-27 14:21:30 +00:00
jgitta abe61d1156 deploy: cache and render source name for news items 2026-05-27 14:19:12 +00:00
jgitta 79b6d4857d deploy: increase accordion height and add premium scrollbar 2026-05-27 14:14:09 +00:00
jgitta 598bd83887 deploy: filter global navs from RSS news cache 2026-05-27 14:12:36 +00:00
jgitta 6a4eee9a15 feat: upgrade local newspaper to full-text and images 2026-05-27 14:12:08 +00:00
jgitta 4b1ea2da11 feat: add local ad-free newspaper 2026-05-27 14:09:15 +00:00
jgitta 0d31af526c deploy: integrate emoji font to first boot installer 2026-05-27 14:00:21 +00:00
jgitta 3d8aef5f22 deploy: accurate waterloo ia geolocation 2026-05-27 13:51:41 +00:00
jgitta ac6ca6d1b3 deploy: secure weather caching 2026-05-27 13:44:22 +00:00
jgitta 46cd4f77c9 deploy: weather patch 2026-05-27 13:40:56 +00:00
jgitta 1b06a29e53 feat: add local ad-free weather page 2026-05-27 13:33:44 +00:00
jgitta fd519a64b0 fix: don't pre-create Chromium profile dir in browser.py 2026-05-26 01:10:21 +00:00
jgitta a2c14a4a39 fix: don't pre-create Chromium profile dir — causes profile error on first launch 2026-05-26 01:06:01 +00:00
jgitta ab423ef1a7 fix: sync before reboot to ensure repo survives first-boot restart 2026-05-26 01:03:30 +00:00
jgitta e9ee8caccf fix: shallow clone (--depth=1) to skip 3.5GB of ISO history blobs 2026-05-26 01:00:00 +00:00
jgitta 68dccf958e fix: remove clear command, not needed for silent install 2026-05-26 00:58:07 +00:00
jgitta 0dcfe060e5 fix: robust SSH auth + always pull latest kiosk-setup from Gitea on boot
Replace sed-based PermitRootLogin fix with explicit appends to guarantee
root SSH with password works for debugging. .bash_profile now pulls the
latest install-kiosk.sh from Gitea before running so fixes take effect
without rebuilding the ISO or recreating the VM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:50:00 +00:00
jgitta 86f6334737 fix: replace xprop with x11-utils, use apt websocket-client package
xprop is not a Debian package name — it's in x11-utils. Also replace
pip3 install with python3-websocket from apt (same library, no pip needed).
This fixes the apt failure that prevented lightdm from being installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:46:31 +00:00
jgitta 2431c6b6ae fix: enable root SSH login in preseed-bootstrap for debugging
Allows SSH as root during first-boot setup phase so we can run
commands without relying on the Proxmox console for paste.
install-kiosk.sh already disables this after kiosk setup completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:36:02 +00:00
jgitta 5d3bcb679d fix: remove CDROM apt source before package install
Debian netinstall adds cdrom: to sources.list. With the ISO still
attached in the VM, apt-get update fails on it. Strip it before
running apt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:24:54 +00:00
jgitta 23779690d6 fix: bypass final installer dialog with background reboot in late_command
d-i finish-install/reboot_in_background doesn't suppress the 'Installation
complete, click Continue' dialog in Trixie. Instead, fork a background
reboot from late_command that fires 3s after setup completes, killing the
dialog before it can block. Also removes the redundant reboot_in_background
setting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:15:35 +00:00
jgitta 96838f9f67 fix: fully silent first-boot — no prompts, auto-reboot on completion
Removes all interactive prompts from install-kiosk.sh. Username fixed
to 'kiosk', display name fixed to 'Kiosk'. Reboots automatically after
setup without waiting for user input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:49:03 +00:00
jgitta 05f20355c8 fix: use generic 'kiosk' username, prompt only for senior's name
Removes the username prompt — all installs use 'kiosk' as the system
user. Only the senior's display name is asked during first-boot setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:48:10 +00:00
jgitta 165e77b9b8 fix: use getty auto-login instead of systemd TTY service for first-boot setup
Replaces kiosk-firstboot.service (which got SIGHUP'd by getty) with a
simpler approach: configure getty@tty1 to auto-login root, run kiosk-setup
from .bash_profile. Cleaner TTY ownership — no service/getty conflicts.
install-kiosk.sh now removes the auto-login config and kiosk-setup script
on completion so root doesn't auto-login after the kiosk is running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:36:42 +00:00
jgitta d3b05f981f fix: stop getty@tty1 before first-boot setup service to prevent SIGHUP kill
Conflicts=getty@tty1 ensures systemd releases TTY1 before kiosk-setup
runs interactively. Also add partman/choose_partition select finish to
preseed so disk write confirmation is fully automated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 23:33:20 +00:00
jgitta b73b8fab65 fix: prefer wired interface in preseed to avoid WiFi passphrase prompt 2026-05-24 11:36:09 +00:00
jgitta 19b02f91e9 fix: use Debian 13.5.0 stable ISO to fix kernel module mismatch; gitignore ISOs 2026-05-24 03:21:37 +00:00
jgitta dcdbedc26e feat: auto-increment version on deploy, show version in toolbar 2026-05-24 03:14:39 +00:00
jgitta 508328b282 fix: add isohybrid --uefi post-process to make ISO USB-bootable 2026-05-24 02:25:50 +00:00
jgitta b3bc19aa44 fix: push via internal Gitea URL to bypass Caddy 502 on git-receive-pack 2026-05-23 15:05:08 +00:00
jgitta fa329c7e3c fix: correct xorriso boot params for Trixie netinstall ISO structure 2026-05-23 14:35:24 +00:00
jgitta cb5bf7ff94 feat: generic user, dynamic paths, simplified ISO build pipeline 2026-05-23 14:26:25 +00:00
jgitta 312db58775 refactor: remove dead code, fix home screen config bug 2026-05-23 13:57:35 +00:00
jgitta e9207b4315 fix: configure deploy.sh to push with Gitea token authentication 2026-05-23 13:04:25 +00:00
jgitta b7866000b4 feat: add custom Debian ISO build for automated silent installation 2026-05-23 12:37:31 +00:00
jgitta 38f7ae467a refactor: use config.json with MVC architecture 2026-05-23 02:52:34 +00:00
jgitta c8534c967c Document Gitea layout from homelab-configs (Caddy, port 2222 SSH).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 21:51:54 +00:00
jgitta 8553705e47 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>
2026-05-22 21:39:07 +00:00
20 changed files with 1742 additions and 394 deletions
-113
View File
@@ -1,113 +0,0 @@
# 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
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:
```bash
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 |
| `deploy.sh` | One-command deploy |
| `/home/jgitta/kiosk-home/index.html` | Senior home screen |
| `~/.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
`https://gitea.jgitta.com/jgitta/senior-kiosk` — master branch
## 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 — handles *.jgitta.com
- **Gitea**: gitea.jgitta.com (port 3002 internal)
- **Headscale**: self-hosted Tailscale on siklos
+2
View File
@@ -2,3 +2,5 @@ __pycache__/
*.pyc *.pyc
*.pyo *.pyo
.env .env
credentials.md
iso-output/*.iso
+3
View File
@@ -0,0 +1,3 @@
{
"claudeCodeChat.permissions.yoloMode": true
}
+22 -5
View File
@@ -58,15 +58,32 @@ sleep 1
|------|---------| |------|---------|
| `browser.py` | Main application — toolbar + CDP + Chromium launcher | | `browser.py` | Main application — toolbar + CDP + Chromium launcher |
| `bookmarks.json` | Nested bookmark structure | | `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 | | `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 | | `~/.config/openbox/autostart` | Auto-restart loop on crash |
| `/usr/local/bin/kiosk-restart` | Safe restart (kills Chromium + Python) | | `/usr/local/bin/kiosk-restart` | Safe restart (kills Chromium + Python) |
| `/etc/X11/xorg.conf.d/50-kiosk-hardening.conf` | Blocks TTY switching | | `/etc/X11/xorg.conf.d/50-kiosk-hardening.conf` | Blocks TTY switching |
## Gitea repo ## Gitea repo (see also `jgitta/homelab-configs`)
`https://gitea.jgitta.com/jgitta/senior-kiosk` — master branch 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.com`**Caddy** `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.com``Port 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 ## Roundcube webmail
@@ -108,6 +125,6 @@ sleep 1
- **Proxmox host**: jg-hud (main), siklos (192.168.88.27) - **Proxmox host**: jg-hud (main), siklos (192.168.88.27)
- **TrueNAS**: 192.168.88.24 - **TrueNAS**: 192.168.88.24
- **Thinkstation** (dev desktop): 192.168.88.41 - **Thinkstation** (dev desktop): 192.168.88.41
- **Caddy reverse proxy**: 192.168.88.110 — handles *.jgitta.com - **Caddy reverse proxy**: 192.168.88.110 — terminates HTTPS for `*.jgitta.com` (including `gitea.jgitta.com` → Gitea :3002)
- **Gitea**: gitea.jgitta.com (port 3002 internal) - **Gitea**: behind Caddy at `gitea.jgitta.com`; app listens on port 3002 on the Gitea host
- **Headscale**: self-hosted Tailscale on siklos - **Headscale**: self-hosted Tailscale on siklos
+99
View File
@@ -0,0 +1,99 @@
# SeniorNet Kiosk — Custom Debian ISO
Build a bootable ISO that installs and auto-configures the kiosk app on any machine.
## On Your Workstation (192.168.88.41)
### 1. Clone the repo (if not already done)
```bash
git clone https://gitea.jgitta.com/jgitta/senior-kiosk.git
cd senior-kiosk
```
### 2. Build the ISO
```bash
sudo ./build-iso.sh
```
This creates: `iso-output/seniornet-kiosk.iso` (~1-2 GB, takes 5-10 min)
## Testing on Production Kiosk (192.168.88.44)
### 1. Write ISO to bootable USB
On your workstation with USB connected:
```bash
# List devices to find your USB
lsblk
# Write ISO (replace sdX with your USB device, e.g., sdb)
sudo dd if=iso-output/seniornet-kiosk.iso of=/dev/sdX bs=4M status=progress && sync
```
### 2. Boot from USB
- Plug USB into production kiosk
- Power on and select USB boot (usually F12, ESC, or DEL during startup)
- Installation starts silently (no prompts)
### 3. Wait for installation
- Silent Debian installation: ~5-10 minutes
- Clones repo, installs dependencies
- System reboots automatically
### 4. Verify kiosk is running
After reboot, you should see the kiosk home screen with buttons.
To check status via SSH:
```bash
ssh jgitta@192.168.88.44
journalctl -u kiosk.service -f
```
## What the ISO Contains
- **Debian 13 Trixie** (minimal base)
- **Chromium** (system package, not snap)
- **Python 3, PyQt5, websocket-client**
- **Openbox** (window manager)
- **Xvfb** (virtual display)
- **Your kiosk app** (cloned from Gitea at install time)
- **Auto-boot to kiosk mode** (no desktop login)
## Files
- `preseed.cfg` — Debian unattended installer config
- `install-kiosk.sh` — Post-install script (runs after Debian install)
- `build-iso.sh` — Build script (creates the ISO)
## Troubleshooting
**If installation hangs:**
- Check network connectivity during install
- Gitea must be reachable (or install will use git clone fallback)
**If kiosk doesn't start:**
```bash
ssh jgitta@192.168.88.44
journalctl -u kiosk.service -20 # View last 20 lines of logs
sudo systemctl restart kiosk.service
```
**To access terminal:**
- Press Alt+F2 during boot
- Log in as `root` (password: `kiosk`)
- Run: `/usr/local/bin/kiosk-restart` to restart
## Customization
To modify the ISO:
1. Edit `preseed.cfg` (installer config)
2. Edit `install-kiosk.sh` (post-install)
3. Edit `build-iso.sh` (ISO build options)
4. Rebuild: `sudo ./build-iso.sh`
## Distribution
Once tested, you can:
- Copy `seniornet-kiosk.iso` to USB drives for deployment
- Host on web server for download
- Share with end users
- Any machine can boot and auto-install (no Proxmox needed)
+1 -1
View File
@@ -22,7 +22,7 @@ The kiosk presents a single Chrome browser window with a custom floating toolbar
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
``` ```
- **OS**: Ubuntu 24.04 LTS Server (minimal, headless) - **OS**: Debian Trixie
- **Display**: X11 + LightDM (auto-login) + Openbox WM - **Display**: X11 + LightDM (auto-login) + Openbox WM
- **Browser**: Google Chrome in `--app` mode (no tabs/address bar) - **Browser**: Google Chrome in `--app` mode (no tabs/address bar)
- **Toolbar**: PyQt5 frameless overlay, always-on-top, 142px tall - **Toolbar**: PyQt5 frameless overlay, always-on-top, 142px tall
+1
View File
@@ -0,0 +1 @@
0.2.5
+455 -222
View File
File diff suppressed because it is too large Load Diff
Executable
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -e
# SeniorNet Kiosk ISO Builder
# Takes the official Debian Trixie netinstall ISO and repacks it with:
# - preseed.cfg embedded for silent auto-install
# - GRUB + isolinux configured for auto-boot (UEFI + BIOS)
#
# The kiosk app itself is NOT bundled — it's downloaded from Gitea at
# first-boot time. Rebuild the ISO only when changing Debian base or preseed.
#
# Run: sudo ./build-iso.sh
# Output: iso-output/seniornet-kiosk.iso
if [ "$EUID" -ne 0 ]; then
echo "ERROR: Run as root: sudo ./build-iso.sh"; exit 1
fi
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
OUTPUT_DIR="$REPO_DIR/iso-output"
WORK_DIR="$(mktemp -d /tmp/kiosk-iso-XXXX)"
trap 'rm -rf "$WORK_DIR"' EXIT
DEBIAN_ISO_URL="https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.5.0-amd64-netinst.iso"
ISO_CACHE="$OUTPUT_DIR/debian-netinst.iso"
OUTPUT_ISO="$OUTPUT_DIR/seniornet-kiosk.iso"
mkdir -p "$OUTPUT_DIR"
# ── Dependencies ───────────────────────────────────────────────────
echo "==> Checking dependencies..."
MISSING=""
for pkg in xorriso isolinux curl; do
dpkg -l "$pkg" &>/dev/null || MISSING="$MISSING $pkg"
done
if [ -n "$MISSING" ]; then
apt-get install -y $MISSING
fi
# ── Download Debian netinstall ISO (cached) ────────────────────────
if [ -f "$ISO_CACHE" ]; then
echo "==> Using cached Debian ISO: $ISO_CACHE"
else
echo "==> Downloading Debian 13 Trixie stable netinstall (~700 MB)..."
curl -L --progress-bar "$DEBIAN_ISO_URL" -o "$ISO_CACHE"
fi
# ── Extract ISO ────────────────────────────────────────────────────
echo "==> Extracting ISO..."
mkdir -p "$WORK_DIR/iso"
xorriso -osirrox on -indev "$ISO_CACHE" -extract / "$WORK_DIR/iso" 2>/dev/null
chmod -R u+w "$WORK_DIR/iso"
# ── Add preseed to ISO root ────────────────────────────────────────
# Accessible during install as /cdrom/preseed.cfg
cp "$REPO_DIR/preseed.cfg" "$WORK_DIR/iso/preseed.cfg"
# ── Auto-boot: GRUB (UEFI) ────────────────────────────────────────
echo "==> Configuring auto-boot..."
cat > "$WORK_DIR/iso/boot/grub/grub.cfg" << 'GRUBEOF'
set default=0
set timeout=5
menuentry "SeniorNet Kiosk — Automatic Install" {
linux /install.amd/vmlinuz preseed/file=/cdrom/preseed.cfg auto=true priority=critical quiet ---
initrd /install.amd/initrd.gz
}
menuentry "SeniorNet Kiosk — Install (verbose)" {
linux /install.amd/vmlinuz preseed/file=/cdrom/preseed.cfg auto=true priority=critical ---
initrd /install.amd/initrd.gz
}
GRUBEOF
# ── Auto-boot: isolinux (BIOS legacy) ─────────────────────────────
if [ -f "$WORK_DIR/iso/isolinux/isolinux.cfg" ]; then
cat > "$WORK_DIR/iso/isolinux/isolinux.cfg" << 'ISOEOF'
default install
timeout 50
label install
menu label SeniorNet Kiosk - Automatic Install
kernel /install.amd/vmlinuz
append initrd=/install.amd/initrd.gz preseed/file=/cdrom/preseed.cfg auto=true priority=critical quiet ---
label verbose
menu label SeniorNet Kiosk - Install (verbose)
kernel /install.amd/vmlinuz
append initrd=/install.amd/initrd.gz preseed/file=/cdrom/preseed.cfg auto=true priority=critical ---
ISOEOF
fi
# ── Repack ISO (UEFI + BIOS hybrid) ───────────────────────────────
echo "==> Building final ISO..."
cd "$WORK_DIR/iso"
xorriso -as mkisofs \
-r -J -joliet-long \
-V "SeniorNet Kiosk" \
-b isolinux/isolinux.bin \
-c isolinux/boot.cat \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-eltorito-alt-boot \
-e boot/grub/efi.img \
-no-emul-boot \
-isohybrid-gpt-basdat \
-o "$OUTPUT_ISO" \
. 2>&1 | tail -5
# Make the ISO bootable from USB on both BIOS and UEFI machines
isohybrid --uefi "$OUTPUT_ISO"
echo
SIZE=$(du -sh "$OUTPUT_ISO" | cut -f1)
echo "==> ✓ ISO complete: $OUTPUT_ISO ($SIZE)"
echo
echo "Write to USB:"
echo " sudo dd if=\"$OUTPUT_ISO\" of=/dev/sdX bs=4M status=progress && sync"
echo
echo "Boot from USB — Debian installs silently (~10 min) then reboots."
echo "On first reboot: setup wizard prompts for username and senior's name."
echo "Second reboot: kiosk is running."
+11
View File
@@ -0,0 +1,11 @@
{
"urls": {
"email": "https://mail.jgitta.com/roundcube/",
"internet": "https://www.duckduckgo.com",
"weather": "weather.html",
"news": "news.html",
"facebook": "https://www.facebook.com",
"messenger": "https://www.messenger.com",
"youtube": "https://www.youtube.com"
}
}
-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.
+74 -3
View File
@@ -7,15 +7,81 @@
KIOSK_LOCAL="jgitta@192.168.88.44" KIOSK_LOCAL="jgitta@192.168.88.44"
KIOSK_REMOTE="jgitta@100.64.0.1" KIOSK_REMOTE="jgitta@100.64.0.1"
REPO_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
COMMIT_MSG="${1:-deploy: update from kiosk-dev $(date +%Y-%m-%d)}" HOME_SRC="$REPO_DIR/home-screen-index.html"
HOME_DST="/home/jgitta/kiosk-home/index.html"
WEATHER_SRC="$REPO_DIR/weather.html"
WEATHER_DST="/home/jgitta/kiosk-home/weather.html"
NEWS_SRC="$REPO_DIR/news.html"
NEWS_DST="/home/jgitta/kiosk-home/news.html"
CONFIG_SRC="$REPO_DIR/config.json"
# Auto-increment patch version (0.2.0 → 0.2.1 → 0.2.2 ...)
VERSION_FILE="$REPO_DIR/VERSION"
if [ -f "$VERSION_FILE" ]; then
CUR=$(cat "$VERSION_FILE" | tr -d '[:space:]')
MAJOR=$(echo "$CUR" | cut -d. -f1)
MINOR=$(echo "$CUR" | cut -d. -f2)
PATCH=$(echo "$CUR" | cut -d. -f3)
NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
echo "$NEW_VERSION" > "$VERSION_FILE"
echo "==> Version: $CUR$NEW_VERSION"
else
NEW_VERSION="unknown"
fi
COMMIT_MSG="${1:-deploy: v$NEW_VERSION $(date +%Y-%m-%d)}"
# Gitea credentials (from credentials.md)
GITEA_TOKEN="4fe93c6c62490ef0bb4e4aeec58f47da26752ce8"
GITEA_USER="jgitta"
inject_config() {
local src="$1"
local dst="$2"
local config="$3"
if [ -f "$config" ]; then
local config_json=$(cat "$config" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ')
sed "s|const config = {};|const config = $config_json;|" "$src" > "$dst"
echo " Injected config.json into home screen"
else
cp "$src" "$dst"
echo " WARNING: config.json missing — deploying without config injection"
fi
}
echo "==> Syncing home screen to $HOME_DST..."
if [ -f "$HOME_SRC" ]; then
mkdir -p "$(dirname "$HOME_DST")"
inject_config "$HOME_SRC" "$HOME_DST" "$CONFIG_SRC"
chown jgitta:jgitta "$HOME_DST" 2>/dev/null || true
else
echo " WARNING: $HOME_SRC missing — home screen not updated"
fi
if [ -f "$WEATHER_SRC" ]; then
cp "$WEATHER_SRC" "$WEATHER_DST"
chown jgitta:jgitta "$WEATHER_DST" 2>/dev/null || true
echo " Synced weather page"
fi
if [ -f "$NEWS_SRC" ]; then
cp "$NEWS_SRC" "$NEWS_DST"
chown jgitta:jgitta "$NEWS_DST" 2>/dev/null || true
echo " Synced news page"
fi
echo "==> Committing and pushing to Gitea..." echo "==> Committing and pushing to Gitea..."
git -C "$REPO_DIR" add -A git -C "$REPO_DIR" add -A
git -C "$REPO_DIR" commit -m "$COMMIT_MSG" 2>/dev/null || echo " (nothing new to commit)" git -C "$REPO_DIR" commit -m "$COMMIT_MSG" 2>/dev/null || echo " (nothing new to commit)"
git -C "$REPO_DIR" push origin master || echo " (nothing new to push)" git -C "$REPO_DIR" remote set-url origin "http://$GITEA_USER:$GITEA_TOKEN@192.168.88.27:3002/jgitta/senior-kiosk.git"
git -C "$REPO_DIR" push origin master || echo " (push failed — check network)"
echo "==> Restarting kiosk-dev..." echo "==> Restarting kiosk-dev..."
/usr/local/bin/kiosk-restart if [ -f "/usr/local/bin/kiosk-restart" ]; then
/usr/local/bin/kiosk-restart
else
ssh kiosk-dev "/usr/local/bin/kiosk-restart"
fi
echo "==> Trying production kiosk..." echo "==> Trying production kiosk..."
KIOSK="" KIOSK=""
@@ -33,6 +99,11 @@ if [ -n "$KIOSK" ]; then
ssh "$KIOSK" " ssh "$KIOSK" "
cd /home/jgitta/kiosk-browser cd /home/jgitta/kiosk-browser
git pull git pull
# Inject config at deploy time
config_json=\\\$(cat config.json | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\/g' | sed 's/\\\"/\\\\\\\"/g' | tr '\\\n' ' ')
sed \"s|const config = {};|const config = \\\$config_json;|\" home-screen-index.html > /home/jgitta/kiosk-home/index.html
cp weather.html /home/jgitta/kiosk-home/weather.html
cp news.html /home/jgitta/kiosk-home/news.html
/usr/local/bin/kiosk-restart /usr/local/bin/kiosk-restart
" "
echo " Production updated." echo " Production updated."
+42 -38
View File
@@ -1,4 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- Source for /home/jgitta/kiosk-home/index.html — copied by deploy.sh and install.sh -->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@@ -94,46 +95,47 @@
<div id="clock">12:00 PM</div> <div id="clock">12:00 PM</div>
<div id="date">Wednesday, January 1</div> <div id="date">Wednesday, January 1</div>
<div class="grid"> <div class="grid" id="buttonGrid"></div>
<a class="btn btn-gmail" href="https://mail.google.com">
<div class="icon">✉️</div>
Email
</a>
<a class="btn btn-browse" href="https://www.google.com">
<div class="icon">🌐</div>
Internet
</a>
<a class="btn btn-weather" href="https://weather.com">
<div class="icon">🌤️</div>
Weather
</a>
<a class="btn btn-news" href="https://news.google.com">
<div class="icon">📰</div>
News
</a>
<a class="btn btn-facebook" href="https://www.facebook.com">
<div class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width="54" height="54" fill="white"><path d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z"/></svg></div>
Facebook
</a>
<a class="btn btn-messenger" href="https://www.messenger.com">
<div class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="54" height="54" fill="white"><path d="M256.55 8C116.52 8 8 110.34 8 248.57c0 72.3 29.71 134.78 78.07 177.94 8.35 7.51 6.63 11.86 8.05 58.23A19.92 19.92 0 0 0 122 502.31c52.91-23.3 53.59-25.14 62.56-22.7C337.85 521.8 504 423.7 504 248.57 504 110.34 396.59 8 256.55 8zm149.24 185.13l-73 115.57a37.37 37.37 0 0 1-53.91 9.93l-58.08-43.47a15 15 0 0 0-18 0l-78.37 59.44c-10.46 7.93-24.16-4.6-17.11-15.67l73-115.57a37.36 37.36 0 0 1 53.91-9.93l58.06 43.46a15 15 0 0 0 18 0l78.34-59.44c10.48-7.93 24.17 4.6 17.12 15.67z"/></svg></div>
Messenger
</a>
<a class="btn btn-youtube" href="https://www.youtube.com">
<div class="icon">▶️</div>
YouTube
</a>
</div>
<script> <script>
const config = {};
const buttons = [
{ id: 'email', label: 'Email', icon: '✉️', class: 'btn-gmail' },
{ id: 'internet', label: 'Internet', icon: '🌐', class: 'btn-browse' },
{ id: 'weather', label: 'Weather', icon: '🌤️', class: 'btn-weather' },
{ id: 'news', label: 'News', icon: '📰', class: 'btn-news' },
{ id: 'facebook', label: 'Facebook', icon: null, class: 'btn-facebook', svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width="54" height="54" fill="white"><path d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z"/></svg>' },
{ id: 'messenger', label: 'Messenger', icon: null, class: 'btn-messenger', svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="54" height="54" fill="white"><path d="M256.55 8C116.52 8 8 110.34 8 248.57c0 72.3 29.71 134.78 78.07 177.94 8.35 7.51 6.63 11.86 8.05 58.23A19.92 19.92 0 0 0 122 502.31c52.91-23.3 53.59-25.14 62.56-22.7C337.85 521.8 504 423.7 504 248.57 504 110.34 396.59 8 256.55 8zm149.24 185.13l-73 115.57a37.37 37.37 0 0 1-53.91 9.93l-58.08-43.47a15 15 0 0 0-18 0l-78.37 59.44c-10.46 7.93-24.16-4.6-17.11-15.67l73-115.57a37.36 37.36 0 0 1 53.91-9.93l58.06 43.46a15 15 0 0 0 18 0l78.34-59.44c10.48-7.93 24.17 4.6 17.12 15.67z"/></svg>' },
{ id: 'youtube', label: 'YouTube', icon: '▶️', class: 'btn-youtube' }
];
function loadButtons() {
const grid = document.getElementById('buttonGrid');
const urls = config.urls || {};
buttons.forEach(btn => {
const url = urls[btn.id] || '#';
const link = document.createElement('a');
link.className = `btn ${btn.class}`;
link.href = url;
const iconDiv = document.createElement('div');
iconDiv.className = 'icon';
if (btn.icon) {
iconDiv.textContent = btn.icon;
} else if (btn.svg) {
iconDiv.innerHTML = btn.svg;
}
link.appendChild(iconDiv);
const label = document.createElement('div');
label.textContent = btn.label;
link.appendChild(label);
grid.appendChild(link);
});
}
function updateClock() { function updateClock() {
const now = new Date(); const now = new Date();
const timeOpts = { hour: 'numeric', minute: '2-digit', hour12: true }; const timeOpts = { hour: 'numeric', minute: '2-digit', hour12: true };
@@ -141,6 +143,8 @@
document.getElementById('clock').textContent = now.toLocaleTimeString('en-US', timeOpts); document.getElementById('clock').textContent = now.toLocaleTimeString('en-US', timeOpts);
document.getElementById('date').textContent = now.toLocaleDateString('en-US', dateOpts); document.getElementById('date').textContent = now.toLocaleDateString('en-US', dateOpts);
} }
loadButtons();
updateClock(); updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
</script> </script>
+145
View File
@@ -0,0 +1,145 @@
#!/bin/bash
set -e
# SeniorNet Kiosk setup script.
# Runs on first boot via kiosk-firstboot.service (which gives it a real TTY).
# Can also be run manually: sudo bash install-kiosk.sh
REPO_BASE="https://gitea.jgitta.com/jgitta/senior-kiosk/raw/branch/master"
echo "============================================"
echo " SeniorNet Kiosk — First-Boot Setup"
echo "============================================"
echo
# ── Fixed user details ─────────────────────────────────────────────
KIOSK_USER="kiosk"
KIOSK_NAME="Kiosk"
echo
# ── Paths ──────────────────────────────────────────────────────────
KIOSK_HOME="/home/$KIOSK_USER"
REPO_DIR="$KIOSK_HOME/kiosk-browser"
HOME_DIR="$KIOSK_HOME/kiosk-home"
CHROMIUM_PROFILE="$KIOSK_HOME/chromium-kiosk"
# ── User ───────────────────────────────────────────────────────────
echo "==> Creating user $KIOSK_USER..."
if ! id "$KIOSK_USER" &>/dev/null; then
useradd -m -s /bin/bash -c "$KIOSK_NAME" "$KIOSK_USER"
fi
# ── Packages ───────────────────────────────────────────────────────
echo "==> Installing packages..."
sed -i '/^deb cdrom:/d' /etc/apt/sources.list
apt-get update -qq
apt-get install -y \
chromium \
openbox \
lightdm \
xserver-xorg \
x11-utils \
xdotool \
python3 \
python3-pyqt5 \
python3-websocket \
sqlite3 \
git \
curl \
openssh-server \
fonts-noto-color-emoji \
2>&1 | grep -Ev "^(Reading|Selecting|Preparing|Unpacking|Setting|Processing|Triggers|Get)"
# ── Kiosk repo ─────────────────────────────────────────────────────
echo "==> Cloning kiosk repo..."
if [ ! -d "$REPO_DIR" ]; then
git clone --depth=1 https://gitea.jgitta.com/jgitta/senior-kiosk.git "$REPO_DIR"
fi
# ── Home screen ────────────────────────────────────────────────────
echo "==> Deploying home screen..."
mkdir -p "$HOME_DIR"
config_json=$(cat "$REPO_DIR/config.json" | tr '\n' ' ')
sed "s|const config = {};|const config = $config_json;|" \
"$REPO_DIR/home-screen-index.html" > "$HOME_DIR/index.html"
# ── LightDM auto-login ─────────────────────────────────────────────
echo "==> Configuring LightDM auto-login..."
mkdir -p /etc/lightdm/lightdm.conf.d
cat > /etc/lightdm/lightdm.conf.d/50-kiosk.conf << EOF
[Seat:*]
autologin-user=$KIOSK_USER
autologin-user-timeout=0
user-session=openbox
EOF
# ── Openbox ────────────────────────────────────────────────────────
echo "==> Configuring Openbox..."
mkdir -p "$KIOSK_HOME/.config/openbox"
cat > "$KIOSK_HOME/.config/openbox/rc.xml" << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<openbox_config xmlns="http://openbox.org/3.4/rc">
<startup/>
</openbox_config>
EOF
cat > "$KIOSK_HOME/.config/openbox/autostart" << 'EOF'
(while true; do
python3 ~/kiosk-browser/browser.py
sleep 2
done) &
EOF
# ── kiosk-restart helper ───────────────────────────────────────────
cat > /usr/local/bin/kiosk-restart << 'EOF'
#!/bin/bash
pkill -f "remote-debugging-port=9222" || true
pkill -f "python3.*browser" || true
sleep 1
echo "Kiosk restarted — new code is live"
EOF
chmod +x /usr/local/bin/kiosk-restart
# ── X11 hardening ──────────────────────────────────────────────────
echo "==> Hardening X11..."
mkdir -p /etc/X11/xorg.conf.d
cat > /etc/X11/xorg.conf.d/50-kiosk.conf << 'EOF'
Section "ServerFlags"
Option "DontVTSwitch" "true"
Option "DontZap" "true"
Option "BlankTime" "0"
Option "StandbyTime" "0"
EndSection
EOF
# ── SSH: allow root login for admin ───────────────────────────────
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
# ── Permissions ────────────────────────────────────────────────────
echo "==> Setting permissions..."
chown -R "$KIOSK_USER:$KIOSK_USER" \
"$KIOSK_HOME/.config" \
"$REPO_DIR" \
"$HOME_DIR"
# ── Enable LightDM, remove first-boot setup ───────────────────────
systemctl enable lightdm
systemctl set-default graphical.target
# Remove auto-login and setup script so root doesn't auto-login after kiosk is running
rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf
rm -f /usr/local/bin/kiosk-setup
# Legacy cleanup (no-op if not present)
systemctl disable kiosk-firstboot 2>/dev/null || true
rm -f /etc/systemd/system/kiosk-firstboot.service
systemctl daemon-reload
echo
echo "============================================"
echo " Setup complete!"
echo " Kiosk will start after reboot as: $KIOSK_NAME ($KIOSK_USER)"
echo "============================================"
echo
sync
reboot
+6 -1
View File
@@ -6,7 +6,7 @@ set -e
echo "=== Installing packages ===" echo "=== Installing packages ==="
apt-get update apt-get update
apt-get install -y python3-pyqt5 unclutter xorg openbox lightdm \ 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 # Install websocket-client
pip3 install websocket-client --break-system-packages 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" \ cp "$SCRIPT_DIR/system-config/xorg-50-kiosk-hardening.conf" \
/etc/X11/xorg.conf.d/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 # Disable unused virtual consoles
for i in 2 3 4 5 6; do for i in 2 3 4 5 6; do
systemctl disable getty@tty$i 2>/dev/null || true systemctl disable getty@tty$i 2>/dev/null || true
+336
View File
@@ -0,0 +1,336 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>News</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
background-attachment: fixed;
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
}
/* Extra-wide custom high-contrast scrollbar for senior usability */
::-webkit-scrollbar {
width: 18px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border: 4px solid transparent;
border-radius: 9px;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
border: 4px solid transparent;
border-radius: 9px;
background-clip: padding-box;
}
/* Frosted glass container */
.container {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 36px;
padding: 40px 30px;
width: 100%;
max-width: 1000px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
}
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
font-size: 48px;
font-weight: bold;
color: #fff;
letter-spacing: 1px;
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
header p {
font-size: 20px;
color: #ffd700;
margin-top: 8px;
font-weight: bold;
}
#loading-box {
text-align: center;
font-size: 26px;
padding: 60px 0;
color: #e2e8f0;
}
.category-title {
font-size: 32px;
font-weight: bold;
color: #38bdf8;
margin: 40px 0 20px 0;
border-bottom: 3px solid rgba(56, 189, 248, 0.3);
padding-bottom: 10px;
display: flex;
align-items: center;
gap: 12px;
}
/* Accordion stories list */
.stories-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.story-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
overflow: hidden;
transition: background 0.2s, transform 0.15s;
scroll-margin-top: 20px; /* Keep beautiful breathing room at the top of viewport when focused */
}
.story-card:hover {
background: rgba(255, 255, 255, 0.09);
transform: translateY(-2px);
}
/* Large tap target for header */
.story-header {
padding: 24px 28px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
user-select: none;
}
.story-headline {
font-size: 24px;
font-weight: bold;
line-height: 1.35;
color: #fff;
}
.story-chevron {
font-size: 28px;
color: #94a3b8;
transition: transform 0.2s;
}
.story-card.open .story-chevron {
transform: rotate(90deg);
color: #38bdf8;
}
/* Smooth accordion drawer */
.story-body {
max-height: 0;
overflow: hidden;
transition: max-height 0.35s ease-in-out;
background: rgba(0, 0, 0, 0.2);
}
.story-card.open .story-body {
/* Transition is controlled dynamically via JS max-height for perfect animations */
}
.story-content {
padding: 24px 28px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.story-summary {
font-size: 22px;
line-height: 1.6;
color: #f1f5f9;
margin-bottom: 16px;
}
.story-meta {
font-size: 16px;
color: #94a3b8;
font-weight: bold;
}
/* Giant Home Button */
.footer-nav {
text-align: center;
margin-top: 50px;
}
.home-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
text-decoration: none;
padding: 22px 55px;
font-size: 28px;
font-weight: bold;
border-radius: 22px;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.35);
cursor: pointer;
border: none;
transition: transform 0.1s, box-shadow 0.1s;
}
.home-btn:hover {
transform: translateY(-3px);
box-shadow: 0 12px 30px rgba(16, 185, 129, 0.5);
}
.home-btn:active {
transform: translateY(0px);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📰 Daily Newspaper</h1>
<p id="newspaper-date">Today's Edition</p>
</header>
<div id="loading-box">🗞️ Delivery in progress... Please wait.</div>
<div id="news-content" style="display: none;">
<!-- World News Section -->
<div class="category-title">🌐 World News</div>
<div class="stories-list" id="world-news-list"></div>
<!-- National News Section -->
<div class="category-title">🇺🇸 National News</div>
<div class="stories-list" id="national-news-list"></div>
<!-- High-viewport spacer to guarantee that even the last stories in the list can scroll perfectly to the top headline -->
<div style="height: 75vh;"></div>
</div>
<div class="footer-nav">
<a href="index.html" class="home-btn">⌂ Go Home</a>
</div>
</div>
<script>
// Format publication dates cleanly
function formatToday() {
const now = new Date();
return now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}
async function loadNews() {
document.getElementById('newspaper-date').textContent = formatToday();
try {
const resp = await fetch("news-data.json");
const data = await resp.json();
renderCategory("World News", data["World News"], "world-news-list");
renderCategory("National News", data["National News"], "national-news-list");
document.getElementById('loading-box').style.display = 'none';
document.getElementById('news-content').style.display = 'block';
} catch (e) {
document.getElementById('loading-box').textContent = "⚠️ Preparing newspaper... Please wait a moment.";
}
}
function renderCategory(catName, stories, containerId) {
const box = document.getElementById(containerId);
box.innerHTML = ""; // Clear loader
if (!stories || stories.length === 0) {
box.innerHTML = `<div style="padding: 20px; font-size: 20px; color: #94a3b8; text-align: center;">No stories available.</div>`;
return;
}
stories.forEach((story, idx) => {
const card = document.createElement('div');
card.className = 'story-card';
// Wrap each scraped paragraph in a comfortable <p> block for readability
const formattedSummary = story.summary
.split('\n\n')
.map(para => `<p style="margin-bottom:18px;">${para}</p>`)
.join('');
card.innerHTML = `
<div class="story-header" onclick="toggleStory(this)">
<div class="story-headline">${story.title}</div>
<div class="story-chevron">▸</div>
</div>
<div class="story-body">
<div class="story-content">
${story.image ? `<img src="${story.image}" style="width:100%; max-height:450px; object-fit:cover; border-radius:16px; margin-bottom:24px; box-shadow: 0 4px 15px rgba(0,0,0,0.35);" onerror="this.style.display='none'">` : ''}
<div class="story-summary">${formattedSummary}</div>
<div class="story-meta">📰 Source: ${story.source || 'Daily Digest'} | ⌚ Published: ${story.date || 'Recent'}</div>
</div>
</div>
`;
box.appendChild(card);
});
}
// Interactive in-place accordion expansion with scroll-to-focus
function toggleStory(headerElement) {
const card = headerElement.parentElement;
const body = card.querySelector('.story-body');
const isOpen = card.classList.contains('open');
// Close ALL other story cards globally across all categories
const allCards = document.querySelectorAll('.story-card');
allCards.forEach(c => {
if (c !== card && c.classList.contains('open')) {
c.classList.remove('open');
const b = c.querySelector('.story-body');
if (b) {
b.style.transition = 'none';
b.style.maxHeight = '0px';
b.offsetHeight; // Force reflow
b.style.transition = ''; // Restore transition for next open
}
}
});
if (!isOpen) {
card.classList.add('open');
if (body) {
body.style.maxHeight = body.scrollHeight + 'px';
}
// Smoothly scroll the newly opened story card to the top of the viewport
setTimeout(() => {
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 50); // Small delay to allow reflow, works perfectly and instantly
} else {
card.classList.remove('open');
if (body) {
body.style.maxHeight = '0px';
}
}
}
window.onload = loadNews;
</script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -e
# Downloaded and run by the preseed late_command after Debian base install.
# Fetches install-kiosk.sh, configures getty@tty1 to auto-login as root,
# and runs the setup wizard automatically on first reboot.
REPO_BASE="https://gitea.jgitta.com/jgitta/senior-kiosk/raw/branch/master"
echo "==> SeniorNet: downloading setup script..."
curl -fsSL "$REPO_BASE/install-kiosk.sh" -o /usr/local/bin/kiosk-setup
chmod +x /usr/local/bin/kiosk-setup
echo "==> SeniorNet: configuring auto-login on TTY1..."
mkdir -p /etc/systemd/system/getty@tty1.service.d
cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << 'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM
EOF
echo "==> SeniorNet: configuring setup to run on first login..."
cat >> /root/.bash_profile << 'EOF'
# SeniorNet first-boot setup — always pulls latest from Gitea, runs once
if [ -f /usr/local/bin/kiosk-setup ]; then
curl -fsSL https://gitea.jgitta.com/jgitta/senior-kiosk/raw/branch/master/install-kiosk.sh \
-o /usr/local/bin/kiosk-setup 2>/dev/null || true
bash /usr/local/bin/kiosk-setup
fi
EOF
# Allow root SSH login with password for debugging during setup
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
# Boot to text mode for the setup wizard
systemctl set-default multi-user.target
echo "==> SeniorNet: ready. Setup wizard will run automatically on next reboot."
+74
View File
@@ -0,0 +1,74 @@
# Debian Preseed — SeniorNet Kiosk
# Silent unattended Debian base install.
# After install, downloads preseed-bootstrap.sh which installs a first-boot
# setup wizard. On first reboot the wizard prompts for username/name,
# installs the kiosk, then reboots into kiosk mode.
# ── Locale / keyboard ──────────────────────────────────────────────
d-i debian-installer/locale string en_US.UTF-8
d-i keyboard-configuration/xkb-keymap select us
# ── Network ────────────────────────────────────────────────────────
# Auto, but early_command will pin to the first wired (eth/en) interface
# if one exists — prevents installer from defaulting to WiFi and asking
# for a WPA passphrase. Ethernet must be plugged in during install.
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string kiosk
d-i netcfg/get_domain string local
# ── Mirror ─────────────────────────────────────────────────────────
d-i mirror/country string manual
d-i mirror/http/hostname string deb.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
# ── Clock / timezone ───────────────────────────────────────────────
d-i time/zone string UTC
d-i clock-setup/utc boolean true
d-i clock-setup/ntp boolean true
# ── Accounts ───────────────────────────────────────────────────────
# Root only — install-kiosk.sh creates the kiosk user interactively.
d-i passwd/root-login boolean true
d-i passwd/make-user boolean false
d-i passwd/root-password password kiosk
d-i passwd/root-password-again password kiosk
# ── Disk — auto-detect first real disk ─────────────────────────────
# Handles /dev/sda, /dev/nvme0n1, /dev/vda, etc.
d-i preseed/early_command string \
DISK=$(ls /sys/block/ | grep -Ev '^(loop|sr|fd|ram)' | head -1); \
debconf-set partman-auto/disk "/dev/$DISK" || true; \
debconf-set grub-installer/bootdev "/dev/$DISK" || true; \
ETH=$(ls /sys/class/net/ 2>/dev/null | grep -E '^(eth|en)' | head -1 || true); \
if [ -n "$ETH" ]; then debconf-set netcfg/choose_interface "$ETH" || true; fi; \
true
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm_write_new_label boolean true
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
# ── APT ────────────────────────────────────────────────────────────
d-i apt-setup/services-select multiselect security, updates
d-i apt-setup/security_host string security.debian.org
# ── Package selection ──────────────────────────────────────────────
tasksel tasksel/first multiselect standard, ssh-server
d-i pkgsel/include string curl git
# ── Bootloader ─────────────────────────────────────────────────────
d-i grub-installer/only boolean true
d-i grub-installer/bootdev string default
# ── Post-install bootstrap ─────────────────────────────────────────
# Downloads preseed-bootstrap.sh which installs the first-boot setup service.
d-i preseed/late_command string \
in-target bash -c 'curl -fsSL https://gitea.jgitta.com/jgitta/senior-kiosk/raw/branch/master/preseed-bootstrap.sh | bash' ; \
(sleep 3 && reboot) &
# ── Finish ─────────────────────────────────────────────────────────
# reboot is triggered by late_command background job, bypassing the final dialog
+299
View File
@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
overflow-x: hidden;
}
/* Frosted glass container card */
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 32px;
padding: 40px 30px;
width: 100%;
max-width: 900px;
text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
#location-name {
font-size: 38px;
font-weight: bold;
margin-bottom: 8px;
color: #e0f2fe;
}
#loading-text {
font-size: 26px;
margin: 40px 0;
color: #aac8e8;
}
.weather-main {
display: flex;
align-items: center;
justify-content: center;
gap: 30px;
margin: 30px 0;
flex-wrap: wrap;
}
.main-temp {
font-size: 88px;
font-weight: bold;
line-height: 1;
color: #fff;
}
.weather-icon {
font-size: 96px;
line-height: 1;
}
.condition-desc {
font-size: 32px;
font-weight: bold;
color: #38bdf8;
margin-bottom: 10px;
text-transform: capitalize;
}
.feels-like {
font-size: 20px;
color: #cbd5e1;
margin-bottom: 30px;
}
/* Info pills */
.details-row {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.pill {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 14px 24px;
border-radius: 16px;
font-size: 20px;
font-weight: bold;
display: flex;
align-items: center;
gap: 10px;
}
/* Forecast layout */
.forecast-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: left;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
padding-bottom: 8px;
color: #93c5fd;
}
.forecast-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-bottom: 40px;
}
.forecast-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
padding: 20px 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.fc-day {
font-size: 22px;
font-weight: bold;
color: #e2e8f0;
}
.fc-icon {
font-size: 48px;
}
.fc-temp {
font-size: 20px;
font-weight: bold;
color: #fff;
}
/* Go Home Navigation button */
.home-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #10b981, #059669);
color: white;
text-decoration: none;
padding: 20px 50px;
font-size: 26px;
font-weight: bold;
border-radius: 20px;
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3);
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
border: none;
}
.home-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.45);
}
.home-btn:active {
transform: translateY(0px);
}
</style>
</head>
<body>
<div class="container" id="weather-container">
<div id="loading-box">
<div id="loading-text">🔍 Determining your location...</div>
</div>
<div id="weather-content" style="display: none;">
<div id="location-name">Detecting Location...</div>
<div class="weather-main">
<div class="weather-icon" id="main-icon">🌤️</div>
<div class="main-temp" id="temp-val">--°F</div>
</div>
<div class="condition-desc" id="condition-txt">Partly Cloudy</div>
<div class="feels-like" id="feels-like-txt">Feels like --°F</div>
<div class="details-row">
<div class="pill">💧 Humidity: <span id="humidity-val">--%</span></div>
<div class="pill">💨 Wind: <span id="wind-val">-- mph</span></div>
</div>
<div class="forecast-title">3-Day Forecast</div>
<div class="forecast-grid" id="forecast-box"></div>
</div>
<a href="index.html" class="home-btn">⌂ Go Home</a>
</div>
<script>
// WMO Weather interpretation codes mapping
const WEATHER_CODES = {
0: { txt: "Sunny", emoji: "☀️" },
1: { txt: "Mainly Clear", emoji: "🌤️" },
2: { txt: "Partly Cloudy", emoji: "⛅" },
3: { txt: "Overcast", emoji: "☁️" },
45: { txt: "Foggy", emoji: "🌫️" },
48: { txt: "Depositing Rime Fog", emoji: "🌫️" },
51: { txt: "Light Drizzle", emoji: "🌦️" },
53: { txt: "Moderate Drizzle", emoji: "🌦️" },
55: { txt: "Dense Drizzle", emoji: "🌦️" },
61: { txt: "Slight Rain", emoji: "🌧️" },
63: { txt: "Moderate Rain", emoji: "🌧️" },
65: { txt: "Heavy Rain", emoji: "🌧️" },
71: { txt: "Light Snowfall", emoji: "🌨️" },
73: { txt: "Moderate Snowfall", emoji: "🌨️" },
75: { txt: "Heavy Snowfall", emoji: "🌨️" },
77: { txt: "Snow Grains", emoji: "🌨️" },
80: { txt: "Slight Showers", emoji: "🌧️" },
81: { txt: "Moderate Showers", emoji: "🌧️" },
82: { txt: "Violent Showers", emoji: "⛈️" },
85: { txt: "Slight Snow Showers", emoji: "🌨️" },
86: { txt: "Heavy Snow Showers", emoji: "🌨️" },
95: { txt: "Thunderstorm", emoji: "⛈️" },
96: { txt: "Thunderstorm with Hail", emoji: "⛈️" },
99: { txt: "Thunderstorm with Heavy Hail", emoji: "⛈️" }
};
function getWeatherInfo(code) {
return WEATHER_CODES[code] || { txt: "Weather Details", emoji: "🌤️" };
}
async function initWeather() {
try {
document.getElementById('loading-text').textContent = "📊 Loading local weather forecasts...";
const resp = await fetch("weather-data.json");
const data = await resp.json();
const locName = data.location || "Your Area";
const fc = data.forecast;
// Render current stats
const current = fc.current;
const wInfo = getWeatherInfo(current.weather_code);
document.getElementById('location-name').textContent = locName;
document.getElementById('temp-val').textContent = `${Math.round(current.temperature_2m)}°F`;
document.getElementById('main-icon').textContent = wInfo.emoji;
document.getElementById('condition-txt').textContent = wInfo.txt;
document.getElementById('feels-like-txt').textContent = `Feels like ${Math.round(current.apparent_temperature)}°F`;
document.getElementById('humidity-val').textContent = `${current.relative_humidity_2m}%`;
document.getElementById('wind-val').textContent = `${Math.round(current.wind_speed_10m)} mph`;
// Render 3-Day Forecast
const daily = fc.daily;
const forecastBox = document.getElementById('forecast-box');
forecastBox.innerHTML = ""; // Clear loader
for (let i = 1; i <= 3; i++) {
const dateStr = daily.time[i];
const dateObj = new Date(dateStr + "T00:00:00");
const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'long' });
const code = daily.weather_code[i];
const fcInfo = getWeatherInfo(code);
const maxT = Math.round(daily.temperature_2m_max[i]);
const minT = Math.round(daily.temperature_2m_min[i]);
const card = document.createElement('div');
card.className = 'forecast-card';
card.innerHTML = `
<div class="fc-day">${dayName}</div>
<div class="fc-icon">${fcInfo.emoji}</div>
<div class="fc-temp">🔆 ${maxT}° / 🌙 ${minT}°F</div>
`;
forecastBox.appendChild(card);
}
// Hide loader and show content
document.getElementById('loading-box').style.display = 'none';
document.getElementById('weather-content').style.display = 'block';
} catch (e) {
document.getElementById('loading-text').textContent = "⚠️ Preparing weather data... Please wait a moment.";
}
}
window.onload = initWeather;
</script>
</body>
</html>