ops: ttyd web terminal sidecar behind nginx auth_request

- tmux-web-attach.sh: attach browser to one window via link-window into a
  private temp session (no session-group pollution); window-size smallest
  while attached so the browser never gets cut off; mouse on for natural
  wheel/touch scrolling; status bar off; destroy-unattached cleanup
- start-ttyd.sh: ttyd on 127.0.0.1:7681, base path /term, url-arg enabled,
  Tokyo Night theme, system mono stack, fontSize 14
- systemd user unit + install script registration
- nginx: /term/ proxied with websocket upgrade, gated by auth_request to
  the kanban cookie check; unauthenticated -> 302 /login

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 21:20:40 +08:00
parent f7d6df8756
commit cd1e9e09e7
5 changed files with 124 additions and 0 deletions

View File

@@ -7,11 +7,14 @@ USER_UNIT_DIR="${HOME}/.config/systemd/user"
mkdir -p "${USER_UNIT_DIR}"
install -m 0644 "${ROOT_DIR}/deploy/systemd/local-kanban.service" "${USER_UNIT_DIR}/local-kanban.service"
install -m 0644 "${ROOT_DIR}/deploy/systemd/local-kanban-nginx-8443.service" "${USER_UNIT_DIR}/local-kanban-nginx-8443.service"
install -m 0644 "${ROOT_DIR}/deploy/systemd/local-kanban-ttyd.service" "${USER_UNIT_DIR}/local-kanban-ttyd.service"
systemctl --user daemon-reload
systemctl --user enable --now local-kanban.service
systemctl --user enable --now local-kanban-nginx-8443.service
systemctl --user enable --now local-kanban-ttyd.service
echo "Local Kanban is managed by user systemd:"
echo " systemctl --user status local-kanban.service"
echo " systemctl --user status local-kanban-nginx-8443.service"
echo " systemctl --user status local-kanban-ttyd.service"

23
scripts/start-ttyd.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# ttyd web terminal for tmux attach.
# Theme: Tokyo Night; font: system monospace stack (fonts render client-side,
# so we rely on whatever mono font the viewing device ships).
# xterm.js client options: https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec /usr/bin/ttyd \
-p 7681 \
-i 127.0.0.1 \
-W \
-a \
-b /term \
-T xterm-256color \
-t titleFixed='Kanban Terminal' \
-t fontSize=14 \
-t lineHeight=1.2 \
-t 'fontFamily=ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace' \
-t cursorBlink=true \
-t 'theme={"background":"#1a1b26","foreground":"#c0caf5","cursor":"#c0caf5","cursorAccent":"#1a1b26","selectionBackground":"#283457","black":"#15161e","red":"#f7768e","green":"#9ece6a","yellow":"#e0af68","blue":"#7aa2f7","magenta":"#bb9af7","cyan":"#7dcfff","white":"#a9b1d6","brightBlack":"#414868","brightRed":"#f7768e","brightGreen":"#9ece6a","brightYellow":"#e0af68","brightBlue":"#7aa2f7","brightMagenta":"#bb9af7","brightCyan":"#7dcfff","brightWhite":"#c0caf5"}' \
"${ROOT_DIR}/scripts/tmux-web-attach.sh"

60
scripts/tmux-web-attach.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Attach a browser (via ttyd) to a single tmux window by linking it into a
# private temporary session. Unlike grouped sessions, link-window leaves no
# permanent group label on the source session, and each browser tab gets an
# independent view (own active window, own status setting).
# Usage: tmux-web-attach.sh <session> [window-index]
set -euo pipefail
sess="${1:-}"
win="${2:-}"
if [ -z "$sess" ]; then
echo "missing session. open this page from the kanban Live Agents panel."
sleep 2
exit 1
fi
# Only accept existing session names; never interpret arbitrary input.
if ! tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -Fxq -- "$sess"; then
echo "unknown session: $sess"
sleep 2
exit 1
fi
# Default to the session's active window; validate the index either way.
if [ -z "$win" ]; then
win="$(tmux display-message -p -t "$sess" '#{window_index}')"
fi
if ! tmux list-windows -t "$sess" -F '#{window_index}' | grep -Fxq -- "$win"; then
echo "unknown window: $sess:$win"
sleep 2
exit 1
fi
src="$sess:$win"
tmp="web-$$-$RANDOM"
tmux new-session -d -s "$tmp" -x 220 -y 60
tmux set-option -t "$tmp" status off
# mouse on (session-scoped, local clients unaffected): wheel / touch scrolling
# from the browser enters copy-mode and scrolls history naturally, no prefix keys.
tmux set-option -t "$tmp" mouse on
# Replace the placeholder window with a link to the target window (-k kills the placeholder).
first="$(tmux display-message -p -t "$tmp" '#{window_index}')"
tmux link-window -k -s "$src" -t "$tmp:$first"
# A tmux window has a single size shared by every viewer. With the global
# "latest" policy a bigger local client cuts the browser view off, so pin this
# window to "smallest" while a web viewer is attached: the browser always sees
# the full content; a larger local client sees dotted padding instead.
tmux set-window-option -t "$src" window-size smallest
cleanup() {
tmux set-window-option -u -t "$src" window-size 2>/dev/null || true
tmux kill-session -t "$tmp" 2>/dev/null || true
}
trap cleanup EXIT HUP TERM INT
# destroy-unattached must be set only after attaching (it kills detached sessions instantly).
tmux attach -t "$tmp" \; set-option destroy-unattached on