Files
kanban/control.sh
egutierrez 5b30fb1ded chore: restore control.sh TUI launcher from issue/notifications-realtime
Script vivia solo en rama issue/notifications-realtime y se perdio al
hacer checkout master para el branch 0128. Es self-contained (no toca
otros archivos de esa rama). Permite ./control.sh para gestionar
backend (WSL) + frontend Vite (Windows).
2026-05-27 11:13:03 +02:00

249 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Kanban control TUI — gestiona backend (WSL) + frontend Vite (Windows) desde WSL.
# Lanzamientos fire-and-forget; status panel auto-refresca cada 2s.
# Lanzar: ./control.sh
set -u
BACKEND_PORT=8095
FRONTEND_PORT=5180
APP_DIR="/home/egutierrez/fn_registry/apps/kanban"
BACKEND_LOG="/tmp/kanban.log"
BUILD_LOG="/tmp/kanban_build.log"
MSG_FILE="/tmp/kanban_control.msg"
WIN_FRONT_DIR='C:\Users\egutierrez\fn_apps\kanban\frontend'
RED=$'\033[31m'; GRN=$'\033[32m'; YLW=$'\033[33m'; CYN=$'\033[36m'; BLD=$'\033[1m'; RST=$'\033[0m'
msg() { printf '%s\n' "$*" > "$MSG_FILE"; }
wsl_pid_on_port() {
local port=$1
ss -ltnp 2>/dev/null | awk -v p=":$port\$" '$4 ~ p {print $0}' \
| grep -oP 'pid=\K[0-9]+' | head -1
}
win_pid_on_port() {
local port=$1
netstat.exe -ano 2>/dev/null | tr -d '\r' \
| awk -v p=":$port\$" '$2 ~ p && $4 == "LISTENING" {print $5; exit}'
}
backend_building() {
[[ -f /tmp/kanban_build.pid ]] && kill -0 "$(cat /tmp/kanban_build.pid 2>/dev/null)" 2>/dev/null
}
# Build + launch en background — retorna inmediatamente
start_backend() {
if [[ -n $(wsl_pid_on_port "$BACKEND_PORT") ]]; then
msg "${YLW}backend ya corriendo${RST}"; return 0
fi
if backend_building; then
msg "${YLW}backend ya esta compilando, espera${RST}"; return 0
fi
local version
version=$(awk -F': ' '/^version:/ {print $2; exit}' "$APP_DIR/app.md" 2>/dev/null || echo dev)
msg "${CYN}lanzando backend en background (version=$version)...${RST}"
(
cd "$APP_DIR/backend" || exit 1
# Rebuild si: binario no existe, .go/.sql mas nuevos, app.md mas nuevo (bump de version)
if [[ ! -x kanban ]] \
|| [[ -n $(find . -maxdepth 3 \( -name '*.go' -o -name '*.sql' \) -newer kanban 2>/dev/null) ]] \
|| [[ "$APP_DIR/app.md" -nt kanban ]]; then
CGO_ENABLED=1 go build -tags fts5 \
-ldflags="-X main.Version=$version" \
-o kanban . > "$BUILD_LOG" 2>&1 || {
printf 'build failed — ver %s\n' "$BUILD_LOG" > "$MSG_FILE"
exit 1
}
fi
cd "$APP_DIR" || exit 1
KANBAN_CLAUDE_BIN=/home/egutierrez/.local/bin/claude \
setsid nohup ./backend/kanban --port "$BACKEND_PORT" --db ./operations.db \
> "$BACKEND_LOG" 2>&1 < /dev/null &
disown
) &
echo $! > /tmp/kanban_build.pid
disown
}
stop_backend() {
local pid
pid=$(wsl_pid_on_port "$BACKEND_PORT")
if [[ -z $pid ]]; then
msg "${YLW}backend ya parado${RST}"; return 0
fi
kill "$pid" 2>/dev/null
( sleep 1; kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null ) &
disown
msg "${GRN}backend stopped (pid $pid)${RST}"
}
wsl_ip() { hostname -I | awk '{print $1}'; }
# WSL frontend → Windows frontend (excluye node_modules, dist, .vite)
sync_frontend() {
local src="$APP_DIR/frontend/"
local dst="/mnt/c/Users/egutierrez/fn_apps/kanban/frontend/"
if [[ ! -d $dst ]]; then
msg "${RED}no existe $dst${RST}"; return 1
fi
rsync -a --delete \
--exclude node_modules --exclude dist --exclude .vite \
--exclude .cache --exclude tsconfig.tsbuildinfo \
"$src" "$dst" 2>&1 | tail -3
# pnpm install si package.json cambio
if ! cmp -s "$src/package.json" "$dst/package.json" 2>/dev/null \
|| [[ ! -d "$dst/node_modules" ]]; then
msg "${CYN}deps cambiaron, lanza pnpm install en Windows...${RST}"
cmd.exe /c start "" cmd /c "cd /d $WIN_FRONT_DIR && pnpm install" >/dev/null 2>&1 &
disown
fi
}
# Lanza ventana cmd Windows con pnpm dev — no bloquea
# Inyecta VITE_API_TARGET con IP WSL real porque localhost forwarding Win→WSL no es fiable
start_vite() {
if [[ -n $(win_pid_on_port "$FRONTEND_PORT") ]]; then
msg "${YLW}vite ya corriendo${RST}"; return 0
fi
sync_frontend
local ip target
ip=$(wsl_ip)
target="http://${ip}:${BACKEND_PORT}"
cmd.exe /c start "" cmd /c "cd /d $WIN_FRONT_DIR && set VITE_API_TARGET=$target && pnpm dev --port $FRONTEND_PORT --strictPort --host" >/dev/null 2>&1 &
disown
msg "${CYN}vite lanzado, proxy → $target${RST}"
}
stop_vite() {
local pid
pid=$(win_pid_on_port "$FRONTEND_PORT")
if [[ -z $pid ]]; then
msg "${YLW}vite ya parado${RST}"; return 0
fi
taskkill.exe /F /T /PID "$pid" >/dev/null 2>&1 &
disown
msg "${GRN}taskkill enviado a vite pid $pid${RST}"
}
kill_stale() {
local found=0 out=""
for pid in $(pgrep -f "backend/kanban --port" 2>/dev/null); do
local cmdl
cmdl=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null)
if ! grep -q -- "--port $BACKEND_PORT" <<<"$cmdl"; then
kill -9 "$pid" 2>/dev/null
out+="killed wsl pid $pid ($cmdl); "
found=1
fi
done
[[ $found -eq 0 ]] && msg "${GRN}sin huerfanos WSL${RST}" || msg "${GRN}${out}${RST}"
}
_prev_frame=""
build_frame() {
local bpid vpid hc others
bpid=$(wsl_pid_on_port "$BACKEND_PORT")
vpid=$(win_pid_on_port "$FRONTEND_PORT")
local out=""
out+=$(printf '%s=== Kanban control ===%s' "$BLD" "$RST")$'\n\n'
if [[ -n $bpid ]]; then
local rv av
rv=$(curl -s -m 1 "http://127.0.0.1:$BACKEND_PORT/api/version" | grep -oP '"version":"\K[^"]+' || echo "?")
av=$(awk -F': ' '/^version:/ {print $2; exit}' "$APP_DIR/app.md" 2>/dev/null || echo "?")
if [[ "$rv" == "$av" ]]; then
hc="${GRN}v$rv${RST}"
else
hc="${YLW}running=v$rv app.md=v$av (rebuild)${RST}"
fi
out+=$(printf ' backend (WSL :%s) %sUP%s pid %s %s' \
"$BACKEND_PORT" "$GRN" "$RST" "$bpid" "$hc")$'\n'
elif backend_building; then
out+=$(printf ' backend (WSL :%s) %sBUILDING/STARTING%s tail %s' \
"$BACKEND_PORT" "$YLW" "$RST" "$BUILD_LOG")$'\n'
else
out+=$(printf ' backend (WSL :%s) %sDOWN%s' "$BACKEND_PORT" "$RED" "$RST")$'\n'
fi
# frontend version + drift WSL↔Win
local fv drift
fv=$(grep -oP '"version":\s*"\K[^"]+' "$APP_DIR/frontend/package.json" 2>/dev/null || echo "?")
drift=$(diff -rq "$APP_DIR/frontend/src" "/mnt/c/Users/egutierrez/fn_apps/kanban/frontend/src" 2>/dev/null \
| grep -c -E "^(Files|Only)" || true)
local dlbl
if [[ ${drift:-0} -eq 0 ]]; then
dlbl="${GRN}sync${RST}"
else
dlbl="${YLW}drift=$drift (sync al start)${RST}"
fi
if [[ -n $vpid ]]; then
out+=$(printf ' vite (WIN :%s) %sUP%s pid %s v%s %s' "$FRONTEND_PORT" "$GRN" "$RST" "$vpid" "$fv" "$dlbl")$'\n'
else
out+=$(printf ' vite (WIN :%s) %sDOWN%s v%s %s' "$FRONTEND_PORT" "$RED" "$RST" "$fv" "$dlbl")$'\n'
fi
others=$(pgrep -af "backend/kanban --port" 2>/dev/null | grep -v -- "--port $BACKEND_PORT" || true)
if [[ -n $others ]]; then
out+=$(printf ' %sOTROS kanban backends WSL:%s' "$YLW" "$RST")$'\n'
out+=$(echo "$others" | sed 's/^/ /')$'\n'
fi
out+=$'\n'
out+=$(printf '%sUltimo evento:%s %s' "$CYN" "$RST" "$(tail -1 "$MSG_FILE" 2>/dev/null || echo '-')")$'\n\n'
out+="${BLD}Acciones${RST} (auto-refresh 2s, tecla suelta):"$'\n'
out+=" 1) Start backend 5) Start TODO"$'\n'
out+=" 2) Stop backend 6) Stop TODO"$'\n'
out+=" 3) Start vite 7) Mata kanban huerfanos"$'\n'
out+=" 4) Stop vite 8) Tail backend log"$'\n'
out+=" 9) Refrescar 0) Salir"$'\n'
out+="> "
printf '%s' "$out"
}
draw_status() {
local frame
frame=$(build_frame)
if [[ $frame == "$_prev_frame" ]]; then
return 0
fi
_prev_frame=$frame
# cursor home + frame + erase-to-end-of-display (limpia lineas residuales)
printf '\033[H%s\033[J' "$frame"
}
tail_log() {
clear
printf '%stail -f %s (Ctrl-C vuelve al menu)%s\n' "$CYN" "$BACKEND_LOG" "$RST"
trap 'trap - INT; return 0' INT
tail -f "$BACKEND_LOG" 2>/dev/null
trap - INT
}
menu() {
: > "$MSG_FILE"
# limpia pantalla una sola vez; redraw posterior usa cursor-home
printf '\033[2J\033[H'
trap 'printf "\033[?25h\n"; exit 0' EXIT INT TERM
printf '\033[?25l' # oculta cursor mientras dibujamos
while true; do
draw_status
# read con timeout 2s — refresco automatico si no hay tecla
local choice=""
if read -rsn1 -t 2 choice; then
case "$choice" in
1) start_backend ;;
2) stop_backend ;;
3) start_vite ;;
4) stop_vite ;;
5) start_backend; start_vite ;;
6) stop_vite; stop_backend ;;
7) kill_stale ;;
8) printf '\033[?25h'; tail_log; printf '\033[?25l'; _prev_frame=""; printf '\033[2J\033[H' ;;
9) : ;;
0|q|Q) printf '\033[?25h'; clear; exit 0 ;;
$'\n'|"") : ;;
*) msg "${RED}opcion invalida: $choice${RST}" ;;
esac
fi
done
}
menu