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).
This commit is contained in:
Executable
+248
@@ -0,0 +1,248 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user