Showing 6 changed files with 241 additions and 73 deletions
+1 -1
projects/autoNAS
@@ -1 +1 @@
1
-Subproject commit ec6d560e9debb952a07887d1d6120ea0cf9fcbd4
1
+Subproject commit 957b7631b97c0c2fb064ef2ac0789e31b6eb7890
+47 -10
projects/autoSMART/lib/SmartCollector.pm
@@ -308,17 +308,14 @@ sub store_smart_data {
308 308
     eval {
309 309
         # Detect/handle HDD migration first
310 310
         my $hdd_id = $self->_detect_or_create_hdd($drive_info, $smart_data);
311
-        
312
-        # Check if we should store this reading using differential storage
313
-        my $should_store = $self->_should_store_reading($hdd_id, $smart_data);
314
-        
315
-        if ($should_store->{store}) {
316
-            # Insert SMART reading with differential storage information
317
-            $self->_insert_smart_reading_differential($hdd_id, $drive_info, $smart_data, $should_store);
318
-            
319
-            $self->_log("Stored SMART data for HDD ID $hdd_id (Serial: $smart_data->{serial_number}, Type: $should_store->{type})", 2);
311
+
312
+        # SCHEMA v2: Store complete reading (no differential storage)
313
+        my $event_id = $self->_insert_collection_event($hdd_id, $drive_info, $smart_data);
314
+
315
+        if ($event_id) {
316
+            $self->_log("Stored SMART data: HDD ID $hdd_id, Event ID $event_id (Serial: $smart_data->{serial_number})", 2);
320 317
         } else {
321
-            $self->_log("Skipped unchanged SMART data for HDD ID $hdd_id (Serial: $smart_data->{serial_number})", 3);
318
+            $self->_log("Failed to store SMART data for HDD ID $hdd_id", 1);
322 319
         }
323 320
     };
324 321
     
@@ -776,6 +773,46 @@ sub _register_node {
776 773
     }
777 774
 }
778 775
 
776
+=head2 _insert_collection_event (SCHEMA v2)
777
+
778
+SCHEMA v2: Insert complete SMART reading (no differential storage)
779
+Calls PostgreSQL function: insert_collection_event()
780
+
781
+=cut
782
+
783
+sub _insert_collection_event {
784
+    my ($self, $hdd_id, $drive_info, $smart_data) = @_;
785
+
786
+    # Build parameters JSON for database function
787
+    my $params_json = encode_json($smart_data->{parameters} || {});
788
+    my $checksum = sha256_hex($params_json . ($smart_data->{temperature} || ''));
789
+
790
+    # Call PostgreSQL function: insert_collection_event(hdd_id, serial, node, ts, temp, ok, checksum, params::JSONB)
791
+    my $sth = $self->{db_handle}->prepare(q{
792
+        SELECT insert_collection_event(?, ?, ?, NOW(), ?, ?, ?, ?::jsonb)
793
+    });
794
+
795
+    eval {
796
+        $sth->execute(
797
+            $hdd_id,
798
+            $smart_data->{serial_number},
799
+            $self->{node_id},
800
+            $smart_data->{temperature} || undef,
801
+            1,  # collection_ok = true
802
+            $checksum,
803
+            $params_json
804
+        );
805
+    };
806
+
807
+    if ($@) {
808
+        $self->_log("ERROR inserting collection event: $@", 1);
809
+        return undef;
810
+    }
811
+
812
+    my ($event_id) = $sth->fetchrow_array();
813
+    return $event_id;
814
+}
815
+
779 816
 =head2 DESTROY
780 817
 
781 818
 Cleanup database connection
+57 -0
projects/autoSMART/scripts/deploy-tapia.sh
@@ -0,0 +1,57 @@
1
+#!/bin/bash
2
+
3
+# autoSMART Schema v2 — Deploy to Tapia (secondary node)
4
+# Date: 2026-05-20
5
+# Copies updated collectors and restarts service
6
+
7
+set -e
8
+
9
+TAPIA_IP="192.168.2.93"
10
+TAPIA_USER="root"
11
+TARGET_DIR="/opt/autoSMART"
12
+PROJECT_DIR="$(dirname "$(dirname "$0")")"
13
+
14
+echo "🚀 Deploying autoSMART collectors to tapia ($TAPIA_IP)..."
15
+echo ""
16
+
17
+# 1. Copy updated scripts
18
+echo "📦 Copying updated scripts..."
19
+scp -q "$PROJECT_DIR/scripts/smart-collector-daemon.pl" "$TAPIA_USER@$TAPIA_IP:$TARGET_DIR/scripts/"
20
+scp -q "$PROJECT_DIR/lib/SmartCollector.pm" "$TAPIA_USER@$TAPIA_IP:$TARGET_DIR/lib/"
21
+echo "✅ Scripts copied"
22
+echo ""
23
+
24
+# 2. Stop service on tapia
25
+echo "🛑 Stopping autosmart service on tapia..."
26
+ssh "$TAPIA_USER@$TAPIA_IP" "systemctl stop autosmart" || true
27
+sleep 2
28
+echo "✅ Service stopped"
29
+echo ""
30
+
31
+# 3. Restart service
32
+echo "🚀 Starting autosmart service on tapia..."
33
+ssh "$TAPIA_USER@$TAPIA_IP" "systemctl start autosmart"
34
+sleep 3
35
+echo "✅ Service started"
36
+echo ""
37
+
38
+# 4. Verify service is running
39
+echo "🔍 Verifying service status..."
40
+if ssh "$TAPIA_USER@$TAPIA_IP" "systemctl is-active --quiet autosmart"; then
41
+    echo "✅ autosmart service is running on tapia"
42
+else
43
+    echo "❌ autosmart service failed to start!"
44
+    exit 1
45
+fi
46
+echo ""
47
+
48
+# 5. Check recent logs
49
+echo "📋 Last 5 log entries from tapia:"
50
+ssh "$TAPIA_USER@$TAPIA_IP" "tail -5 /var/log/syslog | grep autosmart" || true
51
+echo ""
52
+
53
+echo "✅ Deploy to tapia complete!"
54
+echo ""
55
+echo "Next: Verify data collection:"
56
+echo "  ssh root@ebony \"psql -h 192.168.2.102 -U postgres -d autosmart\""
57
+echo "  SELECT node_id, COUNT(*) FROM smart_collection_events GROUP BY node_id;"
+22 -29
projects/autoSMART/scripts/smart-collector-daemon.pl
@@ -213,38 +213,31 @@ sub process_device {
213 213
     
214 214
     # Get or create HDD inventory entry
215 215
     my $hdd_id = get_or_create_hdd($dbh, $serial, $model, $device);
216
-    
217
-    # Check if we should store this reading
216
+
217
+    # SCHEMA v2: Store complete reading via PostgreSQL function
218 218
     my $params_json = encode_json(\%smart_params);
219
-    
220
-    if (!$force_full && !$config->{node}{store_unchanged}) {
221
-        # Check for recent identical reading
222
-        my $sth = $dbh->prepare("
223
-            SELECT id FROM smart_readings 
224
-            WHERE hdd_id = ? AND parameters_json = ? 
225
-            AND timestamp > NOW() - INTERVAL '1 hour'
226
-            LIMIT 1
227
-        ");
228
-        $sth->execute($hdd_id, $params_json);
229
-        
230
-        if ($sth->fetchrow_array()) {
231
-            log_message("  Skipping unchanged parameters") if $debug;
232
-            return;
233
-        }
234
-    }
235
-    
236
-    # Store SMART reading
237
-    my $reading_type = $force_full ? 'full' : 'differential';
238
-    
219
+    my $checksum = sha256_hex($params_json . ($temp || ''));
220
+
239 221
     my $sth = $dbh->prepare("
240
-        INSERT INTO smart_readings (hdd_id, serial_number, device_path, node_id, timestamp, temperature, parameters_json, reading_type)
241
-        VALUES (?, ?, ?, ?, NOW(), ?, ?::jsonb, ?)
242
-        RETURNING id
222
+        SELECT insert_collection_event(?, ?, ?, NOW(), ?, ?, ?, ?::jsonb)
243 223
     ");
244
-    
245
-    my $reading_id = $dbh->selectrow_array($sth, undef, $hdd_id, $serial, $device, $node_id, $temp || 0, $params_json, $reading_type);
246
-    
247
-    log_message("  ✓ SMART reading stored (ID: $reading_id, temp: " . ($temp || 0) . "°C, type: $reading_type)") if $debug;
224
+
225
+    my $event_id = $dbh->selectrow_array(
226
+        $sth, undef,
227
+        $hdd_id,
228
+        $serial,
229
+        $node_id,
230
+        $temp || 0,
231
+        1,  # collection_ok = true
232
+        $checksum,
233
+        $params_json
234
+    );
235
+
236
+    if ($event_id) {
237
+        log_message("  ✓ SMART event stored (ID: $event_id, temp: " . ($temp || 0) . "°C, params: " . scalar(keys %smart_params) . ")") if $debug;
238
+    } else {
239
+        log_message("  ✗ Failed to store SMART event for $serial");
240
+    }
248 241
 }
249 242
 
250 243
 sub get_or_create_hdd {
+64 -0
projects/autoSMART/sql/rename-to-archive-v1.sql
@@ -0,0 +1,64 @@
1
+-- autoSMART — Rename v1 tables to archive
2
+-- Run on-demand after validating collectors work with schema v2
3
+-- Date: 2026-05-20
4
+-- Safe: tables are read-only at this point
5
+
6
+BEGIN;
7
+
8
+-- Rename tables
9
+ALTER TABLE IF EXISTS smart_readings RENAME TO smart_readings_archive_v1;
10
+ALTER TABLE IF EXISTS smart_thresholds RENAME TO smart_thresholds_archive_v1;
11
+ALTER TABLE IF EXISTS alert_history RENAME TO alert_history_archive_v1;
12
+
13
+-- Rename sequences
14
+ALTER SEQUENCE IF EXISTS smart_readings_id_seq RENAME TO smart_readings_archive_v1_id_seq;
15
+ALTER SEQUENCE IF EXISTS smart_thresholds_id_seq RENAME TO smart_thresholds_archive_v1_id_seq;
16
+ALTER SEQUENCE IF EXISTS alert_history_id_seq RENAME TO alert_history_archive_v1_id_seq;
17
+
18
+-- Rename indexes
19
+ALTER INDEX IF EXISTS smart_readings_pkey RENAME TO smart_readings_archive_v1_pkey;
20
+ALTER INDEX IF EXISTS idx_smart_readings_hdd_id RENAME TO idx_smart_readings_archive_v1_hdd_id;
21
+ALTER INDEX IF EXISTS idx_smart_readings_timestamp RENAME TO idx_smart_readings_archive_v1_timestamp;
22
+ALTER INDEX IF EXISTS idx_smart_readings_serial RENAME TO idx_smart_readings_archive_v1_serial;
23
+ALTER INDEX IF EXISTS idx_smart_readings_device_path RENAME TO idx_smart_readings_archive_v1_device_path;
24
+ALTER INDEX IF EXISTS idx_smart_readings_type RENAME TO idx_smart_readings_archive_v1_type;
25
+ALTER INDEX IF EXISTS idx_smart_readings_checksum RENAME TO idx_smart_readings_archive_v1_checksum;
26
+ALTER INDEX IF EXISTS idx_smart_readings_previous RENAME TO idx_smart_readings_archive_v1_previous;
27
+ALTER INDEX IF EXISTS idx_smart_readings_parameters RENAME TO idx_smart_readings_archive_v1_parameters;
28
+ALTER INDEX IF EXISTS idx_smart_readings_changed_params RENAME TO idx_smart_readings_archive_v1_changed_params;
29
+
30
+ALTER INDEX IF EXISTS smart_thresholds_pkey RENAME TO smart_thresholds_archive_v1_pkey;
31
+ALTER INDEX IF EXISTS uq_parameter_name RENAME TO uq_archive_v1_parameter_name;
32
+
33
+ALTER INDEX IF EXISTS alert_history_pkey RENAME TO alert_history_archive_v1_pkey;
34
+ALTER INDEX IF EXISTS idx_alert_history_hdd_id RENAME TO idx_alert_history_archive_v1_hdd_id;
35
+ALTER INDEX IF EXISTS idx_alert_history_sent_at RENAME TO idx_alert_history_archive_v1_sent_at;
36
+ALTER INDEX IF EXISTS idx_alert_history_severity RENAME TO idx_alert_history_archive_v1_severity;
37
+ALTER INDEX IF EXISTS idx_alert_history_serial RENAME TO idx_alert_history_archive_v1_serial;
38
+
39
+-- Rename constraints
40
+ALTER TABLE smart_readings_archive_v1 RENAME CONSTRAINT smart_readings_hdd_id_fkey TO smart_readings_archive_v1_hdd_id_fkey;
41
+ALTER TABLE smart_readings_archive_v1 RENAME CONSTRAINT smart_readings_previous_reading_id_fkey TO smart_readings_archive_v1_previous_reading_id_fkey;
42
+ALTER TABLE alert_history_archive_v1 RENAME CONSTRAINT alert_history_hdd_id_fkey TO alert_history_archive_v1_hdd_id_fkey;
43
+ALTER TABLE alert_history_archive_v1 RENAME CONSTRAINT alert_history_related_reading_id_fkey TO alert_history_archive_v1_related_reading_id_fkey;
44
+ALTER TABLE alert_history_archive_v1 RENAME CONSTRAINT alert_history_related_prediction_id_fkey TO alert_history_archive_v1_related_prediction_id_fkey;
45
+
46
+COMMIT;
47
+
48
+-- Summary
49
+DO $$
50
+BEGIN
51
+    RAISE NOTICE '✅ Archive rename complete!';
52
+    RAISE NOTICE 'Tables renamed:';
53
+    RAISE NOTICE '  • smart_readings → smart_readings_archive_v1';
54
+    RAISE NOTICE '  • smart_thresholds → smart_thresholds_archive_v1';
55
+    RAISE NOTICE '  • alert_history → alert_history_archive_v1';
56
+    RAISE NOTICE '';
57
+    RAISE NOTICE 'These tables are now read-only archives.';
58
+    RAISE NOTICE 'All new data flows through schema v2.';
59
+    RAISE NOTICE '';
60
+    RAISE NOTICE 'To delete archives (after 4-6 week validation):';
61
+    RAISE NOTICE '  DROP TABLE smart_readings_archive_v1 CASCADE;';
62
+    RAISE NOTICE '  DROP TABLE smart_thresholds_archive_v1 CASCADE;';
63
+    RAISE NOTICE '  DROP TABLE alert_history_archive_v1 CASCADE;';
64
+END $$;
+50 -33
projects/autoSMART/sql/schema-v2.sql
@@ -387,45 +387,61 @@ BEGIN
387 387
 END;
388 388
 $$ LANGUAGE plpgsql;
389 389
 
390
-CREATE OR REPLACE FUNCTION enforce_data_retention(p_retain_months INTEGER DEFAULT 24)
391
-RETURNS INTEGER AS $$
390
+-- On-demand data deletion: delete all data for a specific HDD (by serial_number)
391
+-- RETENTION POLICY: On-demand only (no automatic cleanup)
392
+-- Called from frontend when user requests to remove a drive from monitoring
393
+CREATE OR REPLACE FUNCTION delete_hdd_data_by_serial(
394
+    p_serial_number VARCHAR(100),
395
+    p_keep_catalog  BOOLEAN DEFAULT true
396
+) RETURNS TABLE(
397
+    deleted_values BIGINT,
398
+    deleted_events BIGINT,
399
+    deleted_catalog INTEGER
400
+) AS $$
392 401
 DECLARE
393
-    v_cutoff_date   DATE;
394
-    v_dropped       INTEGER := 0;
395
-    v_rec           RECORD;
402
+    v_hdd_id            INTEGER;
403
+    v_deleted_values    BIGINT := 0;
404
+    v_deleted_events    BIGINT := 0;
405
+    v_deleted_catalog   INTEGER := 0;
396 406
 BEGIN
397
-    v_cutoff_date := (CURRENT_DATE - (p_retain_months || ' months')::INTERVAL)::DATE;
407
+    -- Get HDD ID for this serial
408
+    SELECT id INTO v_hdd_id FROM hdd_inventory WHERE serial_number = p_serial_number;
398 409
 
399
-    FOR v_rec IN
400
-        SELECT tablename
401
-        FROM pg_tables
402
-        WHERE tablename LIKE 'smart_param_values_%'
403
-          AND schemaname = 'public'
404
-    LOOP
405
-        DECLARE
406
-            v_year  INTEGER;
407
-            v_month INTEGER;
408
-            v_parts TEXT[];
409
-        BEGIN
410
-            v_parts := regexp_split_to_array(v_rec.tablename, '_');
411
-            IF array_length(v_parts, 1) >= 5 THEN
412
-                v_year  := v_parts[4]::INTEGER;
413
-                v_month := v_parts[5]::INTEGER;
414
-
415
-                IF make_date(v_year, v_month, 1) < v_cutoff_date THEN
416
-                    EXECUTE format('DROP TABLE IF EXISTS %I', v_rec.tablename);
417
-                    v_dropped := v_dropped + 1;
418
-                    RAISE NOTICE 'Dropped old partition: %', v_rec.tablename;
419
-                END IF;
420
-            END IF;
421
-        END;
422
-    END LOOP;
410
+    IF v_hdd_id IS NULL THEN
411
+        RAISE NOTICE 'Serial % not found in inventory', p_serial_number;
412
+        RETURN QUERY SELECT 0::BIGINT, 0::BIGINT, 0::INTEGER;
413
+        RETURN;
414
+    END IF;
415
+
416
+    -- Delete from smart_param_values (cascade via event_id FK)
417
+    DELETE FROM smart_param_values spv
418
+    WHERE event_id IN (
419
+        SELECT id FROM smart_collection_events
420
+        WHERE hdd_id = v_hdd_id
421
+    );
422
+    GET DIAGNOSTICS v_deleted_values = ROW_COUNT;
423 423
 
424
+    -- Delete from smart_collection_events
424 425
     DELETE FROM smart_collection_events
425
-    WHERE collected_at < v_cutoff_date;
426
+    WHERE hdd_id = v_hdd_id OR serial_number = p_serial_number;
427
+    GET DIAGNOSTICS v_deleted_events = ROW_COUNT;
428
+
429
+    -- Optionally delete from hdd_inventory and catalog
430
+    IF NOT p_keep_catalog THEN
431
+        DELETE FROM smart_param_catalog spc
432
+        WHERE NOT EXISTS (
433
+            SELECT 1 FROM smart_param_values
434
+            WHERE param_id = spc.id
435
+        );
436
+        GET DIAGNOSTICS v_deleted_catalog = ROW_COUNT;
437
+
438
+        DELETE FROM hdd_inventory WHERE id = v_hdd_id;
439
+    END IF;
440
+
441
+    RAISE NOTICE 'Deleted % param values, % events for serial %',
442
+        v_deleted_values, v_deleted_events, p_serial_number;
426 443
 
427
-    RETURN v_dropped;
444
+    RETURN QUERY SELECT v_deleted_values, v_deleted_events, v_deleted_catalog;
428 445
 END;
429 446
 $$ LANGUAGE plpgsql;
430 447
 
@@ -452,6 +468,6 @@ BEGIN
452 468
     RAISE NOTICE '✅ autoSMART Schema v2.0 deployed successfully!';
453 469
     RAISE NOTICE 'New tables: smart_param_catalog, smart_collection_events, smart_param_values (partitioned)';
454 470
     RAISE NOTICE 'Views: v_latest_param_values, v_drive_health_summary, v_param_trend, v_cluster_overview, v_smart_readings_compat';
455
-    RAISE NOTICE 'Functions: upsert_param_catalog, insert_collection_event, create_monthly_partition, enforce_data_retention';
471
+    RAISE NOTICE 'Functions: upsert_param_catalog, insert_collection_event, create_monthly_partition, delete_hdd_data_by_serial';
456 472
     RAISE NOTICE 'Next step: Run sql/migrate-v1-to-v2.sql to migrate existing data';
457 473
 END $$;