// db/init.js const db = require('./knex'); const { ensureFaceScanAutomation, } = require('./faceScanAutomation'); /* ============================================================ FOTO ============================================================ */ async function ensurePhotos() { const exists = await db.schema.hasTable('photos'); if (!exists) { await db.schema.createTable('photos', (t) => { t.string('id').primary(); t.string('user'); t.string('cartella'); t.string('name'); t.string('path'); t.text('extPath').notNullable().defaultTo(''); t.text('extMedia').notNullable().defaultTo(''); t.integer('hybrid').notNullable().defaultTo(0); t.string('thub1'); t.string('thub2'); t.string('cast'); t.string('mime_type'); t.integer('width'); t.integer('height'); t.integer('rotation'); t.integer('size_bytes'); t.integer('mtimeMs'); t.integer('duration_ms'); t.string('taken_at'); t.string('data'); t.float('lat'); t.float('lon'); t.float('alt'); t.text('location'); t.string('_indexHash'); t.string('fast_hash'); t.datetime('created_at').defaultTo(db.fn.now()); t.datetime('updated_at').defaultTo(db.fn.now()); t.datetime('deleted_at').nullable(); }); console.log("✔ photos creata"); } else { const cols = [ 'thub1','thub2','fast_hash','created_at','updated_at','deleted_at' ]; for (const col of cols) { const existsCol = await db.schema.hasColumn('photos', col); if (!existsCol) { await db.schema.alterTable('photos', (t) => { if (col === 'created_at' || col === 'updated_at') t.datetime(col).defaultTo(db.fn.now()); else if (col === 'deleted_at') t.datetime(col).nullable(); else t.string(col); }); console.log(`✔ aggiunta colonna photos.${col}`); } } } } /* ============================================================ FOTO — deleted_hard ============================================================ */ async function ensureDeletedHard() { const exists = await db.schema.hasTable('deleted_hard'); if (!exists) { await db.schema.createTable('deleted_hard', (t) => { t.string('id').primary(); t.string('user'); t.datetime('deleted_at').defaultTo(db.fn.now()); }); console.log("✔ deleted_hard creata"); } } /* ============================================================ WS ============================================================ */ async function ensureWsSessions() { const exists = await db.schema.hasTable('ws_sessions'); if (!exists) { await db.schema.createTable('ws_sessions', (t) => { t.string('session_id').notNullable(); t.string('device_id').notNullable(); t.string('user').notNullable(); t.integer('connected_at').notNullable(); t.integer('last_ack').notNullable(); t.integer('last_sync').nullable(); t.boolean('need_full_sync').notNullable().defaultTo(false); t.primary(['session_id', 'device_id']); }); console.log("✔ ws_sessions creata"); } else { const cols = [ { name: 'device_id', type: 'string' }, { name: 'last_sync', type: 'integer' } ]; for (const c of cols) { const existsCol = await db.schema.hasColumn('ws_sessions', c.name); if (!existsCol) { await db.schema.alterTable('ws_sessions', (t) => { if (c.type === 'string') t.string(c.name).notNullable().defaultTo('default'); if (c.type === 'integer') t.integer(c.name).nullable(); }); console.log(`✔ aggiunta colonna ws_sessions.${c.name}`); } } } } async function ensureWsPending() { const exists = await db.schema.hasTable('ws_pending_events'); if (!exists) { await db.schema.createTable('ws_pending_events', (t) => { t.string('event_id').primary(); t.string('session_id').notNullable(); t.text('payload').notNullable(); t.integer('sent_at').notNullable(); t.integer('retries').notNullable().defaultTo(0); }); console.log("✔ ws_pending_events creata"); } } async function ensurePendingEvents() { const exists = await db.schema.hasTable('pending_events'); if (!exists) { await db.schema.createTable('pending_events', (t) => { t.increments('id').primary(); t.string('user_id').notNullable(); t.string('session_id').notNullable(); t.string('device_id').notNullable(); t.string('event_id').notNullable(); t.text('payload').notNullable(); t.integer('created_at').notNullable(); t.unique(['user_id', 'session_id', 'device_id', 'event_id']); }); console.log("✔ pending_events creata"); } } /* ============================================================ 🏃 ATTIVITÀ — nuova tabella ============================================================ */ async function ensureActivities() { const exists = await db.schema.hasTable('activities'); if (!exists) { await db.schema.createTable('activities', (t) => { t.string('id').primary(); t.string('user'); t.string('cartella'); t.string('file_name'); t.string('file_extension'); t.string('format'); t.string('display_name'); t.string('route_name'); t.string('title'); t.string('description'); t.string('notes'); t.string('sport'); t.string('activity_type'); t.string('device_manufacturer'); t.string('device_model'); t.string('source_app'); t.bigInteger('start_time_millis'); t.bigInteger('end_time_millis'); t.bigInteger('duration_millis'); t.float('distance_meters'); t.float('elevation_gain_meters'); t.float('elevation_loss_meters'); t.float('min_elevation_meters'); t.float('max_elevation_meters'); t.float('file_distance_meters'); t.float('file_elevation_gain_meters'); t.float('computed_distance_meters'); t.float('computed_elevation_gain_meters'); t.float('average_speed_mps'); t.float('max_speed_mps'); t.float('average_pace_sec_per_km'); t.integer('average_heart_rate'); t.integer('max_heart_rate'); t.integer('average_cadence'); t.integer('max_cadence'); t.integer('calories'); t.integer('power_avg'); t.integer('power_max'); t.integer('point_count'); t.integer('segment_count'); t.integer('lap_count'); t.float('min_latitude'); t.float('max_latitude'); t.float('min_longitude'); t.float('max_longitude'); t.float('center_latitude'); t.float('center_longitude'); t.bigInteger('size_bytes'); t.bigInteger('last_modified_millis'); t.bigInteger('date_added_millis'); t.bigInteger('date_modified_millis'); t.bigInteger('last_scan_millis'); t.boolean('is_favorite'); t.boolean('is_hidden'); t.integer('rating'); t.text('tags'); t.text('track_preview'); t.string('scan_status'); t.text('scan_error'); t.string('path'); t.text('extPath').notNullable().defaultTo(''); t.integer('hybrid').notNullable().defaultTo(0); t.text('diagnostics'); t.text('location'); t.string('fast_hash'); t.datetime('deleted_at').nullable(); t.datetime('created_at').defaultTo(db.fn.now()); t.datetime('updated_at').defaultTo(db.fn.now()); }); console.log("✔ activities creata"); } } /* ============================================================ ATTIVITÀ — deleted_hard ============================================================ */ async function ensureDeletedHardActivities() { const exists = await db.schema.hasTable('deleted_hard_activities'); if (!exists) { await db.schema.createTable('deleted_hard_activities', (t) => { t.string('id').primary(); t.string('user'); t.datetime('deleted_at').defaultTo(db.fn.now()); }); console.log("✔ deleted_hard_activities creata"); } } /* ============================================================ ATTIVITÀ — sync_done ============================================================ */ async function ensureSyncDoneActivities() { const exists = await db.schema.hasTable('sync_done_activities'); if (!exists) { await db.schema.createTable('sync_done_activities', (t) => { t.string('session_id'); t.string('device_id'); t.bigInteger('last_sync'); }); console.log("✔ sync_done_activities creata"); } } /* ============================================================ VOLTI ============================================================ */ async function ensureFaces() { const exists = await db.schema.hasTable('faces'); if (!exists) { await db.schema.createTable('faces', (t) => { // ID univoco del volto t.string('id').primary(); // Foto da cui è stato estratto t.string('photo_id').notNullable(); t.integer('face_index').notNullable(); // Bounding box nel frame originale t.float('x').notNullable(); t.float('y').notNullable(); t.float('width').notNullable(); t.float('height').notNullable(); // Confidence del detector SCRFD t.float('score').notNullable(); // 5 landmark × 2 coordinate t.float('landmark_0_x'); t.float('landmark_0_y'); t.float('landmark_1_x'); t.float('landmark_1_y'); t.float('landmark_2_x'); t.float('landmark_2_y'); t.float('landmark_3_x'); t.float('landmark_3_y'); t.float('landmark_4_x'); t.float('landmark_4_y'); // 128 × float32 = 512 byte t.binary('embedding').notNullable(); // Risultato HDBSCAN. // NULL = non ancora clusterizzato. t.string('cluster_id').nullable(); // Persona assegnata dall'utente. // NULL = identità non ancora assegnata. t.string('person_id').nullable(); // Stato applicativo del volto. // detected / clustered / assigned t.string('status').notNullable().defaultTo('detected'); t.datetime('created_at').defaultTo(db.fn.now()); t.datetime('updated_at').defaultTo(db.fn.now()); // Indici principali t.index(['photo_id']); t.index(['cluster_id']); t.index(['person_id']); t.index(['status']); t.unique(['photo_id', 'face_index']); // Evita due record per lo stesso volto nella stessa foto // solo se il nostro scanner assegnerà un face_index. }); console.log("✔ faces creata"); } } /* ============================================================ CODA SCANSIONE VOLTI ============================================================ */ async function ensureFaceScanJobs() { const exists = await db.schema.hasTable('face_scan_jobs'); if (!exists) { await db.schema.createTable( 'face_scan_jobs', (t) => { t.string('photo_id').primary(); // pending / processing / done / error t.string('status') .notNullable() .defaultTo('pending'); t.integer('attempts') .notNullable() .defaultTo(0); t.integer('rescan_requested') .notNullable() .defaultTo(0); t.integer('faces_detected').nullable(); t.integer('execution_ms').nullable(); t.integer('applied_rotation').nullable(); t.text('last_error').nullable(); t.datetime('started_at').nullable(); t.datetime('completed_at').nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.index(['status']); t.index(['updated_at']); }, ); console.log("✔ face_scan_jobs creata"); } else { const hasRescanRequested = await db.schema.hasColumn( 'face_scan_jobs', 'rescan_requested', ); if (!hasRescanRequested) { await db.schema.alterTable( 'face_scan_jobs', (t) => { t.integer('rescan_requested') .notNullable() .defaultTo(0); }, ); console.log( "✔ aggiunta face_scan_jobs.rescan_requested", ); } } } /* ============================================================ CLUSTER HDBSCAN ============================================================ */ async function ensureFaceClusters() { const exists = await db.schema.hasTable('face_clusters'); if (!exists) { await db.schema.createTable('face_clusters', (t) => { // ID persistente del cluster t.string('id').primary(); // Identifica una specifica esecuzione di HDBSCAN. // Esempio: "initial-2026-09-23T04:20:00" t.string('run_id').notNullable(); // Label numerica prodotta da HDBSCAN. // Può essere -1 per noise. t.integer('label').notNullable(); // Numero di facce nel cluster t.integer('size').notNullable().defaultTo(0); // Parametri principali usati per generare il cluster t.integer('min_samples'); t.integer('min_cluster_size'); // NULL se il cluster non è ancora stato // associato ad una persona. t.string('person_id').nullable(); t.datetime('created_at').defaultTo(db.fn.now()); t.datetime('updated_at').defaultTo(db.fn.now()); t.index(['run_id']); t.index(['person_id']); // Un label può comparire una sola volta // all'interno dello stesso run. t.unique(['run_id', 'label']); }); console.log("✔ face_clusters creata"); } } /* ============================================================ RUN CLUSTERING VOLTI ============================================================ */ async function ensureFaceClusterRuns() { const exists = await db.schema.hasTable( 'face_cluster_runs', ); if (!exists) { await db.schema.createTable( 'face_cluster_runs', (t) => { // Identificativo stabile della run. t.string('id').primary(); // Proprietario della libreria fotografica. t.string('user') .notNullable(); // pending / processing / done / error t.string('status') .notNullable() .defaultTo('pending'); // all / unclustered / unknown t.string('scope') .notNullable() .defaultTo('all'); t.integer('embedding_count') .notNullable() .defaultTo(0); t.integer('min_cluster_size') .notNullable(); t.integer('min_samples') .notNullable(); t.string('metric') .notNullable() .defaultTo('euclidean'); // eom / leaf t.string('selection_method') .notNullable() .defaultTo('eom'); // sklearn / native / opencl-native t.string('engine') .notNullable() .defaultTo('sklearn'); t.string('engine_version') .nullable(); t.integer('cluster_count') .nullable(); t.integer('clustered_faces') .nullable(); t.integer('noise_faces') .nullable(); t.float('noise_percent') .nullable(); t.text('last_error') .nullable(); t.datetime('started_at') .nullable(); t.datetime('completed_at') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.index(['user']); t.index(['status']); t.index(['user', 'created_at']); }, ); console.log( "✔ face_cluster_runs creata", ); } } /* ============================================================ APPARTENENZE DEI VOLTI AI CLUSTER ============================================================ */ async function ensureFaceClusterMemberships() { const exists = await db.schema.hasTable( 'face_cluster_memberships', ); if (!exists) { await db.schema.createTable( 'face_cluster_memberships', (t) => { // Identificativo stabile della membership. t.string('id').primary(); // Run HDBSCAN che ha prodotto il risultato. t.string('run_id') .notNullable(); // NULL per i volti classificati come noise. t.string('cluster_id') .nullable(); t.string('face_id') .notNullable(); // Label originale HDBSCAN. // -1 indica noise. t.integer('label') .notNullable(); // Probabilità di appartenenza HDBSCAN. t.float('probability') .notNullable() .defaultTo(0); t.integer('noise_flag') .notNullable() .defaultTo(0); // suggested / accepted / excluded t.string('review_status') .notNullable() .defaultTo('suggested'); t.string('reviewed_by') .nullable(); t.datetime('reviewed_at') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); // Un volto compare una sola volta per run. t.unique([ 'run_id', 'face_id', ]); t.index(['run_id']); t.index(['cluster_id']); t.index(['face_id']); t.index(['review_status']); t.index([ 'run_id', 'label', ]); }, ); console.log( "✔ face_cluster_memberships creata", ); } } /* ============================================================ ESCLUSIONI MANUALI PERSONA-VOLTO ============================================================ */ async function ensurePersonFaceExclusions() { const exists = await db.schema.hasTable( 'person_face_exclusions', ); if (!exists) { await db.schema.createTable( 'person_face_exclusions', (t) => { // Persona dalla quale il volto è stato escluso. t.string('person_id') .notNullable(); t.string('face_id') .notNullable(); // Proprietario della libreria. t.string('user') .notNullable(); t.text('reason') .nullable(); t.string('created_by') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.primary([ 'person_id', 'face_id', ]); t.index(['user']); t.index(['face_id']); }, ); console.log( "✔ person_face_exclusions creata", ); } } /* ============================================================ PERSONE ============================================================ */ async function ensurePersons() { const exists = await db.schema.hasTable('persons'); if (!exists) { await db.schema.createTable('persons', (t) => { // ID stabile dell'identità applicativa t.string('id').primary(); // Utente proprietario della libreria t.string('user').nullable(); // Nome assegnato dall'utente t.string('name').notNullable(); t.datetime('created_at').defaultTo(db.fn.now()); t.datetime('updated_at').defaultTo(db.fn.now()); t.index(['user']); t.index(['name']); }); console.log("✔ persons creata"); } } /* ============================================================ PROTOTYPE ATTIVI DELLE PERSONE ============================================================ */ async function ensurePersonFacePrototypes() { const exists = await db.schema.hasTable( 'person_face_prototypes', ); if (!exists) { await db.schema.createTable( 'person_face_prototypes', (t) => { // Persona rappresentata dal prototype. t.string('person_id') .notNullable(); // Volto reale usato come riferimento. t.string('face_id') .notNullable(); // Proprietario della libreria. t.string('user') .notNullable(); // cluster / noise / manual t.string('source_type') .notNullable(); // cluster_medoid / // greedy_leave_one_out_gain / // manual t.string('selection_method') .notNullable(); // Ordine nel set dei prototype. t.integer('selection_order') .nullable(); t.float('detector_score') .nullable(); t.float('quality_score') .nullable(); // 1 = usato nell'indice corrente. t.integer('active') .notNullable() .defaultTo(1); // 1 = prototype protetto manualmente. // Il ricalcolo automatico non lo disattiva. t.integer('locked') .notNullable() .defaultTo(0); // Nome o versione dell'esperimento. t.string('experiment_name') .nullable(); // Metriche e metadati in formato JSON. t.text('metrics_json') .nullable(); t.string('created_by') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.primary([ 'person_id', 'face_id', ]); t.index(['user']); t.index(['person_id']); t.index(['face_id']); t.index(['active']); t.index([ 'user', 'active', ]); }, ); console.log( "✔ person_face_prototypes creata", ); } else { const hasLocked = await db.schema.hasColumn( 'person_face_prototypes', 'locked', ); if (!hasLocked) { await db.schema.alterTable( 'person_face_prototypes', (t) => { t.integer('locked') .notNullable() .defaultTo(0); }, ); console.log( "✔ aggiunta person_face_prototypes.locked", ); } } } /* ============================================================ VOLTI CONFERMATI NON ADATTI COME RIFERIMENTO ============================================================ */ async function ensurePersonFaceReferenceExclusions() { const exists = await db.schema.hasTable( 'person_face_reference_exclusions', ); if (!exists) { await db.schema.createTable( 'person_face_reference_exclusions', (t) => { // La persona rimane confermata. t.string('person_id') .notNullable(); // Il volto non deve entrare // nell'indice di riconoscimento. t.string('face_id') .notNullable(); // Proprietario della libreria. t.string('user') .notNullable(); // Esempi: // occlusion_extreme_pose // blur // face_too_small // bad_crop // manual t.string('reason_code') .notNullable(); t.text('note') .nullable(); t.string('created_by') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.primary([ 'person_id', 'face_id', ]); t.index(['user']); t.index(['person_id']); t.index(['face_id']); t.index(['reason_code']); }, ); console.log( "✔ person_face_reference_exclusions creata", ); } } /* ============================================================ STATO DELL'INDICE DI RICONOSCIMENTO ============================================================ */ async function ensureFaceRecognitionIndexState() { const exists = await db.schema.hasTable( 'face_recognition_index_state', ); if (!exists) { await db.schema.createTable( 'face_recognition_index_state', (t) => { // Una riga per proprietario // della libreria. t.string('user') .primary(); // missing / dirty / building / // ready / error t.string('status') .notNullable() .defaultTo('missing'); // 1 se l'indice deve essere // ricostruito. t.integer('dirty') .notNullable() .defaultTo(1); t.integer('version') .notNullable() .defaultTo(0); t.integer('prototype_count') .notNullable() .defaultTo(0); t.string('index_path') .nullable(); t.string('metadata_path') .nullable(); t.datetime('built_at') .nullable(); t.text('last_error') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.index(['status']); t.index(['dirty']); }, ); console.log( "✔ face_recognition_index_state creata", ); } } /* ============================================================ JOB DI RICALCOLO DEI PROTOTYPE ============================================================ */ async function ensureFacePrototypeJobs() { const exists = await db.schema.hasTable( 'face_prototype_jobs', ); if (!exists) { await db.schema.createTable( 'face_prototype_jobs', (t) => { t.string('id') .primary(); t.string('user') .notNullable(); t.string('person_id') .notNullable(); // pending / processing / done / // error / superseded t.string('status') .notNullable() .defaultTo('pending'); // Motivo dell'ultimo aggiornamento. t.string('reason') .notNullable(); t.string('requested_by') .nullable(); t.datetime('requested_at') .notNullable() .defaultTo(db.fn.now()); t.datetime('started_at') .nullable(); t.datetime('completed_at') .nullable(); t.integer('attempts') .notNullable() .defaultTo(0); t.text('last_error') .nullable(); t.text('result_json') .nullable(); // Firma dello stato dei volti // all'inizio dell'elaborazione. t.string('input_signature') .nullable(); t.datetime('created_at') .defaultTo(db.fn.now()); t.datetime('updated_at') .defaultTo(db.fn.now()); t.index(['user']); t.index(['person_id']); t.index(['status']); t.index([ 'user', 'status', 'requested_at', ]); }, ); console.log( "✔ face_prototype_jobs creata", ); } } /* ============================================================ INIT ============================================================ */ /* async function init() { try { console.log("🔧 init DB..."); await ensurePhotos(); await ensureDeletedHard(); await ensureActivities(); await ensureDeletedHardActivities(); await ensureSyncDoneActivities(); await ensureWsSessions(); await ensureWsPending(); await ensurePendingEvents(); console.log("✅ init completato"); process.exit(0); } catch (err) { console.error("❌ errore init:", err); process.exit(1); } } */ async function init() { try { console.log("🔧 init DB..."); await ensurePhotos(); await ensureDeletedHard(); await ensureActivities(); await ensureDeletedHardActivities(); await ensureSyncDoneActivities(); await ensureWsSessions(); await ensureWsPending(); await ensurePendingEvents(); // Face recognition await ensureFaces(); await ensureFaceScanJobs(); await ensureFaceScanAutomation(db); await ensureFaceClusters(); await ensureFaceClusterRuns(); await ensureFaceClusterMemberships(); await ensurePersons(); await ensurePersonFaceExclusions(); await ensurePersonFacePrototypes(); await ensurePersonFaceReferenceExclusions(); await ensureFaceRecognitionIndexState(); await ensureFacePrototypeJobs(); console.log("✅ init completato"); process.exit(0); } catch (err) { console.error("❌ errore init:", err); process.exit(1); } } init();