Skip to content

Commit 79f4cd4

Browse files
feat: implement startup gate in main.dart to ensure backend readiness before app launch
1 parent 22a9556 commit 79f4cd4

1 file changed

Lines changed: 171 additions & 15 deletions

File tree

dashboard/lib/main.dart

Lines changed: 171 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,181 @@ import 'package:sentracore_dashboard/providers/settings_provider.dart';
77
import 'package:sentracore_dashboard/navigation/dashboard_navigation.dart';
88
import 'package:sentracore_dashboard/screens/dashboard_screen.dart';
99
import 'package:sentracore_dashboard/services/desktop_notification_service.dart';
10+
import 'package:sentracore_dashboard/services/engine_bundled_launcher.dart';
1011
import 'package:sentracore_dashboard/theme/app_theme.dart';
1112

1213
Future<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

31187
class SentraCoreApp extends StatelessWidget {

0 commit comments

Comments
 (0)