232 lines
7.0 KiB
JavaScript
232 lines
7.0 KiB
JavaScript
#!/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();
|