diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml
index 021b574..65d5a4c 100644
--- a/Controls/LeftPanel.xaml
+++ b/Controls/LeftPanel.xaml
@@ -460,6 +460,12 @@
+
+
diff --git a/Models/ScoreboardSample.cs b/Models/ScoreboardSample.cs
new file mode 100644
index 0000000..e2f45b2
--- /dev/null
+++ b/Models/ScoreboardSample.cs
@@ -0,0 +1,29 @@
+namespace ytLive.Models;
+
+/// Development-only sample data for the scoreboard.
+///
+/// Creator (2026-10-30): the strip must be provable in the PREVIEW before a real stream —
+/// "simulate the ticker so we can see it and ensure it's getting composited". Synthetic events go
+/// through the same a live stream uses, so what the preview shows
+/// is exactly what real events would produce; nothing here fakes the renderer.
+///
+/// Kept in the model (not the view-model) so the sample is testable without a window.
+public static class ScoreboardSample
+{
+ public static void Inject(ScoreboardRecorder recorder)
+ {
+ if (!recorder.IsRunning) recorder.StartSession();
+
+ recorder.Record(new ChatMessage { Kind = ChatEventKind.NewMember, AuthorName = "newviewer" });
+ recorder.Record(new ChatMessage
+ {
+ Kind = ChatEventKind.MemberGift, AuthorName = "giftgiver", GiftNewMembersCount = 3,
+ });
+ recorder.Record(new ChatMessage { Kind = ChatEventKind.MemberMilestone, AuthorName = "longtimer" });
+ recorder.Record(new ChatMessage
+ {
+ Kind = ChatEventKind.SuperChat, AuthorName = "tipper",
+ SuperChatAmount = 12.34, SuperChatCurrency = "USD", SuperChatDisplayString = "$12.34",
+ });
+ }
+}
diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs
index 30d0aba..3bedacb 100644
--- a/ViewModels/MainViewModel.Scoreboard.cs
+++ b/ViewModels/MainViewModel.Scoreboard.cs
@@ -45,6 +45,33 @@ public partial class MainViewModel
}
}
+#if DEBUG
+ private const bool DevelopmentBuild = true;
+#else
+ private const bool DevelopmentBuild = false;
+#endif
+
+ /// True in a DEBUG build. Used only to expose development-only controls, never to
+ /// change behaviour — the shipped binary differs by their absence, not by a code path.
+ public bool IsDevelopmentBuild => DevelopmentBuild;
+
+ private bool _scoreboardSimulate;
+
+ /// DEBUG-only: push a representative set of events so the strip is visible in the
+ /// preview before any real stream. Off clears back to an empty session.
+ public bool ScoreboardSimulate
+ {
+ get => _scoreboardSimulate;
+ set
+ {
+ if (!SetProperty(ref _scoreboardSimulate, value)) return;
+ if (value) ScoreboardSample.Inject(_scoreboard);
+ else _scoreboard.StartSession(); // empty session: "nothing yet"
+ RefreshScoreboardPreviews();
+ OnPropertyChanged(nameof(ScoreboardFrame));
+ }
+ }
+
/// What the cadence policy asks: the master switch AND at least one scene showing it.
internal bool ScoreboardPollsEnabled =>
_scoreboardEnabled && Scenes.Any(scene =>
diff --git a/ytLive.Tests/ScoreboardSimulationTests.cs b/ytLive.Tests/ScoreboardSimulationTests.cs
new file mode 100644
index 0000000..9a521a6
--- /dev/null
+++ b/ytLive.Tests/ScoreboardSimulationTests.cs
@@ -0,0 +1,46 @@
+using Xunit;
+using ytLive.Models;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+/// DEBUG-only affordance (creator 2026-10-30): push sample events so the strip is provable
+/// in the PREVIEW before a real stream. The sample must travel the real recorder, so what it paints
+/// is what live events paint — a fake renderer would prove nothing.
+public class ScoreboardSimulationTests
+{
+ [Fact]
+ public void The_dev_sample_feeds_the_real_recorder_and_the_toggle_is_debug_only()
+ {
+ // The sample goes through the recorder a stream uses, and lands all four tallies.
+ var recorder = new ScoreboardRecorder();
+ ScoreboardSample.Inject(recorder);
+
+ Assert.True(recorder.IsRunning);
+ var state = recorder.State;
+ Assert.Equal(4, state.NewMembers); // 1 paid + a gift of 3
+ Assert.Equal(3, state.LastGiftCount);
+ Assert.Equal("giftgiver", state.LastGifterName);
+ Assert.Equal(1, state.MilestoneCount);
+ Assert.Equal(12.34, state.SessionTotal.Total, 2);
+ Assert.Equal("$12.34", state.LastSuperChatDisplay);
+
+ // A toggle OFF clears back to an empty session (so the empty state is visible too).
+ recorder.StartSession();
+ Assert.Equal(0, recorder.State.NewMembers);
+
+ // The control exists and is gated on the build, not on a runtime flag.
+ var panel = SourceAssert.Text("Controls", "LeftPanel.xaml");
+ Assert.Contains("ScoreboardSimulate", panel);
+ Assert.Contains("IsDevelopmentBuild", panel);
+
+ var type = typeof(MainViewModel);
+ Assert.NotNull(type.GetProperty("ScoreboardSimulate"));
+ Assert.NotNull(type.GetProperty("IsDevelopmentBuild"));
+ // DEBUG-gated at compile time, so a Release build has no dev control at all.
+ var vmSource = SourceAssert.Text("ViewModels", "MainViewModel.Scoreboard.cs");
+ Assert.Contains("#if DEBUG", vmSource);
+ Assert.Contains("private const bool DevelopmentBuild = true;", vmSource);
+ Assert.Contains("private const bool DevelopmentBuild = false;", vmSource);
+ }
+}