Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@

## 0.1.0

- Reworked the dashboard into a richer operations console with dedicated views
for tasks, jobs, workflows, workers, failures, audit, events, namespaces, and
search.
- Refactored UI rendering into modular page components and shared table/layout
primitives for better maintainability.
- Introduced a full Tailwind-based styling system and updated responsive layout
behavior for sidebar/header/content rendering.
- Improved navigation and Turbo frame behavior to reduce stale-content flashes
during page switches.
- Expanded dashboard state/service/server models and test coverage to support
the new views and metadata-rich rendering paths.
- Initial release of the `stem_dashboard` package.
32 changes: 32 additions & 0 deletions packages/dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Environment variables mirror the Stem CLI:
- `STEM_RESULT_BACKEND_URL` (defaults to the broker URL when omitted)
- `STEM_NAMESPACE` / `STEM_DASHBOARD_NAMESPACE` (defaults to `stem`)
- `STEM_TLS_*` for TLS-enabled Redis endpoints
- `DASHBOARD_BASE_PATH` (optional mount prefix such as `/dashboard`)

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

## Library Embedding

`stem_dashboard` can run standalone (via `runDashboardServer`) or be mounted
into an existing `routed` engine:

```dart
import 'package:routed/routed.dart';
import 'package:stem_dashboard/dashboard.dart';

Future<void> main() async {
final service = await StemDashboardService.connect();
final state = DashboardState(service: service);
await state.start();

final engine = Engine();
mountDashboard(
engine: engine,
service: service,
state: state,
options: const DashboardMountOptions(basePath: '/dashboard'),
);

await engine.serve(host: '127.0.0.1', port: 8080);
}
```

For embedded usage, the host app owns lifecycle:

- call `state.start()` before serving.
- call `state.dispose()` and `service.close()` on shutdown.

### Local dependency overrides

`pubspec.yaml` contains overrides pointing at the local Stem packages so the
Expand Down
2 changes: 2 additions & 0 deletions packages/dashboard/bin/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Future<void> main(List<String> args) async {
final host = Platform.environment['DASHBOARD_HOST']?.trim();
final portRaw = Platform.environment['DASHBOARD_PORT']?.trim();
final echoRaw = Platform.environment['DASHBOARD_ECHO_ROUTES']?.trim();
final basePath = Platform.environment['DASHBOARD_BASE_PATH']?.trim();

final resolvedHost = host != null && host.isNotEmpty ? host : '127.0.0.1';
final resolvedPort = int.tryParse(portRaw ?? '') ?? 3080;
Expand All @@ -17,6 +18,7 @@ Future<void> main(List<String> args) async {
host: resolvedHost,
port: resolvedPort,
echoRoutes: echoRoutes,
basePath: basePath ?? '',
),
);
}
Expand Down
10 changes: 9 additions & 1 deletion packages/dashboard/lib/dashboard.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export 'src/server.dart' show DashboardServerOptions, runDashboardServer;
export 'src/server.dart'
show
DashboardMountOptions,
DashboardServerOptions,
buildDashboardEngine,
mountDashboard,
registerDashboardRoutes,
runDashboardServer;
export 'src/services/stem_service.dart'
show DashboardDataSource, StemDashboardService;
export 'src/state/dashboard_state.dart' show DashboardState;
81 changes: 81 additions & 0 deletions packages/dashboard/lib/src/config/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class DashboardConfig {
required this.stem,
required this.namespace,
required this.routing,
required this.alertWebhookUrls,
required this.alertBacklogThreshold,
required this.alertFailedTaskThreshold,
required this.alertOfflineWorkerThreshold,
required this.alertCooldown,
});

/// Loads a dashboard config from the provided environment map.
Expand All @@ -29,12 +34,37 @@ class DashboardConfig {
final routing = RoutingConfigLoader(
StemRoutingContext.fromConfig(stemConfig),
).load();
final webhookUrls = _parseCsv(
env['STEM_DASHBOARD_ALERT_WEBHOOK_URLS'] ??
env['STEM_DASHBOARD_WEBHOOK_URLS'],
);
final backlogThreshold = _parsePositiveInt(
env['STEM_DASHBOARD_ALERT_BACKLOG_THRESHOLD'],
fallback: 500,
);
final failedThreshold = _parsePositiveInt(
env['STEM_DASHBOARD_ALERT_FAILED_TASK_THRESHOLD'],
fallback: 25,
);
final offlineThreshold = _parsePositiveInt(
env['STEM_DASHBOARD_ALERT_OFFLINE_WORKER_THRESHOLD'],
fallback: 1,
);
final cooldown = _parseDuration(
env['STEM_DASHBOARD_ALERT_COOLDOWN'],
fallback: const Duration(minutes: 5),
);

return DashboardConfig._(
environment: Map.unmodifiable(env),
stem: stemConfig,
namespace: namespace,
routing: routing,
alertWebhookUrls: webhookUrls,
alertBacklogThreshold: backlogThreshold,
alertFailedTaskThreshold: failedThreshold,
alertOfflineWorkerThreshold: offlineThreshold,
alertCooldown: cooldown,
);
}

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

/// Alert webhook URLs.
final List<String> alertWebhookUrls;

/// Backlog alert threshold.
final int alertBacklogThreshold;

/// Failed task alert threshold.
final int alertFailedTaskThreshold;

/// Offline worker alert threshold.
final int alertOfflineWorkerThreshold;

/// Alert cooldown.
final Duration alertCooldown;

/// Broker URL resolved from the underlying Stem config.
String get brokerUrl => stem.brokerUrl;

Expand All @@ -63,3 +108,39 @@ class DashboardConfig {
/// TLS configuration resolved from the underlying Stem config.
TlsConfig get tls => stem.tls;
}

List<String> _parseCsv(String? raw) {
if (raw == null || raw.trim().isEmpty) return const [];
return raw
.split(',')
.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toList(growable: false);
}

int _parsePositiveInt(String? raw, {required int fallback}) {
if (raw == null || raw.trim().isEmpty) return fallback;
final parsed = int.tryParse(raw.trim());
if (parsed == null || parsed <= 0) return fallback;
return parsed;
}

Duration _parseDuration(String? raw, {required Duration fallback}) {
if (raw == null || raw.trim().isEmpty) return fallback;
final value = raw.trim();
final match = RegExp(r'^(\d+)(ms|s|m|h)$').firstMatch(value);
if (match == null) return fallback;
final amount = int.tryParse(match.group(1) ?? '');
if (amount == null || amount <= 0) return fallback;
switch (match.group(2)) {
case 'ms':
return Duration(milliseconds: amount);
case 's':
return Duration(seconds: amount);
case 'm':
return Duration(minutes: amount);
case 'h':
return Duration(hours: amount);
}
return fallback;
}
Loading
Loading