aegis-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aegis-stack might be problematic. Click here for more details.

Files changed (103) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +374 -0
  3. aegis/core/CLAUDE.md +365 -0
  4. aegis/core/__init__.py +6 -0
  5. aegis/core/components.py +115 -0
  6. aegis/core/dependency_resolver.py +119 -0
  7. aegis/core/template_generator.py +163 -0
  8. aegis/templates/CLAUDE.md +306 -0
  9. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
  10. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
  11. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  12. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
  13. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
  14. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  15. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
  16. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  17. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  18. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  19. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
  20. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
  21. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
  22. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  23. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  24. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
  25. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
  26. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
  27. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
  28. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
  29. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
  30. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  31. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  32. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  33. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  34. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  35. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  100. aegis_stack-0.1.0.dist-info/METADATA +114 -0
  101. aegis_stack-0.1.0.dist-info/RECORD +103 -0
  102. aegis_stack-0.1.0.dist-info/WHEEL +4 -0
  103. aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,687 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable
3
+
4
+ from typing import Any
5
+
6
+ import flet as ft
7
+
8
+ from app.services.system import get_system_status
9
+ from app.services.system.ui import get_status_icon, get_component_label
10
+
11
+ from .core.theme import ThemeManager
12
+
13
+
14
+ def create_frontend_app() -> Callable[[ft.Page], Awaitable[None]]:
15
+ """Returns the Flet target function - system health dashboard"""
16
+
17
+ async def flet_main(page: ft.Page) -> None:
18
+ page.title = "Aegis Stack - System Dashboard"
19
+ page.padding = 20
20
+ page.scroll = ft.ScrollMode.AUTO
21
+
22
+ # Initialize theme system
23
+ theme_manager = ThemeManager(page)
24
+ await theme_manager.initialize_themes()
25
+
26
+ # Theme toggle button
27
+ theme_button = ft.IconButton(
28
+ icon=ft.Icons.DARK_MODE,
29
+ tooltip="Switch to Dark Mode",
30
+ icon_size=24,
31
+ )
32
+
33
+ async def toggle_theme(_: Any) -> None:
34
+ """Toggle theme and update button icon"""
35
+ await theme_manager.toggle_theme()
36
+ # Update button icon based on new theme
37
+ if theme_manager.is_dark_mode:
38
+ theme_button.icon = ft.Icons.LIGHT_MODE
39
+ theme_button.tooltip = "Switch to Light Mode"
40
+ else:
41
+ theme_button.icon = ft.Icons.DARK_MODE
42
+ theme_button.tooltip = "Switch to Dark Mode"
43
+ page.update()
44
+
45
+ theme_button.on_click = toggle_theme
46
+
47
+ # Dashboard header with theme switch
48
+ header = ft.Container(
49
+ content=ft.Row(
50
+ [
51
+ # Left side - title and subtitle
52
+ ft.Column(
53
+ [
54
+ ft.Text(
55
+ "🏛️ Aegis Stack",
56
+ size=36,
57
+ weight=ft.FontWeight.BOLD,
58
+ color=ft.Colors.PRIMARY,
59
+ ),
60
+ ft.Text(
61
+ "System Health Dashboard",
62
+ size=18,
63
+ color=ft.Colors.GREY_700,
64
+ ),
65
+ ],
66
+ spacing=5,
67
+ ),
68
+ # Center - status summary (will be updated)
69
+ ft.Container(
70
+ content=ft.Text(
71
+ "Loading...", size=16, color=ft.Colors.ON_SURFACE
72
+ ),
73
+ padding=15,
74
+ bgcolor=ft.Colors.SURFACE,
75
+ border_radius=8,
76
+ ),
77
+ # Right side - theme toggle
78
+ ft.Container(
79
+ content=theme_button,
80
+ padding=10,
81
+ ),
82
+ ],
83
+ alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
84
+ ),
85
+ margin=ft.margin.only(bottom=30),
86
+ )
87
+
88
+ # Create responsive grid layout containers
89
+ # Top row - System metrics
90
+ metrics_row = ft.Container(
91
+ content=ft.Row([], spacing=15),
92
+ margin=ft.margin.only(bottom=20),
93
+ )
94
+
95
+ # Middle row - Main components
96
+ components_row = ft.Container(
97
+ content=ft.Column(
98
+ [
99
+ ft.Text(
100
+ "🏗️ Infrastructure Components",
101
+ size=22,
102
+ weight=ft.FontWeight.BOLD,
103
+ color=ft.Colors.PRIMARY,
104
+ ),
105
+ ft.Row([], spacing=15, wrap=True),
106
+ ]
107
+ ),
108
+ margin=ft.margin.only(bottom=20),
109
+ )
110
+
111
+ # Bottom row - System info and queues
112
+ details_row = ft.Container(
113
+ content=ft.Row([], spacing=15),
114
+ margin=ft.margin.only(bottom=20),
115
+ )
116
+
117
+ # Add components to page with new horizontal layout
118
+ page.add(
119
+ header,
120
+ metrics_row,
121
+ components_row,
122
+ details_row,
123
+ )
124
+
125
+ async def refresh_status() -> None:
126
+ """Refresh system status data using Material Design color tokens"""
127
+ try:
128
+ status = await get_system_status()
129
+
130
+ # Get current theme state directly from page (most reliable)
131
+ is_light_mode = page.theme_mode == ft.ThemeMode.LIGHT
132
+
133
+ # Extract aegis component and its sub-components
134
+ aegis_component = None
135
+ if "aegis" in status.components:
136
+ aegis_component = status.components["aegis"]
137
+
138
+ if not aegis_component or not aegis_component.sub_components:
139
+ return
140
+
141
+ components = aegis_component.sub_components
142
+
143
+ # Update header status summary
144
+ status_color = (
145
+ ft.Colors.GREEN if status.overall_healthy else ft.Colors.ERROR
146
+ )
147
+ status_icon = "✅" if status.overall_healthy else "❌"
148
+ status_text = (
149
+ "System Healthy" if status.overall_healthy else "Issues Detected"
150
+ )
151
+
152
+ header.content.controls[1].content = ft.Column(
153
+ [
154
+ ft.Row(
155
+ [
156
+ ft.Text(status_icon, size=18),
157
+ ft.Text(
158
+ status_text,
159
+ size=16,
160
+ weight=ft.FontWeight.BOLD,
161
+ color=status_color,
162
+ ),
163
+ ],
164
+ spacing=8,
165
+ ),
166
+ ft.Text(
167
+ f"{status.health_percentage:.1f}% • "
168
+ f"{len(status.healthy_components)} healthy",
169
+ size=12,
170
+ color=ft.Colors.GREY_700,
171
+ ),
172
+ ft.Text(
173
+ f"Updated: {status.timestamp.strftime('%H:%M:%S')}",
174
+ size=10,
175
+ color=ft.Colors.GREY_700,
176
+ ),
177
+ ],
178
+ spacing=2,
179
+ )
180
+
181
+ # Update header status container background
182
+ header.content.controls[1].bgcolor = ft.Colors.SURFACE
183
+
184
+ # Create system metrics cards (horizontal row)
185
+ metrics_cards = []
186
+ backend_component = components.get("backend")
187
+ if backend_component and backend_component.sub_components:
188
+ for metric_name, metric in backend_component.sub_components.items():
189
+ if metric.healthy:
190
+ bg_color = (
191
+ ft.Colors.GREEN_100
192
+ if is_light_mode
193
+ else ft.Colors.GREEN_900
194
+ )
195
+ text_color = (
196
+ ft.Colors.GREEN_800
197
+ if is_light_mode
198
+ else ft.Colors.GREEN_100
199
+ )
200
+ border_color = ft.Colors.GREEN
201
+ else:
202
+ bg_color = (
203
+ ft.Colors.RED_100
204
+ if is_light_mode
205
+ else ft.Colors.RED_900
206
+ )
207
+ text_color = (
208
+ ft.Colors.RED_800
209
+ if is_light_mode
210
+ else ft.Colors.RED_100
211
+ )
212
+ border_color = ft.Colors.ERROR
213
+ icon = get_status_icon(metric.status)
214
+
215
+ # Extract percentage from message
216
+ percentage = "0%"
217
+ if "%" in metric.message:
218
+ percentage = metric.message.split(":")[1].strip()
219
+
220
+ metrics_cards.append(
221
+ ft.Container(
222
+ content=ft.Column(
223
+ [
224
+ ft.Row(
225
+ [
226
+ ft.Text(icon, size=14),
227
+ ft.Text(
228
+ metric_name.upper(),
229
+ size=12,
230
+ weight=ft.FontWeight.BOLD,
231
+ color=text_color,
232
+ ),
233
+ ],
234
+ alignment=ft.MainAxisAlignment.CENTER,
235
+ ),
236
+ ft.Text(
237
+ percentage,
238
+ size=20,
239
+ weight=ft.FontWeight.BOLD,
240
+ color=text_color,
241
+ ),
242
+ ft.Text(
243
+ metric.message.split(":")[0],
244
+ size=10,
245
+ color=text_color,
246
+ ),
247
+ ],
248
+ alignment=ft.MainAxisAlignment.CENTER,
249
+ spacing=4,
250
+ ),
251
+ padding=15,
252
+ bgcolor=bg_color,
253
+ border=ft.border.all(1, border_color),
254
+ border_radius=8,
255
+ width=120,
256
+ height=100,
257
+ )
258
+ )
259
+
260
+ metrics_row.content.controls = metrics_cards
261
+
262
+ # Create main component cards (horizontal grid)
263
+ component_cards = []
264
+
265
+ for comp_name, component in components.items():
266
+ # Show backend as a component card too, for consistency
267
+
268
+ if component.healthy:
269
+ bg_color = (
270
+ ft.Colors.GREEN_100
271
+ if is_light_mode
272
+ else ft.Colors.GREEN_900
273
+ )
274
+ text_color = (
275
+ ft.Colors.GREEN_800
276
+ if is_light_mode
277
+ else ft.Colors.GREEN_100
278
+ )
279
+ border_color = ft.Colors.GREEN
280
+ else:
281
+ bg_color = (
282
+ ft.Colors.RED_100 if is_light_mode else ft.Colors.RED_900
283
+ )
284
+ text_color = (
285
+ ft.Colors.RED_800 if is_light_mode else ft.Colors.RED_100
286
+ )
287
+ border_color = ft.Colors.ERROR
288
+ icon = get_status_icon(component.status)
289
+ tech_name = get_component_label(comp_name)
290
+
291
+ # Build card content
292
+ card_content = [
293
+ ft.Row(
294
+ [
295
+ ft.Text(icon, size=16),
296
+ ft.Column(
297
+ [
298
+ ft.Text(
299
+ comp_name.title(),
300
+ size=16,
301
+ weight=ft.FontWeight.BOLD,
302
+ color=text_color,
303
+ ),
304
+ ft.Text(
305
+ tech_name,
306
+ size=12,
307
+ color=text_color,
308
+ weight=ft.FontWeight.BOLD,
309
+ ),
310
+ ],
311
+ spacing=0,
312
+ ),
313
+ ],
314
+ spacing=10,
315
+ ),
316
+ ft.Text(
317
+ component.message[:50] + "..."
318
+ if len(component.message) > 50
319
+ else component.message,
320
+ size=11,
321
+ color=text_color,
322
+ ),
323
+ ]
324
+
325
+ # Add database-specific metadata display
326
+ if comp_name == "database" and component.metadata:
327
+ db_info = []
328
+
329
+ # Show SQLite version if available
330
+ if "version" in component.metadata:
331
+ db_info.append(f"SQLite v{component.metadata['version']}")
332
+
333
+ # Show file size if available
334
+ if "file_size_human" in component.metadata:
335
+ size = component.metadata['file_size_human']
336
+ db_info.append(f"Size: {size}")
337
+
338
+ # Show WAL status if available
339
+ if "wal_enabled" in component.metadata:
340
+ wal_enabled = component.metadata["wal_enabled"]
341
+ wal_status = "WAL" if wal_enabled else "DELETE"
342
+ db_info.append(f"Mode: {wal_status}")
343
+
344
+ # Show connection pool size if available
345
+ if "connection_pool_size" in component.metadata:
346
+ pool_size = component.metadata['connection_pool_size']
347
+ db_info.append(f"Pool: {pool_size}")
348
+
349
+ # Add database info to card if we have any
350
+ if db_info:
351
+ card_content.append(
352
+ ft.Container(
353
+ content=ft.Column(
354
+ [
355
+ ft.Text(
356
+ "Database Info:",
357
+ size=10,
358
+ weight=ft.FontWeight.BOLD,
359
+ color=text_color,
360
+ ),
361
+ ft.Text(
362
+ " • ".join(db_info),
363
+ size=10,
364
+ color=text_color,
365
+ ),
366
+ ],
367
+ spacing=2,
368
+ ),
369
+ margin=ft.margin.only(top=8),
370
+ )
371
+ )
372
+
373
+ # Add sub-component indicators
374
+ if component.sub_components:
375
+ sub_status = []
376
+ for sub_name, sub_comp in component.sub_components.items():
377
+ sub_icon = get_status_icon(sub_comp.status)
378
+ sub_status.append(f"{sub_icon} {sub_name}")
379
+
380
+ if (
381
+ comp_name == "worker"
382
+ and "queues" in component.sub_components
383
+ ):
384
+ # Special handling for worker queues
385
+ queues_comp = component.sub_components["queues"]
386
+ if queues_comp.sub_components:
387
+ queue_icons = [
388
+ get_status_icon(q.status)
389
+ for q in queues_comp.sub_components.values()
390
+ ]
391
+ card_content.append(
392
+ ft.Container(
393
+ content=ft.Row(
394
+ [
395
+ ft.Text(
396
+ "Queues:",
397
+ size=10,
398
+ weight=ft.FontWeight.BOLD,
399
+ color=text_color,
400
+ ),
401
+ ft.Text(" ".join(queue_icons), size=12),
402
+ ],
403
+ spacing=5,
404
+ ),
405
+ margin=ft.margin.only(top=8),
406
+ )
407
+ )
408
+ else:
409
+ # Show sub-components as compact indicators
410
+ if len(sub_status) <= 3:
411
+ card_content.append(
412
+ ft.Container(
413
+ content=ft.Text(
414
+ " | ".join(sub_status),
415
+ size=10,
416
+ color=text_color,
417
+ ),
418
+ margin=ft.margin.only(top=8),
419
+ )
420
+ )
421
+
422
+ component_cards.append(
423
+ ft.Container(
424
+ content=ft.Column(card_content, spacing=8),
425
+ padding=15,
426
+ bgcolor=bg_color,
427
+ border=ft.border.all(1, border_color),
428
+ border_radius=8,
429
+ width=240,
430
+ height=140,
431
+ )
432
+ )
433
+
434
+ components_row.content.controls[1].controls = component_cards
435
+
436
+ # Create bottom row with system info and detailed worker queues
437
+ bottom_cards = []
438
+
439
+ # System info card
440
+ info_bg_color = (
441
+ ft.Colors.BLUE_100 if is_light_mode else ft.Colors.BLUE_900
442
+ )
443
+ info_text_color = (
444
+ ft.Colors.BLUE_800 if is_light_mode else ft.Colors.BLUE_100
445
+ )
446
+
447
+ sys_info_content = [
448
+ ft.Text(
449
+ "System Info",
450
+ size=14,
451
+ weight=ft.FontWeight.BOLD,
452
+ color=info_text_color,
453
+ )
454
+ ]
455
+ if status.system_info:
456
+ for key, value in status.system_info.items():
457
+ sys_info_content.append(
458
+ ft.Text(
459
+ f"{key.replace('_', ' ').title()}: {value}",
460
+ size=11,
461
+ color=info_text_color,
462
+ )
463
+ )
464
+
465
+ bottom_cards.append(
466
+ ft.Container(
467
+ content=ft.Column(sys_info_content, spacing=4),
468
+ padding=15,
469
+ bgcolor=info_bg_color,
470
+ border=ft.border.all(1, ft.Colors.PRIMARY),
471
+ border_radius=8,
472
+ width=300,
473
+ )
474
+ )
475
+
476
+ # Worker queues detailed card
477
+ worker_comp = components.get("worker")
478
+ if (
479
+ worker_comp
480
+ and worker_comp.sub_components
481
+ and "queues" in worker_comp.sub_components
482
+ ):
483
+ queues_comp = worker_comp.sub_components["queues"]
484
+ queue_bg_color = (
485
+ ft.Colors.PURPLE_100 if is_light_mode else ft.Colors.PURPLE_900
486
+ )
487
+ queue_text_color = (
488
+ ft.Colors.PURPLE_800 if is_light_mode else ft.Colors.PURPLE_100
489
+ )
490
+
491
+ queue_content = [
492
+ ft.Text(
493
+ "Worker Queues",
494
+ size=14,
495
+ weight=ft.FontWeight.BOLD,
496
+ color=queue_text_color,
497
+ )
498
+ ]
499
+
500
+ if queues_comp.sub_components:
501
+ for queue_name, queue in queues_comp.sub_components.items():
502
+ icon = get_status_icon(queue.status)
503
+ # Extract job count from message
504
+ job_info = ""
505
+ if "completed" in queue.message:
506
+ job_info = queue.message.split(":")[-1].strip()
507
+
508
+ queue_content.append(
509
+ ft.Row(
510
+ [
511
+ ft.Text(icon, size=12),
512
+ ft.Text(
513
+ queue_name,
514
+ size=12,
515
+ weight=ft.FontWeight.BOLD,
516
+ color=queue_text_color,
517
+ ),
518
+ ft.Text(
519
+ job_info, size=10, color=queue_text_color
520
+ ),
521
+ ],
522
+ spacing=8,
523
+ )
524
+ )
525
+
526
+ bottom_cards.append(
527
+ ft.Container(
528
+ content=ft.Column(queue_content, spacing=6),
529
+ padding=15,
530
+ bgcolor=queue_bg_color,
531
+ border=ft.border.all(1, ft.Colors.PURPLE),
532
+ border_radius=8,
533
+ width=300,
534
+ )
535
+ )
536
+
537
+ # Database details card (if database component exists)
538
+ database_comp = components.get("database")
539
+ if database_comp and database_comp.metadata:
540
+ db_bg_color = (
541
+ ft.Colors.CYAN_100 if is_light_mode else ft.Colors.CYAN_900
542
+ )
543
+ db_text_color = (
544
+ ft.Colors.CYAN_800 if is_light_mode else ft.Colors.CYAN_100
545
+ )
546
+
547
+ db_content = [
548
+ ft.Text(
549
+ "Database Details",
550
+ size=14,
551
+ weight=ft.FontWeight.BOLD,
552
+ color=db_text_color,
553
+ )
554
+ ]
555
+
556
+ # Show detailed database metadata
557
+ metadata = database_comp.metadata
558
+
559
+ # Version and implementation
560
+ if "version" in metadata and "implementation" in metadata:
561
+ db_content.append(
562
+ ft.Text(
563
+ f"{metadata['implementation'].upper()} "
564
+ f"v{metadata['version']}",
565
+ size=12,
566
+ weight=ft.FontWeight.BOLD,
567
+ color=db_text_color,
568
+ )
569
+ )
570
+
571
+ # File info
572
+ if "file_size_human" in metadata and "file_size_bytes" in metadata:
573
+ db_content.append(
574
+ ft.Text(
575
+ f"File Size: {metadata['file_size_human']} "
576
+ f"({metadata['file_size_bytes']:,} bytes)",
577
+ size=11,
578
+ color=db_text_color,
579
+ )
580
+ )
581
+
582
+ # Connection info
583
+ if "connection_pool_size" in metadata:
584
+ db_content.append(
585
+ ft.Text(
586
+ f"Connection Pool: "
587
+ f"{metadata['connection_pool_size']} connections",
588
+ size=11,
589
+ color=db_text_color,
590
+ )
591
+ )
592
+
593
+ # SQLite PRAGMA settings
594
+ if "pragma_settings" in metadata:
595
+ pragma = metadata["pragma_settings"]
596
+ pragma_info = []
597
+
598
+ if "foreign_keys" in pragma:
599
+ fk_status = "ON" if pragma["foreign_keys"] else "OFF"
600
+ pragma_info.append(f"Foreign Keys: {fk_status}")
601
+
602
+ if "journal_mode" in pragma:
603
+ journal_mode = pragma["journal_mode"].upper()
604
+ pragma_info.append(f"Journal: {journal_mode}")
605
+
606
+ if "cache_size" in pragma:
607
+ # Remove negative sign
608
+ cache_size = abs(pragma["cache_size"])
609
+ if cache_size > 1000:
610
+ cache_display = f"{cache_size // 1000}K pages"
611
+ else:
612
+ cache_display = f"{cache_size} pages"
613
+ pragma_info.append(f"Cache: {cache_display}")
614
+
615
+ if pragma_info:
616
+ db_content.append(
617
+ ft.Text(
618
+ "Configuration:",
619
+ size=11,
620
+ weight=ft.FontWeight.BOLD,
621
+ color=db_text_color,
622
+ )
623
+ )
624
+ for info in pragma_info:
625
+ db_content.append(
626
+ ft.Text(
627
+ f" • {info}",
628
+ size=10,
629
+ color=db_text_color,
630
+ )
631
+ )
632
+
633
+ bottom_cards.append(
634
+ ft.Container(
635
+ content=ft.Column(db_content, spacing=4),
636
+ padding=15,
637
+ bgcolor=db_bg_color,
638
+ border=ft.border.all(1, ft.Colors.CYAN),
639
+ border_radius=8,
640
+ width=300,
641
+ )
642
+ )
643
+
644
+ details_row.content.controls = bottom_cards
645
+
646
+ page.update()
647
+
648
+ except Exception as e:
649
+ # Show error in header status
650
+ header.content.controls[1].content = ft.Column(
651
+ [
652
+ ft.Row(
653
+ [
654
+ ft.Text("❌", size=18),
655
+ ft.Text(
656
+ "Error",
657
+ size=16,
658
+ weight=ft.FontWeight.BOLD,
659
+ color=ft.Colors.ERROR,
660
+ ),
661
+ ],
662
+ spacing=8,
663
+ ),
664
+ ft.Text(
665
+ str(e)[:40] + "..." if len(str(e)) > 40 else str(e),
666
+ size=10,
667
+ color=ft.Colors.GREY_700,
668
+ ),
669
+ ],
670
+ spacing=2,
671
+ )
672
+ header.content.controls[1].bgcolor = ft.Colors.SURFACE
673
+ page.update()
674
+
675
+ async def auto_refresh() -> None:
676
+ # Wait initial delay before starting auto-refresh cycle
677
+ await asyncio.sleep(30)
678
+ while True:
679
+ await refresh_status()
680
+ await asyncio.sleep(30)
681
+
682
+ # Initial load
683
+ await refresh_status()
684
+ # Start auto-refresh task (will wait 30s before first auto-refresh)
685
+ asyncio.create_task(auto_refresh())
686
+
687
+ return flet_main