import path from 'node:path' import { PROJECT_ROOT } from '../../config/runtime.js' import { readJsonFile, writeJsonFile } from '../../utils/json-file-store.js' const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json') const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health' type JsonObject = Record export function getScheduledJobsFilePath() { return SCHEDULED_JOBS_FILE_PATH } export function getScheduledJobsConfig() { return loadScheduledJobsConfigFromFile() } export function saveScheduledJobsConfig(rawValue: unknown) { return writeJsonFile(SCHEDULED_JOBS_FILE_PATH, rawValue, normalizeScheduledJobsConfig) } export function getCloudtentaclesHealthJob(config: JsonObject = getScheduledJobsConfig()) { return (Array.isArray(config.jobs) ? config.jobs : []) .find((item) => String(item.id || '').trim() === CLOUDTENTACLES_HEALTH_JOB_ID) || createDefaultCloudtentaclesHealthJob() } function loadScheduledJobsConfigFromFile() { return readJsonFile( SCHEDULED_JOBS_FILE_PATH, createDefaultScheduledJobsConfig, normalizeScheduledJobsConfig, ) } function normalizeScheduledJobsConfig(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} const rawJobs = Array.isArray(source.jobs) ? source.jobs : [] const jobs = rawJobs.map((item) => normalizeScheduledJob(item)).filter(Boolean) const hasCloudtentaclesHealth = jobs.some((item) => item.id === CLOUDTENTACLES_HEALTH_JOB_ID) if (!hasCloudtentaclesHealth) { jobs.push(createDefaultCloudtentaclesHealthJob()) } return { enabled: typeof source.enabled === 'boolean' ? source.enabled : true, jobs, } } function normalizeScheduledJob(rawValue: unknown) { if (!isPlainObject(rawValue)) { return null } const type = String(rawValue.type || '').trim() if (type !== 'cloudtentacles_health') { return null } return normalizeCloudtentaclesHealthJob(rawValue) } function normalizeCloudtentaclesHealthJob(rawValue: JsonObject) { const config = isPlainObject(rawValue.config) ? rawValue.config : {} return { id: CLOUDTENTACLES_HEALTH_JOB_ID, type: 'cloudtentacles_health', enabled: rawValue.enabled === true, intervalSeconds: normalizeRangeInteger(rawValue.intervalSeconds, 300, 60, 86400), cooldownSeconds: normalizeRangeInteger(rawValue.cooldownSeconds, 1800, 60, 86400), config: { assetThreshold: normalizeRangeInteger(config.assetThreshold, 500, 0, 999999), }, } } function createDefaultScheduledJobsConfig() { return { enabled: true, jobs: [ createDefaultCloudtentaclesHealthJob(), ], } } function createDefaultCloudtentaclesHealthJob() { return { id: CLOUDTENTACLES_HEALTH_JOB_ID, type: 'cloudtentacles_health', enabled: false, intervalSeconds: 300, cooldownSeconds: 1800, config: { assetThreshold: 500, }, } } function normalizeRangeInteger(value: unknown, fallback: number, min: number, max: number) { const parsed = Number(value) if (!Number.isInteger(parsed)) { return fallback } return Math.min(max, Math.max(min, parsed)) } function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' }