From a87710144cbaca90cb6802e227a8a6b52e316a05 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 19 Oct 2025 21:41:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(migration):=20detect=20flat=20structure=20i?= =?UTF-8?q?n=20upsDevices=20for=20proper=20v3=E2=86=92v4=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous migration only checked for upsList field, but saveConfig() strips upsList when saving, creating a race condition. If the daemon restarted with a partially-migrated config (upsDevices with flat structure), the migration wouldn't run because it only looked for upsList. Now shouldRun() also detects: - upsDevices with flat structure (host at top level, no snmp object) And migrate() handles both: - config.upsList (pure v3) - config.upsDevices with flat structure (partially migrated) This fixes the "Cannot read properties of undefined (reading 'host')" error that occurred when configs had upsDevices but flat structure. --- deno.json | 2 +- ts/migrations/migration-v3-to-v4.ts | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/deno.json b/deno.json index f792084..b9f334d 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@serve.zone/nupst", - "version": "4.0.4", + "version": "4.0.5", "exports": "./mod.ts", "tasks": { "dev": "deno run --allow-all mod.ts", diff --git a/ts/migrations/migration-v3-to-v4.ts b/ts/migrations/migration-v3-to-v4.ts index 30b3ed5..7faab71 100644 --- a/ts/migrations/migration-v3-to-v4.ts +++ b/ts/migrations/migration-v3-to-v4.ts @@ -44,17 +44,30 @@ export class MigrationV3ToV4 extends BaseMigration { readonly toVersion = '4.0'; async shouldRun(config: any): Promise { - // V3 format has upsList instead of upsDevices - return !!config.upsList && !config.upsDevices; + // V3 format has upsList OR has upsDevices with flat structure (host at top level) + if (config.upsList && !config.upsDevices) { + return true; // Classic v3 with upsList + } + + // Check if upsDevices exists but has flat structure (v3 format) + if (config.upsDevices && config.upsDevices.length > 0) { + const firstDevice = config.upsDevices[0]; + // V3 has host at top level, v4 has it nested in snmp object + return !!firstDevice.host && !firstDevice.snmp; + } + + return false; } async migrate(config: any): Promise { logger.info(`${this.getName()}: Migrating v3 config to v4 format...`); - logger.dim(` - Renaming upsList → upsDevices`); logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`); + // Get devices from either upsList or upsDevices (for partially migrated configs) + const sourceDevices = config.upsList || config.upsDevices; + // Transform each UPS device from v3 flat structure to v4 nested structure - const transformedDevices = config.upsList.map((device: any) => { + const transformedDevices = sourceDevices.map((device: any) => { // Build SNMP config object const snmpConfig: any = { host: device.host,