#!/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