diff --git a/deploy/nginx.user-8443.conf b/deploy/nginx.user-8443.conf index c4ae32b..627afc9 100644 --- a/deploy/nginx.user-8443.conf +++ b/deploy/nginx.user-8443.conf @@ -45,6 +45,31 @@ http { proxy_set_header X-Forwarded-Proto https; } + location = /api/auth/check { + internal; + proxy_pass http://127.0.0.1:5180; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Forwarded-Proto https; + } + + location /term/ { + auth_request /api/auth/check; + error_page 401 = @term_login; + proxy_pass http://127.0.0.1:7681; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 6h; + proxy_send_timeout 6h; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto https; + } + + location @term_login { + return 302 /login; + } + location / { proxy_pass http://127.0.0.1:5180; proxy_http_version 1.1; diff --git a/deploy/systemd/local-kanban-ttyd.service b/deploy/systemd/local-kanban-ttyd.service new file mode 100644 index 0000000..e0cbc83 --- /dev/null +++ b/deploy/systemd/local-kanban-ttyd.service @@ -0,0 +1,13 @@ +[Unit] +Description=Local Kanban ttyd web terminal (tmux attach) +After=network-online.target + +[Service] +Type=simple +WorkingDirectory=/home/gahow/projects/kanban +ExecStart=/home/gahow/projects/kanban/scripts/start-ttyd.sh +Restart=always +RestartSec=3 + +[Install] +WantedBy=default.target diff --git a/scripts/install-user-systemd.sh b/scripts/install-user-systemd.sh index 50a1fa2..dfb2b7b 100755 --- a/scripts/install-user-systemd.sh +++ b/scripts/install-user-systemd.sh @@ -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" diff --git a/scripts/start-ttyd.sh b/scripts/start-ttyd.sh new file mode 100755 index 0000000..6f3cd8b --- /dev/null +++ b/scripts/start-ttyd.sh @@ -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" diff --git a/scripts/tmux-web-attach.sh b/scripts/tmux-web-attach.sh new file mode 100755 index 0000000..7b404de --- /dev/null +++ b/scripts/tmux-web-attach.sh @@ -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 [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