Files
perinco-CRM/server/src/server.js
T
Sascha WolfandClaude Opus 5 42639c65fa feat(opportunities): Phasen des Vermittlungsprozesses, Kanban zuerst, Zahlen im Menue
Drei Dinge:

- Die Phasen sind jetzt die des Vermittlungsprozesses: Neu, Beworben,
  Erstgespraech Berater, Kundenvorstellung, Interview Kunde, Zusage, Beauftragt,
  Absage. Offen ist alles bis einschliesslich Zusage - dort ist der Einsatz
  bestaetigt, die Beauftragung steht aber noch aus. migrateOpportunityStages()
  stellt bestehende Datenbanken um und setzt alle vorhandenen Opportunities auf
  "Neu": in welcher der neuen Phasen ein Projekt steckt, sagt der alte Trichter
  nicht (so mit dem Nutzer entschieden).

  Der Datenbankwert heisst 'Erstgespraech Berater' ohne Umlaut - Projektkonvention
  fuer Datenbankwerte. Angezeigt wird er ueber STAGE_LABELS; dazu gibt es
  STAGE_HINTS mit der Erlaeuterung je Phase als Titelhinweis an Auswahl, Badge
  und Spaltenkopf.

- Kanban ist die erste Ansicht der Opportunities. Die Pipeline ist die
  Arbeitsansicht, Kachel und Liste sind zum Nachschauen.

- Hinter "Archiv", beiden "Papierkorb"-Punkten und "Unpassend" steht die Anzahl,
  wie schon bei "Aus Mails". Die Zahlen kommen aus einem eigenen Endpunkt
  (/api/badges), weil sie alle ins Menue gehoeren und zusammen kommen sollen;
  Inhalte laedt weiter jede Ansicht selbst. Der Scanner liegt in einer eigenen
  Datenbank und kann fehlen - sein Zaehler ist dann null und die Leiste zeigt
  dort einfach keine Zahl. Der Aufruf haengt bewusst hinter den Daten und nicht
  im Promise.all: faellt er aus, bleibt das Menue ohne Zahlen statt die Ansicht
  mit einem Fehler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:34:41 +02:00

139 lines
5.2 KiB
JavaScript

import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import { initDatabase, pool } from './db.js';
import authRoutes from './routes/auth.js';
import userRoutes from './routes/users.js';
import leadRoutes from './routes/leads.js';
import connectionRoutes from './routes/connections.js';
import eventRoutes from './routes/events.js';
import opportunityRoutes from './routes/opportunities.js';
import acquisitionRoutes from './routes/acquisitions.js';
import badgeRoutes from './routes/badges.js';
import scannerRoutes from './routes/scanner.js';
import mailRoutes from './routes/mail.js';
import { requireAuth } from './middleware/auth.js';
import { initScannerDatabase } from './scanner/models/index.js';
import { startScheduler } from './scanner/services/scheduler.js';
import { SCANNER_KEY_CONFIGURED } from './scanner/utils/crypto.js';
import { startMailScheduler } from './mail/scheduler.js';
const app = express();
const PORT = Number(process.env.PORT || 5006);
const HOST = process.env.HOST || '127.0.0.1';
// Avatare kommen als Data-URL - deshalb ein groesseres Limit als der Default.
app.use(express.json({ limit: '6mb' }));
// Im Produktivbetrieb liefert Apache Frontend und API unter derselben Origin
// aus, CORS ist dann irrelevant. Fuer den lokalen Vite-Dev-Server auf einem
// anderen Port braucht es die Freigabe.
app.use(
cors({
origin: (process.env.CORS_ORIGINS || 'http://localhost:3100,http://127.0.0.1:3100').split(','),
})
);
app.get('/api/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({ ok: true, db: 'up' });
} catch {
res.status(503).json({ ok: false, db: 'down' });
}
});
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/leads', leadRoutes);
app.use('/api/connections', connectionRoutes);
app.use('/api/events', eventRoutes);
app.use('/api/opportunities', opportunityRoutes);
app.use('/api/acquisitions', acquisitionRoutes);
// Zahlen fuer die Seitenleiste - eine Anfrage fuer alle Zaehler.
app.use('/api/badges', badgeRoutes);
// Projekt Scanner. Komplett hinter der CRM-Anmeldung - der urspruengliche
// HTTP-Basic-Auth-Zugang des Scanners entfaellt damit.
app.use('/api/scanner', requireAuth, scannerRoutes);
// Postfachanbindung crm@perinco.de. Lesen darf jeder Angemeldete, konfigurieren
// und anstossen nur Administratoren - das regeln die Routen selbst.
app.use('/api/mail', requireAuth, mailRoutes);
// Das Diktat laeuft komplett im Browser (Whisper ueber transformers.js) - das
// Backend ist daran nicht beteiligt und sieht keine Audiodaten.
app.use('/api', (req, res) => res.status(404).json({ error: 'Endpunkt nicht gefunden.' }));
// Zentrale Fehlerbehandlung: Details ins Log, generische Meldung nach aussen.
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
// Kaputtes JSON o.ae. ist ein Client-Fehler - kein 500, und es muss auch
// nicht als unbehandelter Serverfehler im Log landen.
const status = err?.status || err?.statusCode;
if (err?.type === 'entity.parse.failed' || status === 400) {
return res.status(400).json({ error: 'Ungültige Anfrage (JSON konnte nicht gelesen werden).' });
}
if (err?.type === 'entity.too.large' || status === 413) {
return res.status(413).json({ error: 'Die Anfrage ist zu gross (max. 6 MB).' });
}
console.error('[perinco CRM] Unbehandelter Fehler:', err);
res.status(500).json({ error: 'Interner Serverfehler.' });
});
try {
await initDatabase();
/*
* Der Scanner ist eine vollstaendig eigenstaendige Instanz: eigene Datenbank,
* eigener Verschluesselungsschluessel, eigenes Upload-Verzeichnis, eigener
* Scheduler. Er laeuft damit parallel zur alten App, ohne ihre Daten zu
* beruehren. Fehlt die Datenbank, startet das CRM trotzdem - dann antworten
* nur die Scanner-Endpunkte mit einem Fehler.
*/
let scannerReady = true;
try {
await initScannerDatabase();
} catch {
scannerReady = false;
console.warn('[Scanner] Bereich ist nicht verfuegbar - CRM startet ohne Scanner.');
}
if (scannerReady && !SCANNER_KEY_CONFIGURED) {
console.warn(
'[Scanner] SCANNER_ENCRYPTION_KEY ist nicht gesetzt - Portal-Passwoerter werden mit ' +
'einem bekannten Ersatzschluessel verschluesselt. Fuer den Produktivbetrieb setzen.'
);
}
/*
* Hintergrund-Scheduler (Scraping + OpenAI-Bewertung). Standard: AN, denn der
* Parallelbetrieb soll funktional sein. Beide Scanner bewerten dieselben
* Projekte unabhaengig voneinander - die OpenAI-Kosten fallen also doppelt an,
* solange die alte App laeuft. Zum Abschalten SCANNER_SCHEDULER=false setzen.
*/
if (scannerReady && process.env.SCANNER_SCHEDULER !== 'false') {
await startScheduler();
} else if (scannerReady) {
console.log('[Scanner] Scheduler deaktiviert (SCANNER_SCHEDULER=false).');
}
/*
* Postfachabgleich (Microsoft Graph). Fehlen die Zugangsdaten in der .env,
* meldet der Scheduler das und bleibt aus - die Oberflaeche laeuft weiter und
* zeigt in den Einstellungen, welche Werte fehlen.
*/
await startMailScheduler();
app.listen(PORT, HOST, () => {
console.log(`[perinco CRM] Backend laeuft auf http://${HOST}:${PORT}`);
});
} catch (err) {
console.error('[perinco CRM] Start fehlgeschlagen:', err);
process.exit(1);
}