feat(graph): make the visual editor a full wiring control surface

Lets users wire the system end-to-end from the graph, and fixes the core
bug that made drag-to-wire silently fail.

- Fix drag-to-wire 422s across 5 entity kinds: updateConnection() now echoes
  the target's discriminator (source_type/stream_type/target_type) into the
  partial PUT, so value/colour-strip/audio/picture sources and output targets
  all wire correctly. New contract test (54 cases) in test_graph_wiring_contract.py.
- Re-wire composite layers / mapped zones from the graph (right-click a
  layer/zone source edge -> Re-wire). Whole-list write preserves every sibling
  layer/zone setting, with an optimistic-concurrency guard and undo.
- Secret-safe /graph topology: project entities to id/name/subtype + reference
  roots so the endpoint cannot leak webhook tokens or other credentials.
- Carry slot indices on list edges; node custom-icon + schema-drift refinements;
  rewire i18n keys (en/ru/zh); wiring-control roadmap (TODO.md).
This commit is contained in:
2026-05-29 02:29:19 +03:00
parent 05cf121666
commit 2e51f46dfd
13 changed files with 677 additions and 53 deletions
+57 -21
View File
@@ -1032,32 +1032,68 @@ viewer. Driven by the ULTRA-DEEP review (findings A1A5, B1B6, C1C6, D1
(only offers slots the target entity actually has). Writes the partial
`{ <slot>: { source_id } }` payload → backend `Bindable*.apply_update` merges,
preserving the static value. Verified data-safe (no `from_raw`/value-reset path).
- [x] Render the two functional value-source references `buildGraph` was missing —
`value_source.value_source_id` (gradient_map → inner value source) and
`value_source.color_strip_source_id` (css_extract → strip). Both are runtime-
resolved and already drag-editable; now visible/detachable in the graph.
- [x] **B4 foundation:** backend schema now authoritative about graph-editability
(`is_editable()` + `editable` flag in `/graph/schema`); `validate-connection`
hardened to reject non-editable fields (colour/list/double-nested), not just lists.
- [x] **B4 drift guard + gap fixes:** `checkSchemaDrift()` (graph-connections.ts) warns
once if the frontend `CONNECTION_MAP` editable set diverges from `/graph/schema`
(the automated "10-step checklist"). Surfacing it found 3 real gaps; fixed 2:
`color_strip_source.input_source_id` + `processing_template_id` are now drag-editable
(processed-strip wiring; `apply_update` is partial-safe). The 3rd —
`device.default_css_processing_template_id` — is intentionally NOT drag-editable
(the device PUT route isn't partial-safe; a one-field PUT could null the URL) and is
in the drift-check exclude set. Also broadened `_availableMatches` to hide any slot
the target entity doesn't expose (subtype-accurate; refs are always-emitted so empty
slots stay wireable). Review also caught a **dead `output_target.picture_source_id`
slot** (no output target stores it — not a field/schema, never emitted) — removed
from both registries + `buildGraph`.
- [x] **Comprehensive review pass (4 subagents: backend/frontend-core/orchestrator/security).**
Findings fixed:
- **CRITICAL (security):** `GET /api/v1/graph` leaked plaintext **webhook tokens**
(`asdict` recursed `Automation.rules[].token`, an auth-equivalent secret). Fixed with
**field-projection** — `serialize_entity_for_graph()` / `graph_field_roots()` project
each entity to only `{id, name, subtype, reference-roots}`; secrets can't survive.
Added a structural regression test asserting no projection root is secret-bearing for
any kind (drift-proof boundary) + a token-drop test.
- MEDIUM: added missing `value_source.clock_id` (AnimatedColorValueSource → sync_clock)
to the backend registry for topology/dependents completeness (drift-excluded on the
frontend — value-source PUT needs a `source_type` discriminator, so it's editor-only).
- MEDIUM/LOW: `CSS.escape` on the markIssues id selector; grouped/clarified
`_DRIFT_EXCLUDE`; fixed the stale `_availableMatches` JSDoc; documented the
`checkSchemaDrift` forward-reference. Orchestrator + frontend-core + security: APPROVE.
- Verification: `npm --prefix server run typecheck` + `run build` clean; ruff clean;
graph backend tests 24 pass; full backend suite 1614 pass. 6 code-review passes,
graph backend tests 35 pass; full backend suite green. ~8 code-review passes,
all CRITICAL/HIGH findings fixed.
### Left to do (deferred)
- [ ] **BindableColor slots** (`color`, `color_peak`, `fallback_color`,
`default_color` on color_strip_source) — left non-editable in B1 because
scalar-value-source → colour-slot semantics are unclear. **First check:** do
colour-producing value sources exist? If a value_source can drive a colour,
mark these 4 CONNECTION_MAP entries `bindable: true` (they already validate on
the backend); the write path (`{ color: { source_id } }` → `BindableColor.apply_update`)
is already value-preserving. If not, leave read-only.
- [ ] **B4 — delete the frontend `CONNECTION_MAP` duplication.** Blocked on a backend
write endpoint. Plan:
1. Add `PUT /api/v1/graph/connection` (body: target_kind/id, field, source_id)
that validates (reuse `validate_connection`) then APPLIES the write
server-side — top-level via the owning store's update; single-level bindable
via `apply_update`. Must reuse each entity's existing update path (validation,
factory reconstruction, entity_changed event) — do NOT hand-roll per-store
mutation. Highest regression risk; needs per-kind tests.
2. Switch frontend `updateConnection`/`detachConnection` to call it.
3. Have the frontend fetch `/graph/schema` and build ports/edges from it,
then delete `CONNECTION_MAP` + the buildGraph edge duplication
(graph-connections.ts / graph-layout.ts). Removes the 10-step sync checklist
in `contexts/graph-editor.md`.
- [x] **BindableColor slots** — CHECKED, decision: keep read-only (won't fix).
Value sources are scalar-only (`ValueStream.get_value() -> float`) and every
colour consumer (`color_strip/single.py`, `effect_stream.py`) reads the static
RGB via `bcolor()`, ignoring `source_id`. So a value_source cannot drive a
colour — wiring `color`/`color_peak`/… would be a dead binding. Documented in
`api/graph_schema.py` next to the BindableColor entries. (Would only become
viable if a colour-producing value-source type is added.)
- [~] **B4 — delete the frontend `CONNECTION_MAP` duplication.**
- [x] **Foundation done:** the backend schema now carries an authoritative
`editable` flag per field (`is_editable()` in `api/graph_schema.py`, mirroring
the frontend `_isEditable`: top-level refs + single-level BindableFloat slots;
NOT colour/list/double-nested). `validate-connection` is hardened to reject any
non-editable field (was list-only). `editable` is surfaced in `/graph/schema`.
- [ ] **Remaining (the refactor):** frontend fetches `/graph/schema` on load and
derives connection metadata + edges from it (port the `extract_refs` dot-path/list
grammar to TS), keeping only a tiny `kind → {endpoint, cache}` write-routing table;
then delete the field-level `CONNECTION_MAP` + the `buildGraph` edge loops
(graph-connections.ts / graph-layout.ts). Removes the 10-step sync checklist in
`contexts/graph-editor.md`. **A backend apply-write endpoint is NOT required** —
keep the proven per-entity PUT. Risk: regressing drag-connect/bindable; keep a
dev drift-check (frontend editable set vs `/graph/schema`) during the transition.
Note: frontend `CONNECTION_MAP` also has inert `ha_source_id`/`gradient_id` entries
(no graph node kind) — drop them, the backend schema already omits them.
- [ ] **D6 — blueprint import/instantiate.** Export exists; the apply half (serialize
a selected subgraph's topology + entities, re-import with id remapping, conflict
handling) is large and data-integrity-sensitive (see Data Migration Policy in