Files
Your Name 99085adc76 feat: add web dashboard with slide-in drawer navigation
- Flask backend with 23 API routes (entertainment, telegram, docker, system)
- Alpine.js + Tailwind CSS dark-mode SPA with 4 tabs
- pos-dashboard CLI tool with port/config management
- Mobile slide-in drawer with swipe-to-close
- Sticky header stays pinned on scroll
- Tab completion fixes for category-less tools
- POST /api/telegram/commands endpoint for adding commands
2026-08-18 11:54:41 -04:00

276 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: system dashboard — Web dashboard: start/stop/status/config/url
# POS_FLAGS: --help
# POS_SUBCMDS: start stop status port config url
# POS_CONFIG: dashboard | dashboard.env | DASHBOARD_PORT=num:Port the dashboard listens on (default 8080) | DASHBOARD_HOST=:Bind address (default 0.0.0.0)
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
# ── Config & constants ──────────────────────────────────────────
CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}"
CONFIG_FILE="$CONFIG_DIR/dashboard.env"
SERVICE="pos-dashboard.service"
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"
DASHBOARD_DIR="/usr/local/share/linux_post_install/dashboard"
DASHBOARD_SCRIPT="$DASHBOARD_DIR/app.py"
# ── load config ─────────────────────────────────────────────────
load_config() {
[ -f "$CONFIG_FILE" ] || return 0
local k v
while IFS='=' read -r k v; do
[ -n "$k" ] || continue
case "$k" in
\#*) continue ;;
esac
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
if [ -z "${!k:-}" ]; then
export "$k"="$v"
fi
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
}
# ── defaults ────────────────────────────────────────────────────
load_config
DASHBOARD_PORT="${DASHBOARD_PORT:-8080}"
DASHBOARD_HOST="${DASHBOARD_HOST:-0.0.0.0}"
# ── usage ───────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: pos dashboard <command>
Web dashboard for the pos toolkit — browser-based UI for managing
entertainment, Telegram, Docker, and system health.
Commands:
start Install and start the systemd user service
stop Stop, disable, and remove the service
status Show service state and the dashboard URL
port <num> Change the dashboard port (restarts the service)
config Edit DASHBOARD_PORT, DASHBOARD_HOST
url Print the dashboard URL
Config: $CONFIG_FILE (DASHBOARD_PORT, DASHBOARD_HOST)
Service: $SERVICE (systemd user unit)
Dir: $DASHBOARD_DIR
The dashboard binds to DASHBOARD_HOST:DASHBOARD_PORT (default 0.0.0.0:8080).
Access it over Tailscale at: http://<tailscale-ip>:<port>/
Examples:
pos dashboard start
pos dashboard port 9090
pos dashboard status
pos dashboard url
pos dashboard config
EOF
exit 0
}
# ── deps guard (before --help, per convention) ──────────────────
command -v python3 &>/dev/null || err "python3 not found — install with: sudo apt install python3"
# ── help ────────────────────────────────────────────────────────
case "${1:-}" in
-h|--help) usage ;;
esac
# ── start ───────────────────────────────────────────────────────
cmd_start() {
# Check flask is available
if ! python3 -c "import flask" 2>/dev/null; then
err "python3-flask not installed — install with: sudo apt install python3-flask"
fi
# Check dashboard directory exists
if [ ! -d "$DASHBOARD_DIR" ]; then
err "Dashboard directory not found: $DASHBOARD_DIR — run install.sh first"
fi
if [ ! -f "$DASHBOARD_SCRIPT" ]; then
err "Dashboard app not found: $DASHBOARD_SCRIPT — run install.sh first"
fi
# Ensure config exists
if [ ! -f "$CONFIG_FILE" ]; then
mkdir -p "$CONFIG_DIR"
cp "${BASH_SOURCE[0]%/bin/pos-dashboard}/../config/dashboard.env" "$CONFIG_FILE" 2>/dev/null \
|| warn "No config template found — create $CONFIG_FILE manually"
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
fi
# Install systemd user service
mkdir -p "$USER_SYSTEMD_DIR"
local runner
if [ -x /usr/local/bin/pos-dashboard ]; then
runner=/usr/local/bin/pos-dashboard
else
runner="$(cd "$(dirname "$0")/.." && pwd)/bin/pos-dashboard"
warn "using repo path $runner — re-run 'install.sh' so the service survives a deleted repo"
fi
cat >"$USER_SYSTEMD_DIR/$SERVICE" <<EOF
[Unit]
Description=pos Web Dashboard
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 $DASHBOARD_SCRIPT
Restart=on-failure
RestartSec=5
EnvironmentFile=%h/.config/linux_post_install/dashboard.env
[Install]
WantedBy=default.target
EOF
chmod 644 "$USER_SYSTEMD_DIR/$SERVICE"
systemctl --user daemon-reload
systemctl --user enable --now "$SERVICE"
log "Dashboard service enabled: $SERVICE"
cmd_url
if command -v loginctl >/dev/null 2>&1; then
if ! loginctl show-user "$(id -un)" 2>/dev/null | grep -q '^Linger=yes'; then
warn "enable linger so the dashboard survives logout: sudo loginctl enable-linger $(id -un)"
fi
fi
}
# ── stop ────────────────────────────────────────────────────────
cmd_stop() {
if [ ! -f "$USER_SYSTEMD_DIR/$SERVICE" ]; then
warn "no dashboard service installed ($SERVICE)"
return 0
fi
systemctl --user disable --now "$SERVICE" 2>/dev/null || true
rm -f "$USER_SYSTEMD_DIR/$SERVICE"
systemctl --user daemon-reload
log "Dashboard service disabled"
}
# ── status ──────────────────────────────────────────────────────
cmd_status() {
load_config
DASHBOARD_PORT="${DASHBOARD_PORT:-8080}"
DASHBOARD_HOST="${DASHBOARD_HOST:-0.0.0.0}"
if systemctl --user is-active --quiet "$SERVICE" 2>/dev/null; then
echo "service: running"
else
echo "service: not running"
fi
if systemctl --user is-enabled "$SERVICE" >/dev/null 2>&1; then
echo "autostart: enabled (starts on login)"
else
echo "autostart: disabled"
fi
echo "config: $CONFIG_FILE"
echo "dir: $DASHBOARD_DIR"
cmd_url
}
# ── port ────────────────────────────────────────────────────────
cmd_port() {
local new_port="${1:-}"
if [ -z "$new_port" ]; then
err "Usage: pos dashboard port <number>"
fi
# Validate port is a number between 1 and 65535
if ! [[ "$new_port" =~ ^[0-9]+$ ]] || [ "$new_port" -lt 1 ] || [ "$new_port" -gt 65535 ]; then
err "Invalid port: $new_port — must be a number between 1 and 65535"
fi
# Ensure config file exists
if [ ! -f "$CONFIG_FILE" ]; then
mkdir -p "$CONFIG_DIR"
cp "${BASH_SOURCE[0]%/bin/pos-dashboard}/../config/dashboard.env" "$CONFIG_FILE" 2>/dev/null \
|| echo -e "DASHBOARD_PORT=8080\nDASHBOARD_HOST=0.0.0.0" > "$CONFIG_FILE"
chmod 600 "$CONFIG_FILE"
fi
# Update port in config file
if grep -q '^DASHBOARD_PORT=' "$CONFIG_FILE"; then
sed -i "s/^DASHBOARD_PORT=.*/DASHBOARD_PORT=$new_port/" "$CONFIG_FILE"
else
echo "DASHBOARD_PORT=$new_port" >> "$CONFIG_FILE"
fi
log "Port changed to $new_port in $CONFIG_FILE"
# Update the running variable so cmd_url shows the new port
DASHBOARD_PORT="$new_port"
# Restart service if running
if systemctl --user is-active --quiet "$SERVICE" 2>/dev/null; then
systemctl --user restart "$SERVICE"
log "Dashboard service restarted on port $new_port"
else
warn "Dashboard is not running — start it with: pos dashboard start"
fi
cmd_url
}
# ── config ──────────────────────────────────────────────────────
cmd_config() {
# If pos config-ui.sh is available, use the interactive editor
local cfg_ui_script
for p in \
"$(dirname "$0")/../lib/config-ui.sh" \
"$(dirname "$0")/config-ui.sh" \
"/usr/local/bin/config-ui.sh"; do
if [ -f "$p" ]; then
cfg_ui_script="$p"
break
fi
done
if [ -n "${cfg_ui_script:-}" ]; then
source "$cfg_ui_script" 2>/dev/null
cfg_ui "dashboard"
return
fi
# Fallback: reload and show current values
load_config
DASHBOARD_PORT="${DASHBOARD_PORT:-8080}"
DASHBOARD_HOST="${DASHBOARD_HOST:-0.0.0.0}"
echo "Dashboard config: $CONFIG_FILE"
echo
printf ' DASHBOARD_PORT=%s\n' "$DASHBOARD_PORT"
printf ' DASHBOARD_HOST=%s\n' "$DASHBOARD_HOST"
echo
echo "Edit with: pos config dashboard"
}
# ── url ─────────────────────────────────────────────────────────
cmd_url() {
local ip=""
# Prefer Tailscale IP
if command -v tailscale &>/dev/null; then
ip="$(tailscale ip -4 2>/dev/null || true)"
fi
# Fallback to first non-loopback IP
if [ -z "$ip" ]; then
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
fi
if [ -z "$ip" ]; then
ip="localhost"
fi
echo "http://${ip}:${DASHBOARD_PORT}/"
}
# ── dispatch ────────────────────────────────────────────────────
case "${1:-}" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
port) cmd_port "${2:-}" ;;
config) cmd_config ;;
url) cmd_url ;;
"") usage ;;
*) err "Unknown subcommand '$1' (see --help)" ;;
esac