const { ALERT, ERROR, WARNING, NOTICE, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename); class ReportLineChangeTime { author = `*${this.constructor.name}*`; constructor () { DEBUG(`${this.author} instantiated`); } async run (data, ctx) { if (!data || data.channel !== "event") { return; } const n = data.payload.new; const o = data.payload.old; if (!(n?.labels) && !(o?.labels)) { return; } if (!n?.labels?.includes("FGSP") && !o?.labels?.includes("FGSP") && !n?.labels?.includes("LGSP") && !o?.labels?.includes("LGSP")) { return; } try { DEBUG("Running"); const cur = data; const projectId = cur?.payload?.pid; const forward = (cur?.payload?.old?.labels?.includes("LGSP") || cur?.payload?.new?.labels?.includes("LGSP")); DEBUG("%j", cur); if (!projectId) { throw {message: "No projectID found in event", cur}; return; } async function getLineChangeTime (data, forward = false) { if (forward) { const ospEvents = await ctx.db.event.list(projectId, {label: "FGSP"}); // DEBUG("ospEvents", ospEvents); const osp = ospEvents.filter(i => i.tstamp > data.tstamp).pop(); DEBUG("fsp", osp); // DEBUG("data", data); if (osp) { DEBUG("lineChangeTime", osp.tstamp - data.tstamp); return { lineChangeTime: osp.tstamp - data.tstamp, osp }; } } else { const ospEvents = await ctx.db.event.list(projectId, {label: "LGSP"}); // DEBUG("ospEvents", ospEvents); const osp = ospEvents.filter(i => i.tstamp < data.tstamp).shift(); DEBUG("lsp", osp); // DEBUG("data", data); if (osp) { DEBUG("lineChangeTime", data.tstamp - osp.tstamp); return { lineChangeTime: data.tstamp - osp.tstamp, osp }; } } } function parseInterval (dt) { const daySeconds = (dt/1000) % 86400; const d = Math.floor((dt/1000) / 86400); const dateObject = new Date(null); dateObject.setSeconds(daySeconds); const [ h, m, s ] = dateObject.toISOString().slice(11, 19).split(":").map(Number); return {d, h, m, s}; } function formatInterval (i) { let str = ""; for (let [k, v] of Object.entries(i)) { if (v) { str += " " + v + " " + k; } } return str.trim(); } const deleteStaleEvents = async (seq) => { if (seq) { DEBUG("Will delete lct events related to sequence(s)", seq); const jpq = `$."${this.author}"`; const opts = {jpq}; if (Array.isArray(seq)) { opts.sequences = ctx.unique(seq).filter(i => !!i); } else { opts.sequence = seq; } const staleEvents = await ctx.db.event.list(projectId, opts); DEBUG(staleEvents.length ?? 0, "events to delete"); for (let staleEvent of staleEvents) { DEBUG(`Deleting event id ${staleEvent.id} (seq = ${staleEvent.sequence}, point = ${staleEvent.point})`); if (ctx.dryRun) { DEBUG(`await ctx.db.event.del(${projectId}, ${staleEvent.id});`); } else { await ctx.db.event.del(projectId, staleEvent.id); } } } } const createLineChangeTimeEvents = async (lineChangeTime, data, osp) => { const events = []; const cfg = ctx?.projects?.configuration?.[projectId] ?? {}; const nlcd = cfg?.production?.nominalLineChangeDuration * 60*1000; // m → ms DEBUG("nlcd", nlcd); if (nlcd && lineChangeTime > nlcd) { const excess = lineChangeTime-nlcd; const excessString = formatInterval(parseInterval(excess)); DEBUG("excess", excess, excessString); // ref: The later of the two events const ref = forward ? osp : data; const payload = { // tstamp: new Date(ref.tstamp-1), sequence: ref.sequence, point: ref.point, remarks: `_Nominal line change duration exceeded by ${excessString}_`, labels: [ "Nav", "Prod" ], meta: { auto: true, author: this.author, [this.author]: { parents: [ data.id, osp.id ], type: "excess", value: excess } } } events.push(payload); DEBUG("Created line change duration exceeded event", projectId, payload); } const lctString = formatInterval(parseInterval(lineChangeTime)); // ref: The later of the two events const ref = forward ? osp : data; const payload = { // tstamp: new Date(ref.tstamp-1), sequence: ref.sequence, point: ref.point, remarks: `Line change time: ${lctString}`, labels: [ "Nav", "Prod" ], meta: { auto: true, author: this.author, [this.author]: { parents: [ data.id, osp.id ], type: "lineChangeTime", value: lineChangeTime } } }; events.push(payload); DEBUG("Created line change duration event", projectId, payload); return events; } const maybePostEvent = async (projectId, payload) => { DEBUG("Posting event", projectId, payload); if (ctx.dryRun) { DEBUG(`await ctx.db.event.post(${projectId}, ${payload});`); } else { await ctx.db.event.post(projectId, payload); } } await deleteStaleEvents([cur.old?.sequence, cur.new?.sequence]); if (cur?.payload?.operation == "INSERT") { // NOTE: UPDATE on the event_log view translates to one UPDATE plus one INSERT // on event_log_full, so we don't need to worry about UPDATE here. const data = n; DEBUG("INSERT seen: will add lct events related to ", data.id); if (ctx.withinValidity(data.validity)) { DEBUG("Event within validity period", data.validity, new Date()); data.tstamp = new Date(data.tstamp); const { lineChangeTime, osp } = await getLineChangeTime(data, forward); if (lineChangeTime) { const events = await createLineChangeTimeEvents(lineChangeTime, data, osp); if (events?.length) { DEBUG("Deleting other events for sequence", events[0].sequence); await deleteStaleEvents(events[0].sequence); } for (let payload of events) { await maybePostEvent(projectId, payload); } } } else { DEBUG("Event outside of validity range", data.validity, "lct events not inserted"); } } } catch (err) { ERROR(`${this.author} error`, err); throw err; } } } module.exports = ReportLineChangeTime;