diff --git a/Models/SessionStartDecision.cs b/Models/SessionStartDecision.cs
index 9498b39..20fabee 100644
--- a/Models/SessionStartDecision.cs
+++ b/Models/SessionStartDecision.cs
@@ -29,6 +29,24 @@ public static class SessionStartDecision
return editing ? SessionStartKind.Test : SessionStartKind.Live;
}
+ /// What pressing the button actually does, given whether YouTube is connected.
+ /// A signed-out ON-AIR press must start the SIGN-IN flow, not sit there disabled — a dead
+ /// primary button is the dead end the design principle forbids (creator, 2026-10-30).
+ public enum Route
+ {
+ SignIn,
+ Record,
+ Live,
+ Test,
+ }
+
+ public static Route RouteFor(SessionStartKind kind, bool connected) => kind switch
+ {
+ SessionStartKind.Record => Route.Record,
+ SessionStartKind.Test => connected ? Route.Test : Route.SignIn,
+ _ => connected ? Route.Live : Route.SignIn,
+ };
+
public static string LabelFor(SessionStartKind kind) => kind switch
{
SessionStartKind.Record => "Start Recording",
diff --git a/ViewModels/MainViewModel.Streaming.cs b/ViewModels/MainViewModel.Streaming.cs
index 4f4b5ae..11a5047 100644
--- a/ViewModels/MainViewModel.Streaming.cs
+++ b/ViewModels/MainViewModel.Streaming.cs
@@ -188,11 +188,30 @@ public partial class MainViewModel : ViewModelBase
public string PrimaryStartButtonLabel => SessionStartDecision.LabelFor(PrimaryStartKind);
- /// The button's action follows its label: Test runs the same go-live pipeline with
- /// `IsTestStream` set, Go Live runs it for real.
- public ICommand PrimaryStartCommand => PrimaryStartKind == SessionStartKind.Test
- ? StartTestCommand
- : StartStreamCommand;
+ private ICommand? _primaryStartCommand;
+
+ /// The button's action follows its label — and a signed-out ON-AIR press starts the
+ /// SIGN-IN flow instead of disabling the control (which is what binding straight to
+ /// did: its CanExecute needs a connection, so the button the
+ /// creator was told to press could not be pressed).
+ public ICommand PrimaryStartCommand =>
+ _primaryStartCommand ??= new RelayCommand(_ => StartFromPrimaryButton(), _ => ShowPrimaryStartButton);
+
+ private void StartFromPrimaryButton()
+ {
+ switch (SessionStartDecision.RouteFor(PrimaryStartKind, IsConnected))
+ {
+ case SessionStartDecision.Route.SignIn:
+ _ = SignInAsync();
+ return;
+ case SessionStartDecision.Route.Test:
+ BeginTestStream();
+ return;
+ default:
+ StartSession();
+ return;
+ }
+ }
/// The End button shows while recording OR live.
public bool ShowEndStreamButton => IsLive || IsRecording;
diff --git a/ytLive.Tests/SessionStartDecisionTests.cs b/ytLive.Tests/SessionStartDecisionTests.cs
index 2860f46..d77a484 100644
--- a/ytLive.Tests/SessionStartDecisionTests.cs
+++ b/ytLive.Tests/SessionStartDecisionTests.cs
@@ -26,6 +26,14 @@ public class SessionStartDecisionTests
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, true));
Assert.Equal("Start Recording", SessionStartDecision.LabelFor(SessionStartKind.Record));
+ // Signed out: an ON-AIR press (live OR test) routes to the sign-in flow, never a dead
+ // button. Recording never needs an account.
+ Assert.Equal(SessionStartDecision.Route.SignIn, SessionStartDecision.RouteFor(SessionStartKind.Live, connected: false));
+ Assert.Equal(SessionStartDecision.Route.SignIn, SessionStartDecision.RouteFor(SessionStartKind.Test, connected: false));
+ Assert.Equal(SessionStartDecision.Route.Live, SessionStartDecision.RouteFor(SessionStartKind.Live, connected: true));
+ Assert.Equal(SessionStartDecision.Route.Test, SessionStartDecision.RouteFor(SessionStartKind.Test, connected: true));
+ Assert.Equal(SessionStartDecision.Route.Record, SessionStartDecision.RouteFor(SessionStartKind.Record, connected: false));
+
// Unarmed start records locally (the no-dead-ends rule).
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false, false));
}