feat: add production assurance dashboard flow
This commit is contained in:
+179
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-command local CASAN Control Plane for demos and offline evaluation.
|
||||
# Platform-only: Core stays a non-blocking sensor/enforcer and local spool.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
COMMAND="${1:-start}"
|
||||
[[ $# -gt 0 ]] && shift || true
|
||||
|
||||
TARGET="${CASAN_APP_ROOT:-$PWD}"
|
||||
OPEN_BROWSER=1
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--root) TARGET="${2:-}"; shift 2 ;;
|
||||
--no-open) OPEN_BROWSER=0; shift ;;
|
||||
*) echo "casan dashboard: unknown argument: $1" >&2; exit 64 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || {
|
||||
echo "casan dashboard: project root does not exist: $TARGET" >&2
|
||||
exit 66
|
||||
}
|
||||
[[ -f "$TARGET/.casan/config.json" ]] || {
|
||||
echo "casan dashboard: $TARGET is not an adopted CASAN project" >&2
|
||||
exit 65
|
||||
}
|
||||
[[ -f "$SOURCE_ROOT/package.json" ]] || {
|
||||
echo "casan dashboard: Platform source bundle is incomplete" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
STATE_DIR="${CASAN_CONTROL_PLANE_STATE_DIR:-$TARGET/.specify/state/control-plane}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
API_PID_FILE="$STATE_DIR/api.pid"
|
||||
UI_PID_FILE="$STATE_DIR/ui.pid"
|
||||
API_LOG="$STATE_DIR/api.log"
|
||||
UI_LOG="$STATE_DIR/ui.log"
|
||||
DASHBOARD_URL="${CASAN_DASHBOARD_URL:-http://127.0.0.1:5174}"
|
||||
|
||||
alive() {
|
||||
local file="$1" pid
|
||||
[[ -f "$file" ]] || return 1
|
||||
pid="$(sed -n '1p' "$file" 2>/dev/null || true)"
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
enroll() {
|
||||
python3 - "$TARGET/.casan/config.json" "$DASHBOARD_URL" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
path, url = sys.argv[1:3]
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
config = json.load(handle)
|
||||
control_plane = config.get("control_plane")
|
||||
if not isinstance(control_plane, dict):
|
||||
control_plane = {}
|
||||
control_plane.update({
|
||||
"dashboard_url": url.rstrip("/"),
|
||||
"delivery": "local_spool",
|
||||
"receipt_links_enabled": True,
|
||||
})
|
||||
config["control_plane"] = control_plane
|
||||
directory = os.path.dirname(path)
|
||||
fd, temporary = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=directory)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(config, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
PY
|
||||
}
|
||||
|
||||
open_dashboard() {
|
||||
python3 - "$DASHBOARD_URL" <<'PY'
|
||||
import sys
|
||||
import webbrowser
|
||||
raise SystemExit(0 if webbrowser.open(sys.argv[1], new=2) else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
case "$COMMAND" in
|
||||
start)
|
||||
if alive "$API_PID_FILE" && alive "$UI_PID_FILE"; then
|
||||
enroll
|
||||
echo "CASAN_CONTROL_PLANE_READY url=$DASHBOARD_URL project=$TARGET"
|
||||
[[ "$OPEN_BROWSER" == "1" ]] && open_dashboard || true
|
||||
exit 0
|
||||
fi
|
||||
python3 - "$SOURCE_ROOT" "$TARGET" "$API_LOG" "$UI_LOG" "$API_PID_FILE" "$UI_PID_FILE" <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
source, target, api_log, ui_log, api_pid, ui_pid = sys.argv[1:7]
|
||||
environment = os.environ.copy()
|
||||
environment["CASAN_APP_ROOT"] = target
|
||||
with open(api_log, "ab", buffering=0) as api_output:
|
||||
api = subprocess.Popen(
|
||||
[os.path.join(source, "node_modules", ".bin", "tsx"), "src/main.ts"],
|
||||
cwd=os.path.join(source, "packages", "casan-control-panel", "backend"),
|
||||
env=environment,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=api_output,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
with open(ui_log, "ab", buffering=0) as ui_output:
|
||||
ui = subprocess.Popen(
|
||||
[
|
||||
os.path.join(source, "node_modules", ".bin", "vite"),
|
||||
"--host", "127.0.0.1", "--port", "5174",
|
||||
],
|
||||
cwd=os.path.join(source, "packages", "casan-control-panel", "frontend"),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=ui_output,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
for path, pid in ((api_pid, api.pid), (ui_pid, ui.pid)):
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("%s\n" % pid)
|
||||
PY
|
||||
ready=0
|
||||
for _attempt in $(seq 1 60); do
|
||||
if curl -fsS "http://127.0.0.1:3010/api/v1/project" >/dev/null 2>&1 \
|
||||
&& curl -fsS "$DASHBOARD_URL" >/dev/null 2>&1; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
if [[ "$ready" != "1" ]]; then
|
||||
echo "casan dashboard: Control Plane did not become ready" >&2
|
||||
echo "API log: $API_LOG" >&2
|
||||
echo "UI log: $UI_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
enroll
|
||||
echo "CASAN_CONTROL_PLANE_READY url=$DASHBOARD_URL project=$TARGET"
|
||||
echo "logs=$STATE_DIR"
|
||||
[[ "$OPEN_BROWSER" == "1" ]] && open_dashboard || true
|
||||
;;
|
||||
status)
|
||||
api="stopped"; ui="stopped"
|
||||
alive "$API_PID_FILE" && api="running"
|
||||
alive "$UI_PID_FILE" && ui="running"
|
||||
echo "CASAN_CONTROL_PLANE_STATUS api=$api ui=$ui url=$DASHBOARD_URL project=$TARGET"
|
||||
[[ "$api" == "running" && "$ui" == "running" ]]
|
||||
;;
|
||||
stop)
|
||||
for file in "$API_PID_FILE" "$UI_PID_FILE"; do
|
||||
if alive "$file"; then
|
||||
pid="$(sed -n '1p' "$file")"
|
||||
kill "$pid"
|
||||
fi
|
||||
rm -f "$file"
|
||||
done
|
||||
echo "CASAN_CONTROL_PLANE_STOPPED project=$TARGET"
|
||||
;;
|
||||
open)
|
||||
enroll
|
||||
echo "$DASHBOARD_URL"
|
||||
open_dashboard
|
||||
;;
|
||||
*)
|
||||
echo "casan: usage: casan dashboard <start|status|stop|open> [--root path] [--no-open]" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user