Skip to content

Release 0.1.15: crash protection, Integrations (experimental), vertical without Twitch - #71

Merged
Soulhackzlol merged 56 commits into
mainfrom
feature/integrations
Oct 4, 2026
Merged

Soulhackzlol merged 56 commits into
mainfrom
feature/integrations

Conversation

@Soulhackzlol

Copy link
Copy Markdown
Owner

0.1.15

Everything since 0.1.14, including what shipped in the 0.1.15-rc.1 pre-release (the feature/crash-protection branch, merged in here).

Highlights

  • Crash protection: when OBS crashes, freezes or drops, destinations stay live on a reconnect screen (5 styles) for up to the time you pick, then rejoin past the gap.
  • Integrations (experimental): Twitch chat commands, channel points, Discord, web requests, hotkeys, MIDI, on-stream alerts, recipe import with a full preview of what it runs and who can start it.
  • Vertical without Twitch: per-destination 9:16 output no longer needs a Twitch Dual Format canvas.
  • Enhanced Broadcasting on your GPU, the VOD track script fix, a delay tail that ends cleanly, plus a long list of dashboard and dock polish.

Security

  • Running programs and writing files need a dashboard password whenever the dashboard is open to the network; requests InstantClone makes itself, proxied requests and port forwarders never count as "this PC".
  • Overlays are served sandboxed; the Studio cleans overlay files it bakes.
  • Chat text passed to a program can't become an option or a path escape.

Review

Three review rounds (core, security, UI, dock) before release; every finding fixed, with tests. 846 tests pass, clippy and fmt clean.

Full details in CHANGELOG.md.

With no Twitch destination, InstantClone now builds OBS's Enhanced
Broadcasting config itself from what OBS reports: one track for the main
canvas at its output size, plus the Additional canvas (e.g. Aitum
Vertical) when OBS sends one, so vertical destinations work without
Twitch. The encoder is the H.264 one of the GPU OBS runs on, but only
when OBS's log lists it as available (OBS won't start a stream on an
unknown encoder); otherwise x264. The old fallback always asked for
three x264 encodes, which overloaded CPUs.

- The dashboard no longer locks Vertical behind a Twitch destination.
  A waiting vertical destination shows why on its card, and the log
  says so once per OBS session.
- The old --launch-eb shortcut leaves OBS 32.2+ config alone.
- Fixes: Twitch ingest reset on first edit, the Twitch mobile-risk pill
  that never showed, VOD + EB resetting the audio track, a stale
  Vertical pill after OBS stops, HTML-rendered save errors.
- Polish: wrapping log with a tail that respects reading, aligned
  destination cards, a shorter destination editor, GPU encoder advice
  on the OBS tab.
Version bump, changelog entry, and READMEs updated for vertical without Twitch and the 406-test count.
Crash protection keeps destinations live on a looping reconnect screen
when OBS crashes (fan request). This lands everything except the live
egress wiring, so the new code is not called yet outside its tests.

- crash_protection.* settings, saved with the System > Behavior card,
  with a live preview drawn by the real renderer
  (GET /crash-protection/preview).
- src/slate: a small rasterizer, an Inter TrueType renderer (fonts
  subset by scripts/subset_slate_fonts.py, OFL), a 5x7 pixel font, the
  Whisper and Arcade themes, and a lossless H.264 encoder (I_PCM,
  I16x16 without residual, P_Skip). ffmpeg decodes its 2 s loop back
  pixel for pixel.
- The ingest tells a deliberate stop (FCUnpublish / deleteStream) from
  a dropped connection: Controller::last_ingest_end.
Brings the finished v0.1.15 work (vertical without Twitch, GPU-aware Enhanced Broadcasting, dashboard fixes) under the crash-protection work in progress. No conflicts; 441 tests pass.
- A reconnect screen keeps every destination live when OBS crashes,
  freezes or drops, Enhanced Broadcasting tracks included. It ends on
  OBS's return (rejoining behind the delay when one is armed), after the
  hold time, or from End now in the dashboard, dock, tray, hotkey or MIDI.
- Enhanced Broadcasting asks Twitch for H.264 tracks while another
  destination is on, so HEVC-capable GPUs feed TikTok, Kick and vertical
  destinations. A vertical card names HEVC tracks instead of "No 9:16
  canvas".
- Ending a stream reaches the platform as a clean stop.
- A disabled, incomplete destination no longer blocks saving settings.
…s as H.264

Twitch builds the vertical track as HEVC on GPUs that have it, whatever
codecs OBS offers; only the main track follows supported_codecs. A live
trace showed the narrowed request coming back as an H.264 1080p main
track (down from HEVC 1440p) and a still-HEVC vertical one.

- With a vertical destination on, the proxy switches the vertical track in
  Twitch's reply to the same vendor's H.264 encoder, keeping its size,
  frame rate and bitrate (AMD's Hevc-prefixed options renamed to match).
- The request is narrowed to H.264 only for a horizontal destination
  besides Twitch, so a Twitch + TikTok stream keeps its HEVC main track.
- The waiting log line names HEVC instead of "no 9:16 canvas".
…odecs alone

Beta.2 switched Twitch's HEVC vertical track to H.264 in the config, and
Twitch refused every publish of that session (10053 on connect): it
checks each track against the config it issued. Reverted.

Twitch streams 2K channels (Partners, Affiliates) in HEVC, vertical track
included. The vertical track is now read from the config OBS was given
(OBS numbers tracks in encoder_configurations order), so it is found in
any codec, not only when its H.264 SPS can be measured, and forwarded as
Twitch sent it. Restream takes H.265 and transcodes it to H.264.

The request is still narrowed to H.264 for a horizontal destination
besides Twitch, whose main track Twitch does follow.
- Beacon (radar rings), Orbit (spinner) and Studio (lower third, lifted
  clear of TikTok's buttons on vertical canvases) join Whisper and Arcade.
  Each animates in few steps over a small area; every style stays under
  1 Mbps at 1080p (tested).
- Idle dots and tracks follow the background, so they read on any colour.
- Settings: style cards with live thumbnails, an animated preview with a
  16:9 / 9:16 switch, a hold-time slider, text counters and a reset link.
- The server is the single source of the style list, default backgrounds
  and default text; the dashboard keeps no copy.
- Text alignment lives in font::Line; a circle is a ring with no hole.
Every change fetched 10 separate preview frames plus 5 thumbnails (15
requests, ~4 MB) and swapped the image from a 200 ms timer; picking a
style reloaded the thumbnails too, though a style can't change them.

- /crash-protection/preview?frames=N stacks N moments of the loop into
  one strip (Canvas::stacked); CSS slides it a frame at a time.
- Thumbnails reload only when colours or text change.
- The strip is fetched once and swapped in whole, so it never flashes.
- 37 unused CSS classes (old overlay picker, wizard rows, an unused
  switch and checkbox, st-* leftovers): 5.5 KB off the dashboard.
- Two functions nothing calls: dock.js openOverlay and overlay-runtime
  defaultStateProps.
- The audio egress trace built its line for the first 20 tags of every
  connection and every 200th after, even with tracing off; it is now
  gated like the video one.
- Sub-tabs regrouped: General (startup, auto-arm, buffer), Crash
  protection, Controls (hotkeys, MIDI; hidden where neither exists),
  Twitch & OBS, Access (ingest key, password, ports, LAN), Alerts & logs,
  About. Sections moved verbatim; old sub-tab names map to the new ones,
  so remembered tabs and jump links still land right.
- Find a setting (or "/"): matches titles, data-find keywords and text,
  opens the sub-tab and highlights the section.
- Save bar: sticky, shows while the form differs from what was loaded or
  saved (the whole form body is compared), with Discard, and a prompt
  before closing the page with unsaved changes.
- The pane card's scroll box is capped at the room left in the window,
  so its bottom edge is always on screen.
…ter images

- Style thumbnails went blank whenever settings reloaded (after a Save):
  the cards were rebuilt empty while the thumbnail cache still matched.
- The preview sampled the loop 10 times, which lines up with none of the
  styles' 6, 8 and 12 steps, so steps were skipped and timing stuttered.
  It now shows 24 frames: every step at the moment the stream shows it.
- Previews are PNG from a small built-in encoder (Sub/Up filters, run
  deflate): a 24-frame strip is ~150 KB where 10 BMP frames were 3.8 MB,
  thumbnails 1-3 KB instead of 61 KB. Checked lossless against ffmpeg.
- Previews render on a blocking thread, so drawing a strip never stalls
  the stream's egress on the single-threaded runtime.
- The save bar only appears while there are unsaved changes.
…themselves

- Cards and sections ease in, 40 ms apart, when a tab or sub-tab opens
  (opacity, 8 px rise, slight blur); the class clears on animationend so
  hover transforms stay free.
- A destination pulses once in its platform colour when it goes live.
- Buttons press to scale(.96) instead of a 1 px drop.
- Destination cards say "Goes live when OBS does", "Connecting…",
  "Connected, waiting for video" or "Off. Switch it on to stream here."
  instead of a dash; counters read "frames · cuts · reconnects" and
  appear once there is something to count.
- Everything respects prefers-reduced-motion.
…tinations

- Ctrl+K (or Search in the header) opens a palette built from live state:
  tabs, System settings (the Find index), destinations on/off/edit,
  profiles, context delay actions (Activate when armed, Back to live when
  active), and a typed delay ("45", "2m", "1m30s") to arm directly.
- Event log rows: time, source pill, message; severity from the text
  (errors red, warnings amber, successes green). Filters for Problems and
  Errors with counts, per source, and free text; "Jump to latest" when
  scrolled up. New lines are appended instead of re-rendering the list.
- No destinations yet: "Where do you stream?" with a tile per platform
  (including the local test sink) that opens the form preset.
It's a feature few people reach for; the effort goes into the visual
refresh instead.
- Twitch, YouTube and Kick marks in their colours on destination cards,
  the empty-screen picker and the setup wizard (platformMark); Restream,
  Custom and the test sink get a plain symbol, Trovo keeps its letter.
- Destinations shows the signal path: OBS -> InstantClone (current delay
  or buffering state) -> every platform, each link animating only while
  data flows; rebuilt only when the destination set changes.
- Larger section titles, labels, tab icons (open tab in the accent) and
  stats numbers.
- Stats says "No video yet" over the flat chart before OBS streams; an
  empty Profiles list explains itself; card counters read "1 cut".
- Three drifting lights behind the whole hero (not just the readout half),
  additively blended, with film grain to stop the gradients banding.
- The state colours live on <html> (data-stream-state) with one @Property
  transition, shared by the hero and a faint ambient glow at the page top.
- The hero's top edge catches the state colour; a bloom from the number
  marks every state change.
- Primary buttons gain a gradient and top highlight; cards, tabs and
  sections a lighter top border.
- Removed the signal path strip (didn't fit).
- Fixed header (platform mark in its colour, title, Enabled switch, close)
  and footer (message, Cancel, Save) around a scrolling body, so Save is
  always in reach past the Twitch VOD section.
- The body is a two-column grid on wide screens: name | platform, server |
  stream key, format (or the Twitch note) | audio track. One column under
  760 px; a full-screen sheet under 560 px.
- Field labels in normal case; audio options shortened to fit a column,
  with the Auto behaviour spelled out in the help line.
- Every field keeps its id and markup, so the platform logic is unchanged.
Delay engine (controller, buffer):
- A delay now always airs at least the time you set: the cut lands on the
  newest keyframe at or before the target instead of the nearest one, so a
  long GOP can no longer make it air short.
- With a delay on, a destination waits for the buffer to reach it before
  connecting, instead of going out live (OBS restart mid-delay, a destination
  switched on mid-stream).
- Buffer trim keeps the keyframe the full-buffer delay needs.
- Live recuts use the dead band and the nearest keyframe to live.
- The pump re-checks the publisher and sequence header after every wait.
- Keyframe lookups use primary-track keyframes only.
- Crash protection claims a fresh EB session instead of dropping it when the
  hold ends first, and a frozen OBS expires the hold before resuming.

Enhanced Broadcasting without Twitch (local_eb_config, obs_register, web):
- The fallback config copies the streamer's own OBS stream settings
  (encoder, encoder settings, bitrate, rescale; bitrate in Simple mode).
- The Additional canvas is encoded only for an enabled vertical destination,
  with the same bits per pixel as the main track, on the streamer's codec
  when every vertical destination plays it, H.264 otherwise.

Hardening from the test audit:
- Dashboard: proper 400s for bad bodies, dock token no longer sees custom
  RTMP URLs, /overlay rules apply to GET only, URL rewriter fix.
- Config: lossy UTF-8 startup load that never overwrites an unreadable
  file, legacy "Main" migration fix, OBS service registration keeps an empty
  services list valid.
- H.264 zero-length NAL crash and scaling-list parsing, RTMP chunk and AMF0
  edge cases, slate level 6.1, autostart test isolated from the real Run key.

Tests: delay, hold and pump simulations, route-access net, full settings
round trip, parser fuzz cases and the fallback settings matrix.
- Delay button eases between Arm, Activate, Adjust and Cut; only the number
  flips when only the number changes.
- Delay field rolls its digits; limits at 0 and 600 answer back, and a
  typed value past 600 settles on 600 (and counts as 600 meanwhile).
- A profile too big for the buffer explains itself when clicked.
- "Waiting for OBS" listens with a sweep and dots, and blooms once on
  connect.
- A clicked profile sends a spark into the button it arms; profile chips are
  patched in place so the armed fill never replays.
- The status pill glides between states.
- Aurora: five pools on slow orbits replace the three drifting blobs; the
  light grows with the buffer while arming; holds still under reduced
  motion.
- The per-tick render writes nothing when nothing changed (panel and hero).
- Fixes: primary and warning buttons turned grey on hover; idle destination
  cards squeezed their message; Add destination button restyled.
Changelog for crash protection, vertical without Twitch, the Enhanced
Broadcasting fallback (explained as a fallback, with Twitch's own config
used whenever Twitch is on at Start Streaming), the delay fixes and the
dashboard polish. READMEs: delay wording ("at least N seconds back") and
switching the Vertical destination on before streaming.
Delay
- A delay longer than the buffer holds runs at what a full buffer holds
  instead of waiting forever; logged once per armed value.
- Cuts must move in their direction, so a keyframe interval longer than
  measured never replays the same frames.

Crash protection
- The delay tail airs in full when the hold runs out; End now ends at once.
- A frozen OBS that outlasts the hold ends instead of looping the tail.
- A freeze rejoin keeps the full delay; a destination switched on mid-hold
  joins the reconnect screen.

Egress
- Destination edits (URL, format, audio track) stop the pump cleanly and
  restart it; a stopped pump releases its place in the buffer.
- Connect timeout, backoff that resets only after a stable session, and
  reconnects counted only on real drops.
- Track 2 destinations never get the live track once the second is sent.
- Replies matched by transaction id; unflattenable headers are skipped.

Ingest
- Handshake and unpublished-idle timeouts, a command size cap and a cap on
  unfinished messages per connection.
- A hung publisher hands the slot to a restarted OBS; refused publishes are
  log-throttled; the test sink binds to loopback.
- End-of-stream markers no longer count as keyframes; 1 and 2 byte NAL
  lengths are scanned.
OBS dock
- Buffering counts down on the main button ("Ready in 0:23") and fills
  toward Activate, instead of a spinner rebuilt four times a second.
- Crash hold: the dock says Holding, and the hold card (countdown, held
  destinations, End now) takes the place of the delay controls, or sits
  on top in a dock without them.
- Destinations follow the state stream instead of a 4 s poll, show
  Connecting and Holding, and are patched in place so an open confirm
  survives updates.
- Guarded writes: an unchanged state push touches nothing. The phase chip
  glides, the readout pops only on a user change, the idle setter hugs
  its digits, and Cut delay goes straight to Activate.

Fixes
- The inbound and per-destination bitrate held their last value after
  OBS stopped; one RateMeter now reads 0 once its window goes stale.
- Hovers took a third hue from blending the accent with the state
  colour (green while arming); each state now has one tint.
- "Applying" waited out its 2.5 s fallback after Cut or Disarm because
  the pending check compared a raw phase with a display state.
OBS's clear_archive_encoder (StartStreaming -> SetupVodTrack, every
version 28-32) calls obs_encoder_release() on the index-1 encoder it only
borrowed. On the plain stream outputs (adv_stream / simple_stream) that
stray release destroyed our freshly attached encoder before the stream
started, so OBS sent a single audio track and Twitch had no VOD audio.
Only Enhanced Broadcasting worked, because it streams through a separate
multitrack output that clear_archive_encoder never touches.

On plain outputs, hand our create-reference to OBS instead of releasing
it: the stray release consumes it and the output stays the sole owner.
The post-start check now always warns when an attached track went
missing, instead of only in verbose mode.
The reconnect screen's decode tests require ffmpeg on Linux CI and fail
without it. ci.yml's test-linux already installs it; release.yml's did
not, so the v0.1.15-rc.1 release run failed.
Integrations react to what happens in InstantClone (a crash, a cut, a
chat command, a timer, a web call) by posting to Discord or Twitch chat,
pushing to a phone, calling a web service, running a program, writing a
file, or acting on the delay itself.

- One model for everything: an integration is handlers (trigger + steps).
  Catalog modules are ordinary integrations with a `preset`, so anything
  added from the catalog can be edited freely.
- Steps: messages, web requests (response usable as variables), checks
  with then/otherwise, waits, "wait for the delay to air", delay actions,
  VOD markers, clips, counters, programs and files.
- Templates: {var} and {var|fallback}; JSON bodies pass through as-is.
- The engine runs on its own thread and runtime, so nothing it does can
  delay the stream. Events arrive through a bounded, never-blocking queue
  that works from any thread; concurrency, cooldowns and waits are capped.
- Twitch: Device Code login (no secret, no server), token refresh before
  expiry and hourly validation, chat over IRC/TLS with reconnect and rate
  pacing, markers and clips through Helix.
- The old single Discord webhook migrates once to a Discord connection
  plus a "Stream alerts" integration sending the same messages.
- Recipes share integrations as text without connections or secrets;
  imports arrive switched off and programs/files need an explicit yes.
- Minimal JSON parser (no serde) for the API and recipes.
The dashboard side of integrations, built from the Destinations pieces so
it feels native: a module is a destination card whose screen shows
exactly what it sends, and every editor is the destination modal.

- Grid and list views, filters and search; problems surface as one line
  with a Fix button.
- Catalog of ready-made integrations and starter packs.
- Module editor: moments as a timeline, the message edited on the preview
  itself with variable tokens, every option as a chip with its panel,
  and test runs that show every step.
- Step builder: any trigger, nested if/otherwise, reorder/duplicate,
  per-step inspector, variables panel, test runs.
- Connections: Discord channels, Twitch login by device code (main and
  bot accounts), phone pushes through ntfy.
- Recipes: share and import as text.
- The System tab's Discord webhook field points to Integrations; a
  settings reset keeps integrations, a factory reset clears them and the
  Twitch login.
- Release builds take the Twitch app id from the TWITCH_CLIENT_ID repo
  variable.

Fix: the HTTPS client handed schannel ureq's bundled root list, and
schannel refused Discord's chain, so Discord webhooks never delivered.
It now uses the OS certificate store.
Bugs:
- "Set the delay" (!setdelay) used the hotkey's arm action, which
  toggles: a repeat disarmed the delay and a delay on air refused it. It
  now sets the delay, changing it live when one is on air.
- A refused Twitch token left chat down until the next 5-minute upkeep
  round; the chat connection now asks for a refresh right away.
- A bot account set to the streamer's own account made every command the
  streamer typed get ignored as "our own bot".

Safety:
- Program and file paths can't use variables, and neither can a web
  request's host: a chat message could otherwise pick what runs, where a
  file lands, or which machine gets the request. Recipes carrying any of
  these are refused.
- Test runs never ping (@here/@everyone) a Discord server.
- A destination stuck reconnecting alerts at most once per 5 minutes for
  each of live, dropped and all-down, instead of on every round.
- The delay on/off chat notice has a 30 s cooldown.

UX:
- A switched-off integration saves as a draft with what is still
  missing; switched on, it must be complete. Errors read as instructions
  ("Pick a Discord channel") instead of field names.
- Adding a Discord channel from inside an editor (or an import) comes back
  to it with the work intact and the new channel picked; an editor that
  lacks a channel opens on the chip that fixes it.
- Newly added modules open switched on, so finishing them turns them on.
- A catalog webhook's address applies to all its events at once.
- Live refreshes no longer steal the search caret or wipe the Discord and
  phone forms mid-typing; cards and list rows work from the keyboard.
- Honest test-run wording, clearer delay action help, and a note in the
  catalog when a build can't log in to Twitch.
UI
- Redraws morph the live DOM instead of replacing innerHTML, so focus,
  carets, scroll and transitions survive. Keyed items slide to their new
  place, fade in and fade out; panels animate their height.
- One modal shell that swaps content in place, closes with an animation,
  traps focus and gives it back.
- Unsaved changes show a bar (Discard / Keep editing / Save) instead of
  confirm(); destructive buttons confirm on a second click.
- Save is lit only with changes; Ctrl+S saves; spinners on every button
  that waits on the app; the toggle flips at once and rolls back on error.
- Cards flag "Failing", "Finish setup", "Needs a fix" and "Needs Twitch"
  from the new /integrations issues map; per-module icons; skeleton on
  first load; sliding pills on segmented controls; toggle pills for roles.
- Builder: steps joined by a spine, trigger tabs read "When the hold runs
  out", the insertion point can no longer drift into a hidden list.
- Late replies never draw over another modal, and saves only close the
  editor that started them.

Leaks
- Engine prunes per-viewer and per-trigger cooldowns, flap records and
  stats of deleted integrations every minute.
- Programs started by integrations are reaped without a thread each and
  capped at 16 running.
- Failed step details lose URL paths and queries (webhook tokens).
- Recipes never carry web addresses, headers or file and program paths.
System > Twitch & OBS gets a "Twitch login app" section: paste the Client
ID of your own Twitch app and Integrations log in through it instead of
the one built into releases. The five steps to make one are right there,
with the redirect URL to copy and the one setting that matters (Public).

- New `twitch_client_id` setting, saved with the System save bar and
  checked before saving (letters and digits only, so a stray space or a
  pasted secret never reaches Twitch). A settings reset keeps it.
- The Twitch client id is switchable at run time. A real change cancels a
  login in progress and logs both accounts out, revoking them with the
  app that made them, then tells the user to connect again. A token
  refresh that finishes after the switch is dropped instead of bringing
  the old login back.
- Precedence: your own app, then INSTANTCLONE_TWITCH_CLIENT_ID, then the
  release's built-in app.
- A copy with no app at all opens the section by itself, and Integrations
  > Connections > Twitch links straight to it instead of a dead end.
…en secrets

API commands
- "Try it" under a selected web request: values for the variables it
  uses, Send request (new POST /integrations/fetch, same code path as a
  step), and the answer as a tree. Click a value, then "Reply in chat with
  it" fills the chat step after the request (or adds one). The real
  answer's fields join "Variables here" with their real values.
- Security: values inserted into a request are escaped for where they
  land. URL values are percent-encoded (a viewer can't add parameters or
  change the path), JSON bodies get JSON-string escaping, and headers lose
  control characters (no injected header lines). Covered by tests.

UX
- Card and row menu (Edit, Duplicate, Share as a recipe, Delete), so
  deleting no longer means opening the integration first.
- Undo toasts after adding and deleting; a deleted integration comes back
  in its old place.
- Back button for views opened from another: the catalog (from Build your
  own, Import, or a module that needs a detail, where Back also removes
  the just-added draft), the simple editor (from Open in builder, keeping
  the edits) and the editor that opened Connections.
- Recent activity rows open their integration.

Privacy
- Phone topic, Discord webhook link, request headers and the secret web
  call address are masked until Show, and hide again when the modal
  closes. Connection pills say "connected" instead of the topic or login.
  Addresses in cards, chips and steps show only scheme and host.
- A new integration (and "Another trigger") opens on "How does it
  start?": five big tiles with what each kind is and an example, instead
  of a dropdown that quietly preselected "OBS crashes". Saving or testing
  before picking shows the question again.
- "Something happens" continues to "What happens?": every event as a
  tile with an icon and one line on what it means, grouped like before.
  An existing event trigger shows its event as a card with Change, which
  opens the same tiles and can go back without changing anything.
- The inspector's trigger kind is tiles too; picking a chat command or a
  timer focuses its first field with the placeholder selected.
- Variable chips show a card on hover or focus: what the variable holds,
  an example value and "Click to insert". Descriptions cover every
  built-in variable plus web answers, counters and remembered values.
- Secret fields decode letter by letter when shown: each character
  scrambles briefly in the accent colour, then settles, one after another;
  hiding folds them back into dots from the end. Drawn on a layer over the
  field with one animation frame loop that writes only what changed;
  typing ends it at once. A new random ntfy topic decodes in the same way.
- The Show/Hide eye swaps icons with the scale, opacity and blur cross-fade.
- Counts and chip values roll in when they change (up when a number grew,
  down when it shrank), from morph's text patch, on data-tick elements only.
- Undo toasts drain a thin countdown bar; pointing at one holds both the
  bar and the timer.
- Save shows "Saved" with its check popping in for a beat before the
  editor closes.
- A card that switches on gives one soft pulse in its own colour.
- Menu items, test run rows and a request's answer tree ease in, staggered;
  a new answer is a new tree, so it eases in again.
- All of it is skipped under prefers-reduced-motion.
- Catalog module "Command from a website": a chat command that fetches an
  address and answers with what came back, or a fallback line if the site
  doesn't answer. It opens on its address chip until one is set.
- Paste a command from Nightbot ($(urlfetch)), Fossabot ($(customapi)),
  StreamElements (${customapi.}) or Streamlabs ({readapi.}) into the
  address: the new botcmd module (POST /integrations/convert, with tests)
  returns the address, the reply around the answer and the command name,
  translating their variables ($(user), ${1}, ${1:}, $(touser),
  $(querystring), escape helpers). $(eval) is refused with the reason;
  variables with no match stay as written and are listed.
- "Try it" moved out of the builder into shared functions, so the simple
  editor has it too: send the request, click a value in the answer, and
  "Use in the reply" swaps it in for the whole answer.
- New chat variable {target} (the name after the command without @, or
  the viewer) and {arg4} to {arg9}. The chat variables now live in one
  list, runner::CHAT_VARS, used by previews, validation and the catalog.
- Discord steps can edit the last message they posted (PATCH, falling
  back to a new one when it was deleted). The crash alert is now one
  message: "dropped" with a live <t:{hold_ends_at}:R> countdown, edited
  to "back" or "ended".
- New trigger: a global hotkey or a MIDI pad. Registered by the tray next
  to the delay actions (never one of their keys), pads forwarded by the
  MIDI listener, learned from the dashboard. New "Clip button" preset.
- New step "Show on stream": cards on a public /alerts browser source,
  queued, with a restart-safe feed. Tests never put anything on stream.
- New step "Edit text": first line, between, replace, cut, round, group
  digits, upper/lower, random pick.
- Ready-made website commands (!uptime, !accountage, !followers, !game)
  that answer in the editor right away; previews follow checks.
- Quiet hours per integration, a persisted {uses} counter, and each
  card's last runs as dots. Recipes leave hotkeys and pads out.
Security:
- Refuse non-public routes whose Host is a domain when no password is
  set (DNS rebinding could save an integration that runs a program).
- Access-Control-Allow-Origin only on public routes and /state.
- Program arguments are split before chat values are filled in.
- Recipes drop request bodies and program arguments; import cleans
  the same way export does.
- HTTP steps no longer follow redirects; dot segments are escaped.

Fixes:
- Twitch upkeep panicked in the first hour after boot (Instant math).
- Twitch HTTP timeouts, refresh races with logout and re-login, live
  token for chat reconnects, per-run chat state, bot stops on revoke.
- Delay bindings can't take an integration's key; toggle checks keys;
  refused integration hotkeys show on cards; MIDI knobs fire once.
- Duplicate starts off with a fresh web call token and no shortcut.
- Cooldowns count only started runs; busy runs don't reset failures.
- Ping kept on Discord repost; uses flushed on exit; durable store.
- wait_delay underflow, 501-char chat replies, URL credentials, counter
  cap, timer re-enable, webhook mask, Linux dead-code build.
- Phone layouts for the editor and builder, top-anchored modals, Esc
  closes an open chip first, menu resize crash.
…y deck

- Chat steps wait for Twitch's answer (USERSTATE or a msg_* NOTICE), so
  a message refused for slow mode, a duplicate or a ban fails its step
  with Twitch's reason instead of passing.
- The flap guard keeps each destination's newest held-back event and
  sends it once the window ends if it changes what was last reported.
- The MIDI listener also opens the devices integration pads were
  learned on, and every device during an integration learn; presses
  from those devices never reach a delay action.
- The Controls MIDI learn refuses a pad an integration uses and says
  which; pad conflicts are checked by overlap (any-device bindings
  included) through model::shortcut_owner and pads_overlap.
Catalog rows sized to their tiles (auto rows shrank to share the grid's
height and cut off the buttons), Add tinted in each tile's colour
instead of solid cyan, descriptions clamped to three lines, and shorter
copy for Command from a website, Clip button and the Twitch note.
…l label

- Events for the delay's whole life: armed, ready, cancelled (on top of
  on, changed and off), from Controller::phase each engine tick.
- New "Delay status" alert: one Discord message that follows them all.
- Delay steps can disarm; a delay changed by an integration toasts as
  "Integration", not "Hotkey".
- Builder: a trigger's switch and Remove sit under its name (the confirm
  no longer overflows), alike triggers are numbered, integration-wide
  settings are grouped apart, every block says what it does, and an
  empty trigger says how to start.
- Simple editor: many moments wrap as chips that keep their switches.
- Segmented controls wrap with a row-aware indicator; message boxes and
  indicators follow window resizes.
- Integrations are labelled experimental in the tab and the changelog.
…iew fixes

Stream path:
- Stopping in OBS airs the rest of the delay, then ends (also an
  unprotected crash); End now and Cut delay cut it short; restarting OBS
  mid-tail reconnects at the delay. Supervisor waits for the tail.
- OBS ends Enhanced Broadcasting streams with SequenceEnd tags stamped 0:
  expand_ts pins steps back over 10 s instead of rewinding the timeline.
- After a freeze hold ended (End now or expiry), a later pump or cut never
  seeds on a pre-freeze keyframe (fresh_from_seq), so nothing re-airs.
- A 9:16 destination gets a grace period before teardown and is never torn
  down during a hold, so OBS coming back doesn't drop it.
- Egress backoff resets after a clean session of 5 s or more; one that
  ends at once backs off instead of redialling every second. Switching
  protection off clears the freeze latch.

Security:
- Program and File steps can only be saved, switched on or imported from
  the streaming PC itself, or with a dashboard password (UI shows them as
  "Streaming PC only" with the reason).
- Chat values in program arguments keep only letters, digits, spaces and
  _ . , : # + = / -, collapse "..", and can't start with an option.
- Web request addresses are masked like webhook links.

Integrations:
- Import preview lists each trigger with who can fire it, and what it
  reaches (programs, files, delay changes from anyone in chat in red).
- !setdelay takes whole seconds only (new "is a whole number" check);
  0 turns the delay off.
- Shared Chat partners' messages are ignored unless the new Connections >
  Twitch option is on.
- Dock's dead Discord webhook row now opens the Integrations tab
  (/#integrations); End now answers 409 when nothing is left to end.
- Message boxes keep their height across redraws; busy buttons stay
  disabled when a redraw asked for it.

Also: 10 streamer-profile pump sims, stream-day session tests, issue form
for integrations, experimental note with a Report it link, CHANGELOG and
README for 0.1.15, version 0.1.15. 843 tests.
Each integration in a recipe being imported gets two folds: "See every
step" lists its triggers and steps in the builder's words (checks with
their branches nested, risky steps tinted), and "Show the raw settings"
shows the exact JSON that would be added. Steps whose program, file,
address or OBS scene a recipe can't carry say the importer picks it.
The disarm action now reads "Turn the delay off", which is what it does
on air too.
Each integration of a recipe is a card whose edge shows the worst it can
do, easing in after the one above. "See every step" and "Raw settings"
are rows that open smoothly (interpolate-size, plain open elsewhere),
remember their state across redraws, and count the steps. Steps get
numbered badges tinted by risk, triggers a bolt, the raw settings colored
JSON with a Copy button. Reduced motion turns the animations off.
…box, UI edges)

Stream path:
- The never-re-air floor after a freeze only applies when the hold was
  already over (ended or run out) before OBS recovered, or protection was
  switched off mid-freeze. A short freeze OBS recovered from in time no
  longer makes a new destination wait out the whole delay (new sim).
- On a full ring after a freeze, a destination waits for the ring to roll
  past the freeze instead of joining far short of the delay and looping.

Security:
- Program and File steps need a dashboard password while the dashboard is
  open to the network or reached through a reverse proxy (X-Forwarded-For,
  Forwarded, Via, X-Real-IP); a page loaded from this PC is no longer
  enough. Catalog entries with a File step follow the same rule. A proxied
  request can't set the first password either.
- Overlay pages are served with CSP sandbox allow-scripts: they still
  paint from the open feeds but can't act as the dashboard.
- Chat values in program args are ASCII only (no best-fit look-alikes),
  lose : and #, and no word of them can start with - or /.
- !setdelay with a number too big to read caps at 600 s.
- Logins in web addresses no longer show in step summaries.

UI:
- A secret field's label focuses the field instead of revealing it.
- Save settles back to disabled after a builder save.
- Locked integrations: the card switch, Ctrl+S and the unsaved bar say why
  instead of doing nothing; the footer explains on tap.
- Delay action labels match the builder (Turn the delay off, Back to live
  now); dock and banner say the same thing about Normal Mode; capped
  stagger delays; Copy no longer covers the raw settings; #integrations
  is cleared after opening the tab; Needs OBS setup fits the card.
Security:
- Every web request InstantClone makes carries Via: 1.1 instantclone, so
  a step aimed at the dashboard itself counts as proxied: it can't set
  the first password or a Run a program step (tested end to end).
- Setting up local steps, and the first password, also need the Host to
  name loopback: a port forwarder on this PC (netsh portproxy, ssh -R)
  connects from loopback but keeps the dialled address.
- The Studio bake escapes every < in its stylesheet (\3c) and the
  data-kind attribute, and drops widgets of unknown kinds, so an overlay
  file planted in the folder can't break out of <style> and run on the
  dashboard's origin. The ten built-in overlays bake unchanged.
- program_arg keeps a leading - only on a real negative number (-5x goes).
- maskUrl hides a login up to the last @ (user:p@ss@host).

Stream path:
- The replay floor is also set when the delay has moved past everything
  sent before the freeze: a freeze longer than the delay that OBS
  recovers from in the hold no longer replays its last GOP to a
  reconnecting destination and sits silent across the gap (new sim; it
  fails without the fix).
- Switching protection off mid-freeze ends the hold, as an OBS drop
  without protection would, instead of leaving the screen up.
The stagger animation ended at opacity 1, which overrode a switched-off
destination's dim look until it finished, so the card flashed bright for
a moment and then snapped grey. It now fades to each card's own opacity.
Comment thread src/crypto.rs Dismissed
Comment thread src/crypto.rs Dismissed
Comment thread src/crypto.rs Dismissed
Comment thread src/integrations/obsws.rs
Comment thread src/integrations/obsws.rs
Comment thread src/integrations/obsws.rs Dismissed
Comment thread src/integrations/obsws.rs
Comment thread src/integrations/obsws.rs
Comment thread src/integrations/obsws.rs
Comment thread src/integrations/recipe.rs Dismissed
@Soulhackzlol
Soulhackzlol merged commit 0d2d862 into main Oct 4, 2026
5 of 6 checks passed
@Soulhackzlol
Soulhackzlol deleted the feature/integrations branch October 4, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants