@@ -7,25 +7,181 @@ import 'package:sentracore_dashboard/providers/settings_provider.dart';
77import 'package:sentracore_dashboard/navigation/dashboard_navigation.dart' ;
88import 'package:sentracore_dashboard/screens/dashboard_screen.dart' ;
99import 'package:sentracore_dashboard/services/desktop_notification_service.dart' ;
10+ import 'package:sentracore_dashboard/services/engine_bundled_launcher.dart' ;
1011import 'package:sentracore_dashboard/theme/app_theme.dart' ;
1112
1213Future <void > main () async {
1314 WidgetsFlutterBinding .ensureInitialized ();
14- final notifications = DesktopNotificationService ();
15- await notifications.init (
16- onDidReceiveNotificationResponse: (response) {
17- if (response.notificationResponseType ==
18- NotificationResponseType .selectedNotification) {
19- DashboardNavigation .openAlertsFromNotification ();
20- }
21- },
22- );
23- final settings = SettingsProvider ();
24- await settings.load ();
25- runApp (SentraCoreApp (
26- settings: settings,
27- notifications: notifications,
28- ));
15+ runApp (const StartupGateApp ());
16+ }
17+
18+ class StartupGateApp extends StatefulWidget {
19+ const StartupGateApp ({super .key});
20+
21+ @override
22+ State <StartupGateApp > createState () => _StartupGateAppState ();
23+ }
24+
25+ class _StartupGateAppState extends State <StartupGateApp > {
26+ late Future <_BootResult > _boot;
27+
28+ @override
29+ void initState () {
30+ super .initState ();
31+ _boot = _bootstrap ();
32+ }
33+
34+ Future <_BootResult > _bootstrap () async {
35+ final notifications = DesktopNotificationService ();
36+ await notifications.init (
37+ onDidReceiveNotificationResponse: (response) {
38+ if (response.notificationResponseType ==
39+ NotificationResponseType .selectedNotification) {
40+ DashboardNavigation .openAlertsFromNotification ();
41+ }
42+ },
43+ );
44+ final settings = SettingsProvider ();
45+ await settings.load ();
46+
47+ // READY GATE (strict): do not enter the app until backend is healthy.
48+ final out = await EngineBundledLauncher .ensureReady (
49+ preferredPort: settings.lastEngineHttpPort,
50+ timeout: const Duration (seconds: 25 ),
51+ );
52+ return _BootResult (
53+ settings: settings, notifications: notifications, gate: out);
54+ }
55+
56+ @override
57+ Widget build (BuildContext context) {
58+ return FutureBuilder <_BootResult >(
59+ future: _boot,
60+ builder: (context, snap) {
61+ final theme = AppTheme .lightTheme;
62+ final dark = AppTheme .darkTheme;
63+ if (! snap.hasData) {
64+ return MaterialApp (
65+ debugShowCheckedModeBanner: false ,
66+ theme: theme,
67+ darkTheme: dark,
68+ home: const _StartupSplash (),
69+ );
70+ }
71+
72+ final data = snap.data! ;
73+ if (! data.gate.success) {
74+ return MaterialApp (
75+ debugShowCheckedModeBanner: false ,
76+ theme: theme,
77+ darkTheme: dark,
78+ home: _StartupError (
79+ message: data.gate.message ?? 'Backend failed to start.' ,
80+ onRetry: () => setState (() => _boot = _bootstrap ()),
81+ ),
82+ );
83+ }
84+
85+ return SentraCoreApp (
86+ settings: data.settings, notifications: data.notifications);
87+ },
88+ );
89+ }
90+ }
91+
92+ class _BootResult {
93+ final SettingsProvider settings;
94+ final DesktopNotificationService notifications;
95+ final EngineBootstrapOutcome gate;
96+ const _BootResult ({
97+ required this .settings,
98+ required this .notifications,
99+ required this .gate,
100+ });
101+ }
102+
103+ class _StartupSplash extends StatelessWidget {
104+ const _StartupSplash ();
105+ @override
106+ Widget build (BuildContext context) {
107+ return Scaffold (
108+ body: Center (
109+ child: Column (
110+ mainAxisSize: MainAxisSize .min,
111+ children: [
112+ Text (
113+ 'SentraCore' ,
114+ style: TextStyle (
115+ fontSize: 20 ,
116+ fontWeight: FontWeight .w800,
117+ color: AppTheme .textPrimaryFor (context),
118+ ),
119+ ),
120+ const SizedBox (height: 12 ),
121+ SizedBox (
122+ width: 220 ,
123+ child: LinearProgressIndicator (
124+ backgroundColor:
125+ Theme .of (context).dividerColor.withValues (alpha: 0.25 ),
126+ valueColor:
127+ const AlwaysStoppedAnimation <Color >(AppTheme .primary),
128+ ),
129+ ),
130+ const SizedBox (height: 10 ),
131+ Text (
132+ 'Starting backend…' ,
133+ style: TextStyle (
134+ color: AppTheme .textMutedFor (context), fontSize: 12 ),
135+ ),
136+ ],
137+ ),
138+ ),
139+ );
140+ }
141+ }
142+
143+ class _StartupError extends StatelessWidget {
144+ final String message;
145+ final VoidCallback onRetry;
146+ const _StartupError ({required this .message, required this .onRetry});
147+
148+ @override
149+ Widget build (BuildContext context) {
150+ return Scaffold (
151+ body: Center (
152+ child: Padding (
153+ padding: const EdgeInsets .all (24 ),
154+ child: ConstrainedBox (
155+ constraints: const BoxConstraints (maxWidth: 520 ),
156+ child: Column (
157+ mainAxisSize: MainAxisSize .min,
158+ crossAxisAlignment: CrossAxisAlignment .stretch,
159+ children: [
160+ Text (
161+ 'Backend failed to start' ,
162+ style: TextStyle (
163+ color: AppTheme .textPrimaryFor (context),
164+ fontSize: 18 ,
165+ fontWeight: FontWeight .w800,
166+ ),
167+ ),
168+ const SizedBox (height: 8 ),
169+ Text (
170+ message,
171+ style: TextStyle (color: AppTheme .textMutedFor (context)),
172+ ),
173+ const SizedBox (height: 16 ),
174+ FilledButton (
175+ onPressed: onRetry,
176+ child: const Text ('Retry' ),
177+ ),
178+ ],
179+ ),
180+ ),
181+ ),
182+ ),
183+ );
184+ }
29185}
30186
31187class SentraCoreApp extends StatelessWidget {
0 commit comments