diff --git a/docs/superpowers/plans/2026-09-22-control-panel-operational-ux-v1.md b/docs/superpowers/plans/2026-09-22-control-panel-operational-ux-v1.md new file mode 100644 index 0000000..8a7b4d8 --- /dev/null +++ b/docs/superpowers/plans/2026-09-22-control-panel-operational-ux-v1.md @@ -0,0 +1,309 @@ +# Control Panel Operational UX V1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn the existing server-rendered Control Panel into a bounded operational queue that identifies client/server problems, presents distinct technical/fiscal metrics, and offers safe audited context resolution. + +**Architecture:** Keep FastAPI, Jinja2, project CSS, and minimal JavaScript. Extend `control_plane.py` with bounded query/view-model functions and add a focused `panel_presentation.py` for timestamps, duration, labels, priorities, and attention ordering. Use existing `FiscalContextEntry`, `Job`, `AuditEvent`, CSRF, and human authorization; do not add remote write capabilities or fiscal-engine changes. + +**Tech Stack:** Python 3.12, FastAPI, Jinja2, SQLAlchemy, PostgreSQL/SQLite tests, pytest, vanilla CSS/JavaScript. + +## Global Constraints + +- Keep FastAPI, Jinja2, CSS próprio, and JavaScript mínimo; do not introduce React, Vue, Node build chain, or SPA. +- Do not alter fiscal logic, requiredness, Hub/Agent protocol, Firebird writes, AI writes, deployment, Proxmox, or real Agents. +- `safe_blocked_products` means distinct active tenant/product pairs; `open_blockers` means active blocker rows. +- Enforce `agents_total == online + degraded + offline + failed + unknown` in code and tests. +- UI timezone is configured by `UI_TIMEZONE`, default `America/Bahia`; storage/API timestamps remain UTC. +- Every mutating human action requires human auth, CSRF, deterministic validation, and an `AuditEvent`. +- Weak local passwords are allowed only with `ALLOW_WEAK_DEV_PASSWORD=1` outside production; production requires the strong policy. +- Never log or commit credentials, session tokens, passwords, SQL, shell commands, or raw connection strings. +- Preserve bounded pagination and avoid N+1 queries on 100-installation fixtures. + +--- + +### Task 1: Presentation Helpers And Password Policy + +**Files:** +- Create: `central/app/panel_presentation.py` +- Modify: `central/app/config.py:16-39` +- Modify: `central/app/human_auth.py:create_admin` +- Modify: `central/tests/test_control_plane.py` +- Create: `central/tests/test_panel_presentation.py` +- Modify: `docker-compose.yml` to explicitly enable weak development passwords only for the local service + +**Interfaces:** +- Produces `format_ui_datetime(value) -> str`, `format_age(value, now=None) -> str`, `format_duration(started_at, finished_at, now=None) -> str`, `operational_priority(blocker) -> str`, and `health_label(value) -> str`. +- `format_ui_datetime` returns `-` for no value, a localized `dd/mm/YYYY HH:MM` string, and accepts UTC-naive values as UTC. +- `format_age` returns `-`, `agora`, `há 1 min`, `há N min`, `há 1 h`, or `há N h`; future values return `agora`. +- `format_duration` returns `-` when no start exists, `em andamento` when only started exists, and compact seconds/minutes/hours when both exist. +- `create_admin` accepts short passwords only when `settings.environment != "production"` and `ALLOW_WEAK_DEV_PASSWORD=1`; otherwise it retains the strong minimum. + +- [ ] **Step 1: Write failing timestamp, duration, priority, and password-policy tests** + +```python +def test_ui_times_use_configured_timezone_and_human_age(monkeypatch): + monkeypatch.setenv("UI_TIMEZONE", "America/Bahia") + value = datetime(2026, 9, 22, 19, 11, tzinfo=timezone.utc) + assert format_ui_datetime(value) == "22/09/2026 16:11" + assert format_age(value, datetime(2026, 9, 22, 19, 19, tzinfo=timezone.utc)) == "há 8 min" + +def test_duration_and_priority_are_deterministic(): + start = datetime(2026, 9, 22, 19, 0, tzinfo=timezone.utc) + finish = start + timedelta(seconds=194) + assert format_duration(start, finish) == "3 min 14 s" + assert operational_priority(SimpleNamespace(blocker_type="TECHNICAL_FAILURE", field="", reason_code="X")) == "CRITICAL" + +def test_short_local_password_requires_explicit_setting(monkeypatch): + monkeypatch.delenv("ALLOW_WEAK_DEV_PASSWORD", raising=False) + with pytest.raises(ValueError): + create_admin(session, "admin", "admin") + monkeypatch.setenv("ALLOW_WEAK_DEV_PASSWORD", "1") + assert create_admin(session, "admin", "admin").username == "admin" +``` + +- [ ] **Step 2: Run the focused tests and verify they fail** + +Run: `python -m pytest central/tests/test_panel_presentation.py central/tests/test_control_plane.py -q` + +Expected: FAIL because the presentation module and environment-aware password gate do not yet exist. + +- [ ] **Step 3: Implement the helpers and environment-aware password check** + +Use `zoneinfo.ZoneInfo(settings.ui_timezone)` for display conversion. Add `ui_timezone` and `allow_weak_dev_password` configuration fields, but read the weak-password environment flag at call time so tests and explicit local process settings cannot be stale. Update the local Compose service with `ALLOW_WEAK_DEV_PASSWORD=1`; do not set it for production configuration. + +- [ ] **Step 4: Run focused tests and inspect the diff** + +Run: `python -m pytest central/tests/test_panel_presentation.py central/tests/test_control_plane.py -q` and `git diff --check`. + +Expected: PASS with no whitespace errors and no credential values in the diff. + +- [ ] **Step 5: Commit the slice** + +```text +git add central/app/panel_presentation.py central/app/config.py central/app/human_auth.py central/tests/test_panel_presentation.py central/tests/test_control_plane.py docker-compose.yml +git commit -m "feat: add operational panel presentation helpers" +``` + +### Task 2: Bounded Operational Query View-Models + +**Files:** +- Modify: `central/app/control_plane.py` +- Modify: `central/app/models.py` only if a missing relationship-free field is proven necessary; do not add a migration for presentation-only data +- Modify: `central/tests/test_control_plane.py` +- Create: `central/tests/test_operational_view_models.py` + +**Interfaces:** +- Produces `dashboard_summary(session, now=None)` with `health_counts`, `agents_total`, `safe_blocked_products`, `open_blockers`, `open_blocker_breakdown`, `attention_items`, `failed_jobs`, and `outdated_agents`. +- Produces `list_installations(..., sort="display_name")` with `fiscal_completion_percent`, `technical_health`, `fiscal_status`, `version_drift`, formatted presentation fields, and `installation_id`/`agent_id` as secondary data. +- Produces `list_jobs(..., client=None, search=None)` joining `Job` to `Agent`, `Installation`, and `Tenant`, returning client/display/hostname and duration fields. +- Produces `list_blockers(..., client=None, installation=None, status="OPEN", age=None, search=None)` with summaries, priority, age, and stable pagination. +- Produces `blocker_detail(session, blocker_id)` with blocker, installation identity, related events, related jobs, and resolution audit rows. + +- [ ] **Step 1: Add failing tests for dashboard reconciliation and metric semantics** + +Seed a small SQLite fixture with five health states, two blockers for one product, one blocker for another product, and distinct clients. Assert that `agents_total` equals the five state sum, `safe_blocked_products == 2`, and `open_blockers == 3`. + +- [ ] **Step 2: Add failing tests for attention ordering and limit** + +Seed more than ten installations with mixed health, required blockers, and outdated versions. Assert that the first items are `FAILED`, then `DEGRADED`, `OFFLINE`, `UNKNOWN`, and that the result length is at most ten. Assert every item contains client, display name, hostname, problem, age, and link target. + +- [ ] **Step 3: Add failing tests for agents, jobs, blockers, and detail view-models** + +Assert agent rows expose percentage and separate technical/fiscal labels; job rows expose client/display/hostname and `3 min 14 s`; blocker filters reduce the result server-side; detail rows expose evidence, related job/event IDs, and audit resolution history. + +- [ ] **Step 4: Implement bounded SQL/query helpers** + +Use one installation join for agent/dashboard data, aggregate open blockers with `count(*)` and `count(distinct (tenant_id, product_code))` semantics compatible with SQLite/PostgreSQL, and bulk-load related job/event/audit rows for details. Keep page size capped at 100. Map blocker fields to `CFOP`, `ST`, `ICMS`, `CSOSN`, `NCM`, or `OTHER`. Never turn `safe_blocked` into technical `DEGRADED`. + +- [ ] **Step 5: Run view-model tests and verify query count does not grow per row** + +Run: `python -m pytest central/tests/test_operational_view_models.py central/tests/test_control_plane.py -q` + +Expected: PASS; dashboard and list functions execute bounded joins without per-installation detail queries. + +- [ ] **Step 6: Commit the slice** + +```text +git add central/app/control_plane.py central/tests/test_control_plane.py central/tests/test_operational_view_models.py +git commit -m "feat: add operational control panel view models" +``` + +### Task 3: Safe Blocker Resolution Workflow + +**Files:** +- Create: `central/app/blocker_resolution.py` +- Modify: `central/app/context_keys.py` only to expose the existing enum choices cleanly to the form +- Modify: `central/app/control_api.py` +- Modify: `central/app/web.py` +- Modify: `central/tests/test_control_plane.py` +- Create: `central/tests/test_blocker_resolution_panel.py` + +**Interfaces:** +- `validate_resolution_input(payload) -> normalized, error` validates `key`, `value`, `scope`, `scope_key`, and `reason` through `validate_entry`, allowing only `PRODUCT`, `GROUP`, and `COMPANY` in this workflow. +- `resolve_blocker_with_context(session, blocker_id, admin, payload, request_ip=None) -> {"blocker_id": int, "job_id": int, "audit_id": int}` performs one transaction. +- Human routes are `GET /blockers/{blocker_id}` and `POST /blockers/{blocker_id}/resolve-context`; JSON API gains the same validated behavior without accepting arbitrary context text. + +- [ ] **Step 1: Write failing validation, CSRF, authorization, and audit tests** + +Test valid `entry_scope=INTER` and `product_role=RESALE`; reject invalid enum values, unknown keys, unsupported `NCM` workflow scope, empty reason, missing product scope key, missing CSRF, unauthenticated access, and non-admin access. Assert successful resolution creates `FiscalContextEntry`, a `REPROCESS_PRODUCTS` job, and an audit context containing before/after and job ID. + +- [ ] **Step 2: Run the focused tests and verify the new route/service fails** + +Run: `python -m pytest central/tests/test_blocker_resolution_panel.py -q` + +Expected: FAIL because the service, detail route, and form route do not yet exist. + +- [ ] **Step 3: Implement normalized validation and the transaction service** + +Derive `company_key` from the installation/company context, derive product scope key from the blocker product code, read the current matching `FiscalContextEntry` as `before`, upsert the normalized entry, create a `REPROCESS_PRODUCTS` job with exactly one product code, and record a sanitized audit event with `admin_user_id`, tenant, installation, product, blocker, context key/scope/value, reason, before, after, and `reprocess_job_id`. Commit once; rollback on any validation or persistence error. + +- [ ] **Step 4: Implement detail and form routes** + +Render all required blocker evidence before the form, show select options for known enums, preserve CSRF, return a readable validation error on invalid input, redirect to the blocker detail after success, and remove the old `data-confirm`/hidden-context form. + +- [ ] **Step 5: Run focused tests and verify security boundaries** + +Run: `python -m pytest central/tests/test_blocker_resolution_panel.py central/tests/test_control_plane.py -q`. + +Expected: PASS; no route accepts `AI_WRITE_ALLOWED`, `FIREBIRD_WRITES`, SQL, shell, or arbitrary context keys. + +- [ ] **Step 6: Commit the slice** + +```text +git add central/app/blocker_resolution.py central/app/context_keys.py central/app/control_api.py central/app/web.py central/tests/test_blocker_resolution_panel.py central/tests/test_control_plane.py +git commit -m "feat: add audited blocker context resolution" +``` + +### Task 4: Server-Rendered Operational Pages + +**Files:** +- Modify: `central/app/web.py` +- Modify: `central/templates/base.html` +- Modify: `central/templates/dashboard.html` +- Modify: `central/templates/agents.html` +- Modify: `central/templates/agent_detail.html` +- Modify: `central/templates/blockers.html` +- Create: `central/templates/blocker_detail.html` +- Modify: `central/templates/jobs.html` +- Modify: `central/static/control-panel.css` +- Modify: `central/static/control-panel.js` +- Create: `central/tests/test_control_panel_pages.py` + +**Interfaces:** +- Existing URLs remain stable: `/`, `/agents`, `/agents/{agent_id}`, `/blockers`, `/jobs`, `/login`. +- New URL: `/blockers/{blocker_id}`; POST resolution is `/blockers/{blocker_id}/resolve-context`. +- Query strings are server-side filters and remain in pagination links. + +- [ ] **Step 1: Add failing page-render tests** + +Log in with the test admin and assert dashboard HTML contains all five health labels, separate `Produtos bloqueados` and `Blockers abertos`, `ATENÇÃO NECESSÁRIA`, `Ver todos`, failed-job and outdated sections, and no raw ISO timestamp. Assert agents contain client/display/hostname, fiscal percentage, and separate health/status badges. Assert jobs contain client/hostname/duration. Assert blockers contain visible filter labels, summary metrics, priority, detail links, and no `window.confirm`. + +- [ ] **Step 2: Run page tests and verify they fail** + +Run: `python -m pytest central/tests/test_control_panel_pages.py -q` + +Expected: FAIL against the current templates and routes. + +- [ ] **Step 3: Implement route context and filters** + +Pass the view-model results and current filter values to templates. Add agent worst-coverage sort, blocker filter parameters, job status/search parameters, event filters on agent detail, and bounded pagination links. Keep all data server-rendered. + +- [ ] **Step 4: Replace templates with operational Portuguese UI** + +Use accessible `