Skip to content

Add read-only Twister configuration UI - #10

Draft
oveddan wants to merge 4 commits into
mainfrom
agent/read-only-ui
Draft

Add read-only Twister configuration UI#10
oveddan wants to merge 4 commits into
mainfrom
agent/read-only-ui

Conversation

@oveddan

@oveddan oveddan commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • add mft-config ui, a small local web application for visually inspecting a connected MIDI Fighter Twister's complete configuration: a banked 4x4 knob grid, persistent active/inactive/detent colors, rotary and push MIDI channel/number mappings, switch action and movement mode, indicator/detent/super-knob settings, global settings, device identity/firmware details, compatibility warnings, and the complete raw JSON export
  • support opening a previously exported JSON snapshot when hardware is unavailable, entirely client-side (no MIDI access on import)
  • allow downloading the exact in-memory snapshot as JSON
  • document the architecture and local dev workflow in docs/ui-architecture.md, and link it from the README

Architecture

A small local Node HTTP server (src/ui-server.ts) reuses the existing read-only exporter and protocol decoder that already back mft-config export, and serves dependency-free static HTML/CSS/JS (ui/). The browser never opens MIDI ports; it only talks to two endpoints on the local server: GET /api/devices and POST /api/export. This was chosen over a packaged desktop shell as the smallest approach that preserves reliable SysEx access (Node keeps the native MIDI bindings) while keeping the safety boundary in one place.

Safety boundary

The read-only guarantee is enforced at the transport layer, not just in the UI:

  • the server depends on RtMidiReadOnlyBackend, which exposes only discover/connect — its type has no connectForApply, so it cannot construct the separate write-capable connection the CLI's apply command uses
  • every outbound SysEx frame is independently validated by assertReadOnlyRequest immediately before the native MIDI output call, allowing only the Universal Identity request, global pull (0x02), and encoder bulk-pull (0x04/0x01) — configuration writes (0x01), system/reset/bootloader-shaped commands, and any other command are rejected with a thrown error
  • the HTTP surface has exactly two routes; there is no plan, apply, write, reset, system, or bootloader route, and unknown /api/* paths return 404 without ever opening a MIDI connection

Automated tests assert both layers (test/ui-server.test.ts):

  • the read-only backend's public shape has no apply-connection constructor
  • the transport guard blocks mutating SysEx (0x01 write, malformed bulk push, 0x03 system command) before it reaches the MIDI output, and still permits reads
  • a full snapshot export uses only allowlisted read requests (60+ frames asserted individually)
  • requests to apply, plan, write, reset, bootloader, and system API paths all 404 and never open a connection
  • permission, malformed-response, partial-read, and disconnected-device errors are classified and reported with distinct HTTP status/error codes

Validation

  • npm run build — clean TypeScript build
  • npm test — 21/21 tests pass, including the 5 UI-server tests above
  • npm run test:package — packed tarball smoke test passes with the ui/ assets and docs/ui-architecture.md included
  • manually started node dist/ui-server.js with no hardware attached: index page and /api/devices (empty array) respond 200, and a mutation-shaped route (/api/apply) responds 404 without opening a MIDI connection

Closes #1

Greptile Summary

The PR adds a local read-only Twister configuration viewer, supporting live MIDI exports and offline JSON snapshots, while moving apply state into a per-user directory.

  • Adds a dependency-free browser UI and local HTTP server.
  • Adds a transport-level read-only MIDI backend and request allowlist.
  • Extends packaging, tests, documentation, and CI for the new UI.
  • Adds per-user journal and backup storage.

Confidence Score: 1/5

The PR is not safe to merge until stale device selection, legacy consumed-plan detection, and mutable CI action references are fixed.

Export requests can still omit device identity and silently select another controller after discovery ordering changes; apply ignores legacy completion records and can repeat consumed writes; CI continues to execute actions through mutable tags.

Files Needing Attention: src/ui-server.ts, ui/app.js, src/cli.ts, src/journal.ts, .github/workflows/ci.yml

Important Files Changed

Filename Overview
src/ui-server.ts Adds the read-only HTTP surface and export orchestration, but its attempted stale-device protection remains bypassable when optional identity fields are omitted.
src/midi.ts Adds a restricted backend whose outbound frames are checked against the read-only SysEx allowlist.
ui/app.js Implements live discovery, export, snapshot validation, rendering, import, and download, but conditionally omits device-selection metadata.
src/cli.ts Adds the UI command and per-user state paths, while the previously reported legacy-journal compatibility gap remains.
.github/workflows/ci.yml Adds multi-platform checks and package smoke tests, while executable actions remain referenced through mutable tags.
README.md Documents the UI and per-user state location, though the previously reported obsolete backup-path note remains.

Sequence Diagram

sequenceDiagram
  participant Browser
  participant Server as Local UI server
  participant MIDI as Read-only MIDI backend
  participant Device as MIDI Fighter Twister
  Browser->>Server: GET /api/devices
  Server->>MIDI: discover()
  MIDI->>Device: Identity request
  Device-->>MIDI: Identity response
  MIDI-->>Server: Device descriptors
  Server-->>Browser: Device list
  Browser->>Server: POST /api/export
  Server->>MIDI: discover() and connect()
  MIDI->>Device: Allowlisted configuration pulls
  Device-->>MIDI: Global and encoder responses
  MIDI-->>Server: Complete snapshot
  Server-->>Browser: JSON snapshot
Loading

Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
src/ui-server.ts:166-168
**Optional identity check permits reselection**

When an export request omits `inputPort` and `outputPort` after device discovery ordering changes, the server skips the identity comparison and exports whichever controller now occupies `deviceIndex`, causing the UI to return another Twister's configuration.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (2): Last reviewed commit: "fix: address adversarial review findings..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

oveddan and others added 3 commits August 3, 2026 23:58
Add a local, dependency-free web UI (mft-config ui) for visually inspecting
a connected MIDI Fighter Twister's complete configuration: banked 4x4 knob
grid, persistent active/inactive/detent colors, rotary and push MIDI
mappings, switch/movement/indicator/detent/super-knob settings, global
settings, device identity and firmware details, compatibility warnings, and
raw JSON export/download/offline-import.

The UI reuses the existing read-only exporter and protocol decoder. The
read-only boundary is enforced at the transport layer, not just in the
browser: the server depends on RtMidiReadOnlyBackend, which exposes only
discover/connect and cannot construct the separate write-capable connection;
every outbound frame is also independently validated by
assertReadOnlyRequest immediately before the native MIDI call, and the HTTP
surface exposes no plan/apply/write/reset/system/bootloader route. Automated
tests assert both layers, including that unknown /api/* mutation-shaped
routes 404 without ever opening a MIDI connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread src/cli.ts
Comment thread src/ui-server.ts Outdated
Comment thread .github/workflows/ci.yml
Comment on lines +30 to +31
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Workflow actions use mutable tags

The workflow executes actions/checkout@v4 and actions/setup-node@v4 rather than immutable commit SHAs, so upstream tag movement can change code executed with repository read access or alter the checkout and toolchain used by subsequent checks.

How this was verified: Both executable action references use mutable @v4 tags and the workflow grants contents: read.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 30-31

Comment:
**Workflow actions use mutable tags**

The workflow executes `actions/checkout@v4` and `actions/setup-node@v4` rather than immutable commit SHAs, so upstream tag movement can change code executed with repository read access or alter the checkout and toolchain used by subsequent checks.

**How this was verified:** Both executable action references use mutable `@v4` tags and the workflow grants `contents: read`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28ed467b60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ui/colors.js
@@ -0,0 +1,38 @@
const MF64_HUES = [0, 0, 16, 60, 140, 120, 114, 96, 78, 42, 22, 0, 334, 300, 320];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the MF64 hue table for blue and teal

When an MF64 snapshot contains canonical teal (index 33) or blue (index 45), this table makes the viewer render them as hsl(78 …) and hsl(0 …)—yellow-green and red, respectively—even though src/planner.ts maps those indices to teal and blue. Since the knob grid is intended to visually identify stored colors, these common MF64 settings are presented as the wrong colors; update the corresponding hue groups to match the palette.

Useful? React with 👍 / 👎.

An Opus adversarial review of the read-only Twister UI (write-safety
boundary itself verified solid, untouched here) found six correctness
and robustness issues, all fixed:

1. ui/app.js: assertSnapshot now validates globals.colorMap/superKnob/
   brightness/animationChannels/sleep, device.midiPorts, firmware
   identityBytes, and capturedAt. renderSnapshot is now transactional:
   it only commits state.snapshot and unhides the viewer after every
   render function succeeds; a failure rolls back to the previous good
   snapshot (or the empty state) instead of leaving a spliced mix of
   new/old content on screen with a corrupt snapshot wired to Download.

2. src/ui-server.ts: /api/export now has a single-flight guard so a
   second concurrent export request gets 409 EXPORT_IN_PROGRESS instead
   of racing to open the same MIDI ports and having pullEncoderData
   match replies across requests by tag alone.

3. src/ui-server.ts rejects requests whose Host header doesn't name
   this server (400 INVALID_HOST), closing the cross-origin/DNS-rebind
   path to the local MIDI API. cli.ts now also validates and warns on
   a non-loopback MFT_CONFIG_UI_HOST.

4. src/midi.ts: RtMidiPorts.send is guarded again (defaults to
   assertReadOnlyRequest; RtMidiApplyConnection passes the broader
   assertApplyRequest), restoring "innermost layer is safe by default"
   even if a future call site bypasses the wrapper classes.

5. src/exporter.ts: pullGlobals/pullDeviceId/pullEncoderData once again
   check the DJTT vendor header before trusting the command byte, so
   unrelated SysEx sharing the bus can't be misread as a malformed
   Twister reply.

6. src/ui-server.ts: /api/export now also accepts the inputPort/
   outputPort the browser saw at discovery time and rejects the
   request (409 DEVICE_LIST_CHANGED) if the freshly re-discovered
   device at that index no longer matches, instead of silently
   exporting the wrong (still read-only) device after a replug.

Adds test/ui-app.test.ts (jsdom-backed) covering fix #1, and extends
test/ui-server.test.ts with coverage for fixes #2, #3, and #6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@oveddan

oveddan commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Follow-up: adversarial review fixes

An Opus adversarial review of this branch found six correctness/robustness issues (the write-safety boundary itself was verified solid and left untouched). All six are addressed in c5c32d8:

  1. Malformed offline snapshot corrupting the viewerassertSnapshot now validates globals.colorMap/superKnob/brightness/animationChannels/sleep, device.midiPorts, firmware identityBytes, and capturedAt. renderSnapshot is now transactional: it only commits state.snapshot and unhides the viewer once every render function succeeds; on failure it rolls back to the previous good snapshot (or the empty state) instead of leaving a spliced mix of new/old content with a corrupt snapshot wired to Download JSON.
  2. No serialization on /api/export — added a single-flight guard; a second concurrent export now gets 409 EXPORT_IN_PROGRESS instead of racing to open the same MIDI ports.
  3. No Host validation — the server now rejects requests whose Host header doesn't name it (400 INVALID_HOST), closing the cross-origin/DNS-rebinding path. MFT_CONFIG_UI_HOST is now validated/warned-on in the CLI when non-loopback.
  4. RtMidiPorts.send lost its guard — re-added as a constructor-injected guard defaulting to assertReadOnlyRequest, so the innermost layer is safe by default even if a future call site bypasses the wrapper classes.
  5. Response-matching regression in src/exporter.tspullGlobals/pullDeviceId/pullEncoderData once again check the DJTT vendor header before trusting the command byte.
  6. deviceIndex TOCTOU/api/export now also accepts the inputPort/outputPort names the browser saw at discovery time and rejects (409 DEVICE_LIST_CHANGED) if the freshly re-discovered device at that index no longer matches, instead of silently exporting a different (still read-only) device after a replug.

Added test/ui-app.test.ts (jsdom-backed) covering #1, and extended test/ui-server.test.ts with coverage for #2, #3, and #6. npm run build && npm test passes (28/28).

Comment thread src/ui-server.ts
Comment on lines +166 to +168
if (
(inputPort !== undefined && inputPort !== device.inputPort.name) ||
(outputPort !== undefined && outputPort !== device.outputPort.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Optional identity check permits reselection

When an export request omits inputPort and outputPort after device discovery ordering changes, the server skips the identity comparison and exports whichever controller now occupies deviceIndex, causing the UI to return another Twister's configuration.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui-server.ts
Line: 166-168

Comment:
**Optional identity check permits reselection**

When an export request omits `inputPort` and `outputPort` after device discovery ordering changes, the server skips the identity comparison and exports whichever controller now occupies `deviceIndex`, causing the UI to return another Twister's configuration.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

@oveddan
oveddan marked this pull request as draft August 7, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build a visual read-only configuration UI

1 participant