ctls_logging 1.0.1 copy "ctls_logging: ^1.0.1" to clipboard
ctls_logging: ^1.0.1 copied to clipboard

High-performance columnar log storage system with adaptive tiering, compression, and intelligent query optimization for Dart/Flutter apps.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:ctls_logging/ctls_logging.dart';

// Realistic log message templates
const _authMessages = [
  'User authentication successful',
  'Login attempt from IP %s',
  'Token refresh completed',
  'Password validation failed',
  'Session expired for user %s',
  'OAuth callback received',
  'Biometric authentication enabled',
  'Two-factor code sent',
  'Permission denied for resource %s',
  'Account locked due to suspicious activity',
];

const _networkMessages = [
  'HTTP GET /api/v1/users/%d - 200 OK (%.2fms)',
  'HTTP POST /api/v1/data - 201 Created',
  'WebSocket connection established',
  'Connection timeout after %.2fs',
  'Retry attempt %d for endpoint %s',
  'SSL handshake completed',
  'Request queued: payload size %d bytes',
  'Response cached for %d seconds',
  'Network error: %s unreachable',
  'Rate limit exceeded: retry after %ds',
];

const _databaseMessages = [
  'Query executed in %.2fms: SELECT * FROM users',
  'Transaction committed: %d rows affected',
  'Database connection pool exhausted',
  'Index rebuild started for table %s',
  'Slow query detected: %.2fs',
  'Cache hit ratio: %.1f%%',
  'Migration applied: version %d',
  'Deadlock detected, rolling back',
  'Batch insert completed: %d records',
  'Backup checkpoint created',
];

const _uiMessages = [
  'Screen rendered: %s (%.2fms)',
  'User tapped button: %s',
  'Navigation pushed: %s',
  'Form validation error: %s',
  'Image loaded: %s (%.2fKB)',
  'Animation completed: %s',
  'Keyboard shown/hidden',
  'Scroll position updated: %.1f',
  'Widget rebuild: %s',
  'Theme changed to %s mode',
];

const _systemMessages = [
  'App launched in %.2fs',
  'Memory usage: %.1fMB',
  'Battery level: %d%%',
  'Storage space: %.2fGB free',
  'Background task scheduled: %s',
  'Notification permission %s',
  'Device orientation changed',
  'App state: %s',
  'Push notification received',
  'Crash report sent',
];

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'CTLS Logging Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
          brightness: Brightness.light,
        ),
        cardTheme: CardThemeData(
          elevation: 2,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
          ),
        ),
        filledButtonTheme: FilledButtonThemeData(
          style: FilledButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
            ),
          ),
        ),
      ),
      home: const CtlsExampleHome(),
    );
  }
}

class CtlsExampleHome extends StatefulWidget {
  const CtlsExampleHome({super.key});

  @override
  State<CtlsExampleHome> createState() => _CtlsExampleHomeState();
}

class _CtlsExampleHomeState extends State<CtlsExampleHome> {
  CTLSConfig? _config;
  CTLSWriter? _writer;
  final CTLSReader _reader = CTLSReader();
  final LogExporter _exporter = LogExporter();

  List<File> _blockFiles = const [];
  List<LogEntry> _allEntries = const [];
  List<LogEntry> _searchResults = const [];
  String _exportPreview = '';

  final TextEditingController _messageContainsController =
      TextEditingController();
  final TextEditingController _tagFilterController = TextEditingController();
  LogLevel? _levelFilter;
  DateTime? _startDate;
  DateTime? _endDate;

  bool _busy = false;
  String? _error;

  // Performance metrics
  int _totalLogsWritten = 0;
  double _lastWriteSpeed = 0; // logs per second
  double _lastSearchTime = 0; // milliseconds
  int _rawStorageBytes = 0;
  int _compressedStorageBytes = 0;

  @override
  void initState() {
    super.initState();
    _init();
  }

  @override
  void dispose() {
    _messageContainsController.dispose();
    _tagFilterController.dispose();
    super.dispose();
  }

  Future<void> _init() async {
    try {
      Directory baseDir;
      try {
        baseDir = await getApplicationDocumentsDirectory();
      } catch (_) {
        baseDir = await Directory.systemTemp.createTemp(
          'ctls_logging_example_',
        );
      }

      final config = CTLSConfig.defaultConfig(baseDir.path);
      final writer = CTLSWriter(config: config);

      setState(() {
        _config = config;
        _writer = writer;
        _busy = false;
        _error = null;
      });

      await _refresh();
    } catch (e) {
      setState(() {
        _busy = false;
        _error = e.toString();
      });
    }
  }

  Future<void> _withBusy(Future<void> Function() action) async {
    setState(() {
      _busy = true;
      _error = null;
    });

    try {
      await action();
    } catch (e) {
      setState(() {
        _error = e.toString();
      });
    } finally {
      if (mounted) {
        setState(() {
          _busy = false;
        });
      }
    }
  }

  void _writeSampleLogs({required int count}) {
    final writer = _writer;
    if (writer == null) return;

    final now = DateTime.now();
    final startTime = DateTime.now();

    for (var i = 0; i < count; i++) {
      final level = LogLevel.values[i % LogLevel.values.length];
      final tag = switch (i % 5) {
        0 => 'Auth',
        1 => 'Network',
        2 => 'Database',
        3 => 'UI',
        _ => 'System',
      };

      writer.write(
        LogEntry(
          timestamp: now.add(Duration(milliseconds: i)),
          level: level,
          tag: tag,
          message: 'Sample log #$i ($tag/${level.name})',
        ),
      );
    }

    final elapsed = DateTime.now().difference(startTime);
    _totalLogsWritten += count;
    _lastWriteSpeed = count / elapsed.inMicroseconds * 1000000;

    setState(() {});
  }

  Future<void> _dump30Days() async {
    final writer = _writer;
    if (writer == null) return;

    final random = Random();
    final startTime = DateTime.now();
    final baseDate = DateTime.now().subtract(const Duration(days: 30));

    int totalLogs = 0;

    // Generate 10,000+ logs per day for 30 days
    for (var day = 0; day < 30; day++) {
      final dayStart = baseDate.add(Duration(days: day));
      final logsPerDay = 10000 + random.nextInt(5000); // 10K-15K per day

      // Calculate microseconds between logs to spread evenly across 24 hours
      final microsecondsPerLog = (24 * 60 * 60 * 1000000) ~/ logsPerDay;

      for (var i = 0; i < logsPerDay; i++) {
        final tagIndex = random.nextInt(5);
        final tag = switch (tagIndex) {
          0 => 'Auth',
          1 => 'Network',
          2 => 'Database',
          3 => 'UI',
          _ => 'System',
        };

        // Realistic log level distribution
        final levelRoll = random.nextInt(100);
        final level = switch (levelRoll) {
          < 50 => LogLevel.info, // 50% info
          < 70 => LogLevel.debug, // 20% debug
          < 85 => LogLevel.warning, // 15% warning
          < 97 => LogLevel.error, // 12% error
          _ => LogLevel.critical, // 3% critical
        };

        // Generate realistic messages
        final messages = switch (tag) {
          'Auth' => _authMessages,
          'Network' => _networkMessages,
          'Database' => _databaseMessages,
          'UI' => _uiMessages,
          _ => _systemMessages,
        };

        var message = messages[random.nextInt(messages.length)];

        // Add realistic data to message templates
        if (message.contains('%s')) {
          message = message.replaceFirst(
            '%s',
            'resource_${random.nextInt(100)}',
          );
        }
        if (message.contains('%d')) {
          message = message.replaceFirst('%d', '${random.nextInt(1000)}');
        }
        if (message.contains('%.2f')) {
          message = message.replaceFirst(
            '%.2f',
            (random.nextDouble() * 100).toStringAsFixed(2),
          );
        }

        // Sequential timestamps - critical for delta encoding
        final timestamp = dayStart.add(
          Duration(microseconds: i * microsecondsPerLog),
        );

        writer.write(
          LogEntry(
            timestamp: timestamp,
            level: level,
            tag: tag,
            message: message,
          ),
        );

        totalLogs++;

        // Flush periodically to avoid memory issues
        if (i > 0 && i % 5000 == 0) {
          await writer.flush();
        }
      }

      // Flush after each day
      await writer.flush();
    }

    final elapsed = DateTime.now().difference(startTime);
    _totalLogsWritten = totalLogs;
    _lastWriteSpeed = totalLogs / elapsed.inMicroseconds * 1000000;

    await _refresh();
    await _calculateStorageStats();
  }

  Future<void> _calculateStorageStats() async {
    final config = _config;
    if (config == null) return;

    int compressedSize = 0;
    for (final file in _blockFiles) {
      if (await file.exists()) {
        compressedSize += await file.length();
      }
    }

    // Estimate raw size (average 148 bytes per entry)
    final rawSize = _totalLogsWritten * 148;

    setState(() {
      _rawStorageBytes = rawSize;
      _compressedStorageBytes = compressedSize;
    });
  }

  Future<void> _flush() async {
    final writer = _writer;
    if (writer == null) return;

    await writer.flush();
    await _refresh();
  }

  Future<void> _refresh() async {
    final config = _config;
    if (config == null) return;

    final dir = Directory(config.logDirectory);
    final exists = await dir.exists();
    if (!exists) {
      setState(() {
        _blockFiles = const [];
        _allEntries = const [];
        _searchResults = const [];
        _exportPreview = '';
        _totalLogsWritten = 0;
        _rawStorageBytes = 0;
        _compressedStorageBytes = 0;
      });
      return;
    }

    final files =
        dir
            .listSync()
            .whereType<File>()
            .where((f) => f.path.toLowerCase().endsWith('.ctls'))
            .toList()
          ..sort((a, b) => a.path.compareTo(b.path));

    final entries = await _reader.readBlocks(files);

    setState(() {
      _blockFiles = files;
      _allEntries = entries;
      _totalLogsWritten = entries.length;
    });

    await _calculateStorageStats();
  }

  Future<void> _runSearch() async {
    final startTime = DateTime.now();

    final criteria = SearchCriteria(
      levels: _levelFilter != null ? {_levelFilter!} : {},
      tags: _tagFilterController.text.trim().isEmpty
          ? {}
          : {_tagFilterController.text.trim()},
      messageContains: _messageContainsController.text.trim().isEmpty
          ? null
          : _messageContainsController.text.trim(),
    );

    var results = await _reader.search(_blockFiles, criteria);

    // Apply date range filter if set
    if (_startDate != null || _endDate != null) {
      results = results.where((entry) {
        final timestamp = entry.timestamp;
        if (_startDate != null && timestamp.isBefore(_startDate!)) {
          return false;
        }
        if (_endDate != null &&
            timestamp.isAfter(_endDate!.add(const Duration(days: 1)))) {
          return false;
        }
        return true;
      }).toList();
    }

    final elapsed = DateTime.now().difference(startTime);
    _lastSearchTime = elapsed.inMicroseconds / 1000;

    setState(() {
      _searchResults = results;
    });
  }

  Future<void> _exportSearchResults() async {
    if (_searchResults.isEmpty) return;

    final startDate = _startDate?.toIso8601String().split('T')[0] ?? 'all';
    final endDate = _endDate?.toIso8601String().split('T')[0] ?? 'all';

    // Simple JSON export
    final json = jsonEncode(
      _searchResults
          .map(
            (e) => {
              'timestamp': e.timestamp.toIso8601String(),
              'level': e.level.name,
              'tag': e.tag,
              'message': e.message,
            },
          )
          .toList(),
    );

    setState(() {
      _exportPreview =
          'Exported ${_searchResults.length} results ($startDate to $endDate)\n\n${json.substring(0, json.length > 500 ? 500 : json.length)}...';
    });

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            'Exported ${_searchResults.length} results ($startDate to $endDate). Check Export tab to view.',
          ),
          duration: const Duration(seconds: 3),
        ),
      );
    }
  }

  String _formatBytes(int bytes) {
    if (bytes < 1024) return '$bytes B';
    if (bytes < 1024 * 1024) {
      return '${(bytes / 1024).toStringAsFixed(1)} KB';
    }
    return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
  }

  Future<void> _export(ExportFormat format) async {
    final options = switch (format) {
      ExportFormat.txt => ExportOptions.txt(),
      ExportFormat.json => ExportOptions.json(pretty: true),
      ExportFormat.csv => ExportOptions.csv(),
    };

    // For large exports, show a preview with first 10K logs only
    final maxLogsForPreview = 10000;
    final totalLogs = _allEntries.length;

    setState(() {
      _exportPreview =
          'Exporting $totalLogs logs in ${format.name.toUpperCase()} format...\n\n'
          'Please wait, this may take a moment for large datasets.\n'
          '(Showing preview of first $maxLogsForPreview logs)';
    });

    // Process in smaller chunks to avoid UI freeze
    try {
      // For preview, only export first 10K logs
      final previewFiles = _blockFiles.take(5).toList(); // First few files
      final previewContent = await _exporter.exportToString(
        previewFiles,
        options,
      );

      final previewLimit = 5000; // Show only 5000 chars in preview
      final truncated = previewContent.length > previewLimit
          ? '${previewContent.substring(0, previewLimit)}\n\n... (truncated for display, full export contains $totalLogs logs)'
          : previewContent;

      setState(() {
        _exportPreview =
            '✅ Export Preview (${format.name.toUpperCase()})\n'
            'Total Logs: $totalLogs\n'
            'Total Files: ${_blockFiles.length}\n\n'
            '$truncated';
      });
    } catch (e) {
      setState(() {
        _exportPreview = 'Export error: $e';
      });
    }
  }

  Future<void> _clearAllLogs() async {
    final config = _config;
    if (config == null) return;

    final dir = Directory(config.logDirectory);
    if (await dir.exists()) {
      await dir.delete(recursive: true);
    }

    setState(() {
      _blockFiles = const [];
      _allEntries = const [];
      _searchResults = const [];
      _exportPreview = '';
    });
  }

  @override
  Widget build(BuildContext context) {
    final config = _config;
    final writer = _writer;

    return DefaultTabController(
      length: 5,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('CTLS Logging Example'),
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(icon: Icon(Icons.dashboard), text: 'Dashboard'),
              Tab(icon: Icon(Icons.list), text: 'Logs'),
              Tab(icon: Icon(Icons.search), text: 'Search'),
              Tab(icon: Icon(Icons.file_download), text: 'Export'),
              Tab(icon: Icon(Icons.folder), text: 'Files'),
            ],
          ),
          actions: [
            IconButton(
              tooltip: 'Refresh',
              onPressed: _busy ? null : () => _withBusy(_refresh),
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        body: _busy
            ? const Center(child: CircularProgressIndicator())
            : Column(
                children: [
                  if (_error != null)
                    MaterialBanner(
                      content: Text(_error!),
                      actions: [
                        TextButton(
                          onPressed: () => setState(() => _error = null),
                          child: const Text('Dismiss'),
                        ),
                      ],
                    ),
                  Expanded(
                    child: TabBarView(
                      children: [
                        _DashboardTab(
                          config: config,
                          writer: writer,
                          totalLogsWritten: _totalLogsWritten,
                          lastWriteSpeed: _lastWriteSpeed,
                          lastSearchTime: _lastSearchTime,
                          rawStorageBytes: _rawStorageBytes,
                          compressedStorageBytes: _compressedStorageBytes,
                          blockFilesCount: _blockFiles.length,
                          allEntries: _allEntries,
                          onDump30Days: () => _withBusy(_dump30Days),
                          onWrite1K: () => _writeSampleLogs(count: 1000),
                          onFlush: () => _withBusy(_flush),
                          onClearLogs: () => _withBusy(_clearAllLogs),
                          onRefresh: () => _withBusy(_refresh),
                          formatBytes: _formatBytes,
                        ),
                        _LogsTab(entries: _allEntries),
                        _SearchTab(
                          messageContainsController: _messageContainsController,
                          tagFilterController: _tagFilterController,
                          levelFilter: _levelFilter,
                          startDate: _startDate,
                          endDate: _endDate,
                          onLevelChanged: (v) =>
                              setState(() => _levelFilter = v),
                          onStartDateChanged: (d) =>
                              setState(() => _startDate = d),
                          onEndDateChanged: (d) => setState(() => _endDate = d),
                          onSearch: () => _withBusy(_runSearch),
                          onExport: () => _withBusy(_exportSearchResults),
                          results: _searchResults,
                          lastSearchTime: _lastSearchTime,
                          filesSearched: _blockFiles.length,
                        ),
                        _ExportTab(
                          preview: _exportPreview,
                          onExportTxt: () =>
                              _withBusy(() => _export(ExportFormat.txt)),
                          onExportJson: () =>
                              _withBusy(() => _export(ExportFormat.json)),
                          onExportCsv: () =>
                              _withBusy(() => _export(ExportFormat.csv)),
                        ),
                        _FilesTab(files: _blockFiles),
                      ],
                    ),
                  ),
                ],
              ),
      ),
    );
  }
}

class _DashboardTab extends StatelessWidget {
  final CTLSConfig? config;
  final CTLSWriter? writer;
  final int totalLogsWritten;
  final double lastWriteSpeed;
  final double lastSearchTime;
  final int rawStorageBytes;
  final int compressedStorageBytes;
  final int blockFilesCount;
  final List<LogEntry> allEntries;
  final VoidCallback onDump30Days;
  final VoidCallback onWrite1K;
  final VoidCallback onFlush;
  final VoidCallback onClearLogs;
  final VoidCallback onRefresh;
  final String Function(int) formatBytes;

  const _DashboardTab({
    required this.config,
    required this.writer,
    required this.totalLogsWritten,
    required this.lastWriteSpeed,
    required this.lastSearchTime,
    required this.rawStorageBytes,
    required this.compressedStorageBytes,
    required this.blockFilesCount,
    required this.allEntries,
    required this.onDump30Days,
    required this.onWrite1K,
    required this.onFlush,
    required this.onClearLogs,
    required this.onRefresh,
    required this.formatBytes,
  });

  int _calculateLogDuration() {
    if (allEntries.isEmpty) return 0;
    final first = allEntries.first.timestamp;
    final last = allEntries.last.timestamp;
    return last.difference(first).inDays + 1;
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Storage Efficiency Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          Icon(
                            Icons.storage,
                            color: Theme.of(context).colorScheme.primary,
                          ),
                          const SizedBox(width: 8),
                          Text(
                            'Storage Efficiency',
                            style: Theme.of(context).textTheme.titleLarge
                                ?.copyWith(fontWeight: FontWeight.bold),
                          ),
                        ],
                      ),
                      IconButton(
                        icon: const Icon(Icons.refresh),
                        onPressed: onRefresh,
                        tooltip: 'Refresh storage metrics',
                        style: IconButton.styleFrom(
                          foregroundColor: Theme.of(
                            context,
                          ).colorScheme.primary,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  if (totalLogsWritten == 0)
                    Center(
                      child: Padding(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                        child: Text(
                          'Generate logs to see storage metrics',
                          style: Theme.of(context).textTheme.bodyMedium
                              ?.copyWith(
                                color: Theme.of(context).colorScheme.outline,
                              ),
                        ),
                      ),
                    )
                  else ...[
                    // Compact storage bars
                    _CompactStorageBar(
                      label: 'Raw Data Size (without CTLS)',
                      value: formatBytes(rawStorageBytes),
                      color: Colors.blue,
                      percentage: 100,
                      tooltip:
                          'Estimated size if logs were stored as plain text JSON (≈148 bytes per log entry)',
                    ),
                    const SizedBox(height: 8),
                    _CompactStorageBar(
                      label: 'Compressed Size (with CTLS)',
                      value: formatBytes(compressedStorageBytes),
                      color: Colors.green,
                      percentage: rawStorageBytes > 0
                          ? (compressedStorageBytes / rawStorageBytes * 100)
                          : 0,
                      tooltip:
                          'Actual disk usage with CTLS columnar compression (gzip + delta encoding)',
                    ),
                    const SizedBox(height: 12),
                    // Compact stats grid
                    Container(
                      padding: const EdgeInsets.all(12),
                      decoration: BoxDecoration(
                        color: Colors.green.withValues(alpha: 0.1),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceAround,
                        children: [
                          _CompactStat(
                            label: 'Space Saved',
                            value: formatBytes(
                              rawStorageBytes - compressedStorageBytes,
                            ),
                            color: Colors.green,
                          ),
                          Container(
                            width: 1,
                            height: 30,
                            color: Colors.green.withValues(alpha: 0.3),
                          ),
                          _CompactStat(
                            label: 'Efficiency',
                            value:
                                '${rawStorageBytes > 0 ? (100 - (compressedStorageBytes / rawStorageBytes * 100)).toStringAsFixed(1) : "0"}%',
                            color: Colors.green,
                          ),
                        ],
                      ),
                    ),
                    const SizedBox(height: 12),
                    // Quick metrics row
                    Row(
                      children: [
                        Expanded(
                          child: _QuickMetric(
                            icon: Icons.description,
                            value:
                                '${(totalLogsWritten / 1000).toStringAsFixed(1)}K',
                            label: 'Logs',
                          ),
                        ),
                        Expanded(
                          child: _QuickMetric(
                            icon: Icons.folder,
                            value: '$blockFilesCount',
                            label: 'Files',
                          ),
                        ),
                        Expanded(
                          child: _QuickMetric(
                            icon: Icons.calendar_today,
                            value: '${_calculateLogDuration()}',
                            label: 'Days',
                          ),
                        ),
                      ],
                    ),
                  ],
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          // Performance Card
          if (totalLogsWritten > 0)
            Card(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(
                          Icons.speed,
                          color: Theme.of(context).colorScheme.primary,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          'Performance',
                          style: Theme.of(context).textTheme.titleLarge
                              ?.copyWith(fontWeight: FontWeight.bold),
                        ),
                      ],
                    ),
                    const SizedBox(height: 20),
                    _MetricRow(
                      icon: Icons.speed,
                      label: 'Write Speed',
                      value: '${(lastWriteSpeed / 1000).toStringAsFixed(1)}K/s',
                      color: Colors.orange,
                    ),
                    const Divider(),
                    _MetricRow(
                      icon: Icons.search,
                      label: 'Search Time',
                      value: '${lastSearchTime.toStringAsFixed(0)}ms',
                      color: Colors.purple,
                    ),
                  ],
                ),
              ),
            ),
          if (totalLogsWritten > 0) const SizedBox(height: 16),
          // Actions Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Icon(
                        Icons.play_circle,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                      const SizedBox(width: 8),
                      Text(
                        'Actions',
                        style: Theme.of(context).textTheme.titleLarge?.copyWith(
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  SizedBox(
                    width: double.infinity,
                    child: FilledButton.icon(
                      onPressed: writer == null ? null : onDump30Days,
                      icon: const Icon(Icons.cloud_download),
                      label: const Text('Generate 30 Days (300K+ logs)'),
                      style: FilledButton.styleFrom(
                        padding: const EdgeInsets.all(16),
                      ),
                    ),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Expanded(
                        child: FilledButton.tonalIcon(
                          onPressed: writer == null ? null : onWrite1K,
                          icon: const Icon(Icons.add),
                          label: Text(
                            'Write 1K\n(${writer?.entryCount ?? 0} buffered)',
                          ),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: FilledButton.icon(
                          onPressed: writer == null ? null : onFlush,
                          icon: const Icon(Icons.save),
                          label: const Text('Flush to Disk'),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  SizedBox(
                    width: double.infinity,
                    child: FilledButton.tonalIcon(
                      onPressed: config == null ? null : onClearLogs,
                      icon: const Icon(Icons.delete_sweep),
                      label: const Text('Clear All Logs'),
                      style: FilledButton.styleFrom(
                        backgroundColor: Colors.red.withValues(alpha: 0.1),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          // Info Card
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      const Icon(Icons.info_outline, size: 20),
                      const SizedBox(width: 8),
                      Text(
                        'Configuration',
                        style: Theme.of(context).textTheme.titleSmall,
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Text(
                    config == null
                        ? 'Initializing…'
                        : 'Directory: ${config!.logDirectory}',
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _MetricRow extends StatelessWidget {
  final IconData icon;
  final String label;
  final String value;
  final Color color;

  const _MetricRow({
    required this.icon,
    required this.label,
    required this.value,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: color.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Icon(icon, color: color, size: 24),
          ),
          const SizedBox(width: 16),
          Expanded(
            child: Text(label, style: Theme.of(context).textTheme.bodyMedium),
          ),
          Text(
            value,
            style: Theme.of(context).textTheme.headlineSmall?.copyWith(
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
        ],
      ),
    );
  }
}

class _CompactStorageBar extends StatelessWidget {
  final String label;
  final String value;
  final Color color;
  final double percentage;
  final String tooltip;

  const _CompactStorageBar({
    required this.label,
    required this.value,
    required this.color,
    required this.percentage,
    required this.tooltip,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Expanded(
              child: Row(
                children: [
                  Flexible(
                    child: Text(
                      label,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                  const SizedBox(width: 4),
                  GestureDetector(
                    onTap: () {
                      showDialog(
                        context: context,
                        builder: (context) => AlertDialog(
                          title: Text(label),
                          content: Text(tooltip),
                          actions: [
                            TextButton(
                              onPressed: () => Navigator.pop(context),
                              child: const Text('OK'),
                            ),
                          ],
                        ),
                      );
                    },
                    child: Icon(
                      Icons.info_outline,
                      size: 14,
                      color: Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ],
              ),
            ),
            Text(
              value,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                fontWeight: FontWeight.bold,
                color: color,
              ),
            ),
          ],
        ),
        const SizedBox(height: 6),
        Stack(
          children: [
            Container(
              height: 8,
              decoration: BoxDecoration(
                color: color.withValues(alpha: 0.2),
                borderRadius: BorderRadius.circular(4),
              ),
            ),
            FractionallySizedBox(
              widthFactor: percentage / 100,
              child: Container(
                height: 8,
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: BorderRadius.circular(4),
                ),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

class _CompactStat extends StatelessWidget {
  final String label;
  final String value;
  final Color color;

  const _CompactStat({
    required this.label,
    required this.value,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(
          label,
          style: Theme.of(context).textTheme.bodySmall?.copyWith(
            color: Theme.of(context).colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: 4),
        Text(
          value,
          style: Theme.of(context).textTheme.titleMedium?.copyWith(
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
      ],
    );
  }
}

class _QuickMetric extends StatelessWidget {
  final IconData icon;
  final String value;
  final String label;

  const _QuickMetric({
    required this.icon,
    required this.value,
    required this.label,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Column(
        children: [
          Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
          const SizedBox(height: 4),
          Text(
            value,
            style: Theme.of(
              context,
            ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
          ),
          Text(label, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}

class _LogsTab extends StatelessWidget {
  final List<LogEntry> entries;

  const _LogsTab({required this.entries});

  @override
  Widget build(BuildContext context) {
    if (entries.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.inbox,
              size: 64,
              color: Theme.of(context).colorScheme.outline,
            ),
            const SizedBox(height: 16),
            Text('No logs yet', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            Text(
              'Go to Dashboard to generate logs',
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ],
        ),
      );
    }

    return ListView.separated(
      itemCount: entries.length,
      padding: const EdgeInsets.all(8),
      separatorBuilder: (_, __) => const Divider(height: 1),
      itemBuilder: (context, index) {
        final e = entries[index];
        return ListTile(
          title: Text(e.message),
          subtitle: Text('${e.timestamp.toIso8601String()}  ${e.tag}'),
          trailing: Text(e.level.name.toUpperCase()),
          dense: true,
        );
      },
    );
  }
}

class _SearchTab extends StatelessWidget {
  final TextEditingController messageContainsController;
  final TextEditingController tagFilterController;
  final LogLevel? levelFilter;
  final DateTime? startDate;
  final DateTime? endDate;
  final ValueChanged<LogLevel?> onLevelChanged;
  final ValueChanged<DateTime?> onStartDateChanged;
  final ValueChanged<DateTime?> onEndDateChanged;
  final VoidCallback onSearch;
  final VoidCallback onExport;
  final List<LogEntry> results;
  final double lastSearchTime;
  final int filesSearched;

  const _SearchTab({
    required this.messageContainsController,
    required this.tagFilterController,
    required this.levelFilter,
    required this.startDate,
    required this.endDate,
    required this.onLevelChanged,
    required this.onStartDateChanged,
    required this.onEndDateChanged,
    required this.onSearch,
    required this.onExport,
    required this.results,
    required this.lastSearchTime,
    required this.filesSearched,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Compact Search Form
        Padding(
          padding: const EdgeInsets.all(12),
          child: Column(
            children: [
              TextField(
                controller: messageContainsController,
                decoration: InputDecoration(
                  labelText: 'Search message',
                  hintText: 'e.g., "error", "timeout"',
                  prefixIcon: const Icon(Icons.search),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                  contentPadding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 8,
                  ),
                ),
                onSubmitted: (_) => onSearch(),
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: tagFilterController,
                      decoration: InputDecoration(
                        labelText: 'Tag',
                        prefixIcon: const Icon(Icons.label, size: 20),
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                        contentPadding: const EdgeInsets.symmetric(
                          horizontal: 12,
                          vertical: 8,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: DropdownButtonFormField<LogLevel?>(
                      initialValue: levelFilter,
                      items: [
                        const DropdownMenuItem<LogLevel?>(
                          value: null,
                          child: Text('All levels'),
                        ),
                        ...LogLevel.values.map(
                          (l) => DropdownMenuItem<LogLevel?>(
                            value: l,
                            child: Text(l.name.toUpperCase()),
                          ),
                        ),
                      ],
                      onChanged: onLevelChanged,
                      decoration: InputDecoration(
                        labelText: 'Level',
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                        contentPadding: const EdgeInsets.symmetric(
                          horizontal: 12,
                          vertical: 8,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    child: InkWell(
                      onTap: () async {
                        final date = await showDatePicker(
                          context: context,
                          initialDate: startDate ?? DateTime.now(),
                          firstDate: DateTime(2020),
                          lastDate: DateTime.now(),
                        );
                        if (date != null) onStartDateChanged(date);
                      },
                      child: InputDecorator(
                        decoration: InputDecoration(
                          labelText: 'From Date',
                          prefixIcon: const Icon(
                            Icons.calendar_today,
                            size: 20,
                          ),
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                          contentPadding: const EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 8,
                          ),
                        ),
                        child: Text(
                          startDate != null
                              ? startDate!.toString().split(' ')[0]
                              : 'Any',
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: InkWell(
                      onTap: () async {
                        final date = await showDatePicker(
                          context: context,
                          initialDate: endDate ?? DateTime.now(),
                          firstDate: DateTime(2020),
                          lastDate: DateTime.now(),
                        );
                        if (date != null) onEndDateChanged(date);
                      },
                      child: InputDecorator(
                        decoration: InputDecoration(
                          labelText: 'To Date',
                          prefixIcon: const Icon(Icons.event, size: 20),
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                          contentPadding: const EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 8,
                          ),
                        ),
                        child: Text(
                          endDate != null
                              ? endDate!.toString().split(' ')[0]
                              : 'Any',
                          style: Theme.of(context).textTheme.bodyMedium,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Expanded(
                    flex: 2,
                    child: FilledButton.icon(
                      onPressed: onSearch,
                      icon: const Icon(Icons.search, size: 20),
                      label: const Text('Search'),
                      style: FilledButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: FilledButton.tonalIcon(
                      onPressed: results.isEmpty ? null : onExport,
                      icon: const Icon(Icons.file_download, size: 20),
                      label: const Text('Export'),
                      style: FilledButton.styleFrom(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                      ),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
        // Results
        Expanded(
          child: results.isEmpty
              ? const SizedBox.shrink()
              : Column(
                  children: [
                    // Results Header with metrics
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Theme.of(context).colorScheme.primaryContainer,
                        border: Border(
                          bottom: BorderSide(
                            color: Theme.of(context).colorScheme.outline,
                            width: 1,
                          ),
                        ),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: [
                              Row(
                                children: [
                                  Icon(
                                    Icons.check_circle,
                                    color: Theme.of(
                                      context,
                                    ).colorScheme.primary,
                                    size: 24,
                                  ),
                                  const SizedBox(width: 8),
                                  Text(
                                    '${results.length} results found',
                                    style: Theme.of(context)
                                        .textTheme
                                        .titleMedium
                                        ?.copyWith(
                                          fontWeight: FontWeight.bold,
                                          color: Theme.of(
                                            context,
                                          ).colorScheme.onPrimaryContainer,
                                        ),
                                  ),
                                ],
                              ),
                              if (lastSearchTime > 0)
                                Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 12,
                                    vertical: 6,
                                  ),
                                  decoration: BoxDecoration(
                                    color: Colors.green.withValues(alpha: 0.2),
                                    borderRadius: BorderRadius.circular(12),
                                  ),
                                  child: Row(
                                    children: [
                                      const Icon(
                                        Icons.speed,
                                        size: 16,
                                        color: Colors.green,
                                      ),
                                      const SizedBox(width: 4),
                                      Text(
                                        '${lastSearchTime.toStringAsFixed(0)}ms',
                                        style: const TextStyle(
                                          fontWeight: FontWeight.bold,
                                          color: Colors.green,
                                        ),
                                      ),
                                    ],
                                  ),
                                ),
                            ],
                          ),
                          if (results.isNotEmpty) ...[
                            const SizedBox(height: 12),
                            Wrap(
                              spacing: 16,
                              runSpacing: 8,
                              children: [
                                Row(
                                  mainAxisSize: MainAxisSize.min,
                                  children: [
                                    Icon(
                                      Icons.calendar_today,
                                      size: 16,
                                      color: Theme.of(context)
                                          .colorScheme
                                          .onPrimaryContainer
                                          .withValues(alpha: 0.7),
                                    ),
                                    const SizedBox(width: 6),
                                    Text(
                                      '${results.first.timestamp.toLocal().toString().split(' ')[0]} → ${results.last.timestamp.toLocal().toString().split(' ')[0]}',
                                      style: Theme.of(context)
                                          .textTheme
                                          .bodySmall
                                          ?.copyWith(
                                            color: Theme.of(context)
                                                .colorScheme
                                                .onPrimaryContainer
                                                .withValues(alpha: 0.7),
                                          ),
                                    ),
                                  ],
                                ),
                                Row(
                                  mainAxisSize: MainAxisSize.min,
                                  children: [
                                    Icon(
                                      Icons.folder_open,
                                      size: 16,
                                      color: Theme.of(context)
                                          .colorScheme
                                          .onPrimaryContainer
                                          .withValues(alpha: 0.7),
                                    ),
                                    const SizedBox(width: 6),
                                    Text(
                                      '$filesSearched files searched',
                                      style: Theme.of(context)
                                          .textTheme
                                          .bodySmall
                                          ?.copyWith(
                                            color: Theme.of(context)
                                                .colorScheme
                                                .onPrimaryContainer
                                                .withValues(alpha: 0.7),
                                          ),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                          ],
                        ],
                      ),
                    ),
                    // Results List
                    Expanded(
                      child: ListView.builder(
                        padding: const EdgeInsets.all(8),
                        itemCount: results.length,
                        itemBuilder: (context, index) {
                          final e = results[index];
                          final levelColor = switch (e.level) {
                            LogLevel.debug => Colors.grey,
                            LogLevel.info => Colors.blue,
                            LogLevel.warning => Colors.orange,
                            LogLevel.error => Colors.red,
                            LogLevel.critical => Colors.purple,
                          };

                          return Card(
                            margin: const EdgeInsets.only(bottom: 8),
                            child: ListTile(
                              leading: CircleAvatar(
                                backgroundColor: levelColor.withValues(
                                  alpha: 0.2,
                                ),
                                child: Icon(
                                  Icons.circle,
                                  color: levelColor,
                                  size: 12,
                                ),
                              ),
                              title: Text(
                                e.message,
                                maxLines: 2,
                                overflow: TextOverflow.ellipsis,
                              ),
                              subtitle: Text(
                                '${e.timestamp.toIso8601String().split('T')[1].split('.')[0]} • ${e.tag}',
                                style: TextStyle(fontSize: 12),
                              ),
                              trailing: Chip(
                                label: Text(
                                  e.level.name.toUpperCase(),
                                  style: TextStyle(
                                    fontSize: 10,
                                    color: levelColor,
                                  ),
                                ),
                                backgroundColor: levelColor.withValues(
                                  alpha: 0.1,
                                ),
                                side: BorderSide.none,
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ],
                ),
        ),
      ],
    );
  }
}

class _ExportTab extends StatelessWidget {
  final String preview;
  final VoidCallback onExportTxt;
  final VoidCallback onExportJson;
  final VoidCallback onExportCsv;

  const _ExportTab({
    required this.preview,
    required this.onExportTxt,
    required this.onExportJson,
    required this.onExportCsv,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(12),
          child: Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              FilledButton.tonal(
                onPressed: onExportTxt,
                child: const Text('TXT'),
              ),
              FilledButton.tonal(
                onPressed: onExportJson,
                child: const Text('JSON'),
              ),
              FilledButton.tonal(
                onPressed: onExportCsv,
                child: const Text('CSV'),
              ),
            ],
          ),
        ),
        const Divider(height: 1),
        Expanded(
          child: preview.isEmpty
              ? const Center(child: Text('Export preview will appear here.'))
              : SingleChildScrollView(
                  padding: const EdgeInsets.all(12),
                  child: SelectableText(preview),
                ),
        ),
      ],
    );
  }
}

class _FilesTab extends StatelessWidget {
  final List<File> files;

  const _FilesTab({required this.files});

  String _formatBytes(int bytes) {
    if (bytes < 1024) return '$bytes B';
    if (bytes < 1024 * 1024) {
      return '${(bytes / 1024).toStringAsFixed(1)} KB';
    }
    return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
  }

  String _getFileAge(File file) {
    try {
      if (!file.existsSync()) return 'N/A';
      final stat = file.statSync();
      final age = DateTime.now().difference(stat.modified);
      if (age.inDays > 0) return '${age.inDays}d ago';
      if (age.inHours > 0) return '${age.inHours}h ago';
      return '${age.inMinutes}m ago';
    } catch (_) {
      return 'N/A';
    }
  }

  @override
  Widget build(BuildContext context) {
    if (files.isEmpty) {
      return const Center(child: Text('No block files yet.'));
    }

    return Column(
      children: [
        // Log Rotation Info Banner
        Container(
          padding: const EdgeInsets.all(12),
          margin: const EdgeInsets.all(8),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.primaryContainer,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Row(
            children: [
              Icon(
                Icons.auto_delete,
                color: Theme.of(context).colorScheme.primary,
                size: 20,
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  'Log Rotation: Files older than 30 days are automatically deleted',
                  style: Theme.of(
                    context,
                  ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
                ),
              ),
            ],
          ),
        ),
        // Files List
        Expanded(
          child: ListView.separated(
            itemCount: files.length,
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            separatorBuilder: (_, __) => const Divider(height: 1),
            itemBuilder: (context, index) {
              final f = files[index];
              final name = f.uri.pathSegments.isEmpty
                  ? f.path
                  : f.uri.pathSegments.last;
              final size = f.existsSync() ? f.lengthSync() : 0;
              final age = _getFileAge(f);

              return ListTile(
                leading: Icon(
                  Icons.insert_drive_file,
                  size: 20,
                  color: Theme.of(context).colorScheme.primary,
                ),
                title: Text(name, style: const TextStyle(fontSize: 13)),
                subtitle: Text(
                  'Age: $age',
                  style: const TextStyle(fontSize: 11),
                ),
                trailing: Text(
                  _formatBytes(size),
                  style: const TextStyle(
                    fontSize: 12,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                dense: true,
              );
            },
          ),
        ),
      ],
    );
  }
}
0
likes
160
points
42
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

High-performance columnar log storage system with adaptive tiering, compression, and intelligent query optimization for Dart/Flutter apps.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

archive, crypto, path

More

Packages that depend on ctls_logging