Skip to content

refactor(app): carve settings into static/js/settings.js (R3d) - #920

Merged
byrongamatos merged 1 commit into
mainfrom
r3d/settings
Jul 12, 2026
Merged

byrongamatos merged 1 commit into
mainfrom
r3d/settings

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

22 declarations, 446 lines. app.js 4,218 → 3,772. Bodies verbatim.

Settings load/save, the AV-offset nudge, the default-arrangement pin, the instrument pathway, and the app-update channel.

Interface width 1 — and it got there by drawing the boundary in the right place

app.js calls loadSettings() and nothing else.

The first cut was not clean. _defaultArrangement was written from outside the cluster, and an imported binding is read-only — so that one write would have forced a setter or a state container, as it did for the player (player-state.js) and the library (library-state.js).

But the writers were saveSettings and pinCurrentArrangementDefault, which are settings functions. Widening the slice to include them left zero outside writes. Every export is now a plain read-only import, and no container is needed.

Worth naming, because I reached for a container twice before: "this binding is written from outside" sometimes means you need a container, and sometimes it just means the boundary is in the wrong place. Measure the writers before you build machinery.

handleSliderInput stays a host hook, deliberately

It lives in settings now (it is a settings control), but player-controls.js must not import it: this module already imports player-controls (_applyMastery, _autoplayExitEnabled, …), so a direct back-import would close a cycle.

player-controls keeps reading it through the host seam, and app.js — the root, which imports both — wires it. That's exactly what the seam is for, and the contract test proves the wiring survived.

Verification

A/B against origin/main in two browsers — identical, zero page errors:

the window contract
the settings screen renders
AV-offset and default-arrangement controls present
a real input event dispatched on a slider — the path that goes through the host seam

node 1045 · pytest 2425 · ESLint 0 (no-cycle clean) · host contract 2/2 · Codex 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added centralized settings management for preferences, instrument pathways, default arrangements, and DLC configuration.
    • Added A/V offset calibration controls with slider adjustments, nudging, and automatic saving.
    • Added app update channel selection and manual update checks on supported desktop platforms.
    • Improved settings loading and persistence across sessions.
  • Bug Fixes

    • Settings controls now handle unavailable options and unsupported update features without disrupting the interface.

22 declarations, 446 lines. app.js 4,218 -> 3,772. Bodies VERBATIM.

Settings load/save, the AV-offset nudge, the default-arrangement pin, the instrument pathway,
and the app-update channel.

━━━ INTERFACE WIDTH 1, AND IT GOT THERE BY DRAWING THE BOUNDARY IN THE RIGHT PLACE ━━━

app.js calls loadSettings() and nothing else.

The first cut was NOT clean: _defaultArrangement was written from OUTSIDE the cluster, and an
imported binding is READ-ONLY, so that one write would have forced a setter or a state
container — as it did for the player (player-state.js) and the library (library-state.js).

But the writers were saveSettings and pinCurrentArrangementDefault, which ARE settings
functions. Widening the slice to include them left ZERO outside writes. Every export is now a
plain read-only import and no container is needed.

Worth naming, because I reached for a container twice before: the fix for "this binding is
written from outside" is sometimes a container, and sometimes it just means the boundary is in
the wrong place. Measure the writers before you build machinery.

━━━ handleSliderInput STAYS A HOST HOOK, DELIBERATELY ━━━

It lives in settings now (it is a settings control), but player-controls.js must NOT import it:
this module already imports player-controls (_applyMastery, _autoplayExitEnabled, …), so a
direct back-import would close a cycle. player-controls keeps reading it through the host seam,
and app.js — the root, which imports both — wires it. That is exactly what the seam is for, and
the contract test proves the wiring survived.

VERIFIED. A/B against origin/main in two browsers: the window contract, the settings screen
rendering, the AV-offset and default-arrangement controls present, and a real `input` event
dispatched on a slider — which is the path that goes through the host seam. IDENTICAL, zero
page errors.

node 1045, pytest 2425, ESLint 0 (no-cycle clean), host contract 2/2, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ad6770c-0367-4bf5-8583-e60f1928cf2c

📥 Commits

Reviewing files that changed from the base of the PR and between 69aac32 and 573d9ec.

📒 Files selected for processing (3)
  • static/app.js
  • static/js/settings.js
  • tests/js/settings_autosave.test.js

📝 Walkthrough

Walkthrough

Settings hydration, persistence, A/V calibration, arrangement pinning, and desktop app-update controls were moved from static/app.js into a new static/js/settings.js module. Autosave tests now inspect the extracted module.

Changes

Settings module extraction

Layer / File(s) Summary
Settings contracts, hydration, and arrangement wiring
static/js/settings.js, static/app.js
Pathway normalization, default-arrangement synchronization, settings hydration, and the arr-select listener are provided through the new module imported by app.js.
Calibration and settings persistence
static/js/settings.js, tests/js/settings_autosave.test.js
A/V offset handling, bulk and serialized setting persistence, pathway updates, status reporting, and autosave test loading now use settings.js.
Desktop app-update controls
static/js/settings.js, static/app.js
Desktop update-channel and check-now handling is implemented in settings.js, replacing the removed inline block in app.js.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • got-feedBack/feedBack#753: Refactors pathway persistence and hydration into static/js/settings.js, matching this module’s /api/settings integration.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: extracting settings logic from app.js into static/js/settings.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch r3d/settings

Comment @coderabbitai help to get the list of available commands.

@byrongamatos
byrongamatos merged commit 84fe296 into main Jul 12, 2026
5 checks passed
@byrongamatos
byrongamatos deleted the r3d/settings branch July 12, 2026 12:08
byrongamatos added a commit that referenced this pull request Jul 12, 2026
…creen, closeCurrentSong (R3d) (#921)

36 declarations + the 4 autoplay/auto-exit gate statements. 359 lines.
app.js 3,772 -> 3,242. Bodies VERBATIM.

━━━ THIS WAS "THE UNCUTTABLE HEART", AND IT IS 359 LINES ━━━

At the start of this epic, seeding a dependency closure from count-in, from loops, from
section-practice, or from the JUCE seek shim all returned the SAME 178-function, 3,360-line set.
playSong and showScreen called each other; everything called them; nothing could be cut anywhere.
The conclusion — correct at the time — was that NO closure-based carve could touch it at any
seed, and the answer was a host seam.

That was true THEN. Every slice taken out since (transport, loops, count-in, section-practice,
the library, the edit modal, settings) removed edges, and the strongly-connected component
DISSOLVED. This closure is 36 declarations with an interface width of FOUR.

The lesson is not that the seam was wrong — the seam is what MADE this possible, by letting the
carves proceed against a cyclic core instead of stalling on it. The lesson is to RE-MEASURE. An
SCC is a fact about a graph at a moment, not a property of the code.

━━━ THE BUG NO SCAN COULD SEE, AND THE A/B DID ━━━

First cut passed every gate — no-undef clean, no-cycle clean, 1045/1045, pytest green — and
THREW IN THE BROWSER: "Assignment to constant variable."

window.feedBack.holdAutoplay / holdAutoExit and their two event handlers are TOP-LEVEL
STATEMENTS, not declarations. They WRITE this cluster's state (_autoplayHeld, _autoExitTimer, …),
and an imported binding is READ-ONLY — so left behind in app.js, every one threw the instant the
module existed.

A dependency scan that walks DECLARATIONS cannot see them. Mine didn't. This is the same blind
spot that nearly shipped a dead library A-Z rail (#896): app.js keeps its public API in top-level
statements, and a call-graph is blind to every one of them.

The extractor now finds them by construction — any top-level statement that WRITES a moved
binding comes with the carve — and the gate statements live beside the machinery they drive,
which is where they belonged anyway.

━━━ ZERO OUTSIDE WRITES, BY MOVING THE BOUNDARY RATHER THAN BUILDING MACHINERY ━━━

The autoplay scalars and the wake-lock state were written from outside the cluster, which would
have forced a setter or a state container. But the writers — _releaseAutoplay, _acquireWakeLock —
plainly belong here. Pulling them in left ZERO outside writes, so every export is a plain import.
Same move as settings (#920): measure the writers before you reach for a container.

VERIFIED. A/B against origin/main in two browsers, IDENTICAL, zero page errors — including the
autoplay gate driven end to end: a plugin HOLDS autoplay, the song loads but does not start, the
RELEASE fires it, and a stale release is a no-op. That is the exact machinery that was throwing.

node 1045, pytest 2425, ESLint 0 (no-cycle clean), host contract 2/2, Codex 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant