← back to Wallco Ai
scripts/apply-cull-decisions.js
114 lines
#!/usr/bin/env node
// apply-cull-decisions.js — read fuzzy-cull-decisions.json (from the viewer)
// and execute the user's decisions against Mac2 and prod.
//
// REJECT decision per design_id:
// 1. Set is_published=FALSE in spoon_all_designs (Mac2).
// 2. Move the original from data/generated/ → data/generated-rejected/
// (archive-not-delete; the watermarked thumb stays until prod-removed).
// 3. Move the watermarked from data/generated-web/ → data/generated-web-rejected/.
// 4. Remove the entry from designs.json on Mac2.
// 5. ssh prod: rm /root/public-projects/wallco-ai/data/generated/<file>.png
// 6. ssh prod: remove from prod designs.json + pm2 restart wallco-ai.
// 7. Append to data/cull-audit.jsonl.
//
// KEEP decision per design_id: no-op (these are already in the catalog).
//
// Usage: node apply-cull-decisions.js path/to/fuzzy-cull-decisions.json [--dry-run]
"use strict";
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const ROOT = path.join(__dirname, "..");
const DESIGNS_JSON = path.join(ROOT, "data", "designs.json");
const GEN_DIR = path.join(ROOT, "data", "generated");
const WEB_DIR = path.join(ROOT, "data", "generated-web");
const REJ_GEN = path.join(ROOT, "data", "generated-rejected");
const REJ_WEB = path.join(ROOT, "data", "generated-web-rejected");
const AUDIT = path.join(ROOT, "data", "cull-audit.jsonl");
const args = process.argv.slice(2);
const decisionsPath = args.find(a => !a.startsWith("--"));
const DRY = args.includes("--dry-run");
if (!decisionsPath) {
console.error("Usage: node apply-cull-decisions.js <decisions.json> [--dry-run]");
process.exit(2);
}
const dec = JSON.parse(fs.readFileSync(decisionsPath, "utf8")).decisions || {};
const rejectIds = Object.entries(dec).filter(([, v]) => v === "reject").map(([k]) => parseInt(k, 10));
const keepIds = Object.entries(dec).filter(([, v]) => v === "keep").map(([k]) => parseInt(k, 10));
console.log(`Decisions: reject=${rejectIds.length} keep=${keepIds.length} ${DRY ? "(DRY RUN)" : ""}`);
if (rejectIds.length === 0) { console.log("Nothing to reject. Exiting."); process.exit(0); }
[REJ_GEN, REJ_WEB].forEach(d => { if (!DRY) try { fs.mkdirSync(d, { recursive: true }); } catch {} });
function psql(sql) {
return execSync(`psql dw_unified -At -q`, { input: sql, encoding: "utf8" }).trim();
}
// 1. fetch local_path basenames for the reject IDs
const inList = rejectIds.join(",");
const fileMap = new Map();
const rows = psql(`SELECT id, regexp_replace(local_path,'^.*/','') FROM spoon_all_designs WHERE id IN (${inList});`);
for (const line of rows.split("\n").filter(Boolean)) {
const [id, fn] = line.split("|");
fileMap.set(parseInt(id, 10), fn);
}
console.log(`Resolved ${fileMap.size}/${rejectIds.length} filenames from DB`);
// 2. quarantine local files
for (const [id, fn] of fileMap) {
for (const [src, dst] of [[GEN_DIR, REJ_GEN], [WEB_DIR, REJ_WEB]]) {
const from = path.join(src, fn), to = path.join(dst, fn);
if (fs.existsSync(from)) {
if (!DRY) fs.renameSync(from, to);
console.log(` mv ${path.basename(src)}/${fn} → ${path.basename(dst)}/`);
}
}
}
// 3. Mac2 spoon_all_designs is_published=FALSE
if (!DRY) {
psql(`UPDATE spoon_all_designs SET is_published=FALSE, user_removed=TRUE WHERE id IN (${inList});`);
console.log(`Mac2 DB: is_published=FALSE + user_removed=TRUE on ${rejectIds.length} rows`);
}
// 4. Mac2 designs.json — drop the entries
if (fs.existsSync(DESIGNS_JSON) && !DRY) {
const cur = JSON.parse(fs.readFileSync(DESIGNS_JSON, "utf8"));
const before = cur.length;
const out = cur.filter(d => !rejectIds.includes(d.id));
fs.writeFileSync(DESIGNS_JSON + ".tmp", JSON.stringify(out, null, 2));
fs.renameSync(DESIGNS_JSON + ".tmp", DESIGNS_JSON);
console.log(`Mac2 designs.json: ${before} → ${out.length}`);
}
// 5. Prod: rm watermarked + remove from prod designs.json + restart
if (!DRY && fileMap.size > 0) {
const files = Array.from(fileMap.values()).map(f => `data/generated/${f}`).join(" ");
execSync(`ssh -o ConnectTimeout=15 root@45.61.58.125 'cd /root/public-projects/wallco-ai && rm -f ${files}'`, { stdio: "inherit" });
const tmpIds = `/tmp/cull-ids-${Date.now()}.json`;
fs.writeFileSync(tmpIds, JSON.stringify(rejectIds));
execSync(`scp ${tmpIds} root@45.61.58.125:/tmp/cull-ids.json`, { stdio: "inherit" });
execSync(`ssh root@45.61.58.125 'node -e "
const fs=require(\\"fs\\"); const ids=new Set(JSON.parse(fs.readFileSync(\\"/tmp/cull-ids.json\\",\\"utf8\\")));
const F=\\"/root/public-projects/wallco-ai/data/designs.json\\";
const cur=JSON.parse(fs.readFileSync(F,\\"utf8\\"));
const out=cur.filter(d=>!ids.has(d.id));
fs.writeFileSync(F+\\".tmp\\",JSON.stringify(out,null,2));
fs.renameSync(F+\\".tmp\\",F);
console.log(\\"prod designs.json: \\"+cur.length+\\" → \\"+out.length);
" && pm2 restart wallco-ai --update-env >/dev/null && echo "wallco-ai restarted"'`, { stdio: "inherit" });
}
// 6. audit log
if (!DRY) {
const ts = new Date().toISOString();
fs.appendFileSync(AUDIT, JSON.stringify({ ts, action: "cull", rejected: rejectIds, kept: keepIds, classifier_source: decisionsPath }) + "\n");
console.log(`Audit logged → ${AUDIT}`);
}
console.log(DRY ? "\nDRY RUN — nothing changed." : "\nCull complete.");