Skip to content

dmx: loud reporting for venue universes the profile can't drive - #424

Merged
mdwn merged 1 commit into
mainfrom
multi-universe-guardrails
Aug 20, 2026
Merged

dmx: loud reporting for venue universes the profile can't drive#424
mdwn merged 1 commit into
mainfrom
multi-universe-guardrails

Conversation

@mdwn

@mdwn mdwn commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up from a venue-portability review: the DMX render path is already multi-universe end to end (per-fixture universes in the venue DSL and model, per-universe command routing, per-universe output threads via OLA) — but a venue patching universes the active profile doesn't configure failed silently: update_effects dropped those commands with no warning anywhere. At an unfamiliar venue with a multi-universe patch, most of the rig would simply stay dark.

  • Venue registration now reports each unconfigured universe once, naming the fixtures patched there ("Venue fixtures are patched on universe 2 ... they will not light").
  • The effects loop warns once per universe (not per 44Hz tick) if commands are dropped anyway — backstop, not report. The warned-set resets on re-registration.
  • Example + docs: a multi-universe warehouse venue and a docs section stating the rule — shows never mention universes (they target groups), and every universe a venue references needs an output under dmx.universes.
  • Design doc: universe coverage joins the P1b pre-show lint list, and chase direction is recorded as position-resolved once venues carry positions (today's left_to_right is group-list order, spatially meaningless on any number of universes).

Test plan

  • cargo test — 3422 pass; fmt/clippy/licensure clean.
  • New unit tests for the gap-report helper (unconfigured universes grouped and sorted, fully-configured venues report nothing).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SKzGqZbcZtMR5ZqtDJoDzz

A venue can patch fixtures on universes the active profile configures
no output for; the engine used to drop their DMX silently — three
quarters of a four-universe house rig staying dark at a gig with no
clue why. Now:

- Venue registration reports each unconfigured universe once, naming
  the fixtures patched there.
- The effects loop's routing warns once per universe (not per 44Hz
  tick) if commands are dropped anyway — the backstop, not the report.
- A multi-universe example venue (warehouse) and docs cover the rule:
  shows never mention universes; every universe a venue references
  needs an output under dmx.universes.
- Design doc: universe-coverage joins the pre-show lint list (P1b), and
  chase direction is noted as position-resolved once venues carry
  positions — list order today has no spatial or cross-universe
  meaning.

The render path was already multi-universe end to end (per-fixture
universes, per-universe routing and output threads); these are the
guardrails that make a multi-universe venue fail loudly instead of
partially and silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKzGqZbcZtMR5ZqtDJoDzz
@mdwn
mdwn merged commit 3431e04 into main Aug 20, 2026
13 checks passed
@mdwn
mdwn deleted the multi-universe-guardrails branch August 20, 2026 03:55
mdwn pushed a commit that referenced this pull request Sep 9, 2026
Backfills the GDTF import work (#422, #423, #425, #426) and the
multi-universe reporting change (#424), which landed after the 0.16.0
release commit without changelog entries, and adds the Debian packaging
entry alongside them.

The internal-only groundwork PRs are folded into the single GDTF entry
rather than listed separately: on their own they had no user-facing
surface, and it is the feature they add up to that is worth reading
about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgFMfQVvNPsM4huUYmqXwn
mdwn added a commit that referenced this pull request Sep 9, 2026
…#427)

* packaging: Debian package and R2-hosted apt archive

Adds a .deb and the pipeline to publish it, giving the Raspberry Pi image an
install payload and everyone else an upgrade path that is not "download a
tarball again".

The package does what the deployment guide currently asks operators to do by
hand: creates the mtrack service account, adds it to audio, creates and chowns
the project directory, generates the systemd unit, and enables the service.
Because /etc/default/mtrack is unpacked first, MTRACK_PATH is known by the time
postinst runs, so the unit is rendered with ProtectSystem=strict and a matching
ReadWritePaths rather than the weaker pathless fallback -- the hardened form is
what you get by default instead of what you get if you remember the argument.

The unit is generated by the installed binary rather than shipped as a file, so
src/cli.rs stays its only definition and an upgrade re-renders it -- which is
the "regenerate your unit" step the guide has to ask for today. Operator edits
are preserved: postinst compares against an MD5 of what it last wrote and backs
off if the file has changed, since the same guide tells people to add
RequiresMountsFor= themselves. It also resolves the binary absolutely, never
through PATH, so a leftover /usr/local/bin install from the tarball era cannot
end up baked into ExecStart.

Purging keeps /var/lib/mtrack and the account that owns it. A purge should not
take a band's set with it, and removing the user would strand those files under
a uid a reinstall would not reallocate.

Dependencies are hand-written rather than cargo-deb's $auto. dpkg-shlibdeps
emits versioned dependencies, and libasound2t64 only carries
"Provides: libasound2 (= <version>)"; a versioned Provides satisfies an
unversioned Depends but not a versioned one, so an auto-generated package would
build on bookworm and refuse to install on trixie. The alternation resolves on
both sides of the time_t rename.

Packages are built by repackaging the binaries the release already published,
so a .deb and a tarball of the same version hold identical bytes and one amd64
runner emits both architectures.

The apt archive is a static tree synced to R2, published pool-first so an index
never names an object the store does not have yet, and never pruned so
`apt install mtrack=<older>` keeps working as a rollback. The publish-apt job
is skipped until APT_R2_BUCKET is set, so the .deb on the release page is
useful before anyone owns a signing key. packaging/README.md carries the
one-time setup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgFMfQVvNPsM4huUYmqXwn

* docs: restore [Unreleased] changelog section

Backfills the GDTF import work (#422, #423, #425, #426) and the
multi-universe reporting change (#424), which landed after the 0.16.0
release commit without changelog entries, and adds the Debian packaging
entry alongside them.

The internal-only groundwork PRs are folded into the single GDTF entry
rather than listed separately: on their own they had no user-facing
surface, and it is the feature they add up to that is worth reading
about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgFMfQVvNPsM4huUYmqXwn

* packaging: install-and-purge test for the Debian package

The jobs that build and publish the package live in publish.yaml, which
runs on tags, so nothing exercised the maintainer scripts on the change
that touched them -- a broken postinst would have surfaced during a
release. This adds `make test-deb` and runs it on pull requests.

It covers the cases that only exist in the package: that the generated
unit names the packaged binary and carries the strict sandbox, that a
reinstall leaves an operator-edited unit alone and says so, that an
unedited one is re-rendered, that a stale /usr/local/bin copy cannot
capture ExecStart, and that purging keeps the song library and the
account that owns it. Verified it fails when the PATH resolution fix is
reverted, rather than passing regardless.

The test builds into its own target directory. It stages a debug binary
where cargo-deb expects a release one, and doing that under
target/release would leave a debug binary that cargo -- which fingerprints
against its own database, not the file -- may then decline to rebuild.

Unlike the systemd integration test this runs on pull requests: it costs
a debug build against a cache the other jobs have already warmed rather
than a container image, and what it checks is what lands on disk, not
whether the service comes up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgFMfQVvNPsM4huUYmqXwn

* ci: lint shell scripts with shellcheck

Nothing checked the shell in this repo, and the Debian maintainer scripts
this branch adds run as root on a user's machine at install time -- the
least forgiving place to leave a quoting mistake. Adds `make lint-shell`
and a job that runs it on pull requests.

The file list is discovered rather than written down, so a script added
later is linted without anyone remembering to add it. The Debian
maintainer scripts carry no extension and so are missed by a *.sh glob;
everything in packaging/debian is shell except the conffile.

shellcheck is installed rather than taken from the runner image, which
carries an older version. A lint gate that moves underneath you flags
things nobody changed and misses things nobody added.

Two pre-existing scripts needed adjusting to pass:

- tests/systemd/test.sh writes its assertions as
  `bash -c '... "$0" ...' "$arg"`, where $0 is bash -c's positional
  parameter and must not be expanded by the outer shell. The single
  quotes are correct -- the file takes an SC2016 disable rather than a
  change, and the comment says why so nobody "fixes" it later.
- scripts/hardware-test.sh ran `cd "$PROJECT_ROOT"` unguarded, so a
  failed cd would have built and tested in whatever directory it
  happened to be in (SC2164).

All eight shell files in the repo pass. Verified the target fails on a
deliberately broken script rather than passing vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgFMfQVvNPsM4huUYmqXwn

---------

Co-authored-by: Claude <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