Init
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
// Convert a Let it Snow chrome.storage.local dump into FORGE's seed archives +
|
||||
// a config bundle. Run: node scripts/dump-to-archives.mjs [path/to/storage-dump.json]
|
||||
// then reload the DB: npm run reseed
|
||||
//
|
||||
// Outputs (server/data/):
|
||||
// active_archive.json — { tickets:[sn_tickets], jira:{num:info} } (operational, live board)
|
||||
// closed_archive.json — { tickets:{num:merged} } (analytics master)
|
||||
// config.json — thresholds, FX, size thresholds, SLA norms, palettes
|
||||
//
|
||||
// The closed archive MERGES two dump sources by RITM number:
|
||||
// - analytics_data.ticketsMeta (954) → analytics fields: openedBy, openedDate,
|
||||
// closedDate, toDoAt, inUatAt, jiraKey, year, businessUnit, SLA minutes, cost
|
||||
// - analytics_meta_cache (965) → operational fields: lastActivityAt/By,
|
||||
// stateChangedAt/By, requestedFor, openedAt (datetime), _updated
|
||||
// ticketsMeta wins for analytics; meta_cache fills operational gaps; union of both.
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const dumpPath = process.argv[2] || path.join(root, 'storage-dump.json');
|
||||
const dataDir = path.join(root, 'server', 'data');
|
||||
|
||||
const dump = JSON.parse(readFileSync(dumpPath, 'utf-8'));
|
||||
const cs = dump.chromeStorageLocal ?? {};
|
||||
const wl = dump.windowLocalStorage ?? {};
|
||||
|
||||
const sn = cs.sn_tickets ?? [];
|
||||
const metaCache = cs.analytics_meta_cache ?? {};
|
||||
const ticketsMeta = cs.analytics_data?.ticketsMeta ?? [];
|
||||
const jiraRaw = cs.jira_status_map ?? {};
|
||||
|
||||
// --- active (unchanged) ----------------------------------------------------
|
||||
const jira = {};
|
||||
for (const [num, j] of Object.entries(jiraRaw)) {
|
||||
if (j && typeof j === 'object') {
|
||||
jira[num] = { status: j.status ?? null, statusChangedAt: j.statusChangedAt ?? null,
|
||||
key: j.key ?? null, url: j.url ?? null, assignee: j.assignee ?? null };
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich jira with per-status durations (ms) from the board-state snapshot, keyed
|
||||
// to RITM via customfield_26001. Powers chart #17 (Jira status durations). The
|
||||
// board's column order is the workflow order used to render the chart.
|
||||
const boardState = cs.jira_board_state?.boardState ?? {};
|
||||
const jiraColumns = (boardState.columns ?? []).map(c => c?.name).filter(Boolean);
|
||||
for (const issues of Object.values(boardState.colIssues ?? {})) {
|
||||
for (const iss of (issues ?? [])) {
|
||||
const ritm = iss?.fields?.customfield_26001;
|
||||
if (!ritm || typeof ritm !== 'string') continue;
|
||||
const sd = iss._statusDurations;
|
||||
if (!sd || typeof sd !== 'object') continue;
|
||||
const e = jira[ritm] ?? (jira[ritm] = { status: null, statusChangedAt: null, key: null, url: null, assignee: null });
|
||||
e.statusDurations = sd; // { statusName: milliseconds }
|
||||
if (!e.key) e.key = iss.key ?? null;
|
||||
if (!e.status && iss.fields?.status?.name) e.status = iss.fields.status.name;
|
||||
}
|
||||
}
|
||||
|
||||
// --- merged closed ---------------------------------------------------------
|
||||
const byNumTM = {};
|
||||
for (const t of ticketsMeta) if (t?.number) byNumTM[t.number] = t;
|
||||
|
||||
const closed = {};
|
||||
const nums = new Set([...Object.keys(metaCache), ...Object.keys(byNumTM)]);
|
||||
for (const num of nums) {
|
||||
const mc = metaCache[num] ?? {};
|
||||
const tm = byNumTM[num] ?? {};
|
||||
closed[num] = {
|
||||
// analytics (ticketsMeta wins, fall back to meta_cache)
|
||||
state: tm.state ?? mc.state ?? '',
|
||||
shortDesc: tm.shortDesc ?? mc.shortDesc ?? '',
|
||||
assignedTo: tm.assignedTo ?? mc.assignedTo ?? null,
|
||||
brand: tm.brand ?? mc.brand ?? null,
|
||||
market: tm.market ?? mc.market ?? null,
|
||||
businessUnit: tm.businessUnit ?? mc.businessUnit ?? null,
|
||||
finalCost: tm.finalCost ?? mc.finalCost ?? null,
|
||||
currencyCode: tm.currencyCode ?? mc.currencyCode ?? null,
|
||||
ttfrMinutes: tm.ttfrMinutes ?? mc.ttfrMinutes ?? null,
|
||||
clientRespMinutes: tm.clientRespMinutes ?? mc.clientRespMinutes ?? null,
|
||||
firstReplyAt: tm.firstReplyAt ?? mc.firstReplyAt ?? null,
|
||||
firstAssignedDate: tm.firstAssignedDate ?? mc.firstAssignedDate ?? null,
|
||||
fulfillmentDate: tm.fulfillmentDate ?? mc.fulfillmentDate ?? null,
|
||||
openedBy: tm.openedBy ?? null,
|
||||
openedDate: tm.openedDate ?? null,
|
||||
closedDate: tm.closedDate ?? null,
|
||||
toDoAt: tm.toDoAt ?? null,
|
||||
inUatAt: tm.inUatAt ?? null,
|
||||
jiraKey: tm.jiraKey ?? null,
|
||||
year: tm.year ?? null,
|
||||
// operational (meta_cache)
|
||||
requestedFor: mc.requestedFor ?? null,
|
||||
openedAt: mc.openedAt ?? tm.openedDate ?? null,
|
||||
stateChangedAt: mc.stateChangedAt ?? null,
|
||||
stateChangedBy: mc.stateChangedBy ?? null,
|
||||
lastActivityAt: mc.lastActivityAt ?? null,
|
||||
lastActivityBy: mc.lastActivityBy ?? null,
|
||||
_updated: mc._updated ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// --- config bundle ---------------------------------------------------------
|
||||
const parseWl = (k, fallback) => { try { return JSON.parse(wl[k]); } catch { return fallback; } };
|
||||
const config = {
|
||||
insightsThresholds: cs.insights_thresholds ?? {},
|
||||
fxRates: cs.fx_rates_cache?.rates ?? { GBP: 1, EUR: 1.2, MXN: 20 },
|
||||
sizeThresholds: parseWl('otd_cost_thresh', { XS: 120, S: 300, M: 600, L: 1200, XL: 3000, XXL: 9000 }),
|
||||
norms: {
|
||||
otd: parseWl('otd_day_norms', { XS: 7, S: 14, M: 30, L: 90, XL: 90, XXL: 90 }),
|
||||
avgdays: parseWl('avgdays_day_norms', { XS: 7, S: 14, M: 30, L: 90, XL: 90, XXL: 90 }),
|
||||
asla: parseWl('asla_day_norms', { XS: 1, S: 1, M: 1, L: 1, XL: 1, XXL: 1 }),
|
||||
psla: parseWl('psla_day_norms', { XS: 3, S: 6, M: 9, L: 15, XL: 30, XXL: 30 }),
|
||||
ttfr: parseWl('lisr_ttfr_norms', { XS: 24, S: 24, M: 24, L: 24, XL: 24, XXL: 24 }),
|
||||
cresp: parseWl('lisr_cresp_norms', { XS: 24, S: 24, M: 24, L: 24, XL: 24, XXL: 24 }),
|
||||
},
|
||||
brandColors: cs.brand_colors ?? {},
|
||||
snowColors: cs.snow_colors ?? {},
|
||||
statesOrder: cs.sn_states_order ?? [],
|
||||
displayCurrency: cs.display_currency ?? 'GBP',
|
||||
colleagues: String(cs.colleague_names ?? '').split(/[\n,]/).map(s => s.trim()).filter(Boolean),
|
||||
latamAssignees: String(cs.latam_assignees ?? '').split(/[\n,]/).map(s => s.trim()).filter(Boolean),
|
||||
jiraColumns,
|
||||
};
|
||||
|
||||
// --- finance (per-ticket cost / PO / invoiced, from the SharePoint xlsx) ------
|
||||
// Column names are data-driven; map the known ones case-insensitively.
|
||||
const financeRaw = cs.finance_xlsx_data?.rows ?? {};
|
||||
const cell = (row, ...names) => {
|
||||
for (const k of Object.keys(row)) if (names.some(n => k.toLowerCase() === n.toLowerCase())) return row[k];
|
||||
return null;
|
||||
};
|
||||
const finance = { rows: {} };
|
||||
for (const [num, row] of Object.entries(financeRaw)) {
|
||||
if (!row || typeof row !== 'object') continue;
|
||||
const po = cell(row, 'PO');
|
||||
finance.rows[num] = {
|
||||
cost: cell(row, 'Cost'),
|
||||
currency: cell(row, 'Currency'),
|
||||
po: po == null ? '' : String(po).trim(), // '' = in finance but no PO; key for waiting-PO
|
||||
invoiced: cell(row, 'Invoiced'),
|
||||
milestone: cell(row, 'Milestone'),
|
||||
state: cell(row, 'State'),
|
||||
pm: cell(row, 'PM'),
|
||||
};
|
||||
}
|
||||
|
||||
const generatedAt = new Date().toISOString();
|
||||
writeFileSync(path.join(dataDir, 'finance.json'), JSON.stringify({ schema: 1, generatedAt, count: Object.keys(finance.rows).length, rows: finance.rows }));
|
||||
writeFileSync(path.join(dataDir, 'active_archive.json'),
|
||||
JSON.stringify({ schema: 2, generatedAt, count: sn.length, tickets: sn, jira }));
|
||||
writeFileSync(path.join(dataDir, 'closed_archive.json'),
|
||||
JSON.stringify({ schema: 2, generatedAt, count: Object.keys(closed).length, tickets: closed }));
|
||||
writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify(config, null, 2));
|
||||
|
||||
const overlap = Object.keys(closed).filter(n => sn.some(t => t.number === n)).length;
|
||||
console.log(`active=${sn.length} closed(merged meta_cache∪ticketsMeta)=${Object.keys(closed).length} jira=${Object.keys(jira).length} overlap=${overlap} (active wins)`);
|
||||
console.log(`ticketsMeta analytics rows folded in: ${Object.keys(byNumTM).length}`);
|
||||
console.log('Wrote server/data/{active,closed}_archive.json + config.json — now run: npm run reseed');
|
||||
Reference in New Issue
Block a user