Server (spans the v0.6→v0.8 arc in one file): - template-manager mounted with the productsStore catalog, PNG-only mockup multer, facet personalization config (§R10, FACET_KEY-gated, inert when unset), and the marketing-preview drainer (2s tick, one render per tick, .unref()). - auth-manager bootstrap: DATABASE_URL gates the pg Pool, migrations, and mountAuth (magic-link customer + Google SSO admin + facet); absent DATABASE_URL logs a warning and the host boots without auth. - goods-editor design persistence (#23): idempotent 001_designs.sql applied at boot, db handle passed to mountEditorApi so /api/designs routes mount; without Postgres the client falls back to cart-without-design_id. - reference-protected TTL sweep (#24): before each pass, every /uploads/ basename referenced by template-manager JSON or persisted designs is collected and protected, and exports/designs (db-driven lifecycle, unrecoverable sources) is skipped entirely. Sweep kickoff moves below the bootstrap (pgPool TDZ). sweep-worker.mjs is the standalone process variant for production. Client: main.jsx wraps the tree in AuthProvider (single /api/auth/me fetch shared by useCustomer/useAdmin, degrades on 404) and mounts AppRouter. eslint bumps server.js to ecmaVersion 2022 (top-level await); .env.example documents the full v0.8 env surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Sweep worker. Runs runSweep on a tick — trash hard-delete, asset
|
|
* orphans, audit log rotation, marketing preview queue drain.
|
|
*
|
|
* Usage:
|
|
* node scripts/sweep-worker.mjs # one-shot
|
|
* node scripts/sweep-worker.mjs --loop # run on interval
|
|
*
|
|
* Production: run as a separate process (PM2, systemd, etc.) so the
|
|
* main API server doesn't lose responsiveness when sweep does heavy
|
|
* work (especially preview rendering, which can take seconds per
|
|
* template at scale).
|
|
*
|
|
* For v1 the sweep interval is 60 minutes when run with --loop.
|
|
* Adjust via the --interval=<minutes> flag.
|
|
*/
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
import {
|
|
runSweep,
|
|
marketingPreviewRenderer,
|
|
createTemplateIndex,
|
|
} from 'template-manager/server';
|
|
import { PRODUCT_TYPES } from '../src/constants/products.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const STORAGE_PATH = join(__dirname, '..', 'templates');
|
|
|
|
const args = process.argv.slice(2);
|
|
const LOOP = args.includes('--loop');
|
|
const intervalArg = args.find((a) => a.startsWith('--interval='));
|
|
const INTERVAL_MIN = intervalArg ? parseInt(intervalArg.split('=')[1], 10) : 60;
|
|
|
|
async function buildRenderer() {
|
|
try {
|
|
const { renderDesignComposite } = await import('goods-editor/server');
|
|
const canvasMod = await import('canvas');
|
|
const sharpMod = await import('sharp').catch(() => null);
|
|
|
|
const factory = marketingPreviewRenderer({
|
|
editorRenderer: async (args) => renderDesignComposite({
|
|
...args,
|
|
canvas: canvasMod.default || canvasMod,
|
|
sharp: sharpMod ? (sharpMod.default || sharpMod) : null,
|
|
mockupsDir: join(__dirname, '..', 'mockups'),
|
|
uploadsDir: join(__dirname, '..', 'uploads'),
|
|
stickersDir: join(__dirname, '..', 'public', 'stickers'),
|
|
storageDir: STORAGE_PATH,
|
|
}),
|
|
});
|
|
|
|
// The factory returns a renderer expecting context. We don't have
|
|
// the in-memory marketingPreviewQueue here — that lives in the
|
|
// running API server's process. The worker reconciles via the
|
|
// metadata.autoStaleSince field: any template with that field
|
|
// set is a regen candidate.
|
|
return async (templateId) => {
|
|
// Skip if no audit log handle in this script — that's mounted
|
|
// server-side. The renderer logs failures via the audit log
|
|
// when one is provided; without one, errors silently retry on
|
|
// the next sweep.
|
|
const { createAuditLog } = await import('template-manager/server/lib/auditLog.js').catch(() => ({}));
|
|
const auditLog = createAuditLog ? createAuditLog(STORAGE_PATH) : null;
|
|
return factory({
|
|
templateId,
|
|
storagePath: STORAGE_PATH,
|
|
variantCatalog: PRODUCT_TYPES,
|
|
auditLog,
|
|
});
|
|
};
|
|
} catch (err) {
|
|
console.warn('⚠ Renderer not available; sweep will skip preview regen:', err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function runOnce(renderPreview) {
|
|
const templateIndex = createTemplateIndex(STORAGE_PATH);
|
|
const summary = await runSweep({
|
|
storagePath: STORAGE_PATH,
|
|
templateIndex,
|
|
renderPreview,
|
|
// marketingPreviewQueue is intentionally absent — the in-process
|
|
// queue lives in the API server. Worker process reconciles via
|
|
// metadata.autoStaleSince instead.
|
|
marketingPreviewQueue: null,
|
|
});
|
|
console.log('Sweep:', JSON.stringify(summary, null, 2));
|
|
}
|
|
|
|
async function main() {
|
|
const renderPreview = await buildRenderer();
|
|
if (!LOOP) {
|
|
await runOnce(renderPreview);
|
|
process.exit(0);
|
|
}
|
|
console.log(`▶ Sweep worker loop (interval: ${INTERVAL_MIN} min)`);
|
|
const tick = async () => {
|
|
try { await runOnce(renderPreview); }
|
|
catch (err) { console.error('Sweep tick failed:', err); }
|
|
};
|
|
await tick();
|
|
setInterval(tick, INTERVAL_MIN * 60 * 1000);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Sweep worker failed:', err);
|
|
process.exit(1);
|
|
});
|