#!/usr/bin/env node /** * Retime messages JSON so the newest entry is today (same time-of-day), * then ensure at least N days of coverage and cap at M items by removing oldest. * * Usage: * node ./mocks/scripts/retimeMessages.mjs [filePath] [minDays=30] [maxItems=4000] * Default file: mocks/device-cgi-simulator/meldungen/messages_all.json */ import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const workspaceRoot = path.resolve(__dirname, "../.."); function pad(n) { return String(n).padStart(2, "0"); } function formatDate(d) { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad( d.getHours() )}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function parseDateTime(str) { const m = typeof str === "string" && str.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/); if (!m) return null; const [, y, mo, d, h, mi, s] = m.map(Number); return new Date(y, mo - 1, d, h, mi, s, 0); } function withTodayDateAndTimeOf(baseTime) { const today = new Date(); return new Date( today.getFullYear(), today.getMonth(), today.getDate(), baseTime.getHours(), baseTime.getMinutes(), baseTime.getSeconds(), 0 ); } async function readJson(fp) { return JSON.parse(await fs.readFile(fp, "utf-8")); } async function writeJson(fp, data) { await fs.writeFile(fp, JSON.stringify(data, null, 2) + "\n", "utf-8"); } function detectOrder(dates) { if (dates.length < 2) return "desc"; return dates[0] >= dates[1] ? "desc" : "asc"; } async function retimeMessages(filePath) { const arr = await readJson(filePath); if (!Array.isArray(arr) || arr.length === 0) return { status: "skip", reason: "not an array or empty" }; const times = arr.map((x) => parseDateTime(x?.t)); const validIdx = times.map((t, i) => (t ? i : -1)).filter((i) => i !== -1); if (!validIdx.length) return { status: "skip", reason: "no parsable t fields" }; const order = detectOrder(validIdx.slice(0, 2).map((i) => times[i])); const baseIdx = order === "desc" ? validIdx[0] : validIdx[validIdx.length - 1]; const baseOrig = times[baseIdx]; const baseNew = withTodayDateAndTimeOf(baseOrig); const newTimes = new Array(arr.length).fill(null); newTimes[baseIdx] = baseNew; if (order === "desc") { for (let k = 1; k < validIdx.length; k++) { const prev = validIdx[k - 1]; const i = validIdx[k]; const delta = Math.max(0, times[prev].getTime() - times[i].getTime()); const prevNew = newTimes[prev] || baseNew; newTimes[i] = new Date(prevNew.getTime() - delta); } } else { // asc for (let k = validIdx.length - 2; k >= 0; k--) { const next = validIdx[k + 1]; // newer const i = validIdx[k]; // older const delta = Math.max(0, times[next].getTime() - times[i].getTime()); const nextNew = newTimes[next] || baseNew; newTimes[i] = new Date(nextNew.getTime() - delta); } } const updated = arr.map((item, idx) => { const nt = newTimes[idx]; if (!nt) return item; return { ...item, t: formatDate(nt) }; }); return { status: "ok", updated, order }; } async function main() { const arg = process.argv[2]; const daysArg = process.argv[3]; const maxArg = process.argv[4]; const filePath = path.resolve( workspaceRoot, arg || "mocks/device-cgi-simulator/meldungen/messages_all.json" ); const minDays = Number.isFinite(Number(daysArg)) ? Number(daysArg) : 30; const maxItems = Number.isFinite(Number(maxArg)) ? Number(maxArg) : 4000; try { const res = await retimeMessages(filePath); if (res.status !== "ok") { console.log( `[skip] ${path.relative(workspaceRoot, filePath)}: ${res.reason}` ); return; } let { updated, order } = res; // Re-parse times after retime const times = updated.map((x) => parseDateTime(x?.t)); const validIdx = times.map((t, i) => (t ? i : -1)).filter((i) => i !== -1); if (!validIdx.length) { await writeJson(filePath, updated); console.log( `[ok] Updated ${path.relative(workspaceRoot, filePath)} (${ updated.length } items)` ); return; } // Determine newest and oldest let newestIdx; if (order === "desc") { // first valid is newest newestIdx = validIdx[0]; } else { newestIdx = validIdx[validIdx.length - 1]; } const newestTime = times[newestIdx]; let oldestIdx = validIdx[0]; let oldestTime = times[oldestIdx]; for (const i of validIdx) { if (times[i].getTime() < oldestTime.getTime()) { oldestTime = times[i]; oldestIdx = i; } } const dayMs = 24 * 60 * 60 * 1000; const targetOldestMs = newestTime.getTime() - minDays * dayMs + 1000; // Build intervals list (positive sequential deltas in array order) const deltas = []; for (let k = 1; k < validIdx.length; k++) { const a = times[validIdx[k - 1]]; const b = times[validIdx[k]]; const d = Math.abs(a.getTime() - b.getTime()); if (d > 0) deltas.push(d); } deltas.sort((x, y) => x - y); const median = deltas.length ? deltas[Math.floor(deltas.length / 2)] : 15 * 60 * 1000; // fallback 15 minutes const interval = Math.max(1000, Math.min(median, 7 * 24 * 60 * 60 * 1000)); let added = 0; if (oldestTime.getTime() > targetOldestMs) { // Need backfill towards older end const template = updated[oldestIdx]; const clones = []; let t = new Date(oldestTime.getTime()); while ( t.getTime() > targetOldestMs && updated.length + clones.length < maxItems ) { t = new Date(t.getTime() - interval); const clone = { ...template, t: formatDate(t) }; clones.push(clone); } if (clones.length) { if (order === "desc") { // Older items at the end updated = updated.concat(clones); } else { // Older items at the beginning updated = clones.concat(updated); } added = clones.length; } } // Cap to maxItems by removing oldest side let trimmed = 0; if (updated.length > maxItems) { const removeCount = updated.length - maxItems; if (order === "desc") { // remove from end (oldest) updated = updated.slice(0, updated.length - removeCount); } else { // remove from start (oldest) updated = updated.slice(removeCount); } trimmed = removeCount; } await writeJson(filePath, updated); const rel = path.relative(workspaceRoot, filePath); console.log( `[ok] Updated ${rel} (${updated.length} items, +${added} added, -${trimmed} trimmed, minDays=${minDays}, maxItems=${maxItems})` ); } catch (e) { console.error( `[error] ${path.relative(workspaceRoot, filePath)}:`, e.message ); process.exitCode = 1; } } await main();