Status TV: Config files
Exact copies of every file deployed on the Status TV Pi (statusadmin@10.10.75.9).
~ is /home/statusadmin. See Status TV Setup for where each file fits in.
The source of truth is the devops-status-tv
repository (pi/ folder), deployed with Ansible. If you change a file there, update this page too.
~/status-tv/kiosk.sh
Owner: statusadmin, mode 755.
#!/bin/bash
# Status TV kiosk: one Chromium window showing Grafana via the local token
# proxy (nginx on 127.0.0.1:3000). Usage: kiosk.sh <name> (URL from urls.conf)
# <name> becomes the Wayland app_id; ~/.config/labwc/rc.xml uses it to place
# the window on the right screen. Started from ~/.config/autostart/status-tv-*.desktop;
# restarts Chromium if it exits.
NAME="$1"
URL=$(awk -v n="$NAME" '$1 == n { print $2 }' "$(dirname "$0")/urls.conf")
PROFILE="$HOME/.config/chromium-$NAME"
# Wait for the proxy (and network) to be up
for i in $(seq 1 60); do
curl -s -o /dev/null -m 3 http://127.0.0.1:3000/api/health && break
sleep 2
done
while true; do
# Avoid "Chromium didn't shut down correctly" bubble after power loss
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/; s/"exit_type":"[^"]*"/"exit_type":"Normal"/' \
"$PROFILE/Default/Preferences" 2>/dev/null
chromium \
--kiosk "$URL" \
--class="$NAME" \
--user-data-dir="$PROFILE" \
--ozone-platform=wayland \
--noerrdialogs \
--disable-infobars \
--no-first-run \
--disable-session-crashed-bubble \
--disable-features=Translate \
--password-store=basic \
--check-for-update-interval=31536000 \
--overscroll-history-navigation=0 \
--disable-pinch
sleep 5
done
~/status-tv/restart.sh
Owner: statusadmin, mode 755.
#!/bin/bash
# Restart both Status TV kiosk windows (e.g. after editing urls.conf).
export XDG_RUNTIME_DIR=/run/user/$(id -u) WAYLAND_DISPLAY=wayland-0
DIR=$(dirname "$(readlink -f "$0")")
pkill -f "$DIR/kiosk.sh"
pkill -x chromium
sleep 3
for name in $(awk '!/^#/ && NF { print $1 }' "$DIR/urls.conf"); do
setsid -f "$DIR/kiosk.sh" "$name" >/tmp/$name.log 2>&1 < /dev/null
sleep 2
done
~/status-tv/urls.conf
Owner: statusadmin, mode 644.
# Status TV dashboards: <kiosk name> <url> (read by kiosk.sh, no shell expansion)
# Grafana is reached via the local token proxy on 127.0.0.1:3000.
# Each screen plays a Grafana playlist (edit contents/interval in Grafana:
# Dashboards -> Playlists). Dashboards use their saved time range/variables.
status-tv-left http://127.0.0.1:3000/playlists/play/hetzv9p?kiosk
status-tv-right http://127.0.0.1:3000/playlists/play/hetrgxv?kiosk
# Previous single-dashboard links:
# status-tv-left http://127.0.0.1:3000/d/ef693154-9727-4c56-a7be-3e151fd0f11d/tms-stability-monitor?orgId=1&from=now-24h&to=now&timezone=browser&var-deploy_type=stable&var-active_pubsub_subs=$__all&refresh=1m&kiosk
# status-tv-right http://127.0.0.1:3000/d/6752404e-fa3e-427b-b2a5-41362fee85d8/eazytms?orgId=1&from=now-12h&to=now&timezone=Europe%2FHelsinki&var-deploy=stable&var-service=$__all&var-server_ips=$__all&refresh=30s&kiosk
~/.config/autostart/status-tv-left.desktop
Owner: statusadmin, mode 644.
[Desktop Entry]
Type=Application
Name=Status TV kiosk (left: TMS stability monitor)
Exec=/home/statusadmin/status-tv/kiosk.sh status-tv-left
X-GNOME-Autostart-enabled=true
~/.config/autostart/status-tv-right.desktop
Owner: statusadmin, mode 644.
[Desktop Entry]
Type=Application
Name=Status TV kiosk (right: eazytms)
Exec=/home/statusadmin/status-tv/kiosk.sh status-tv-right
X-GNOME-Autostart-enabled=true
~/.config/labwc/rc.xml
Owner: statusadmin, mode 644.
<?xml version="1.0"?>
<!-- Merged on top of /etc/xdg/labwc/rc.xml (labwc runs with -m).
Status TV: pin each kiosk window to its screen (see ~/status-tv/kiosk.sh). -->
<labwc_config>
<windowRules>
<windowRule identifier="status-tv-left">
<action name="MoveToOutput" output="HDMI-A-1" />
</windowRule>
<windowRule identifier="status-tv-right">
<action name="MoveToOutput" output="HDMI-A-2" />
</windowRule>
</windowRules>
</labwc_config>
~/.config/kanshi/config
Owner: statusadmin, mode 644.
# One screen (current Samsung on HDMI-A-2)
profile single {
output HDMI-A-2 enable mode 2560x1440@59.951 position 0,0
}
# Two screens: HDMI-A-1 left, HDMI-A-2 right (swap positions if physically reversed)
profile dual {
output HDMI-A-1 enable mode 2560x1440 position 0,0
output HDMI-A-2 enable mode 2560x1440@59.951 position 2560,0
}
/etc/nginx/sites-available/status-tv
Owner: root, mode 644; symlinked from /etc/nginx/sites-enabled/status-tv.
# Status TV: local-only proxy to Grafana that injects the read-only
# service-account token (sa-1-indola-status-tv). Chromium kiosk uses it.
map $http_upgrade $status_tv_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 127.0.0.1:3000;
server_name localhost;
location / {
proxy_pass https://dash.aholadigital.com;
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header Host dash.aholadigital.com;
proxy_set_header Origin https://dash.aholadigital.com;
proxy_set_header Cookie "";
include /etc/nginx/grafana-token.conf;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $status_tv_connection_upgrade;
proxy_read_timeout 3600s;
}
}
/etc/nginx/grafana-token.conf
Owner: root, mode 600. Template only: the real token is in Proton Pass.
# Copy to /etc/nginx/grafana-token.conf on the Pi (root:root, mode 600) and
# replace the placeholder with the Grafana service-account token.
# NEVER commit the real token.
proxy_set_header Authorization "Bearer glsa_REPLACE_ME";
/etc/systemd/timesyncd.conf.d/10-local-ntp.conf
Owner: root, mode 644.
[Time]
NTP=172.31.0.20 172.31.0.17
~/status-tv/tv-power.sh
Owner: statusadmin, mode 755.
#!/bin/bash
# Switch the Status TV screens on/off over HDMI-CEC (Samsung "Anynet+" must be on).
# tv-power.sh on | off | status | auto
# auto = apply the schedule from TV_ON / TV_OFF / TV_DAYS (set by the systemd unit):
# on between TV_ON and TV_OFF on TV_DAYS (date +%u: 1=Mon … 7=Sun, range "1-5"), else off.
# One /dev/cecN per Pi HDMI port: cec0 = HDMI-A-1 (left TV), cec1 = HDMI-A-2 (right TV).
set -u
ACTION="${1:-status}"
log() { echo "$(date '+%F %T') $*"; }
# Register as a playback device (not persistent across reboots) and print the
# physical address the TV gave this port, e.g. 1.0.0.0
setup() {
cec-ctl -d "$1" --playback --osd-name StatusTV >/dev/null 2>&1
cec-ctl -d "$1" 2>/dev/null | awk '/Physical Address/ { print $4; exit }'
}
state() {
timeout 8 cec-ctl -d "$1" --to 0 --give-device-power-status 2>/dev/null \
| awk '/pwr-state:/ { print $2; exit }'
}
power_on() { # wake the TV and switch it to the Pi's input
cec-ctl -d "$1" --to 0 --image-view-on >/dev/null 2>&1
cec-ctl -d "$1" --to 15 --active-source phys-addr="$2" >/dev/null 2>&1
}
power_off() { # Samsung ignores standby unless the Pi is the active source
cec-ctl -d "$1" --to 15 --active-source phys-addr="$2" >/dev/null 2>&1
sleep 2
cec-ctl -d "$1" --to 0 --standby >/dev/null 2>&1
}
if [ "$ACTION" = auto ]; then
day=$(date +%u) now=$(date +%H:%M)
first=${TV_DAYS%-*} last=${TV_DAYS#*-}
if [ "$day" -ge "$first" ] && [ "$day" -le "$last" ] \
&& [[ ! "$now" < "$TV_ON" ]] && [[ "$now" < "$TV_OFF" ]]; then
ACTION=on
else
ACTION=off
fi
log "schedule: days $TV_DAYS $TV_ON-$TV_OFF, now day $day $now -> $ACTION"
fi
rc=0
for dev in /dev/cec*; do
phys=$(setup "$dev")
if [ -z "$phys" ] || [ "$phys" = "f.f.f.f" ]; then
log "$dev: no TV connected"
continue
fi
case "$ACTION" in
on) want=on; power_on "$dev" "$phys" ;;
off) want=standby; power_off "$dev" "$phys" ;;
status) log "$dev ($phys): $(state "$dev")"; continue ;;
*) echo "usage: $0 on|off|status|auto" >&2; exit 2 ;;
esac
# Verify, retry once
for try in 1 2; do
sleep 8
now_state=$(state "$dev")
[ "$now_state" = "$want" ] && break
[ "$try" = 1 ] && { [ "$want" = on ] && power_on "$dev" "$phys" || power_off "$dev" "$phys"; }
done
log "$dev ($phys): $ACTION -> ${now_state:-no reply}"
[ "$now_state" = "$want" ] || rc=1
done
exit $rc
/etc/systemd/system/status-tv-tv-power.service
Owner: root, mode 644. Ansible template: values come from inventory.ini.
# Managed by devops-status-tv (Ansible) – schedule in inventory.ini
[Unit]
Description=Status TV: switch TVs on/off by schedule (HDMI-CEC)
After=multi-user.target
[Service]
Type=oneshot
User={{ ansible_user }}
Environment=TV_ON={{ tv_on_time }} TV_OFF={{ tv_off_time }} TV_DAYS={{ tv_days }}
ExecStart={{ home }}/status-tv/tv-power.sh auto
/etc/systemd/system/status-tv-tv-power.timer
Owner: root, mode 644. Ansible template: values come from inventory.ini.
# Managed by devops-status-tv (Ansible) – schedule in inventory.ini
{% set names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] %}
{% set first, last = tv_days.split('-') %}
[Unit]
Description=Status TV: TV power schedule
[Timer]
OnCalendar={{ names[first | int - 1] }}..{{ names[last | int - 1] }} *-*-* {{ tv_on_time }}:00
OnCalendar={{ names[first | int - 1] }}..{{ names[last | int - 1] }} *-*-* {{ tv_off_time }}:00
# Also apply the schedule shortly after boot (desktop + CEC up)
OnBootSec=2min
[Install]
WantedBy=timers.target
/etc/ssh/sshd_config.d/10-status-tv.conf
Owner: root, mode 644. Only deployed when ssh_keys_only=true (not yet).
# Managed by devops-status-tv (Ansible): SSH login with keys only.
# Keys: keys/*.pub in the repo. The password still works for sudo and on the
# Pi's own keyboard/screen (local console), so there's a way back in.
# Read before 50-cloud-init.conf (which says "yes"); sshd uses the first value.
PasswordAuthentication no
KbdInteractiveAuthentication no