Files
claudetools/projects/dataforth-dos/datasheet-pipeline/implementation-upload/database/back-populate-api-uploaded.js
Mike Swanson 733d87f20e Dataforth UI push + dedup + refactor, GuruRMM roadmap evolution, Azure signing setup
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>
2026-04-15 17:39:32 -07:00

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); });