"""Backfill SCMVAS/SCMHVAS datasheets to \\ad2\webshare\For_Web. Deploys a one-off node script that: - Queries PASS records with NULL forweb_exported_at AND (SCMVAS/SCMHVAS/VAS-M/HVAS-M model OR log_type=VASLOG_ENG) - For VASLOG_ENG: copies source .txt verbatim to For_Web\.TXT (pass-through) - For VASLOG SCMVAS/SCMHVAS: runs generateExactDatasheet and writes - Updates forweb_exported_at per batch Runs in --dry-run mode by default; pass --go to actually write. Also supports --limit N to cap. """ import argparse, base64, subprocess, sys, yaml, paramiko HOST='192.168.0.6'; USER='sysadmin' NODE_SCRIPT = r''' const fs = require('fs'); const path = require('path'); const db = require('./database/db'); const { loadAllSpecs, getSpecs } = require('./parsers/spec-reader'); const { generateExactDatasheet } = require('./templates/datasheet-exact'); const OUTPUT_DIR = '\\\\ad2\\webshare\\For_Web'; async function main() { const args = process.argv.slice(2); const dry = args.includes('--dry-run'); const limitIdx = args.indexOf('--limit'); const limit = limitIdx >= 0 ? parseInt(args[limitIdx + 1], 10) : 0; console.log('[INFO] output: ' + OUTPUT_DIR); console.log('[INFO] dry-run: ' + dry); console.log('[INFO] limit: ' + (limit || 'none')); if (!fs.existsSync(OUTPUT_DIR)) { console.error('[FAIL] output dir not reachable: ' + OUTPUT_DIR); process.exit(1); } console.log('[INFO] loading specs...'); const specMap = loadAllSpecs(); const where = [ "overall_result = 'PASS'", "forweb_exported_at IS NULL", "((model_number LIKE 'SCMVAS%' OR model_number LIKE 'SCMHVAS%' OR model_number LIKE 'VAS-M%' OR model_number LIKE 'HVAS-M%') OR log_type = 'VASLOG_ENG')" ].join(' AND '); let sql = 'SELECT * FROM test_records WHERE ' + where + ' ORDER BY test_date DESC'; if (limit > 0) sql += ' LIMIT ' + limit; const rows = await db.query(sql); console.log('[INFO] ' + rows.length + ' records to process'); let exported = 0; let skipped = 0; let errors = 0; let passthrough = 0; let rendered = 0; const batchIds = []; const BATCH = 200; async function flush() { if (batchIds.length === 0) return; if (dry) { batchIds.length = 0; return; } const now = new Date().toISOString(); await db.transaction(async (tx) => { for (const id of batchIds) { await tx.execute('UPDATE test_records SET forweb_exported_at = $1 WHERE id = $2', [now, id]); } }); batchIds.length = 0; } for (let i = 0; i < rows.length; i++) { const record = rows[i]; try { const outPath = path.join(OUTPUT_DIR, record.serial_number + '.TXT'); if (record.log_type === 'VASLOG_ENG') { if (record.source_file && fs.existsSync(record.source_file)) { if (!dry) fs.copyFileSync(record.source_file, outPath); passthrough++; } else { if (!dry) fs.writeFileSync(outPath, record.raw_data || '', 'utf8'); passthrough++; } } else { const specs = getSpecs(specMap, record.model_number); if (!specs) { skipped++; continue; } const txt = generateExactDatasheet(record, specs); if (!txt) { skipped++; continue; } if (!dry) fs.writeFileSync(outPath, txt, 'utf8'); rendered++; } batchIds.push(record.id); exported++; if (batchIds.length >= BATCH) { await flush(); process.stdout.write('[PROGRESS] ' + exported + '/' + rows.length + '\n'); } } catch (e) { errors++; console.error('[ERR] ' + record.serial_number + ': ' + e.message); } } await flush(); console.log(''); console.log('========================================'); console.log('Backfill Complete' + (dry ? ' (DRY RUN)' : '')); console.log('========================================'); console.log('Processed: ' + exported); console.log(' rendered: ' + rendered); console.log(' passthrough: ' + passthrough); console.log('Skipped: ' + skipped); console.log('Errors: ' + errors); await db.close(); } main().catch(e => { console.error('[FATAL] ' + e.message); process.exit(1); }); ''' def pwd(): r = subprocess.run(['sops','-d','D:/vault/clients/dataforth/ad2.sops.yaml'], capture_output=True, text=True, timeout=30, check=True) return yaml.safe_load(r.stdout)['credentials']['password'].replace('\\','') def ps(c, cmd, to=7200): enc = base64.b64encode(cmd.encode('utf-16-le')).decode() stdin, stdout, stderr = c.exec_command(f'powershell -NoProfile -EncodedCommand {enc}', timeout=to) return stdout.read().decode('utf-8','replace'), stderr.read().decode('utf-8','replace'), stdout.channel.recv_exit_status() def main(): ap = argparse.ArgumentParser() ap.add_argument('--go', action='store_true', help='actually write (default is dry-run)') ap.add_argument('--limit', type=int, default=0, help='cap records processed') args = ap.parse_args() dry = not args.go c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(HOST, username=USER, password=pwd(), timeout=30, banner_timeout=45, look_for_keys=False, allow_agent=False) try: sftp = c.open_sftp() remote = 'C:/Shares/testdatadb/_backfill_scmvas.js' with sftp.open(remote,'w') as fh: fh.write(NODE_SCRIPT) sftp.close() print(f'[OK] deployed {remote}', flush=True) flags = ['--dry-run'] if dry else [] if args.limit > 0: flags += ['--limit', str(args.limit)] cmd = r'cd C:\Shares\testdatadb; & node ./_backfill_scmvas.js ' + ' '.join(flags) print(f'[RUN] {cmd}', flush=True) out, err, rc = ps(c, cmd) print(f'[rc={rc}]', flush=True) print(out, flush=True) if err.strip() and 'CLIXML' not in err: print('--- STDERR ---', flush=True) print(err[:2000], flush=True) sftp = c.open_sftp() try: sftp.remove(remote) except Exception: pass sftp.close() finally: c.close() if __name__ == '__main__': main()