← back to Tk11438 Postgres Migration
verification/fleet-classification/gracie-preflight.cjs
33 lines
// Read-only. Never import the application or invoke request routes/DDL/email.
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),http=require('node:http');
const assert=require('node:assert/strict');
const deps='/Users/macstudio3/.npm-global/lib/node_modules/pm2/node_modules/';
const axon=require(deps+'pm2-axon'),rpc=require(deps+'pm2-axon-rpc');
const root='/Users/macstudio3/Projects/gracie-internal';
const hash=x=>crypto.createHash('sha256').update(x).digest('hex');
async function monitor(){return new Promise((resolve,reject)=>{const req=axon.socket('req'),c=new rpc.Client(req);const t=setTimeout(()=>{req.close();reject(Error('PM2 timeout'));},30000);req.connect('/Users/macstudio3/.pm2/rpc.sock');c.call('getMonitorData',{},(e,list)=>{clearTimeout(t);req.close();e?reject(e):resolve(list);});});}
async function get(port,url,auth){return new Promise((resolve,reject)=>{const r=http.get({hostname:'127.0.0.1',port,path:url,headers:auth?{Authorization:'Basic '+Buffer.from(auth).toString('base64')}:{},timeout:5000},res=>{let b='';res.on('data',c=>b+=c);res.on('end',()=>resolve({status:res.statusCode,bytes:Buffer.byteLength(b),body_sha256:hash(b),...(url==='/api/products'&&res.statusCode===200?{product_count:JSON.parse(b).count}:{})}));});r.on('error',reject);r.on('timeout',()=>r.destroy(Error('HTTP timeout')));});}
function target(v){if(!v)return null;const u=new URL(v);return{host:u.hostname,database:u.pathname.slice(1),port:u.port||'5432',socketHost:u.searchParams.get('host'),explicitRole:!!u.username};}
(async()=>{
const p=(await monitor()).find(p=>p.name==='gracie-internal');assert(p);const e=p.pm2_env;
assert.equal(e.pm_cwd,root);assert.equal(e.pm_exec_path,root+'/server.js');assert.equal(e.status,'online');
const source=fs.readFileSync(root+'/server.js','utf8');
const auth=e.BASIC_AUTH||source.match(/process\.env\.BASIC_AUTH \|\| '([^']+)'/)[1];
const dump=JSON.parse(fs.readFileSync('/Users/macstudio3/.pm2/dump.pm2'));
const saved=dump.filter(r=>r.name==='gracie-internal');assert.equal(saved.length,1);
const fields=[];for(const [label,obj] of [['effective',e],['effective.env',e.env||{}],['saved',saved[0]],['saved.env',saved[0].env||{}]])fields.push({label,PG:target(obj.PG),PGHOST:obj.PGHOST||null,DATA_SOURCE:obj.DATA_SOURCE||null});
const old="const PG = process.env.PG || 'postgresql://localhost/dw_unified';";
const replacement="const PG = process.env.PG || (process.platform === 'darwin' ? 'postgresql://localhost/dw_unified?host=/tmp' : 'postgresql://localhost/dw_unified');";
assert.equal(source.split(old).length-1,1);const after=source.replace(old,replacement);
const line=source.split('\n').findIndex(l=>l===old)+1;
fs.writeFileSync(path.join(__dirname,'patches/gracie-server.patch'),`--- a/server.js\n+++ b/server.js\n@@ -${line} +${line} @@\n-${old}\n+${replacement}\n`);
const checks=[];for(const route of ['/healthz','/api/products'])for(const [label,a] of [['missing',null],['invalid','invalid:invalid'],['valid',auth]]){const result=await get(e.PORT||10073,route,a);assert.equal(result.status,route==='/healthz'||label==='valid'?200:401);checks.push({route,auth:label,...result});}
const result={status:'ready for scoped approval; refresh baseline before execution',name:p.name,pid:p.pid,restart_count:e.restart_time,fields,
source:{path:root+'/server.js',before_sha256:hash(source),after_sha256:hash(after),patch:'patches/gracie-server.patch'},
bundle_present:fs.existsSync(root+'/data/gracie.jsonl'),http_checks:checks,
email_sends:0,application_imports:0,requests_route_invoked:false,
startup:'mountVendorRequests immediately runs CREATE TABLE/INDEX IF NOT EXISTS; restart is canonical DDL gated. Catalog may use JSONL; vendor requests still use PostgreSQL.',
proposed_scope:['two source files only: server.js and lib/vendor-requests.js','PG URI socket query in exact Gracie effective/saved PM2 fields; preserve all other fields','one Gracie restart; existing startup DDL only; no POST /api/request and no email sends','verify auth boundaries, read-only catalog/request GET after startup, socket pool identity/no process TCP5432, saved/effective agreement','private file and scoped PM2 backups before apply; restore exact originals and restart only Gracie on regression']};
fs.writeFileSync(path.join(__dirname,'gracie-preflight.json'),JSON.stringify(result,null,2)+'\n');console.log(JSON.stringify({status:result.status,pid:p.pid,http_checks:checks.length,fields:fields.length,bundle_present:result.bundle_present}));
})().catch(e=>{console.error(e.code||e.name,'Gracie preflight failed');process.exitCode=1;});