export function maintenanceEnd(item) { return Math.min(item.endsAt, item.cancelledAt ?? Infinity, item.completedAt ?? Infinity); } export function maintenancePhase(item, now = Date.now()) { if (item.cancelledAt !== null && item.cancelledAt <= now) return 'cancelled'; if (item.completedAt !== null && item.completedAt <= now) return 'completed'; if (now >= item.endsAt) return 'ended'; return now < item.startsAt ? 'scheduled' : 'active'; } export function affectsService(item, serviceId, time = Date.now()) { return (!item.serviceIds.length || item.serviceIds.includes(serviceId)) && item.startsAt <= time && time < maintenanceEnd(item); } export function validateMaintenance(input, serviceIds) { for (const key of ['titleRu', 'titleEn', 'messageRu', 'messageEn']) { if (typeof input[key] !== 'string' || !input[key].trim() || input[key].length > 2000) throw new Error(`Provide ${key} (1–2000 characters)`); } if (!Number.isSafeInteger(input.startsAt) || !Number.isSafeInteger(input.endsAt) || input.endsAt <= input.startsAt) throw new Error('End must be after start'); if (!Array.isArray(input.serviceIds) || input.serviceIds.some(id => !serviceIds.includes(id)) || new Set(input.serviceIds).size !== input.serviceIds.length) throw new Error('Unknown or duplicate service'); return input; } export function parseInstant(value) { const valid = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value)); if (!valid || new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) !== value.slice(0, 10)) throw new Error('Use a valid ISO timestamp with an explicit timezone, for example 2026-10-13T02:00:00+03:00'); return Date.parse(value); } export function combinedStatus(availability, scenario, maintenance = false) { if (maintenance) return 'maintenance'; if (availability === 'down' || availability === 'unknown') return availability; if (scenario && ['down', 'degraded'].includes(scenario.status)) return 'degraded'; if (scenario?.status === 'unknown') return 'unknown'; return availability; }