Merge branch 'fix/doctor-host-install' into dev

This commit is contained in:
Juan Denis committed 2026-09-03 00:37:55 -04:00
commit 4eaf4e9801
4 files changed
+285 -44

No files matched your search

+7 -3
View File
@@ -168,11 +168,15 @@ rm -rf "$BUILD_DIR/frontend/node_modules"
find "$BUILD_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find "$BUILD_DIR" -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find "$BUILD_DIR" -type f -name '*.pyc' -delete 2>/dev/null || true
# Never strip the built SPA bundle: Vite emits real image assets into
# frontend/dist/assets (extension icons, etc.) that the panel 404s without.
# Never strip anything under frontend/: dist/assets holds the images Vite
# emitted (extension icons, etc.) that the panel 404s without, and src/assets
# holds the ones the JS imports — delete those and the shipped tree can no
# longer rebuild its own frontend (`serverkit doctor --fix` on 1.9.x died on
# 18 unresolved png imports, issue #127). The tar excludes above already keep
# docs/dev screenshots out; this pass only catches stragglers elsewhere.
# (Use ! -path, not -prune: -delete implies -depth, which disables -prune.)
find "$BUILD_DIR" -type f \( -name '*.png' -o -name '*.jpeg' -o -name '*.jpg' \) \
! -path "$BUILD_DIR/frontend/dist/*" -delete 2>/dev/null || true
! -path "$BUILD_DIR/frontend/*" -delete 2>/dev/null || true
# Runtime directory the app expects to exist.
mkdir -p "$BUILD_DIR/backend/instance"
+104 -4
View File
@@ -101,8 +101,10 @@ fi
# T2 — L6: cmd_start must warn (not raw-abort) when systemctl/docker fail
# (partial installs, non-systemd boxes). cmd_stop already had these guards.
# --------------------------------------------------------------------------
t="$WORK/t2"; mkdir -p "$t/bin" "$t/install"
t="$WORK/t2"; mkdir -p "$t/bin" "$t/install" "$t/nginx/sites-enabled"
CALL_LOG="$t/calls.log"; : > "$CALL_LOG"
# Legacy layout (nginx proxies / to the container) so the docker half runs.
printf 'server {\n location / { proxy_pass http://127.0.0.1:8080; }\n}\n' > "$t/nginx/sites-enabled/serverkit.conf"
cat > "$t/bin/systemctl" <<EOF
#!/usr/bin/env bash
printf 'systemctl %s\n' "\$*" >> "$CALL_LOG"
@@ -119,13 +121,14 @@ out="$(
export PATH="$t/bin:$PATH"
check_root() { :; }
INSTALL_DIR="$t/install"
NGINX_DIR="$t/nginx"
cmd_start 2>&1
)"
rc=$?
if [ "$rc" -eq 0 ] \
&& printf '%s' "$out" | grep -q 'Could not start backend' \
&& printf '%s' "$out" | grep -q 'Could not start frontend' \
&& grep -q 'docker compose up -d' "$CALL_LOG"; then
&& grep -q 'docker compose --profile legacy-frontend up -d frontend' "$CALL_LOG"; then
ok "start survives failing systemctl/docker with warnings (no raw abort)"
else
bad "start aborted or lost a warning: rc=$rc out=[$out]"
@@ -168,8 +171,9 @@ fi
# T4 — L7: `serverkit logs` on a journal-less box must not die before showing
# the Docker logs that would have worked.
# --------------------------------------------------------------------------
t="$WORK/t4"; mkdir -p "$t/bin" "$t/install"
t="$WORK/t4"; mkdir -p "$t/bin" "$t/install" "$t/nginx/sites-enabled"
CALL_LOG="$t/calls.log"; : > "$CALL_LOG"
printf 'server {\n location / { proxy_pass http://127.0.0.1:8080; }\n}\n' > "$t/nginx/sites-enabled/serverkit.conf"
cat > "$t/bin/journalctl" <<EOF
#!/usr/bin/env bash
printf 'journalctl %s\n' "\$*" >> "$CALL_LOG"
@@ -185,12 +189,13 @@ out="$(
set -Eeuo pipefail
export PATH="$t/bin:$PATH"
INSTALL_DIR="$t/install"
NGINX_DIR="$t/nginx"
cmd_logs all 2>&1
)"
rc=$?
if [ "$rc" -eq 0 ] \
&& printf '%s' "$out" | grep -q 'Could not read the backend journal' \
&& grep -q 'docker compose logs --tail=50' "$CALL_LOG"; then
&& grep -q 'docker compose --profile legacy-frontend logs --tail=50 frontend' "$CALL_LOG"; then
ok "logs falls through a failing journalctl to the Docker-logs half"
else
bad "logs died before the Docker half: rc=$rc out=[$out]"
@@ -442,6 +447,101 @@ else
bad "doctor healthy path broken: rc=$rc out=[$out]"
fi
# --------------------------------------------------------------------------
# T12b — issue #127: on a host install (nginx serves frontend/dist from disk,
# the install.sh default) doctor must recognise the static layout, pass, and
# never reach for `docker compose` — that built the whole panel image on
# the operator's box and died on 18 missing png imports.
# --------------------------------------------------------------------------
t="$WORK/t12b"
mkdir -p "$t/bin" "$t/install/backend/instance" "$t/install/frontend/dist" \
"$t/nginx/sites-available" "$t/nginx/sites-enabled" "$t/nginx/serverkit-locations"
printf 'x\n' > "$t/install/backend/instance/serverkit.db"
printf '<html></html>\n' > "$t/install/frontend/dist/index.html"
cat > "$t/nginx/sites-available/serverkit.conf" <<EOF
server {
listen 80;
root $t/install/frontend/dist;
location / {
try_files \$uri /index.html;
}
location /api {
proxy_pass http://127.0.0.1:5000;
}
}
EOF
# Recording docker stub: no containers, and every invocation is logged.
cat > "$t/bin/docker" <<EOF
#!/usr/bin/env bash
printf '%s\n' "\$*" >> "$t/docker.log"
exit 0
EOF
cat > "$t/bin/ss" <<'EOF'
#!/usr/bin/env bash
printf 'LISTEN 0 128 127.0.0.1:5000 users:(("gunicorn",pid=1,fd=1))\n'
exit 0
EOF
chmod +x "$t/bin"/*
out="$(
set -Eeuo pipefail
export PATH="$t/bin:$PATH"
INSTALL_DIR="$t/install"
NGINX_DIR="$t/nginx"
cmd_doctor 2>&1
)"
rc=$?
if [ "$rc" -eq 0 ] \
&& printf '%s' "$out" | grep -q 'Static bundle present' \
&& ! printf '%s' "$out" | grep -q 'is not running' \
&& ! grep -q 'compose' "$t/docker.log" 2>/dev/null; then
ok "doctor: static host install passes; no container check, no docker compose"
else
bad "doctor static layout: rc=$rc compose=[$(cat "$t/docker.log" 2>/dev/null)] out=[$out]"
fi
# --------------------------------------------------------------------------
# T12c — issue #127: static layout with the bundle missing. `--fix` must
# report it, point at `serverkit update`, and still never run compose.
# --------------------------------------------------------------------------
rm -f "$t/install/frontend/dist/index.html" "$t/docker.log"
out="$(
set -Eeuo pipefail
export PATH="$t/bin:$PATH"
INSTALL_DIR="$t/install"
NGINX_DIR="$t/nginx"
check_root() { :; } # --fix wants root; the sandbox is not
cmd_doctor --fix 2>&1
)"
rc=$?
if [ "$rc" -ne 0 ] \
&& printf '%s' "$out" | grep -q 'Frontend bundle missing' \
&& printf '%s' "$out" | grep -q 'serverkit update' \
&& ! grep -q 'compose' "$t/docker.log" 2>/dev/null; then
ok "doctor --fix: missing static bundle suggests 'serverkit update', never runs compose"
else
bad "doctor --fix static layout: rc=$rc compose=[$(cat "$t/docker.log" 2>/dev/null)] out=[$out]"
fi
# --------------------------------------------------------------------------
# T12d — frontend_mode reads the layout off the vhost: proxy_pass on / is the
# legacy container profile, a dist root is static, nothing at all is unknown.
# The legacy fix path must target the profile's service, not a bare
# `docker compose up -d` (which starts the all-in-one panel instead).
# --------------------------------------------------------------------------
u="$WORK/t12d"; mkdir -p "$u/nginx/sites-enabled" "$u/install"
m_unknown="$( INSTALL_DIR="$u/install"; NGINX_DIR="$u/nginx"; frontend_mode )"
printf 'server {\n location / { proxy_pass http://127.0.0.1:8080; }\n}\n' > "$u/nginx/sites-enabled/serverkit.conf"
m_container="$( INSTALL_DIR="$u/install"; NGINX_DIR="$u/nginx"; frontend_mode )"
printf 'server {\n root /opt/serverkit/frontend/dist;\n location /api { proxy_pass http://127.0.0.1:5000; }\n}\n' > "$u/nginx/sites-enabled/serverkit.conf"
m_static="$( INSTALL_DIR="$u/install"; NGINX_DIR="$u/nginx"; frontend_mode )"
if [ "$m_unknown" = unknown ] && [ "$m_container" = container ] && [ "$m_static" = static ] \
&& grep -q 'docker compose --profile legacy-frontend "\$@" frontend' "$CLI" \
&& [ "$(awk '/INSTALL_DIR/ {p=1; next} /^[[:space:]]*$/ {next} p && /docker compose (up|down|stop|restart|logs|ps)/ && !/--profile/ {c++} {p=0} END {print c+0}' "$CLI")" -eq 0 ]; then
ok "frontend_mode: unknown/container/static detected; legacy path targets the profile service"
else
bad "frontend_mode wrong: unknown=[$m_unknown] container=[$m_container] static=[$m_static], or a bare 'docker compose up -d' is back in the CLI"
fi
# --------------------------------------------------------------------------
# T13 — L12: list-sites port extraction without GNU grep -oP; conf.d layouts
# are listed too.
+10
View File
@@ -74,6 +74,16 @@ expect_text "$RELEASE" 'Refusing to overwrite existing release' \
expect_text "$RELEASE" 'overwrite_files: false' \
'release assets cannot be overwritten'
# The release tarball must be able to rebuild its own frontend: build-release
# used to strip every png outside frontend/dist, which emptied
# frontend/src/assets and made any rebuild from /opt/serverkit die on
# unresolved imports (issue #127).
BUILD_RELEASE="$ROOT/scripts/build-release.sh"
expect_text "$BUILD_RELEASE" '! -path "$BUILD_DIR/frontend/*"' \
'release image cleanup leaves the whole frontend/ tree alone'
expect_absent "$BUILD_RELEASE" '! -path "$BUILD_DIR/frontend/dist/*" -delete' \
'release image cleanup no longer strips frontend/src assets'
if [ "$failures" -ne 0 ]; then
printf '\n%d release workflow guard assertion(s) failed\n' "$failures" >&2
exit 1
+164 -37
View File
@@ -4,7 +4,8 @@
#
# Architecture:
# - Backend: systemd service (runs directly on host)
# - Frontend: Docker container (nginx)
# - Frontend: static bundle served by host nginx (or, on the legacy
# layout, the serverkit-frontend container — see frontend_mode)
#
# Usage: serverkit <command> [options]
#
@@ -40,6 +41,60 @@ NGINX_DIR="${SERVERKIT_NGINX_DIR:-/etc/nginx}"
BACKEND_SERVICE="serverkit"
FRONTEND_CONTAINER="serverkit-frontend"
# How the SPA is served on this box. install.sh has host nginx serve the
# prebuilt bundle straight from frontend/dist; the `serverkit-frontend`
# container only exists under the opt-in `legacy-frontend` compose profile.
# Read the answer off the nginx vhost instead of assuming either layout —
# a plain `docker compose up -d` on a host install does not start that
# container, it builds the whole all-in-one panel image (issue #127).
#
# static nginx `root .../frontend/dist` (install.sh, the default)
# container nginx proxies `location /` to a local port (legacy profile)
# unknown no serverkit vhost found
serverkit_vhost() {
local conf
for conf in "$NGINX_DIR/sites-enabled/serverkit.conf" \
"$NGINX_DIR/sites-available/serverkit.conf" \
"$NGINX_DIR/conf.d/serverkit.conf"; do
if [ -f "$conf" ]; then
echo "$conf"
return 0
fi
done
return 1
}
# The directory nginx serves the SPA from (static mode only).
frontend_static_root() {
local conf
conf="$(serverkit_vhost)" || return 1
sed -n 's/^[[:space:]]*root[[:space:]]\{1,\}\([^;]*\);.*/\1/p' "$conf" \
| grep 'frontend/dist' | head -1
}
frontend_mode() {
local conf
if conf="$(serverkit_vhost)"; then
if grep -Eq '^[[:space:]]*root[[:space:]]+[^;]*frontend/dist' "$conf"; then
echo static
elif grep -Eq 'proxy_pass[[:space:]]+http://127\.0\.0\.1:[0-9]+' "$conf"; then
echo container
else
echo unknown
fi
elif [ -f "$INSTALL_DIR/frontend/dist/index.html" ]; then
# No vhost yet but the bundle is on disk: install.sh layout.
echo static
else
echo unknown
fi
}
# The compose invocation that actually starts the legacy frontend container.
compose_frontend() {
docker compose --profile legacy-frontend "$@" frontend
}
# Helper functions
print_header() {
local ver
@@ -356,9 +411,13 @@ cmd_start() {
# readable warning, not a raw set -e abort.
systemctl start $BACKEND_SERVICE || print_warning "Could not start backend service '$BACKEND_SERVICE' (is systemd available?)"
# Start frontend (Docker)
cd "$INSTALL_DIR"
docker compose up -d || print_warning "Could not start frontend container (is Docker running?)"
# Frontend: static files need nginx up; only the legacy layout has a container.
if [ "$(frontend_mode)" = container ]; then
cd "$INSTALL_DIR"
compose_frontend up -d || print_warning "Could not start frontend container (is Docker running?)"
else
systemctl start nginx || print_warning "Could not start nginx (it serves the frontend)"
fi
sleep 3
print_success "ServerKit started"
@@ -373,9 +432,12 @@ cmd_stop() {
# Stop backend (systemd)
systemctl stop $BACKEND_SERVICE || true
# Stop frontend (Docker)
cd "$INSTALL_DIR"
docker compose down || true
# Legacy frontend container only. nginx stays up on a host install: it
# also fronts every managed site, not just the panel.
if [ "$(frontend_mode)" = container ]; then
cd "$INSTALL_DIR"
compose_frontend stop || true
fi
print_success "ServerKit stopped"
}
@@ -389,9 +451,12 @@ cmd_restart() {
# manager is reported, never a raw abort.
systemctl restart $BACKEND_SERVICE || print_warning "Could not restart backend service '$BACKEND_SERVICE' (is systemd available?)"
# Restart frontend (Docker)
cd "$INSTALL_DIR"
docker compose restart || print_warning "Could not restart frontend container (is Docker running?)"
if [ "$(frontend_mode)" = container ]; then
cd "$INSTALL_DIR"
compose_frontend restart || print_warning "Could not restart frontend container (is Docker running?)"
else
systemctl reload nginx || print_warning "Could not reload nginx (it serves the frontend)"
fi
sleep 3
print_success "ServerKit restarted"
@@ -410,11 +475,22 @@ cmd_status_brief() {
fi
# Frontend status
if docker ps --format '{{.Names}}' | grep -q $FRONTEND_CONTAINER; then
print_success "Frontend: running"
else
print_error "Frontend: stopped"
fi
case "$(frontend_mode)" in
container)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
print_success "Frontend: running (container)"
else
print_error "Frontend: stopped (container)"
fi
;;
*)
if systemctl is-active --quiet nginx 2>/dev/null; then
print_success "Frontend: served by nginx"
else
print_error "Frontend: nginx is not running"
fi
;;
esac
# API health
if curl -s http://127.0.0.1:5000/api/v1/system/health > /dev/null 2>&1; then
@@ -434,10 +510,17 @@ cmd_status() {
systemctl status $BACKEND_SERVICE --no-pager -l 2>/dev/null || print_warning "Backend service not found"
echo ""
echo "Frontend Container (Docker):"
echo "─────────────────────────────"
cd "$INSTALL_DIR"
docker compose ps 2>/dev/null || print_warning "Frontend container not found"
if [ "$(frontend_mode)" = container ]; then
echo "Frontend Container (Docker):"
echo "─────────────────────────────"
cd "$INSTALL_DIR"
compose_frontend ps 2>/dev/null || print_warning "Frontend container not found"
else
echo "Frontend (static, served by nginx):"
echo "─────────────────────────────"
echo " root: $(frontend_static_root 2>/dev/null || echo "$INSTALL_DIR/frontend/dist")"
systemctl status nginx --no-pager -l 2>/dev/null | head -5 || print_warning "nginx service not found"
fi
echo ""
cmd_status_brief
@@ -456,8 +539,14 @@ cmd_logs() {
fi
;;
frontend)
cd "$INSTALL_DIR"
docker compose logs -f || print_warning "Could not read frontend container logs (is Docker running?)"
if [ "$(frontend_mode)" = container ]; then
cd "$INSTALL_DIR"
compose_frontend logs -f || print_warning "Could not read frontend container logs (is Docker running?)"
else
print_info "The frontend is static files served by nginx; following nginx logs..."
tail -f /var/log/nginx/access.log /var/log/nginx/error.log 2>/dev/null \
|| print_warning "Could not read /var/log/nginx/*.log"
fi
;;
all|*)
print_info "Showing backend logs (Ctrl+C to switch to frontend)..."
@@ -471,10 +560,16 @@ cmd_logs() {
print_warning "journalctl not found — skipping backend logs"
fi
echo ""
print_info "Showing frontend logs..."
echo ""
cd "$INSTALL_DIR"
docker compose logs --tail=50 || print_warning "Could not read frontend container logs (is Docker running?)"
if [ "$(frontend_mode)" = container ]; then
print_info "Showing frontend logs..."
echo ""
cd "$INSTALL_DIR"
compose_frontend logs --tail=50 || print_warning "Could not read frontend container logs (is Docker running?)"
else
print_info "Showing nginx logs (the frontend is static files served by nginx)..."
echo ""
tail -n 50 /var/log/nginx/error.log 2>/dev/null || print_warning "Could not read /var/log/nginx/error.log"
fi
echo ""
print_info "For live logs, use: serverkit logs backend OR serverkit logs frontend"
;;
@@ -1103,6 +1198,13 @@ cmd_doctor() {
print_warning "SKIP: $1 (run with --fix to auto-fix)"
}
# For issues doctor can diagnose but must not fix on its own (a missing
# release bundle, a layout it cannot tell apart): say what to run instead
# of promising an auto-fix that does not exist.
doctor_hint() {
echo " Try: $1"
}
# ── 1. Backend service ──────────────────────────────────────
echo "Backend"
echo "───────────────────────────────────────"
@@ -1131,15 +1233,39 @@ cmd_doctor() {
echo ""
# ── 2. Frontend container ───────────────────────────────────
# ── 2. Frontend ─────────────────────────────────────────────
# Two layouts exist (see frontend_mode). Check the one this box actually
# uses; never "fix" a static install by starting containers.
FRONTEND_RUNNING=false
FRONTEND_PORT=""
FRONTEND_MODE="$(frontend_mode)"
if command -v docker >/dev/null 2>&1; then
echo "Frontend"
echo "───────────────────────────────────────"
echo "Frontend"
echo "───────────────────────────────────────"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
case "$FRONTEND_MODE" in
static)
STATIC_ROOT="$(frontend_static_root 2>/dev/null || true)"
[ -n "$STATIC_ROOT" ] || STATIC_ROOT="$INSTALL_DIR/frontend/dist"
if [ -f "$STATIC_ROOT/index.html" ]; then
doctor_ok "Static bundle present, served by nginx from $STATIC_ROOT"
else
doctor_issue "Frontend bundle missing: $STATIC_ROOT/index.html"
echo " The host install serves a prebuilt bundle through nginx; there is no container to start."
doctor_hint "serverkit update (reinstalls the release, bundle included)"
fi
# A leftover legacy container is harmless but wastes a port and confuses
# port-mismatch checks; point at it, do not touch it.
if command -v docker >/dev/null 2>&1 \
&& docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
print_warning "Container '$FRONTEND_CONTAINER' is running but nginx serves the frontend from disk; it is unused"
doctor_hint "cd $INSTALL_DIR && docker compose --profile legacy-frontend stop frontend"
fi
;;
container)
if ! command -v docker >/dev/null 2>&1; then
doctor_issue "nginx proxies the frontend to a container but Docker is not installed or not in PATH"
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
FRONTEND_RUNNING=true
# Get the actual host port the container is mapped to
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
@@ -1148,14 +1274,14 @@ cmd_doctor() {
doctor_issue "Container '$FRONTEND_CONTAINER' is not running"
if [ "$FIX_MODE" = "--fix" ]; then
cd "$INSTALL_DIR"
docker compose up -d 2>/dev/null && {
compose_frontend up -d 2>/dev/null && {
sleep 3
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | sed -n 's/.*:\([0-9][0-9]*\).*/\1/p' || true)
FRONTEND_RUNNING=true
doctor_fixed "Started frontend container (port $FRONTEND_PORT)"
} || print_error " Could not start container"
else
doctor_skip "docker compose up -d"
doctor_skip "docker compose --profile legacy-frontend up -d frontend"
fi
fi
@@ -1166,11 +1292,12 @@ cmd_doctor() {
doctor_issue "Container running but not responding on port $FRONTEND_PORT"
fi
fi
else
echo "Frontend"
echo "───────────────────────────────────────"
doctor_issue "Docker not installed or not in PATH; skipping frontend checks"
fi
;;
*)
doctor_issue "Cannot tell how the frontend is served: no serverkit vhost under $NGINX_DIR and no $INSTALL_DIR/frontend/dist"
doctor_hint "serverkit update (reinstalls the release and its nginx config)"
;;
esac
echo ""