From c45bcfaa2fdd323741e45ccdb3111078c2ff1675 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 7 May 2026 13:38:10 -0400 Subject: [PATCH] revert(maintenance): keep one-time TTL cleanup, narrow PR to validator-only Per review feedback: auto-deleting completed one-time maintenance windows is intentional cleanup, not a bug. The actual bug is past-dated end values being accepted at creation time, which the validator refines now reject up front. Reverts: - Restore expiry field + TTL index on MaintenanceWindow schema - Restore repository's auto-set of expiry = end for repeat=0 - Drop migration 0007 (no longer needed) - Restore expiry properties in openapi.json - Restore expiry in create/edit validators (still optional) Kept from this PR: - .strict() on create/edit body schemas - end > start refine on create/edit - end > now refine on create when repeat=0 (the real fix) - DurationUnits const tuple + reuse (rules 5/8/10) - Client no longer sends redundant expiry (server still derives it) --- server/openapi.json | 10 ++++ .../0007_dropMaintenanceWindowExpiryIndex.ts | 58 ------------------- server/src/db/migration/index.ts | 2 - server/src/db/models/MaintenanceWindow.ts | 5 ++ .../MongoMaintenanceWindowsRepository.ts | 5 ++ .../validation/maintenanceWindowValidation.ts | 2 + 6 files changed, 22 insertions(+), 60 deletions(-) delete mode 100644 server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts diff --git a/server/openapi.json b/server/openapi.json index 058e934b7..f62062332 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -4657,6 +4657,11 @@ "repeat": { "type": "number", "minimum": 0 + }, + "expiry": { + "type": "string", + "nullable": true, + "format": "date-time" } }, "required": ["monitors", "name", "duration", "durationUnit", "start", "end", "repeat"] @@ -5185,6 +5190,11 @@ "nullable": true, "format": "date-time" }, + "expiry": { + "type": "string", + "nullable": true, + "format": "date-time" + }, "monitors": { "type": "array", "items": { diff --git a/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts b/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts deleted file mode 100644 index 38e55c0d7..000000000 --- a/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts +++ /dev/null @@ -1,58 +0,0 @@ -import mongoose from "mongoose"; -import { logger } from "@/utils/logger.js"; - -/** - * Drops the TTL index on MaintenanceWindow.expiry and unsets the field on existing rows. - * - * Why: a previous version copied `end` into `expiry` for one-time windows. Combined with the - * TTL index on `expiry`, this caused Mongo to silently delete rows whose `end` was in the past - * at creation time (a common symptom for automation tools sending non-UTC timestamps). The - * fix stops writing `expiry`; this migration removes the live exposure on existing installs. - */ -export async function dropMaintenanceWindowExpiryIndex(): Promise { - const SERVICE_NAME = "Migration:DropMaintenanceWindowExpiryIndex"; - - try { - logger.info({ - service: SERVICE_NAME, - message: "Starting drop of MaintenanceWindow.expiry TTL index", - }); - - const db = mongoose.connection.db; - if (!db) { - throw new Error("Mongo connection not initialized"); - } - const collection = db.collection("maintenancewindows"); - - const indexes = await collection.indexes(); - const expiryIndex = indexes.find( - (idx) => idx.expireAfterSeconds !== undefined && idx.key && Object.keys(idx.key).length === 1 && idx.key.expiry === 1 - ); - - if (expiryIndex?.name) { - await collection.dropIndex(expiryIndex.name); - logger.info({ - service: SERVICE_NAME, - message: `Dropped TTL index ${expiryIndex.name} on MaintenanceWindow.expiry`, - }); - } else { - logger.info({ - service: SERVICE_NAME, - message: "No TTL index found on MaintenanceWindow.expiry", - }); - } - - const result = await collection.updateMany({ expiry: { $exists: true } }, { $unset: { expiry: "" } }); - logger.info({ - service: SERVICE_NAME, - message: `Unset expiry on ${result.modifiedCount} maintenance windows`, - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error({ - service: SERVICE_NAME, - message: `Error dropping expiry index: ${errorMessage}`, - }); - throw error; - } -} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index fcf0cf1fb..5b74a96fc 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -5,7 +5,6 @@ import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds. import MigrationModel from "../models/Migration.js"; import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js"; import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplicateMonitorStatsForUniqueIndex.js"; -import { dropMaintenanceWindowExpiryIndex } from "./0007_dropMaintenanceWindowExpiryIndex.js"; import type { ILogger } from "@/utils/logger.js"; type MigrationEntry = { @@ -20,7 +19,6 @@ const migrations: MigrationEntry[] = [ { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray }, { name: "0006_cleanupDuplicateMonitorStatsForUniqueIndex", execute: cleanupDuplicateMonitorStatsForUniqueIndex }, - { name: "0007_dropMaintenanceWindowExpiryIndex", execute: dropMaintenanceWindowExpiryIndex }, ]; const runMigrations = async (logger?: ILogger) => { diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts index 1fc9b5f4a..703c144c8 100644 --- a/server/src/db/models/MaintenanceWindow.ts +++ b/server/src/db/models/MaintenanceWindow.ts @@ -6,6 +6,7 @@ type MaintenanceWindowDocumentBase = Omit( end: { type: Date, }, + expiry: { + type: Date, + index: { expires: "0s" }, + }, }, { timestamps: true, diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index 0cdda805e..ad96328c5 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -45,6 +45,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository create = async (data: Partial): Promise => { const maintenanceWindow = new MaintenanceWindowModel(data); + + // If the maintenance window is a one time window, set the expiry to the end date + if (maintenanceWindow.repeat === 0) { + maintenanceWindow.expiry = maintenanceWindow.end; + } const result = await maintenanceWindow.save(); return this.toEntity(result); }; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index bdd11df5f..c53e39b20 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -18,6 +18,7 @@ export const createMaintenanceWindowBodyValidation = z start: dateToString, end: dateToString, repeat: z.number().min(0, "Repeat must be a non-negative number"), + expiry: dateToString.optional(), }) .strict() .superRefine((data, ctx) => { @@ -70,6 +71,7 @@ export const editMaintenanceByIdWindowBodyValidation = z repeat: z.number().optional(), start: dateToString.optional(), end: dateToString.optional(), + expiry: dateToString.optional(), monitors: z.array(z.string()).optional(), duration: z.number().optional(), durationUnit: z.enum(DurationUnits).optional(),