mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 19:06:34 +00:00
feat: deployment artifacts + CEC relay + auth-check endpoint
Deployment (deploy/): - systemd units for server (system) and kiosk (user session) - Angie/nginx proxy config — routes admin, api, ws, node-red - Dockerfile + docker-compose for containerized deployment - deploy/README.md with install instructions Auth: - /api/admin/_check endpoint for proxy auth_request subrequest - Returns 200 if admin session valid, 401/403 otherwise - Sets X-BetterFrame-User header for upstream CEC (Pi5 HDMI control): - kiosk/src/cec.rs wraps cec-ctl subprocess - Standby/wake/active-source commands - WS message types "standby" / "wake" dispatched to CEC - Admin UI: Wake/Standby buttons on kiosk edit page - Server sendToKiosk via coordinator
This commit is contained in:
parent
766bf8dee0
commit
cbb1683c5d
13 changed files with 460 additions and 1 deletions
84
deploy/README.md
Normal file
84
deploy/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# BetterFrame deployment
|
||||||
|
|
||||||
|
## Native install (Raspberry Pi)
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Node.js 23
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_23.x | sudo bash -
|
||||||
|
sudo apt install -y nodejs build-essential
|
||||||
|
|
||||||
|
# Create user + dirs
|
||||||
|
sudo useradd -r -m -d /var/lib/betterframe betterframe
|
||||||
|
sudo mkdir -p /opt/betterframe /var/log/betterframe /etc/betterframe
|
||||||
|
sudo chown betterframe:betterframe /var/lib/betterframe /var/log/betterframe
|
||||||
|
|
||||||
|
# Deploy code
|
||||||
|
sudo git clone https://github.com/BetterCorp/BetterFrame.git /opt/betterframe
|
||||||
|
cd /opt/betterframe
|
||||||
|
sudo -u betterframe npm install
|
||||||
|
sudo cp sec-config.yaml /opt/betterframe/server/sec-config.yaml
|
||||||
|
|
||||||
|
# Install systemd unit
|
||||||
|
sudo cp deploy/systemd/betterframe-server.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now betterframe-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kiosk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install GTK4 + GStreamer + WebKit
|
||||||
|
sudo apt install -y libgtk-4-dev libgstreamer1.0-dev \
|
||||||
|
libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good \
|
||||||
|
gstreamer1.0-plugins-bad gstreamer1.0-libav \
|
||||||
|
gstreamer1.0-gtk4 libwebkitgtk-6.0-dev libssl-dev
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source ~/.cargo/env
|
||||||
|
|
||||||
|
# Build
|
||||||
|
cd ~/betterframe/kiosk
|
||||||
|
cargo build --release
|
||||||
|
sudo install -Dm755 target/release/betterframe-kiosk /opt/betterframe/kiosk/betterframe-kiosk
|
||||||
|
|
||||||
|
# Install systemd user unit
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp deploy/systemd/betterframe-kiosk.service ~/.config/systemd/user/
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now betterframe-kiosk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Angie proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y angie # or nginx
|
||||||
|
sudo cp deploy/angie/betterframe.conf /etc/angie/conf.d/
|
||||||
|
sudo systemctl reload angie
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Kiosk still runs natively on the Pi (needs Wayland/HDMI), not in Docker.
|
||||||
|
|
||||||
|
Access: `http://<pi-ip>/setup` for first-run.
|
||||||
|
|
||||||
|
## Production secrets
|
||||||
|
|
||||||
|
For production, store the server key via `systemd-creds`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemd-creds encrypt --name=betterframe-secret \
|
||||||
|
/etc/betterframe/secret.key.plain /etc/betterframe/secret.key
|
||||||
|
sudo chmod 0600 /etc/betterframe/secret.key
|
||||||
|
sudo chown root:root /etc/betterframe/secret.key
|
||||||
|
```
|
||||||
|
|
||||||
|
The systemd unit's `LoadCredential=` directive injects this into the
|
||||||
|
service's `$CREDENTIALS_DIRECTORY`.
|
||||||
113
deploy/angie/betterframe.conf
Normal file
113
deploy/angie/betterframe.conf
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# BetterFrame Angie/nginx config — routes admin, kiosk-api, ws, node-red.
|
||||||
|
#
|
||||||
|
# Place in /etc/angie/conf.d/betterframe.conf or /etc/nginx/conf.d/betterframe.conf
|
||||||
|
# Run on the Pi alongside the server. TLS termination here; backend services
|
||||||
|
# bind to 0.0.0.0 but firewall should restrict to localhost in production.
|
||||||
|
|
||||||
|
# Upstreams (BSB services)
|
||||||
|
upstream betterframe_admin { server 127.0.0.1:18080; keepalive 16; }
|
||||||
|
upstream betterframe_api { server 127.0.0.1:18081; keepalive 16; }
|
||||||
|
upstream betterframe_ws { server 127.0.0.1:18082; }
|
||||||
|
upstream betterframe_nodered { server 127.0.0.1:1880; keepalive 8; }
|
||||||
|
|
||||||
|
# Rate limiting for public endpoints
|
||||||
|
limit_req_zone $binary_remote_addr zone=bf_public:10m rate=30r/s;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name betterframe.local _;
|
||||||
|
|
||||||
|
# In production: redirect to HTTPS
|
||||||
|
# return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
# For now: serve plain HTTP
|
||||||
|
client_max_body_size 16M;
|
||||||
|
|
||||||
|
# ---- Admin UI + admin API (session-authenticated) ----
|
||||||
|
location /admin/ {
|
||||||
|
proxy_pass http://betterframe_admin;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /admin { return 301 /admin/; }
|
||||||
|
location /setup { proxy_pass http://betterframe_admin; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
||||||
|
location /auth/ { proxy_pass http://betterframe_admin; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
||||||
|
location /static/ { proxy_pass http://betterframe_admin; }
|
||||||
|
|
||||||
|
# ---- Kiosk REST API (Bearer kiosk-key) ----
|
||||||
|
location /api/kiosk/ {
|
||||||
|
proxy_pass http://betterframe_api;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/pair/ {
|
||||||
|
# Rate-limit pairing initiate to deter brute force
|
||||||
|
limit_req zone=bf_public burst=10 nodelay;
|
||||||
|
proxy_pass http://betterframe_api;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Admin API (session-authenticated) ----
|
||||||
|
location /api/admin/ {
|
||||||
|
proxy_pass http://betterframe_admin;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Live kiosk WebSocket channel ----
|
||||||
|
location /ws/kiosk {
|
||||||
|
proxy_pass http://betterframe_ws;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 86400s; # long-lived
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Node-RED dashboard (admin-only) ----
|
||||||
|
location /nrdp/ {
|
||||||
|
# auth_request /api/admin/_check; # enable when auth-check endpoint ready
|
||||||
|
rewrite ^/nrdp/(.*) /$1 break;
|
||||||
|
proxy_pass http://betterframe_nodered;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Node-RED HTTP-in (public, rate-limited) ----
|
||||||
|
location /in/public/ {
|
||||||
|
limit_req zone=bf_public burst=20 nodelay;
|
||||||
|
rewrite ^/in/public/(.*) /public/$1 break;
|
||||||
|
proxy_pass http://betterframe_nodered;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Node-RED HTTP-in (kiosk-gated) ----
|
||||||
|
location /in/kiosk/ {
|
||||||
|
# Bearer kiosk-key validated by Node-RED flow
|
||||||
|
rewrite ^/in/kiosk/(.*) /kiosk/$1 break;
|
||||||
|
proxy_pass http://betterframe_nodered;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Health/readiness/version (public) ----
|
||||||
|
location ~ ^/(healthz|readyz|version)$ {
|
||||||
|
proxy_pass http://betterframe_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- Root redirect ----
|
||||||
|
location = / {
|
||||||
|
proxy_pass http://betterframe_admin;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
deploy/docker/Dockerfile.server
Normal file
45
deploy/docker/Dockerfile.server
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# BetterFrame server image — Node 23 + native deps for argon2/sqlite
|
||||||
|
FROM node:23-bookworm-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build deps for argon2
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential python3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY server/package.json ./server/
|
||||||
|
COPY tsconfig.base.json ./
|
||||||
|
RUN npm ci --ignore-scripts && npm rebuild argon2
|
||||||
|
|
||||||
|
COPY server ./server
|
||||||
|
|
||||||
|
# ---- Runtime image ----
|
||||||
|
FROM node:23-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd -m -d /var/lib/betterframe -s /bin/false betterframe
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/server ./server
|
||||||
|
COPY --from=builder /app/tsconfig.base.json ./
|
||||||
|
COPY --from=builder /app/package.json ./
|
||||||
|
|
||||||
|
# Default data dir
|
||||||
|
RUN mkdir -p /var/lib/betterframe && chown betterframe:betterframe /var/lib/betterframe
|
||||||
|
VOLUME /var/lib/betterframe
|
||||||
|
|
||||||
|
EXPOSE 18080 18081 18082
|
||||||
|
|
||||||
|
USER betterframe
|
||||||
|
WORKDIR /app/server
|
||||||
|
|
||||||
|
ENV NODE_OPTIONS=--import=tsx
|
||||||
|
|
||||||
|
CMD ["node", "--import", "tsx", "/app/node_modules/@bsb/base/lib/scripts/bsb-plugin-cli.js", "start"]
|
||||||
65
deploy/docker/docker-compose.yml
Normal file
65
deploy/docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# BetterFrame stack — server + Angie proxy + Node-RED.
|
||||||
|
# Kiosk runs on the Pi natively (not in Docker, needs Wayland/HDMI).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker compose -f deploy/docker/docker-compose.yml up -d
|
||||||
|
#
|
||||||
|
# Volumes:
|
||||||
|
# betterframe-data — sqlite DB + secret.key
|
||||||
|
# nodered-data — Node-RED flows
|
||||||
|
#
|
||||||
|
# Bind 0.0.0.0:80 on the host (Angie). Backend services only reachable
|
||||||
|
# from within the Docker network.
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/Dockerfile.server
|
||||||
|
container_name: betterframe-server
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- betterframe-data:/var/lib/betterframe
|
||||||
|
- ../../sec-config.yaml:/app/sec-config.yaml:ro
|
||||||
|
expose:
|
||||||
|
- "18080"
|
||||||
|
- "18081"
|
||||||
|
- "18082"
|
||||||
|
networks:
|
||||||
|
- betterframe
|
||||||
|
|
||||||
|
angie:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: betterframe-angie
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
- nodered
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ../angie/betterframe.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks:
|
||||||
|
- betterframe
|
||||||
|
|
||||||
|
nodered:
|
||||||
|
image: nodered/node-red:latest
|
||||||
|
container_name: betterframe-nodered
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
volumes:
|
||||||
|
- nodered-data:/data
|
||||||
|
expose:
|
||||||
|
- "1880"
|
||||||
|
networks:
|
||||||
|
- betterframe
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
betterframe-data:
|
||||||
|
nodered-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
betterframe:
|
||||||
|
driver: bridge
|
||||||
21
deploy/systemd/betterframe-kiosk.service
Normal file
21
deploy/systemd/betterframe-kiosk.service
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[Unit]
|
||||||
|
Description=BetterFrame Kiosk
|
||||||
|
Documentation=https://github.com/BetterCorp/BetterFrame
|
||||||
|
After=graphical-session.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/.betterframe-kiosk
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||||
|
Environment=WAYLAND_DISPLAY=wayland-0
|
||||||
|
Environment=GST_DEBUG=1
|
||||||
|
ExecStart=/opt/betterframe/kiosk/betterframe-kiosk
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
29
deploy/systemd/betterframe-server.service
Normal file
29
deploy/systemd/betterframe-server.service
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
[Unit]
|
||||||
|
Description=BetterFrame Server (BSB)
|
||||||
|
Documentation=https://github.com/BetterCorp/BetterFrame
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=betterframe
|
||||||
|
Group=betterframe
|
||||||
|
WorkingDirectory=/opt/betterframe/server
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
Environment=NODE_OPTIONS=--import tsx
|
||||||
|
ExecStart=/usr/bin/node --import tsx /opt/betterframe/node_modules/@bsb/base/lib/scripts/bsb-plugin-cli.js start
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ReadWritePaths=/var/lib/betterframe /var/log/betterframe
|
||||||
|
LoadCredential=betterframe-secret:/etc/betterframe/secret.key
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
49
kiosk/src/cec.rs
Normal file
49
kiosk/src/cec.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! CEC (HDMI Consumer Electronics Control) — manages display power state
|
||||||
|
//! via `cec-ctl` subprocess. v4l-utils package provides cec-ctl on Pi5.
|
||||||
|
//!
|
||||||
|
//! Commands:
|
||||||
|
//! - standby: tell TV to sleep
|
||||||
|
//! - image-view-on: wake TV
|
||||||
|
//! - is-active-source: query state
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
const CEC_DEVICE: &str = "/dev/cec0";
|
||||||
|
|
||||||
|
/// Send CEC standby (sleep) to all connected devices.
|
||||||
|
pub fn standby() -> bool {
|
||||||
|
info!("cec: standby");
|
||||||
|
run_cec(&["--standby", "--to", "0"])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send CEC image-view-on (wake) to TV.
|
||||||
|
pub fn wake() -> bool {
|
||||||
|
info!("cec: wake");
|
||||||
|
run_cec(&["--image-view-on", "--to", "0"])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch HDMI input on TV to this device.
|
||||||
|
pub fn become_active_source() -> bool {
|
||||||
|
info!("cec: become active source");
|
||||||
|
run_cec(&["--active-source", "phys-addr=0.0.0.0"])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_cec(args: &[&str]) -> bool {
|
||||||
|
match Command::new("cec-ctl")
|
||||||
|
.arg("-d")
|
||||||
|
.arg(CEC_DEVICE)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(out) if out.status.success() => true,
|
||||||
|
Ok(out) => {
|
||||||
|
warn!("cec-ctl failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("cec-ctl not available: {e}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
mod server;
|
mod server;
|
||||||
mod bundle;
|
mod bundle;
|
||||||
|
mod cec;
|
||||||
mod pipeline;
|
mod pipeline;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod ws_client;
|
mod ws_client;
|
||||||
|
|
||||||
pub enum ServerMsg {
|
pub enum ServerMsg {
|
||||||
ReloadBundle,
|
ReloadBundle,
|
||||||
|
Standby,
|
||||||
|
Wake,
|
||||||
}
|
}
|
||||||
|
|
||||||
use gtk4::prelude::{ApplicationExt, ApplicationExtManual};
|
use gtk4::prelude::{ApplicationExt, ApplicationExtManual};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use gtk4::{self as gtk, Application, ApplicationWindow, Box as GtkBox, Grid, Lab
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::bundle::KioskBundle;
|
use crate::bundle::KioskBundle;
|
||||||
|
use crate::cec;
|
||||||
use crate::pipeline;
|
use crate::pipeline;
|
||||||
use crate::server;
|
use crate::server;
|
||||||
use crate::ws_client;
|
use crate::ws_client;
|
||||||
|
|
@ -79,7 +80,7 @@ fn activate(app: &Application) {
|
||||||
ws_client::run(&server_ws, &key_ws, ws_tx);
|
ws_client::run(&server_ws, &key_ws, ws_tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for WS messages and re-fetch bundle on reload
|
// Listen for WS messages and dispatch
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
for msg in ws_rx {
|
for msg in ws_rx {
|
||||||
match msg {
|
match msg {
|
||||||
|
|
@ -88,6 +89,8 @@ fn activate(app: &Application) {
|
||||||
let bundle = server::fetch_bundle(&server_for_reload, &key_for_reload);
|
let bundle = server::fetch_bundle(&server_for_reload, &key_for_reload);
|
||||||
let _ = tx_for_reload.send(WorkerMsg::RenderBundle(bundle));
|
let _ = tx_for_reload.send(WorkerMsg::RenderBundle(bundle));
|
||||||
}
|
}
|
||||||
|
ServerMsg::Standby => { cec::standby(); }
|
||||||
|
ServerMsg::Wake => { cec::wake(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ pub fn run(server_url: &str, kiosk_key: &str, tx: Sender<ServerMsg>) {
|
||||||
} else if text.contains("\"type\":\"reload-bundle\"") {
|
} else if text.contains("\"type\":\"reload-bundle\"") {
|
||||||
info!("ws: reload-bundle received");
|
info!("ws: reload-bundle received");
|
||||||
let _ = tx.send(ServerMsg::ReloadBundle);
|
let _ = tx.send(ServerMsg::ReloadBundle);
|
||||||
|
} else if text.contains("\"type\":\"standby\"") {
|
||||||
|
info!("ws: standby received");
|
||||||
|
let _ = tx.send(ServerMsg::Standby);
|
||||||
|
} else if text.contains("\"type\":\"wake\"") {
|
||||||
|
info!("ws: wake received");
|
||||||
|
let _ = tx.send(ServerMsg::Wake);
|
||||||
} else {
|
} else {
|
||||||
info!("ws: msg: {text}");
|
info!("ws: msg: {text}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,25 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
||||||
registerAdminRoutes(app, deps);
|
registerAdminRoutes(app, deps);
|
||||||
registerAccountRoutes(app, deps);
|
registerAccountRoutes(app, deps);
|
||||||
|
|
||||||
|
// Auth-check endpoint for Angie auth_request subrequest.
|
||||||
|
// Returns 200 if session cookie is valid + admin role, 401 otherwise.
|
||||||
|
app.get("/api/admin/_check", (event) => {
|
||||||
|
const cookie = event.req.headers.get("cookie") ?? "";
|
||||||
|
const match = cookie.match(new RegExp(`${deps.cookieName}=([^;]+)`));
|
||||||
|
if (!match) return new Response(null, { status: 401 });
|
||||||
|
const resolved = deps.auth.resolveSession(match[1]!);
|
||||||
|
if (!resolved || resolved.session.totp_pending) {
|
||||||
|
return new Response(null, { status: 401 });
|
||||||
|
}
|
||||||
|
if (resolved.user.role !== "admin") {
|
||||||
|
return new Response(null, { status: 403 });
|
||||||
|
}
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: { "x-betterframe-user": resolved.user.username },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/healthz", () => ({ status: "ok" }));
|
app.get("/healthz", () => ({ status: "ok" }));
|
||||||
app.get("/readyz", () => {
|
app.get("/readyz", () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -748,4 +748,17 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
||||||
deps.repo.deleteKiosk(id);
|
deps.repo.deleteKiosk(id);
|
||||||
return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- CEC power commands -----------------------------------------------
|
||||||
|
app.post("/admin/kiosks/:id/power/standby", (event) => {
|
||||||
|
const id = Number(getRouterParam(event, "id"));
|
||||||
|
getCoordinator().sendToKiosk(id, { type: "standby" });
|
||||||
|
return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/admin/kiosks/:id/power/wake", (event) => {
|
||||||
|
const id = Number(getRouterParam(event, "id"));
|
||||||
|
getCoordinator().sendToKiosk(id, { type: "wake" });
|
||||||
|
return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -750,6 +750,15 @@ export function KioskEditPage(props: KioskEditProps) {
|
||||||
<div>Paired: {k.paired_at ? formatTime(k.paired_at) : "—"}</div>
|
<div>Paired: {k.paired_at ? formatTime(k.paired_at) : "—"}</div>
|
||||||
<div>Last seen: {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"}</div>
|
<div>Last seen: {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:1rem; padding-top:1rem; border-top:1px solid #eee">
|
||||||
|
<div style="font-size:0.85rem; font-weight:600; margin-bottom:0.5rem">Display Power (CEC)</div>
|
||||||
|
<form method="post" action={`/admin/kiosks/${k.id}/power/wake`} style="display:inline">
|
||||||
|
<button type="submit" class="btn btn-sm">Wake</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action={`/admin/kiosks/${k.id}/power/standby`} style="display:inline; margin-left:0.5rem">
|
||||||
|
<button type="submit" class="btn btn-sm btn-ghost">Standby</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Associated displays */}
|
{/* Associated displays */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue