Skip to content

Commit 25e6efe

Browse files
# Conflicts: # dashboard/lib/providers/engine_provider.dart # dashboard/lib/screens/dashboard_screen.dart # dashboard/lib/screens/hardware_screen.dart # dashboard/lib/services/engine_service.dart
2 parents 7fde936 + ac061fd commit 25e6efe

46 files changed

Lines changed: 7004 additions & 288 deletions

Some content is hidden

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

.githooks/commit-msg

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/sh
22
set -eu
33

4-
# Strip Cursor co-author trailer(s) from commit message.
4+
# Strip AI co-author/trailer lines from commit message.
55
# Runs after the message is composed, before the commit is finalized.
66
#
77
# Usage: enable repo hooks with:
@@ -14,12 +14,14 @@ fi
1414

1515
tmp="${MSG_FILE}.tmp.$$"
1616

17-
# Remove any "Co-authored-by: Cursor ..." trailer lines (case-insensitive).
18-
# Keep all other co-authors intact.
17+
# Remove:
18+
# - ALL "Co-authored-by:" trailer lines (case-insensitive)
19+
# - ANY git trailer line mentioning Cursor/cursoragent (case-insensitive)
1920
awk '
2021
BEGIN { IGNORECASE = 1 }
21-
/^[[:space:]]*Co-authored-by:[[:space:]]*Cursor([[:space:]]|<|$)/ { next }
22-
/^[[:space:]]*Co-Authored-By:[[:space:]]*Cursor([[:space:]]|<|$)/ { next }
22+
NR==1 { print; next }
23+
/^[[:space:]]*Co-authored-by:[[:space:]]*/ { next }
24+
/^[[:space:]]*[A-Za-z0-9-]+:[[:space:]]+.*(cursoragent|cursor).*/ { next }
2325
{ print }
2426
' "$MSG_FILE" > "$tmp"
2527

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,11 @@ release.md
258258

259259
# Pages
260260
landingpage.md
261+
262+
# Datastore
263+
/engine/datastore/*.json
264+
/engine/datastore/state/*.json
265+
/engine/datastore/history/**
266+
/engine/datastore/logs/**
267+
/engine/datastore/cache/**
268+
/engine/datastore/reports/**

dashboard/lib/navigation/dashboard_navigation.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:flutter/scheduler.dart';
44
class DashboardNavigation {
55
DashboardNavigation._();
66

7-
/// Switch main rail: 0 Overview … 4 Diagnostics, 5 Hardware, 6 Settings.
7+
/// Switch main rail: 0 Overview … 4 Diagnostics, 5 Hardware, 6 Storage, 7 Settings.
88
static void Function(int index)? selectMainTab;
99

1010
/// Switches Diagnostics inner tab to "Alerts & RCA" (index 1).

dashboard/lib/providers/engine_provider.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@ class EngineProvider extends ChangeNotifier {
446446
if (!_connected) {
447447
_connected = true;
448448
_connectionError = '';
449+
// Once we are talking to the engine, pull the authoritative history
450+
// archive so charts survive a dashboard restart even if SharedPreferences
451+
// is empty (e.g. after `flutter clean` or a fresh install).
452+
_history?.startPeriodicRefresh(_service);
449453
}
450454

451455
_currentState = state;
@@ -502,6 +506,70 @@ class EngineProvider extends ChangeNotifier {
502506
return r ?? {'ok': false, 'error': 'No response'};
503507
}
504508

509+
// ── Datastore (history, cache, baseline) ──
510+
511+
/// Fetch on-disk layout + history summary from the engine.
512+
Future<Map<String, dynamic>?> getStorageInfo() {
513+
return _service.getStorageInfo();
514+
}
515+
516+
/// Delete every file under the engine's cache/ directory.
517+
Future<Map<String, dynamic>?> clearEngineCache() async {
518+
final r = await _service.clearCache();
519+
notifyListeners();
520+
return r;
521+
}
522+
523+
/// Wipe the engine-side history archive AND the dashboard's offline mirror.
524+
Future<Map<String, dynamic>?> clearAllHistory() async {
525+
final r = await _service.deleteHistory();
526+
_history?.clear();
527+
notifyListeners();
528+
return r;
529+
}
530+
531+
/// Reset the behavioral baseline (engine continues running).
532+
Future<Map<String, dynamic>?> resetEngineBaseline() async {
533+
final r = await _service.resetBaseline();
534+
notifyListeners();
535+
return r;
536+
}
537+
538+
/// Pull the latest history window from the engine immediately.
539+
Future<void> refreshHistoryNow() async {
540+
await _history?.refreshFromEngine(_service);
541+
}
542+
543+
// ── Disk cleanup + large file finder ──
544+
545+
Future<Map<String, dynamic>?> getCleanupCategories() {
546+
return _service.getCleanupCategories();
547+
}
548+
549+
Future<Map<String, dynamic>?> runCleanupScan({List<String>? categoryIds}) {
550+
return _service.runCleanupScan(categoryIds: categoryIds);
551+
}
552+
553+
Future<Map<String, dynamic>?> applyCleanup({
554+
required String scanId,
555+
required List<String> categoryIds,
556+
String mode = 'recycle',
557+
}) {
558+
return _service.applyCleanup(
559+
scanId: scanId,
560+
categoryIds: categoryIds,
561+
mode: mode,
562+
);
563+
}
564+
565+
Future<Map<String, dynamic>?> findLargeFiles({
566+
required String path,
567+
double minMb = 100.0,
568+
int limit = 200,
569+
}) {
570+
return _service.findLargeFiles(path: path, minMb: minMb, limit: limit);
571+
}
572+
505573
/// Aggregated CPU / memory / disk health (slow probe — caller should cache).
506574
Future<Map<String, dynamic>?> getHardwareHealth({bool refresh = false}) {
507575
return _service.getHardwareHealth(refresh: refresh);
@@ -519,6 +587,7 @@ class EngineProvider extends ChangeNotifier {
519587
_eventFetchTimer?.cancel();
520588
_reconnectTimer?.cancel();
521589
_cooldownTicker?.cancel();
590+
_history?.stopPeriodicRefresh();
522591
_service.dispose();
523592
// Do NOT kill the engine on app close. The engine is designed to be a
524593
// background component and should survive dashboard restarts.

dashboard/lib/providers/history_provider.dart

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,49 @@ import 'package:shared_preferences/shared_preferences.dart';
66

77
import 'package:sentracore_dashboard/models/history_sample.dart';
88
import 'package:sentracore_dashboard/models/system_state.dart';
9-
9+
import 'package:sentracore_dashboard/services/engine_service.dart';
10+
11+
/// History samples ultimately live with the engine (persisted under
12+
/// ``history/`` next to the rest of its datastore). This provider:
13+
///
14+
/// * Pulls authoritative samples from the engine via `/api/v1/history`.
15+
/// * Keeps a small `SharedPreferences` mirror so charts still render when the
16+
/// engine is offline (e.g. during the first second of dashboard startup or
17+
/// while the engine restarts).
18+
/// * Records live samples opportunistically — the data is rolled into the
19+
/// offline mirror but the engine remains the source of truth across
20+
/// restarts.
1021
class HistoryProvider extends ChangeNotifier {
11-
static const _kHistorySamples = 'history_samples_v1';
22+
static const _kHistorySamples = 'history_samples_v2';
1223

13-
/// Default sampling cadence (engine pushes every ~2s; we downsample to keep storage sane).
24+
/// Local downsample cadence (engine pushes every ~2s).
1425
static const Duration sampleInterval = Duration(seconds: 30);
1526

16-
/// Hard cap to prevent unbounded growth in SharedPreferences.
17-
static const int maxSamples =
18-
12000; // ~4 days at 30s; UI ranges use downsampling
27+
/// Hard cap for the offline mirror; the server-side archive holds the rest.
28+
static const int maxSamples = 12000;
29+
30+
/// How often we re-fetch from the engine while it is connected.
31+
static const Duration _refreshInterval = Duration(minutes: 1);
32+
33+
/// How far back we ask the engine for on each refresh.
34+
static const Duration _refreshWindow = Duration(days: 7);
1935

2036
final List<HistorySample> _samples = [];
2137
bool _loaded = false;
2238
DateTime? _lastSampleAt;
2339
Timer? _saveDebounce;
2440

41+
/// When `true`, _samples reflects data pulled from the engine and is the
42+
/// source of truth. Local appends from [recordIfDue] still augment it
43+
/// between server refreshes.
44+
bool _syncedFromEngine = false;
45+
2546
List<HistorySample> get samples => List<HistorySample>.unmodifiable(_samples);
2647
bool get loaded => _loaded;
2748

49+
/// True once we've populated samples from the engine since launch.
50+
bool get syncedFromEngine => _syncedFromEngine;
51+
2852
Future<void> load() async {
2953
if (_loaded) return;
3054
final p = await SharedPreferences.getInstance();
@@ -58,10 +82,98 @@ class HistoryProvider extends ChangeNotifier {
5882
void clear() {
5983
_samples.clear();
6084
_lastSampleAt = null;
85+
_syncedFromEngine = false;
6186
notifyListeners();
6287
_scheduleSave();
6388
}
6489

90+
/// Pull the persistent server-side archive and replace local samples with
91+
/// it. Falls back silently if the engine is unreachable so we keep showing
92+
/// the offline mirror.
93+
Future<void> refreshFromEngine(EngineService service,
94+
{Duration? window}) async {
95+
final now = DateTime.now();
96+
final from = now.subtract(window ?? _refreshWindow);
97+
final raw = await service.getHistory(
98+
from: from,
99+
to: now,
100+
// Engine collects at 30s spacing; granularity matches so we don't waste
101+
// bytes on near-duplicate samples for long ranges.
102+
granularitySec: 30.0,
103+
limit: maxSamples,
104+
);
105+
if (raw.isEmpty) return;
106+
107+
final parsed = <HistorySample>[];
108+
for (final m in raw) {
109+
final at = m['at'];
110+
DateTime when;
111+
if (at is num) {
112+
when = DateTime.fromMillisecondsSinceEpoch(
113+
(at.toDouble() * 1000).round(),
114+
);
115+
} else {
116+
continue;
117+
}
118+
parsed.add(
119+
HistorySample(
120+
at: when,
121+
cpuPercent: (m['cpu_percent'] as num?)?.toDouble() ?? 0,
122+
memPercent: (m['mem_percent'] as num?)?.toDouble() ?? 0,
123+
diskPressurePercent:
124+
(m['disk_pressure_percent'] as num?)?.toDouble() ?? 0,
125+
topProcesses: _parseEngineProcesses(m['top_processes']),
126+
),
127+
);
128+
}
129+
if (parsed.isEmpty) return;
130+
131+
parsed.sort((a, b) => a.at.compareTo(b.at));
132+
_samples
133+
..clear()
134+
..addAll(parsed);
135+
_lastSampleAt = parsed.last.at;
136+
_syncedFromEngine = true;
137+
notifyListeners();
138+
_scheduleSave();
139+
}
140+
141+
Timer? _refreshTimer;
142+
143+
/// Begin periodically pulling from the engine. Safe to call repeatedly.
144+
void startPeriodicRefresh(EngineService service) {
145+
_refreshTimer?.cancel();
146+
// Fire an immediate refresh then continue on the interval.
147+
unawaited(refreshFromEngine(service));
148+
_refreshTimer = Timer.periodic(
149+
_refreshInterval,
150+
(_) => unawaited(refreshFromEngine(service)),
151+
);
152+
}
153+
154+
void stopPeriodicRefresh() {
155+
_refreshTimer?.cancel();
156+
_refreshTimer = null;
157+
}
158+
159+
static List<HistoryProcessSample> _parseEngineProcesses(dynamic raw) {
160+
if (raw is! List) return const [];
161+
final out = <HistoryProcessSample>[];
162+
for (final m in raw) {
163+
if (m is! Map) continue;
164+
out.add(
165+
HistoryProcessSample(
166+
name: '${m['name'] ?? ''}',
167+
pid: (m['pid'] as num?)?.toInt() ?? 0,
168+
cpuPercent: (m['cpu_percent'] as num?)?.toDouble() ?? 0,
169+
memPercent: (m['mem_percent'] as num?)?.toDouble() ?? 0,
170+
impact: (m['impact'] as num?)?.toDouble() ?? 0,
171+
),
172+
);
173+
}
174+
return out;
175+
}
176+
65177
/// Called by EngineProvider on every live state; this function decides whether
66178
/// we should record a new sample based on [sampleInterval].
67179
void recordIfDue({
@@ -127,6 +239,8 @@ class HistoryProvider extends ChangeNotifier {
127239
void dispose() {
128240
_saveDebounce?.cancel();
129241
_saveDebounce = null;
242+
_refreshTimer?.cancel();
243+
_refreshTimer = null;
130244
super.dispose();
131245
}
132246
}

0 commit comments

Comments
 (0)