Add per-room callback subscriptions for speaker changes
Goal Allow each meeting room to have multiple independently configured HTTP callback destinations in cloud-router, so Kaukus and an external partner both receive speaker changes while the partner receives no uploads or unrelated events. Implementation authorized by the user; follow the implementation handoff and acceptance criteria below. Proposed behavior An authorized room manager configures a list of destinations through the router API and dashboard room settings. Each destination has an endpoint, authentication secret, enabled state and explicit event subscriptions. Fan out matching events independently; a failed destination must not block room control or delivery to other destinations. Confirmed speaker scope is actual speaker starts, stops and replacements regardless of origin: physical microphone changes, router dashboard/API actions and future sources. Do not restrict events to microphone hardware or require a seat mapping. Queue edits alone are outside the initial scope. Use a shared transition boundary so future sources inherit callback behavior. Supply stable room/meeting/speaker identifiers, occurrence time and relevant speaker details without unrelated upload data. Keep callback credentials cloud-side, redact them from reads/logs and preserve stored secrets when editing other fields. Repository evidence / reuse - cloud-router/src/callback.rs provides the existing reqwest/Bearer JSON-envelope transport: one CALLBACK_ENDPOINT/CALLBACK_TOKEN, fire-and-forget POST, 10-second timeout, no retries. - handlers/meeting.rs emits meeting_created, meeting_started and meeting_ended; event_sub.rs emits uploaded and materializes SpeakerStartedExternally. No speaker callbacks currently exist. - handlers/speaker.rs confirms API-driven transitions after acknowledgements and auto-stops previous speakers. status_sync.rs detects external microphone stops. recovery.rs changes orphaned pending state. Timeout/late acknowledgement and repeated status paths must not produce false or duplicate events. - db/speaker.rs and RoomStatus distinguish microphone and seatless overlay speakers; both can coexist. Do not assume a single universal active speaker. - Extend SQLite migrations/database methods, RequireRoomsManage and room/municipality authorization, RoomDetailView.vue and cloud-dashboard/src/api.js. Expose callback settings separately from the existing room municipality update semantics so callback edits cannot detach tenant ownership. - Consolidate org callback metadata duplicated between handlers/org.rs and event_sub.rs. Reuse one callback envelope/dispatcher and transition path rather than separate partner transport or sends scattered across handlers/subscribers. Acceptance criteria 1. Per-room multiple-destination settings persist across restart; tenant-scoped authorization applies; credentials cannot be read back or leaked. Validate subscription types, destination count/input limits and endpoint URLs; define outbound destination/redirect policy to prevent SSRF and credential forwarding. 2. Two matching destinations receive the same speaker change; a speaker-only destination receives no meeting/upload events; disabled/nonmatching destinations and other rooms receive nothing. 3. Confirmed API and physical microphone starts/stops/replacements use one coherent event contract. Rejected or merely pending commands do not announce success. Handle duplicate acknowledgements/events/status, automatic previous-speaker stops, meeting teardown, router restart/reconciliation and seatless overlay coexistence according to the agreed contract. 4. Delivery is best effort: one bounded POST per matching destination, no retries, durable queue or restart replay, and no guaranteed arrival ordering. Failures/timeouts are isolated and diagnosable without credentials or payload leakage. Room control never waits for external HTTP delivery. 5. Migrate the existing global CALLBACK_ENDPOINT/TOKEN destination into explicit per-room entries subscribed to its existing meeting/upload events. Preserve existing envelopes, avoid duplicate entries/sends on repeated migration, and remove global fallback delivery after migration. Newly created rooms use explicit settings rather than implicit inheritance. Partner entries remain speaker-only. 6. OpenAPI/configuration docs, API/DB/transport regressions and dashboard validation pass; browser verification covers destination management and failure states. Run Rust fmt/check/test/clippy for affected crates and workspace checks where practical, plus dashboard build and available checks. No new dependency without approval. Refactor scope A focused router callback dispatcher and shared speaker-transition integration are expected; avoid a general event-bus rewrite. Durable delivery storage, retry workers and replay are out of scope for this best-effort iteration. Protocol changes only if existing acknowledgement/event/status signals cannot establish agreed speaker semantics. Confirmed planning decisions - Speaker callbacks cover actual speaker transitions from microphones, router dashboard/API and future sources, rather than being hardware-only. Queue-only edits are not part of this iteration. - Best-effort delivery is sufficient for now; no durable retry/replay infrastructure. - Replace the global destination with explicit per-room entries through an idempotent migration, not a permanent global fallback. - Implementation started after user authorization; preserve best-effort delivery and explicit per-room migration decisions. Implementation handoff (investigated HEAD 4b6121be541db91b5a81ca83dffbce31d05bd511; clean working tree) Execution findings - AckRouter::spawn_subscriber only routes to transient HTTP waiters; late acks have no entity target and are discarded. command_log stores meeting_id/type but no speaker_id. create_speaker, stop_speaker and stop_speaker_internal mutate DB only inside their HTTP acknowledgement branches. - status_sync tracks only active_speaker disappearance, seeds its first snapshot silently, ignores overlay_speaker and does not confirm pending starts. RoomStatus already supplies generated_at, active_meeting, active_speaker/name/seat and overlay_speaker/name. Reuse these; no initial NATS protocol change is necessary. - local-streamer commands::start_speaker has separate microphone/camera and seatless-overlay lanes. commands::bosch_chair_start_speaker emits SpeakerStartedExternally; bosch_self_stop_speaker signals status. Chair priority interjections intentionally do not replace the logical current speaker; retain that behavior. StopCase does not end speakers. StopMeeting clears both lanes, but cloud db::mark_meeting_stopped currently does not close speaker rows. - db::mark_speaker_stopped unconditionally overwrites timestamps; mark_speaker_active reports repeated transitions as not-found. Replace lifecycle internals with conditional transactional outcomes, not callback calls after unconditional writes. Preserve genuine missing-row errors separately from unchanged outcomes. - Schema currently ends at migration 22. db::with_conn serializes blocking SQLite operations. tests reside in src module tests, not cloud-router/tests. Existing migration idempotence test only checks version 1 and needs real reopen/upgrade coverage. - Production/dev compose do not forward CALLBACK_ENDPOINT/TOKEN today. cloud-dashboard has build/dev/preview scripts only, no test/lint runner. Existing reqwest 0.13 supports retry::never, redirect::Policy::none, no_proxy and resolve_to_addrs; tokio full, tower, axum and reqwest are already dependencies. Proposed contracts (new symbols/routes/schema, not existing APIs) 1. Closed CallbackEvent enum: meeting_created, meeting_started, meeting_ended, uploaded, speaker_started, speaker_stopped. Replacement emits stopped(old) then started(new), never a third duplicate replacement event. Both microphone/camera and seatless overlay starts count; speaker payload metadata includes lane (microphone or overlay), meeting_room_id, meeting_id, speaker_id, name, optional seat_id/subtitle, started_at/confirmed_at/stopped_at, occurred_at and observation source. Top-level event/stream_name/timestamp/metadata remains unchanged for legacy events. Build one payload per transition and clone it for all matching destinations. Queue-only, pause, case and mere display-name edits emit no speaker transitions. Status-derived occurrence times are observations, not fabricated exact physical-button times. 2. Proposed GET/PUT /api/rooms/{meeting_room_id}/callbacks. Both require rooms.manage plus municipality authorization; use separate settings, never PUT room municipality semantics. GET returns revision and destination summaries (id, label, endpoint, enabled, events, has_token), never tokens. PUT replaces the list atomically with expected revision; stale writes return 409. Existing IDs must belong to that room; omitted entries delete; absent/null token preserves an existing token, nonempty token replaces it, empty is invalid; new entries require token. Changing endpoint origin requires a replacement token. Suggested limits: 8 destinations, 100-character labels, 2048-byte URLs, 4096-byte tokens, 32 KiB body, nonempty known subscriptions, no duplicate IDs. Proposed db/callback.rs tables store room FK/cascade, list revision and destination records; secrets are recoverable bearer credentials in the protected SQLite file, not API-key hashes. Do not derive Debug or Serialize on internal secret-bearing types. Protect DB/backups accordingly. 3. Proposed CallbackDispatcher replaces optional global CallbackClient. Resolve/filter destinations inside detached best-effort work; bounded in-flight concurrency (e.g. 64 try-acquired permits), immediate observable drop on saturation, no unbounded task backlog, no retries/queue/replay. Include DNS and connect in the 10-second total attempt deadline; disable reqwest automatic retries, redirects and ambient proxy use. Log only destination ID, room/event and bounded failure class/status, not endpoint/token/payload/response bodies/raw reqwest errors. A settings edit affects subsequent snapshots; already dispatched requests may finish after disable/delete. 4. Outbound safety: HTTPS and public global-unicast targets by default; reject URL userinfo/fragments and invalid schemes/ports. Resolve each send, reject nonpublic IPv4/IPv6 (including mapped IPv4 and mixed public/private answers), and pin vetted addresses with reqwest resolve_to_addrs so validation cannot race a second DNS lookup; preserve host/TLS verification. Proposed operator-only CALLBACK_ALLOWED_PRIVATE_ORIGINS exact-origin list permits deliberately configured internal/dev targets, including HTTP where explicitly listed; never tenant-configurable and never an unrestricted development bypass. Use the same policy at write/import and send time; DNS failures are bounded/sanitized. Unit-test address classification and injected resolver answers, not live Internet DNS. Ordered implementation approach A. Define event/settings/payload types and add DB migration(s) after 22: callback settings/destinations, one-time import receipt, and nullable speaker_id target on command_log. Extend DB helpers with transactional list replacement/revision checks, FK cleanup and internal matching reads. Add focused db/callback.rs tests and file-backed reopen/upgrade/rollback tests. Use std temp paths + UUID if no existing temp helper; no new dependency. B. Add dispatcher transport, safe endpoint policy and shared DB-based organization metadata lookup (consolidate handlers/org.rs::org_callback_metadata and event_sub.rs::fetch_org_for_callback). Test exact body/Bearer header, fan-out/filtering, identical event snapshots, failures/timeouts/no retry/no redirect, DNS pinning, saturation and secret-safe errors with local Axum receivers and injected policy/resolver. Test-only injection must not enable production bypasses. C. Add proposed speaker_lifecycle.rs service backed by conditional DB transitions. Pending insertion/command-target logging must commit before NATS publish. Ack subscriber observes speaker-command acknowledgements using command_log.speaker_id before routing the waiter; handlers call the same idempotent service or only read the resulting state, never independently send. Persist exact start/stop target, room and meeting validation; process late acks even without a waiter; do not alter unrelated command semantics. Timeout log updates cannot overwrite already applied/rejected outcomes. Active->stopped emits once; pending->active emits once; unconfirmed pending cleanup/rejection emits neither. Stopped rows never reactivate. Same-lane replacement closes old confirmed speaker(s), never the other lane. Commit before dispatch; no network under DB lock. DB/ack/handler tests must race duplicate Applied, timeout/cancellation, status and external-event paths. D. Extend event_sub to validate subject/envelope/meeting-room identity and live meeting, upsert external starts idempotently through lifecycle service, and use shared dispatcher for uploaded. Extend status_sync into testable per-status reconciliation for both lanes and known pending starts; materialize unknown observed active IDs only with a validated matching live meeting and sufficient room details, so direct/future sources are not ignored. Do not overwrite existing richer metadata from a snapshot. Use per-room observation freshness (room-generated timestamps, also on acks/events) to reject stale observations; reconcile matching speaker IDs only, never stop a newly pending command because it is absent from an older snapshot. First startup snapshot reconciles silently, with no synthetic callback replay; live changes after baseline emit normally. Missing/offline status alone is never proof of stop. Guard against event-after-status resurrection and cross-room IDs. Preserve sync_output_bindings. Test chair takeover/self-stop, both lanes coexisting, first/repeated/stale snapshots, meeting changes, missing events and row failures. E. On confirmed meeting stop, close both speaker lanes via the same transition service and emit one stop per previously confirmed speaker; races with status are idempotent. Preserve existing special NoActiveMeeting/WrongMeeting cleanup responses but do not invent successful start events from rejected commands. Boot interruption/orphan cleanup reconciles silently; pending orphan cleanup never emits successful speaker lifecycle. Reconcile before orphan cleanup so an observed active pending speaker is not swept. Do not change case stop or queue semantics. Cover meeting teardown/recovery and state monotonicity in tests. F. Add authenticated settings handlers, main routing/state/subscriber wiring and OpenAPI. Prefer DB-only substate via Axum FromRef for settings handlers so API tests use in-memory DB and a local Axum server without a fake Publisher or new dependencies. Reuse existing permission extractors, tenant-not-found semantics and error envelope; factor shared DB-based room authorization rather than copying scope logic. Verify unauthorized/out-of-scope/readonly writes, secret redaction, token retention, origin-change protection, atomic rollback, revision conflicts and limits. G. Import global credentials once at startup after DB open and before subscribers/HTTP: valid configured pair -> transactionally create one explicit destination per existing room subscribed only to four legacy events and commit global import receipt. Missing pair -> no import and no receipt; partial/invalid configured pair -> clear startup error without secrets. Receipt prevents reimport even if entries are subsequently edited/deleted or env changes; new rooms stay empty. No fallback/global send path remains. Build dispatcher and finish import before event consumers. Leave variables available only as documented transitional import inputs; forward them through dev/prod compose for the upgrade, then operator removes after verified import. Test restart/env removal/credential changes/deleted entries/new room/empty DB and rollback; preserve legacy payloads from actual current handlers rather than stale doc examples. H. Proposed cloud-dashboard/src/components/CallbacksPanel.vue mounted in RoomDetailView setup, visible only to roomsManage. Reuse api.js request/ApiError, can permissions, OutputsPanel draft/modal/confirm-remove/toast conventions. Add list/create/edit/delete/enable/subscription controls; blank token means unchanged, never hydrate/log/persist token, clear drafts after success/cancel, preserve nonsecret draft on failure, reload on revision conflict. Works while room offline (cloud-only settings). No new frontend dependencies or invented test/lint commands. Browser tests cover two receivers, credential retention, subscriptions, disabled/deleted rows, validation/errors/conflicts, read-only users, reload persistence and offline-room settings. I. Update cloud-router/README.md, cloud-dashboard/README.md, docs/design/architecture.md and external-callback section of docs/design/nats-protocol.md; add migration/rollback/security/best-effort operational guidance and compose inputs. Preserve legacy event payload names exactly (current code uses ingest_base_url, while older docs mention rtmp_base_url). Snapshot/contract regressions cover all four legacy events and both new speaker events. Validate through independent local receivers and a NATS-driven speaker/meeting smoke flow; production callback receivers are not contacted during tests. Validation commands (implementation phase; not run during planning) - cargo fmt --all -- --check; cargo test -p cloud-router db::; cargo test -p cloud-router callback; cargo test -p cloud-router speaker_lifecycle; cargo test -p cloud-router ack_router; cargo test -p cloud-router status_sync; cargo test -p cloud-router event_sub; cargo test -p cloud-router handlers::callbacks (new modules/filters as proposed). - cargo check -p cloud-router --all-targets; cargo test -p cloud-router; cargo clippy -p cloud-router --all-targets -- -D warnings. - npm --prefix cloud-dashboard ci; npm --prefix cloud-dashboard run build. No dashboard lint/test script currently exists; real-browser validation required. - cargo check --workspace --all-targets; cargo test --workspace; cargo clippy --workspace --all-targets; git diff --check. Full workspace requires native GStreamer/private git dependency access; report baseline/environment failures rather than silently skipping tests. CI currently runs workspace check/test only despite older project notes mentioning fmt/clippy. Risks/operational boundaries Single cloud-router process remains the architecture. Best-effort crash between committed transition and detached send may lose notification, with no replay. HTTP arrival order is not guaranteed. Room timestamps can regress if its clock changes; test/drop stale observations and document recovery, do not infer stop from silence. Existing room-originated event channel is ephemeral; status repairs current state but cannot reconstruct an entire missed rapid speech history. Chair priority interjections remain outside logical speaker replacement. Existing global destination may require an explicitly allowed private origin; verify before deployment, never silently drop/import an unusable configuration. Back up protected SQLite before upgrade; rollback to an older binary would restore old global env behavior and ignore per-room subscriptions, so stop the new instance and explicitly reconcile configuration before rollback. No production credentials or existing deployment state were inspected. Implemented outcome and verification - Revisioned room destination lists, tenant-scoped rooms.manage API/dashboard, write-only retained/rotated Bearer tokens, explicit event filtering, protected outbound origins/DNS pinning, bounded best-effort fan-out and atomic one-time legacy import are implemented. No new dependencies. Internal CallbackClient name is retained as the shared dispatcher interface; it is no longer an optional global destination. - SQLite migrations 23/24 add callback settings/import receipt/command targets and persisted per-lane observation watermarks (not a delivery queue). Shared SpeakerLifecycle handles late acknowledgements, physical events, both status lanes, replacement and teardown. First status baseline remains silent; orphan cleanup cannot stop a concurrently confirmed speaker. - Review exposed ordering gaps in the original handoff assumptions. local-streamer now captures status generated_at with speaker fields under the meeting lock, and command/physical starts capture mutation time under that lock. Applied acknowledgements carry additive optional speaker_started_at, preserving legacy decoding; router uses mutation rather than delayed command-completion time. Deploy the matching local-streamer update before enabling callbacks. An older room's completion/snapshot timestamps cannot safely order all takeover races. Protocol/local-panel test constructors updated; no unrelated panel behavior changed. - DNS validation now permits retaining unchanged endpoints and disabling broken destinations without successful DNS; new/changed/enabled targets are resolved and every actual send is checked/pinned. Precise IPv4 CIDRs and adjacent public ranges are regression-tested. - Verified: cargo fmt --all -- --check, cargo check --workspace --all-targets, cargo test --workspace (404 passed, 0 failed, 3 existing ignored), cargo clippy --workspace --all-targets (passes with existing warnings), npm --prefix cloud-dashboard ci and run build, git diff --check. Router has 87 passing tests; local-streamer 73. Strict Clippy is blocked by existing result_large_err/collapsible_if and dependency doc_lazy_continuation/needless_borrow lints; scoped router --no-deps -D warnings passes with the two pre-existing router categories allowed. npm ci reports 9 existing lockfile dependency audit advisories (1 low, 1 moderate, 7 high); dependencies/lockfile were not changed or auto-upgraded. - Final disposable local NATS/HTTP smoke uses the built router and the additive acknowledgement field: migration/repeated restart/new-room isolation, API microphone and overlay starts, replacement, physical event/status self-stop, meeting teardown, legacy uploaded event and an HTTP503 partner. Both receivers get four starts/four stops; partner gets no meeting/upload events. Production endpoints never contacted. Native Bosch/GStreamer hardware and production rollout are not exercised. - Two Chrome sessions collectively verified create/edit/reload/disable/enable/confirmed delete, blank-token retention with actual receiver authentication, matching speaker payloads, speaker-only filtering, failing receiver isolation, validation, offline-room settings, read-only panel exclusion, actual backend409 recovery and injected settings503 recovery. Draft nonsecret fields survive errors, password clears, busy resets and retry succeeds. API regressions establish response-token exclusion; complete hidden-DOM inspection was not independently exhaustive. No component exceptions or functional browser defects found. - Static feature reviews found and verified five material corrections; fixes have regression coverage. The last mutation-time correction is tested but no subsequent independent review approval is claimed. Committed as 426e112 (feature: Add per-room speaker callback subscriptions) on feature/add-room-speaker-callbacks, based directly on current origin/main with no inherited commits. Branch pushed with upstream; working tree clean. Pull request #126: https://git.kaukus.no/Kaukus/streaming-engines/pulls/126 — head feature/add-room-speaker-callbacks, base main. All 40 intended session files included. Prior successful validation applies to the exact committed source; formatting and whitespace checks rerun during PR preparation. No production deployment or remote CI success claimed. Authorized follow-up corrections from PR self-review - On an actual room municipality change (including attach/detach), atomically delete callback destinations and advance the settings revision; unchanged assignments preserve settings. Verify new-tenant events cannot reach the former receiver and old editor revisions cannot restore removed destinations. Already dispatched pre-transfer requests may finish under the documented delivery contract. - Treat targeted speaker stops as per-speaker transitions, not whole-lane observations. Do not advance lane watermarks from stop command completion or reconstruct them from stopped_at after restart. Preserve actual start/status ordering and terminal speaker IDs. - Meeting teardown changes only its target meeting and speakers. WrongMeeting cleanup must not advance room-wide lane watermarks or suppress another meeting's observations. - Silent initial status suppresses historical replay only: speakers announced during the current process must still produce their matching stops even when stopped by the first status snapshot. - Add failing deterministic regressions before fixes, including restart reconstruction, two-meeting cleanup and silent historical startup control cases. Keep original completed checklist history; track these corrections separately. No new commit/push authorization in this follow-up request. Verified follow-up outcome - All four review findings are fixed in the working tree on feature/add-room-speaker-callbacks. Migration 25 installs an ownership-change trigger that deletes destinations/credentials and advances settings revisions atomically, including foreign-key detachment when a municipality is deleted. Same-owner assignments preserve configuration. Tests cover old-tenant delivery suppression, former-owner API exclusion, stale editor conflict, rollback and version-24 upgrade/reopen. - Targeted speaker stops and meeting cleanup no longer advance room-wide watermarks; restart hydration excludes stopped_at completion timestamps. Tests prove delayed physical starts survive both live processing and lifecycle restart, closed speaker IDs remain terminal, and WrongMeeting cleanup cannot suppress another meeting's microphone/overlay observations. - Per-room in-process announcement tracking preserves matching stops during the first status without emitting historical baseline stops. Mixed historical/live and both-lane cases are covered. - Red-green evidence: the initial reproductions failed in eight cases on the original implementation while the historical-silence control passed; an additional foreign-key detachment repro failed before the trigger fix. All 13 follow-up regression tests now pass. Router suite: 100 passed. Full workspace: 417 passed, 0 failed, 3 existing ignored. Workspace all-target check, normal Clippy (existing warnings), formatting and diff whitespace checks pass. Scoped router no-deps warnings-as-errors passes with only the two previously documented baseline lint categories allowed. - Follow-up patch-scoped static review found no further runtime findings and identified an asynchronous negative-assertion weakness. Added a test-only bounded permit-based dispatch drain, shared by HTTP ownership and lifecycle tests; silence/exact-count assertions now wait for detached deliveries without fixed sleeps. The final full-workspace suite passed after this test correction; the 13 focused tests were rerun successfully after resuming from the payload discussion. - Callback payload/API shape is unchanged by these corrections. Documentation now describes ownership invalidation, lane-order boundaries and historical-only startup silence. No new dependencies, frontend code changes, production/hardware activity or browser rerun in this follow-up. Earlier browser evidence applies to the unchanged UI, not a new browser acceptance claim. - User subsequently authorized push. Committed all 14 intended follow-up files as 2af44dd (fix: Isolate callback ownership and speaker reconciliation) and pushed to feature/add-room-speaker-callbacks, updating PR #126: https://git.kaukus.no/Kaukus/streaming-engines/pulls/126 . Working tree clean, upstream synchronized; no history rewrite. Existing full validation applies to the exact committed source; formatting/whitespace checks rerun during push preparation. Original completed checklist history is preserved. - Remote requested-changes review discovered during push preparation contains three additional findings outside this four-fix batch: stale dashboard responses across room navigation, stale starts leaving pending speaker rows, and lifecycle persistence errors suppressing acknowledgement routing. These have not been independently investigated or fixed in this push. PR comment explicitly records that limitation; no remote review comments resolved and no remote CI/production success claimed. Tracking status was not changed merely for the push.
Checklist
10 of 10 complete- Codify confirmed source-independent speaker scope, best-effort contract and explicit per-room migration; add event/payload contract tests.
- Verify persistent revisioned room callback settings/API, tenant authorization, secret-safe editing, URL/DNS/redirect safeguards and restart/rollback tests.
- Verify shared filtered dispatcher and idempotent speaker lifecycle across API/late acks, physical events, both status lanes, replacement, meeting teardown and silent recovery.
- Verify one-time atomic global callback import, preserved legacy envelopes, no reimport/duplicates/fallback and no inheritance for new rooms.
- Deliver room callback dashboard management and OpenAPI/configuration/security/migration documentation; verify offline editing and credential-retaining/conflict/error flows.
- Pass API/database/transport/lifecycle regressions, focused and workspace Rust checks, dashboard build, NATS smoke and real-browser two-receiver/failure-state validation; record limitations.
- Verify municipality transfers atomically invalidate callbacks/revisions and prevent former-tenant delivery while unchanged assignments preserve settings.
- Verify targeted stop completion cannot suppress delayed physical starts, including after restart; preserve terminal IDs and genuine status/start watermarks.
- Verify WrongMeeting/old-meeting teardown does not suppress another meeting's observations or stops.
- Verify first-status stops remain observable for speakers announced during this process, historical startup stays silent, and relevant regression/workspace validation passes.
- Area
- Streaming
- Project
- streaming-engines
- Kind
- Feature
- Task ID
tsk_eyng7n7agz- Updated