Skip to content

Commit 8035fe4

Browse files
authored
Merge pull request #18 from kingwill101/dashboard_refresh
feat: expand dashboard UX and enrich stem observability metadata
2 parents 311c2be + 69880d2 commit 8035fe4

72 files changed

Lines changed: 12016 additions & 1125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/dashboard/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,15 @@
22

33
## 0.1.0
44

5+
- Reworked the dashboard into a richer operations console with dedicated views
6+
for tasks, jobs, workflows, workers, failures, audit, events, namespaces, and
7+
search.
8+
- Refactored UI rendering into modular page components and shared table/layout
9+
primitives for better maintainability.
10+
- Introduced a full Tailwind-based styling system and updated responsive layout
11+
behavior for sidebar/header/content rendering.
12+
- Improved navigation and Turbo frame behavior to reduce stale-content flashes
13+
during page switches.
14+
- Expanded dashboard state/service/server models and test coverage to support
15+
the new views and metadata-rich rendering paths.
516
- Initial release of the `stem_dashboard` package.

packages/dashboard/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Environment variables mirror the Stem CLI:
3636
- `STEM_RESULT_BACKEND_URL` (defaults to the broker URL when omitted)
3737
- `STEM_NAMESPACE` / `STEM_DASHBOARD_NAMESPACE` (defaults to `stem`)
3838
- `STEM_TLS_*` for TLS-enabled Redis endpoints
39+
- `DASHBOARD_BASE_PATH` (optional mount prefix such as `/dashboard`)
3940

4041
Because the dashboard reuses `StemConfig`, any broker/result backend supported
4142
by Stem (`redis://`, `rediss://`, `postgres://`, `postgresql://`, `memory://`)
@@ -45,6 +46,37 @@ The events page keeps a websocket open to `/dash/streams` so new
4546
queue/worker deltas appear instantly without refreshing. Tasks and workers
4647
pages use Turbo Frames for navigation and sorting.
4748

49+
## Library Embedding
50+
51+
`stem_dashboard` can run standalone (via `runDashboardServer`) or be mounted
52+
into an existing `routed` engine:
53+
54+
```dart
55+
import 'package:routed/routed.dart';
56+
import 'package:stem_dashboard/dashboard.dart';
57+
58+
Future<void> main() async {
59+
final service = await StemDashboardService.connect();
60+
final state = DashboardState(service: service);
61+
await state.start();
62+
63+
final engine = Engine();
64+
mountDashboard(
65+
engine: engine,
66+
service: service,
67+
state: state,
68+
options: const DashboardMountOptions(basePath: '/dashboard'),
69+
);
70+
71+
await engine.serve(host: '127.0.0.1', port: 8080);
72+
}
73+
```
74+
75+
For embedded usage, the host app owns lifecycle:
76+
77+
- call `state.start()` before serving.
78+
- call `state.dispose()` and `service.close()` on shutdown.
79+
4880
### Local dependency overrides
4981

5082
`pubspec.yaml` contains overrides pointing at the local Stem packages so the

packages/dashboard/bin/dashboard.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Future<void> main(List<String> args) async {
66
final host = Platform.environment['DASHBOARD_HOST']?.trim();
77
final portRaw = Platform.environment['DASHBOARD_PORT']?.trim();
88
final echoRaw = Platform.environment['DASHBOARD_ECHO_ROUTES']?.trim();
9+
final basePath = Platform.environment['DASHBOARD_BASE_PATH']?.trim();
910

1011
final resolvedHost = host != null && host.isNotEmpty ? host : '127.0.0.1';
1112
final resolvedPort = int.tryParse(portRaw ?? '') ?? 3080;
@@ -17,6 +18,7 @@ Future<void> main(List<String> args) async {
1718
host: resolvedHost,
1819
port: resolvedPort,
1920
echoRoutes: echoRoutes,
21+
basePath: basePath ?? '',
2022
),
2123
);
2224
}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
export 'src/server.dart' show DashboardServerOptions, runDashboardServer;
1+
export 'src/server.dart'
2+
show
3+
DashboardMountOptions,
4+
DashboardServerOptions,
5+
buildDashboardEngine,
6+
mountDashboard,
7+
registerDashboardRoutes,
8+
runDashboardServer;
29
export 'src/services/stem_service.dart'
310
show DashboardDataSource, StemDashboardService;
11+
export 'src/state/dashboard_state.dart' show DashboardState;

packages/dashboard/lib/src/config/config.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ class DashboardConfig {
1111
required this.stem,
1212
required this.namespace,
1313
required this.routing,
14+
required this.alertWebhookUrls,
15+
required this.alertBacklogThreshold,
16+
required this.alertFailedTaskThreshold,
17+
required this.alertOfflineWorkerThreshold,
18+
required this.alertCooldown,
1419
});
1520

1621
/// Loads a dashboard config from the provided environment map.
@@ -29,12 +34,37 @@ class DashboardConfig {
2934
final routing = RoutingConfigLoader(
3035
StemRoutingContext.fromConfig(stemConfig),
3136
).load();
37+
final webhookUrls = _parseCsv(
38+
env['STEM_DASHBOARD_ALERT_WEBHOOK_URLS'] ??
39+
env['STEM_DASHBOARD_WEBHOOK_URLS'],
40+
);
41+
final backlogThreshold = _parsePositiveInt(
42+
env['STEM_DASHBOARD_ALERT_BACKLOG_THRESHOLD'],
43+
fallback: 500,
44+
);
45+
final failedThreshold = _parsePositiveInt(
46+
env['STEM_DASHBOARD_ALERT_FAILED_TASK_THRESHOLD'],
47+
fallback: 25,
48+
);
49+
final offlineThreshold = _parsePositiveInt(
50+
env['STEM_DASHBOARD_ALERT_OFFLINE_WORKER_THRESHOLD'],
51+
fallback: 1,
52+
);
53+
final cooldown = _parseDuration(
54+
env['STEM_DASHBOARD_ALERT_COOLDOWN'],
55+
fallback: const Duration(minutes: 5),
56+
);
3257

3358
return DashboardConfig._(
3459
environment: Map.unmodifiable(env),
3560
stem: stemConfig,
3661
namespace: namespace,
3762
routing: routing,
63+
alertWebhookUrls: webhookUrls,
64+
alertBacklogThreshold: backlogThreshold,
65+
alertFailedTaskThreshold: failedThreshold,
66+
alertOfflineWorkerThreshold: offlineThreshold,
67+
alertCooldown: cooldown,
3868
);
3969
}
4070

@@ -54,6 +84,21 @@ class DashboardConfig {
5484
/// Routing registry resolved for this dashboard session.
5585
final RoutingRegistry routing;
5686

87+
/// Alert webhook URLs.
88+
final List<String> alertWebhookUrls;
89+
90+
/// Backlog alert threshold.
91+
final int alertBacklogThreshold;
92+
93+
/// Failed task alert threshold.
94+
final int alertFailedTaskThreshold;
95+
96+
/// Offline worker alert threshold.
97+
final int alertOfflineWorkerThreshold;
98+
99+
/// Alert cooldown.
100+
final Duration alertCooldown;
101+
57102
/// Broker URL resolved from the underlying Stem config.
58103
String get brokerUrl => stem.brokerUrl;
59104

@@ -63,3 +108,39 @@ class DashboardConfig {
63108
/// TLS configuration resolved from the underlying Stem config.
64109
TlsConfig get tls => stem.tls;
65110
}
111+
112+
List<String> _parseCsv(String? raw) {
113+
if (raw == null || raw.trim().isEmpty) return const [];
114+
return raw
115+
.split(',')
116+
.map((value) => value.trim())
117+
.where((value) => value.isNotEmpty)
118+
.toList(growable: false);
119+
}
120+
121+
int _parsePositiveInt(String? raw, {required int fallback}) {
122+
if (raw == null || raw.trim().isEmpty) return fallback;
123+
final parsed = int.tryParse(raw.trim());
124+
if (parsed == null || parsed <= 0) return fallback;
125+
return parsed;
126+
}
127+
128+
Duration _parseDuration(String? raw, {required Duration fallback}) {
129+
if (raw == null || raw.trim().isEmpty) return fallback;
130+
final value = raw.trim();
131+
final match = RegExp(r'^(\d+)(ms|s|m|h)$').firstMatch(value);
132+
if (match == null) return fallback;
133+
final amount = int.tryParse(match.group(1) ?? '');
134+
if (amount == null || amount <= 0) return fallback;
135+
switch (match.group(2)) {
136+
case 'ms':
137+
return Duration(milliseconds: amount);
138+
case 's':
139+
return Duration(seconds: amount);
140+
case 'm':
141+
return Duration(minutes: amount);
142+
case 'h':
143+
return Duration(hours: amount);
144+
}
145+
return fallback;
146+
}

0 commit comments

Comments
 (0)