Remade frontend dashboard as flutter dashboard, still WIP
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user