717 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 56425fca6a Let a song carry named markers on a strip in the song tab (#338)
feuerwerk / gate (push) Successful in 5m22s
feuerwerk / suite (push) Failing after 19m54s
Stage 1 of #338. Game sync needs named places in a song: an event when
the playhead passes one, and a switch that jumps to one. This stage
adds the markers themselves. Games do nothing with them yet.

- A song has markers, each with id, name and tick. A song without
  markers stores nothing for them. Two markers may share tick and
  name, since nodes will name them by id.
- Markers change neither the sound nor the bake fingerprint.
- The song tab draws a strip under the ruler. A marker has a green
  flag with its name and a faint line through the tracks. Tracks and
  lanes sit 18 px lower. The ruler of the pattern tab is unchanged.
- Double click on an empty place adds a marker on the grid, with Alt
  or Shift on the exact tick. Dragging moves it in one undo step.
- A click on a marker sets the start of playback there. A double click
  renames it in the name dialog of the hierarchy groups.
- The context menu offers Play from here, Rename and Delete marker,
  on an empty place Add marker. A tooltip shows the full name. The
  wheel over the strip zooms time like the ruler.
- New agent commands add_song_marker, change_song_marker and
  remove_song_marker. get_asset lists them under markers.

Tests:
- song-markers in core 6 and editor 4, 1 agent test, with the touched
  song, agent and MCP tests 175/175
- 9 counter-probes, all compile, all red
- e2e 65 on atx: ALL PASSED, 70 checks, 3 new in 65g3. Counter-probe
  where a dragged marker is not taken over: 65g3 red
- gate on atx: CHECK EXIT 0, 332 files, 5093 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:44:58 +02:00
adminandClaude Opus 5.5 2099511695 Let the agent set automation lanes of a song track (#337)
feuerwerk / gate (push) Successful in 6m3s
feuerwerk / suite (push) Successful in 20m14s
Stage 4 of #337. Lanes could only be drawn in the song tab. Now the
agent can set them in one step and sees which parameters a lane of a
track can move.

- New command set_song_lane. The target is named as it is stored:
  volume, pan or param:<name>. All points come at once, each with
  tick, value and ease, from the same eases as animation keys,
  curve assets included.
- Points on one tick keep the order they come in, which makes a jump.
  points [] removes a volume or pan lane, remove a parameter lane.
- It refuses with a clear message: a value outside the range, an
  unknown ease, a missing curve asset, a range for volume or pan, and
  a new lane for a parameter nothing on the track has. The message
  then names the parameters there are.
- Without min and max a new parameter lane gets the range the dialog
  in the song tab offers.
- get_asset names under params of each song track the parameters a
  lane can move, with their default. The lanes were already in it.

Live in the user's project: get_asset named both parameters of their
two lanes (frequency, env) with defaults 20000 and 0.5. Playing to
3 s and 4.2 s, both tracks were heard, the audio clock running.

Tests:
- agent-music 13/13 (2 new), agent and MCP tests 129/129
- 6 counter-probes, all compile, all red
- gate on atx: CHECK EXIT 0, 330 files, 5082 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:54:51 +02:00
adminandClaude Opus 5.5 b50c7dbc35 Let a song track automate parameters of its sounds and FX (#337)
Stage 3 of #337. Lanes moved volume and pan only. Now a lane can
steer a parameter of anything on its track, and a voice already
planned or still waiting for its file follows it too.

- A lane may aim at param:<name>. A parameter has no range of its
  own, so the lane keeps min and max. A parameter lane stays without
  points, so its range is not lost. Remove lane takes it away.
- It reaches the track FX, the FX of an item, the instrument of a
  note with its own FX, and the FX of a clip. When several have the
  name, all of them change, as with Set sound FX parameters.
- The scheduler hands the lane value to a note or clip at its start.
  Values a voice only reads at its start act from the next note.
- While a voice sounds, the knobs of buses and voices of the track
  follow the lane on the audio clock, read every 10 ms, because a
  knob is not always the parameter itself (trackParams.ts).
- A voice waiting for its file takes the marks when it starts.
- When a lane is removed, the parameter goes back to its default.
  An edit while playing does not reset a parameter a lane rules.
- This holds in the song tab, for Play song, in compositions and in
  the bake.
- Song tab: Add parameter lane in the track head menu opens a dialog
  for parameter and range (ParamLaneDialog.ts). The lane head menu
  has Range and Remove lane.
- Inspector: while a lane rules a value, a hint at the track names
  the lanes and says the fields act once the lane is gone.

Known limit: a knob that depends on two automated parameters reckons
each lane with the last value of the other, so it may lag one window.

Tests:
- automation-params.test.ts (9, new), 3 new in sound-library and 3
  in song-lanes. Affected files 407/407.
- 17 counter checks, all compile, all red.
- e2e 65 on melektaus-atx: ALL PASSED, 67 checks. 65h3 is new: a
  clip with FX parameter cut starts at the lane value and its knob
  stays within 0.04 dB of the lane. It goes red when automateBus
  does not pass parameters on.
- Gate on melektaus-atx: CHECK EXIT 0, 330 files, 5080 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 10:30:42 +02:00
adminandClaude Opus 5.5 bd99f9e79d Let automation lanes of a song track be drawn in the song tab (#337)
Stage 2 of #337. The lanes of stage 1 played, but nobody could make
one. Now a track folds out a lane for volume and one for pan, and
points are set, dragged and eased right on the timeline.

- A lane is a row of its own, as high as a track, below its track.
  The view functions stay row based, so zoom, fit, item drags and
  the composition (which shares them) are unchanged.
- itemAt and itemsWithin take a map of track id to row.
- A triangle in the track head folds the lanes out. The head menu
  shows or hides each lane. A lane with points is open by itself.
- Which lanes are open belongs to the view of the tab and is saved
  with it. It is not part of the song and makes no undo step.
- A click sets a point on the grid, Alt or Shift on the exact tick.
  Press and drag sets one and drags it in a single undo step.
- A dragged point stops at its neighbour. Two points on one tick
  make a jump. Double click or Delete removes a point.
- The value shows next to a dragged point and as a tooltip.
- Right click on a point opens "Ease to next point" with the same
  choice as animation keys, curve assets included.
- The lane head shows the value at the playhead. Its menu clears
  or hides the lane.
- While a lane rules a value, the fader shows it in the lane colour
  and cannot be dragged (decision 4). A hint says why.
- Geometry in laneView.ts, edit rules in laneEdit.ts, the easing
  menu in easingMenu.ts. New test hooks automationAt,
  automationHeadAt and automation under __gm.song.

Open: the Volume (dB) field of a track in the inspector can still be
changed while a lane rules it. It has no effect while playing.

Tests:
- song-lanes.test.ts (12, new) and song-edit adapted: 27/27.
  Editor tests of song and composition 62/62.
- 9 counter checks, all compile, all red.
- e2e 65 on melektaus-atx: ALL PASSED, 66 checks. 65g2 is new with
  6 checks. It goes red when a set point is not taken on release.
- Gate on melektaus-atx: CHECK EXIT 0, 329 files, 5065 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:31:48 +02:00
adminandClaude Opus 5.5 389a7c0fd1 Let volume and pan of a song track follow automation lanes (#337)
A song track can now change its volume and pan over time, played on
the audio clock wherever the song is heard. This is stage 1 of #337:
the model and the playback. Lanes cannot be drawn in the song tab yet.

- A track holds automation lanes for volume (dB) and pan. Each point
  has a tick, a value and an easing from the shared list, curve assets
  included. Two points on one tick make a jump. Before the first point
  its value holds, after the last point the last value holds.
- The rules are pure functions in core/music/automation.ts: the value
  at a tick and the pieces of a lane inside one scheduling window.
- The scheduler hands those pieces to the host in every window. The
  host sets them as ramps or jumps on the gain and panner of the
  track's bus. Shapes the clock cannot ramp directly (dB as gain,
  curved easings) are sampled every 10 ms. At the loop end the value
  jumps back with the playhead.
- This works in the song tab, for Play song, inside a composition
  (across the song loop, on the item's own bus) and in a bake.
- While a lane has points the fader no longer sets that value. When
  the lane goes, the fader takes over from the current value.
- Automation enters the bake fingerprint only when there is some, so
  existing bakes stay valid.
- The level helpers moved to core/music/level.ts, so song.ts and
  automation.ts do not import each other.

Known gap: a delaying sound FX on a bus after the track (a pitch
shifter) makes the automation late by that delay.

Tests:
- automation.test.ts (13, new), 3 new in sound-library.test.ts.
  Song, bake and composition tests together 286/286.
- 12 counter-checks, all compile, all red.
- e2e 65 on atx: ALL PASSED, 60 checks. New 65h2: in the song tab the
  bus level follows a volume lane, 0.09 dB off, also with the fader at
  -60 dB. Red when the song tab does not pass the automation on.
- Gate on atx: 328 files, 5053 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 06:45:55 +02:00
adminandClaude Opus 5.5 b5e071ca68 Let the agent read what is heard in the running game (#436)
The agent has no ears. It could start music and sounds, but not tell
whether anything reached the speakers: a song without an instrument, a
bus at volume 0 or a browser that holds the audio clock all looked the
same from outside.

- get_runtime has a section audio. Under levels it gives the peak and
  RMS in dB of the master, each bus of the mix, each pattern, song or
  composition playing and the tracks of a song. Below -90 dB it says
  silent.
- The levels come from a tap of their own after the level and pan of
  each bus, an AnalyserNode per side as for the meters of the Song tab.
  It looks back about 0.7 s, so the gap between two notes is not
  silence. A pause holds the values, which fits play with until.
- Under voices it names what sounds now: a sound by its asset, a note
  of an instrument with its note name, each with its bus and object.
  Next to them planned, playing, waiting (bytes not here yet) and
  failed.
- Under clock it gives the state of the audio clock, with a hint when
  it stands still while the game runs. The state is told as it was
  before a pause, else the pause would hide that case.
- The buses are tapped only while the agent is switched on.
- The note now travels with a voice from the Start of a pattern or song
  to the SoundLibrary, so a voice can be named by it.
- tools.json and the relay's instructions tell the agent to check under
  audio that music and sounds are really heard.

Found on the user's project: started right after F5, before a click in
the page, the browser kept the audio clock suspended for the whole run.
Nothing was heard and the notes stayed planned. That is what clock now
shows.

Tests:
- Unit: sound-library (taps, levels, voices, clock), agent-play
  (audioView). Pattern and song tests expect the note in their timing.
- 9 counter-checks, 8 red. Not dropping a tap with its bus stays green,
  as the bus is cut off and gone anyway.
- e2e 71 (melektaus-atx): 28/28. New 71h4: a sound on the master and
  one on a bus at volume 0, both voices named, the master loud, the bus
  silent, the values held in the pause.
- Gate (melektaus-atx): CHECK EXIT 0, 327 files, 5037 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:14:07 +02:00
adminandClaude Opus 5.5 dd2096e0df Make a bus of the mix at its level, so a quiet bus starts quiet
A bus of the project's mix was made at unity and then glided to its
level. Where the audio context comes with the first sound of a run,
that sound already plays on the new bus. On a bus at volume 0 it was
heard for a moment, loud, while the level glided down.

- setMix now sets the level of a bus it makes at once, without a
  glide. A bus that is there already glides to a new level as before.
- Found while building #436: the e2e check with a sound on a bus at
  volume 0 measured -7.8 dB on that bus. With the fix it is silent.

Tests:
- New unit test in sound-library: buses made at their level with no
  glide, a later change glides. Red without the fix.
- sound-library 130/130, tsc clean on this change alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:13:48 +02:00
adminandClaude Opus 5.5 70402aa19a Let the agent hold the game at a given game time (#435)
feuerwerk / gate (push) Successful in 5m31s
feuerwerk / suite (push) Successful in 19m9s
The agent could not look at a short moment of a running game. Its
calls took longer than the moment lasted, so an intro window from
0.5 to 2 s slipped past twice in the 5d test. Now the editor itself
stops the clock at the time the agent names.

- play takes until: play, resume or pause let the game run on to
  that game time and hold it there. The call answers once the game
  is paused, so get_runtime and screenshot show that moment.
- The frame that would step past the time is cut short
  (frameWithin in src/core/time.ts, used in PlaySession). The game
  stands at 0.5 s, not one frame later.
- play_input takes a last step {"pause": true}. After a wait it
  holds the game at the time of the wait, after a key or click at
  once. Its answer names the game time and whether it is paused.
- A pause that comes first (toolbar or a Break node), a Load scene
  or a Restart game call the until off, and the answer says so.
- When the clock stands still, the call gives up after 25 s with
  the time it is at. A tab in the background is refused at once.
- A recording that does not run in real time is not cut. There the
  game stops in the first frame that reaches the time.
- get_runtime and play_input give times to the millisecond, not to
  the hundredth, so 0.505 no longer reads as 0.51.
- The relay's instructions name both ways.

Tests:
- Unit agent-play (6 new) and time (2 new): 19/19, related editor
  and relay tests 1158/1158.
- 6 counter-checks, all compile, all red. One stayed green at
  first: 0.3 s is exactly 18 frames. The test now takes 0.31 s.
- e2e 71 (atx): ALL PASSED, 27 checks. New 71h3 holds at 0.505 s
  and 0.8 s and after a wait of 0.31 s. Without the cut in
  PlaySession it went red at 0.5166 s.
- Gate (atx): CHECK EXIT 0, 327 files, 5033 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:26:53 +02:00
adminandClaude Opus 5.5 e4ccf5f72f Keep other nodes out of a group the agent makes round new nodes
feuerwerk / gate (push) Successful in 5m18s
feuerwerk / suite (push) Successful in 19m7s
A group made in an agent call is fitted round its nodes only after
the call has arranged them, and the arrangement knew nothing of it.
With On key held, Move and Owner placed fresh, a group round key and
move and one round the owner, the Owner landed beside the key. Move
is taller than the key, so the Owner stood in the first group and the
two groups lay on each other. lint_graph counted none of it. Found
while testing copy_nodes (#434).

- arrange takes the new option blocks: sets of arranged nodes a group
  is fitted round afterwards. Once the columns are laid out, whatever
  would stand in such a group goes below it, a node or the group of
  another set, with everything arranged under it.
- The flow keeps its columns. A first try laid each set out as one
  block instead, but then the link from the Owner ran over the key
  inside the group (throughNodes 1).
- AgentEdit passes every group made in the call whose nodes are all
  new. Groups round nodes already there and the Arrange button stay
  as they were.

Tests:
- agent-edit: new test with the setup that showed the fault. No node
  stands in a group not its own, the groups lie apart, lint_graph has
  0 overlaps, throughNodes, backwards and coveredNotes.
- Counter-check: without the new step exactly that test is red.
- Agent, graph-arrange and graph-follow tests 120/120.
- e2e 71 on atx: ALL PASSED, 26 checks.
- Gate on atx: CHECK EXIT 0, 327 files, 5025 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:53:45 +02:00
adminandClaude Opus 5.5 fb981b5b0f Let the agent copy nodes, also into another graph (#434)
The agent built every part again that already stood in a graph,
node by node, while the editor has Ctrl+C and Ctrl+V for it. The new
command `copy_nodes` in the area graphs copies nodes into the same
graph or another one, in one undo step with the rest of the call.

- The links between the copied nodes come along, and so do groups
  and comments that are only about copied nodes. A node belongs to a
  group by its middle, as when it is dragged. `boxesAbout` in
  annotationFollow.ts decides this.
- `made` names each copy by the id of its original, so later steps
  link to it with `$copy.<id>`. `$copy` alone is the first one.
- Without x and y the copies are arranged at the end of the call like
  new nodes. With them they keep their layout, top left corner there.
- Nodes the target graph does not take are refused by name, not left
  out as Ctrl+V does. The rule is the graph tab's: `fitsGraph` and
  `fittingPart` moved from clipboard.ts to paletteEntries.ts.
- A copied Variables node takes the variables of its behavior along
  into the target behavior (`CarryVariablesCommand`), with default,
  value, Keep and Private. A variable of that name of another kind
  there is refused.
- The relay's instructions name `copy_nodes`.

Found on the way:
- Groups made by the agent can take in a node of another group once
  the nodes are arranged. lint_graph does not count that.
- Undo after a paste leaves an empty `groups: []`, as
  AddAnnotationCommand does. It is the same as no list.

Tests:
- agent-copy (new, 3 tests) with the agent, relay, clipboard and
  palette tests: 118/118.
- Counter-probes: 6 bendings, all compile, all red.
- e2e 71 (atx): ALL PASSED, 26 checks.
- Gate (atx): CHECK EXIT 0, 327 files, 5024 tests.
- Live in the MCP Test Project: 5 nodes of Arrow keys walk into a new
  behavior, 6 links, group, comment and speed (300) along, lint_graph
  0 everywhere, taken back with undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:39:37 +02:00
adminandClaude Opus 5.5 e4b0419a53 Measure e2e 68h against what the sound is held to (#418)
The check failed in CI twice in a row (tasks 740 and 741) with the
sound 61 and 52 ms ahead of the picture, against a fixed limit of
50 ms. Neither commit touched the sound timing. The limit was tighter
than what the composition promises.

- The sound follows the unrounded `player.time` and is started again
  once it lies more than `SLIP` (40 ms) off.
- The check reads the picture from `videoTime`, which counts in whole
  frames of the cut. It can lie up to one frame (1/60 s) behind.
- The first frame at 0.5 s may also come past it (0.5167 s in 740).
  That overshoot is now taken off.
- The limit is SLIP plus one frame plus 5 ms for the audio clock
  moving on between the game's frame and the test's.

Recomputed, the two red runs give 44.5 and 52.1 ms, under the new
limit of 61.7 ms.

Tests:
- e2e 68 (atx): ALL PASSED, 21 checks, 68h at 16 ms.
- Counter-probe: sound planned 100 ms early, 68h red at 116 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:39:14 +02:00
adminandClaude Opus 5.5 24cd9fdeaa Let the agent use stored files, presets and usages (#431 #432 #433)
feuerwerk / gate (push) Successful in 5m16s
feuerwerk / suite (push) Failing after 19m8s
The agent got stuck where the editor has it easy: it could not make a
Sample play without a sound asset, it built instruments node by node
although the Preset menu has them ready, and it could not tell what
names an asset before reworking or deleting it.

- list_files is a new tool. It lists the pictures, fonts, sounds or
  videos the server holds, with size, video state and the asset that
  already wears a file. Bakes and stream copies of songs are left out.
- add_file_asset makes an asset of such a file, as the picker does.
  It reuses the create rules of the import pipeline, so the four
  import kinds are exported now. It refuses a file already in the
  project (naming that asset), a picture without a size, a video not
  yet converted and a file the server does not hold.
- run_commands fetches the file lists its steps need before it runs,
  so the commands themselves stay synchronous.
- apply_preset sets one of the 16 instrument presets or 7 sfxr
  presets, like the Preset menu. Presets take every note, so the
  C4 to B4 range of a new Notes node is no trap there.
- get_asset gains usedBy: every asset naming the asset or one of its
  frames or clips, with the object, node, item or key that does, plus
  start scene, master FX and master routing of the project.
- usesOf in src/core/model/usages.ts walks the project data instead of
  a list of fields, so no new field can be missed. The node every
  asset has in the master routing does not count as a use.
- The relay tells the agent about all three ways.

Found on the way: suite 740 for 2e4c0a0 (#430) was red in e2e 68h
only, a sound clip 61 ms early against 50 allowed. #430 did not touch
sound timing, and e2e 68 on the same tree was green on atx (-4 ms).
Taken as a timing wobble under load until the next suite says more.

Tests:
- agent-files (new, 4 tests) with the other agent and relay tests:
  130/130.
- 8 counter checks, all compiling, all red.
- e2e 71 on atx: 26/26, new 71g9 lists an uploaded sound, makes an
  asset of it and fills an instrument from a preset.
- Gate on atx: CHECK EXIT 0, 326 files, 5021 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:06:15 +02:00
adminandClaude Opus 5.5 2e4c0a09ea Open clips and frames from the asset panel by double click (#430)
feuerwerk / gate (push) Successful in 4m58s
feuerwerk / suite (push) Failing after 19m37s
A double click on a clip row did not open its tab. The first click of
the double click had already selected the clip for the inspector, so
the second click selected the same thing again and the editor saw no
change. Frames of textures and pixel art had no rows at all.

- A double click now asks for the tab with openSelection, even when
  the selection is already right.
- Textures with frames and pixel art with more than one frame get a
  chevron and one row per frame, like sounds and videos. Which rows an
  asset has is decided in one place, src/core/model/assetParts.ts,
  used by the panel and the inspector link.
- A texture frame shows in the inspector on click and opens the
  texture with the frame selected on double click.
- A pixel art frame selects the pixel art on click, so an open tab
  paints on that frame. A double click opens the tab on the frame.
- A frame can be dragged onto a composition track like a video clip.
  The item carries the sprite of the whole image, the frame becomes
  the texture of its object, and it is 3 s long. The agent can do the
  same with add_composition_item.
- A texture label that names a frame now points at the frame row.

Found on the way: the e2e check from #425 only double clicked the
texture label, where the first click is a ping, so the bug never
showed there.

Tests:
- Unit: asset-parts (new, 6), composition-sprites and references
  extended, related tests 303/303, agent tests 21/21
- 5 mutations, all red in the matching test
- e2e 69 (atx): ALL PASSED, 26 checks, 69t new
- Gate (atx): CHECK EXIT 0, 325 files, 5017 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:30:29 +02:00
adminandClaude Opus 5.5 37779a2070 Fold object groups in the timeline, with summary keys (#414)
feuerwerk / gate (push) Successful in 5m21s
feuerwerk / suite (push) Successful in 20m7s
Long animations with many objects fill the timeline with rows. A
group per object can now fold away, and its header row still shows
where the group has keys, so they stay reachable while folded.

- A click on an object header or its triangle folds the group. The
  groups of the objects below it fold along.
- Which groups fold along follows the real parent in the hierarchy,
  not the indent. The root shows at the level of its children, so
  folding it hides only its own rows.
- Every header shows a summary diamond on each frame where a row of
  the group or of a group below has a key. Click selects all those
  keys, drag moves them, Ctrl+drag duplicates, a band catches them
  and Delete removes them, each through the existing commands.
- The fold state is a view of the session per animation. It is no
  undo step and is not saved.
- Selecting a row without keys inside a folded group unfolds it, so
  a fresh row from + Row does not stay hidden.
- The fold rules live in the new timelineFold.ts. keyAt became
  keysAt, which returns every key behind a summary diamond.

Found on the way: the triangle moved the object name to x 22 while
its rows stayed at x 20, left of the name (#408 check in e2e 25).
Rows now sit one step behind the name, as in the hierarchy.

Tests:
- Unit animation + timeline-view 114/114, 6 new
- 6 counter-checks, all red in the matching test
- e2e 25 (atx) ALL PASSED, 41 checks, 4 new (25k)
- Gate (atx) CHECK EXIT 0, 324 files, 5008 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:01:30 +02:00
adminandClaude Opus 5.5 19902a9f02 Keep groups, comments and silent music in view for the agent (#429)
feuerwerk / gate (push) Successful in 4m47s
feuerwerk / suite (push) Successful in 19m5s
A live test of the music areas built a pattern, a song and a
composition that stayed silent: no instrument at first, and later
notes outside the C4 to B4 range of a new Notes node. The agent got
no hint of either. The same test showed that new nodes left groups
behind, that a comment could end up under other nodes, and that an
end node like Output stayed left of the devices put in front of it.

- get_asset lists under silent what of a pattern, song or
  composition is not heard. That is a missing instrument, an
  instrument that sounds no note, or notes outside what its Notes
  nodes take in, in a song after the transpose.
- soundingNotes moves from the pattern tab to
  src/core/instrument/compile.ts. The tab and the agent use the
  same rule.
- When new nodes go between two that have no room, everything from
  the fed node on moves right together. This holds for Arrange with
  a selection too, so no link runs backwards.
- A new node linked to a node in a group joins that group, and the
  group grows in the same undo step. A group that would take in a
  foreign node stays as it is.
- Arrange takes groups and comments along: a group whose nodes all
  moved the same keeps its size, otherwise it is fitted anew. The
  rules live in the new src/editor/graph/annotationFollow.ts.
- add_comment and fit_group put a comment below its nodes when a
  node or a group title is in the way above. lint_graph counts
  covered comments as coveredNotes.
- add_notes, get_asset, lint_graph and the relay instructions name
  the default range and how to mend a covered comment.

Tests:
- graph-follow (new, 12), agent-edit, agent-music (11) and the
  other agent, graph, pattern and relay tests green
- 10 deliberate breakages, each one red in its own test
- e2e 71: 25/25
- Gate: CHECK EXIT 0, 324 files, 5002 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:27:57 +02:00
adminandClaude Opus 5.5 10f5e3ba8f Let the agent write music and build compositions (#429)
feuerwerk / gate (push) Successful in 5m17s
feuerwerk / suite (push) Successful in 18m54s
The agent could already draw, animate and wire graphs, but songs,
patterns, sound clips and compositions were out of its reach. This
last step of #429 adds the two missing catalog areas, so the agent
can now use 168 of the editor's commands.

- Area music (15 commands): notes, pattern and song loops, song
  tracks and items, sends, and clips of sounds and videos.
- Area composition (9 commands): tracks, items for objects, images
  and audio, changes with fades, volume and bus, removal, and
  animation clips.
- A pitch is a MIDI number or a name like C4 or F#3. A note past the
  end of its pattern is refused.
- Song and master routing stay graphs. The agent links them with
  connect, and set_send sets level and tap. get_graph now shows the
  send of every link.
- Lengths follow the same rules as dragging in the tab. A sound has
  a length only after the editor loaded it. get_asset reports it as
  seconds, and without it the agent has to give a length.
- Moving a composition item takes its clips along, as dragging does.
  A clip that would then overlap another one is refused. Fades are
  only allowed on items with sound.
- get_asset reads notes back with their names and gives the length
  of a pattern in ticks.

Found while testing live: two song tracks in the same column made
lint_graph report a backwards link. arrange_graph cleared it, as the
agent instructions ask.

Tests:
- agent-music (new, 9 tests) and the other agent and relay tests:
  109/109.
- 9 mutations of the new code, each one red in its own test.
- e2e 71 on melektaus-atx: 25/25. New check: notes are read back by
  name, and a composition with a pattern and a sprite opens in its
  tab.
- Gate on melektaus-atx: CHECK EXIT 0, 323 files, 4987 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:32:44 +02:00
adminandClaude Opus 5.5 0684e76dc5 Let the agent draw pixel art and vector art (#429)
feuerwerk / gate (push) Successful in 5m46s
feuerwerk / suite (push) Successful in 19m14s
Stage 5c of the MCP work. The agent could place sprites and edit
graphs, but it could not draw: pixel art, palettes and vector art had
no commands in its catalog. Two new areas bring 29 commands, so the
catalog now holds 144.

- pixelart (13): paint, layers (add, copy, remove, change, move,
  merge down), frames (add, also as a copy, remove, move), canvas size
  with an anchor, palette colors set and removed.
- paint_pixels edits one layer on one frame. Its steps run in order:
  rows (a small picture as characters, a space keeps the pixel),
  pixels, line, rect, ellipse, fill, clear. An index the palette does
  not have, a pixel off the canvas and an unknown character are
  refused.
- vectorart (16): shapes from rings or as rect and ellipse, outline,
  style (fill, opacity, gradient or image fill, stroke), move, scale,
  rotate and flip, copy, remove, combine, rename, order, move into
  groups, group, layers.
- get_asset gives the palette of pixel art as colors. Up to 16384
  pixels over all layers and frames each layer comes as rows, above
  that only the number of painted pixels. Palette and vector colors
  come as #rrggbb.
- Merge down, canvas size and removing a color are now builders in
  pixelActions.ts. The panel and the agent share the same code.
- Color and point helpers moved into catalog/shared.ts, and graphs.ts
  uses them too.

Found while testing:

- The ellipse tests only checked the four extreme points, which stay
  right even with wrong handle lengths. The test now checks that every
  point of the drawn curve lies on the ellipse.

Tests:

- agent-drawing (new, 9) and the other agent, pixel and relay tests:
  133/133.
- 7 mutations, all red in the matching test.
- e2e 71 on melektaus-atx: 24/24. New check: the agent's pixel art
  opens in its tab, reads back as rows, and its cel is on the server
  after saving (status 200).
- Live in a test project: an 8x8 sprite and a copied second frame
  read back as painted, a rotated square and a radial gradient disc
  with their corners where computed.
- Gate on melektaus-atx: CHECK EXIT 0, 322 files, 4978 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:59:08 +02:00
adminandClaude Opus 5.5 d01e2b832e Let the agent edit animations, frames, tiles and tilemaps (#429)
feuerwerk / gate (push) Successful in 5m14s
feuerwerk / suite (push) Successful in 18m43s
After stage 5a the agent could build behaviors and scenes, but not
animate an object, cut a texture into frames or paint a tilemap.
Stage 5b adds three areas with 30 commands, 115 in all.

- animation (12): set the root, add, retarget and make rows relative,
  set, move, copy and delete keys, easing, insert or remove frames,
  and curve points.
- textures (8): add, change and delete frames, slice a grid, nine
  slice, sorting line and collision outline.
- tiles (10): the images of a tile, painting tilemaps, layers (add,
  copy, move, change) and the border of a placed tilemap.
- get_asset names the targets of an animation with their animatable
  properties. A tilemap up to 400 cells shows its cells, a bigger one
  only their count.
- After such a call the editor opens the tab of the animation, curve,
  texture, tile or tilemap. A frame edit selects the frame there.
- One paint step takes rectangles and single cells. Rectangles go
  first, so a cell can cut a hole into a rectangle. Cells outside the
  grid, ids that are no tile and more than 20 000 cells are refused.
  The grid is the tilemap's largest, a grown placement included.

Left out on purpose: Generate collision, because it reads the pixels
of the texture, and pasting keys, which goes through the clipboard.

Found on the way:
- Nine slice without fill now keeps the fill the image has. The
  model's default is repeat, not stretch as first described.
- Painting ran the other way round, rectangles overwrote cells.

Tests:
- agent-art (new, 11) and the other agent tests green, 64/64.
  5 bends of the code, all red.
- e2e 71 on atx: 23/23, the new check opens the agent's animation in
  its tab and reads its targets.
- Gate on atx: 321 files, 4969 tests.
- Live on a test project: a Spin animation of 360 degrees in 30
  frames matched 12 degrees per frame to 0.01 frames in the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:24:27 +02:00
adminandClaude Opus 5.5 0d8fd6ff52 Let the agent use the graph, scene and project commands (#429)
feuerwerk / gate (push) Successful in 5m10s
feuerwerk / suite (push) Failing after 18m49s
The agent knew 23 of the editor's commands. It could not point a
Variables node at a variable, group nodes, edit ports, transitions,
containers, colliders or the project settings. Stage 5a brings the
catalog to 85 commands in five areas and keeps every answer short.

- The catalog lists names and descriptions only. With a query (an
  area or command names) it adds the arguments. All schemas at once
  would pass the 60 000 character limit of one answer.
- add_port points a Variables node at a variable. The port takes the
  variable's kind, and an unknown name is refused with the list of
  the ones that exist.
- add_group and add_comment take nodes instead of coordinates. They
  follow the nodes after the arrange step, in the same undo step.
- The rule for which list a port picks from moved into portChoices.ts.
  Inspector and agent now share it.
- The frame of a group around nodes moved into annotationDrag.ts, so
  the button and the agent get the same frame.
- list_assets returns the project settings. get_asset shows the
  content of placed containers with the ids overrides need. get_graph
  shows declared ports and frame colors.
- A new node is placed at the height of its port, in the free spot
  whose links cross the fewest others. A source goes left of the node
  it feeds. Before, new nodes always went right of the rest.
- Arrange breaks a tie in crossings by putting the flow on top, and
  exec links pull harder when a column is sorted.
- A comment over a group keeps clear of the group's title bar.
- The agent's instructions and lint_graph ask to look at crossings
  too and to call arrange_graph when there are more than needed.

Left out on purpose: file assets (no upload), paste from the
clipboard, internal wrapper commands and switching plugins on.

Found on the way:
- Variable commands on a Struct asset did not run through
  WithStructTypes. New fields did not reach variables of that type.
- A query "variables" also matched set_variables_scope. A word that
  names an area now means only that area.

Tests:
- agent-catalog (new, 10), agent-edit, graph-arrange (3 new) and
  relay green. 11 bends of the code, all red.
- e2e 71 on atx: 22/22, the new check sees port speed and a group
  around all 4 drawn nodes.
- Live on a test project: a speed variable of 300 moved the player
  to x 150.03 in 0.5 s. A re-placed Variables node landed below Axis
  input with 4 crossings instead of 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:01:15 +02:00
adminandClaude Opus 5.5 1943539a7a Let the agent play the game and read its state through MCP (#429)
feuerwerk / gate (push) Successful in 5m7s
feuerwerk / suite (push) Successful in 18m44s
An agent could build a game object but not see whether it works. It
had to ask the user to press the keys. Now it can play the game in the
editor itself, send keys and mouse input and read what happened.

- play_input sends steps into the running game: hold or tap a key,
  keep it down or let it go, click, press, release or move the mouse,
  turn the wheel, wait. Holding and waiting count game seconds.
- A mouse target is a point in world units or an object by id or
  name. A click on an object reports whether it landed on it.
- The answer names the game time used, the keys still held, the mouse
  and the new console lines.
- One tool instead of press_keys and pointer from the plan. A step
  list can then mix both, like holding Shift during a click.
- get_runtime reads the running game: objects with id, type and
  position, the own variables of their behaviors, the Variables
  assets, the camera, keys, mouse and the console. Named objects come
  with all their properties.
- A hidden editor tab gets no frames. play_input says so at once. If
  game time stands still for 25 s it stops, names the step and lets
  go of every key it held.
- get_view and play say when the scene has no active camera. The
  first agent test missed that and only saw it on a screenshot.
- The relay instructions tell the agent to test what it built.

Found in the live test on a real project:

- A click on an object by its name reported hit false. The hit test
  looked the target up by id only.
- get_runtime listed the built-in switches of a behavior (enabled,
  runInEditor) as its variables.

Tests:
- agent-play (new), agent-edit, agent-view, relay green.
- 5 counter-checks red: hold counts one frame, key never released,
  click ignores the object, hidden tab not refused, no variable
  filter.
- e2e 71: 21/21. The agent holds ArrowLeft 0.5 s (x -120 to -270)
  and clicks the object (hit true).
- Gate: CHECK EXIT 0, 319 files, 4944 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:01:32 +02:00
adminandClaude Opus 5.5 f296f427ef Measure the stream's pause before the forced garbage collection
feuerwerk / gate (push) Successful in 4m38s
feuerwerk / suite (push) Successful in 18m39s
e2e 34 "the pause holds the stream" was red in CI tasks 712, 719 and
721. The pause was not at fault. Right before it the step forces a
full garbage collection for its memory check. On the CI runner that
held the page for seconds, and the player ran dry before the pause was
even clicked: 0.02 s and 0.05 s were left ahead in 719 and 721, against
about 2 s in the green CI runs and 4 s on melektaus-atx.

- The worklet counts a late sample only once it arrives and reports
  every 4800 samples read. So the underrun from the collection showed
  up after the resume and looked like samples the pause had lost.
- The pause is now measured first, on a stream held at least a second
  ahead, with half a second for the worklet to report. The collection
  and the renderer's size follow while the game still plays.
- The check judges what the pause adds: as many skipped samples after
  the resume as before the pause.

Tests:
- e2e 34 on melektaus-atx with a 5 s busy loop on the page where the
  collection was: 12375 samples skipped before the pause, the same
  after it. The old condition, none skipped, fails on these values.
  The new one passes.
  ALL PASSED, 148 checks
- Lint and Prettier on the module are clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 13:13:00 +02:00
adminandClaude Opus 5.5 97e0d93168 Let the editor follow the agent's edits on screen (#429)
feuerwerk / gate (push) Successful in 4m45s
feuerwerk / suite (push) Failing after 18m0s
The user asked to see what the agent is working on through MCP. Until
now the agent had to call open for that, and mostly did not.

- After run_commands the editor shows what the call worked on last. A
  graph opens its tab with the touched nodes selected, deleted ones
  left out. A scene or container shows with its objects selected. Any
  other asset shows in the inspector.
- Objects in another scene switch the scene inside the same undo step.
  A switch of its own would be its own step, and Ctrl+Z would take back
  the switch before the agent's edit.
- A graph tab that is open already moves its camera after the nodes.
  It stays put while they are in view, moves only as far as needed with
  the zoom kept, and frames them like Fit only when they do not fit.
  This is revealBoxes in framing.ts and the state's reveal event.
- A tab that has just opened frames itself first and moves after that.
- The account menu has "Follow agent" below "Agent", on by default and
  kept in the browser. While the game plays the editor never follows.
- The run_commands description and the relay's instructions tell the
  agent about it.

Tests:
- agent-edit.test.ts: 3 new tests, each red with its bent-back fix
  (scene switch outside the undo step, deleted nodes selected, switch
  ignored)
- graph-framing.test.ts 29/29, 6 of them new
- e2e 71 on melektaus-atx: ALL PASSED, 20 checks. 71g4 adds a node
  3000 px to the right of an open tab: it ends 24 px inside the edge,
  zoom still 1
- Tried live on a test project through a real MCP session: the tab
  opened, the nodes and the object were selected. With the old build
  a far node stayed out of view, after the reload the camera went there
- Gate on melektaus-atx: CHECK EXIT 0, 318 files, 4931 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:48:35 +02:00
adminandClaude Opus 5.5 732989a452 Keep Fit inside the song tab's header again (#429)
Suite 719 for 0dd671e was red in e2e 65. The Arrange button that came
with b9d1602 sits in every graph tab's header, the song's too. There it
pushed Bake and Fit past the right edge of the tab. The Fit click hit
nothing, the last track stayed below the edge, and its head menu with
"Add track below" never opened.

- The song tab shows Arrange with its routing only. On the timeline it
  was greyed out and of no use anyway.
- SnapToggle.setArrangeShown hides the button and lays its header out
  again, so the timeline's header is as wide as before #429.
- The magnet stays where it was, greyed out on the timeline, as e2e 65
  expects.

The other red check in suite 719, e2e 34 "the pause holds the stream",
is the wobble left open in 5b9d084 (task 712). Its message now shows
the stream behind before the pause already (skipped 12950), so the
pause did not lose its place. On melektaus-atx it passed.

Tests:
- snap-toggle.test.ts 6/6, the new test is red without the relayout
- e2e 65 on melektaus-atx: red before (same error as in CI), now
  ALL PASSED, 59 checks
- Gate on melektaus-atx: CHECK EXIT 0, 318 files, 4931 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:48:15 +02:00
adminandClaude Opus 5.5 0dd671ea26 Count each connected agent once in the editor badge (#429)
feuerwerk / gate (push) Successful in 5m17s
feuerwerk / suite (push) Failing after 14m54s
The Agent badge showed 2 or 3 while one Claude Code session was
connected. The relay log against the open TCP connections in the
container showed two causes.

- A client may drop its event stream while the relay still asks the
  API about its token. The close was over before the relay listened
  for it, so the stream stayed in the list and counted for good. The
  relay now listens before the token check and keeps no stream that
  is already gone. It listens on the response, not the request.
- Claude Code opens three sessions whenever it connects and keeps a
  stream on one of them. It never ends the others with a DELETE. Any
  session counted for two minutes after its last request, so the
  badge stood at 3 after each start. Now a session counts while its
  stream is open or it called a tool in the last two minutes.
- The relay logs session start, stream open and close and each change
  of the count, with session ids cut to 8 characters.

One case is left: after a relay restart Claude Code sometimes repeats
its first call on a spare session. The badge then shows 2 for two
minutes and drops to 1 on its own.

Tests:
- relay.test.ts: 27/27, three new. A dropped stream counts out, a
  stream dropped during the token check never counts, and spare
  sessions without a tool call do not count.
- Counter-checks: without the early close listener the drop test
  turns red, with the old count two tests turn red.
- Live: the badge showed 2 with one open SSE connection before. After
  the fix it shows "Agent · 1" and stays there after a normal call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:10:02 +02:00
adminandClaude Opus 5.5 b9d1602b94 Arrange graph nodes for the agent and with a button (#429)
An agent that built a graph through MCP left every new node in one
row. In the first real test that put nine nodes on a line 2080 px
long, with the Owner's links running straight through all the others.
Placing nodes by hand cost nine move_node calls and guessed heights.
The agent now gives no coordinates, and the editor lays the nodes out.

- arrange.ts is a pure function. It puts the flow into columns from
  left to right and sets an Owner or Get right before the first node
  it feeds. It orders each column for the fewest crossings and lines
  nodes up by their linked ports without overlaps.
- Node sizes come from the drawn NodeView in the browser and from an
  estimate in unit tests. Both share the constants in nodeBox.ts.
- run_commands arranges the nodes that came without x and y when the
  call ends, inside the same undo step. Nodes that were there stay.
- New command arrange_graph and new tool lint_graph. The lint counts
  overlaps, crossings, links through foreign nodes, backward links
  and the longest links, with example ids.
- The relay instructions tell the agent not to give coordinates and
  to fetch far values with a second Get or Owner node.
- An Arrange button next to the snap magnet in all eight graph tabs.
  It arranges the selected nodes, or all of them, as one undo step.

Found while testing on a real project:

- lint_graph missed a crossing where two links swap rows exactly
  mirrored. Both curves then share a sample point in the middle, and
  only a crossing between two sample points was counted. Links that
  meet on a point count now (segmentsMeet).
- The Arrow-key graph with one Owner for four Moves cannot get below
  8 crossings. The Owner feeds object in row 2 of Move, between exec
  and dt of the key, so it always crosses one of them.

Tests:
- graph-arrange, agent-edit, snap-toggle: 33/33. The swapped-rows
  test was red before the fix (0 instead of 1).
- Four counter-checks that bend arrange or the edit plan, all red.
- e2e 71 on melektaus-atx: 18/18. Lint after the build shows 0
  overlaps, 0 links through nodes, 0 backward. Arrange takes a
  tangled graph from 3 backward links and 2 crossings to 0, and one
  Ctrl+Z restores it.
- Gate on melektaus-atx: CHECK EXIT 0, 318 files, 4917 tests,
  measured before the lint fix.
- Live through MCP on the user's project: lint_graph reports 8
  crossings after Arrange, matching the screenshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:09:52 +02:00
adminandClaude Opus 5.5 25af064249 Let an AI agent read and edit the open project through MCP (#429)
feuerwerk / gate (push) Successful in 4m40s
feuerwerk / suite (push) Successful in 18m43s
Stage 2 of #429. Until now an agent could only look at the editor.
Now it reads the project and changes it through the same commands
the user's own edits run, so every change shows up live and one
Ctrl+Z takes back a whole agent call.

- Read tools: list_assets, get_asset, get_graph and catalog. The
  catalog lists the commands with their arguments, the nodes of a
  graph from the same palette the user sees, asset types and
  variable kinds.
- run_commands runs its steps as one undo step named "Agent: ...".
  A failing step rolls back the steps before it. The answer names
  the step and the reason the editor gave.
- A step can name what it made ("as") and later steps use "$name".
  Each step is built after the one before has run, so one call can
  make a behavior and fill it.
- 23 commands cover assets, variables, scene objects and graphs.
  Textures, fonts, sounds and videos need a file and are left out.
- open, select, play and undo/redo drive the editor.
- Deleting asks the user with the same text as their own delete.
  The tab sends "wait", and the relay then gives the call 2 minutes
  instead of 30 seconds.
- tools.json is the only source of the argument schemas. argCheck
  checks every call against them before anything runs.
- An open menu now names its first entries in get_view, instead of
  an empty title.
- The new host functions live in editorToolHost.ts, so Editor.ts
  does not grow further.

Found while testing: e2e 71 hung for 30 s in Page.navigate after a
failure. The beforeunload guard of an unsaved project held the page
and hid the real error. The module now reports the error first and
waits for the save before it navigates.

Tests:
- Gate on melektaus-atx: CHECK EXIT 0, 317 files, 4903 tests.
- agent-edit 14/14, agent-view 10/10, relay 24/24.
- e2e 71 16/16 on atx and against the live site. One call builds a
  sprite with an arrow-key behavior, play moves it from x 0 to -125,
  and one undo restores the assets exactly.
- Counter-checks turned red: no rollback, a partial undo, no delete
  question, a relay that ignores "wait".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:08:39 +02:00
adminandClaude Opus 5.5 48ec722660 Let an AI agent see the open editor through MCP (#429)
feuerwerk / gate (push) Successful in 5m41s
feuerwerk / suite (push) Successful in 18m44s
Claude Code should be able to work inside the user's open editor tab.
This first stage connects the two and lets the agent look: what the
user sees and a picture of it. Changing the project comes in stage 2.

- An agent token per account opens only /mcp, no API route. The
  dialog "Agent tokens..." in the account menu creates it once, shows
  the ready "claude mcp add" line and revokes it. The server keeps
  only its sha256 (api/lib/AgentTokens.php).
- Wrong tokens count into the sign-in brake from #427. Changing the
  password, renaming or deleting an account ends its tokens.
- The relay (tools/mcp/) is a small Node service without packages,
  WebSocket server included. It runs in the container feuerwerk-mcp
  because PHP-FPM cannot hold connections. Whether a token or a tab
  ticket is valid is still decided by the PHP API, through two routes
  that answer only with the relay key.
- The tab joins over WebSocket on the same origin with the normal
  session, an Origin check and a one-time ticket. It connects only
  after "Agent" is switched on in the account menu. One tab per
  account is active, a new one takes over. Guests get nothing.
- Tools: get_view (project, tab, document, selection with names,
  camera, play state, open dialog) and screenshot. The resource
  feuerwerk://view can be subscribed. Claude Code does not act on
  that reliably, so a changed view is also noted on the next answer.
- A badge next to the account menu shows the connected agents. It
  counts only agents with an open stream or a request in the last
  two minutes.
- CI starts the relay in front of php -S (tools/ci/serve.sh), and
  e2e 71 runs the whole path in the browser.

Found on the live test: the badge showed four agents because old
sessions of revoked tokens were still counted.

Tests:
- Gate on atx: 316 files, 4888 tests
- relay protocol test with fake API and fake tabs: 23/23
- get_view, tools, socketUrl: 16/16
- PHP agents 29, rights 103 checks
- e2e 71 on atx 10/10, against the live site 10/10, no CSP violation
- real Claude Code against the live site: get_view answered
- ZAP against /mcp: nothing at High or Medium
- every new check fails when the guard it covers is taken out (6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:34 +02:00
adminandClaude Opus 5.5 edef2b6323 Serve the editor from feuerwerk.nockal.com as well
feuerwerk / gate (push) Successful in 4m45s
feuerwerk / suite (push) Successful in 19m52s
The subdomain proxies to the same folder, but the build had the path
/feuerwerk/ baked in. On feuerwerk.nockal.com the page loaded its
assets, the API and the data URLs from /feuerwerk/... and got 404. The
session cookie had Path=/feuerwerk/, so the browser would never have
sent it to /api/ there either.

- site.ts takes the base path from the folder of the open page:
  /feuerwerk/ on nockal.com, / on the subdomain. Release links point at
  the address you are working on.
- vite.config.ts builds with a relative base. The copy plugin rewrites
  the pages to ./dist/assets/ and stops the build if it finds nothing
  to rewrite. The dev server now runs at http://localhost:5175/.
- Auth.php sets the cookie without a Path, so it belongs to the API
  folder the browser sees. Sign-in and sign-out also clear the old
  /feuerwerk/ cookie, otherwise it would keep signing in after a
  logout. Existing sessions stay valid.
- README: the new dev server address.

Tests:
- curl on both addresses: pages, JS, CSS and API 200
- sign-in, session, sign-out on both addresses
- e2e 70 against both addresses: 13/13 each, no CSP violation
- tsc, lint, prettier clean, site and releases tests 14/14
- PHP tests 7 of 8 files green, videos.test.php fails only because
  the web-php-fpm container has no ffmpeg

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:40:18 +02:00
adminandClaude Opus 5.5 5b9d084dfe Keep a clip that starts later playing at its first frame
The e2e 68 check "a clip that starts later" was red in 3 of the last
8 suites (CI tasks 697, 703, 712). Two causes sat on top of each other.

- The check measured too late. It started sampling after play(), and
  under CI load the clock was already at 0.05 to 0.22 s. It now starts
  before the click, like 68d does.
- Measured early, a real bug showed: the video held for one frame at
  its first picture. The first picture triggers a redraw, and the game
  takes its first step in the same frame. readSpeed got both calls with
  the same frame time and divided by 1 ms. The speed came out at 16.7x,
  which counted as a jump, so the player paused.
- readSpeed now measures a step without elapsed wall time against one
  frame at 60 fps. A real jump still counts as a jump.
- e2e 34 "the pause holds the stream" also failed in task 712. Its
  cause is still open. The failure message now names the state before
  the pause, so the next red run shows where the samples got lost.

Tests:
- video-follow.test.ts 18/18, the new test is red without the fix
- e2e 68 on melektaus-atx: ALL PASSED, 21 checks
- Gate on melektaus-atx: CHECK EXIT 0, 314 files, 4853 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:40:11 +02:00
adminandClaude Opus 5.5 0d7672a5af Play the guest's changed demo in e2e 70 (#428)
feuerwerk / gate (push) Successful in 4m34s
feuerwerk / suite (push) Failing after 18m31s
The test plan of #428 asks a guest to change the demo and play it,
with no request that writes reaching the server. e2e 70 only changed
it.

- 70f now plays the changed demo and stops it before it counts the
  requests.

Tests: e2e 70 (atx) ALL PASSED, 13 checks, mode "play", no writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 07:19:31 +02:00
adminandClaude Opus 5.5 925a39a276 Let guests try a demo project without an account (#428)
feuerwerk / gate (push) Successful in 4m51s
feuerwerk / suite (push) Successful in 19m23s
The editor was only open to somebody with an account. A guest now picks
a demo project on the sign-in screen and gets the whole editor on a copy
held in the tab. The server writes nothing for a guest, so there is no
guest session, no guest folder and no quota to keep.

- Demo copies are frozen copies like a release, in data/demos/. An
  administrator makes, updates and removes one with "Demo copy..." in
  the project's main menu. GET demos and GET demos/{token} need no
  session. An empty project is just a demo that is empty.
- Demo material: an administrator offers an uploaded file with "Offer
  as demo" in the pickers (PATCH {demo}, data/demo-files.json). Every
  list carries it with demo: true, for accounts and guests alike.
  Signed out, a list holds the demo material alone, without the owner.
- An offered file is only an administrator's to delete. A file a demo
  copy names counts as used, like one a release names.
- The guest's project lives in MemoryProjectStore. Every store that
  would write refuses in the tab before a request is made, and the
  autosave is not attached at all.
- The main menu drops projects, snapshots and release for a guest. The
  pickers drop Upload and Delete, a dropped or pasted file is refused,
  and a recording is saved as a download.
- The toolbar says the work is not saved. The Account menu holds
  "Sign in..." instead of Logout and the password.
- A guest's demo never goes into the address: a reload brings the
  sign-in screen back, not the copy. Found by e2e 70.
- GET plugins needs no session any more, since a demo may switch one
  on. It only reads the server's own modules.

Tests:
- e2e 70 (atx): ALL PASSED, 13 checks. Counter-test with the guest
  autosave sent to the server: 70f and 70g red on the PUT.
- tests/core/guest.test.ts: 9 tests. PHP: demos 20, files 46,
  rights 80 checks (the matrix asks every new route).
- Gate (atx): CHECK EXIT 0, 314 files, 4852 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:58:30 +02:00
adminandClaude Opus 5.5 ce1f25f2eb Prune old sign-in failures by their current time (#427)
feuerwerk / gate (push) Successful in 5m12s
feuerwerk / suite (push) Canceled after 4m52s
The CI gate of cfd6d7a was red in limits.test.php: "pruning drops an
address whose failures are all old". PHP 8.2 in the CI image still
answered the modification time it had cached before the test moved it
back, PHP 8.5 on stein2 did not.

- LoginThrottle::prune() clears the stat cache before it reads times.

Tests: limits.test.php in feuerwerk-ci (PHP 8.2.34) 21/21, red before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:12:10 +02:00
adminandClaude Opus 5.5 cfd6d7a8df Lock the server down before the editor goes public (#427)
feuerwerk / gate (push) Failing after 5m1s
feuerwerk / suite (push) Canceled after 4m52s
The editor is about to be opened to strangers: a post on Reddit and a
guest access (#428). nginx served the whole workspace and ran every PHP
file in it, the API let anybody guess passwords as fast as they liked,
and one account could fill the disk.

- nginx (shared web stack, outside this repo, backup in
  /root/config-backup-20261015/) hands out only the two pages, dist/,
  public/brand/, the uploads by kind and ending, and api/index.php.
  Everything else under /feuerwerk/ is a 404.
- PHP behind it has display_errors off and no X-Powered-By. Plain HTTP
  forwards to HTTPS, the API refuses it.
- Headers: a Content-Security-Policy without inline code or eval,
  nosniff, frame-ancestors, HSTS, Referrer-, Permissions-, COOP, CORP.
- vite.config.ts writes the same policy into the built pages, so CI
  runs the whole suite under it. Pixi gets by without eval through
  pixi.js/unsafe-eval. The page CSS left its inline style for a file.
- The harness fails a module for anything the policy refuses, from
  Chrome's log and from the page's securitypolicyviolation event.
- LoginThrottle: five wrong passwords from one address within 15
  minutes, and it gets 429 until 15 minutes after the last of them.
  The address is the last X-Forwarded-For entry, the Brandmauer's own.
- Quota: 1 GB per account for uploads, projects and snapshots. Only a
  write that grows the account is refused (413). Admins have no limit.
- A write whose Sec-Fetch-Site says another site is refused (403).
- The session cookie carries Secure, nginx passes HTTPS on.
- showFailure puts the message in as text, not as HTML.
- Every PHP script outside api/ stops unless it runs on the command
  line.
- The video worker sees the checkout read-only and can only write
  data/videos. No capabilities, 2 GB and 256 processes at most.
- tests/php/rights.test.php runs the API under php -S on a scratch
  directory and asks every route as signed out, owner, stranger and
  admin. A route named in api/index.php but missing in the matrix
  fails it. limits.test.php covers the brake and the quota.

Findings:

- tmp/ was readable from outside, with 79 copies of project files.
  Closed on 15.10. at 20:02 with chmod 750. The log back to 11.10.
  shows only our own requests.
- Port 8080 is bound on 0.0.0.0, so the LAN could reach nginx past the
  Brandmauer. Left as it is: the web stack is not this project's.
- npm audit finds nothing in what is shipped. Dev tools (vitest,
  brace-expansion, source-map-js) have advisories, not touched here.

Tests: check 313 files / 4843 tests. PHP limits 21, rights 60,
accounts 56, snapshots 15, releases 20, files 46 (videos 50/51, no
ffmpeg in the web container). e2e on stein2, atx was off: 34+38+41
197/198, the one red check (a click in 41) passes with 41 alone,
27/27. 67 misses frames at 1080p on stein2 with and without this.
Path list from outside: 10 served, 26 refused. ZAP baseline against
the live site: nothing High or Medium under /feuerwerk/.
Counter-checks: rights.test.php without the cross-site guard, the
quota, the brake, the admin check of users, or with an undocumented
route fails each time. A policy without wasm-unsafe-eval fails e2e 41
with the harness's own CSP line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:05:32 +02:00
adminandClaude Opus 5.5 a9e0b6d9fa Show the README without Gitea's Unicode warnings (#426)
feuerwerk / gate (push) Successful in 4m30s
feuerwerk / suite (push) Failing after 19m58s
The repo's start page marked eight characters in the README and
offered an Escape button. All eight were en dashes (U+2013), which
Gitea flags as ambiguous because they look like a hyphen. The wiki
dropped them in #416, the README now follows the same rule.

- Prose: the dash became a colon, a comma or a full stop.
- Comments in the code blocks: a semicolon.

Tests:
- grep over README.md for en dash, em dash, times, minus and
  single guillemet finds nothing.
- prettier --check README.md is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:35:13 +02:00
adminandClaude Opus 5.5 2f13f9a0b9 Keep a recording behind another tab from dropping its frames
feuerwerk / gate (push) Successful in 4m52s
feuerwerk / suite (push) Successful in 19m5s
In CI run 699 check 68k (real time behind a tab) got 21 of 120 frames.
The beat ticked 100 times, but 83 frames were dropped as busy. Late in
a long browser session the GPU behind a hidden tab took up to 1.5 s to
finish a read back, so all three read-back buffers were still on their
way when the next frame was due. Locally on melektaus-atx this showed
only in the full run of modules 01 to 68, and not in every one.

- The render loop no longer draws the screen while the page is hidden.
  Nobody sees it there, and the drawing was competing with the read
  backs for the slow GPU.
- A recording of the editor copies the canvas, so it turns the drawing
  back on with `drawWhileHidden` while it runs.
- `PictureReader.collect` reads the oldest buffer at once when the page
  is hidden and no buffer is free, instead of dropping the frame. The
  main thread waits for the GPU there, which nobody sees either.
- More buffers are no fix: with 30 of them the frames piled up at the
  encoder instead (45 frames, 56 dropped).

Tests:
- render.test.ts: 18/18, the new test is red without the hidden check
- Full run of modules 01 to 68 on melektaus-atx with a counting probe:
  read at once only 18 frames, 63 dropped; both parts 106 frames,
  2 dropped
- e2e 67 and 68 on melektaus-atx without probe: 68k green, 97 frames,
  0 dropped. Red there only "a clip that starts later", the known
  timing wobble from task 697
- Gate on melektaus-atx: CHECK EXIT 0, 313 files, 4841 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:02:08 +02:00
adminandClaude Opus 5.5 406839baea Drag clips of videos and sounds straight onto a track (#425)
A clip of a video could not be laid on a composition track at all, and
neither could a clip of a sound, although #418 and the wiki said so.
The only place a drag starts is the asset panel, and it listed assets,
not clips. The texture label of an item playing a video clip was dead
too, because the reference lookup did not know video clips.

- The asset panel gives a sound or video with clips a chevron of its
  own. It unfolds one row per clip and starts closed.
- A clip row drags like an asset. One click shows the clip in the
  inspector without leaving the tab (`EditorState.glance`). A double
  click opens its sound or video with the clip picked.
- A video clip dropped on a composition track becomes an item as long
  as the clip. It wears the sprite of its whole video, so a composition
  keeps one sprite per video, and the clip is a texture override on
  the object. Sprite, item and override are one undo step
  (`droppedPicture`).
- A sound clip becomes an audio item as long as the clip. The song tab
  takes sound clips on the same way, through `findSound`.
- `referencedTarget` lets a reference label link to the clip itself.
  One click pings the clip row, a double click opens the asset with the
  clip picked. The Plays row of an audio item reads "Sound / Clip".
- `clipOwner` in `sound.ts` finds the sound or video a clip id belongs
  to. `activateAssetOrClip` uses it for the double clicks.

Found on the way: the Blip of the e2e seed is only 0.25 s long, so a
clip of 0.2 to 0.7 s on it is cut to one frame. 69s takes the Drone
(0.75 s) instead.

Tests:
- tsc, eslint, Prettier clean
- Unit on stein2 (composition, references, sound, video, editor):
  94 files, 1238 tests green
- e2e 69 on melektaus-atx: ALL PASSED, 25 checks, 2 of them new (69s)
- Gate on melektaus-atx with the fix for 68k on top: CHECK EXIT 0,
  313 files, 4841 tests
- Counter checks, all red: no clip rows in e2e 69 (no drag starts),
  no clip override in the unit test, the link aimed at the asset
  instead of the clip
- Not covered by its own e2e check: sound clips on the song tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:01:04 +02:00
adminandClaude Opus 5.5 0701fe3856 Share the parts the sound devices and timelines repeated (#85)
feuerwerk / gate (push) Successful in 5m5s
feuerwerk / suite (push) Failing after 18m39s
Round 12 of the clean-up. Duplicate code had grown from 33 clones
(249 lines) after round 11 to 140 clones (1263 lines), almost all of
it in the panels and faces of the sound FX devices (#379 to #401).
It is down to 57 clones (416 lines) now, and src is 816 lines
shorter.

- deviceParts.ts holds what the device panels built by hand: the
  head, rows of knobs, the screen style and the row layout for
  groups of knobs. Twelve panels use it. panel.knobs stays because
  the debug hooks read it.
- DeviceFace is the base of the twelve faces. It finds the node and
  does wired, preview, commit, set, hide, reload and audition.
  compressorWired lives there as paramWired.
- TextPool replaces ten copies of "make texts as needed, hide the
  rest" in the wave and video tabs, the rulers, the piano roll and
  the song and composition timelines. clipStrip.ts is the clip label
  strip the wave and video tabs share.
- core/util/bytes.ts has the concat that ogg, matroska and avcColour
  each wrote out.
- Dead code is gone: clear on the video players (never called since
  #402), posterFileOf and videoTimeAt (only tests called them) and
  easeOut (the same as glide(t, 3)). Values only used in their own
  file lose their export.
- Comments over 125 columns are wrapped, and comments that told the
  history ("as before", "used to") say what is, in src and in the
  e2e modules.
- Left alone on purpose: AnimationEditor and CompositionEditor share
  only the play button, and the curve panels are built differently.
- Chip now places its boxes through put, which rounds. They were
  whole numbers already. The three row layouts are byte-identical
  before and after.
- README lists the video and composition assets and the Videos and
  Compositions pages.

Tests:
- Gate (melektaus-atx): CHECK EXIT 0, 313 files, 4835 tests.
- e2e 25, 34, 64, 65, 66, 69 (melektaus-atx): ALL PASSED, 330 checks.
- TextPool without keeping its texts and framesToRoom without making
  room both go red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:40:05 +02:00
adminandClaude Opus 5.5 df6dbe97c5 Build only the meshes of the tiles a painted cell changes (#424)
feuerwerk / gate (push) Successful in 4m34s
feuerwerk / suite (push) Failing after 18m22s
Since #348 a painted cell worked out only the cells around it, but the
layer's tiles still came as a new list. groupByTile grouped that list
afresh, so meshFor saw no group again and every mesh on the layer was
built anew. On 120x120 that was the last cost that grew with the map.

- A patched list of tiles remembers the list it came from and which
  entries went and came (tilesPatch). A cell that is drawn the same
  keeps its entry, and a patch that changes nothing drawn hands back
  the very same list.
- groupByTile and layerRows patch the groups of the list before
  (patchedGroups). An untouched group stays the same array, so its
  mesh is not built again. On a layer sorted by Y this holds for each
  row and for the groups in it.
- A tile new to the layer still works the whole layer out again, as
  since #348.
- Stage 2 of the issue (patching quads inside a mesh) is not needed.
  The tilemap geometry no longer shows in the CPU profile.

Measured with the #348 probe, a brush stroke on a copy of Garden of
Sacrifices on stein2:

- Meshes built per frame: 21.9 to 0.94 on 60x60, 40.7 to 1.22 on
  120x120.
- fps while painting: 49.4 to 51.1 on 60x60, 42.0 to 45.8 on 120x120.
- Painting now costs both sizes about 10 fps. The gap left between the
  sizes is drawing the larger map, which shows without painting too.

Tests:
- Gate on melektaus-atx green, 312 files, 4832 tests.
- The random walk from #348 also compares groups and rows with a fresh
  grouping after each of its 400 steps, and counts reused groups.
- A fixed test paints a cell and keeps every other group and row.
- Three counter-checks turn red: entries not removed from a group,
  rows sorted by their first cell, groupByTile ignoring the patch.
- e2e 39 + 40 on melektaus-atx: ALL PASSED, 107 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:41:41 +02:00
adminandClaude Opus 5.5 88e1cf1c7d Work out only the cells around a painted one on a tilemap (#348)
A painted cell worked out the tiles and blocking cells of the whole
map again, so painting got slower the bigger the map. One cell cost
16.8 ms on 60x60 and 54.6 ms on 120x120. Now it costs what is around
it: 3.5 ms and 4.0 ms, against 3.4 ms without painting.

- The brush, the paint command and its undo name the keys they
  changed. Each new version of the cells keeps them for the last 16
  versions (cellsChangedSince).
- The cell index, the tiles of a layer and the blocking cells patch
  only the changed cells and their eight neighbours. Everything else
  stays the same object as before.
- Both lists are in cell order now, so a patch is one walk that keeps
  every other entry.
- The rectangles are merged row by row. A patch joins again only the
  rows around it, one row further than the changed cells, and stacks
  the rest as they were.
- A writer that names no keys costs one walk over the cells. A
  replaced cell object (load, resize, edits at run time) is worked out
  in full as before.
- The tiles a map names come sorted by id. Two trigger tiles on one
  cell now keep that order, whichever way they were painted.
- resolveTilemap walked all tiles every frame, even without painting.
  It now does so only when the list changed.

Found on the way: deleting an outlined autotile can turn the one below
into a full box, which cuts a scaled box two rows further down. Merging
only the patched rows missed that. It is a test of its own now.

A brush stroke on the map doubled to 120x120 runs at 42 fps instead of
28, p95 of a frame 52 ms instead of 92 ms. What still grows with the
map is the mesh of a layer, which is built again on every paint.

Tests:
- tests/core/tilemap-patch.test.ts (new, 7): 400 random steps on two
  layers with every kind of tile, each compared with a fresh full run
- Unit, tilemap and viewport: 1284 green in 57 files
- e2e 39 + 40 (atx): ALL PASSED, 107 checks
- Gate (atx): 312 files, 4831 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:14:39 +02:00
adminandClaude Opus 5.5 8a49392d7f Keep the shifter check in e2e 65 from losing the clip's start (#415)
The check that a track plays through one shifter on its bus asked for
the start of the clip only after the note was planned. The clip ends
where the note starts, and the note is planned just 0.126 s ahead. In
a slow browser the clip's voice was already gone by then, and
`__gm.sound.startedAt` gave `null`.

- Each reading takes the plan, both starts and the audio clock in one
  `evaluate`. The clock goes into the detail of the check.
- The start of the clip is kept from the first reading that has it.
  Readings come every 150 ms plus the round trip, which is longer than
  the 0.126 s window, so one reading alone would not be enough.
- The note's start is still read where the note is planned.

Tests:
- Prettier, eslint on 65-song.mjs: clean
- e2e 65 on melektaus-atx: ALL PASSED, 59 checks
- Bent check (wait until the clip is over, read the start as before):
  red as the only one of 59, `clipStart: null` at clock 21.943 after
  the clip ended at 21.828

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:33:17 +02:00
adminandClaude Opus 5.5 39c509a8e5 Keep a video item in a recording when the tab goes back and forth
A recording made while switching between tabs had stretches without
the video item and without its sound, and in between the video stood
for several frames. Two things went wrong behind another tab.

- A player opened there got no first picture. It waited for
  requestVideoFrameCallback, which Chrome never calls behind a tab.
  A recording not in real time then waited 3 s per frame and took it
  without the video. Now the player takes its first picture as soon
  as it has data while the tab is hidden.
- The span of a picture was learnt from steps that skipped pictures.
  After a time behind the tab it learnt 0.133 s instead of 1/30 s,
  left out seeks within that span and never corrected it. Now it
  learns only from steps where the time sought moved at most half a
  step, so no picture can lie between them.
- Behind the tab a seek forgets which picture shows but keeps the
  span. A landing without a frame counts as the same picture only
  once the tab has been in front for 500 ms.

Found in the user's recording 7ea9146b: 5363 frames, all 1/60 s
apart, with the video missing at 10.63 s (8 frames), 43.17 s
(5 frames) and 46.93 s (28 frames), and the sound gone with it.

Tests:
- Unit tests/viewport: 377 green, 4 new or rebuilt. With the old
  rule and the first picture only by requestVideoFrameCallback both
  new tests go red.
- e2e 68 on melektaus-atx: ALL PASSED, 21 checks. New 68l: a video
  item that begins behind another tab has its colours exactly, and
  a tab switched every 350 ms gives colour changes at 0/30/60/90.
- One run in five with switches had a change one frame late, so the
  check allows one frame either way.
- Gate on melektaus-atx: green, 311 files, 4824 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:36:00 +02:00
adminandClaude Opus 5.5 b1173ba6bb Show how fast a recording not in real time goes as a speed
The tempo of a recording not in real time read as how many times
slower than real time it was made. "5.1x real time" for a recording
that took five times as long as its video looked like it ran five
times faster.

- The log line at the end and the notice now give seconds of video per
  second it took, with two decimals: 0.20x for the example above.
- It may go above 1x. Nothing holds a recording not in real time back
  to the wall clock, and a cheap scene on a fast screen makes more
  frames a second than the video has.
- A recording of a single frame took 0 s and has no tempo line now,
  instead of a division by zero.

Tests:
- tsc, eslint, Prettier clean
- Gate on melektaus-atx: green, 311 files, 4821 tests
- No check reads the figure itself. 68j looks for "x real time, and
  has every frame", which still matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:05:20 +02:00
adminandClaude Opus 5.5 abb8fa51d8 Keep a recording going behind another browser tab
Chrome stops calling requestAnimationFrame for a hidden tab, and Pixi's
ticker hangs on it. The run, the drawing and the recording all stood
still as soon as the user switched to another tab. A plain run keeps
standing still there, so a forgotten game does not play on unheard.

- backgroundBeat.ts ticks the editor from a worker at the recording's
  fps while a recording goes and the tab is hidden, or the page has
  not drawn a frame of its own for 250 ms. Once the page draws again,
  the beat keeps out of the way.
- The worker looks every 4 ms whether a tick is due. A 16 ms interval
  came in bursts behind a busy page, and the recording threw away the
  second tick of each burst.
- backgroundTimers.ts and timerWorker.ts give the short waits of a
  recording timers in a worker. Chrome slows the page's own timers to
  once a second in a hidden tab. This covers the waits while saving
  and the wait for a video's picture after a seek.
- Behind a tab Chrome does not call requestVideoFrameCallback, but the
  picture is there at seeked. The wait there is 10 ms instead of
  150 ms while the tab is hidden.
- The beat runs for Start recording, the exported game and Start
  recording... in the main menu.

Tests:
- tsc, eslint, Prettier clean
- tests/viewport: 374 green, 3 new in background-beat.test.ts
- e2e 68k (3 checks) on melektaus-atx with the tab really hidden:
  not in real time all 120 frames, colour changes every 30 frames,
  the blip 8 ms from green; in real time 97 of 120 frames (73 with
  the tab in front)
- Counter-check without the beat: all three 68k checks red, 3 frames
  in 60 s not in real time, 7 frames in real time
- Gate on melektaus-atx: green, 311 files, 4821 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:56:56 +02:00
adminandClaude Opus 5.5 87b2ee9402 Record every frame with Real time off on Start recording (#423)
At 4K and 60 fps the user's computer reaches about 39 fps in real time.
Reading each frame back from the GPU takes longer than a frame, and
neither reading back later nor the canvas route helped. With Real time
off the run moves on by one frame of the recording at a time, however
long the computer takes, so the file has every frame.

- Start recording has a new check Real time, on by default. Only the
  node records so, the editor recording stays in real time.
- SteadyPace in recording.ts is the pace as a pure class. A step is
  1/fps once the frame before is taken and the sound up to it is made,
  otherwise 0 and the run holds. A frame's time is its number.
- The sound is made apart, as decided in the issue. AudioSwitch routes
  what the run plays to the live library, or to the offline library of
  a SteadySound while such a recording lasts. That library has the same
  mix, buses, FX and limiter and is rendered up to each next frame.
- A driven video holds at every picture asked for, and the frame waits
  for it. Its sound is decoded from the MP4 and played in step through
  its bus.
- Two Chrome quirks measured and worked around. A held video hands
  over the picture of a seek before seeked, so the frame callback is
  asked for when the seek begins. A seek right onto where a picture
  begins shows that one or the one before, so such a seek lands half a
  millisecond further. Together 30 s became 10.7 s for 2 s of video.
- The notice tells how far the recording has got, and the line at the
  end how long it took.

Deviations and limits:

- Input is not ignored, it still reaches the game, which runs slower.
- At most ten minutes per recording. Chrome holds the whole buffer of
  the OfflineAudioContext while rendering, 230 MB for ten minutes.
- A free video plays on in real time and does not fit. A sound playing
  before the start stays at the speakers.

Found along the way:

- A check in e2e 60 was red since 77365f1. It expected one log line,
  the start line of the recording came on top. CI was off, so nobody
  saw it. The expectation is updated.

Tests:

- First gate run red: 1 test in node-catalog.test.ts still expected
  the settings of Start recording without realTime. Updated.
- Second gate run on melektaus-atx green: 310 files,
  4818 tests.
- e2e 66 + 68 all passed, 50 checks, 68j new with 3 checks. ffmpeg
  reads all 120 frames 1/60 s apart, the colour changes at frames
  30/60/90, the video's sound from the first frame, the blip 8 ms off.
- e2e 60 (20 checks) and 67 green.
- Counter-check without the pace: all three 68j checks red, 32 of 119
  frames, colour changes at 33/62/96, no sound. AudioSwitch's unit test
  goes red when the ids are mixed up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:57:15 +02:00
adminandClaude Opus 5.5 77365f17d3 Say when a recording starts and when it cannot keep up
A 4K60 game view recording on the user's machine had 517 of 827
frames. The rest were skipped without a word: when the editor's tick
came late, FrameGate let the due slots pass, and only frames lost to a
busy encoder or read back counted as dropped. The file kept its time
but stuttered, and nothing said so.

- FrameGate counts the frames it took and the slots the recording's
  time has room for. The difference is skipped, next to dropped in
  __gm.recording().
- FramePace judges the missed frames over spans of 2 s. The first
  time more than 10 % are missed, the log warns once with the frames
  a second the recording really got.
- At the end, when more than 5 % are missed, a warning gives the
  frames of the whole file and what helps (30 fps or a smaller
  resolution). It comes after the upload's notice, which would cover
  it at once in the editor.
- The first second counts for neither warning. Without that, a short
  e2e recording warned about 3 frames lost while starting.
- A recording that starts logs an info line with its size, fps, codec
  and sound. TakeHost.log takes a level for it. The editor recording
  keeps the line to the console, its notice would be in the video.
- consoleLine is the one place with an eslint exception for
  console.info, so the console panel shows the line plain.

Found along the way: headless Chrome in WSL records the editor with
real stutter in e2e 67 (1280x800@60 27 %, 2304x1440@30 12-14 %). That
is the software encoder there, and only now shown.

Tests:
- Gate on melektaus-atx: green, 309 files, 4810 tests.
- Unit recording.test.ts: 36 green, 6 new for FrameGate.skipped and
  FramePace. With skipped stuck at 0 the test turns red.
- e2e 67 on melektaus-atx: ALL PASSED, 15 checks, run twice. The new
  67b-ii slows the editor to about 20 fps: a 640x360@60 recording
  says "about 19 of 60 fps" and saves with 95 of 192 frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:21:46 +02:00
adminandClaude Opus 5.5 4d0a696327 Keep composition sound in step and give compositions a bus of their own
A composition's sound ran on the audio clock while its picture ran on
the frames. A frame counts at most 0.1 s, so after a hitch the sound
stayed ahead of the picture until the next stop. Songs in a trailer
drifted off the cut. The items of a composition also had no bus of
their own, and a video sprite could not pick where its sound goes.

- CompositionSound.follow compares the audio clock with the picture
  each frame. More than 40 ms apart, it starts the sound again where
  the picture is. This holds in the tab and in a run.
- A time scale other than 1 now restarts the sound again and again.
  It stays in time but comes in pieces.
- The sprite of a video item shows "Driven (by composition)" as a
  fixed Video clock row. Video time is hidden there, because the
  composition sets it every frame.
- Every composition has a bus in the mix and a node in the master
  routing. A new one goes to the master unless other compositions are
  linked elsewhere, apart from where the sounds go.
- Audio items pick From asset, To composition, Master or a bus. New
  items start with To composition. Items saved before keep From asset,
  so music already sent through a bus does not bypass it.
- Video sprites have a new row Video bus: From asset by default, the
  master or a bus. In a composition the default is the composition's
  bus, also for video sprites inside a container item.
- The chosen bus is part of the key of the video player, so a sprite
  with another bus gets a player of its own.

Found along the way:

- Six tests with full lists of sprite defaults expected the old rows
  and now list Video bus too.

Tests:
- tsc, eslint, Prettier clean
- Unit: 971 green in the 28 files touched
- npm run check on melektaus-atx: green, 309 files, 4804 tests
- e2e 68 (melektaus-atx): 13 checks green, 68i new. The sound item
  and the video item go into mix:cp68-cut, which goes to the master.
- e2e 69 (melektaus-atx): ALL PASSED, 23 checks, 69q and 69r new.
  After a hitch of 500 ms the sound restarted twice and then lay
  0.5 to 5 ms off the playhead.
- Counter-checks in unit tests: without the comparison the hitch test
  fails, without the bus in the preload the run test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:31:16 +02:00
adminandClaude Opus 5.5 875c76026a Silence a free video once its sprite is destroyed (#420)
A sprite whose Video clock is free played its video on after it was
destroyed, heard in a game, until the player was let go 2 to 4 s
later. A driven one was already held and quiet after 100 ms.

- The library holds and quiets a free player nothing asked for in
  HUSH_MS, as it did a driven one. That covers Destroy, a scene load,
  the end of a run and a sprite switched off.
- Asked for again before it is let go, it plays on from where it was
  held, heard only in a run. A hidden page leaves it alone, as the
  sweep does.
- A sprite only out of view keeps its free video playing and heard.
  Whether a video is heard should not hang on the camera. Before, it
  ran 2 to 4 s and was then let go. Now it plays on, which opens its
  player even where it was never in view.

Tests:
- Unit: video-library 1 new. A free player losing its wearer is held
  and quiet 200 ms later, stays so through a new run, plays on heard
  when asked again with the same element, and runs on on a hidden
  page.
- e2e 66 on melektaus-atx: ALL PASSED, 36 checks, 66g-ii new. Heard
  31 frames before Destroy, held and muted in every frame read after.
- Counter-probe without hushing free players: the unit test and only
  66g-ii fail, the player plays on heard (paused false, muted false).
- Gate on melektaus-atx: 309 files, 4797 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:49:12 +02:00
adminandClaude Opus 5.5 acd12326dc Fade sounds and videos in and out on the items of a composition
A trailer needs its music and its clips to come in softly and leave
softly. Until now an item of a composition could only play at one
level, and a video item could not even be turned down on its own.

- Every item with sound gets a grip at its upper left and upper right
  corner. Dragging one inwards fades the item in or out. Dragging it
  back to the edge removes the fade. Lower down the same edge still
  trims as before.
- The fade is drawn as its curve, with the part above it darkened.
  The inspector shows Fade in and Fade out in seconds, each with the
  curves sound clips already know.
- The model keeps fadeIn and fadeOut in frames plus curveIn and
  curveOut on the item. Old projects load unchanged. An item shorter
  than its fades plays them cut to its length but keeps the setting.
- Sound, pattern and song items ramp the gain of their own bus. The
  scheduler plans the ramps on the audio clock next to the notes.
  Starting in the middle of a fade picks it up from there.
- Video items set the new sprite property Video volume each frame.
  It multiplies with the fade and is only shown for a driven video.
  The player glides to each new level over 15 ms so it does not click
  between frames.

Not covered: a container item that carries a video further down gets
no grips. Only a video on the item's own sprite counts.

Tests:
- tsc, eslint, Prettier clean.
- Unit tests for composition, music, song and sound (12 files): 269
  passed, 7 of them new.
- e2e 69 on melektaus-atx: ALL PASSED, 21 checks. 69p drags a grip to
  a fade in of 10 frames and hears the item bus rise from 0.127 over
  0.229 and 0.339 to 1 within one second.
- Counter-checks as unit tests: without the audio ramps, and without
  the level on the video sprite in the game, both go red.
- Gate on melektaus-atx: 309 files, 4796 tests passed, 8 more than
  at 77ac9d1. The first run failed 14 tests in 6 files that list the
  default sprite properties or the bus levels in full. They now
  expect videoVolume and the new level field of busLevels.
- Not tried in a browser: video fades, pattern and song items with a
  fade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:05:36 +02:00
adminandClaude Opus 5.5 77ac9d1aaf Play sounds, patterns and songs on the tracks of a composition (#418)
feuerwerk / gate (push) Successful in 4m31s
feuerwerk / suite (push) Successful in 18m21s
A trailer needs its music and effects on the same timeline as its
pictures. Until now a composition could only place objects, so sound
had to be started by hand from a behavior and drifted against the
picture.

- Audio items: a sound, a clip of a sound, a pattern or a song dragged
  from the asset panel onto a track becomes an item as long as one
  pass of it. It may share a track with object and video items and
  overlap them.
- Editing works as for other items: move, resize, trim on the left
  (moves the offset), fit to media, delete, copy and paste, box
  selection. Sounds draw their waveform, the colour tells the kind.
- The inspector shows what it plays, start, length, offset, volume
  (-60 to +12 dB) and bus. The bus is "From asset", the master or one
  of the buses, as asked for in the issue.
- An item longer than its material plays in rounds. Sounds and
  patterns start over, a song runs on in its loop. Loop marks on the
  item follow the same rule, a song's first mark sits at its loop end.
- compositionAudio.ts turns the items into sequences, each with its
  own tempo. CompositionSound plays them, in the tab and in a game.
- In the tab the items are heard from the playhead on, straight into
  the master like every tab. A scrub is silent. Playing a song or
  pattern in its own tab stops the composition, and the other way
  round.
- In a game they are planned on the audio clock for the moment the
  composition clock reaches them, with the scheduler's LEAD made up.
  While the clock stands (waiting for a video, paused, placement off)
  they are silent and resume at the right spot.

Found along the way:

- The asset panel never let a song out of its list, so a song could
  not be dragged anywhere. Song and scene tab reject it as they reject
  a pattern.
- The asset panel scrolled to the selected asset on every rebuild of
  its list, so folding a group far below the selection jumped back up.
  Only a new selection scrolls now.
- At the end of the list, folding a group pulled every row down by the
  rows that went away. ScrollContainer has a new opt-in holdOffset,
  set only in the asset panel, that keeps the offset until the user
  scrolls up.

Limits: a time scale other than 1 or a frame longer than 0.1 s moves
the picture and not the sound. A baked song plays its notes here, not
the bake.

Tests:
- tsc, eslint, Prettier clean. Gate on melektaus-atx green,
  309 files, 4788 tests (4772 plus 16 new).
- Unit: composition-audio.test.ts 13 new, scroll-container 3 new.
  Counter-checks: without catching up on load time or without the
  silence on pause the unit tests go red.
- e2e 68h: the sound is planned 1.2 ms after the composition reaches
  0.5 s. Without the LEAD make-up it is 54 ms late and red.
- e2e 04 and 69 ALL PASSED, 86 checks. 69o-ii drags a pattern and a
  song onto a track, 20 frames each.
- e2e 04: folding Sound at the end of the list keeps its header at
  y 234. With the old panel it jumps to 984 and the check is red.
- Not run in a browser: the loop marks of a song item, copy and paste
  of audio items, the bus choice.
- README and wiki count 2072 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:28:36 +02:00
adminandClaude Opus 5.5 30b618d9a9 Show game view recordings in their true colours in other players
A game view recording looked darker and its colours off in VLC, and
each round of recording a recording made it worse. Chrome's encoder,
fed RGB frames, wrote full-range H.264 and left the colour out of the
stream. Only our Matroska head said "full range", and VLC reads only
the stream. Chrome itself plays the converted MP4, which was right.

- The GPU now turns the game view into I420, limited range BT.709,
  before it is read back (PictureReader). The encoder gets the planes
  with that colour, as it does for a recording of the whole editor.
- Where the planes lie in the read-back picture is a plain function,
  i420Layout in recording.ts.
- Only 1.5 instead of 4 bytes a pixel come back from the GPU.
- avcColour.ts writes the colour into the SPS of the encoder's avcC as
  well, so a player that reads only the stream knows it. An SPS with a
  VUI only gets its colour block replaced. One without gets a VUI that
  holds the colour. An SPS it cannot read stays as the encoder made it.
- A first try sent the frames through a 2D canvas. That gave limited
  range too, but only 18 to 20 of 33 frames at 1080p30.

Found on the way:

- With the colour in the stream alone, VLC 3.0.8 still showed the
  full-range picture too dark. It ignores the range.
- The one old full-range original on the server got the same SPS fix
  without loss: the clusters are byte-equal, the decoded frames are
  equal (framemd5), backup in /root/config-backup-20261013/videos/.
- Older game view recordings stay full range. Fixing that in VLC would
  mean encoding them again.

Tests:
- tsc, eslint, Prettier clean.
- Gate on melektaus-atx: CHECK EXIT 0, 308 files, 4772 tests.
- avc-colour.test.ts new (9), expectations read back with ffmpeg
  trace_headers. Counter-check: the old bug in the no-VUI path turns
  it red.
- e2e 67: ALL PASSED, 12 checks. 67d-ii reads tv/bt709 from the file
  and from the bare H.264 stream, red with the old code.
- 1080p30 34 frames, 640x360@60 68 frames, none dropped (was 33/47).
- Checked by the user: a new recording looks right in VLC 3.0.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:48:44 +02:00