Ctrl K
ComunidadGuías…

evidence/DevInPanel.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# DevInPanel — one truthful Devin screen in the dashboard

Worktree: `w85-devinpanel` (branch `feat/devin-panel`, from `19ff92e`). Lane: DevInPanel.
Scope: a new `#devin` section in `src/dashboard/index.html` plus `src/dashboard/devin-panel.js`,
one dashboard route, i18n, tests and the smoke flow. The stats and theme sections were not touched.

## Goal

Expose the Devin surface the proxy can actually observe — sessions/trajectories, model
catalog/status, usage/quota, settings — in one screen, using only existing endpoints.
Anything the proxy cannot read is declared unavailable with a machine-readable reason;
nothing is rendered as a zero or an empty list that would read as a real measurement.

## What shipped

### Route — `GET /dashboard/api/devin/panel`

Auth is inherited from the dashboard gate (401 without it, verified). The response is an
in-process snapshot; opening the page makes no upstream call.

```json
{
  "ok": true,
  "generatedAt": 1760000000000,
  "runtime": {
    "uptimeSec": 3600, "startedAt": 1760000000000,
    "switches": [{ "key": "devinConnect", "value": true, "source": "config", "overridden": true }],
    "credentials": { "credStoreEnabled": true, "remoteCredStore": false, "writable": false },
    "langServer": { "running": true, "pid": 123, "port": 42100, "instances": 1, "ready": 1,
                    "activeRequests": 0, "totalRssBytes": 123456, "admission": { "...": 0 } },
    "detach": { "enabled": true, "entries": 0, "bytes": 0, "draining": false, "counters": { "admit": 0 } }
  },
  "sessions": {
    "sources": [
      { "id": "proxyTurns", "available": true, "scope": "process",
        "notes": ["billed_successes_only", "process_local"] },
      { "id": "cascadeTrajectories", "available": false, "reason": "ls_only", "rpcs": ["..."] },
      { "id": "acpSessions", "available": false, "reason": "not_tracked" }
    ],
    "turns": { "available": true, "scanned": 3, "totalAccounts": 3, "capped": false,
               "accountsWithTurns": 1, "retained": 2,
               "summary": { "totalTokens": 30, "credit": 2, "acu": 0.5, "lastTs": 0 },
               "window": { "ms": 3600000, "turns": 1, "totalTokens": 30, "credit": 2, "acu": 0.5 },
               "shown": 2, "items": [{ "ts": 0, "accountRef": "acct-…", "model": "swe-2",
                                       "promptTokens": 10, "completionTokens": 5,
                                       "totalTokens": 15, "credit": 1, "acu": 0.25 }] }
  },
  "quota": {
    "drought": { "active": false, "threshold": 20, "activeAccounts": 3, "knownAccounts": 3,
                 "quotaKnownAccounts": 3, "droughtAccounts": 0, "coverageMet": true,
                 "lowestWeeklyPercent": 80, "lowestDailyPercent": 90, "restrictEnabled": true,
                 "restrictionFailOpen": false, "freeTierModels": ["swe-2"] },
    "knobs": [{ "key": "cooldownMs", "value": 1800000, "override": null,
                "source": "env", "overridden": false }]
  }
}
```

`GET` only; a non-GET falls through to the normal 404. `Cache-Control: no-store` is set
because the snapshot is time-stamped and must not be replayed from a browser cache.

`runtime.switches` and `/runtime-env-status` now share one helper (`runtimeSwitchRows()` in
`api.js`) so the two can never report a different effective value or source for the same
switch. The PUT path keeps its whitelist and null-clears semantics; the panel itself is
read-only for switches and links to Settings for writes.

### Page — `#devin`

Sidebar entry under 用量, section `#p-devin`, six render methods in the shipped `App`
object (not a mirror implementation):

| Section | Source | What it shows |
|---|---|---|
| KPI strip | `/devin/panel` + `/models` + `/models/catalog` | transport + CLI mode, turns in the last hour, retained turns, free/unreachable models, catalog drift, drought verdict |
| 运行时 Runtime | `/devin/panel` | LS process/port/uptime, ready/instances, active requests, RSS, admission counters and last failure, detach-cache switch/entries/bytes/counters, credential-store presence (read-only) |
| 会话与轨迹 Sessions | `/devin/panel` | source table (available + two unavailable with reasons), merged billed turns with time, hashed account ref, model, prompt/completion, tokens, credits, ACU; totals and last-hour window |
| 模型目录与状态 Catalog | `/models`, `/models/catalog`, button `/models/reconcile` | reachability, free/billable/unknown tri-state, daily/weekly quota state, cost tier, promo end, stale-pricing count, live-vs-snapshot diff, on-demand reconcile result |
| 用量与配额 Quota | `/devin/panel`, button `/usage/orgs` | drought decision and inputs, free-tier list, fail-open warning, five quota/spend knobs; on-demand org-scoped ACU cards (deduped by org) |
| 设置 Settings | `/devin/panel`, `PUT /quota-config` | six effective switch values with source/override, five quota knobs with env-following vs config override, save path for the knobs |

The two reads that can reach upstream are explicit buttons, never on mount:

- `GET /models/reconcile` — local vs live vs snapshot counts, `partial`, `stale`, `degraded[]`.
- `GET /usage/orgs` — org-scoped ACU/usage; `refresh=1` only on a forced refresh.

### Truthfulness rules (the point of the lane)

| Situation | Rendering | Test |
|---|---|---|
| Cascade trajectories / ACP sessions not readable | `available:false` + `reason` (`ls_only`, `not_tracked`), never `[]` | unit + route + render + smoke |
| Proxy turns | labelled `billed_successes_only` + `process_local`; aborts/429s never appear | unit + render |
| Missing numeric field | `—`, never `0` | render |
| Live catalog not synced | drift KPI is `—` + “Live catalog not synced yet”, not `0` | render |
| Drought coverage below threshold | KPI says “Not assessed”, not “Clear” | render |
| Pool larger than the 500-account scan cap | “scanned N / M” + a note naming the cap | unit + render + route |
| Account identity in turns | `accountRef` hash only; no raw id, key or email in the body | route test asserts body does not contain them |
| Credential-store switches | presence booleans, `writable:false`; not accepted on PUT | unit + route |

## Verification

| Check | Command | Result |
|---|---|---|
| Panel builders | `node --test test/dashboard-devin-panel.test.js` | 21/21 pass |
| Shipped-template render guards | `node --test test/dashboard-devin-panel-render.test.js` | 29/29 pass |
| Route contract + shared switch rows | `node --test test/dashboard-devin-panel-routes.test.js` | 9/9 pass |
| Escape call-site guard | `node --test test/dashboard-escape-callsites.test.js` | 5/5 pass |
| Dashboard smoke (incl. new `devin-panel` flow) | `node --test test/dashboard-smoke.test.js` | 1/1 pass |
| Dashboard copy gate | `npm run check:copy` | pass |
| i18n catalogue | `node src/dashboard/check-i18n.js` | all checks pass |
| Full repo suite | `npm test` | 6502 tests, 6405 pass, 0 fail, 97 skipped, exit 0 |
| Local gate (declared platform skips) | `GATE_INERT_SKIP_PATHS="test/detach-ls-pool-pressure.test.js;test/detach-soak.test.js;test/devin-connect-relogin-live.test.js;test/mutate-verify-harness.test.js;test/self-update-untracked-safety.test.js;test/wire-byte-identity.test.js" npm run gate` | **PASS exit=0** |

The render tests execute the methods sliced out of the shipped `index.html`, so a change
that only edits a test mirror cannot pass them. The route tests call the real
`handleDashboardApi` with a fake response and a real account/ring, assert that the
raw account key and email never reach the body, and pin that opening the panel makes
zero `fetch` calls.

### Gate

```
PASS spec-static-check exit=0
PASS spec-baseline-check exit=0
PASS secret-scan exit=0
PASS test:release exit=0 — 6405 pass / 0 fail / 97 skip (528 files) — 6 inert file(s), 6 declared
PASS git diff --check exit=0
PASS git diff --cached --check exit=0
SKIP mutation EXECUTION — this incremental gate checks spec shape, anchors and baseline
     values, but does not run the mutants themselves; the Linux full-gate recipe runs the sweep.
INCREMENTAL GATE: PASS exit=0
```

The six declarations are the repo's deliberate platform/env gates: the POSIX-only
detach pool-pressure harness, the `DETACH_SOAK=1` 120 s soak, the live-credential
relogin suite, the mutation harness, the self-update untracked-safety suite and the
wire-byte-identity base-tree comparison. The first, undeclared run failed **only** on
that inert-file policy (`test:release` itself reported `6404 pass / 0 fail / 97 skip`);
it did not fail a test. With the declarations the gate is green.

## Research mapping

- **Q1 (§3.4, detached completion cache)** — the runtime card surfaces the detach registry
  (`enabled`, entries, bytes, admit/serve/re_detach) that already exists, so the two-week
  rollout can be observed from the same screen as the rest of the Devin state.
- **Q1 (§2 row 3, model catalog updater; §3.3, four-class quota)** — the catalog section
  shows the live-vs-snapshot diff and the price/quota tri-state; the quota section shows
  the drought decision, its coverage and the five knobs that shape it.
- **Q4 (G3, G5, G7 truthfulness)** — the account-pool review's core lesson (skipped work
  counted as success, KPIs that lie about their predicate) is applied here: unavailable
  sources carry reasons, missing values are dashes, unsynced catalogs are not zero drift,
  uncovered drought is not “clear”, and a capped scan says so.
- **Q4 (G21/G22, covered-but-unused data)** — per-account billed rings (`/accounts/:id/requests`)
  and the org-scoped ACU reader (`/usage/orgs`) are surfaced without new server state.
- Q1's larger absorption items (descriptor extraction, LS supply chain, detached cache
  implementation) are backend work and are deliberately not re-implemented in this lane.

## Files

| File | Change |
|---|---|
| `src/dashboard/devin-panel.js` | new; pure builders for runtime/turns/sources/quota/panel |
| `src/dashboard/api.js` | `GET /devin/panel`; shared `runtimeSwitchRows()` |
| `src/dashboard/index.html` | nav entry, `#p-devin` markup, six render methods, two on-demand loaders, quota save |
| `src/dashboard/i18n/en.json`, `zh-CN.json` | 174 `devin.*` keys (parity checked) |
| `scripts/dashboard-smoke.mjs`, `test/helpers/dashboard-smoke-upstream.mjs` | `devin-panel` smoke flow |
| `test/dashboard-devin-panel.test.js` | builder contract |
| `test/dashboard-devin-panel-render.test.js` | shipped-template render guards |
| `test/dashboard-devin-panel-routes.test.js` | route/auth/leak/cache contract |
| `test/dashboard-escape-callsites.test.js` | reviewed line numbers re-anchored to the new file |

## Not verified / limits

- **No live browser screenshot in this lane.** Rendering is verified by executing the
  shipped template methods and by the dashboard smoke flow; the visual layout was not
  re-measured in a browser. `[UNVERIFIED]` for pixel-level layout at 1440/720.
- **Cascade trajectories remain unread.** The proxy does not call the local-LS
  `GetAllCascadeTrajectories` / `GetCascadeTranscriptForTrajectoryId` RPCs, and a
  `DEVIN_CONNECT` deployment has no local LS. The panel states this; it does not guess.
- **ACP sessions remain untracked.** `devin acp` runs are not registered anywhere after
  the turn ends, so there is no honest session list to show.
- **Turn rings are process-local.** A restart starts a new window; the panel says so.
- **Scan cap.** At most 500 accounts (newest `lastUsed` first) are read per request; the
  response carries `totalAccounts`/`capped` and the UI names the cap when it bites.
- **ACU/reconcile are billable/cold-cache reads.** They only run when the operator presses
  the button; their copy says so.