diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md index 2b3677083..6900fb39b 100644 --- a/.claude/agents/test-runner.md +++ b/.claude/agents/test-runner.md @@ -189,6 +189,8 @@ The test suite covers: - **File Upload** (unit/test_file_upload.py) - Telegram file extraction, download, message router validation, parse_message with files (#354 Phase 1); workspace delivery — filename sanitization (NFKC, traversal, length, collision dedup), chat injection format `[File uploaded by {uploader}]`, all-writes-failed signaling, partial failure handling (#487 Phase 2) [UNIT] - **Telegram Voice** (unit/test_telegram_voice.py) - Voice transcription validation (duration/size limits), formatting, placeholder constants (#318) [UNIT] - **Inter-Agent Timeout** (test_inter_agent_timeout_unit.py) - FanOutRequest Optional timeout, per-subtask None dispatch, conditional asyncio.timeout wrap (#418) [UNIT] +- **Subprocess PGroup** (unit/test_subprocess_pgroup.py) - Process-group lifecycle: terminate, drain_reader_threads (natural-drain ordering #531, buffered-data preservation), safe_close_pipes, signal_process_tree (#407, #531) [UNIT] +- **Empty Result Classification** (unit/test_empty_result_classification.py) - Clean exit with lost result line → 502, two-field null check, raw_messages num_turns fallback, populated-metadata pass-through (#520, #521, #531) [UNIT] ### Avatars & Image Generation - **Avatars** (test_avatars.py) - Avatar serving, generation, regeneration, deletion, emotions, identity prompts, default generation (AVATAR-001/002/003) [SMOKE + Agent] @@ -231,9 +233,9 @@ The test suite covers: ## Test Suite Statistics -**Total Tests**: ~2,257 tests across 124 test files +**Total Tests**: ~2,262 tests across 124 test files **Smoke Tests**: ~578 tests (fast, no agent creation) -**Unit Tests**: ~165 tests (no backend needed, rate limit detection, watchdog logic, context formula, OTel trace logging, file upload, voice transcription, inter-agent timeout, scheduler sync loop, team-share gate, WhatsApp adapter (67)) +**Unit Tests**: ~170 tests (no backend needed, rate limit detection, watchdog logic, context formula, OTel trace logging, file upload, voice transcription, inter-agent timeout, scheduler sync loop, team-share gate, WhatsApp adapter (67), subprocess pgroup (11), empty result classification (13)) **Core Tests (not slow)**: ~2,112 tests **Slow Tests**: ~99 tests (chat execution, fleet ops, system agent ops, execution termination, WhatsApp live-backend integration) **WebSocket Tests**: ~10 tests (web terminal, execution streaming) @@ -263,6 +265,40 @@ Use these thresholds to assess test health (based on **executed** tests, not inc - **Warning**: 75-90% pass rate, <5 failures - **Critical**: <75% pass rate or >5 failures +## Recent Test Additions (2026-04-27) + +| Test File | Description | Tests Added | +|-----------|-------------|-------------| +| `unit/test_subprocess_pgroup.py` | Regression test for #531 — `test_buffered_data_preserved_after_grandchild_kill` verifies that data written to stdout before a grandchild is killed (including the final result JSON line) is not dropped when `drain_reader_threads` is called | +1 test (file total 11) | +| `unit/test_empty_result_classification.py` | Four new tests for `_classify_empty_result` raw_messages fallback (#531): `test_raw_messages_fallback_derives_num_turns`, `test_raw_messages_fallback_not_used_when_num_turns_present`, `test_raw_messages_empty_falls_back_to_zero`, `test_raw_messages_none_falls_back_to_zero` | +4 tests (file total 13) | + +**Pipe drain ordering fix (#531)** — `drain_reader_threads` in `subprocess_pgroup.py`: + +Root cause of the "Execution completed without a result message" HTTP 502 on long agentic tasks. The old code called `safe_close_pipes()` immediately after `terminate_process_group()`, discarding the kernel pipe buffer (including the `{"type":"result"}` final JSON line) before the reader thread could drain it. Fixed: kill grandchildren first, then wait up to `post_kill_grace=30s` for natural drain; force-close only as a last resort for genuine wedges. + +The new `test_buffered_data_preserved_after_grandchild_kill` test spawns a subprocess that writes a sentinel `RESULT_LINE` then forks a grandchild that holds stdout open; verifies the sentinel survives the grandchild kill. The `_classify_empty_result` tests verify that `num_turns` is derived from `raw_messages` when the result line was lost (since `metadata.num_turns` is only populated by that line), while `tool_count` continues to come from metadata (accumulated per-message during parsing, so accurate even without the result line). + +--- + +## Recent Test Additions (2026-04-25) + +| Test File | Description | Tests Added | +|-----------|-------------|-------------| +| `unit/test_github_init_gitignore.py` | Gitignore widen + migrate-on-push (#462) — full exclusion list coverage, idempotency, doc/constant sync, `git rm --cached` for newly-ignored files | 4 new tests (file total 6) | + +**Gitignore Widen + Migrate (#462)** (`unit/test_github_init_gitignore.py`): + +- `test_preserves_preexisting_gitignore` — existing `.gitignore` content is retained after init (pre-existing test) +- `test_fresh_agent_ignores_env_and_mcp_json` — fresh agent gets `.env` and `.mcp.json` exclusions (pre-existing test) +- `test_full_documented_exclusion_list_present` — all patterns in `_GITIGNORE_PATTERNS` constant are written to `.gitignore` (new) +- `test_idempotent_double_run` — running `initialize_git_in_container` twice does not duplicate patterns (new) +- `test_doc_and_constant_in_sync` — `_GITIGNORE_PATTERNS` constant matches the exclusion block in `TRINITY_COMPATIBLE_AGENT_GUIDE.md` (new) +- `test_rm_cached_for_newly_ignored_files` — `sync_to_github` calls `git rm --cached` for files that are tracked but now match a `.gitignore` pattern (new) + +Pure unit tests over `git_service.py` stubs; no backend container required. Run via `pytest tests/unit/test_github_init_gitignore.py`. + +--- + ## Recent Test Additions (2026-04-23) | Test File | Description | Tests Added | diff --git a/.claude/skills/cso/SKILL.md b/.claude/skills/cso/SKILL.md index f55c958a3..7e0281bbf 100644 --- a/.claude/skills/cso/SKILL.md +++ b/.claude/skills/cso/SKILL.md @@ -132,7 +132,20 @@ For each workflow file in `.github/workflows/`: **Docker socket access**: Check if backend has Docker socket mounted and what permissions it has. -**Severity**: CRITICAL for prod DB URLs with credentials in committed config. HIGH for root containers in prod / Docker socket access without read-only. MEDIUM for missing USER directive / exposed ports. +**Redis authentication and network isolation**: +- Check `redis.conf` or Docker env for `requirepass` — unauthenticated Redis reachable from the agent container network is CRITICAL (agents get full read/write to all task queues and secrets including other tenants') +- Check if agent containers have direct TCP access to Redis (e.g., `redis:6379`) — they should reach the platform only via the backend HTTP API, not the Redis port directly +- Check Redis keyspace for per-user isolation — if all users share a Redis namespace without user-scoped key prefixes, cross-tenant enumeration is possible even with auth + +**Docker `base_image` allowlist**: +- Search agent creation code for the `base_image` field — verify it's validated against an allowlist (regex or explicit list), not accepted as free text from the API request body +- An unvalidated `base_image` lets any authenticated user pull and execute arbitrary Docker images inside the agent network, giving access to Redis, the MCP server, and credential env vars + +**Setup/onboarding endpoint exposure**: +- Check if the first-run setup endpoint (`/api/setup`, `/setup`, etc.) is network-accessible or bound to 127.0.0.1 only +- If accessible from the public internet, check whether it uses a single-use bootstrapping token to prevent a race-condition hijack granting full admin control to an external caller + +**Severity**: CRITICAL for Redis without auth accessible from agent network. CRITICAL for unvalidated `base_image`. HIGH for setup endpoint reachable from public internet without a single-use token. HIGH for root containers in prod / Docker socket access without read-only. MEDIUM for missing USER directive / exposed ports. **FP rules**: `docker-compose.yml` for local dev with localhost = not a finding. @@ -140,6 +153,14 @@ For each workflow file in `.github/workflows/`: **Webhook routes**: Find files containing webhook/hook/callback route patterns. Check whether they also contain signature verification. +**Internal endpoint auth completeness**: +- For every route in the internal router (routes under `/api/internal/` or any "internal" prefix), verify each endpoint enforces the shared-secret header (`X-Internal-Secret`) — not just that the pattern exists in the file, but that every individual route uses it +- Probe: send request to each internal endpoint without the header and confirm 401/403, not 200 + +**Webhook trigger auth (not just signature)**: +- For webhook trigger endpoints, check for the presence of `Depends(get_current_user)` or an equivalent auth gate — not just HMAC signature verification +- Trigger routes have been found missing auth entirely while signature verification exists elsewhere in the same file; the two are independent checks + **TLS verification disabled**: Search for `verify=False`, `VERIFY_NONE`, etc. **MCP server security**: Check `src/mcp-server/` for authentication, input validation, and tool permission boundaries. @@ -156,8 +177,9 @@ Trinity-specific AI security concerns: - **AI API keys in code**: Hardcoded API key assignments (not env vars) - **Credential injection safety**: Are injected credentials exposed to agent code in unexpected ways? - **Cost/resource attacks**: Can a user trigger unbounded LLM calls via agent chat? +- **Agent container network reach**: From inside an agent container, check what internal services are directly reachable (Redis, MCP server backend port, etc.). Agent containers should reach the platform only through the backend HTTP API — direct TCP access to Redis, internal service ports, or other agents' containers outside the API layer is a CRITICAL isolation failure. Check `docker-compose.yml` network definitions and confirm agent containers cannot reach `redis:6379` or `mcp-server:8080` directly. -**Severity**: CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls. +**Severity**: CRITICAL for user input in system prompts / unsanitized LLM output rendered as HTML. CRITICAL for agent containers with direct Redis or internal service access bypassing the API. HIGH for missing tool call validation / exposed AI API keys. MEDIUM for unbounded LLM calls. **FP rules**: User content in the user-message position of an AI conversation is NOT prompt injection. @@ -178,10 +200,14 @@ For each OWASP category, perform targeted analysis. Scope file extensions to Pyt - Missing auth on routes (check FastAPI dependency injection for auth) - Direct object reference (can user A access user B's agents by changing IDs?) - Horizontal/vertical privilege escalation +- **Internal router completeness**: Every endpoint in the internal router must use the shared-secret dependency — audit the full router, not a sample. No route should return 200 without the secret header. +- **Role-permission consistency across ALL creation routes**: For each resource-creation endpoint (`POST /api/agents`, `/api/processes`, `/deploy-local`, webhook-triggered deployments, etc.), verify the same role-check dependency is used. The lowest-privilege role must not bypass the creation gate via any alternate route. +- **File-write path policy completeness**: For any file-write API, verify the path-deny/allowlist is evaluated in full — not just basename matching. Specifically check: can `.mcp.json`, `.env`, `.credentials.enc`, or `.ssh/*` be reached via path traversal or a sibling endpoint (e.g., `/credentials/inject`) that enforces a shorter deny list? #### A02: Cryptographic Failures - Weak crypto (MD5, SHA1, DES, ECB) or hardcoded secrets - JWT implementation (algorithm, expiration, secret management) +- **SSH key server-side generation**: Check if SSH private keys are generated server-side and returned in API responses. Flag as HIGH — clients should generate keypairs locally and submit only the public key. Server-generated private keys are exposed in transit and in API/access logs. #### A03: Injection - SQL injection: raw queries, string interpolation in SQL (check `database.py`) @@ -192,6 +218,8 @@ For each OWASP category, perform targeted analysis. Scope file extensions to Pyt - Rate limits on authentication endpoints? - Account lockout after failed attempts? - Business logic validated server-side? +- **Rate limiting quality**: Per-IP-only rate limits are bypassable via rotating proxies/Tor/IPv6 subnets. Check that rate limiting on auth endpoints (login, OTP, password reset) uses a per-account/per-email counter as the primary bucket. Also check that hard lockout on IP isn't itself a DoS primitive — progressive delay or CAPTCHA is safer than a hard block. +- **OTP brute-force window**: For email OTP flows, verify the code space (6 digits = 1M possibilities) is protected by an attempt counter that survives process restarts (stored in Redis/DB, not in-memory), and that codes expire within 5–10 minutes. #### A05: Security Misconfiguration - CORS configuration (wildcard origins?) diff --git a/.claude/skills/generate-user-docs/SKILL.md b/.claude/skills/generate-user-docs/SKILL.md index e27bbe66f..95bbdfd30 100644 --- a/.claude/skills/generate-user-docs/SKILL.md +++ b/.claude/skills/generate-user-docs/SKILL.md @@ -30,9 +30,10 @@ Read backend routers, frontend views, feature flows, and recent changes to produ | Feature flow index | `docs/memory/feature-flows.md` | Yes | No | | Requirements | `docs/memory/requirements.md` | Yes | No | | Architecture | `docs/memory/architecture.md` | Yes | No | -| Deployment config | `.env.example`, `scripts/deploy/*.sh`, `docker-compose.yml` | Yes | No | +| Deployment config | `.env.example`, `scripts/deploy/*.sh`, `docker-compose.yml`, `docker-compose.prod.yml`, `deploy.config.example` | Yes | No | | Trinity Docs site | `../trinity-docs/app/getting-started/*.tsx` | Yes | No | | Abilities repo | `github.com/abilityai/abilities` (README) | Yes | No | +| Ops runbook (private, pattern source for ops content) | `../ops-runbook/playbooks/*.md`, `../ops-runbook/instances/_template/scripts/*.sh`, `../ops-runbook/instances/_template/CLAUDE.md` | Yes | No | | Git history | `git log --since` | Yes | No | | Existing user docs | `docs/user-docs/**/*.md` | Yes | Yes | @@ -47,19 +48,70 @@ These tutorial-style guides walk users through end-to-end tasks. Keep them in sy | Guide | Source | Purpose | |-------|--------|---------| -| `guides/deploying-trinity.md` | `trinity-docs/app/getting-started/deploying-trinity/page.tsx` | Cloud vs self-hosted setup | +| `guides/deploying-trinity.md` | `trinity-docs/app/getting-started/deploying-trinity/page.tsx` + this skill's deploy hub rules | Hub: cloud vs self-hosted decision, prerequisites, links into spokes | +| `guides/deploying/local-development.md` | `docker-compose.yml`, `scripts/deploy/start.sh`, `.env.example` | Docker Desktop, dev compose, hot reload, base image build | +| `guides/deploying/single-server.md` | `docker-compose.prod.yml`, `deploy.config.example`, `.env.example` | VPS/bare-metal: prod compose, env, base image, email, Redis password, host paths | +| `guides/deploying/public-access.md` | `docker-compose.prod.yml` (cloudflared profile), `.env.example` (TUNNEL_TOKEN, PUBLIC_CHAT_URL) | Cloudflare Tunnel, public webhook surface, DNS | +| `guides/deploying/upgrading.md` | This skill's operational template + ops runbook patterns | Pre-flight → backup → pull → rebuild platform services → restart → verify → rollback | +| `guides/deploying/backup-and-restore.md` | This skill's operational template + ops runbook patterns | Volume-mounted alpine `cp` pattern, retention, daily cron template | +| `guides/deploying/monitoring.md` | This skill's operational template + ops runbook patterns + `/api/ops/fleet/health` router | Six health probes, fleet-health API, resource thresholds table, common-recovery patterns | | `guides/using-trinity.md` | `trinity-docs/app/getting-started/using-trinity/page.tsx` | UI tour: dashboard, agents, monitoring | | `guides/building-agents.md` | `trinity-docs/app/getting-started/building-agents/page.tsx` | Create, develop, deploy with abilities | **Sync rule**: When the trinity-docs source changes, update the corresponding guide to match. Convert TSX to markdown, preserving structure and content. **Code wins on conflict** — if trinity-docs disagrees with `.env.example`, `scripts/deploy/*.sh`, or `docker-compose.yml`, fix the local guide to match observed repo behavior and note the divergence for upstream. +**Ops-runbook rule**: Pages under `guides/deploying/` (especially `upgrading.md`, `backup-and-restore.md`, `monitoring.md`) draw their *patterns* from the local private ops runbook. Treat that as a pattern library, not a paste source. See Step 2h for what's safe to import vs. what must stay private. + +### Deployment config reading rules + +When reading scripts and compose files to produce deployment docs: + +1. Read `scripts/deploy/start.sh` literally — document what it **actually does**, not what comments say it does +2. Check `docker-compose.yml` environment blocks for which vars are **actually forwarded** to each service +3. For vars in `.env.example` marked `[PROD]` or `[OVERLAY]`, note their scope clearly — do not present them as universally available in dev compose +4. Never describe a feature as "auto" unless the script code proves it is +5. `verify-platform.sh` is the canonical six-probe checklist — reference it by name, don't duplicate it inline + +### Operational guide template + +Pages under `guides/deploying/` that cover operations (upgrade, backup, monitoring) follow this structure: + +```markdown +## [Operation Name] + +[One sentence: what this operation does and when to run it] + +### Pre-flight + +[What to check before starting. Backup steps. Smoke tests.] + +### Steps + +[Numbered, concrete commands. No vague instructions.] + +### Verify + +[How to confirm success. Commands to run. What to look for.] + +### Rollback + +[How to undo if something went wrong. Commands to restore prior state.] +``` + ## Target Structure ``` docs/user-docs/ ├── README.md # Index + navigation ├── guides/ # Tutorial-style walkthroughs -│ ├── deploying-trinity.md # Cloud vs self-hosted setup +│ ├── deploying-trinity.md # Hub: cloud vs self-hosted decision, links into spokes +│ ├── deploying/ # Self-hosted deployment spokes (per-scenario + ops) +│ │ ├── local-development.md # Docker Desktop, dev compose, hot reload, base image +│ │ ├── single-server.md # VPS/bare-metal: prod compose, env, email, backups +│ │ ├── public-access.md # Cloudflare Tunnel, PUBLIC_CHAT_URL, webhook surface +│ │ ├── upgrading.md # Pre-flight → backup → rebuild → restart → verify → rollback +│ │ ├── backup-and-restore.md # Volume-mounted alpine cp, retention, daily cron +│ │ └── monitoring.md # Six health probes, /api/ops/fleet/health, thresholds │ ├── using-trinity.md # UI tour: dashboard, agents, monitoring │ └── building-agents.md # Create, develop, deploy with abilities ├── getting-started/ @@ -140,7 +192,7 @@ git log --oneline --since="2 weeks ago" | head -30 ``` This identifies what has changed recently and which docs may need updating. -**2e. Deployment config (for deploying/setup guides)** — Read `.env.example`, `scripts/deploy/start.sh`, `scripts/deploy/stop.sh`, and `docker-compose.yml`. Extract: required env vars, auto-generated vs user-set secrets, default ports and override vars (e.g., `FRONTEND_PORT`), first-boot behavior, and what the start/stop scripts actually print and do. This is the authoritative source for deploy/setup docs. +**2e. Deployment config (for deploying/setup guides)** — Read `.env.example`, `scripts/deploy/start.sh`, `scripts/deploy/stop.sh`, `scripts/deploy/build-base-image.sh`, `docker-compose.yml`, `docker-compose.prod.yml`, `docker-compose.gitea.yml`, and `deploy.config.example`. Extract: required env vars, auto-generated vs user-set secrets, default ports and override vars (e.g., `FRONTEND_PORT`), first-boot behavior, and what the start/stop scripts actually print and do. **Cross-check `.env.example` keys against the `environment:` blocks in each compose file** — flag any key present in `.env.example` but not forwarded by a given compose as "prod-only / overlay-only / requires compose edit," because docs should not promise behavior the chosen compose can't deliver. This is the authoritative source for deploy/setup docs. **2f. Backend routers** — Glob `src/backend/routers/*.py` and read router files relevant to the section being written. Extract: - Endpoint paths and HTTP methods @@ -152,6 +204,29 @@ This identifies what has changed recently and which docs may need updating. - User-facing labels and actions - State management patterns +**2h. Ops-runbook patterns (for `guides/deploying/upgrading.md`, `backup-and-restore.md`, `monitoring.md`)** — Read the local private ops runbook: `playbooks/upgrade-instance.md`, `playbooks/monitoring-instance.md`, `instances/_template/CLAUDE.md`, and the helper scripts in `instances/_template/scripts/` (`update.sh`, `health-check.sh`, `restart.sh`, `status.sh`). Treat as a **pattern library**, not a paste source. + +**Safe to import (the rules and shapes that make production work):** +- The "use `docker compose restart`, never `down/up`" rule and *why* (preserves agent containers and `trinity-agent-network`). +- The "rebuild only platform services, not the base image" rule (`docker compose build --no-cache backend frontend mcp-server scheduler`). +- The pre-flight → backup → pull → rebuild → restart → verify → rollback sequence shape. +- The six-probe verification list (backend `/health`, scheduler `:8001/health`, frontend HTTP 200, redis PONG, MCP `/health`, Vector `:8686/health`). +- The fleet-status API surface (`/api/ops/fleet/health`, `/api/ops/fleet/status`). +- The resource-thresholds table (warning/critical bands for context %, CPU, memory, disk, error rate, container restarts, DB size, log size). +- The volume-mounted alpine `cp` pattern for SQLite backup/restore. +- Common-recovery patterns (agent network not found, agent context >90%, database locked). +- The daily-DB-backup + 14-day-retention cron template. + +**Forbidden to import (private detail; would leak in a public repo):** +- Any host name, IP address, Tailscale identity, or tailnet name. +- Any `sshpass`, `ssh -i`, or `ssh user@host`-prefixed command — local-host examples only. +- Any reference to specific instance directories (`instances/dgx/` etc.) or instance-specific scripts. +- Any password, token, or API-key value (even masked) from ops `.env` files. +- Any private repo name or internal path that reveals the ops repo's structure. +- Multi-instance management workflows (`source .env && ./scripts/run.sh ...`) — that's an operator-fleet pattern, not a single-instance user pattern. + +When in doubt, rewrite the *idea* in localhost form. A production-ops `sshpass -p $PW ssh user@host "sudo docker logs trinity-backend"` becomes the public `docker logs trinity-backend`. The rule survives; the access pattern stays private. + ### Step 3: Generate/Update Documentation For each section in the target structure, produce or update the markdown file following these rules: @@ -208,6 +283,137 @@ Only include if meaningful.] Use judgment: if the same content keeps needing to sit awkwardly inside "How It Works" or "Limitations," promote it to its own `##` section. +#### Operational guide template (use for `guides/deploying/upgrading.md`, `backup-and-restore.md`, `monitoring.md`) + +The dual-audience template above is for *features*. Operational guides are *procedures with checkpoints* — different shape. Use this instead: + +```markdown +# [Procedure Name] + +[1-2 sentence summary: what this procedure achieves and when an operator runs it.] + +## When to Run This + +[Trigger conditions. E.g. "Before every Trinity update," "Daily as a cron job," +"When the dashboard sync-health dot goes red." Concrete, not aspirational.] + +## Pre-flight + +- [ ] [Check 1 — e.g., backup target has space] +- [ ] [Check 2 — e.g., no scheduled tasks running in the next N minutes] +- [ ] [Check 3 — e.g., on the right branch / version] + +## Procedure + +### Step 1: [name] + +[What and why. Then the command in a code block. Then "expected output: ..."] + +### Step 2: [name] +... + +## Verify + +After the procedure, confirm each of these returns the expected result. If any +fails, do NOT proceed — go to **Rollback** or **Recovery** below. + +| Check | Command | Expected | +|-------|---------|----------| +| Backend | `curl -s http://localhost:8000/health` | `{"status":"healthy",...}` | +| ... | ... | ... | + +## Rollback / Recovery + +[For procedures with destructive or risky steps, include the inverse procedure. +For monitoring guides, include common-issue resolution table instead.] + +## Automation + +[Optional: cron template, CI hook, or scheduling guidance if the procedure is +intended to run regularly.] + +## See Also +``` + +**Why this shape:** operators care about (1) when to run, (2) safety pre-checks, (3) numbered steps, (4) explicit verification, (5) rollback. The feature template's "How It Works / For Agents / Limitations" doesn't map to any of those. + +#### Reusable snippets for operational guides + +The following are canonical snippets. Use verbatim across the deploy spokes so the same rule reads identically in every place. Do **not** paraphrase; consistency is the point. + +**The "use restart, never down/up" rule:** + +> **Use `docker compose restart`, not `down/up`.** `docker compose down` removes the `trinity-agent-network`, which orphans every running agent container — they keep running but lose their network and have to be removed and recreated. `restart` preserves both the agents and the network. The only times to use `down` are: (1) intentional full teardown, (2) recovering from a corrupted compose state. + +**The "rebuild platform services only" rule:** + +> When updating Trinity code, rebuild the platform images only: +> ```bash +> docker compose build --no-cache backend frontend mcp-server scheduler +> ``` +> The `trinity-agent-base` image is **not** rebuilt by this command. It changes much less often, and rebuilding it forces every agent to be re-deployed. Rebuild it only when `docker/base-image/Dockerfile` itself changes, via `./scripts/deploy/build-base-image.sh`. + +**The six-probe verification list:** + +```bash +# 1. Backend +curl -s http://localhost:8000/health +# Expected: {"status":"healthy",...} + +# 2. Scheduler +curl -s http://localhost:8001/health +# Expected: {"status":"healthy","active_schedules":N} + +# 3. Frontend (HTTP 200) +curl -s -o /dev/null -w '%{http_code}' http://localhost +# Expected: 200 + +# 4. Redis +docker exec trinity-redis redis-cli ping +# Expected: PONG + +# 5. MCP Server +curl -s http://localhost:8080/health +# Expected: 200 OK + +# 6. Vector (log aggregation) +docker exec trinity-vector wget -q -O - http://localhost:8686/health +# Expected: non-empty response +``` + +**Resource thresholds table:** + +| Metric | Warning | Critical | Action | +|---|---|---|---| +| Backend `/health` | — | not 200 | Restart `trinity-backend` | +| Scheduler `/health` | — | not 200 | Restart `trinity-scheduler` | +| Agent context usage | >75% | >90% | Reset agent context or restart agent container | +| Host CPU | >80% | >95% | Investigate runaway processes | +| Host memory | >85% | >95% | Check container memory limits | +| Disk free | <20% | <5% | Prune Docker, archive logs | +| Error rate (per hour) | >10 | >50 | Inspect platform.json log | +| Container restarts | any | repeated | `docker logs ` | +| `trinity.db` size | >1 GB | >5 GB | Archive old data | +| Vector log size | >5 GB | >10 GB | Trigger archival rotation | + +**SQLite backup pattern (volume-mounted alpine):** + +```bash +# Backup (run on the host, with services running) +docker run --rm \ + -v trinity_trinity-data:/data \ + -v ~/backups:/backup \ + alpine cp /data/trinity.db /backup/trinity-$(date +%Y%m%d-%H%M%S).db + +# Restore (services stopped first) +docker compose stop backend scheduler +docker run --rm \ + -v trinity_trinity-data:/data \ + -v ~/backups:/backup \ + alpine cp /backup/trinity-YYYYMMDD-HHMMSS.db /data/trinity.db +docker compose start backend scheduler +``` + 3. **No redundancy** — Do not repeat information from other docs. Cross-reference instead. The `Concepts` section in `getting-started/overview.md` is the canonical glossary; other docs reference it rather than re-defining terms. 4. **Code-derived accuracy** — Every claim must trace to code or a feature flow. Do not invent features. If a feature flow says "planned" or a router has TODO comments, note it as upcoming rather than documenting it as available. @@ -263,25 +469,46 @@ Create directories and write all approved files: ```bash mkdir -p docs/user-docs/{getting-started,agents,credentials,collaboration,automation,operations,sharing-and-access,integrations,advanced,api-reference} +mkdir -p docs/user-docs/guides/deploying ``` Write each markdown file using the Write or Edit tool. ### Step 7: Verify +**Count files written:** + ```bash find docs/user-docs -name "*.md" -type f | wc -l ``` +**Public-safety scan** — every page generated under `guides/deploying/` must pass these greps with zero hits. If any hit, the page is leaking ops-internal detail and must be rewritten: + +```bash +# Tokens that indicate private-repo or operator-fleet patterns +grep -rE 'sshpass|tailnet|ts\.net|ssh -i [^ ]+ [^ ]+@' docs/user-docs/guides/deploying/ + +# Real IPs (allow only loopback and the documented agent subnet 172.28.0.0/16) +grep -rE '\b(10|192\.168|172\.(1[6-9]|2[0-9]|3[01]))\.[0-9]+\.[0-9]+' docs/user-docs/guides/deploying/ \ + | grep -vE '172\.28\.0\.[0-9]+/16' + +# Instance directory references that hint at private-repo structure +grep -rE 'instances/[a-z0-9_-]+/' docs/user-docs/guides/deploying/ +``` + Confirm the expected number of files were created/updated. Report the final count. ## Completion Checklist -- [ ] All sections in target structure have corresponding files -- [ ] Every doc follows the dual-audience template (How It Works + For Agents) +- [ ] All sections in target structure have corresponding files (including `guides/deploying/` spokes) +- [ ] Every feature doc follows the dual-audience template (How It Works + For Agents) +- [ ] Every operational doc under `guides/deploying/` follows the operational template (When to Run → Pre-flight → Procedure → Verify → Rollback) - [ ] No redundant explanations across docs (MECE verified) - [ ] All API references link to Swagger (`/docs`) rather than duplicating schemas -- [ ] No real credentials, internal URLs, or PII in any doc +- [ ] Every key in `.env.example` is annotated in the relevant deploy spoke as: dev-compose / prod-compose-only / overlay-only / requires-compose-edit (no key promised to work in a compose that doesn't forward it) +- [ ] Reusable snippets (the "use restart" rule, "rebuild platform services only" rule, six-probe verification, thresholds table, alpine `cp` backup pattern) are used verbatim, not paraphrased, across deploy spokes +- [ ] No real credentials, internal URLs, host IPs, or PII in any doc +- [ ] Public-safety greps from Step 7 return zero hits across `guides/deploying/` - [ ] README.md index is complete and links are valid - [ ] Changes reviewed by user before writing diff --git a/.claude/skills/test-runner-ui-e2e/SKILL.md b/.claude/skills/test-runner-ui-e2e/SKILL.md new file mode 100644 index 000000000..04b202dd4 --- /dev/null +++ b/.claude/skills/test-runner-ui-e2e/SKILL.md @@ -0,0 +1,300 @@ +--- +name: test-runner-ui-e2e +description: Run the Playwright frontend e2e suite against a live Trinity stack, analyze failures, and update visual regression snapshots. Use after UI changes, before merging a PR with the `ui` label, or when the `frontend-e2e` CI workflow fails. +allowed-tools: [Bash, Read, Write, Edit, Grep, Glob] +user-invocable: true +argument-hint: "[] [--update-snapshots] [--headed] [--ui]" +automation: manual +--- + +# Test Runner — UI E2E + +Run the Playwright e2e suite that lives in `src/frontend/e2e/` against a live Trinity stack. Frontend equivalent of `/test-runner` (which covers backend pytest only). + +## When to Use + +- After any change in `src/frontend/src/` to confirm the UI still works end-to-end +- Before merging a PR carrying the `ui` label (the same PRs that trigger `frontend-e2e` in CI) +- When the `frontend-e2e` CI job fails — to reproduce locally and diagnose +- After a design-system migration to update visual regression snapshots +- When adding new specs to `e2e/` to validate them locally before pushing + +## Usage + +``` +/test-runner-ui-e2e # Run the full suite against http://localhost +/test-runner-ui-e2e smoke # Run specs matching "smoke" +/test-runner-ui-e2e --update-snapshots # Update visual regression baselines +/test-runner-ui-e2e --headed # Run with a visible browser +/test-runner-ui-e2e --ui # Open Playwright's interactive UI +``` + +## Arguments + +| Arg | Description | +|---|---| +| `` | Pattern passed to `playwright test --grep` | +| `--update-snapshots` | Regenerate `e2e/**/*-snapshots/*.png` baselines after intentional UI changes | +| `--headed` | Run with a visible Chromium window (debugging) | +| `--ui` | Open Playwright's interactive test UI (debugging) | + +## Process + +### Step 1: Prerequisites check + +Before running tests, verify the stack is up. The e2e workflow assumes Trinity is reachable at the configured `baseURL` (default `http://localhost`). + +```bash +# 1. Trinity backend health +curl -fsS http://localhost:8000/health + +# 2. Trinity frontend (Vite dev or nginx) on port 80 +curl -sI http://localhost/ | head -1 + +# 3. Containers +docker ps --filter 'name=trinity' --format 'table {{.Names}}\t{{.Status}}' +``` + +If backend is down or returning 404, **diagnose and recover** (see "Common failure modes" below) before trying tests. **Do not** skip this step — every failure I've seen has a stack-state precondition. + +### Step 2: Resolve admin password + +The Playwright auth setup needs `ADMIN_PASSWORD`: + +```bash +ADMIN_PASSWORD=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2) +test -n "$ADMIN_PASSWORD" || { echo "ADMIN_PASSWORD not set in .env"; exit 1; } +``` + +Confirm it actually works by hitting the login endpoint directly first: + +```bash +curl -s -X POST http://localhost:8000/api/token \ + --data-urlencode "username=admin" --data-urlencode "password=$ADMIN_PASSWORD" \ + | head -c 200 +``` + +If that returns `{"detail":"Not Found"}` the backend is dead. If it returns `Invalid username or password`, the password is wrong. If it returns `{"access_token": "..."}`, you're good to run tests. + +### Step 3: Run Playwright + +From `src/frontend/`: + +```bash +cd src/frontend +rm -rf e2e/.auth/admin.json e2e/test-results 2>/dev/null + +# Translate the skill args into npm scripts +SCRIPT="test:e2e" +[[ "$ARGS" == *"--update-snapshots"* ]] && SCRIPT="test:e2e:update" +[[ "$ARGS" == *"--headed"* ]] && SCRIPT="test:e2e:headed" +[[ "$ARGS" == *"--ui"* ]] && SCRIPT="test:e2e:ui" + +ADMIN_PASSWORD="$ADMIN_PASSWORD" npm run "$SCRIPT" -- ${FILTER:+--grep "$FILTER"} +``` + +### Step 4: Parse results + +A passing run looks like: + +``` +Running 5 tests using 4 workers + ✓ 1 [setup] › e2e/auth.setup.js:8:6 › authenticate as admin (2.5s) + ✓ 2 [chromium] › e2e/smoke.spec.js:25:7 › smoke › templates page loads (842ms) + ... + 5 passed (5.1s) +``` + +Failing runs include attachments (screenshot + video + trace) under `e2e/test-results//`. The most useful artifact is `error-context.md` which has the page snapshot at the moment of failure. + +### Step 5: Generate report + +``` +## E2E Test Results + +**Status**: PASSED / FAILED +**Duration**: Xs +**Specs**: X passed, Y failed +**Browser**: chromium + +### Failures (if any) +- spec name: + → File: e2e/: + → Page state: + +### HTML report +e2e/playwright-report/index.html + +### Recommendations +- ... +``` + +If snapshots updated: list the new/changed `*.png` paths so the user knows to commit them. + +### Step 6: Reminder about the `ui` label + +If the run was triggered for a PR-bound change, remind the user: + +> Add the `ui` label to your PR so CI runs the same suite (`frontend-e2e.yml`). + +## Common failure modes (learned 2026-04-29) + +These patterns recur. Diagnose by checking the page snapshot in `e2e/test-results//error-context.md`. + +### A. Stack is down before tests run + +**Symptom**: `auth.setup` fails with `net::ERR_CONNECTION_REFUSED at http://localhost/`. + +**Cause**: `trinity-frontend` or `trinity-backend` container exited. + +**Diagnose**: +```bash +docker ps --filter 'name=trinity' --format 'table {{.Names}}\t{{.Status}}' +docker logs trinity-backend --tail 20 +``` + +**Recover**: +```bash +docker rm -f trinity-backend 2>/dev/null +docker compose up -d backend +``` + +If port 8000 is "already allocated" by `com.docker.backend` after `docker rm -f`, you have the **Docker Desktop port-zombie bug**. The only reliable fix is restarting Docker Desktop: +```bash +osascript -e 'quit app "Docker Desktop"' +# wait for daemon stop, then reopen +open -a "Docker Desktop" +``` + +### B. Stuck on /setup wizard (CI fresh-DB scenario) + +**Symptom**: Page snapshot shows ` +
+ + + + DM default + + + +
@@ -70,7 +90,7 @@ diff --git a/src/frontend/src/components/StatusIndicator.vue b/src/frontend/src/components/StatusIndicator.vue index 67b0b86d6..c1e3c3682 100644 --- a/src/frontend/src/components/StatusIndicator.vue +++ b/src/frontend/src/components/StatusIndicator.vue @@ -23,11 +23,11 @@ const props = defineProps({ const statusClasses = computed(() => { switch (props.status) { case 'in_progress': - return 'bg-blue-500' + return 'bg-status-info-500' case 'completed': - return 'bg-green-500' + return 'bg-status-success-500' case 'error': - return 'bg-red-500' + return 'bg-status-danger-500' case 'pending': default: return 'bg-gray-300 border border-gray-400' diff --git a/src/frontend/src/components/TasksPanel.vue b/src/frontend/src/components/TasksPanel.vue index 46473b452..4b25f8e31 100644 --- a/src/frontend/src/components/TasksPanel.vue +++ b/src/frontend/src/components/TasksPanel.vue @@ -1108,10 +1108,10 @@ function formatDuration(ms) { function getContextBarColor(used, max) { if (!used || !max) return 'bg-gray-400' const percent = (used / max) * 100 - if (percent < 50) return 'bg-green-500' - if (percent < 75) return 'bg-yellow-500' - if (percent < 90) return 'bg-orange-500' - return 'bg-red-500' + if (percent < 50) return 'bg-status-success-500' + if (percent < 75) return 'bg-status-warning-500' + if (percent < 90) return 'bg-status-urgent-500' + return 'bg-status-danger-500' } // Start polling diff --git a/src/frontend/src/components/chat/ChatBubble.vue b/src/frontend/src/components/chat/ChatBubble.vue index e9946ebd3..43979fa1a 100644 --- a/src/frontend/src/components/chat/ChatBubble.vue +++ b/src/frontend/src/components/chat/ChatBubble.vue @@ -2,21 +2,21 @@
-
+
Voice
-

{{ content }}

+

{{ content }}

{{ formattedTime }}

-
+
@@ -64,7 +64,7 @@
-
+
Voice
diff --git a/src/frontend/src/components/chat/ChatEmptyState.vue b/src/frontend/src/components/chat/ChatEmptyState.vue new file mode 100644 index 000000000..6a634c1b0 --- /dev/null +++ b/src/frontend/src/components/chat/ChatEmptyState.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/frontend/src/components/chat/ChatHistoryDropdown.vue b/src/frontend/src/components/chat/ChatHistoryDropdown.vue new file mode 100644 index 000000000..ff9102d76 --- /dev/null +++ b/src/frontend/src/components/chat/ChatHistoryDropdown.vue @@ -0,0 +1,158 @@ + + + diff --git a/src/frontend/src/components/chat/ChatInput.vue b/src/frontend/src/components/chat/ChatInput.vue index 4b9be6275..274c33df3 100644 --- a/src/frontend/src/components/chat/ChatInput.vue +++ b/src/frontend/src/components/chat/ChatInput.vue @@ -1,5 +1,10 @@ diff --git a/src/frontend/src/components/chat/index.js b/src/frontend/src/components/chat/index.js index 992ddf980..f3714cc89 100644 --- a/src/frontend/src/components/chat/index.js +++ b/src/frontend/src/components/chat/index.js @@ -2,3 +2,5 @@ export { default as ChatBubble } from './ChatBubble.vue' export { default as ChatLoadingIndicator } from './ChatLoadingIndicator.vue' export { default as ChatInput } from './ChatInput.vue' export { default as ChatMessages } from './ChatMessages.vue' +export { default as ChatEmptyState } from './ChatEmptyState.vue' +export { default as ChatHistoryDropdown } from './ChatHistoryDropdown.vue' diff --git a/src/frontend/src/components/operator/NotificationsPanel.vue b/src/frontend/src/components/operator/NotificationsPanel.vue index fa0a936af..52214c7f8 100644 --- a/src/frontend/src/components/operator/NotificationsPanel.vue +++ b/src/frontend/src/components/operator/NotificationsPanel.vue @@ -94,11 +94,11 @@
-
{{ notificationsStore.pendingCount }}
+
{{ notificationsStore.pendingCount }}
Pending
-
{{ acknowledgedCount }}
+
{{ acknowledgedCount }}
Acknowledged
@@ -159,8 +159,8 @@ :key="notification.id" class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" :class="{ - 'bg-red-50 dark:bg-red-900/10': notification.status === 'pending' && notification.priority === 'urgent', - 'bg-orange-50 dark:bg-orange-900/10': notification.status === 'pending' && notification.priority === 'high', + 'bg-status-danger-50 dark:bg-status-danger-900/10': notification.status === 'pending' && notification.priority === 'urgent', + 'bg-status-urgent-50 dark:bg-status-urgent-900/10': notification.status === 'pending' && notification.priority === 'high', 'opacity-60': notification.status === 'dismissed' }" > @@ -218,7 +218,7 @@ {{ notification.notification_type }} - + Acknowledged @@ -245,7 +245,7 @@ @@ -195,21 +195,21 @@
-
- +
+

Your MCP API Key is Ready!

-
+
- +
-

+

Copy the configuration below before closing - the key won't be shown again!

@@ -228,7 +228,7 @@ @click="copyMcpConfig" class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-md" :class="copiedConfig - ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300' + ? 'bg-status-success-100 dark:bg-status-success-900/50 text-status-success-700 dark:text-status-success-300' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-900'" > @@ -274,7 +274,7 @@ - + diff --git a/src/frontend/src/views/Monitoring.vue b/src/frontend/src/views/Monitoring.vue index cc2e392b4..47977d813 100644 --- a/src/frontend/src/views/Monitoring.vue +++ b/src/frontend/src/views/Monitoring.vue @@ -17,9 +17,9 @@ - + {{ monitoringStore.enabled ? 'Monitoring Active' : 'Monitoring Disabled' }} @@ -64,28 +64,28 @@
{{ monitoringStore.summary.total_agents }}
Total Agents
-
-
{{ monitoringStore.summary.healthy }}
+
+
{{ monitoringStore.summary.healthy }}
Healthy
-
-
{{ monitoringStore.summary.degraded }}
+
+
{{ monitoringStore.summary.degraded }}
Degraded
-
-
{{ monitoringStore.summary.unhealthy }}
+
+
{{ monitoringStore.summary.unhealthy }}
Unhealthy
-
-
{{ monitoringStore.summary.critical }}
+
+
{{ monitoringStore.summary.critical }}
Critical
-
+
-

+

Active Alerts ({{ monitoringStore.alerts.length }})

@@ -99,7 +99,7 @@
{{ alert.agent_name }} @@ -175,7 +175,7 @@
-
+
{{ agent.issues.length }} issue{{ agent.issues.length > 1 ? 's' : '' }}
@@ -373,30 +373,30 @@ function getStatusIcon(status) { function getStatusBgClass(status) { switch (status) { - case 'healthy': return 'bg-green-100 dark:bg-green-900/30' - case 'degraded': return 'bg-yellow-100 dark:bg-yellow-900/30' - case 'unhealthy': return 'bg-red-100 dark:bg-red-900/30' - case 'critical': return 'bg-red-200 dark:bg-red-900/50' + case 'healthy': return 'bg-status-success-100 dark:bg-status-success-900/30' + case 'degraded': return 'bg-status-warning-100 dark:bg-status-warning-900/30' + case 'unhealthy': return 'bg-status-danger-100 dark:bg-status-danger-900/30' + case 'critical': return 'bg-status-danger-200 dark:bg-status-danger-900/50' default: return 'bg-gray-100 dark:bg-gray-700' } } function getStatusTextClass(status) { switch (status) { - case 'healthy': return 'text-green-600 dark:text-green-400' - case 'degraded': return 'text-yellow-600 dark:text-yellow-400' - case 'unhealthy': return 'text-red-600 dark:text-red-400' - case 'critical': return 'text-red-700 dark:text-red-500' + case 'healthy': return 'text-status-success-600 dark:text-status-success-400' + case 'degraded': return 'text-status-warning-600 dark:text-status-warning-400' + case 'unhealthy': return 'text-status-danger-600 dark:text-status-danger-400' + case 'critical': return 'text-status-danger-700 dark:text-status-danger-500' default: return 'text-gray-500 dark:text-gray-400' } } function getStatusBadgeClass(status) { switch (status) { - case 'healthy': return 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300' - case 'degraded': return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300' - case 'unhealthy': return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300' - case 'critical': return 'bg-red-200 dark:bg-red-900/50 text-red-800 dark:text-red-200' + case 'healthy': return 'bg-status-success-100 dark:bg-status-success-900/30 text-status-success-700 dark:text-status-success-300' + case 'degraded': return 'bg-status-warning-100 dark:bg-status-warning-900/30 text-status-warning-700 dark:text-status-warning-300' + case 'unhealthy': return 'bg-status-danger-100 dark:bg-status-danger-900/30 text-status-danger-700 dark:text-status-danger-300' + case 'critical': return 'bg-status-danger-200 dark:bg-status-danger-900/50 text-status-danger-800 dark:text-status-danger-200' default: return 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400' } } diff --git a/src/frontend/src/views/PublicChat.vue b/src/frontend/src/views/PublicChat.vue index ef84ba036..232988cf5 100644 --- a/src/frontend/src/views/PublicChat.vue +++ b/src/frontend/src/views/PublicChat.vue @@ -10,18 +10,27 @@ Trinity
- - + +
+ + + + +
@@ -197,6 +206,22 @@
+ +
+ + Viewing past session — read only + + +
+ + + +

{{ chatError }}

- + route.params.token) +const authStore = useAuthStore() // State const loading = ref(true) @@ -281,6 +320,9 @@ const isVerified = computed(() => !linkInfo.value?.require_email || !!sessionTok const chatSessionId = ref(localStorage.getItem(`public_chat_session_id_${token.value}`) || '') const historyLoading = ref(false) +// Read-only history view (when a past session is loaded from ChatHistoryDropdown) +const viewingHistorySession = ref(null) + // Chat const message = ref('') const messages = ref([]) @@ -300,6 +342,26 @@ const introLoading = ref(false) const introError = ref(null) const introFetched = ref(false) +// Playbooks (for empty-state quick actions) +const playbooks = ref([]) +const userMessageCount = computed(() => messages.value.filter(m => m.role === 'user').length) +const showQuickActions = computed(() => userMessageCount.value === 0 && !introLoading.value) +const loadPlaybooks = async () => { + try { + const response = await axios.get(`/api/public/playbooks/${token.value}`) + playbooks.value = response.data.skills || [] + } catch { + playbooks.value = [] + } +} +const onEmptyStateSelect = ({ text, sendImmediately }) => { + if (sendImmediately) { + sendMessage(text) + } else { + message.value = text + } +} + // Load link info const loadLinkInfo = async () => { loading.value = true @@ -523,6 +585,26 @@ const confirmNewConversation = async () => { } } +// Load a past session in read-only view (from ChatHistoryDropdown) +const handleHistorySessionSelected = ({ messages: sessionMessages, session }) => { + viewingHistorySession.value = session + messages.value = sessionMessages + chatError.value = null +} + +// Exit read-only history view and return to live chat +const exitHistoryView = async () => { + viewingHistorySession.value = null + messages.value = [] + introFetched.value = false + const hasHistory = await loadHistory() + if (!hasHistory) { + await fetchIntro() + } else { + introFetched.value = true + } +} + // THINK-001: Update loading text with minimum display time to prevent flicker const updateLoadingText = (newText) => { if (!newText) return @@ -660,8 +742,8 @@ const pollExecution = async (executionId) => { } // Send chat message -const sendMessage = async (userMessage) => { - if (!userMessage || chatLoading.value) return +const sendMessage = async (userMessage, files = []) => { + if ((!userMessage && files.length === 0) || chatLoading.value) return chatError.value = null @@ -680,7 +762,8 @@ const sendMessage = async (userMessage) => { try { const payload = { message: userMessage, - async_mode: true + async_mode: true, + files: files.length > 0 ? files : undefined, } // Include session token if email verification was required @@ -766,6 +849,8 @@ onMounted(async () => { } else { introFetched.value = true // Mark intro as done since we have history } + + loadPlaybooks() } } }) diff --git a/src/frontend/src/views/Settings.vue b/src/frontend/src/views/Settings.vue index 80aa9e674..4e7f0c9f5 100644 --- a/src/frontend/src/views/Settings.vue +++ b/src/frontend/src/views/Settings.vue @@ -152,6 +152,18 @@ Save +
@@ -245,6 +257,18 @@ Save +
@@ -441,6 +465,18 @@ Save Credentials + ✓ Saved @@ -1611,16 +1647,26 @@ Example:
+ +