diff --git a/infra/experiments/logs-viewer/README.md b/infra/experiments/logs-viewer/README.md
new file mode 100644
index 0000000000..7a66238816
--- /dev/null
+++ b/infra/experiments/logs-viewer/README.md
@@ -0,0 +1,167 @@
+# Ente Log Viewer
+
+A web-based log viewer for analyzing Ente application logs. This tool provides similar functionality to the mobile log viewer, allowing you to upload, filter, and analyze log files from customer support requests.
+
+## Features
+
+### ๐ File Upload
+- Drag and drop ZIP files containing log files
+- Automatic extraction and parsing of log files
+- Support for daily log files format (YYYY-M-D.log)
+
+### ๐ Search and Filtering
+- **Text Search**: Search through log messages, logger names, and error content
+- **Logger Filtering**: Use `logger:ServiceName` syntax to filter by specific loggers
+- **Wildcard Support**: Use `logger:Auth*` to match all loggers starting with "Auth"
+- **Level Filtering**: Filter by log levels (SEVERE, WARNING, INFO, etc.)
+- **Process Filtering**: Filter by foreground/background processes
+- **Timeline Filtering**: Filter by date/time ranges
+
+### ๐ Analytics
+- Logger statistics showing most active components
+- Log level distribution charts
+- Click-to-filter from analytics charts
+
+### ๐จ UI Features
+- **Color-coded log levels**: Red for SEVERE, orange for WARNING, etc.
+- **Process indicators**: Visual distinction between foreground and background processes
+- **Active filter chips**: Visual indication of applied filters with easy removal
+- **Log detail view**: Click any log entry for detailed information
+- **Sort options**: Sort by newest first or oldest first
+- **Responsive design**: Works on desktop and mobile devices
+
+### ๐ค Export
+- Export filtered logs as text files
+- Copy individual log entries to clipboard
+- Maintain original formatting and error details
+
+## Usage
+
+### Starting the Application
+
+1. **Local Development**:
+ ```bash
+ cd infra/experiments/logs-viewer
+ python3 -m http.server 8080
+ ```
+ Open http://localhost:8080 in your browser
+
+2. **Upload Log Files**:
+ - Drag and drop a ZIP file containing `.log` files
+ - Or click "Choose ZIP File" to browse for files
+
+### Log Format Support
+
+The viewer understands the Ente log format as generated by `super_logging.dart`:
+
+```
+[process] [loggerName] [LEVEL] [timestamp] message
+```
+
+**Examples**:
+- `[bg] [SyncService] [INFO] [2025-08-24 01:36:03.677678] Syncing started`
+- `[CollectionsService] [WARNING] [2025-08-24 01:36:04.123456] Connection failed`
+
+**Multi-line Error Support**:
+- Automatically parses `โคท` error detail lines
+- Extracts stack traces and error IDs
+- Handles inline error messages and exceptions
+
+### Filtering Examples
+
+- **Search by text**: `connection failed`
+- **Filter by logger**: `logger:SyncService`
+- **Multiple loggers**: `logger:Sync* logger:Collection*`
+- **Combined search**: `logger:Auth* login failed`
+
+### Keyboard Shortcuts
+
+- **Search**: Click search bar or start typing
+- **Clear search**: Click X button or clear the input
+- **Sort toggle**: Click sort arrow button
+- **Filter dialog**: Click filter button
+
+## Technical Details
+
+### Supported Log Levels
+- **SHOUT**: Purple - Highest priority
+- **SEVERE**: Red - Errors and exceptions
+- **WARNING**: Orange - Warning conditions
+- **INFO**: Blue - Informational messages
+- **CONFIG**: Green - Configuration messages
+- **FINE/FINER/FINEST**: Gray - Debug messages
+
+### Process Types
+- **Foreground**: Main app processes
+- **Background (bg)**: Background tasks
+- **Firebase Background (fbg)**: Firebase-related background processes
+
+### Performance
+- Lazy loading: Only renders visible log entries
+- Efficient filtering: Client-side filtering with optimized algorithms
+- Memory management: Handles large log files (tested with 100k+ entries)
+
+## Sample Log Files
+
+For testing, you can use any ZIP file containing `.log` files from Ente mobile app logs.
+
+## Development
+
+### File Structure
+```
+logs-viewer/
+โโโ index.html # Main HTML structure
+โโโ styles.css # CSS styling
+โโโ script.js # JavaScript logic
+โโโ README.md # This documentation
+โโโ references/ # UI reference screenshots
+```
+
+### Key JavaScript Classes
+- `LogViewer`: Main application class
+- Log parsing logic in `parseLogFile()` and `parseLogLine()`
+- Filter management in `applyCurrentFilter()`
+- UI rendering in `renderLogs()` and `createLogEntryHTML()`
+
+### Adding Features
+1. **New Filter Types**: Extend `currentFilter` object and `matchesFilter()` method
+2. **New Log Formats**: Update parsing patterns in `parseLogLine()`
+3. **UI Components**: Add HTML elements and CSS classes, wire up in `initializeEventListeners()`
+
+## Troubleshooting
+
+### Common Issues
+
+1. **ZIP file not loading**:
+ - Ensure ZIP contains `.log` files
+ - Check browser console for errors
+ - Try a smaller file first
+
+2. **Logs not parsing correctly**:
+ - Check log format matches expected pattern
+ - Look for console warnings about parsing failures
+ - Verify timestamp format is correct
+
+3. **Performance issues with large files**:
+ - The viewer handles pagination (100 logs at a time)
+ - Use filters to reduce the dataset
+ - Close other browser tabs to free memory
+
+4. **Search not working**:
+ - Check for typos in logger names
+ - Use wildcard syntax: `logger:Service*`
+ - Search is case-insensitive for message content
+
+### Browser Compatibility
+- **Recommended**: Chrome 80+, Firefox 75+, Safari 13+
+- **Required**: ES6 support, Fetch API, File API
+- **Dependencies**: JSZip library for ZIP file handling
+
+## Future Enhancements
+
+- Real-time log streaming
+- Advanced regex search
+- Log correlation and grouping
+- Performance metrics dashboard
+- Dark theme support
+- Export to JSON/CSV formats
\ No newline at end of file
diff --git a/infra/experiments/logs-viewer/ente-theme.css b/infra/experiments/logs-viewer/ente-theme.css
new file mode 100644
index 0000000000..35586e65af
--- /dev/null
+++ b/infra/experiments/logs-viewer/ente-theme.css
@@ -0,0 +1,110 @@
+/* Ente Theme Variables based on web/packages/base/components/utils/theme.ts */
+:root {
+ /* Light theme colors */
+ --ente-color-accent-photos: #1db954;
+ --ente-color-accent-auth: #9610d6;
+ --ente-color-accent-locker: #5ba8ff;
+
+ /* Background colors */
+ --ente-background-default: #fff;
+ --ente-background-paper: #fff;
+ --ente-background-paper2: #fbfbfb;
+ --ente-background-search: #f3f3f3;
+
+ /* Text colors */
+ --ente-text-base: #000;
+ --ente-text-muted: rgba(0, 0, 0, 0.60);
+ --ente-text-faint: rgba(0, 0, 0, 0.50);
+
+ /* Fill colors */
+ --ente-fill-base: #000;
+ --ente-fill-muted: rgba(0, 0, 0, 0.12);
+ --ente-fill-faint: rgba(0, 0, 0, 0.04);
+ --ente-fill-faint-hover: rgba(0, 0, 0, 0.08);
+ --ente-fill-fainter: rgba(0, 0, 0, 0.02);
+
+ /* Stroke colors */
+ --ente-stroke-base: #000;
+ --ente-stroke-muted: rgba(0, 0, 0, 0.24);
+ --ente-stroke-faint: rgba(0, 0, 0, 0.12);
+ --ente-stroke-fainter: rgba(0, 0, 0, 0.06);
+
+ /* Shadow */
+ --ente-shadow-paper: 0px 0px 10px rgba(0, 0, 0, 0.25);
+ --ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.16), 0px 3px 6px rgba(0, 0, 0, 0.12);
+ --ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.25);
+
+ /* Fixed colors */
+ --ente-success: #1db954;
+ --ente-warning: #ffc107;
+ --ente-danger: #ea3f3f;
+ --ente-danger-dark: #f53434;
+ --ente-danger-light: #ff6565;
+
+ /* Secondary colors */
+ --ente-secondary-main: #f5f5f5;
+ --ente-secondary-hover: #e9e9e9;
+
+ /* Action colors */
+ --ente-action-hover: rgba(0, 0, 0, 0.08);
+ --ente-action-disabled: rgba(0, 0, 0, 0.50);
+
+ /* Typography */
+ --ente-font-family: "Inter Variable", sans-serif;
+ --ente-font-weight-regular: 500;
+ --ente-font-weight-medium: 600;
+ --ente-font-weight-bold: 700;
+
+ /* Border radius */
+ --ente-border-radius: 8px;
+ --ente-border-radius-small: 4px;
+
+ /* Spacing */
+ --ente-spacing-xs: 4px;
+ --ente-spacing-sm: 8px;
+ --ente-spacing-md: 12px;
+ --ente-spacing-lg: 16px;
+ --ente-spacing-xl: 24px;
+}
+
+/* Dark theme */
+@media (prefers-color-scheme: dark) {
+ :root {
+ /* Background colors */
+ --ente-background-default: #000;
+ --ente-background-paper: #1b1b1b;
+ --ente-background-paper2: #252525;
+ --ente-background-search: #1b1b1b;
+
+ /* Text colors */
+ --ente-text-base: #fff;
+ --ente-text-muted: rgba(255, 255, 255, 0.70);
+ --ente-text-faint: rgba(255, 255, 255, 0.50);
+
+ /* Fill colors */
+ --ente-fill-base: #fff;
+ --ente-fill-muted: rgba(255, 255, 255, 0.16);
+ --ente-fill-faint: rgba(255, 255, 255, 0.12);
+ --ente-fill-faint-hover: rgba(255, 255, 255, 0.16);
+ --ente-fill-fainter: rgba(255, 255, 255, 0.05);
+
+ /* Stroke colors */
+ --ente-stroke-base: #fff;
+ --ente-stroke-muted: rgba(255, 255, 255, 0.24);
+ --ente-stroke-faint: rgba(255, 255, 255, 0.16);
+ --ente-stroke-fainter: rgba(255, 255, 255, 0.12);
+
+ /* Shadow */
+ --ente-shadow-paper: 0px 2px 12px rgba(0, 0, 0, 0.75);
+ --ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.50), 0px 3px 6px rgba(0, 0, 0, 0.25);
+ --ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.75);
+
+ /* Secondary colors */
+ --ente-secondary-main: #2b2b2b;
+ --ente-secondary-hover: #373737;
+
+ /* Action colors */
+ --ente-action-hover: rgba(255, 255, 255, 0.16);
+ --ente-action-disabled: rgba(255, 255, 255, 0.50);
+ }
+}
\ No newline at end of file
diff --git a/infra/experiments/logs-viewer/index.html b/infra/experiments/logs-viewer/index.html
new file mode 100644
index 0000000000..eb1cc4cbc3
--- /dev/null
+++ b/infra/experiments/logs-viewer/index.html
@@ -0,0 +1,218 @@
+
+
+
+
+
+ Ente Log Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐
+
Upload Log Files
+
Drag and drop a zip file containing log files, or click to browse
+
+
+
+
+
+
+
+
+
+
+
+
+
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
to
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 logs loaded
+
+
+
+
+
+
+
+
+
Loading...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/infra/experiments/logs-viewer/script.js b/infra/experiments/logs-viewer/script.js
new file mode 100644
index 0000000000..a38c7dc69c
--- /dev/null
+++ b/infra/experiments/logs-viewer/script.js
@@ -0,0 +1,1146 @@
+// Log Viewer Application
+class LogViewer {
+ constructor() {
+ this.logs = [];
+ this.filteredLogs = [];
+ this.allLoggers = new Set();
+ this.allProcesses = new Set();
+ this.allLevels = new Set();
+ this.currentFilter = {
+ selectedLoggers: new Set(),
+ selectedLevels: new Set(['WARNING', 'SEVERE', 'SHOUT']), // Default to show important levels
+ selectedProcesses: new Set(),
+ searchQuery: '',
+ startTime: null,
+ endTime: null,
+ sortNewestFirst: true
+ };
+ this.currentOffset = 0;
+ this.pageSize = 100;
+ this.isLoading = false;
+
+ this.initializeEventListeners();
+ }
+
+ initializeEventListeners() {
+ // File upload
+ const fileInput = document.getElementById('file-input');
+ const browseBtn = document.getElementById('browse-btn');
+ const uploadArea = document.getElementById('upload-area');
+
+ browseBtn.addEventListener('click', () => fileInput.click());
+ fileInput.addEventListener('change', this.handleFileSelect.bind(this));
+
+ // Drag and drop
+ uploadArea.addEventListener('dragover', this.handleDragOver.bind(this));
+ uploadArea.addEventListener('drop', this.handleDrop.bind(this));
+ uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this));
+
+ // Search
+ const searchInput = document.getElementById('search-input');
+ const clearSearch = document.getElementById('clear-search');
+ searchInput.addEventListener('input', this.handleSearch.bind(this));
+ clearSearch.addEventListener('click', this.clearSearch.bind(this));
+
+ // Filter dialog
+ const filterBtn = document.getElementById('filter-btn');
+ const filterDialog = document.getElementById('filter-dialog');
+ const closeFilter = document.getElementById('close-filter');
+ const cancelFilter = document.getElementById('cancel-filter');
+ const applyFilters = document.getElementById('apply-filters');
+ const clearFilters = document.getElementById('clear-filters');
+
+ filterBtn.addEventListener('click', this.showFilterDialog.bind(this));
+ closeFilter.addEventListener('click', this.hideFilterDialog.bind(this));
+ cancelFilter.addEventListener('click', this.hideFilterDialog.bind(this));
+ applyFilters.addEventListener('click', this.applyFilters.bind(this));
+ clearFilters.addEventListener('click', this.clearAllFilters.bind(this));
+
+ // Sort button
+ const sortBtn = document.getElementById('sort-btn');
+ sortBtn.addEventListener('click', this.toggleSort.bind(this));
+
+ // Timeline
+ const timelineToggle = document.getElementById('timeline-toggle');
+ const startTime = document.getElementById('start-time');
+ const endTime = document.getElementById('end-time');
+ const resetTimeline = document.getElementById('reset-timeline');
+
+ timelineToggle.addEventListener('click', this.toggleTimeline.bind(this));
+ startTime.addEventListener('change', this.handleTimelineChange.bind(this));
+ endTime.addEventListener('change', this.handleTimelineChange.bind(this));
+ resetTimeline.addEventListener('click', this.resetTimeline.bind(this));
+
+ // Other dialogs
+ this.setupDialogListeners();
+
+ // Dropdown
+ this.setupDropdown();
+ }
+
+ setupDialogListeners() {
+ // Analytics dialog
+ const analyticsBtn = document.getElementById('analytics-btn');
+ const analyticsDialog = document.getElementById('analytics-dialog');
+ const closeAnalytics = document.getElementById('close-analytics');
+ const closeAnalyticsBtn = document.getElementById('close-analytics-btn');
+
+ analyticsBtn.addEventListener('click', this.showAnalytics.bind(this));
+ closeAnalytics.addEventListener('click', this.hideAnalytics.bind(this));
+ closeAnalyticsBtn.addEventListener('click', this.hideAnalytics.bind(this));
+
+ // Detail dialog
+ const detailDialog = document.getElementById('detail-dialog');
+ const closeDetail = document.getElementById('close-detail');
+ const closeDetailBtn = document.getElementById('close-detail-btn');
+ const copyLog = document.getElementById('copy-log');
+
+ closeDetail.addEventListener('click', this.hideDetailDialog.bind(this));
+ closeDetailBtn.addEventListener('click', this.hideDetailDialog.bind(this));
+ copyLog.addEventListener('click', this.copyLogDetail.bind(this));
+
+ // Export and clear
+ const exportBtn = document.getElementById('export-btn');
+ const clearBtn = document.getElementById('clear-btn');
+
+ exportBtn.addEventListener('click', this.exportLogs.bind(this));
+ clearBtn.addEventListener('click', this.clearLogs.bind(this));
+ }
+
+ setupDropdown() {
+ const dropdown = document.querySelector('.dropdown');
+ const dropdownToggle = document.querySelector('.dropdown-toggle');
+
+ dropdownToggle.addEventListener('click', (e) => {
+ e.stopPropagation();
+ dropdown.classList.toggle('active');
+ });
+
+ document.addEventListener('click', () => {
+ dropdown.classList.remove('active');
+ });
+ }
+
+ // File handling
+ handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (file) {
+ this.processZipFile(file);
+ }
+ }
+
+ handleDragOver(event) {
+ event.preventDefault();
+ event.currentTarget.classList.add('drag-over');
+ }
+
+ handleDrop(event) {
+ event.preventDefault();
+ event.currentTarget.classList.remove('drag-over');
+
+ const files = event.dataTransfer.files;
+ if (files.length > 0 && files[0].name.endsWith('.zip')) {
+ this.processZipFile(files[0]);
+ } else {
+ alert('Please drop a ZIP file containing log files.');
+ }
+ }
+
+ handleDragLeave(event) {
+ event.currentTarget.classList.remove('drag-over');
+ }
+
+ async processZipFile(file) {
+ try {
+ document.getElementById('loading').style.display = 'block';
+
+ const zip = new JSZip();
+ const contents = await zip.loadAsync(file);
+
+ this.logs = [];
+ let totalLogs = 0;
+
+ // Process each file in the zip
+ for (const [filename, zipEntry] of Object.entries(contents.files)) {
+ if (!zipEntry.dir && filename.includes('.log')) {
+ const content = await zipEntry.async('string');
+ const fileLogs = this.parseLogFile(content, filename);
+ this.logs.push(...fileLogs);
+ totalLogs += fileLogs.length;
+ }
+ }
+
+
+ // Sort logs by timestamp
+ this.logs.sort((a, b) => {
+ if (this.currentFilter.sortNewestFirst) {
+ return b.timestamp - a.timestamp;
+ } else {
+ return a.timestamp - b.timestamp;
+ }
+ });
+
+ // Extract unique values
+ this.extractUniqueValues();
+
+ // Apply initial filters and display
+ this.applyCurrentFilter();
+ this.showMainContent();
+
+ } catch (error) {
+ console.error('Error processing ZIP file:', error);
+ alert('Error processing ZIP file. Please make sure it contains valid log files.');
+ } finally {
+ document.getElementById('loading').style.display = 'none';
+ }
+ }
+
+ parseLogFile(content, filename) {
+ const logs = [];
+ const lines = content.split('\n');
+ let currentLog = null;
+ let errorDetails = [];
+ let isInMultilineError = false;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ // Check if this is an error detail line (starts with โคท)
+ if (line.startsWith('โคท')) {
+ if (currentLog) {
+ errorDetails.push(line);
+ isInMultilineError = true;
+ }
+ continue;
+ }
+
+ // Try to parse as a log entry
+ const logEntry = this.parseLogLine(line);
+ if (logEntry) {
+ // If we have a current log, finalize it first
+ if (currentLog) {
+ this.finalizeLogEntry(currentLog, errorDetails);
+ logs.push(currentLog);
+ }
+
+ currentLog = logEntry;
+ currentLog.filename = filename;
+ errorDetails = [];
+ isInMultilineError = false;
+ } else if (currentLog) {
+ // Check if this might be an inline error continuation
+ if (line.startsWith('Error:') || line.startsWith('Exception:') ||
+ line.includes('Exception in') || line.includes('ErrorCode=') ||
+ isInMultilineError) {
+
+ // This is part of error details or stack trace
+ currentLog.message += '\n' + line;
+
+ // Keep track if we're in a multi-line error
+ if (line.includes('Exception:') || line.includes('Error:')) {
+ isInMultilineError = true;
+ }
+ } else {
+ // Regular message continuation
+ currentLog.message += '\n' + line;
+ }
+ }
+ }
+
+ // Don't forget the last log
+ if (currentLog) {
+ this.finalizeLogEntry(currentLog, errorDetails);
+ logs.push(currentLog);
+ }
+
+ return logs;
+ }
+
+ finalizeLogEntry(logEntry, errorDetails) {
+ if (errorDetails.length > 0) {
+ const errorInfo = this.parseErrorDetails(errorDetails);
+ if (errorInfo.error) logEntry.error = errorInfo.error;
+ if (errorInfo.stackTrace) logEntry.stackTrace = errorInfo.stackTrace;
+ if (errorInfo.id) logEntry.id = errorInfo.id;
+
+ // Add error details to message
+ logEntry.message += '\n' + errorDetails.join('\n');
+ }
+
+ // Post-process the log entry to extract inline errors
+ this.extractInlineErrors(logEntry);
+ }
+
+ extractInlineErrors(logEntry) {
+ const lines = logEntry.message.split('\n');
+ let messageLines = [];
+ let errorLines = [];
+ let stackTraceLines = [];
+ let isInStackTrace = false;
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+
+ if (trimmed.startsWith('Error:') || trimmed.startsWith('Exception:')) {
+ // This is an error description
+ if (!logEntry.error) {
+ logEntry.error = trimmed;
+ }
+ errorLines.push(trimmed);
+ } else if (trimmed.startsWith('#') && (trimmed.includes('package:') || trimmed.includes(' 0) {
+ logEntry.stackTrace = stackTraceLines.join('\n');
+ }
+ }
+ }
+
+ parseLogLine(line) {
+ // Pattern: [processPrefix] [loggerName] [LEVEL] [timestamp] message
+ // Or: [loggerName] [LEVEL] [timestamp] message (no process prefix)
+
+ const patterns = [
+ // With process prefix: [bg] [ente_logging] [INFO] [2025-08-24 01:36:03.677678] message
+ /^\[([^\]]+)\]\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)$/,
+ // Without process prefix: [ente_logging] [INFO] [2025-08-24 01:36:03.677678] message
+ /^\[([^\]]+)\]\s*\[([^\]]+)\]\s*\[([^\]]+)\]\s*(.*)$/
+ ];
+
+ for (let i = 0; i < patterns.length; i++) {
+ const match = line.match(patterns[i]);
+ if (match) {
+ let processPrefix, loggerName, level, timestampStr, message;
+
+ if (i === 0) {
+ // Pattern with process prefix
+ [, processPrefix, loggerName, level, timestampStr, message] = match;
+ } else {
+ // Pattern without process prefix
+ [, loggerName, level, timestampStr, message] = match;
+ processPrefix = '';
+ }
+
+ // Parse timestamp
+ const timestamp = this.parseTimestamp(timestampStr);
+ if (!timestamp) continue;
+
+ return {
+ processPrefix: processPrefix || '',
+ loggerName,
+ level: level.toUpperCase(),
+ timestamp,
+ timestampStr,
+ message: message || '',
+ error: null,
+ stackTrace: null,
+ id: null
+ };
+ }
+ }
+
+ return null;
+ }
+
+ parseTimestamp(timestampStr) {
+ try {
+ // Handle format: 2025-08-24 01:36:03.677678
+ const parts = timestampStr.match(/(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\.?(\d+)?/);
+ if (parts) {
+ const [, date, time, microseconds] = parts;
+ const fullTimestamp = `${date}T${time}.${(microseconds || '000').padEnd(3, '0').substring(0, 3)}Z`;
+ return new Date(fullTimestamp);
+ }
+ } catch (error) {
+ console.warn('Failed to parse timestamp:', timestampStr, error);
+ }
+ return null;
+ }
+
+ parseErrorDetails(errorLines) {
+ const result = { error: null, stackTrace: null, id: null };
+ let stackTraceLines = [];
+
+ for (const line of errorLines) {
+ if (line.startsWith('โคท error:')) {
+ result.error = line.substring(9).trim();
+ } else if (line.startsWith('โคท trace:')) {
+ stackTraceLines.push(line.substring(9).trim());
+ } else if (line.startsWith('โคท id:')) {
+ result.id = line.substring(6).trim();
+ } else if (line.startsWith('โคท type:')) {
+ // Include type info in error
+ const type = line.substring(8).trim();
+ if (result.error) {
+ result.error = `${type}: ${result.error}`;
+ } else {
+ result.error = type;
+ }
+ } else if (stackTraceLines.length > 0) {
+ // Additional stack trace lines
+ stackTraceLines.push(line.replace(/^โคท\s*/, ''));
+ }
+ }
+
+ if (stackTraceLines.length > 0) {
+ result.stackTrace = stackTraceLines.join('\n');
+ }
+
+ return result;
+ }
+
+ extractUniqueValues() {
+ this.allLoggers.clear();
+ this.allProcesses.clear();
+ this.allLevels.clear();
+
+ for (const log of this.logs) {
+ this.allLoggers.add(log.loggerName);
+ this.allLevels.add(log.level);
+
+ const processName = log.processPrefix || 'Foreground';
+ this.allProcesses.add(processName);
+ }
+
+ }
+
+ showMainContent() {
+ document.getElementById('upload-section').style.display = 'none';
+ document.getElementById('main-content').style.display = 'block';
+
+ this.updateTimelineSection();
+ this.updateStats();
+ }
+
+ updateTimelineSection() {
+ if (this.logs.length > 0) {
+ const timelineSection = document.getElementById('timeline-section');
+ timelineSection.style.display = 'block';
+
+ // Set min/max for datetime inputs
+ const startTime = document.getElementById('start-time');
+ const endTime = document.getElementById('end-time');
+
+ const minTime = new Date(Math.min(...this.logs.map(log => log.timestamp)));
+ const maxTime = new Date(Math.max(...this.logs.map(log => log.timestamp)));
+
+ const formatForInput = (date) => {
+ return date.toISOString().slice(0, 16);
+ };
+
+ startTime.min = formatForInput(minTime);
+ startTime.max = formatForInput(maxTime);
+ startTime.value = formatForInput(minTime);
+
+ endTime.min = formatForInput(minTime);
+ endTime.max = formatForInput(maxTime);
+ endTime.value = formatForInput(maxTime);
+ }
+ }
+
+ // Search functionality
+ handleSearch(event) {
+ const query = event.target.value;
+ const clearBtn = document.getElementById('clear-search');
+
+ clearBtn.style.display = query ? 'block' : 'none';
+
+ this.currentFilter.searchQuery = query;
+ this.parseSearchQuery(query);
+ this.applyCurrentFilter();
+ }
+
+ parseSearchQuery(query) {
+ if (!query) {
+ this.currentFilter.selectedLoggers.clear();
+ return;
+ }
+
+ // Parse logger:name syntax
+ const loggerPattern = /logger:(\S+)/g;
+ const matches = [...query.matchAll(loggerPattern)];
+
+ if (matches.length > 0) {
+ const newLoggers = new Set();
+ for (const match of matches) {
+ const loggerPattern = match[1];
+ if (loggerPattern.endsWith('*')) {
+ // Wildcard pattern
+ const prefix = loggerPattern.slice(0, -1);
+ for (const logger of this.allLoggers) {
+ if (logger.startsWith(prefix)) {
+ newLoggers.add(logger);
+ }
+ }
+ } else {
+ newLoggers.add(loggerPattern);
+ }
+ }
+ this.currentFilter.selectedLoggers = newLoggers;
+
+ // Remove logger patterns from search query
+ this.currentFilter.searchQuery = query.replace(loggerPattern, '').trim();
+ }
+ }
+
+ clearSearch() {
+ document.getElementById('search-input').value = '';
+ document.getElementById('clear-search').style.display = 'none';
+ this.currentFilter.searchQuery = '';
+ this.currentFilter.selectedLoggers.clear();
+ this.applyCurrentFilter();
+ }
+
+ // Filtering
+ applyCurrentFilter() {
+
+ this.filteredLogs = this.logs.filter(log => this.matchesFilter(log));
+
+ // Sort filtered logs
+ this.filteredLogs.sort((a, b) => {
+ if (this.currentFilter.sortNewestFirst) {
+ return b.timestamp - a.timestamp;
+ } else {
+ return a.timestamp - b.timestamp;
+ }
+ });
+
+ this.currentOffset = 0;
+ this.renderLogs();
+ this.updateStats();
+ this.updateActiveFilters();
+ this.updateFilterButton();
+ }
+
+ matchesFilter(log) {
+ // Level filter
+ if (this.currentFilter.selectedLevels.size > 0) {
+ if (!this.currentFilter.selectedLevels.has(log.level)) {
+ return false;
+ }
+ }
+
+ // Logger filter
+ if (this.currentFilter.selectedLoggers.size > 0) {
+ if (!this.currentFilter.selectedLoggers.has(log.loggerName)) {
+ return false;
+ }
+ }
+
+ // Process filter
+ if (this.currentFilter.selectedProcesses.size > 0) {
+ const processName = log.processPrefix || 'Foreground';
+ if (!this.currentFilter.selectedProcesses.has(processName)) {
+ return false;
+ }
+ }
+
+ // Text search
+ if (this.currentFilter.searchQuery) {
+ const query = this.currentFilter.searchQuery.toLowerCase();
+ const searchText = `${log.message} ${log.loggerName} ${log.error || ''}`.toLowerCase();
+ if (!searchText.includes(query)) {
+ return false;
+ }
+ }
+
+ // Time range filter
+ if (this.currentFilter.startTime && log.timestamp < this.currentFilter.startTime) {
+ return false;
+ }
+ if (this.currentFilter.endTime && log.timestamp > this.currentFilter.endTime) {
+ return false;
+ }
+
+ return true;
+ }
+
+ renderLogs() {
+ const logList = document.getElementById('log-list');
+ const logsToShow = this.filteredLogs.slice(0, this.currentOffset + this.pageSize);
+
+ if (logsToShow.length === 0) {
+ logList.innerHTML = `
+
+
๐
+
No logs found
+
${this.logs.length === 0 ? 'Upload a ZIP file to view logs' : 'Try adjusting your filters'}
+
+ `;
+ this.setupInfiniteScroll(); // Still setup scroll listener
+ return;
+ }
+
+ logList.innerHTML = logsToShow.map(log => this.createLogEntryHTML(log)).join('');
+
+ // Add click listeners
+ logList.querySelectorAll('.log-entry').forEach((entry, index) => {
+ entry.addEventListener('click', () => this.showLogDetail(logsToShow[index]));
+ });
+
+ this.currentOffset = logsToShow.length;
+
+ // Hide load more button since we have infinite scroll
+ const loadMore = document.getElementById('load-more');
+ loadMore.style.display = 'none';
+
+ // Setup infinite scroll
+ this.setupInfiniteScroll();
+ }
+
+ setupInfiniteScroll() {
+ const logList = document.getElementById('log-list');
+
+ // Remove existing scroll listener to prevent duplicates
+ if (this.scrollListener) {
+ logList.removeEventListener('scroll', this.scrollListener);
+ }
+
+ this.scrollListener = () => {
+ const { scrollTop, scrollHeight, clientHeight } = logList;
+
+ // Load more when scrolled to 80% of the way down
+ if (scrollTop + clientHeight >= scrollHeight * 0.8) {
+ this.loadMoreLogs();
+ }
+ };
+
+ logList.addEventListener('scroll', this.scrollListener);
+ }
+
+ loadMoreLogs() {
+ // Prevent multiple simultaneous loads
+ if (this.isLoadingMore || this.currentOffset >= this.filteredLogs.length) {
+ return;
+ }
+
+ this.isLoadingMore = true;
+
+ // Show loading indicator
+ const loading = document.getElementById('loading');
+ loading.style.display = 'block';
+
+ // Simulate a small delay for smooth UX (optional)
+ setTimeout(() => {
+ const logList = document.getElementById('log-list');
+ const newLogsToShow = this.filteredLogs.slice(this.currentOffset, this.currentOffset + this.pageSize);
+
+ // Append new logs
+ const newLogsHTML = newLogsToShow.map(log => this.createLogEntryHTML(log)).join('');
+ logList.insertAdjacentHTML('beforeend', newLogsHTML);
+
+ // Add click listeners to new entries
+ const newEntries = logList.querySelectorAll('.log-entry:nth-last-child(-n+' + newLogsToShow.length + ')');
+ newEntries.forEach((entry, index) => {
+ const logIndex = this.currentOffset + index;
+ entry.addEventListener('click', () => this.showLogDetail(this.filteredLogs[logIndex]));
+ });
+
+ this.currentOffset += newLogsToShow.length;
+ this.isLoadingMore = false;
+ loading.style.display = 'none';
+
+ }, 100);
+ }
+
+ createLogEntryHTML(log) {
+ const levelClass = log.level.toLowerCase();
+ const processDisplay = this.getProcessDisplayName(log.processPrefix);
+ const formattedTime = this.formatTime(log.timestamp);
+ const truncatedMessage = this.truncateMessage(log.message);
+
+ return `
+
+
+
+
+
${this.escapeHtml(truncatedMessage)}
+ ${log.error ? `
${this.escapeHtml(log.error)}
` : ''}
+
+
+ `;
+ }
+
+ getProcessDisplayName(processPrefix) {
+ if (!processPrefix) return 'Foreground';
+
+ const cleanPrefix = processPrefix.replace(/[\[\]]/g, '');
+ switch (cleanPrefix) {
+ case 'bg': return 'Background';
+ case 'fbg': return 'Firebase Background';
+ default: return cleanPrefix || 'Foreground';
+ }
+ }
+
+ formatTime(timestamp) {
+ const date = new Date(timestamp);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ const seconds = date.getSeconds().toString().padStart(2, '0');
+ const millis = date.getMilliseconds().toString().padStart(3, '0');
+ return `${hours}:${minutes}:${seconds}.${millis}`;
+ }
+
+ truncateMessage(message) {
+ const lines = message.split('\n');
+ const maxLines = 4;
+
+ if (lines.length <= maxLines) {
+ return message;
+ }
+
+ return lines.slice(0, maxLines).join('\n') + '...';
+ }
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ updateStats() {
+ const logCount = document.getElementById('log-count');
+ const filteredCount = document.getElementById('filtered-count');
+
+ logCount.textContent = `${this.logs.length} logs loaded`;
+
+ if (this.filteredLogs.length !== this.logs.length) {
+ filteredCount.textContent = `(${this.filteredLogs.length} shown)`;
+ filteredCount.style.display = 'inline';
+ } else {
+ filteredCount.style.display = 'none';
+ }
+ }
+
+ updateActiveFilters() {
+ const activeFilters = document.getElementById('active-filters');
+ const filterChips = document.getElementById('filter-chips');
+
+ const chips = [];
+
+ // Level filters
+ for (const level of this.currentFilter.selectedLevels) {
+ chips.push(`${level} โ`);
+ }
+
+ // Logger filters
+ for (const logger of this.currentFilter.selectedLoggers) {
+ chips.push(`${logger} โ`);
+ }
+
+ // Process filters
+ for (const process of this.currentFilter.selectedProcesses) {
+ const displayName = this.getProcessDisplayName(process);
+ chips.push(`${displayName} โ`);
+ }
+
+ if (chips.length > 0) {
+ filterChips.innerHTML = chips.join('');
+ activeFilters.style.display = 'block';
+ } else {
+ activeFilters.style.display = 'none';
+ }
+ }
+
+ removeFilter(type, value) {
+ switch (type) {
+ case 'level':
+ this.currentFilter.selectedLevels.delete(value);
+ break;
+ case 'logger':
+ this.currentFilter.selectedLoggers.delete(value);
+ break;
+ case 'process':
+ this.currentFilter.selectedProcesses.delete(value);
+ break;
+ }
+ this.applyCurrentFilter();
+ }
+
+ updateFilterButton() {
+ const filterCount = document.getElementById('filter-count');
+ const totalActiveFilters = this.currentFilter.selectedLevels.size +
+ this.currentFilter.selectedLoggers.size +
+ this.currentFilter.selectedProcesses.size;
+
+ if (totalActiveFilters > 0) {
+ filterCount.textContent = totalActiveFilters;
+ filterCount.style.display = 'flex';
+ } else {
+ filterCount.style.display = 'none';
+ }
+ }
+
+ // Filter Dialog
+ showFilterDialog() {
+ this.populateFilterDialog();
+ document.getElementById('filter-dialog').style.display = 'flex';
+ }
+
+ hideFilterDialog() {
+ document.getElementById('filter-dialog').style.display = 'none';
+ }
+
+ populateFilterDialog() {
+ // Populate level chips
+ const levelChips = document.getElementById('level-chips');
+ const levels = ['FINEST', 'FINER', 'FINE', 'CONFIG', 'INFO', 'WARNING', 'SEVERE', 'SHOUT'];
+
+ levelChips.innerHTML = levels.map(level => {
+ const active = this.currentFilter.selectedLevels.has(level) ? 'active' : '';
+ return `${level}
`;
+ }).join('');
+
+ // Add click listeners to level chips
+ levelChips.querySelectorAll('.level-chip').forEach(chip => {
+ chip.addEventListener('click', () => {
+ chip.classList.toggle('active');
+ });
+ });
+
+ // Populate process list
+ const processList = document.getElementById('process-list');
+ const processes = Array.from(this.allProcesses).sort();
+
+ processList.innerHTML = processes.map(process => {
+ const checked = this.currentFilter.selectedProcesses.has(process) ? 'checked' : '';
+ const displayName = this.getProcessDisplayName(process);
+ return `
+
+
+
+
+ `;
+ }).join('');
+
+ // Populate logger list
+ const loggerList = document.getElementById('logger-list');
+ const loggers = Array.from(this.allLoggers).sort();
+
+ loggerList.innerHTML = loggers.map(logger => {
+ const checked = this.currentFilter.selectedLoggers.has(logger) ? 'checked' : '';
+ return `
+
+
+
+
+ `;
+ }).join('');
+ }
+
+ applyFilters() {
+ // Get selected levels
+ const selectedLevels = new Set();
+ document.querySelectorAll('.level-chip.active').forEach(chip => {
+ selectedLevels.add(chip.dataset.level);
+ });
+
+ // Get selected processes
+ const selectedProcesses = new Set();
+ document.querySelectorAll('#process-list input:checked').forEach(checkbox => {
+ const process = checkbox.id.replace('process-', '');
+ selectedProcesses.add(process);
+ });
+
+ // Get selected loggers
+ const selectedLoggers = new Set();
+ document.querySelectorAll('#logger-list input:checked').forEach(checkbox => {
+ const logger = checkbox.id.replace('logger-', '');
+ selectedLoggers.add(logger);
+ });
+
+ // Update current filter
+ this.currentFilter.selectedLevels = selectedLevels;
+ this.currentFilter.selectedProcesses = selectedProcesses;
+ this.currentFilter.selectedLoggers = selectedLoggers;
+
+ this.applyCurrentFilter();
+ this.hideFilterDialog();
+ }
+
+ clearAllFilters() {
+ this.currentFilter.selectedLevels.clear();
+ this.currentFilter.selectedProcesses.clear();
+ this.currentFilter.selectedLoggers.clear();
+ this.currentFilter.searchQuery = '';
+ this.currentFilter.startTime = null;
+ this.currentFilter.endTime = null;
+
+ document.getElementById('search-input').value = '';
+ document.getElementById('clear-search').style.display = 'none';
+
+ this.applyCurrentFilter();
+ this.hideFilterDialog();
+ }
+
+ // Sort functionality
+ toggleSort() {
+ this.currentFilter.sortNewestFirst = !this.currentFilter.sortNewestFirst;
+ const sortBtn = document.getElementById('sort-btn');
+ const iconElement = sortBtn.querySelector('.material-icons');
+ if (iconElement) {
+ iconElement.textContent = this.currentFilter.sortNewestFirst ? 'arrow_downward' : 'arrow_upward';
+ } else {
+ sortBtn.textContent = this.currentFilter.sortNewestFirst ? 'โ' : 'โ';
+ }
+ sortBtn.title = this.currentFilter.sortNewestFirst ? 'Sort oldest first' : 'Sort newest first';
+ this.applyCurrentFilter();
+ }
+
+ // Timeline functionality
+ toggleTimeline() {
+ const timelineControls = document.getElementById('timeline-controls');
+ const timelineBtn = document.getElementById('timeline-toggle');
+
+ const isVisible = timelineControls.style.display === 'block';
+ timelineControls.style.display = isVisible ? 'none' : 'block';
+ timelineBtn.style.background = isVisible ? 'none' : '#e3f2fd';
+ }
+
+ handleTimelineChange() {
+ const startTime = document.getElementById('start-time');
+ const endTime = document.getElementById('end-time');
+
+ if (startTime.value && endTime.value) {
+ this.currentFilter.startTime = new Date(startTime.value);
+ this.currentFilter.endTime = new Date(endTime.value);
+ this.applyCurrentFilter();
+ }
+ }
+
+ resetTimeline() {
+ if (this.logs.length > 0) {
+ const minTime = new Date(Math.min(...this.logs.map(log => log.timestamp)));
+ const maxTime = new Date(Math.max(...this.logs.map(log => log.timestamp)));
+
+ const formatForInput = (date) => date.toISOString().slice(0, 16);
+
+ document.getElementById('start-time').value = formatForInput(minTime);
+ document.getElementById('end-time').value = formatForInput(maxTime);
+
+ this.currentFilter.startTime = null;
+ this.currentFilter.endTime = null;
+ this.applyCurrentFilter();
+ }
+ }
+
+ // Log details
+ showLogDetail(log) {
+ const detailContent = document.getElementById('detail-content');
+
+ const formatTimestamp = (timestamp) => {
+ return new Date(timestamp).toISOString().replace('T', ' ').replace('Z', '');
+ };
+
+ detailContent.innerHTML = `
+
+
Log Entry Details
+
+ | Timestamp | ${formatTimestamp(log.timestamp)} |
+ | Level | ${log.level} |
+ | Logger | ${log.loggerName} |
+ | Process | ${this.getProcessDisplayName(log.processPrefix)} |
+ ${log.filename ? `| File | ${log.filename} |
` : ''}
+ ${log.id ? `| ID | ${log.id} |
` : ''}
+
+
+
Message
+
${this.escapeHtml(log.message)}
+
+ ${log.error ? `
+
Error
+
${this.escapeHtml(log.error)}
+ ` : ''}
+
+ ${log.stackTrace ? `
+
Stack Trace
+
${this.escapeHtml(log.stackTrace)}
+ ` : ''}
+
+ `;
+
+ document.getElementById('detail-dialog').style.display = 'flex';
+ this.currentDetailLog = log;
+ }
+
+ hideDetailDialog() {
+ document.getElementById('detail-dialog').style.display = 'none';
+ this.currentDetailLog = null;
+ }
+
+ copyLogDetail() {
+ if (this.currentDetailLog) {
+ const logText = this.formatLogForExport(this.currentDetailLog);
+ navigator.clipboard.writeText(logText).then(() => {
+ // Show a brief confirmation
+ const btn = document.getElementById('copy-log');
+ const originalText = btn.textContent;
+ btn.textContent = 'Copied!';
+ setTimeout(() => {
+ btn.textContent = originalText;
+ }, 1000);
+ });
+ }
+ }
+
+ // Analytics
+ showAnalytics() {
+ const analyticsContent = document.getElementById('analytics-content');
+
+ // Calculate logger statistics
+ const loggerStats = this.calculateLoggerStats();
+ const levelStats = this.calculateLevelStats();
+
+ analyticsContent.innerHTML = `
+
+
Top Loggers
+ ${loggerStats.slice(0, 10).map(stat => `
+
+
+
${stat.name}
+
${stat.count}
+
${stat.percentage.toFixed(1)}%
+
+ `).join('')}
+
+
+
+
Log Levels
+ ${levelStats.map(stat => `
+
+
+
${stat.name}
+
${stat.count}
+
${stat.percentage.toFixed(1)}%
+
+ `).join('')}
+
+ `;
+
+ document.getElementById('analytics-dialog').style.display = 'flex';
+ }
+
+ calculateLoggerStats() {
+ const loggerCounts = {};
+ for (const log of this.filteredLogs) {
+ loggerCounts[log.loggerName] = (loggerCounts[log.loggerName] || 0) + 1;
+ }
+
+ const total = this.filteredLogs.length;
+ return Object.entries(loggerCounts)
+ .map(([name, count]) => ({ name, count, percentage: (count / total) * 100 }))
+ .sort((a, b) => b.count - a.count);
+ }
+
+ calculateLevelStats() {
+ const levelCounts = {};
+ for (const log of this.filteredLogs) {
+ levelCounts[log.level] = (levelCounts[log.level] || 0) + 1;
+ }
+
+ const total = this.filteredLogs.length;
+ return Object.entries(levelCounts)
+ .map(([name, count]) => ({ name, count, percentage: (count / total) * 100 }))
+ .sort((a, b) => b.count - a.count);
+ }
+
+ getLevelColor(level) {
+ switch (level) {
+ case 'SEVERE': return '#f44336';
+ case 'WARNING': return '#ff9800';
+ case 'INFO': return '#2196f3';
+ case 'CONFIG': return '#4caf50';
+ case 'FINE':
+ case 'FINER':
+ case 'FINEST': return '#9e9e9e';
+ case 'SHOUT': return '#9c27b0';
+ default: return '#9e9e9e';
+ }
+ }
+
+ filterByLogger(loggerName) {
+ this.hideAnalytics();
+ document.getElementById('search-input').value = `logger:${loggerName}`;
+ this.handleSearch({ target: { value: `logger:${loggerName}` } });
+ }
+
+ filterByLevel(level) {
+ this.hideAnalytics();
+ this.currentFilter.selectedLevels.clear();
+ this.currentFilter.selectedLevels.add(level);
+ this.applyCurrentFilter();
+ }
+
+ hideAnalytics() {
+ document.getElementById('analytics-dialog').style.display = 'none';
+ }
+
+ // Export functionality
+ exportLogs() {
+ const filteredData = this.filteredLogs.map(log => this.formatLogForExport(log)).join('\n\n');
+ const header = `=== Ente App Logs ===
+Exported at: ${new Date().toISOString()}
+Total logs: ${this.filteredLogs.length}
+${'='.repeat(40)}\n\n`;
+
+ const content = header + filteredData;
+ this.downloadFile(content, 'ente-logs.txt', 'text/plain');
+ }
+
+ formatLogForExport(log) {
+ let text = `[${new Date(log.timestamp).toISOString()}] [${log.loggerName}] [${log.level}]\n${log.message}`;
+
+ if (log.error) {
+ text += `\nError: ${log.error}`;
+ }
+
+ if (log.stackTrace) {
+ text += `\nStack trace:\n${log.stackTrace}`;
+ }
+
+ return text;
+ }
+
+ downloadFile(content, filename, mimeType) {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+
+ // Clear logs
+ clearLogs() {
+ if (confirm('Are you sure you want to clear all logs? This will reload the page.')) {
+ location.reload();
+ }
+ }
+}
+
+// Initialize the application
+const logViewer = new LogViewer();
\ No newline at end of file
diff --git a/infra/experiments/logs-viewer/styles.css b/infra/experiments/logs-viewer/styles.css
new file mode 100644
index 0000000000..7d89b43977
--- /dev/null
+++ b/infra/experiments/logs-viewer/styles.css
@@ -0,0 +1,1065 @@
+/* Reset and base styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--ente-font-family);
+ line-height: 1.6;
+ color: var(--ente-text-base);
+ background: var(--ente-background-default);
+ font-weight: var(--ente-font-weight-regular);
+ letter-spacing: -0.011em;
+}
+
+/* App container */
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: var(--ente-background-default);
+}
+
+/* Header */
+.header {
+ background: var(--ente-color-accent-photos);
+ color: var(--ente-background-default);
+ padding: var(--ente-spacing-lg);
+ box-shadow: var(--ente-shadow-paper);
+ border-bottom: 1px solid var(--ente-stroke-fainter);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.header h1 {
+ font-size: 24px;
+ font-weight: var(--ente-font-weight-medium);
+ line-height: 29px;
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-sm);
+}
+
+.header-actions {
+ display: flex;
+ gap: var(--ente-spacing-sm);
+ align-items: center;
+}
+
+/* Material-UI Enhanced Components */
+.mui-icon-btn {
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ padding: var(--ente-spacing-md);
+ border-radius: 50%;
+ color: white;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 48px;
+ height: 48px;
+ border: 1px solid transparent;
+}
+
+.mui-icon-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.mui-icon-btn:active {
+ transform: translateY(0);
+}
+
+.mui-icon-btn .material-icons {
+ font-size: 24px;
+}
+
+.filter-count {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ background: var(--ente-danger);
+ color: white;
+ font-size: 11px;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: var(--ente-font-weight-medium);
+ border: 2px solid var(--ente-color-accent-photos);
+}
+
+/* Dropdown */
+.dropdown {
+ position: relative;
+}
+
+.mui-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ background: var(--ente-background-paper);
+ border-radius: var(--ente-border-radius);
+ box-shadow: var(--ente-shadow-menu);
+ min-width: 200px;
+ display: none;
+ z-index: 1000;
+ overflow: hidden;
+ border: 1px solid var(--ente-stroke-faint);
+}
+
+.dropdown.active .mui-menu {
+ display: block;
+ animation: slideInDown 0.2s ease-out;
+}
+
+@keyframes slideInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.mui-menu-item {
+ width: 100%;
+ padding: var(--ente-spacing-md) var(--ente-spacing-lg);
+ border: none;
+ background: none;
+ text-align: left;
+ cursor: pointer;
+ color: var(--ente-text-base);
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-md);
+ font-size: 14px;
+ font-weight: var(--ente-font-weight-regular);
+}
+
+.mui-menu-item:hover {
+ background: var(--ente-fill-faint-hover);
+}
+
+.mui-menu-item.danger {
+ color: var(--ente-danger);
+}
+
+.mui-menu-item .material-icons {
+ font-size: 20px;
+ opacity: 0.8;
+}
+
+/* Upload Section */
+.upload-section {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--ente-spacing-xl);
+ background: var(--ente-background-default);
+}
+
+.upload-area {
+ border: 2px dashed var(--ente-stroke-faint);
+ border-radius: var(--ente-border-radius);
+ padding: 4rem 2rem;
+ text-align: center;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: var(--ente-background-paper);
+ max-width: 600px;
+ width: 100%;
+ position: relative;
+ overflow: hidden;
+}
+
+.upload-area::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg, var(--ente-color-accent-photos) 0%, var(--ente-color-accent-auth) 100%);
+ opacity: 0;
+ transition: opacity 0.3s;
+ z-index: 0;
+}
+
+.upload-area.drag-over {
+ border-color: var(--ente-color-accent-photos);
+ background: var(--ente-background-paper2);
+ transform: scale(1.02);
+ box-shadow: var(--ente-shadow-paper);
+}
+
+.upload-area.drag-over::before {
+ opacity: 0.05;
+}
+
+.upload-content {
+ position: relative;
+ z-index: 1;
+}
+
+.upload-icon {
+ font-size: 4rem;
+ margin-bottom: var(--ente-spacing-lg);
+ opacity: 0.8;
+}
+
+.upload-content h2 {
+ margin-bottom: var(--ente-spacing-sm);
+ color: var(--ente-text-base);
+ font-size: 20px;
+ font-weight: var(--ente-font-weight-medium);
+}
+
+.upload-content p {
+ color: var(--ente-text-muted);
+ margin-bottom: var(--ente-spacing-xl);
+ font-size: 14px;
+}
+
+/* Buttons */
+.primary-btn {
+ background: var(--ente-color-accent-photos);
+ color: white;
+ border: none;
+ padding: var(--ente-spacing-md) var(--ente-spacing-xl);
+ border-radius: var(--ente-border-radius-small);
+ cursor: pointer;
+ font-weight: var(--ente-font-weight-medium);
+ font-size: 16px;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ display: inline-flex;
+ align-items: center;
+ gap: var(--ente-spacing-sm);
+ position: relative;
+ overflow: hidden;
+}
+
+.primary-btn:hover {
+ background: var(--ente-color-accent-photos);
+ transform: translateY(-1px);
+ box-shadow: var(--ente-shadow-button);
+ filter: brightness(1.1);
+}
+
+.primary-btn:active {
+ transform: translateY(0);
+}
+
+.mui-button {
+ background: var(--ente-color-accent-photos);
+ color: white;
+ border: none;
+ padding: var(--ente-spacing-sm) var(--ente-spacing-lg);
+ border-radius: var(--ente-border-radius-small);
+ cursor: pointer;
+ font-weight: var(--ente-font-weight-medium);
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-sm);
+ text-transform: none;
+ font-size: 14px;
+ min-height: 40px;
+}
+
+.mui-button:hover {
+ background: var(--ente-color-accent-photos);
+ filter: brightness(1.1);
+ transform: translateY(-1px);
+}
+
+.mui-button.secondary {
+ background: var(--ente-secondary-main);
+ color: var(--ente-text-base);
+ border: 1px solid var(--ente-stroke-faint);
+}
+
+.mui-button.secondary:hover {
+ background: var(--ente-secondary-hover);
+ filter: none;
+}
+
+/* Main Content */
+.main-content {
+ flex: 1;
+ max-width: 1400px;
+ margin: 0 auto;
+ width: 100%;
+ padding: var(--ente-spacing-xl) var(--ente-spacing-lg);
+ background: var(--ente-background-default);
+}
+
+/* Search Section */
+.search-section {
+ margin-bottom: var(--ente-spacing-lg);
+}
+
+.mui-search-container {
+ position: relative;
+}
+
+.mui-textfield {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.mui-input {
+ flex: 1;
+ padding: var(--ente-spacing-lg) var(--ente-spacing-lg);
+ padding-left: 56px;
+ padding-right: 56px;
+ border: 2px solid var(--ente-stroke-faint);
+ border-radius: var(--ente-border-radius);
+ font-size: 16px;
+ font-family: var(--ente-font-family);
+ font-weight: var(--ente-font-weight-regular);
+ background: var(--ente-background-search);
+ color: var(--ente-text-base);
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ outline: none;
+}
+
+.mui-input:focus {
+ border-color: var(--ente-color-accent-photos);
+ box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1);
+ background: var(--ente-background-paper);
+}
+
+.mui-input::placeholder {
+ color: var(--ente-text-faint);
+}
+
+.mui-search-icon {
+ position: absolute;
+ left: var(--ente-spacing-lg);
+ color: var(--ente-text-muted);
+ pointer-events: none;
+ z-index: 2;
+ font-size: 24px;
+}
+
+.mui-clear-btn {
+ position: absolute;
+ right: var(--ente-spacing-sm);
+ background: none;
+ border: none;
+ color: var(--ente-text-muted);
+ cursor: pointer;
+ padding: var(--ente-spacing-sm);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ z-index: 2;
+}
+
+.mui-clear-btn:hover {
+ background: var(--ente-action-hover);
+ color: var(--ente-text-base);
+}
+
+/* Timeline Section */
+.timeline-section {
+ background: var(--ente-background-paper);
+ border-radius: var(--ente-border-radius);
+ margin-bottom: var(--ente-spacing-lg);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ border: 1px solid var(--ente-stroke-fainter);
+ overflow: hidden;
+}
+
+.timeline-header {
+ display: flex;
+ align-items: center;
+ padding: var(--ente-spacing-lg);
+ gap: var(--ente-spacing-md);
+ cursor: pointer;
+ transition: background-color 0.2s;
+ background: var(--ente-background-paper);
+}
+
+.timeline-header:hover {
+ background: var(--ente-background-paper2);
+}
+
+.timeline-header .material-icons {
+ color: var(--ente-color-accent-photos);
+ font-size: 24px;
+}
+
+.timeline-header span:not(.material-icons) {
+ font-size: 16px;
+ font-weight: var(--ente-font-weight-medium);
+ color: var(--ente-text-base);
+}
+
+.timeline-btn {
+ margin-left: auto;
+}
+
+.timeline-controls {
+ padding: var(--ente-spacing-lg);
+ border-top: 1px solid var(--ente-stroke-fainter);
+ background: var(--ente-background-paper2);
+}
+
+.timeline-range {
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-lg);
+ flex-wrap: wrap;
+}
+
+.timeline-range .mui-textfield {
+ min-width: 200px;
+ flex: 1;
+}
+
+.timeline-range .mui-input {
+ padding: var(--ente-spacing-md);
+ font-size: 14px;
+ background: var(--ente-background-paper);
+}
+
+.range-separator {
+ color: var(--ente-text-muted);
+ font-weight: var(--ente-font-weight-medium);
+ font-size: 14px;
+}
+
+/* Active Filters */
+.active-filters {
+ margin-bottom: var(--ente-spacing-lg);
+}
+
+.filter-chips {
+ display: flex;
+ gap: var(--ente-spacing-sm);
+ flex-wrap: wrap;
+}
+
+.filter-chip {
+ background: var(--ente-background-paper);
+ color: var(--ente-text-base);
+ padding: var(--ente-spacing-sm) var(--ente-spacing-md);
+ border-radius: 20px;
+ font-size: 13px;
+ font-weight: var(--ente-font-weight-medium);
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-sm);
+ border: 1px solid var(--ente-stroke-faint);
+ transition: all 0.2s;
+ animation: slideInUp 0.3s ease-out;
+}
+
+@keyframes slideInUp {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.filter-chip.level-WARNING {
+ background: rgba(255, 193, 7, 0.1);
+ color: var(--ente-warning);
+ border-color: rgba(255, 193, 7, 0.3);
+}
+
+.filter-chip.level-SEVERE {
+ background: rgba(234, 63, 63, 0.1);
+ color: var(--ente-danger);
+ border-color: rgba(234, 63, 63, 0.3);
+}
+
+.filter-chip.level-SHOUT {
+ background: rgba(150, 16, 214, 0.1);
+ color: var(--ente-color-accent-auth);
+ border-color: rgba(150, 16, 214, 0.3);
+}
+
+.filter-chip.level-INFO {
+ background: rgba(29, 185, 84, 0.1);
+ color: var(--ente-color-accent-photos);
+ border-color: rgba(29, 185, 84, 0.3);
+}
+
+.filter-chip .remove {
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+}
+
+.filter-chip .remove:hover {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* Log Stats */
+.log-stats {
+ background: var(--ente-background-paper);
+ padding: var(--ente-spacing-lg);
+ border-radius: var(--ente-border-radius);
+ margin-bottom: var(--ente-spacing-lg);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 14px;
+ color: var(--ente-text-muted);
+ border: 1px solid var(--ente-stroke-fainter);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+#log-count {
+ font-weight: var(--ente-font-weight-medium);
+ color: var(--ente-text-base);
+}
+
+/* Log List */
+.log-list-container {
+ background: var(--ente-background-paper);
+ border-radius: var(--ente-border-radius);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ border: 1px solid var(--ente-stroke-fainter);
+}
+
+.log-list {
+ max-height: 70vh;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+}
+
+.log-entry {
+ padding: var(--ente-spacing-lg);
+ border-bottom: 1px solid var(--ente-stroke-fainter);
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ display: flex;
+ align-items: flex-start;
+ gap: var(--ente-spacing-md);
+ position: relative;
+ animation: fadeInEntry 0.3s ease-out;
+}
+
+@keyframes fadeInEntry {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.log-entry:hover {
+ background: var(--ente-background-paper2);
+ transform: translateX(4px);
+}
+
+.log-entry:last-child {
+ border-bottom: none;
+}
+
+.log-entry.severe {
+ background: rgba(234, 63, 63, 0.03);
+ border-left: 4px solid var(--ente-danger);
+}
+
+.log-entry.warning {
+ background: rgba(255, 193, 7, 0.03);
+ border-left: 4px solid var(--ente-warning);
+}
+
+.log-level {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ margin-top: 6px;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px var(--ente-background-paper);
+}
+
+.log-level.INFO { background: var(--ente-color-accent-photos); }
+.log-level.WARNING { background: var(--ente-warning); }
+.log-level.SEVERE { background: var(--ente-danger); }
+.log-level.SHOUT { background: var(--ente-color-accent-auth); }
+.log-level.FINE { background: var(--ente-text-faint); }
+.log-level.FINER { background: var(--ente-text-faint); }
+.log-level.FINEST { background: var(--ente-text-faint); }
+.log-level.CONFIG { background: var(--ente-success); }
+
+.log-content {
+ flex: 1;
+}
+
+.log-header {
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-md);
+ margin-bottom: var(--ente-spacing-sm);
+ font-size: 13px;
+ flex-wrap: wrap;
+}
+
+.log-time {
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ font-weight: var(--ente-font-weight-medium);
+ color: var(--ente-text-muted);
+ background: var(--ente-background-paper2);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 12px;
+}
+
+.log-logger {
+ background: var(--ente-background-paper2);
+ color: var(--ente-text-base);
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: var(--ente-font-weight-medium);
+ border: 1px solid var(--ente-stroke-fainter);
+}
+
+.log-process {
+ background: var(--ente-color-accent-photos);
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: var(--ente-font-weight-medium);
+ opacity: 0.9;
+}
+
+.log-message {
+ font-size: 15px;
+ line-height: 1.5;
+ color: var(--ente-text-base);
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 4.5em;
+ overflow: hidden;
+ position: relative;
+ font-family: var(--ente-font-family);
+}
+
+.log-message.expanded {
+ max-height: none;
+}
+
+.log-message::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1.5em;
+ background: linear-gradient(transparent, var(--ente-background-paper2));
+ pointer-events: none;
+}
+
+.log-message.expanded::after {
+ display: none;
+}
+
+.log-error {
+ color: var(--ente-danger);
+ font-weight: var(--ente-font-weight-medium);
+ margin-top: var(--ente-spacing-sm);
+ font-size: 14px;
+ padding: var(--ente-spacing-sm);
+ background: rgba(234, 63, 63, 0.05);
+ border-radius: var(--ente-border-radius-small);
+ border-left: 3px solid var(--ente-danger);
+}
+
+/* Loading and Load More */
+.loading {
+ text-align: center;
+ padding: var(--ente-spacing-xl);
+ color: var(--ente-text-muted);
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--ente-spacing-md);
+}
+
+.loading::before {
+ content: '';
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--ente-stroke-fainter);
+ border-top: 2px solid var(--ente-color-accent-photos);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Dialog */
+.dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: var(--ente-spacing-lg);
+ animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.dialog {
+ background: var(--ente-background-paper);
+ border-radius: var(--ente-border-radius);
+ max-width: 500px;
+ width: 100%;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: var(--ente-shadow-paper);
+ animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ border: 1px solid var(--ente-stroke-fainter);
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.dialog.large {
+ max-width: 900px;
+}
+
+.dialog-header {
+ padding: var(--ente-spacing-xl) var(--ente-spacing-xl) var(--ente-spacing-lg);
+ border-bottom: 1px solid var(--ente-stroke-fainter);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--ente-background-paper);
+}
+
+.dialog-header h2 {
+ font-size: 20px;
+ font-weight: var(--ente-font-weight-medium);
+ color: var(--ente-text-base);
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ padding: var(--ente-spacing-sm);
+ color: var(--ente-text-muted);
+ border-radius: 50%;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.close-btn:hover {
+ background: var(--ente-action-hover);
+ color: var(--ente-text-base);
+}
+
+.dialog-content {
+ padding: var(--ente-spacing-xl) var(--ente-spacing-xl) var(--ente-spacing-lg);
+ overflow-y: auto;
+ flex: 1;
+ background: var(--ente-background-paper);
+}
+
+.dialog-actions {
+ padding: var(--ente-spacing-lg) var(--ente-spacing-xl);
+ border-top: 1px solid var(--ente-stroke-fainter);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--ente-spacing-md);
+ background: var(--ente-background-paper2);
+}
+
+/* Filter Dialog Enhancements */
+.filter-section {
+ margin-bottom: var(--ente-spacing-xl);
+}
+
+.filter-section:last-child {
+ margin-bottom: 0;
+}
+
+.filter-section h3 {
+ margin-bottom: var(--ente-spacing-lg);
+ font-size: 16px;
+ font-weight: var(--ente-font-weight-medium);
+ color: var(--ente-text-base);
+ display: flex;
+ align-items: center;
+ gap: var(--ente-spacing-sm);
+}
+
+.level-chips {
+ display: flex;
+ gap: var(--ente-spacing-md);
+ flex-wrap: wrap;
+}
+
+.level-chip {
+ padding: var(--ente-spacing-md) var(--ente-spacing-lg);
+ border: 2px solid var(--ente-stroke-faint);
+ border-radius: 20px;
+ background: var(--ente-background-paper);
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ font-size: 14px;
+ font-weight: var(--ente-font-weight-medium);
+ min-width: 80px;
+ text-align: center;
+ position: relative;
+}
+
+.level-chip:hover {
+ background: var(--ente-background-paper2);
+ transform: translateY(-1px);
+}
+
+.level-chip.active {
+ border-color: currentColor;
+ background: currentColor;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ transform: translateY(-1px);
+}
+
+.level-chip.active::after {
+ content: attr(data-level);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: white;
+ font-weight: var(--ente-font-weight-medium);
+ font-size: 14px;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+/* Level colors */
+.level-chip.FINEST, .level-chip.FINER, .level-chip.FINE {
+ color: var(--ente-text-faint);
+}
+.level-chip.CONFIG { color: var(--ente-success); }
+.level-chip.INFO { color: var(--ente-color-accent-photos); }
+.level-chip.WARNING { color: var(--ente-warning); }
+.level-chip.SEVERE { color: var(--ente-danger); }
+.level-chip.SHOUT { color: var(--ente-color-accent-auth); }
+
+.process-list, .logger-list {
+ max-height: 280px;
+ overflow-y: auto;
+ border: 1px solid var(--ente-stroke-faint);
+ border-radius: var(--ente-border-radius);
+ padding: var(--ente-spacing-md);
+ background: var(--ente-background-paper2);
+ gap: var(--ente-spacing-xs);
+}
+
+.checkbox-item {
+ display: flex;
+ align-items: center;
+ padding: var(--ente-spacing-md) var(--ente-spacing-sm);
+ cursor: pointer;
+ border-radius: var(--ente-border-radius-small);
+ transition: background-color 0.2s;
+ font-size: 14px;
+ margin-bottom: var(--ente-spacing-xs);
+}
+
+.checkbox-item:hover {
+ background: var(--ente-action-hover);
+}
+
+.checkbox-item input {
+ margin-right: var(--ente-spacing-md);
+ accent-color: var(--ente-color-accent-photos);
+}
+
+.checkbox-item label {
+ cursor: pointer;
+ color: var(--ente-text-base);
+ font-weight: var(--ente-font-weight-regular);
+}
+
+/* Empty states */
+.empty-state {
+ text-align: center;
+ padding: 4rem 2rem;
+ color: var(--ente-text-muted);
+}
+
+.empty-state-icon {
+ font-size: 4rem;
+ margin-bottom: var(--ente-spacing-lg);
+ opacity: 0.5;
+}
+
+.empty-state h3 {
+ margin-bottom: var(--ente-spacing-sm);
+ color: var(--ente-text-base);
+ font-size: 18px;
+ font-weight: var(--ente-font-weight-medium);
+}
+
+.empty-state p {
+ font-size: 14px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .header-content {
+ padding: 0;
+ }
+
+ .header h1 {
+ font-size: 20px;
+ }
+
+ .main-content {
+ padding: var(--ente-spacing-lg) var(--ente-spacing-md);
+ }
+
+ .timeline-range {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .dialog {
+ max-width: none;
+ margin: var(--ente-spacing-lg);
+ max-height: calc(100vh - 2rem);
+ }
+
+ .log-entry {
+ padding: var(--ente-spacing-md);
+ flex-direction: column;
+ gap: var(--ente-spacing-sm);
+ }
+
+ .log-header {
+ flex-wrap: wrap;
+ }
+
+ .mui-icon-btn {
+ min-width: 44px;
+ height: 44px;
+ }
+}
+
+/* Scrollbar styling */
+.log-list::-webkit-scrollbar,
+.process-list::-webkit-scrollbar,
+.logger-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+.log-list::-webkit-scrollbar-track,
+.process-list::-webkit-scrollbar-track,
+.logger-list::-webkit-scrollbar-track {
+ background: var(--ente-background-paper2);
+}
+
+.log-list::-webkit-scrollbar-thumb,
+.process-list::-webkit-scrollbar-thumb,
+.logger-list::-webkit-scrollbar-thumb {
+ background: var(--ente-stroke-faint);
+ border-radius: 3px;
+}
+
+.log-list::-webkit-scrollbar-thumb:hover,
+.process-list::-webkit-scrollbar-thumb:hover,
+.logger-list::-webkit-scrollbar-thumb:hover {
+ background: var(--ente-stroke-muted);
+}
+
+/* Focus styles for accessibility */
+.mui-icon-btn:focus,
+.mui-button:focus,
+.primary-btn:focus {
+ outline: 2px solid var(--ente-color-accent-photos);
+ outline-offset: 2px;
+}
+
+.mui-input:focus {
+ border-color: var(--ente-color-accent-photos);
+ box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1);
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .log-entry {
+ border-bottom: 2px solid var(--ente-stroke-muted);
+ }
+
+ .filter-chip,
+ .log-logger,
+ .log-time {
+ border: 1px solid var(--ente-stroke-muted);
+ }
+}
\ No newline at end of file