Remade frontend dashboard as flutter dashboard, still WIP

This commit is contained in:
Nate Anderson
2025-06-13 09:20:42 -06:00
parent 8ea06b12f7
commit b373a93f0e
207 changed files with 9869 additions and 46 deletions
+254
View File
@@ -0,0 +1,254 @@
import 'dart:async';
import 'dart:io';
import 'package:xp_nix/src/models/activity_event.dart';
enum LogLevel {
debug(0, 'DEBUG'),
info(1, 'INFO'),
warn(2, 'WARN'),
error(3, 'ERROR');
const LogLevel(this.value, this.name);
final int value;
final String name;
}
class Logger {
static Logger? _instance;
static Logger get instance => _instance ??= Logger._();
Logger._();
LogLevel _currentLevel = LogLevel.info;
String _logDirectory = 'logs';
int _maxFileSizeMB = 10;
int _maxFiles = 5;
File? _currentLogFile;
final StreamController<String> _logStreamController = StreamController<String>.broadcast();
Stream<String> get logStream => _logStreamController.stream;
Future<void> initialize({
LogLevel level = LogLevel.info,
String logDirectory = 'logs',
int maxFileSizeMB = 10,
int maxFiles = 5,
}) async {
_currentLevel = level;
_logDirectory = logDirectory;
_maxFileSizeMB = maxFileSizeMB;
_maxFiles = maxFiles;
await _setupLogFile();
_info('Logger initialized with level: ${level.name}');
}
Future<void> _setupLogFile() async {
final logDir = Directory(_logDirectory);
if (!await logDir.exists()) {
await logDir.create(recursive: true);
}
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').split('.')[0];
_currentLogFile = File('$_logDirectory/xp_nix_$timestamp.log');
// Create the file if it doesn't exist
if (!await _currentLogFile!.exists()) {
await _currentLogFile!.create();
}
}
Future<void> _rotateLogIfNeeded() async {
if (_currentLogFile == null) return;
try {
final stats = await _currentLogFile!.stat();
final fileSizeMB = stats.size / (1024 * 1024);
if (fileSizeMB > _maxFileSizeMB) {
await _cleanupOldLogs();
await _setupLogFile();
_info('Log file rotated due to size limit');
}
} catch (e) {
// If we can't check file size, just continue
print('Could not check log file size: $e');
}
}
Future<void> _cleanupOldLogs() async {
final logDir = Directory(_logDirectory);
if (!await logDir.exists()) return;
try {
final logFiles =
await logDir.list().where((entity) => entity is File && entity.path.endsWith('.log')).cast<File>().toList();
logFiles.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
if (logFiles.length >= _maxFiles) {
for (int i = _maxFiles - 1; i < logFiles.length; i++) {
_info('Deleting old log file ${logFiles[i].toString()}');
await logFiles[i].delete();
}
}
} catch (e) {
print('Could not cleanup old logs: $e');
}
}
void _log(LogLevel level, String message, [Object? error, StackTrace? stackTrace]) {
if (level.value < _currentLevel.value) return;
final timestamp = DateTime.now().toIso8601String();
final logEntry = '[$timestamp] [${level.name}] $message';
// Print to console
print(logEntry);
// Add to stream for real-time monitoring
_logStreamController.add(logEntry);
// Write to file asynchronously
_writeToFileAsync(logEntry, error, stackTrace);
}
void _writeToFileAsync(String logEntry, [Object? error, StackTrace? stackTrace]) {
// Use Future.microtask to avoid blocking the current execution
Future.microtask(() async {
await _writeToFile(logEntry, error, stackTrace);
});
}
Future<void> _writeToFile(String logEntry, [Object? error, StackTrace? stackTrace]) async {
if (_currentLogFile == null) return;
try {
final buffer = StringBuffer();
buffer.writeln(logEntry);
if (error != null) {
buffer.writeln('Error: $error');
}
if (stackTrace != null) {
buffer.writeln('Stack trace: $stackTrace');
}
// Write to file using writeAsString with append mode
await _currentLogFile!.writeAsString(buffer.toString(), mode: FileMode.append, flush: true);
// Check if rotation is needed
await _rotateLogIfNeeded();
} catch (e) {
// If writing fails, try to recreate the log file
print('Log writing failed: $e');
try {
await _setupLogFile();
} catch (setupError) {
print('Could not recreate log file: $setupError');
}
}
}
// Static convenience methods
static void debug(String message) => instance._log(LogLevel.debug, message);
static void info(String message) => instance._log(LogLevel.info, message);
static void warn(String message) => instance._log(LogLevel.warn, message);
static void error(String message, [Object? error, StackTrace? stackTrace]) =>
instance._log(LogLevel.error, message, error, stackTrace);
// Instance methods for when you have a logger instance
// ignore: unused_element
void _debug(String message) => _log(LogLevel.debug, message);
// ignore: unused_element
void _info(String message) => _log(LogLevel.info, message);
// ignore: unused_element
void _warn(String message) => _log(LogLevel.warn, message);
// ignore: unused_element
void _error(String message, [Object? error, StackTrace? stackTrace]) =>
_log(LogLevel.error, message, error, stackTrace);
// Activity-specific logging methods
static void logActivity(String activityType, String application, int durationSeconds, int xpGained) {
info('ACTIVITY: $activityType in $application for ${durationSeconds}s (+$xpGained XP)');
}
static void logFocusSession(int minutes, int bonusXP) {
info('FOCUS_SESSION: ${minutes}min session completed (+$bonusXP XP)');
}
static void logLevelUp(int oldLevel, int newLevel, int totalXP) {
info('LEVEL_UP: $oldLevel$newLevel (Total XP: $totalXP)');
}
static void logAchievement(String name, String description, int xpReward) {
info('ACHIEVEMENT: $name - $description (+$xpReward XP)');
}
static void logConfigChange(String path, dynamic oldValue, dynamic newValue) {
info('CONFIG_CHANGE: $path changed from $oldValue to $newValue');
}
static void logXPCalculation(ActivityEventType activityType, int baseXP, double multiplier, int finalXP) {
debug('XP_CALC: ${activityType.displayName} base=$baseXP × $multiplier = $finalXP');
}
static void logIdleStateChange(bool isIdle, Duration? idleDuration) {
if (isIdle) {
debug('IDLE: User became idle');
} else {
final duration = idleDuration?.inMinutes ?? 0;
info('ACTIVE: User became active after ${duration}min idle');
}
}
static void logZoomStatusChange(String oldStatus, String newStatus) {
info('ZOOM_STATUS: $oldStatus$newStatus');
}
static void logThemeChange(int level, String themeName) {
info('THEME_CHANGE: Applied theme "$themeName" for level $level');
}
static void logXPGain(String source, int xpGained, String activity, int currentXP, int currentLevel) {
info('XP_GAIN: +$xpGained from $source ($activity) - Total: $currentXP XP, Level: $currentLevel');
}
// Performance logging
static void logPerformanceMetric(String operation, Duration duration) {
debug('PERFORMANCE: $operation took ${duration.inMilliseconds}ms');
}
// Get recent log entries for dashboard
Future<List<String>> getRecentLogs([int count = 100]) async {
if (_currentLogFile == null || !await _currentLogFile!.exists()) {
return [];
}
try {
final lines = await _currentLogFile!.readAsLines();
return lines.reversed.take(count).toList();
} catch (e) {
_error('Failed to read log file: $e');
return [];
}
}
// Get logs by level
Future<List<String>> getLogsByLevel(LogLevel level, [int count = 50]) async {
final allLogs = await getRecentLogs(count * 2);
return allLogs.where((log) => log.contains('[${level.name}]')).take(count).toList();
}
// Update log level dynamically
void setLogLevel(LogLevel level) {
_currentLevel = level;
info('Log level changed to: ${level.name}');
}
Future<void> dispose() async {
await _logStreamController.close();
}
}