From bea8d8727cda59e37b9036ef7679846fb8fc3a5e Mon Sep 17 00:00:00 2001 From: Tomek Zebrowski Date: Sun, 15 Feb 2026 20:05:40 +0100 Subject: [PATCH] feat: add trip info rerpot --- index.html | 38 ++- src/projectmanager.js | 11 +- src/signals.json | 12 + src/style.css | 601 +++++++++++++++++++++++++++++++-------- src/telemetryanalyzer.js | 159 +++++++++++ src/ui.js | 189 ++++++++++-- 6 files changed, 868 insertions(+), 142 deletions(-) create mode 100644 src/telemetryanalyzer.js diff --git a/index.html b/index.html index 8ebd33f..090948f 100644 --- a/index.html +++ b/index.html @@ -103,6 +103,31 @@ > + +
+ + + + @@ -606,7 +636,11 @@

No Telemetry Loaded

- +
diff --git a/src/projectmanager.js b/src/projectmanager.js index f67d634..cb5bdbc 100644 --- a/src/projectmanager.js +++ b/src/projectmanager.js @@ -7,6 +7,7 @@ class ProjectManager { #currentProject; #isReplaying; #libraryContainer; + #hydrationPromise; constructor() { this.#currentProject = @@ -14,7 +15,7 @@ class ProjectManager { this.#isReplaying = false; this.#libraryContainer = null; - dbManager.init().then(async () => { + this.#hydrationPromise = dbManager.init().then(async () => { await this.#hydrateActiveFiles(); this.renderLibrary(); }); @@ -31,6 +32,14 @@ class ProjectManager { initLibraryUI(containerId) { this.#libraryContainer = document.getElementById(containerId); this.renderLibrary(); + + if (this.#hydrationPromise) { + this.#hydrationPromise.then(() => { + if (AppState.files.length > 0) { + messenger.emit('dataprocessor:batch-load-completed', {}); + } + }); + } } async renderLibrary() { diff --git a/src/signals.json b/src/signals.json index 64bdab8..36faa5e 100644 --- a/src/signals.json +++ b/src/signals.json @@ -1,4 +1,16 @@ [ + { + "name": "Engine Coolant Temp", + "default": false, + "aliases": ["Engine Coolant Temp"] + }, + + { + "name": "Oil Temp", + "default": false, + "aliases": ["Estimated Oil Temp", "Engine Oil Temperature"] + }, + { "name": "Fuel Level", "default": false, diff --git a/src/style.css b/src/style.css index c7f1e28..2af0a52 100644 --- a/src/style.css +++ b/src/style.css @@ -85,14 +85,12 @@ body.dark-theme { body.dark-theme .modal-content h2, body.dark-theme .signal-item, body.dark-theme label { + color: #333 !important; color: var(--text-color) !important; } -body { - transition: background 0.3s ease; -} - canvas { + background-color: #fff; background-color: var(--chart-canvas-bg); } @@ -102,18 +100,24 @@ html { padding: 0; font-family: 'Segoe UI', Roboto, sans-serif; font-size: 15px; + background-color: #fcfcfc; background-color: var(--bg-color); + color: #333; color: var(--text-main); + transition: background 0.3s ease; transition: background var(--transition-speed) ease; } body.docs-body.dark-theme { + background-color: #0a0a0a; background-color: var(--dark-bg); + color: #f4f4f4; color: var(--light-text); } body.docs-body.dark-theme .feature-card { background: #1a1a1a; + border-color: #1c3d72; border-color: var(--brand-blue); } @@ -130,7 +134,9 @@ body.docs-body.dark-theme .feature-card { min-width: 290px; max-width: 600px; width: 290px; + background: #f8f9fa; background: var(--sidebar-bg); + border-right: 1px solid #ddd; border-right: 1px solid var(--border-color); box-shadow: 2px 0 5px #0000000d; transition: margin-left 0.3s ease; @@ -145,8 +151,10 @@ body.docs-body.dark-theme .feature-card { .sidebar h2 { margin-top: 0; + color: #c22636; color: var(--brand-red); font-size: 1.4em; + border-bottom: 2px solid #1c3d72; border-bottom: 2px solid var(--brand-blue); padding: 10px; flex-shrink: 0; @@ -160,6 +168,7 @@ body.docs-body.dark-theme .feature-card { .control-group { margin-bottom: 5px; padding-bottom: 15px; + border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color); flex-shrink: 0; } @@ -190,6 +199,7 @@ body.docs-body.dark-theme .feature-card { font-weight: 900; font-size: 0.8em; transition: transform 0.2s ease; + color: #666; color: var(--text-muted); margin-left: 10px; } @@ -219,6 +229,7 @@ body.docs-body.dark-theme .feature-card { .control-group.flex-grow.collapsed { flex: 0 0 auto !important; + border-bottom: 1px solid #ddd !important; border-bottom: 1px solid var(--border-color) !important; padding-bottom: 10px !important; } @@ -236,6 +247,7 @@ body.docs-body.dark-theme .feature-card { } .config-link { + color: #01804f; color: var(--brand-green); font-weight: 700; } @@ -243,11 +255,11 @@ body.docs-body.dark-theme .feature-card { input[type='number'], input[type='text'] { margin-bottom: 5px; - border: 1px solid #ddd; } .btn-green { display: inline-block; + background-color: #01804f; background-color: var(--brand-green); color: #fff; padding: 15px 35px; @@ -269,9 +281,11 @@ input[type='text'] { } .btn { + border-radius: 8px; border-radius: var(--btn-radius); padding: 10px 16px; font-size: 0.9rem; + font-weight: 600; font-weight: var(--btn-font-weight); cursor: pointer; transition: all 0.2s ease; @@ -280,13 +294,17 @@ input[type='text'] { justify-content: center; gap: 8px; background-color: #fff; + color: #333; color: var(--text-main); + border: 1px solid #ddd; border: 1px solid var(--border-color); + box-shadow: 0 2px 5px #0000001a; box-shadow: var(--btn-shadow); } .btn:hover:not(:disabled) { transform: translateY(-1px); + box-shadow: 0 4px 10px #00000026; box-shadow: var(--btn-hover-shadow); background-color: #f8f9fa; } @@ -308,6 +326,7 @@ input[type='text'] { } .btn-primary { + background-color: #01804f; background-color: var(--brand-green); color: #fff; border: 0; @@ -323,6 +342,7 @@ input[type='text'] { .btn-add, .btn-secondary { + background-color: #1c3d72; background-color: var(--brand-blue); } @@ -343,6 +363,7 @@ input[type='text'] { } .btn-add { + background: #1c3d72; background: var(--brand-blue); color: #fff; border: 0; @@ -379,8 +400,6 @@ input[type='text'] { .template-select { font-weight: 700; - color: #444; - border: 1px solid #ccc; } #filtersContainer { @@ -415,6 +434,7 @@ input[type='text'] { .filter-row .file-select { flex: 1.2; + border-left: 3px solid #c22636; border-left: 3px solid var(--brand-red); } @@ -457,13 +477,19 @@ input[type='text'] { justify-content: space-between; } +.result-item.selected, .result-item:hover { + background: #fdf2f2; background: var(--active-bg); +} + +.result-item:hover { + color: #c22636; color: var(--brand-red); } .result-item.selected { - background: var(--active-bg); + border-left: 3px solid #c22636; border-left: 3px solid var(--brand-red); } @@ -574,12 +600,15 @@ input[type='range']::-webkit-slider-thumb { margin-top: 20px; padding: 8px 24px; background-color: #fff; + color: #c22636; color: var(--primary-red); + border: 1px solid #c22636; border: 1px solid var(--primary-red); transition: all 0.2s ease; } .loading-overlay .btn:hover { + background-color: #fdf2f2; background-color: var(--active-bg); } @@ -594,6 +623,7 @@ input[type='range']::-webkit-slider-thumb { .resizer:active, .resizer:hover { + background: #c22636; background: var(--primary-red); width: 6px; } @@ -640,6 +670,7 @@ input[type='range']::-webkit-slider-thumb { } .chart-card-compact { + background-color: #fff; background-color: var(--card-bg); } @@ -683,10 +714,11 @@ input[type='range']::-webkit-slider-thumb { } .modal-content { + background: #fff; background: var(--card-bg, #fff); border-radius: 12px; box-shadow: 0 20px 50px #0000004d; - border: 1px solid rgb(255 255 255/10%); + border: 1px solid rgb(255 255 255 / 10%); width: 100%; max-width: 480px; overflow: hidden; @@ -708,8 +740,10 @@ input[type='range']::-webkit-slider-thumb { } .modal-header { + background: #f8f9fa; background: var(--sidebar-bg, #f8f9fa); padding: 15px 20px; + border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color, #eaeaea); display: flex; justify-content: space-between; @@ -719,6 +753,7 @@ input[type='range']::-webkit-slider-thumb { .modal-header h2 { font-size: 1.1rem; font-weight: 600; + color: #333; color: var(--text-color, #333); margin: 0; letter-spacing: 0.5px; @@ -736,6 +771,7 @@ input[type='range']::-webkit-slider-thumb { } .btn-close:hover { + color: #c22636; color: var(--brand-red, #e31837); } @@ -753,6 +789,7 @@ input[type='range']::-webkit-slider-thumb { margin-bottom: 8px; font-size: 0.85rem; font-weight: 600; + color: #666; color: var(--text-muted, #666); text-transform: uppercase; letter-spacing: 0.5px; @@ -763,8 +800,9 @@ input[type='number'], input[type='text'] { width: 100%; border: 1px solid var(--border-color, #ddd); - border-radius: 8px; + background-color: #fff; background-color: var(--input-bg, #fff); + color: #333; color: var(--text-color, #333); font-size: 0.95rem; transition: all 0.2s ease; @@ -774,6 +812,7 @@ input[type='text'] { .template-select:focus, input:focus { outline: 0; + border-color: #c22636; border-color: var(--brand-red, #e31837); box-shadow: 0 0 0 3px #e3183726; } @@ -790,6 +829,7 @@ input:focus { } .modal-footer .btn-primary { + background: linear-gradient(135deg, #c22636 0, #c0152e); background: linear-gradient( 135deg, var(--brand-red, #e31837) 0%, @@ -826,6 +866,7 @@ input:focus { .info-card { background-color: #f8f9fa; border: 1px solid #eee; + border-left: 4px solid #c22636; border-left: 4px solid var(--brand-red, #c22636); border-radius: 8px; padding: 15px; @@ -841,6 +882,9 @@ input:focus { .info-card p { font-size: 0.85rem; margin: 0; +} + +.info-card p { color: #666; } @@ -978,6 +1022,7 @@ input:focus { } .custom-checkbox-container input:checked ~ .checkmark { + background-color: #c22636; background-color: var(--primary-red); } @@ -1005,6 +1050,7 @@ input:focus { justify-content: center; align-items: center; display: flex; + border: 2px dashed #ddd; border: 2px dashed var(--border-color); border-radius: 8px; padding: 4px; @@ -1015,6 +1061,7 @@ input:focus { } .drop-zone.drag-over { + border-color: #c22636; border-color: var(--primary-red); background: #ff00000d; transform: scale(1.01); @@ -1022,14 +1069,20 @@ input:focus { .drop-zone-content i { font-size: 2rem; + color: #666; color: var(--text-muted); margin-bottom: 2px; } +.drop-zone-content p span, +.version-badge-tag:hover { + text-decoration: underline; +} + .drop-zone-content p span, a { + color: #c22636; color: var(--primary-red); - text-decoration: underline; } .version-container { @@ -1037,25 +1090,30 @@ a { align-items: center; gap: 8px; font-size: 0.85rem; + color: #666; color: var(--text-muted); } +.version-badge-tag, +a { + text-decoration: none; +} + .version-badge-tag { background-color: var(--telemetry-boost); padding: 2px 8px; border-radius: 12px; font-weight: 600; - text-decoration: none; transition: opacity 0.2s; } .version-badge-tag:hover { opacity: 0.8; - text-decoration: underline; } .app-version-container { font-size: 12px; + color: #666; color: var(--text-muted); display: flex; align-items: center; @@ -1067,13 +1125,21 @@ footer { border-top: 1px solid var(--border-color); } +.preferences-list hr { + border: 0; + margin: 8px 0; +} + footer { + border-top: 1px solid #ddd; position: fixed; bottom: 0; right: 0; padding: 5px 15px; + background: #fff; background: var(--card-bg); border-top-left-radius: 8px; + border-left: 1px solid #ddd; border-left: 1px solid var(--border-color); font-size: 0.8rem; } @@ -1088,10 +1154,13 @@ footer { } .fa-info-circle { + color: #c22636; color: var(--brand-red); } +.chart-actions .btn-icon:hover, .fa-file-upload { + color: #1c3d72; color: var(--brand-blue); } @@ -1113,6 +1182,7 @@ footer { .chart-actions .btn-icon, .mobile-close-btn { background: 0 0; + color: #666; color: var(--text-muted); cursor: pointer; } @@ -1134,10 +1204,12 @@ footer { } .info-btn i { + color: #c22636 !important; color: var(--primary-red) !important; } .chart-actions .btn-icon { + border: 1px solid #ddd; border: 1px solid var(--border-color); border-radius: 4px; padding: 4px 8px; @@ -1150,7 +1222,7 @@ footer { .chart-actions .btn-icon:hover { background: var(--hover-bg); - color: var(--brand-blue); + border-color: #1c3d72; border-color: var(--brand-blue); } @@ -1209,7 +1281,12 @@ footer { padding: 20px 15px !important; } - .hero h1, + .hero h1 { + margin-bottom: 8px; + font-size: 2.2rem !important; + letter-spacing: 1px; + } + .tech-specs h2 { font-size: 1.4rem !important; margin-bottom: 8px; @@ -1254,6 +1331,7 @@ footer { height: 100vh !important; margin-left: -300px !important; padding: 15px 10px !important; + background: #f8f9fa !important; background: var(--sidebar-bg) !important; overflow: hidden auto !important; display: block !important; @@ -1303,8 +1381,12 @@ footer { .feature-card i, .sidebar h2 { - font-size: 1.1rem !important; margin-bottom: 10px !important; + font-size: 2rem !important; + } + + .sidebar h2 { + font-size: 1.1rem !important; } .sidebar.active { @@ -1342,10 +1424,6 @@ footer { padding: 15px 20px !important; } - .feature-card i { - font-size: 2rem !important; - } - .feature-card h3 { font-size: 1.05rem !important; margin-bottom: 5px !important; @@ -1360,11 +1438,6 @@ footer { border-bottom-width: 3px; } - .hero h1 { - font-size: 2.2rem !important; - letter-spacing: 1px; - } - .hero p { line-height: 1.4; margin-bottom: 25px; @@ -1404,12 +1477,13 @@ footer { } a { - text-decoration: none; font-weight: 700; } .docs-body { + background-color: #0a0a0a; background-color: var(--dark-bg); + color: #f4f4f4; color: var(--light-text); } @@ -1420,6 +1494,7 @@ a { linear-gradient(#1c3d72d9, #0a0a0ae5), url('../resources/background.png'); background-size: cover; background-position: center; + border-bottom: 4px solid #c22636; border-bottom: 4px solid var(--brand-red); } @@ -1451,22 +1526,26 @@ a { padding: 30px; border-radius: 15px; text-align: center; + border-top: 3px solid #1c3d72; border-top: 3px solid var(--brand-blue); transition: 0.3s; } .feature-card:hover { + border-top-color: #01804f; border-top-color: var(--brand-green); transform: translateY(-5px); } .feature-card i { font-size: 3rem; + color: #c22636; color: var(--brand-red); margin-bottom: 20px; } .tech-specs { + background-color: #1c3d72; background-color: var(--brand-blue); padding: 60px 20px; text-align: center; @@ -1476,17 +1555,13 @@ a { background: #ffffff1a; padding: 10px 20px; border-radius: 30px; + border: 1px solid #01804f; border: 1px solid var(--brand-green); font-size: 0.9rem; margin: 5px; display: inline-block; } -.preferences-list hr { - border: 0; - margin: 8px 0; -} - .preferences-list { display: flex; flex-direction: column; @@ -1508,11 +1583,13 @@ a { .pref-title { font-size: 0.85rem; font-weight: 600; + color: #333; color: var(--text-main); } .pref-desc { font-size: 0.7rem; + color: #666; color: var(--text-muted); } @@ -1551,6 +1628,7 @@ a { } input:checked + .slider-round { + background-color: #01804f; background-color: var(--brand-green); } @@ -1565,9 +1643,12 @@ input:checked + .slider-round::before { height: 48px; z-index: 3000; transition: all 0.4s ease; + background-color: #121212bf; background-color: var(--nav-glass-bg); backdrop-filter: blur(12px); + border-bottom: 1px solid rgb(255 255 255 / 10%); border-bottom: 1px solid var(--nav-border); + box-shadow: 0 4px 30px #0000001a; box-shadow: var(--nav-shadow); } @@ -1600,12 +1681,14 @@ input:checked + .slider-round::before { .alert-modal.error .alert-header h2, .nav-link:hover { + color: #c22636; color: var(--brand-red); } .nav-icon-btn { background: 0 0; border: 0; + color: #c22636; color: var(--primary-red); font-size: 1.3rem; cursor: pointer; @@ -1621,6 +1704,7 @@ input:checked + .slider-round::before { } .nav-btn-primary { + background-color: #c22636; background-color: var(--brand-red); color: #fff; padding: 8px 16px; @@ -1648,11 +1732,13 @@ input:checked + .slider-round::before { height: 100%; text-align: center; padding: 20px; + color: #fdf2f2; color: var(--text-empty-state); } .empty-icon { font-size: 4rem; + color: #ddd; color: var(--border-color); margin-bottom: 20px; } @@ -1668,6 +1754,7 @@ input:checked + .slider-round::before { .analyzer-active .top-nav { position: static; + background-color: #f8f9fa; background-color: var(--sidebar-bg); } @@ -1693,6 +1780,7 @@ body.analyzer-active { } .nav-link { + color: #fff !important; color: var(--nav-text) !important; text-decoration: none; font-weight: 500; @@ -1703,24 +1791,28 @@ body.analyzer-active { position: absolute; inset: 0; pointer-events: none; - background: linear-gradient(to bottom, rgb(255 255 255/5%), transparent); + background: linear-gradient(to bottom, rgb(255 255 255 / 5%), transparent); } .alert-modal { max-width: 450px; + border-top: 5px solid #c22636; border-top: 5px solid var(--brand-red); } .alert-modal.success { + border-top-color: #01804f; border-top-color: var(--brand-green); } .alert-modal.success .alert-header h2 { + color: #01804f; color: var(--brand-green); } #alertMessage { white-space: pre-line; + color: #333; color: var(--text-main); line-height: 1.5; } @@ -1728,13 +1820,16 @@ body.analyzer-active { #driveList { max-height: 450px !important; overflow-y: auto; + background: #f8f9fa; background: var(--sidebar-bg); } .drive-file-card { display: flex; align-items: flex-start; + background: #fff; background: var(--card-bg, #fff); + border: 1px solid #ddd; border: 1px solid var(--border-color, #ddd); border-radius: 8px; padding: 12px; @@ -1769,6 +1864,7 @@ body.analyzer-active { .file-name-title { font-weight: 700; font-size: 0.95em; + color: #333; color: var(--text-color); margin-bottom: 6px; word-break: break-all; @@ -1786,6 +1882,7 @@ body.analyzer-active { align-items: center; gap: 5px; font-size: 0.75em; + color: #666; color: var(--text-muted, #666); } @@ -1798,6 +1895,7 @@ body.analyzer-active { #clearDriveFilters { background: 0 0; border: 0; + color: #c22636; color: var(--brand-red); cursor: pointer; padding: 5px; @@ -1818,8 +1916,10 @@ body.analyzer-active { .template-select, input[type='number'], input[type='text'] { + border-radius: 8px !important; border-radius: var(--btn-radius) !important; padding: 10px !important; + border: 1px solid #ddd; border: 1px solid var(--border-color); } @@ -1877,6 +1977,7 @@ input[type='text'] { .view-mode-btn:hover:not(.active), .view-mode-btn:not(.active):hover { background-color: #f1f3f5; + color: #333; color: var(--text-main); } @@ -1903,6 +2004,7 @@ input[type='text'] { margin-left: 5px; background: #00000005; border-left: 1px solid #eee; + color: #333; color: var(--text-color); } @@ -1980,6 +2082,7 @@ input[type='text'] { align-items: center; text-align: center; margin: 10px 0 5px; + color: #666; color: var(--text-muted); font-size: 0.7em; font-weight: 700; @@ -1991,6 +2094,7 @@ input[type='text'] { .signal-separator::before { content: ''; flex: 1; + border-bottom: 1px dashed #ddd; border-bottom: 1px dashed var(--border-color); } @@ -2004,9 +2108,12 @@ input[type='text'] { .searchable-input { padding: 10px; + border-radius: 8px; border-radius: var(--btn-radius); + border: 1px solid #ddd; border: 1px solid var(--border-color); background-color: #fff; + color: #333; color: var(--text-main); } @@ -2016,6 +2123,7 @@ body.pref-theme-dark .searchable-input { .search-results-list { border: 1px solid var(--border-color); + border-radius: 0 0 8px 8px; border-radius: 0 0 var(--btn-radius) var(--btn-radius); } @@ -2027,6 +2135,7 @@ body.pref-theme-dark .searchable-input { .search-option { font-size: 0.9em; + border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color); } @@ -2057,6 +2166,7 @@ body.pref-theme-dark .search-option:hover { top: 50%; transform: translateY(-50%); z-index: 10; + color: #666; color: var(--text-muted); pointer-events: none; } @@ -2410,9 +2520,6 @@ body.pref-theme-dark .search-option:hover { box-shadow: 0 2px 4px #0000000d; border: 1px solid #e5e7eb; padding: 6px !important; - flex: 1; - display: flex; - flex-direction: column; } .xy-panel-toolbar { @@ -2494,7 +2601,7 @@ body.pref-theme-dark .xy-panel-toolbar { } .view-mode-btn { - flex: unset !important; + flex: initial !important; width: 100%; min-height: 38px; display: flex; @@ -2518,24 +2625,14 @@ body.pref-theme-dark .xy-panel-toolbar { } .view-mode-btn.active { + background-color: #1c3d72 !important; background-color: var(--brand-blue) !important; + border-color: #1c3d72 !important; border-color: var(--brand-blue) !important; color: #fff !important; box-shadow: 0 2px 5px #1c3d724d; } -#signalList .signal-item label { - padding: 2px 0; - cursor: pointer; - border-bottom: 1px solid #f9f9f9; - font-size: 0.9em; - color: #444; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - .integrated-toolbar, .view-switcher-container { display: none; @@ -2629,10 +2726,6 @@ body.analyzer-active .view-switcher-container { text-align: center; } -.flex-1 { - flex: 1; -} - .flex-1-min-0 { flex: 1; min-width: 0; @@ -2643,11 +2736,6 @@ body.analyzer-active .view-switcher-container { position: relative; } -.flex-row-gap-5 { - display: flex; - gap: 5px; -} - .w-full { width: 100%; } @@ -2671,6 +2759,7 @@ body.analyzer-active .view-switcher-container { } .btn-brand-red { + background-color: #c22636 !important; background-color: var(--brand-red) !important; } @@ -2691,14 +2780,6 @@ body.analyzer-active .view-switcher-container { opacity: 0.5; } -.btn-rename-project { - border: none; - background: transparent; - cursor: pointer; - color: #888; - padding: 5px; -} - .history-list { max-height: 150px; overflow-y: auto; @@ -2779,7 +2860,7 @@ body.analyzer-active .view-switcher-container { } .xy-toolbar-label { - font-weight: bold; + font-weight: 700; color: #555; } @@ -2798,7 +2879,6 @@ body.analyzer-active .view-switcher-container { flex: 1; display: flex; flex-direction: column; - padding: 10px; min-width: 0; } @@ -2809,7 +2889,6 @@ body.analyzer-active .view-switcher-container { .xy-panel-toolbar { display: flex; gap: 5px; - margin-bottom: 10px; align-items: center; } @@ -2842,13 +2921,12 @@ body.analyzer-active .view-switcher-container { .timeline-header { font-size: 0.8em; - font-weight: bold; + font-weight: 700; color: #555; margin-bottom: 5px; padding-left: 5px; } -/* Math Channels */ .math-desc-container { margin-top: 10px; padding: 10px; @@ -2918,7 +2996,7 @@ body.analyzer-active .view-switcher-container { } .chm-bold-text { - font-weight: bold; + font-weight: 700; } .chm-truncate { @@ -2928,7 +3006,7 @@ body.analyzer-active .view-switcher-container { } .chm-border-none { - border: none !important; + border: 0 !important; } .chm-cursor-help { @@ -3074,18 +3152,14 @@ body.analyzer-active .view-switcher-container { color: #666; } -.drive-search-container input[type='date'] { - background: var(--card-bg); - color: var(--text-color); - font-family: inherit; -} - .drv-search-container { padding: 10px; position: sticky; top: 0; + background: #f8f9fa; background: var(--sidebar-bg); z-index: 5; + border-bottom: 1px solid #ddd; border-bottom: 1px solid var(--border-color); display: flex; flex-direction: column; @@ -3101,6 +3175,7 @@ body.analyzer-active .view-switcher-container { .drv-search-icon { position: absolute; left: 10px; + color: #666; color: var(--text-muted); font-size: 0.9em; } @@ -3109,6 +3184,7 @@ body.analyzer-active .view-switcher-container { width: 100%; padding: 8px 30px; border-radius: 6px; + border: 1px solid #ddd; border: 1px solid var(--border-color); font-size: 0.9em; box-sizing: border-box; @@ -3117,6 +3193,7 @@ body.analyzer-active .view-switcher-container { .drv-clear-icon { position: absolute; right: 10px; + color: #666; color: var(--text-muted); cursor: pointer; display: none; @@ -3133,8 +3210,11 @@ body.analyzer-active .view-switcher-container { flex: 1; padding: 4px; border-radius: 4px; + border: 1px solid #ddd; border: 1px solid var(--border-color); + background: #fff; background: var(--card-bg); + color: #333; color: var(--text-color); font-family: inherit; } @@ -3147,8 +3227,9 @@ body.analyzer-active .view-switcher-container { .drv-result-count { font-size: 0.75em; + color: #666; color: var(--text-muted); - font-weight: bold; + font-weight: 700; } .drv-sort-btn { @@ -3159,24 +3240,26 @@ body.analyzer-active .view-switcher-container { gap: 4px; } +.drv-month-header, +.drv-recent-header { + display: flex; + justify-content: space-between; +} + .drv-month-header { padding: 8px 12px; font-size: 0.75em; font-weight: 800; color: #e31837; - background: rgb(227 24 55 / 5%); + background: #e318370d; border-left: 3px solid #e31837; margin: 10px 0 5px; text-transform: uppercase; cursor: pointer; - display: flex; - justify-content: space-between; align-items: center; } .drv-recent-header { - display: flex; - justify-content: space-between; margin-bottom: 5px; } @@ -3195,12 +3278,14 @@ body.analyzer-active .view-switcher-container { justify-content: space-between; align-items: center; padding: 15px 10px; + border-top: 1px solid #ddd; border-top: 1px solid var(--border-color); margin-top: 10px; font-size: 0.8em; } .drv-page-info { + color: #666; color: var(--text-muted); } @@ -3226,6 +3311,7 @@ body.analyzer-active .view-switcher-container { font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.5px; + color: #666; color: var(--text-muted); } @@ -3235,9 +3321,10 @@ body.analyzer-active .view-switcher-container { .pm-btn-purge { font-size: 0.8em; + color: #c22636; color: var(--brand-red); - background: transparent; - border: none; + background: 0 0; + border: 0; cursor: pointer; } @@ -3249,6 +3336,7 @@ body.analyzer-active .view-switcher-container { .pm-library-empty { padding: 20px; + color: #666; color: var(--text-muted); font-size: 0.9em; text-align: center; @@ -3256,36 +3344,39 @@ body.analyzer-active .view-switcher-container { } .pm-library-item { - display: flex; - align-items: center; justify-content: space-between; padding: 8px 10px; margin-bottom: 6px; - background: rgb(255 255 255 / 3%); + background: #ffffff08; + border: 1px solid #ddd; border: 1px solid var(--border-color); border-radius: 6px; transition: all 0.2s ease; } .pm-library-item.pm-active { - background: rgb(76 175 80 / 10%); - border-color: rgb(76 175 80 / 30%); + background: #4caf501a; + border-color: #4caf504d; } -.pm-col-left { +.pm-col-left, +.pm-col-right, +.pm-library-item { display: flex; align-items: center; +} + +.pm-col-left { flex-grow: 1; overflow: hidden; } .pm-col-right { - display: flex; - align-items: center; flex-shrink: 0; } .pm-icon { + color: #666; color: var(--text-muted); margin-right: 10px; font-size: 1.1em; @@ -3306,6 +3397,7 @@ body.analyzer-active .view-switcher-container { .pm-name { font-size: 0.9em; font-weight: 500; + color: #333; color: var(--text-color); white-space: nowrap; overflow: hidden; @@ -3314,48 +3406,331 @@ body.analyzer-active .view-switcher-container { .pm-meta { font-size: 0.75em; + color: #666; color: var(--text-muted); margin-top: 2px; } -.pm-add-btn { - color: var(--text-color); - background: rgb(255 255 255 / 10%); +.pm-add-btn, +.pm-del-btn { width: 24px; height: 24px; - border-radius: 4px; - margin-right: 8px; - border: none; + border: 0; cursor: pointer; display: flex; align-items: center; justify-content: center; } -.pm-add-btn i { +.pm-add-btn { + color: #333; + color: var(--text-color); + background: #ffffff1a; + border-radius: 4px; + margin-right: 8px; +} + +.pm-add-btn i, +.pm-del-btn i { pointer-events: none; } .pm-del-btn { + color: #666; color: var(--text-muted); - background: transparent; - width: 24px; - height: 24px; - border: none; - cursor: pointer; + background: 0 0; opacity: 0.6; +} + +.pm-status-loaded { + font-size: 0.75em; + font-weight: 700; + color: #4caf50; + margin-right: 8px; +} + +.ui-report-card { + background: linear-gradient( + 145deg, + rgb(255 255 255 / 5%) 0%, + rgb(255 255 255 / 1%) 100% + ); + backdrop-filter: blur(10px); + border: 1px solid rgb(255 255 255 / 10%); + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 6px rgb(0 0 0 / 10%); + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + display: flex; + flex-direction: column; +} + +.ui-report-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px #0003; + border-color: #fff3; +} + +.ui-card-title { + font-weight: 600; + color: var(--text-muted, #aaa); + margin: 0; +} + +.ui-stat-label { + color: var(--text-muted, #999); +} + +.ui-report-empty { + padding: 40px; + text-align: center; + color: #666; + color: var(--text-muted); +} + +.ui-report-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 15px; + padding: 20px; + max-width: 1200px; + margin: 0 auto; +} + +.ui-report-card { + background: #ffffff0d; + padding: 20px; + border-radius: 8px; + border: 1px solid #ddd; + border: 1px solid var(--border-color); +} + +.ui-card-title { + font-size: 0.85em; + color: #666; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; +} + +.ui-stat-row-center, +.ui-stat-row-end { display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 5px; +} + +.ui-stat-row-center { align-items: center; - justify-content: center; + margin-bottom: 8px; } -.pm-del-btn i { - pointer-events: none; +.ui-stat-label { + font-size: 0.9em; + color: #ccc; } -.pm-status-loaded { - font-size: 0.75em; - font-weight: bold; +.ui-stat-label-muted { + color: #aaa; +} + +.ui-val-lg, +.ui-val-xl-red { + font-size: 1.8em; + font-weight: 700; + color: #ff5252; +} + +.ui-val-lg { + font-size: 1.5em; + color: #fff; +} + +.ui-val-md, +.ui-val-stat-green { + font-size: 1.4em; + font-weight: 700; + color: #fff; +} + +.ui-val-stat-green { color: #4caf50; - margin-right: 8px; +} + +.ui-val-stat-blue, +.ui-val-stat-orange { + font-size: 1.4em; + font-weight: 700; + color: #ff9800; +} + +.ui-val-stat-blue { + color: #2196f3; +} + +.ui-stat-sublabel { + font-size: 0.8em; + color: #aaa; +} + +.ui-grid-2col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.ui-efficiency-row { + display: flex; + justify-content: space-between; + font-size: 0.9em; + margin-bottom: 4px; +} + +.ui-progress-track { + height: 6px; + background: #333; + border-radius: 3px; + overflow: hidden; +} + +.ui-progress-fill { + background: #ff9800; + height: 100%; +} + +.ui-avg-speed-container { + text-align: center; + margin-top: 15px; + font-size: 0.9em; + color: #888; +} + +.ui-text-highlight { + color: #eee; +} + +.ui-signal-empty { + padding: 10px; + color: #666; +} + +.ui-search-wrapper { + position: sticky; + top: 0; + background: #fff; + background: var(--card-bg); + z-index: 10; + padding: 10px 5px; + border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-color); +} + +.ui-search-box { + position: relative; + display: flex; + align-items: center; +} + +.ui-search-icon { + position: absolute; + left: 10px; + color: #666; + color: var(--text-muted); + font-size: 0.9em; +} + +.ui-search-input { + width: 100%; + padding: 8px 30px; + border-radius: 6px; + border: 1px solid #ddd; + border: 1px solid var(--border-color); + font-size: 0.9em; + box-sizing: border-box; +} + +.ui-search-clear { + position: absolute; + right: 10px; + color: #666; + color: var(--text-muted); + cursor: pointer; + display: none; +} + +.ui-file-header { + padding: 8px 5px; + font-weight: 700; + border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + background: #f8f9fa; + background: var(--sidebar-bg); +} + +.ui-header-group { + display: flex; + align-items: center; + gap: 8px; +} + +.ui-delete-icon { + color: #c22636; + color: var(--brand-red); + cursor: pointer; +} + +.ui-btn-group-sm { + display: flex; + gap: 4px; +} + +.ui-signal-sublist { + padding-left: 10px; +} + +.ui-signal-item { + display: flex; + align-items: center; + gap: 8px; + padding: 2px 5px; +} + +.ui-color-picker { + width: 18px; + height: 18px; + border: 0; + padding: 0; + background: 0 0; +} + +.ui-color-picker-active { + cursor: pointer; + opacity: 1; +} + +.ui-color-picker-disabled { + cursor: default; + opacity: 0.4; +} + +.ui-signal-label { + font-size: 0.85em; + flex-grow: 1; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ui-signal-label-math { + color: #01804f; + font-weight: 700; } diff --git a/src/telemetryanalyzer.js b/src/telemetryanalyzer.js new file mode 100644 index 0000000..03759a3 --- /dev/null +++ b/src/telemetryanalyzer.js @@ -0,0 +1,159 @@ +import { signalRegistry } from './signalregistry.js'; + +class TelemetryAnalyzer { + constructor() {} + + analyze(file) { + if (!file || !file.signals) return null; + + const stats = { + duration: 0, + distanceKm: 0, + maxSpeed: 0, + avgSpeed: 0, + maxRPM: 0, + maxBoost: 0, + maxOilTemp: 0, + maxCoolantTemp: 0, + idleTimeSec: 0, + idlePercentage: 0, + zeroToSixty: null, + }; + + const signals = file.availableSignals || []; + + const speedName = + signalRegistry.findSignal('Vehicle Speed', signals) || + signalRegistry.findSignal('GPS Speed', signals); + + const rpmName = signalRegistry.findSignal('Engine Speed', signals); + + const boostName = + signalRegistry.findSignal('Boost', signals) || + signalRegistry.findSignal('Intake Manifold Pressure', signals); + + const oilTempName = signalRegistry.findSignal('Oil Temp', signals); + const coolantTempName = signalRegistry.findSignal( + 'Engine Coolant Temp', + signals + ); + + const speedSig = speedName ? file.signals[speedName] : null; + const rpmSig = rpmName ? file.signals[rpmName] : null; + const boostSig = boostName ? file.signals[boostName] : null; + const oilTempSig = oilTempName ? file.signals[oilTempName] : null; + const coolantTempSig = coolantTempName + ? file.signals[coolantTempName] + : null; + + if (!speedSig || !speedSig.length) return stats; + + let totalSpeed = 0; + let validSpeedCount = 0; + let lastTime = speedSig[0].x; + + let perfTimer = { active: false, startTime: 0 }; + + for (let i = 0; i < speedSig.length; i++) { + const p = speedSig[i]; + const speed = parseFloat(p.y); + const time = p.x; + const dt = (time - lastTime) / 1000; + + if (speed > stats.maxSpeed) stats.maxSpeed = speed; + if (speed > 1) { + totalSpeed += speed; + validSpeedCount++; + } + + let isIdle = false; + if (speed < 2) { + if (rpmSig) { + const rpmVal = this.#getValueAt(rpmSig, time); + if (rpmVal > 400) isIdle = true; + } else { + isIdle = true; + } + } + + if (isIdle && dt > 0 && dt < 5) { + stats.idleTimeSec += dt; + } + + // Distance Accumulation + if (i > 0 && dt > 0 && dt < 5) { + const prevSpeed = parseFloat(speedSig[i - 1].y); + const avgSegSpeed = (speed + prevSpeed) / 2; // km/h + const distSegKm = avgSegSpeed * (dt / 3600); + stats.distanceKm += distSegKm; + } + + // 0-100 km/h Detection + if (!perfTimer.active && speed < 1.0) { + // Ready + } else if ( + !perfTimer.active && + speed > 1.0 && + i > 0 && + parseFloat(speedSig[i - 1].y) <= 1.0 + ) { + perfTimer.active = true; + perfTimer.startTime = time; + } else if (perfTimer.active) { + if (speed >= 100) { + const duration = (time - perfTimer.startTime) / 1000; + if (stats.zeroToSixty === null || duration < stats.zeroToSixty) { + stats.zeroToSixty = duration; + } + perfTimer.active = false; + } else if (speed < parseFloat(speedSig[i - 1].y) - 5) { + perfTimer.active = false; // Aborted run + } + } + + lastTime = time; + } + + if (rpmSig) stats.maxRPM = this.#getMax(rpmSig); + + if (boostSig) { + let maxB = this.#getMax(boostSig); + if (maxB > 2000) maxB = maxB / 1000; + stats.maxBoost = maxB; + } + + if (oilTempSig) stats.maxOilTemp = this.#getMax(oilTempSig); + if (coolantTempSig) stats.maxCoolantTemp = this.#getMax(coolantTempSig); + + const totalTimeSec = + (speedSig[speedSig.length - 1].x - speedSig[0].x) / 1000; + stats.duration = totalTimeSec; + + if (validSpeedCount > 0) { + stats.avgSpeed = totalSpeed / validSpeedCount; + } + + if (stats.duration > 0) { + stats.idlePercentage = (stats.idleTimeSec / stats.duration) * 100; + } + + return stats; + } + + #getMax(data) { + if (!data || data.length === 0) return 0; + let max = -Infinity; + for (let i = 0; i < data.length; i++) { + const val = parseFloat(data[i].y); + if (val > max) max = val; + } + return max; + } + + #getValueAt(signalData, time) { + const p = signalData.find((p) => p.x >= time); + return p ? parseFloat(p.y) : 0; + } +} + +export const telemetryAnalyzer = new TelemetryAnalyzer(); diff --git a/src/ui.js b/src/ui.js index 6604d2e..26f842a 100644 --- a/src/ui.js +++ b/src/ui.js @@ -8,6 +8,7 @@ import { messenger } from './bus.js'; import { projectManager } from './projectmanager.js'; import { mapManager } from './mapmanager.js'; import { signalRegistry } from './signalregistry.js'; +import { telemetryAnalyzer } from './telemetryanalyzer.js'; export const UI = { STORAGE_KEY: 'sidebar_collapsed_states', @@ -17,6 +18,7 @@ export const UI = { UI.initVersionInfo(); UI.initSidebarSectionsCollapse(); UI.initMobileUI(); + UI.setupMainTabs(); // Initialize merged Library/Project UI in the specific slot projectManager.initLibraryUI('librarySlot'); @@ -42,7 +44,6 @@ export const UI = { messenger.on('dataprocessor:batch-load-completed', (event) => { UI.renderSignalList(); - // 1. Reveal the container first UI.updateDataLoadedState(true); UI.setLoading(false); @@ -51,7 +52,6 @@ export const UI = { fileInfo.innerText = `${AppState.files.length} logs loaded`; } - // 2. Wait for DOM reflow before rendering chart. if (AppState.files.length > 0) { requestAnimationFrame(() => { ChartManager.render(); @@ -107,6 +107,147 @@ export const UI = { }; }, + setupMainTabs() { + const tabs = document.querySelectorAll('.main-tab-btn'); + const containers = { + graph: document.getElementById('chartContainer'), + report: document.getElementById('report-container'), + }; + + tabs.forEach((tab) => { + tab.addEventListener('click', () => { + tabs.forEach((t) => t.classList.remove('active')); + tab.classList.add('active'); + + const targetId = tab.dataset.target; + + Object.keys(containers).forEach((key) => { + const el = containers[key]; + if (!el) return; + + if (key === targetId) { + el.classList.remove('hidden'); + el.style.display = 'block'; + el.classList.add('active'); + } else { + el.classList.add('hidden'); + el.style.display = 'none'; + el.classList.remove('active'); + } + }); + + if (targetId === 'report') { + this.renderReportView(); + } + }); + }); + }, + + renderReportView() { + const container = document.getElementById('report-container'); + if (!container) return; + + if (AppState.files.length === 0) { + container.innerHTML = + '
No log loaded. Please select a trip from the Library.
'; + return; + } + + const fileIndex = 0; + const file = AppState.files[fileIndex]; + const stats = telemetryAnalyzer.analyze(file); + + if (!stats) { + container.innerHTML = + '
Could not analyze data.
'; + return; + } + + const fmt = (val, dec = 1) => + val !== null && val !== undefined && !isNaN(val) + ? val.toFixed(dec) + : '--'; + const fmtTime = (sec) => { + if (!sec) return '0s'; + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${m}m ${s}s`; + }; + + container.innerHTML = ` +
+ +
+
Performance
+ +
+ 0-100 km/h + ${stats.zeroToSixty ? fmt(stats.zeroToSixty, 2) + 's' : '--'} +
+
+ Max Speed + ${fmt(stats.maxSpeed, 0)} km/h +
+
+ +
+
Trip Info
+ +
+ Distance + ${fmt(stats.distanceKm, 1)} km +
+
+ Duration + ${fmtTime(stats.duration)} +
+
+ +
+
Engine Peaks
+ +
+
+
${fmt(stats.maxBoost, 2)}
+
Boost (bar)
+
+
+
${fmt(stats.maxRPM, 0)}
+
RPM
+
+
+
${fmt(stats.maxOilTemp, 0)}°
+
Oil Temp
+
+
+
${fmt(stats.maxCoolantTemp, 0)}°
+
Coolant
+
+
+
+ +
+
Efficiency
+ +
+
+ Idle Time + ${fmt(stats.idlePercentage, 0)}% +
+
+
+
+
+ +
+ Avg Speed: ${fmt(stats.avgSpeed, 0)} km/h +
+
+ +
+ `; + }, + populateXYSelectors() { const fileSel = document.getElementById('xyFileSelect'); const xSel = document.getElementById('xyXAxis'); @@ -515,21 +656,20 @@ export const UI = { if (!AppState.files || AppState.files.length === 0) { container.innerHTML = - '
No signals available. Load a file first.
'; + '
No signals available. Load a file first.
'; return; } const isCustomEnabled = Preferences.prefs.useCustomPalette; const searchHtml = ` -
-
- +
+
@@ -545,16 +685,15 @@ export const UI = { fileGroup.className = 'file-group-container'; const fileHeader = document.createElement('div'); - fileHeader.className = 'file-meta-header'; - fileHeader.style.cssText = `padding: 8px 5px; font-weight: bold; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; cursor: pointer; background: var(--sidebar-bg);`; + fileHeader.className = 'file-meta-header ui-file-header'; fileHeader.innerHTML = ` - - + + ${file.name} -
+
@@ -562,7 +701,7 @@ export const UI = { const signalListContainer = document.createElement('div'); signalListContainer.id = `sig-list-f${fileIdx}`; - signalListContainer.style.paddingLeft = '10px'; + signalListContainer.className = 'ui-signal-sublist'; fileHeader.onclick = () => { const isHidden = signalListContainer.style.display === 'none'; @@ -616,23 +755,21 @@ export const UI = { const uniqueId = `chk-f${fileIdx}-s${sigIdx}`; const signalItem = document.createElement('div'); - signalItem.className = 'signal-item'; + signalItem.className = 'signal-item ui-signal-item'; signalItem.setAttribute('data-signal-name', signal.toLowerCase()); - signalItem.style.cssText = - 'display: flex; align-items: center; gap: 8px; padding: 2px 5px;'; - const pickerStyle = `width: 18px; height: 18px; border: none; padding: 0; background: none; cursor: ${isCustomEnabled ? 'pointer' : 'default'}; opacity: ${isCustomEnabled ? '1' : '0.4'};`; + const pickerClass = `signal-color-picker ui-color-picker ${isCustomEnabled ? 'ui-color-picker-active' : 'ui-color-picker-disabled'}`; - const labelStyle = isMath - ? 'font-size: 0.85em; flex-grow: 1; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #01804f; font-weight: bold;' - : 'font-size: 0.85em; flex-grow: 1; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'; + const labelClass = isMath + ? 'ui-signal-label ui-signal-label-math' + : 'ui-signal-label'; signalItem.innerHTML = ` - - + `; const picker = signalItem.querySelector('.signal-color-picker');