Files
craftvia/scripts/craftvia-worker.ts
T
msolarczekandClaude Opus 5 d9290a187c L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job
- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt,
  Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status
- /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung),
  Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot,
  Enumeration-Schutz, Slug-Kollisionen
- Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern,
  umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit)
- Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi),
  Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil
- Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand
- Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding
- Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:01:47 +02:00

54 lines
2.3 KiB
TypeScript

import "dotenv/config";
import { Worker } from "bullmq";
import { JOB_QUEUES, workerConnection, closeJobQueues, scheduleRecurringJobs, type JobPayload } from "../src/server/jobs/queues";
import { PROCESSORS } from "../src/server/jobs/processors";
import { isJobBlockedByTrial } from "../src/server/services/trial/jobs";
/** Craftvia background worker: `npm run worker:craftvia`. One BullMQ worker per registered queue. */
async function main() {
const connection = workerConnection();
if (!connection) {
console.error("[worker] REDIS_URL is not set — nothing to do.");
process.exit(1);
}
const workers: Worker<JobPayload>[] = [];
for (const name of Object.values(JOB_QUEUES)) {
const load = PROCESSORS[name];
if (!load) {
console.warn(`[worker] no processor registered for ${name} — skipped`);
continue;
}
const processor = await load();
const geocode = name === JOB_QUEUES.geocodeSite; // L13: OSM Nominatim policy — max. 1 request/s
const concurrency = name === JOB_QUEUES.reportPdf ? 2 : geocode ? 1 : 4;
const w = new Worker<JobPayload>(name, async (job) => {
// L15 Testphase: user-triggered jobs of an expired trial tenant (read-only) are skipped
if (await isJobBlockedByTrial(name, job.data)) {
console.warn(`[worker] ${name} job ${job.id} skipped: tenant is read-only (trial expired)`);
return;
}
return processor(job.data);
}, { connection, concurrency, ...(geocode ? { limiter: { max: 1, duration: 1_000 } } : {}) });
w.on("failed", (job, err) => console.error(`[worker] ${name} job ${job?.id} failed:`, err.message));
workers.push(w);
console.info(`[worker] listening on ${name}`);
}
// L10b: recurring jobs (AI log retention); a scheduling failure must not stop the queue workers
await scheduleRecurringJobs(connection).then(
() => console.info("[worker] recurring jobs scheduled"),
(err) => console.error("[worker] scheduling recurring jobs failed:", (err as Error).message),
);
const shutdown = async () => {
await Promise.all(workers.map((w) => w.close()));
await closeJobQueues();
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});