diff --git a/index.html b/index.html index 3c4f12b..8ebd33f 100644 --- a/index.html +++ b/index.html @@ -297,43 +297,117 @@
Timeline of analysis steps.
- - + -diff --git a/src/dataprocessor.js b/src/dataprocessor.js index 7d6f897..4aa45f6 100644 --- a/src/dataprocessor.js +++ b/src/dataprocessor.js @@ -3,6 +3,7 @@ import { Config, AppState, DOM } from './config.js'; import { Alert } from './alert.js'; import { messenger } from './bus.js'; import { projectManager } from './projectmanager.js'; +import { dbManager } from './dbmanager.js'; /** * DataProcessor Module @@ -40,7 +41,6 @@ class DataProcessor { Config.ANOMALY_TEMPLATES = providedTemplates; } catch (error) { console.error('Config Loader:', error); - // Fallback to safe state try { Config.ANOMALY_TEMPLATES = {}; } catch (e) { @@ -69,17 +69,16 @@ class DataProcessor { files.forEach((file) => { const reader = new FileReader(); - reader.onload = (e) => { + reader.onload = async (e) => { try { let rawData; if (file.name.endsWith('.csv')) { const parsedCSV = this.#parseCSV(e.target.result); - // Normalize "Wide" CSVs (exported from app) to "Long" format rawData = this.#normalizeWideCSV(parsedCSV); } else { rawData = JSON.parse(e.target.result); } - this.#process(rawData, file.name); + await this.#process(rawData, file.name); } catch (err) { const msg = `Error parsing ${file.name}: ${err.message}`; console.error(msg); @@ -100,8 +99,8 @@ class DataProcessor { * @param {Array} data - Array of {s, t, v} points * @param {string} fileName - Source file identifier */ - process(data, fileName) { - const result = this.#process(data, fileName); + async process(data, fileName) { + const result = await this.#process(data, fileName); this.#finalizeBatchLoad(); return result; } @@ -110,17 +109,15 @@ class DataProcessor { * Processes raw telemetry array into a structured log entry. * @private */ - #process(data, fileName) { + async #process(data, fileName) { try { if (!Array.isArray(data)) throw new Error('Input data must be an array'); let telemetryPoints = data; let fileMetadata = {}; - // Check if the first element is a metadata block if (data.length > 0 && data[0].metadata) { fileMetadata = data[0].metadata; - // The rest of the array is the actual telemetry data telemetryPoints = data.slice(1); } @@ -134,7 +131,7 @@ class DataProcessor { // Detect schema based on the first actual data point const schema = this.#detectSchema(telemetryPoints[0]); - // CHANGED: Use flatMap to handle 1-to-many expansion (e.g. Object -> Multiple Signals) + // Use flatMap to handle 1-to-many expansion (e.g. Object -> Multiple Signals) const processedPoints = telemetryPoints.flatMap((item) => this.#applyMappingAndCleaning(item, schema) ); @@ -145,10 +142,33 @@ class DataProcessor { result.metadata = fileMetadata; result.size = telemetryPoints.length; - AppState.files.push(result); + // --- CHANGED: Check for duplicates in Library before saving --- + const allLibraryFiles = await dbManager.getAllFiles(); + const existingFile = allLibraryFiles.find( + (f) => f.name === fileName && f.size === result.size + ); + + if (existingFile) { + console.log( + `File '${fileName}' already exists in library (ID: ${existingFile.id}). Skipping DB save.` + ); + result.dbId = existingFile.id; + } else { + const dbId = await dbManager.saveTelemetry(result); + result.dbId = dbId; + } + + const isAlreadyInSession = AppState.files.some( + (f) => f.dbId === result.dbId + ); + if (!isAlreadyInSession) { + AppState.files.push(result); + } + // Register with project manager (it handles its own duplicate checks for resources) projectManager.registerFile({ name: fileName, + dbId: result.dbId, size: result.size, metadata: result.metadata, }); @@ -265,7 +285,7 @@ class DataProcessor { const keys = Object.keys(rows[0]); - // 1. If it already has the standard columns, return as is. + // If it already has the standard columns, return as is. if ( keys.includes('SensorName') && (keys.includes('Time_ms') || keys.includes('time')) @@ -273,7 +293,7 @@ class DataProcessor { return rows; } - // 2. Detect Time Column + // Detect Time Column const timeKey = keys.find((k) => k.toLowerCase().includes('time')); if (!timeKey) return rows; diff --git a/src/dbmanager.js b/src/dbmanager.js new file mode 100644 index 0000000..d97a8f5 --- /dev/null +++ b/src/dbmanager.js @@ -0,0 +1,142 @@ +import { messenger } from './bus.js'; +import { EVENTS } from './config.js'; + +class DBManager { + #db = null; + #DB_NAME = 'GiuliaTelemetryDB'; + #VERSION = 1; + #isSupported = false; + + constructor() { + // Check if environment supports IndexedDB + this.#isSupported = typeof indexedDB !== 'undefined'; + } + + async init() { + if (!this.#isSupported) { + console.warn( + 'DBManager: IndexedDB is not available in this environment.' + ); + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.#DB_NAME, this.#VERSION); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + // Store file metadata (lightweight) + if (!db.objectStoreNames.contains('files')) { + db.createObjectStore('files', { keyPath: 'id', autoIncrement: true }); + } + // Store heavy signal data separately + if (!db.objectStoreNames.contains('signals')) { + db.createObjectStore('signals', { keyPath: 'fileId' }); + } + }; + + request.onsuccess = () => { + this.#db = request.result; + resolve(); + }; + + request.onerror = () => { + console.error('DB Init Error', request.error); + reject(request.error); + }; + }); + } + + /** + * Saves a processed file to the database. + * Returns the new DB ID. + */ + async saveTelemetry(fileObj) { + if (!this.#isSupported) return null; + if (!this.#db) await this.init(); + if (!this.#db) return null; // Safety check if init failed or still unsupported + + return new Promise((resolve, reject) => { + const transaction = this.#db.transaction( + ['files', 'signals'], + 'readwrite' + ); + + const metadata = { + name: fileObj.name, + size: fileObj.size, + startTime: fileObj.startTime, + duration: fileObj.duration, + availableSignals: fileObj.availableSignals, + metadata: fileObj.metadata || {}, + addedAt: Date.now(), + }; + + const fileRequest = transaction.objectStore('files').add(metadata); + + fileRequest.onsuccess = (event) => { + const fileId = event.target.result; + + // Save the heavy signals array linked by fileId + const signalRequest = transaction.objectStore('signals').add({ + fileId: fileId, + data: fileObj.signals, // The heavy payload + }); + + signalRequest.onsuccess = () => resolve(fileId); + }; + + transaction.onerror = () => reject(transaction.error); + }); + } + + async getAllFiles() { + if (!this.#isSupported) return []; + if (!this.#db) await this.init(); + if (!this.#db) return []; + + return new Promise((resolve) => { + const transaction = this.#db.transaction('files', 'readonly'); + const store = transaction.objectStore('files'); + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => resolve([]); + }); + } + + async getFileSignals(fileId) { + if (!this.#isSupported) return null; + if (!this.#db) await this.init(); + if (!this.#db) return null; + + return new Promise((resolve) => { + const transaction = this.#db.transaction('signals', 'readonly'); + const store = transaction.objectStore('signals'); + const request = store.get(fileId); + request.onsuccess = () => resolve(request.result?.data || null); + request.onerror = () => resolve(null); + }); + } + + async deleteFile(fileId) { + if (!this.#isSupported) return; + if (!this.#db) await this.init(); + if (!this.#db) return; + + const transaction = this.#db.transaction(['files', 'signals'], 'readwrite'); + transaction.objectStore('files').delete(fileId); + transaction.objectStore('signals').delete(fileId); + } + + async clearAll() { + if (!this.#isSupported) return; + if (!this.#db) await this.init(); + if (!this.#db) return; + + const transaction = this.#db.transaction(['files', 'signals'], 'readwrite'); + transaction.objectStore('files').clear(); + transaction.objectStore('signals').clear(); + } +} + +export const dbManager = new DBManager(); diff --git a/src/mathdefinitions.js b/src/mathdefinitions.js index adf3795..9d1fab0 100644 --- a/src/mathdefinitions.js +++ b/src/mathdefinitions.js @@ -292,7 +292,7 @@ export const MATH_DEFINITIONS = [ name: 'Fuel Consumption', label: 'Known Avg Cons. (L/100km)', isConstant: true, - defaultValue: 10.5, // Changed to a more realistic daily average + defaultValue: 10.5, }, ], formula: (values) => { diff --git a/src/projectmanager.js b/src/projectmanager.js index 4b2ff2d..f67d634 100644 --- a/src/projectmanager.js +++ b/src/projectmanager.js @@ -1,66 +1,214 @@ -import { AppState } from './config.js'; +import { AppState, EVENTS } from './config.js'; import { mathChannels } from './mathchannels.js'; import { messenger } from './bus.js'; +import { dbManager } from './dbmanager.js'; class ProjectManager { #currentProject; #isReplaying; + #libraryContainer; constructor() { this.#currentProject = this.#loadFromStorage() || this.#createEmptyProject(); this.#isReplaying = false; + this.#libraryContainer = null; + + dbManager.init().then(async () => { + await this.#hydrateActiveFiles(); + this.renderLibrary(); + }); messenger.on('action:log', (data) => { this.logAction(data.type, data.description, data.payload, data.fileIndex); }); + + messenger.on('dataprocessor:batch-load-completed', () => + this.renderLibrary() + ); } - #createEmptyProject() { - return { - id: crypto.randomUUID(), - name: `Project ${new Date().toLocaleDateString()}`, - createdAt: Date.now(), - resources: [], - history: [], - }; + initLibraryUI(containerId) { + this.#libraryContainer = document.getElementById(containerId); + this.renderLibrary(); } - #loadFromStorage() { - const data = localStorage.getItem('current_project'); - return data ? JSON.parse(data) : null; + async renderLibrary() { + if (!this.#libraryContainer) return; + + const allStoredFiles = await dbManager.getAllFiles(); + allStoredFiles.sort((a, b) => b.addedAt - a.addedAt); + + this.#libraryContainer.innerHTML = ` +