chore(specs): sync spec_kit state and realtime stack wiring #14
@@ -1,28 +1,154 @@
|
||||
# Axe d’amélioration : badge & conformité
|
||||
# Tasks autonomie locale (execution)
|
||||
|
||||
- Automatiser la génération, vérification et publication des badges (sécurité, SBOM, doc, qualité, communauté, coverage)
|
||||
- Centraliser guides badge dans docs/badges/
|
||||
- Vérifier la fraîcheur des badges à chaque commit (timestamp ≥ commit)
|
||||
- Inclure badges et rapports dans l’evidence pack
|
||||
- Ajouter checklist badge en tête du README
|
||||
- Documenter la politique badge & conformité (docs/COMPLIANCE.md)
|
||||
- Planifier un audit badge à chaque release majeure
|
||||
Last updated: 2026-02-21
|
||||
|
||||
## Planification audit badge
|
||||
Format:
|
||||
|
||||
- À chaque release majeure, exécuter un audit badge :
|
||||
- Vérifier la couleur et l’actualisation de chaque badge
|
||||
- Vérifier la présence des rapports JSON
|
||||
- Vérifier l’inclusion dans l’evidence pack
|
||||
- Documenter les résultats dans docs/badges/audit_<release>.md
|
||||
# Easter Egg musique concrète
|
||||
- `[ ]` non fait
|
||||
- `[x]` fait
|
||||
- `AC` = acceptance criteria
|
||||
- `Evidence` = fichier/commande de preuve
|
||||
|
||||
_« Les tâches sont des sons trouvés : chaque action, chaque gate, est une pièce du puzzle acousmatique. »_ — Pierre Schaeffer
|
||||
# Tasks (Backlog exécutable)
|
||||
## Sprint actuel
|
||||
|
||||
Format conseillé (copiable en GitHub Issues) :
|
||||
- [x] T-001 - Merger la PR autonomie ZeroClaw
|
||||
- AC: PR mergee sur `main`.
|
||||
- Evidence: `https://github.com/electron-rare/Kill_LIFE/pull/5`
|
||||
|
||||
- [ ] T1 — ...
|
||||
- AC: ...
|
||||
- Evidence: ...
|
||||
- [ ] T2 — ...
|
||||
- [x] T-002 - Fermer ou merger la PR miroir restante
|
||||
- AC: plus de PR redondante ouverte pour le meme scope.
|
||||
- Evidence: `gh pr list --state open`
|
||||
|
||||
- [x] T-003 - Configurer secret fallback OpenRouter local
|
||||
- AC: `~/.zeroclaw/env` present avec `OPENROUTER_API_KEY` et mode `600`.
|
||||
- Evidence: `ls -l ~/.zeroclaw/env`
|
||||
|
||||
- [x] T-004 - Installer backend Prometheus local
|
||||
- AC: commande `prometheus --version` disponible.
|
||||
- Evidence: sortie shell `prometheus, version ...`
|
||||
|
||||
- [x] T-005 - Stabiliser pairing bearer auto
|
||||
- AC: `artifacts/zeroclaw/pair_token.txt` utilisable pour webhook sans override manuel.
|
||||
- Evidence: `tools/ai/zeroclaw_webhook_send.sh --repo-hint rtc "pairing check"`
|
||||
- Status: `2026-02-21` done (401 resolved + webhook HTTP 200 after provider/model auto-fix in stack bootstrap).
|
||||
|
||||
- [x] T-006 - Activer fallback IA locale (macOS)
|
||||
- AC: `ollama` installe, service actif, modele local disponible, stack capable de le preferer.
|
||||
- Evidence: `tools/ai/ollama_local_setup.sh --no-pull --no-warmup` + config `default_provider = "ollama"`.
|
||||
- Status: `2026-02-21` done (`llama3.2:1b` local); mode local est optionnel via `ZEROCLAW_PREFER_LOCAL_AI=1` pour garder la fiabilite webhook par defaut.
|
||||
|
||||
- [x] T-007 - Renforcer mode local-only agentic (ZeroClaw + OpenClaw)
|
||||
- AC: `tools/ai/zeroclaw_dual_chat.sh <repo> --local-only --provider-check` choisit un provider local (`ollama` ou `lmstudio`) sinon exit explicite.
|
||||
- Evidence: sortie provider-check + `openclaw models status --plain`.
|
||||
- Status: `2026-02-21` done (`openclaw` default model force sur `ollama/llama3.2:1b`, fallbacks `openai-codex` puis `openrouter`).
|
||||
|
||||
- [x] T-008 - Scanner les options provider/agentic disponibles
|
||||
- AC: rapport local avec statut runtime `openclaw/ollama/lmstudio/litellm` et matrice providers ZeroClaw.
|
||||
- Evidence: `tools/ai/zeroclaw_provider_scan.sh --no-refresh` -> `artifacts/zeroclaw/provider_options.md`.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [x] T-009 - Verrouiller le mapping matériel par cible
|
||||
- AC: la détection automatique affecte `zacus` aux ports `usbmodem`/S3 et `rtc` à l’Audio Kit (pas d’interversion habituelle).
|
||||
- Evidence:
|
||||
- `ZEROCLAW_ZACUS_UPLOAD_PORT_HINT="1a86,usbmodem,ch340,freenove" tools/ai/zeroclaw_hw_firmware_loop.sh zacus --port '' --monitor-secs 5`
|
||||
- `ZEROCLAW_RTC_UPLOAD_PORT_HINT="cp2102,10c4,esp32audiokit,audio" tools/ai/zeroclaw_hw_firmware_loop.sh rtc --port '' --monitor-secs 5`
|
||||
- Status: `2026-02-21` done (`usbmodem` prioritaire pour zacus, `usbmodem` pénalisé pour rtc).
|
||||
|
||||
## Boucle hardware RTC
|
||||
|
||||
- [x] T-101 - Discover hardware RTC
|
||||
- AC: au moins un port detecte pour carte RTC.
|
||||
- Evidence: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
|
||||
|
||||
- [x] T-102 - Build firmware RTC
|
||||
- AC: `pio run -e esp32dev` termine sans erreur bloquante.
|
||||
- Evidence: logs build RTC.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [x] T-103 - Upload + monitor RTC (forced default)
|
||||
- AC: `pio run -e esp32dev -t upload` puis monitor 60s executes.
|
||||
- Evidence: logs upload/monitor RTC.
|
||||
- Status: `2026-02-21` done (`ESP32-D0WD-V3` flash + serial monitor output captured).
|
||||
|
||||
- [x] T-104 - Trace webhook RTC
|
||||
- AC: une ligne JSONL avec `repo_hint=rtc` apparait.
|
||||
- Evidence: `artifacts/zeroclaw/conversations.jsonl`
|
||||
- Status: `2026-02-21` done (`http_status=200`, `ok=true`).
|
||||
|
||||
## Boucle hardware Zacus
|
||||
|
||||
- [x] T-201 - Discover hardware Zacus
|
||||
- AC: au moins un port detecte pour carte Zacus.
|
||||
- Evidence: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
|
||||
|
||||
- [x] T-202 - Build firmware Zacus
|
||||
- AC: `pio run -e esp32dev` (dans `hardware/firmware`) termine sans erreur bloquante.
|
||||
- Evidence: logs build Zacus.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [x] T-203 - Upload + monitor Zacus (forced default)
|
||||
- AC: `pio run -e <env valide> -t upload` puis monitor executes.
|
||||
- Evidence: logs upload/monitor Zacus.
|
||||
- Status: `2026-02-21` done (`ESP32-S3` detecte, env fallback `freenove_esp32s3`, flash + monitor OK).
|
||||
|
||||
- [x] T-204 - Trace webhook Zacus
|
||||
- AC: une ligne JSONL avec `repo_hint=zacus` apparait.
|
||||
- Evidence: `artifacts/zeroclaw/conversations.jsonl`
|
||||
- Status: `2026-02-21` done (`http_status=200`, `ok=true`).
|
||||
|
||||
## Observabilite et cout
|
||||
|
||||
- [x] T-301 - Stack endpoints smoke
|
||||
- AC: `3000/health`, `8788`, `9090/-/ready` tous OK.
|
||||
- Evidence: captures `curl`.
|
||||
|
||||
- [x] T-302 - Dry-run webhook budget
|
||||
- AC: `--dry-run` passe sans ecriture execution JSONL.
|
||||
- Evidence: sortie script + diff JSONL.
|
||||
- Status: `2026-02-21` done.
|
||||
|
||||
- [x] T-303 - Quota call limiter
|
||||
- AC: depassement quota bloque avec code non-zero.
|
||||
- Evidence: message `[budget] hourly call limit reached`.
|
||||
- Status: `2026-02-21` done (test sandbox local `max_calls=1`, second call blocked with exit 12).
|
||||
|
||||
- [x] T-304 - Quota chars limiter
|
||||
- AC: message trop long bloque avec code non-zero.
|
||||
- Evidence: message `[budget] message length ... exceeds ...`.
|
||||
- Status: `2026-02-21` done (`ZEROCLAW_WEBHOOK_MAX_CHARS=5`, exit 11).
|
||||
|
||||
## Integrations Open WebUI + n8n
|
||||
|
||||
- [x] T-401 - Ajouter API alerts + autotriage backend
|
||||
- AC: endpoints `/api/alerts`, `/api/alerts/ack`, `/api/pr/autotriage` disponibles.
|
||||
- Evidence: `tools/ai/zeroclaw_orchestrator_server.py`.
|
||||
|
||||
- [x] T-402 - Etendre WebUI orchestrateur
|
||||
- AC: panneau Alerts + Integrations status + action `PR autotriage now`.
|
||||
- Evidence: `tools/ai/zeroclaw_stack_up.sh`.
|
||||
|
||||
- [x] T-403 - Refactor Pipe Open WebUI en mapping mixte
|
||||
- AC: pipe expose `zc.router` + modeles dedies et parsing `/run`/`/pr`.
|
||||
- Evidence: `tools/ai/integrations/openwebui/zeroclaw_orchestrator_pipe.py`.
|
||||
|
||||
- [x] T-404 - Ajouter workflow n8n PR autotriage
|
||||
- AC: workflow avec Webhook + Cron fallback + alert/question outputs.
|
||||
- Evidence: `tools/ai/integrations/n8n/zeroclaw_pr_autotriage_workflow.json`.
|
||||
|
||||
- [x] T-405 - Ajouter provisioning local Docker integrations
|
||||
- AC: compose + scripts `up/down/status` + import n8n + assist Open WebUI.
|
||||
- Evidence:
|
||||
- `tools/ai/integrations/docker/docker-compose.openwebui-n8n.yml`
|
||||
- `tools/ai/zeroclaw_integrations_up.sh`
|
||||
- `tools/ai/zeroclaw_integrations_down.sh`
|
||||
- `tools/ai/zeroclaw_integrations_status.sh`
|
||||
- `tools/ai/zeroclaw_integrations_import_n8n.sh`
|
||||
- `tools/ai/zeroclaw_integrations_openwebui_assist.sh`
|
||||
|
||||
## Definition of done
|
||||
|
||||
- [x] Au moins une boucle complete RTC + Zacus executee en local.
|
||||
- [x] Dashboard live exploitable pour suivi continu.
|
||||
- [x] Prometheus disponible avec target gateway scrapee.
|
||||
- [x] Logs et preuves archives dans `artifacts/zeroclaw/`.
|
||||
- [x] Aucune commande documentee n'utilise `-e native` ou `-e test`.
|
||||
|
||||
@@ -1,36 +1,70 @@
|
||||
# TODO: Dual Hardware Autonomy (RTC + Zacus)
|
||||
# TODO: Dual hardware autonomy runbook (RTC + Zacus)
|
||||
|
||||
Last updated: 2026-02-21
|
||||
|
||||
## Phase A: Local Baseline
|
||||
## Immediate now
|
||||
|
||||
- [ ] Run `tools/ai/zeroclaw_dual_bootstrap.sh` and capture hardware snapshot.
|
||||
- [ ] Validate `tools/ai/zeroclaw_dual_chat.sh rtc -m "<diagnostic prompt>"`.
|
||||
- [ ] Validate `tools/ai/zeroclaw_dual_chat.sh zacus -m "<diagnostic prompt>"`.
|
||||
- [ ] Record detected ports and map preferred role per board.
|
||||
- [x] I-001 - Merge autonomie stack into `main` (`PR #5`).
|
||||
- [x] I-002 - Configure local secret file `~/.zeroclaw/env` (`OPENROUTER_API_KEY` placeholder + mode `600`).
|
||||
- [x] I-003 - Ensure local Prometheus backend exists (`prometheus` binary installed).
|
||||
- [x] I-004 - Resolve open mirror PR redundancy (`PR #7` merge/close decision).
|
||||
- [x] I-005 - Enable local AI fallback (`ollama` + local model) for credit savings.
|
||||
- [x] I-006 - Enforce local-only chat mode option (`--local-only`) with `ollama/lmstudio` detection.
|
||||
- [x] I-007 - Align OpenClaw default model to local `ollama/llama3.2:1b` + cloud fallbacks for continuity.
|
||||
- [x] I-008 - Add provider/agentic scanner (`tools/ai/zeroclaw_provider_scan.sh`) for live capability matrix.
|
||||
|
||||
## Phase B: Repo Specs and PR Cadence
|
||||
## Daily autonomous sequence
|
||||
|
||||
- [ ] Create/refresh one issue in `RTC_BL_PHONE` for ZeroClaw-assisted hardware loop.
|
||||
- [ ] Create/refresh one issue in `le-mystere-professeur-zacus` for ZeroClaw-assisted hardware loop.
|
||||
- [ ] Open one small PR per repo focused on one gate (build, tests, hardware smoke, docs).
|
||||
- [ ] Require code review pass before merge (`gh pr review --approve` only after checks).
|
||||
- [x] D-001 - `tools/ai/zeroclaw_stack_down.sh` then `ZEROCLAW_PROM_MODE=auto tools/ai/zeroclaw_stack_up.sh`.
|
||||
- [x] D-002 - Smoke endpoints:
|
||||
- `curl -fsS http://127.0.0.1:3000/health`
|
||||
- `curl -fsS http://127.0.0.1:8788/`
|
||||
- `curl -fsS http://127.0.0.1:9090/-/ready`
|
||||
- [x] D-003 - RTC loop:
|
||||
- `tools/ai/zeroclaw_dual_chat.sh rtc --provider-check`
|
||||
- `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh rtc` (build+upload+monitor forced default)
|
||||
- webhook trace with `--repo-hint rtc`
|
||||
- status: `2026-02-21` done (flash + monitor + webhook HTTP 200).
|
||||
- [x] D-004 - Zacus loop:
|
||||
- `tools/ai/zeroclaw_dual_chat.sh zacus --provider-check`
|
||||
- `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
|
||||
- `tools/ai/zeroclaw_hw_firmware_loop.sh zacus` (build+upload+monitor forced default)
|
||||
- webhook trace with `--repo-hint zacus`
|
||||
- status: `2026-02-21` done (ESP32-S3 mismatch auto-corrected to `freenove_esp32s3`).
|
||||
- [x] D-005 - Review `artifacts/zeroclaw/gateway.log` + `conversations.jsonl`.
|
||||
|
||||
## Phase C: Autonomy + Cost Optimization
|
||||
- [x] D-006 - Corriger l'association cible↔port
|
||||
- `RTC_UPLOAD_PORT_HINT` cible Audio Kit (`cp2102`, `esp32audiokit`, `audio`).
|
||||
- `ZACUS_UPLOAD_PORT_HINT` cible Freenove/S3 (`usbmodem`, `1a86`, `ch340`, `freenove`).
|
||||
- status: `2026-02-21` done.
|
||||
|
||||
- [ ] Keep prompts short, target one repo at a time.
|
||||
- [ ] Use provider auto-fallback (`copilot` -> `openai-codex` -> `openrouter`) to avoid dead sessions.
|
||||
- [ ] Add repo-level path filters in workflows to avoid expensive irrelevant runs.
|
||||
- [ ] Use `workflow_dispatch` for hardware-required jobs to avoid noisy CI failures.
|
||||
## Hardware safety gates
|
||||
|
||||
## Phase D: Hardware Robustness
|
||||
- [x] H-001 - Flash/upload/monitor are forced by default when a board is detected.
|
||||
- [x] H-002 - Resolve stable serial target before upload.
|
||||
- [x] H-003 - Keep per-run logs under `artifacts/zeroclaw/`.
|
||||
|
||||
- [ ] Add serial-port resolver step before every upload/flash action.
|
||||
- [ ] Fail fast if no expected USB device is detected.
|
||||
- [ ] Archive logs per run for replayability (`artifacts/<timestamp>/...`).
|
||||
## Cost/control gates
|
||||
|
||||
## Exit Criteria
|
||||
- [x] C-001 - Validate `tools/ai/zeroclaw_webhook_send.sh --dry-run`.
|
||||
- [x] C-002 - Validate hourly quota guard (`ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR`).
|
||||
- [x] C-003 - Validate message length guard (`ZEROCLAW_WEBHOOK_MAX_CHARS`).
|
||||
|
||||
- [ ] Both repos can be targeted with one command (`rtc` or `zacus`) without workspace leakage.
|
||||
- [ ] Hardware discovery passes before action on connected boards.
|
||||
- [ ] At least one successful PR cycle completed per repo with this orchestration path.
|
||||
## Exit criteria
|
||||
|
||||
- [x] E-001 - One successful complete loop RTC in local hardware.
|
||||
- [x] E-002 - One successful complete loop Zacus in local hardware.
|
||||
- [x] E-003 - Dashboard live usable for continuous supervision.
|
||||
- [x] E-004 - Prometheus target scrape confirmed on gateway metrics.
|
||||
|
||||
## Integrations (Open WebUI + n8n)
|
||||
|
||||
- [x] I-201 - Add mixed Open WebUI Pipe mapping (`zc.router` + dedicated models).
|
||||
- [x] I-202 - Add n8n PR autotriage workflow (webhook + cron fallback).
|
||||
- [x] I-203 - Add local docker compose stack for Open WebUI (`3001`) and n8n (`5678`).
|
||||
- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`, `openwebui_assist`).
|
||||
- [ ] I-205 - Validate end-to-end import + activation on local Docker runtime.
|
||||
- command: `tools/ai/zeroclaw_integrations_up.sh`
|
||||
- command: `tools/ai/zeroclaw_integrations_status.sh`
|
||||
- command: `tools/ai/zeroclaw_integrations_import_n8n.sh`
|
||||
|
||||
@@ -117,6 +117,34 @@ Format:
|
||||
- Evidence: message `[budget] message length ... exceeds ...`.
|
||||
- Status: `2026-02-21` done (`ZEROCLAW_WEBHOOK_MAX_CHARS=5`, exit 11).
|
||||
|
||||
## Integrations Open WebUI + n8n
|
||||
|
||||
- [x] T-401 - Ajouter API alerts + autotriage backend
|
||||
- AC: endpoints `/api/alerts`, `/api/alerts/ack`, `/api/pr/autotriage` disponibles.
|
||||
- Evidence: `tools/ai/zeroclaw_orchestrator_server.py`.
|
||||
|
||||
- [x] T-402 - Etendre WebUI orchestrateur
|
||||
- AC: panneau Alerts + Integrations status + action `PR autotriage now`.
|
||||
- Evidence: `tools/ai/zeroclaw_stack_up.sh`.
|
||||
|
||||
- [x] T-403 - Refactor Pipe Open WebUI en mapping mixte
|
||||
- AC: pipe expose `zc.router` + modeles dedies et parsing `/run`/`/pr`.
|
||||
- Evidence: `tools/ai/integrations/openwebui/zeroclaw_orchestrator_pipe.py`.
|
||||
|
||||
- [x] T-404 - Ajouter workflow n8n PR autotriage
|
||||
- AC: workflow avec Webhook + Cron fallback + alert/question outputs.
|
||||
- Evidence: `tools/ai/integrations/n8n/zeroclaw_pr_autotriage_workflow.json`.
|
||||
|
||||
- [x] T-405 - Ajouter provisioning local Docker integrations
|
||||
- AC: compose + scripts `up/down/status` + import n8n + assist Open WebUI.
|
||||
- Evidence:
|
||||
- `tools/ai/integrations/docker/docker-compose.openwebui-n8n.yml`
|
||||
- `tools/ai/zeroclaw_integrations_up.sh`
|
||||
- `tools/ai/zeroclaw_integrations_down.sh`
|
||||
- `tools/ai/zeroclaw_integrations_status.sh`
|
||||
- `tools/ai/zeroclaw_integrations_import_n8n.sh`
|
||||
- `tools/ai/zeroclaw_integrations_openwebui_assist.sh`
|
||||
|
||||
## Definition of done
|
||||
|
||||
- [x] Au moins une boucle complete RTC + Zacus executee en local.
|
||||
|
||||
@@ -57,3 +57,14 @@ Last updated: 2026-02-21
|
||||
- [x] E-002 - One successful complete loop Zacus in local hardware.
|
||||
- [x] E-003 - Dashboard live usable for continuous supervision.
|
||||
- [x] E-004 - Prometheus target scrape confirmed on gateway metrics.
|
||||
|
||||
## Integrations (Open WebUI + n8n)
|
||||
|
||||
- [x] I-201 - Add mixed Open WebUI Pipe mapping (`zc.router` + dedicated models).
|
||||
- [x] I-202 - Add n8n PR autotriage workflow (webhook + cron fallback).
|
||||
- [x] I-203 - Add local docker compose stack for Open WebUI (`3001`) and n8n (`5678`).
|
||||
- [x] I-204 - Add runtime scripts (`integrations_up/down/status`, `import_n8n`, `openwebui_assist`).
|
||||
- [ ] I-205 - Validate end-to-end import + activation on local Docker runtime.
|
||||
- command: `tools/ai/zeroclaw_integrations_up.sh`
|
||||
- command: `tools/ai/zeroclaw_integrations_status.sh`
|
||||
- command: `tools/ai/zeroclaw_integrations_import_n8n.sh`
|
||||
|
||||
@@ -57,12 +57,22 @@ PROM_MANAGED_FILE="$ART_DIR/prometheus.managed"
|
||||
WATCHER_SCRIPT="$ROOT_DIR/tools/ai/zeroclaw_watch_1min.sh"
|
||||
ORCHESTRATOR_SERVER="$ROOT_DIR/tools/ai/zeroclaw_orchestrator_server.py"
|
||||
RT_LOG="$ART_DIR/realtime_1min.log"
|
||||
INTEGRATIONS_DIR="$ROOT_DIR/tools/ai/integrations"
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$CONVO_FILE"
|
||||
touch "$GW_LOG"
|
||||
touch "$RT_LOG"
|
||||
|
||||
sync_integrations() {
|
||||
if [[ ! -d "$INTEGRATIONS_DIR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
rm -rf "$ART_DIR/integrations"
|
||||
mkdir -p "$ART_DIR/integrations"
|
||||
cp -R "$INTEGRATIONS_DIR"/. "$ART_DIR/integrations/"
|
||||
}
|
||||
|
||||
if [[ -f "$HOME/.zeroclaw/config.toml" ]]; then
|
||||
chmod 600 "$HOME/.zeroclaw/config.toml" >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -713,6 +723,8 @@ case "$PROM_MODE" in
|
||||
;;
|
||||
esac
|
||||
|
||||
sync_integrations
|
||||
|
||||
cat >"$INDEX_FILE" <<EOF
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -878,6 +890,56 @@ cat >"$INDEX_FILE" <<EOF
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.workbench-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.compact {
|
||||
min-height: 320px;
|
||||
}
|
||||
.select,
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: "SF Pro Text", "Segoe UI", Arial, sans-serif;
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
}
|
||||
.inline-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.hub-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.hub-tab-btn {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
}
|
||||
.hub-tab-btn.active {
|
||||
background: #245fa1;
|
||||
color: #fff;
|
||||
border-color: #245fa1;
|
||||
}
|
||||
.hub-tab-panel {
|
||||
display: none;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.hub-tab-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
.question-item {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
@@ -949,6 +1011,7 @@ cat >"$INDEX_FILE" <<EOF
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.ops-grid { grid-template-columns: 1fr; }
|
||||
.workbench-grid { grid-template-columns: 1fr; }
|
||||
.questions-grid { grid-template-columns: 1fr; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.panel { min-height: 320px; }
|
||||
@@ -963,7 +1026,7 @@ cat >"$INDEX_FILE" <<EOF
|
||||
Polling interval: <code>1s</code> |
|
||||
Display cap: <code>500 lines/panel</code> |
|
||||
Prometheus: <code>$PROM_STATUS</code> |
|
||||
API: <code>/api/status</code> <code>/api/run</code>
|
||||
API: <code>/api/status</code> <code>/api/run</code> <code>/api/pr/scan</code> <code>/api/pr/autotriage</code> <code>/api/alerts</code>
|
||||
</div>
|
||||
<div id="questionAlert" class="alert-banner"></div>
|
||||
<div class="links">
|
||||
@@ -972,6 +1035,13 @@ cat >"$INDEX_FILE" <<EOF
|
||||
<a href="/realtime_1min.log">/realtime_1min.log</a>
|
||||
<a href="/orchestrator.log">/orchestrator.log</a>
|
||||
<a href="/prometheus.yml">/prometheus.yml</a>
|
||||
<a href="/integrations/README.md">/integrations/README.md</a>
|
||||
<a href="/integrations/openwebui/zeroclaw_orchestrator_pipe.py">openwebui pipe</a>
|
||||
<a href="/integrations/n8n/zeroclaw_orchestrator_workflow.json">n8n workflow</a>
|
||||
<a href="/integrations/n8n/zeroclaw_pr_autotriage_workflow.json">n8n pr-autotriage workflow</a>
|
||||
<a href="/integrations/docker/docker-compose.openwebui-n8n.yml">compose openwebui+n8n</a>
|
||||
<a href="/integrations/langgraph/README.md">langgraph runbook</a>
|
||||
<a href="/integrations/autogen/README.md">autogen runbook</a>
|
||||
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/health">/health</a>
|
||||
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/metrics">/metrics</a>
|
||||
<a href="http://$PROM_HOST:$PROM_PORT/targets">prometheus /targets</a>
|
||||
@@ -1023,6 +1093,114 @@ cat >"$INDEX_FILE" <<EOF
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="workbench-grid">
|
||||
<section class="panel compact">
|
||||
<h2>Agent Workbench</h2>
|
||||
<div class="body">
|
||||
<p class="hint">Agents specializes: orchestrator, WebUI/UX, firmware RTC, firmware Zacus, QA/PR.</p>
|
||||
<select id="agentRoleSelect" class="select"></select>
|
||||
<textarea id="agentMissionInput">Stabilise orchestration UX, unblock firmware loops, and propose next PR steps.</textarea>
|
||||
<div class="btn-row">
|
||||
<button onclick="refreshAgents()">Refresh Agents</button>
|
||||
<button onclick="deriveAgentPrompts()">Generate Role Prompts</button>
|
||||
<button onclick="applyAgentPrompts()">Apply to RTC/Zacus</button>
|
||||
<button class="primary" onclick="runAgentPreferredAction()">Run Preferred Chain</button>
|
||||
</div>
|
||||
<pre id="agentStatus">(agent status)</pre>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel compact">
|
||||
<h2>PR Cockpit (gh)</h2>
|
||||
<div class="body">
|
||||
<div class="inline-row">
|
||||
<select id="prRepoSelect" class="select">
|
||||
<option value="electron-rare/Kill_LIFE">Kill_LIFE</option>
|
||||
<option value="electron-rare/RTC_BL_PHONE">RTC_BL_PHONE</option>
|
||||
<option value="electron-rare/le-mystere-professeur-zacus">Zacus</option>
|
||||
</select>
|
||||
<input id="prNumberInput" class="input" type="number" min="1" placeholder="PR #" />
|
||||
</div>
|
||||
<div class="inline-row">
|
||||
<select id="prMergeMethodSelect" class="select">
|
||||
<option value="squash">squash</option>
|
||||
<option value="rebase">rebase</option>
|
||||
<option value="merge">merge</option>
|
||||
</select>
|
||||
<label class="hint"><input id="prDeleteBranchInput" type="checkbox" checked> delete branch</label>
|
||||
</div>
|
||||
<textarea id="prBodyInput">LGTM from orchestrator.</textarea>
|
||||
<div class="btn-row">
|
||||
<button onclick="refreshPrScan()">Scan Open PRs</button>
|
||||
<button class="primary" onclick="runPrAutotriageNow()">PR autotriage now</button>
|
||||
<button onclick="runPrAction('checks')">Checks</button>
|
||||
<button onclick="runPrAction('view')">View</button>
|
||||
<button onclick="runPrAction('approve')">Approve</button>
|
||||
<button onclick="runPrAction('request_changes')">Request changes</button>
|
||||
<button onclick="runPrAction('comment')">Comment</button>
|
||||
<button class="primary" onclick="runPrAction('merge_auto')">Merge auto</button>
|
||||
<button onclick="runPrAction('merge_admin')">Merge admin</button>
|
||||
</div>
|
||||
<pre id="prSummary">(PR summary)</pre>
|
||||
<pre id="prActionResult">(PR action result)</pre>
|
||||
|
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="workbench-grid">
|
||||
<section class="panel compact">
|
||||
<h2>Alerts</h2>
|
||||
<div class="body">
|
||||
<div class="btn-row">
|
||||
<button onclick="refreshAlerts()">Refresh Alerts</button>
|
||||
<button onclick="ackAllAlerts()">Ack All</button>
|
||||
</div>
|
||||
<pre id="alertsSummary">(alerts summary)</pre>
|
||||
<div id="activeAlerts">(no active alerts)</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel compact">
|
||||
<h2>Integrations Status</h2>
|
||||
<div class="body">
|
||||
<pre id="integrationsStatus">(integrations status)</pre>
|
||||
<div class="btn-row">
|
||||
<button onclick="window.open('http://127.0.0.1:3001','_blank')">Open WebUI</button>
|
||||
<button onclick="window.open('http://127.0.0.1:5678','_blank')">Open n8n</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="workbench-grid">
|
||||
<section class="panel compact">
|
||||
<h2>Workflow & Agents</h2>
|
||||
<div class="body">
|
||||
<div class="hub-tabs">
|
||||
<button id="hubTabAgentsBtn" class="hub-tab-btn active" onclick="setHubTab('agents')">Agents</button>
|
||||
<button id="hubTabWorkflowsBtn" class="hub-tab-btn" onclick="setHubTab('workflows')">Workflows</button>
|
||||
<button id="hubTabRecapBtn" class="hub-tab-btn" onclick="setHubTab('recap')">Recap Usage</button>
|
||||
<button onclick="refreshWorkflowHub()">Refresh</button>
|
||||
</div>
|
||||
<div id="hubTabAgents" class="hub-tab-panel active">
|
||||
<pre id="hubAgents">(loading agents)</pre>
|
||||
</div>
|
||||
<div id="hubTabWorkflows" class="hub-tab-panel">
|
||||
<div id="hubWorkflows">(loading workflows)</div>
|
||||
</div>
|
||||
<div id="hubTabRecap" class="hub-tab-panel">
|
||||
<pre id="hubRecap">(loading recap)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel compact">
|
||||
<h2>Quick Start</h2>
|
||||
<div class="body">
|
||||
<p class="hint">Point d'entrée recommandé.</p>
|
||||
<pre id="hubQuickStart">1) tools/ai/zeroclaw_stack_up.sh
|
||||
2) tools/ai/zeroclaw_integrations_up.sh
|
||||
3) Ouvrir http://127.0.0.1:8788/ (monitoring)
|
||||
4) Ouvrir http://127.0.0.1:3001/ (Open WebUI)
|
||||
5) Ouvrir http://127.0.0.1:5678/ (n8n)</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="questions-grid">
|
||||
<section class="panel">
|
||||
<h2>Questions / Decisions</h2>
|
||||
@@ -1069,6 +1247,17 @@ cat >"$INDEX_FILE" <<EOF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alertPopup" class="modal">
|
||||
<div class="modal-card">
|
||||
<h3>Alert</h3>
|
||||
<p class="small" id="alertPopupMeta"></p>
|
||||
<pre id="alertPopupBody"></pre>
|
||||
<div class="btn-row">
|
||||
<button id="alertPopupAckBtn">Ack</button>
|
||||
<button onclick="closeAlertPopup()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const MAX_LINES = 500;
|
||||
const POLL_MS = 1000;
|
||||
@@ -1081,6 +1270,12 @@ cat >"$INDEX_FILE" <<EOF
|
||||
let lastOk = null;
|
||||
let runInFlight = false;
|
||||
let lastPopupQuestionId = Number(localStorage.getItem("zc_last_popup_question_id") || "0");
|
||||
let lastPopupAlertId = Number(localStorage.getItem("zc_last_popup_alert_id") || "0");
|
||||
let agentCatalog = [];
|
||||
let latestDerivedAgent = null;
|
||||
let prScanCache = null;
|
||||
let workflowHubCache = null;
|
||||
let hubTab = "agents";
|
||||
|
||||
function trimTail(lines) {
|
||||
if (lines.length <= MAX_LINES) return lines;
|
||||
@@ -1092,6 +1287,14 @@ cat >"$INDEX_FILE" <<EOF
|
||||
return String(input);
|
||||
}
|
||||
|
||||
function prettyJson(input) {
|
||||
try {
|
||||
return JSON.stringify(input, null, 2);
|
||||
} catch (error) {
|
||||
return safeText(input);
|
||||
}
|
||||
}
|
||||
|
||||
function renderConvo(line) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
@@ -1159,6 +1362,519 @@ cat >"$INDEX_FILE" <<EOF
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function selectedPrRepo() {
|
||||
return document.getElementById("prRepoSelect").value;
|
||||
}
|
||||
|
||||
function selectedPrNumber() {
|
||||
return Number(document.getElementById("prNumberInput").value || "0");
|
||||
}
|
||||
|
||||
function selectedPrMergeMethod() {
|
||||
return document.getElementById("prMergeMethodSelect").value || "squash";
|
||||
}
|
||||
|
||||
function selectedPrBody() {
|
||||
return document.getElementById("prBodyInput").value.trim();
|
||||
}
|
||||
|
||||
function selectedAgentRole() {
|
||||
return document.getElementById("agentRoleSelect").value || "orchestrator";
|
||||
}
|
||||
|
||||
function selectedAgentMission() {
|
||||
return document.getElementById("agentMissionInput").value.trim();
|
||||
}
|
||||
|
||||
function updateAgentStatus(text) {
|
||||
document.getElementById("agentStatus").textContent = text;
|
||||
}
|
||||
|
||||
function renderAgentOptions() {
|
||||
const select = document.getElementById("agentRoleSelect");
|
||||
const previous = select.value;
|
||||
select.innerHTML = "";
|
||||
if (!agentCatalog.length) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = "orchestrator";
|
||||
opt.textContent = "orchestrator";
|
||||
select.appendChild(opt);
|
||||
} else {
|
||||
for (const agent of agentCatalog) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = safeText(agent.id);
|
||||
opt.textContent = safeText(agent.label) + " [" + safeText(agent.id) + "]";
|
||||
select.appendChild(opt);
|
||||
}
|
||||
}
|
||||
if (previous) {
|
||||
select.value = previous;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAgents() {
|
||||
try {
|
||||
const data = await fetchJson("/api/agents");
|
||||
agentCatalog = Array.isArray(data.agents) ? data.agents : [];
|
||||
renderAgentOptions();
|
||||
const lines = [];
|
||||
lines.push("roles=" + safeText(agentCatalog.length));
|
||||
for (const agent of agentCatalog) {
|
||||
lines.push(
|
||||
safeText(agent.id) +
|
||||
" -> focus: " + safeText(agent.focus) +
|
||||
" | preferred_action=" + safeText(agent.preferred_action)
|
||||
);
|
||||
}
|
||||
updateAgentStatus(lines.join("\n"));
|
||||
} catch (error) {
|
||||
updateAgentStatus("agents api error: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deriveAgentPrompts() {
|
||||
const payload = {
|
||||
role: selectedAgentRole(),
|
||||
message: selectedAgentMission(),
|
||||
};
|
||||
try {
|
||||
const response = await fetch("/api/agents/derive", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await response.text();
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = body ? JSON.parse(body) : {};
|
||||
} catch (error) {
|
||||
parsed = { raw: body };
|
||||
}
|
||||
if (!response.ok || !parsed.ok) {
|
||||
throw new Error(parsed.error || ("HTTP " + response.status));
|
||||
}
|
||||
latestDerivedAgent = parsed;
|
||||
const lines = [
|
||||
"role=" + safeText(parsed.role) + " label=" + safeText(parsed.role_label),
|
||||
"preferred_action=" + safeText(parsed.preferred_action),
|
||||
"",
|
||||
"[general]",
|
||||
safeText(parsed.general_prompt),
|
||||
];
|
||||
updateAgentStatus(lines.join("\n"));
|
||||
setStatus("agent prompts generated for role " + safeText(parsed.role), false);
|
||||
} catch (error) {
|
||||
updateAgentStatus("derive failed: " + error.message);
|
||||
setStatus("agent derive failed: " + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAgentPrompts() {
|
||||
if (!latestDerivedAgent) {
|
||||
setStatus("run Generate Role Prompts first", true);
|
||||
return;
|
||||
}
|
||||
document.getElementById("generalPromptInput").value = safeText(latestDerivedAgent.general_prompt);
|
||||
document.getElementById("rtcPromptInput").value = safeText(latestDerivedAgent.rtc_prompt);
|
||||
document.getElementById("zacusPromptInput").value = safeText(latestDerivedAgent.zacus_prompt);
|
||||
setStatus("applied agent-derived prompts to RTC/Zacus/general", false);
|
||||
}
|
||||
|
||||
async function runAgentPreferredAction() {
|
||||
if (!latestDerivedAgent) {
|
||||
await deriveAgentPrompts();
|
||||
}
|
||||
if (!latestDerivedAgent) {
|
||||
return;
|
||||
}
|
||||
applyAgentPrompts();
|
||||
const action = safeText(latestDerivedAgent.preferred_action || "daily_chain");
|
||||
const needsMessage = action === "rtc_operator_chain" || action === "zacus_operator_chain";
|
||||
await runAction(action, needsMessage);
|
||||
}
|
||||
|
||||
function summarizePrScan(scan) {
|
||||
if (!scan) return "(no PR scan yet)";
|
||||
const lines = [];
|
||||
const totals = scan.totals || {};
|
||||
lines.push(
|
||||
"open_prs=" + safeText(totals.open_prs) +
|
||||
" generated_at=" + safeText(scan.generated_at)
|
||||
);
|
||||
const repos = Array.isArray(scan.repos) ? scan.repos : [];
|
||||
for (const repoEntry of repos) {
|
||||
if (!repoEntry.ok) {
|
||||
lines.push("! " + safeText(repoEntry.repo) + " error=" + safeText(repoEntry.error));
|
||||
continue;
|
||||
}
|
||||
lines.push("- " + safeText(repoEntry.repo) + " open=" + safeText(repoEntry.open_count));
|
||||
}
|
||||
lines.push("");
|
||||
const prs = Array.isArray(scan.all_prs) ? scan.all_prs : [];
|
||||
for (const pr of prs.slice(0, 40)) {
|
||||
lines.push(
|
||||
"[" + safeText(pr.repo) + " #" + safeText(pr.number) + "] " +
|
||||
safeText(pr.title) +
|
||||
" | checks=" + safeText(pr.checksBucket) +
|
||||
" | review=" + safeText(pr.reviewDecision) +
|
||||
" | merge=" + safeText(pr.mergeStateStatus)
|
||||
);
|
||||
lines.push(" " + safeText(pr.url));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function refreshPrScan() {
|
||||
try {
|
||||
const data = await fetchJson("/api/pr/scan");
|
||||
prScanCache = data;
|
||||
document.getElementById("prSummary").textContent = summarizePrScan(data);
|
||||
} catch (error) {
|
||||
document.getElementById("prSummary").textContent = "pr scan error: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runPrAction(prAction) {
|
||||
const repo = selectedPrRepo();
|
||||
const number = selectedPrNumber();
|
||||
const body = selectedPrBody();
|
||||
if (number <= 0) {
|
||||
setStatus("PR number is required for action " + prAction, true);
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
pr_action: prAction,
|
||||
repo: repo,
|
||||
number: number,
|
||||
body: body,
|
||||
merge_method: selectedPrMergeMethod(),
|
||||
delete_branch: document.getElementById("prDeleteBranchInput").checked,
|
||||
};
|
||||
if (prAction === "merge_auto" || prAction === "merge_admin") {
|
||||
const ok = window.confirm(
|
||||
"Confirm " + prAction + " on " + repo + " #" + String(number) + "?"
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/pr/action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const raw = await response.text();
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = raw ? JSON.parse(raw) : {};
|
||||
} catch (error) {
|
||||
parsed = { raw: raw };
|
||||
}
|
||||
if (!response.ok || parsed.ok === false) {
|
||||
throw new Error(parsed.error || ("HTTP " + response.status));
|
||||
}
|
||||
document.getElementById("prActionResult").textContent = prettyJson(parsed);
|
||||
setStatus("pr action ok: " + prAction, false);
|
||||
await refreshPrScan();
|
||||
} catch (error) {
|
||||
document.getElementById("prActionResult").textContent = "pr action failed: " + error.message;
|
||||
setStatus("pr action failed: " + prAction + " -> " + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPrAutotriageNow() {
|
||||
const payload = {
|
||||
repos: [
|
||||
"electron-rare/Kill_LIFE",
|
||||
"electron-rare/RTC_BL_PHONE",
|
||||
"electron-rare/le-mystere-professeur-zacus"
|
||||
],
|
||||
strict: {
|
||||
allow_draft: false,
|
||||
checks_bucket_required: "pass",
|
||||
merge_state_required: ["CLEAN"]
|
||||
},
|
||||
actions: {
|
||||
approve: true,
|
||||
auto_merge: true,
|
||||
merge_method: selectedPrMergeMethod(),
|
||||
delete_branch: document.getElementById("prDeleteBranchInput").checked
|
||||
},
|
||||
dry_run: false
|
||||
};
|
||||
try {
|
||||
const response = await fetch("/api/pr/autotriage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const raw = await response.text();
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = raw ? JSON.parse(raw) : {};
|
||||
} catch (error) {
|
||||
parsed = { raw: raw };
|
||||
}
|
||||
if (!response.ok && response.status !== 207) {
|
||||
throw new Error(parsed.error || ("HTTP " + response.status));
|
||||
}
|
||||
document.getElementById("prActionResult").textContent = prettyJson(parsed);
|
||||
const sum = parsed.summary || {};
|
||||
setStatus(
|
||||
"autotriage scanned=" + safeText(sum.scanned) +
|
||||
" eligible=" + safeText(sum.eligible) +
|
||||
" approved=" + safeText(sum.approved) +
|
||||
" merge_auto=" + safeText(sum.merge_auto_requested),
|
||||
false
|
||||
);
|
||||
await refreshPrScan();
|
||||
await refreshAlerts();
|
||||
} catch (error) {
|
||||
document.getElementById("prActionResult").textContent = "pr autotriage failed: " + error.message;
|
||||
setStatus("pr autotriage failed: " + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function setHubTab(tab) {
|
||||
hubTab = tab;
|
||||
const tabs = ["agents", "workflows", "recap"];
|
||||
for (const name of tabs) {
|
||||
const panel = document.getElementById("hubTab" + name.charAt(0).toUpperCase() + name.slice(1));
|
||||
const btn = document.getElementById("hubTab" + name.charAt(0).toUpperCase() + name.slice(1) + "Btn");
|
||||
if (!panel || !btn) continue;
|
||||
const active = name === tab;
|
||||
panel.classList.toggle("active", active);
|
||||
btn.classList.toggle("active", active);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkflowHubAgents(data) {
|
||||
const agents = Array.isArray(data.agents) ? data.agents : [];
|
||||
if (!agents.length) {
|
||||
document.getElementById("hubAgents").textContent = "(no agents found)";
|
||||
return;
|
||||
}
|
||||
const lines = [];
|
||||
for (const agent of agents) {
|
||||
lines.push(
|
||||
"[" + safeText(agent.id) + "] " +
|
||||
safeText(agent.label) +
|
||||
"\nfocus: " + safeText(agent.focus) +
|
||||
"\npreferred_action: " + safeText(agent.preferred_action)
|
||||
);
|
||||
lines.push("");
|
||||
}
|
||||
document.getElementById("hubAgents").textContent = lines.join("\n").trim();
|
||||
}
|
||||
|
||||
async function runCatalogAction(actionId, needsMessage) {
|
||||
if (needsMessage) {
|
||||
const input = window.prompt("Message pour " + actionId, document.getElementById("generalPromptInput").value.trim());
|
||||
if (!input || !input.trim()) return;
|
||||
document.getElementById("generalPromptInput").value = input.trim();
|
||||
}
|
||||
await runAction(actionId, needsMessage);
|
||||
}
|
||||
|
||||
function renderWorkflowHubWorkflows(data) {
|
||||
const root = document.getElementById("hubWorkflows");
|
||||
root.innerHTML = "";
|
||||
|
||||
const actions = Array.isArray(data.actions) ? data.actions : [];
|
||||
const n8nWf = Array.isArray(data.n8n_workflows) ? data.n8n_workflows : [];
|
||||
const scripts = Array.isArray(data.scripts) ? data.scripts : [];
|
||||
|
||||
const actionsBox = document.createElement("div");
|
||||
actionsBox.className = "question-item";
|
||||
const actionsTitle = document.createElement("div");
|
||||
actionsTitle.className = "question-title";
|
||||
actionsTitle.textContent = "Orchestrator Actions";
|
||||
actionsBox.appendChild(actionsTitle);
|
||||
for (const action of actions) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "btn-row";
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = "Run " + safeText(action.id);
|
||||
btn.onclick = function () {
|
||||
runCatalogAction(action.id, !!action.needs_message);
|
||||
};
|
||||
const txt = document.createElement("span");
|
||||
txt.className = "hint";
|
||||
txt.textContent = safeText(action.description || "");
|
||||
row.appendChild(btn);
|
||||
row.appendChild(txt);
|
||||
actionsBox.appendChild(row);
|
||||
}
|
||||
root.appendChild(actionsBox);
|
||||
|
||||
const n8nBox = document.createElement("div");
|
||||
n8nBox.className = "question-item";
|
||||
const n8nTitle = document.createElement("div");
|
||||
n8nTitle.className = "question-title";
|
||||
n8nTitle.textContent = "n8n Workflows (repo)";
|
||||
n8nBox.appendChild(n8nTitle);
|
||||
if (!n8nWf.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "hint";
|
||||
empty.textContent = "(none)";
|
||||
n8nBox.appendChild(empty);
|
||||
} else {
|
||||
for (const wf of n8nWf) {
|
||||
const line = document.createElement("div");
|
||||
line.className = "question-meta";
|
||||
line.textContent = safeText(wf.name) + " | " + safeText(wf.path);
|
||||
n8nBox.appendChild(line);
|
||||
}
|
||||
}
|
||||
root.appendChild(n8nBox);
|
||||
|
||||
const scriptsBox = document.createElement("div");
|
||||
scriptsBox.className = "question-item";
|
||||
const scriptsTitle = document.createElement("div");
|
||||
scriptsTitle.className = "question-title";
|
||||
scriptsTitle.textContent = "Scripts";
|
||||
scriptsBox.appendChild(scriptsTitle);
|
||||
for (const script of scripts) {
|
||||
const line = document.createElement("div");
|
||||
line.className = "question-meta";
|
||||
line.textContent = safeText(script.path) + " — " + safeText(script.usage);
|
||||
scriptsBox.appendChild(line);
|
||||
}
|
||||
root.appendChild(scriptsBox);
|
||||
}
|
||||
|
||||
function renderWorkflowHubRecap(data) {
|
||||
const recap = Array.isArray(data.usage_recap) ? data.usage_recap : [];
|
||||
document.getElementById("hubRecap").textContent = recap.length ? recap.join("\n") : "(no recap)";
|
||||
}
|
||||
|
||||
async function refreshWorkflowHub() {
|
||||
try {
|
||||
const data = await fetchJson("/api/workflows");
|
||||
workflowHubCache = data;
|
||||
renderWorkflowHubAgents(data);
|
||||
renderWorkflowHubWorkflows(data);
|
||||
renderWorkflowHubRecap(data);
|
||||
} catch (error) {
|
||||
document.getElementById("hubAgents").textContent = "workflow hub error: " + error.message;
|
||||
document.getElementById("hubWorkflows").textContent = "workflow hub error: " + error.message;
|
||||
document.getElementById("hubRecap").textContent = "workflow hub error: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function closeAlertPopup() {
|
||||
document.getElementById("alertPopup").classList.remove("open");
|
||||
}
|
||||
|
||||
async function ackAlert(id) {
|
||||
const payload = id ? { id: id } : {};
|
||||
try {
|
||||
const response = await fetch("/api/alerts/ack", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const body = await response.text();
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = body ? JSON.parse(body) : {};
|
||||
} catch (error) {
|
||||
parsed = { raw: body };
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(parsed.error || ("HTTP " + response.status));
|
||||
}
|
||||
await refreshAlerts();
|
||||
} catch (error) {
|
||||
setStatus("ack alert failed: " + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function ackAllAlerts() {
|
||||
await ackAlert(0);
|
||||
}
|
||||
|
||||
function showAlertPopup(alertItem) {
|
||||
if (!alertItem) return;
|
||||
document.getElementById("alertPopupMeta").textContent =
|
||||
"id=" + safeText(alertItem.id) +
|
||||
" severity=" + safeText(alertItem.severity) +
|
||||
" source=" + safeText(alertItem.source) +
|
||||
" ts=" + safeText(alertItem.ts);
|
||||
let body = safeText(alertItem.title);
|
||||
if (alertItem.context) body += "\n\n" + safeText(alertItem.context);
|
||||
if (alertItem.repo) body += "\nrepo=" + safeText(alertItem.repo);
|
||||
if (alertItem.pr_number) body += " pr=" + safeText(alertItem.pr_number);
|
||||
document.getElementById("alertPopupBody").textContent = body;
|
||||
const ackBtn = document.getElementById("alertPopupAckBtn");
|
||||
ackBtn.onclick = function () {
|
||||
ackAlert(alertItem.id);
|
||||
closeAlertPopup();
|
||||
};
|
||||
document.getElementById("alertPopup").classList.add("open");
|
||||
}
|
||||
|
||||
function renderActiveAlerts(items) {
|
||||
const root = document.getElementById("activeAlerts");
|
||||
root.innerHTML = "";
|
||||
if (!items.length) {
|
||||
root.textContent = "(no active alerts)";
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const box = document.createElement("div");
|
||||
box.className = "question-item";
|
||||
const title = document.createElement("div");
|
||||
title.className = "question-title";
|
||||
title.textContent =
|
||||
"#" + safeText(item.id) + " [" + safeText(item.severity) + "] " + safeText(item.title);
|
||||
box.appendChild(title);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "question-meta";
|
||||
meta.textContent =
|
||||
"source=" + safeText(item.source) +
|
||||
" ts=" + safeText(item.ts) +
|
||||
" repo=" + safeText(item.repo || "-") +
|
||||
" pr=" + safeText(item.pr_number || "-");
|
||||
box.appendChild(meta);
|
||||
const ctx = document.createElement("div");
|
||||
ctx.className = "question-context";
|
||||
ctx.textContent = safeText(item.context || "(no context)");
|
||||
box.appendChild(ctx);
|
||||
const row = document.createElement("div");
|
||||
row.className = "btn-row";
|
||||
const popupBtn = document.createElement("button");
|
||||
popupBtn.textContent = "Popup";
|
||||
popupBtn.onclick = function () { showAlertPopup(item); };
|
||||
const ackBtn = document.createElement("button");
|
||||
ackBtn.textContent = "Ack";
|
||||
ackBtn.onclick = function () { ackAlert(item.id); };
|
||||
row.appendChild(popupBtn);
|
||||
row.appendChild(ackBtn);
|
||||
box.appendChild(row);
|
||||
root.appendChild(box);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAlerts() {
|
||||
try {
|
||||
const data = await fetchJson("/api/alerts");
|
||||
const activeItems = Array.isArray(data.active_items) ? data.active_items : [];
|
||||
document.getElementById("alertsSummary").textContent =
|
||||
"active=" + safeText(data.active_count) +
|
||||
" total=" + safeText(Array.isArray(data.items) ? data.items.length : 0);
|
||||
renderActiveAlerts(activeItems);
|
||||
const latest = data.latest_active;
|
||||
if (latest && Number(latest.id) > lastPopupAlertId) {
|
||||
lastPopupAlertId = Number(latest.id);
|
||||
localStorage.setItem("zc_last_popup_alert_id", String(lastPopupAlertId));
|
||||
showAlertPopup(latest);
|
||||
window.alert("New alert #" + safeText(latest.id) + ": " + safeText(latest.title));
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById("alertsSummary").textContent = "alerts api error: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function boolText(value) {
|
||||
if (value === true) return "yes";
|
||||
if (value === false) return "no";
|
||||
@@ -1336,16 +2052,30 @@ cat >"$INDEX_FILE" <<EOF
|
||||
const health = status.health || {};
|
||||
const last = status.last || {};
|
||||
const job = status.job || {};
|
||||
const integrations = status.integrations || {};
|
||||
const alerts = status.alerts || {};
|
||||
const ops =
|
||||
"gateway_ok=" + boolText(health.gateway_ok) +
|
||||
" status=" + safeText(health.gateway_status) +
|
||||
" paired=" + boolText(health.paired) +
|
||||
" prom_ok=" + boolText(health.prometheus_ok) +
|
||||
" prom_status=" + safeText(health.prometheus_status) +
|
||||
" openwebui_ok=" + boolText(health.openwebui_ok) +
|
||||
" n8n_ok=" + boolText(health.n8n_ok) +
|
||||
"\nactive_alerts: " + safeText(alerts.active_count || 0) +
|
||||
"\nlast_convo: " + safeText(last.conversation || "(none)") +
|
||||
"\nlast_rt1m: " + safeText(last.realtime_1min || "(none)");
|
||||
document.getElementById("opsStatus").textContent = ops;
|
||||
|
||||
const integrationsText =
|
||||
"openwebui_url=" + safeText(integrations.openwebui_url || "http://127.0.0.1:3001") +
|
||||
" status=" + safeText(health.openwebui_status) +
|
||||
" ok=" + boolText(health.openwebui_ok) +
|
||||
"\nn8n_url=" + safeText(integrations.n8n_url || "http://127.0.0.1:5678") +
|
||||
" status=" + safeText(health.n8n_status) +
|
||||
" ok=" + boolText(health.n8n_ok);
|
||||
document.getElementById("integrationsStatus").textContent = integrationsText;
|
||||
|
||||
const jobInfo =
|
||||
"job_id=" + safeText(job.job_id) +
|
||||
" running=" + boolText(job.running) +
|
||||
@@ -1505,6 +2235,15 @@ cat >"$INDEX_FILE" <<EOF
|
||||
setInterval(tick, POLL_MS);
|
||||
refreshOps();
|
||||
setInterval(refreshOps, API_POLL_MS);
|
||||
refreshAgents();
|
||||
setInterval(refreshAgents, 10000);
|
||||
setHubTab("agents");
|
||||
refreshWorkflowHub();
|
||||
setInterval(refreshWorkflowHub, 12000);
|
||||
refreshPrScan();
|
||||
setInterval(refreshPrScan, 15000);
|
||||
refreshAlerts();
|
||||
setInterval(refreshAlerts, API_POLL_MS);
|
||||
refreshQuestions();
|
||||
setInterval(refreshQuestions, API_POLL_MS);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user
This dashboard now advertises
/integrations/*assets as available, but this commit does not addtools/ai/integrations(or the related integration scripts), sosync_integrationscopies nothing and these links are guaranteed to 404 at runtime. I verified scope by checking the commit tree (git ls-tree -r ... tools/ai) and repo file list (rg --files), which contain nointegrations/payload; users following the new integration flow cannot access the referenced Open WebUI/n8n files.Useful? React with 👍 / 👎.