feat(adr-186): P5/P6 + acceptance verification — dashboard honesty, HTTP tests, Accepted

Completes ADR-186 phases P5 (dashboard honesty / fallback guarantee) and P6
(tests + witness) on top of the P1–P4 reconnection, and flips the ADR to Accepted.

P5 — dashboard honesty:
- Backend (training_api): a runtime enablement gate (`RUVIEW_DISABLE_SERVER_TRAINING`)
  makes the three start endpoints return a structured `{enabled:false, reason,
  cli:"wifi-densepose train-room"}` HTTP 409 (never a silent success) when server
  training is disabled; `/api/v1/train/status` now carries an `enabled` flag. A runtime
  flag (not a Cargo feature) so `--no-default-features` builds keep training ON (resolves
  §9.4). Handlers return `Response` to carry the 409.
- Frontend (TrainingPanel.js): reads `enabled` off the status payload and disables the
  Start/Pretrain/LoRA buttons with a CLI tooltip when server training is off; a rejected
  start tears down the optimistically-opened WS and refreshes. The enabled path was
  already fully wired (opens the WS before POST, renders live epoch/loss/ETA + terminal
  state) — verified.

P6 — tests & witness:
- New HTTP-level tests in a `#[cfg(test)] mod adr186_http_tests` built on a minimal
  `AppStateInner::minimal()` test-state helper: a live-socket test that completes a
  genuine 101 WebSocket handshake (tokio-tungstenite) and receives a real progress frame
  after a POST start; a 426-not-404 route-wired check; a full POST→poll-status→`.rvf`-exists
  round-trip; and the disabled-409 structured-response test. Added `tokio-tungstenite`
  dev-dep (version already in the workspace lock).
- CHANGELOG updated; ADR §6 ledger + §7 acceptance criteria checked off with evidence;
  Status → Accepted. README/CLAUDE have no training route table (no edit needed).

Fixed a test-only parallelism race found under `cargo test --workspace`: two
model-writing tests deleted `.rvf`s by directory-diff and could remove a file a third
test asserted existed. Each test now cleans only its own artifact (data/models is
gitignored).

Verified this session: `cargo test -p wifi-densepose-sensing-server -p wifi-densepose-train
--no-default-features` — sensing-server bin 217 passed / 0 failed, all train suites 0
failed. Full `--workspace` re-run in progress to reconfirm untouched crates.
This commit is contained in:
ruv
2026-07-21 17:19:20 -07:00
parent a4a3f456e1
commit 65648fe4c2
7 changed files with 537 additions and 78 deletions
+25 -2
View File
@@ -154,7 +154,14 @@ export default class TrainingPanel {
};
await trainingService[method](payload);
await this.refresh();
} catch (e) { this._set({ loading: false, error: `Training failed: ${e.message}` }); }
} catch (e) {
// Start was rejected (e.g. server training disabled → HTTP 409). Tear down
// the progress socket we opened optimistically and refresh so the button
// reflects the real (possibly disabled) state instead of a silent no-op.
trainingService.disconnectProgressStream();
this._set({ loading: false, error: `Training failed: ${e.message}` });
this.refresh();
}
}
async _stopTraining() {
@@ -272,13 +279,29 @@ export default class TrainingPanel {
form.appendChild(ir('LoRA Profile (opt.)', 'text', this.config.lora_profile_name, v => { this.config.lora_profile_name = v; }));
s.appendChild(form);
// ADR-186 P5: if the server reports in-server training disabled
// (enabled:false), the Start buttons must be disabled with a CLI tooltip —
// never a silent no-op. Enablement is surfaced on the status payload.
const ts = this.state.trainingStatus;
const disabled = ts && ts.enabled === false;
const cli = (ts && ts.cli) || 'wifi-densepose train-room';
if (disabled) {
const note = this._el('div', 'tp-empty',
`In-server training is disabled on this build. Train from the CLI: ${cli}`);
s.appendChild(note);
}
const acts = this._el('div', 'tp-train-actions');
const btns = [
this._btn('Start Training', 'tp-btn tp-btn-success', () => this._launchTraining('startTraining', { patience: this.config.patience, base_model: this.config.base_model || undefined })),
this._btn('Pretrain', 'tp-btn tp-btn-secondary', () => this._launchTraining('startPretraining')),
this._btn('LoRA', 'tp-btn tp-btn-secondary', () => this._launchTraining('startLoraTraining', { base_model: this.config.base_model || undefined, profile_name: this.config.lora_profile_name || 'default' }))
];
btns.forEach(b => { b.disabled = this.state.loading; acts.appendChild(b); });
btns.forEach(b => {
b.disabled = this.state.loading || disabled;
if (disabled) b.title = `In-server training disabled — use: ${cli}`;
acts.appendChild(b);
});
s.appendChild(acts);
return s;
}