From 5b30fb1ded4caae83b50f1a44fea456417edab8d Mon Sep 17 00:00:00 2001 From: egutierrez Date: Wed, 27 May 2026 11:13:03 +0200 Subject: [PATCH] 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). --- control.sh | 248 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100755 control.sh diff --git a/control.sh b/control.sh new file mode 100755 index 0000000..fb0fe0d --- /dev/null +++ b/control.sh @@ -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