Dataforth (projects/dataforth-dos/): - UI feature: row coloring + PUSH/RE-PUSH buttons + Website Status filter - Database dedup to one row per SN (2.89M -> 469K rows, UNIQUE constraint added) - Import logic handles FAIL -> PASS retest transition - Refactored upload-to-api.js to render datasheets in-memory (dropped For_Web filesystem dep) - Bulk pushed 170,984 records to Hoffman API - Statistical sanity check: 100/100 stamped SNs verified on Hoffman GuruRMM (projects/msp-tools/guru-rmm/): - ROADMAP.md: added Terminology (5-tier hierarchy), Tunnel Channels Phase 2, Logging/Audit/Observability, Multi-tenancy, Modular Architecture, Protocol Versioning, Certificates sections + Decisions Log - CONTEXT.md: hierarchy table, new anti-patterns (bootstrap sacred, no cross-module imports), revised next-steps priorities Session logs for both projects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
3.1 KiB
JavaScript
75 lines
3.1 KiB
JavaScript
/**
|
|
* One-time back-population of api_uploaded_at from server_inventory.txt.
|
|
*
|
|
* Reads SN list, UPDATEs test_records.api_uploaded_at = NOW() in batches
|
|
* for records whose serial_number appears in the inventory.
|
|
*
|
|
* Usage: node back-populate-api-uploaded.js [--inventory path] [--batch 1000] [--dry-run]
|
|
*/
|
|
const fs = require('fs');
|
|
const db = require('./db');
|
|
|
|
const args = process.argv.slice(2);
|
|
const arg = (n, d) => { const i = args.indexOf(n); return i >= 0 ? args[i+1] : d; };
|
|
const flag = n => args.includes(n);
|
|
|
|
const INVENTORY = arg('--inventory', 'C:\\ProgramData\\dataforth-uploader\\server_inventory.txt');
|
|
const BATCH = parseInt(arg('--batch', '1000'), 10);
|
|
const DRY = flag('--dry-run');
|
|
|
|
async function main() {
|
|
if (!fs.existsSync(INVENTORY)) {
|
|
console.error(`[FAIL] inventory not found: ${INVENTORY}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const data = fs.readFileSync(INVENTORY, 'utf8');
|
|
const sns = data.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
console.log(`[INFO] inventory: ${sns.length} serial numbers`);
|
|
console.log(`[INFO] batch size: ${BATCH} dry-run: ${DRY}`);
|
|
|
|
const t0 = Date.now();
|
|
let totalMatched = 0;
|
|
|
|
for (let i = 0; i < sns.length; i += BATCH) {
|
|
const chunk = sns.slice(i, i + BATCH);
|
|
const placeholders = chunk.map((_, j) => `$${j + 1}`).join(',');
|
|
if (DRY) {
|
|
const row = await db.queryOne(
|
|
`SELECT COUNT(*) as c FROM test_records WHERE serial_number IN (${placeholders}) AND api_uploaded_at IS NULL`,
|
|
chunk,
|
|
);
|
|
totalMatched += parseInt(row.c, 10) || 0;
|
|
} else {
|
|
const result = await db.execute(
|
|
`UPDATE test_records SET api_uploaded_at = NOW() WHERE serial_number IN (${placeholders}) AND api_uploaded_at IS NULL`,
|
|
chunk,
|
|
);
|
|
totalMatched += result.rowCount || 0;
|
|
}
|
|
if ((i / BATCH) % 20 === 0) {
|
|
const rate = (i + chunk.length) / Math.max(1, (Date.now() - t0) / 1000);
|
|
const eta = Math.round((sns.length - i - chunk.length) / Math.max(1, rate));
|
|
console.log(` progress ${i + chunk.length}/${sns.length} matched-so-far=${totalMatched} rate=${rate.toFixed(0)}/s eta=${eta}s`);
|
|
}
|
|
}
|
|
|
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
console.log(`\n[DONE] ${elapsed}s`);
|
|
console.log(` inventory size: ${sns.length}`);
|
|
console.log(` ${DRY ? 'would update' : 'updated'}: ${totalMatched}`);
|
|
|
|
// Sanity: how many records have api_uploaded_at set vs null?
|
|
const tot = await db.queryOne(`SELECT COUNT(*) as c FROM test_records`);
|
|
const set = await db.queryOne(`SELECT COUNT(*) as c FROM test_records WHERE api_uploaded_at IS NOT NULL`);
|
|
const nul = await db.queryOne(`SELECT COUNT(*) as c FROM test_records WHERE api_uploaded_at IS NULL`);
|
|
console.log(`\n[DB STATE]`);
|
|
console.log(` total records: ${tot.c}`);
|
|
console.log(` api_uploaded_at SET: ${set.c}`);
|
|
console.log(` api_uploaded_at NULL: ${nul.c}`);
|
|
|
|
await db.close();
|
|
}
|
|
|
|
main().catch(e => { console.error('[FATAL]', e); process.exit(1); });
|