Neuer Admin-Reiter "KI": zeigt die letzten vier Zeichen des hinterlegten OpenAI-Schluessels samt Quelle, nimmt einen neuen an (vorher gegen /v1/models geprueft) und laesst je Einsatzzweck das Modell per Dropdown waehlen - Scanner-Bewertung, Abgleich "bereits beworben", erweiterte Suche. Die Auswahl kommt live von OpenAI, Vorgaben sind die bisher fest verdrahteten Modelle. Reasoning-Modelle bekommen reasoning_effort statt temperature, damit ein Wechsel keinen 400er ausloest. Schluesselfeld aus dem Scanner-Reiter entfernt; Doku in docs/14-ki.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
160 lines
6.1 KiB
JavaScript
160 lines
6.1 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 dashboardRoutes from './routes/dashboard.js';
|
|
import scannerRoutes from './routes/scanner.js';
|
|
import mailRoutes from './routes/mail.js';
|
|
import integrationRoutes from './routes/integrations.js';
|
|
import iconCrmRoutes from './routes/iconCrm.js';
|
|
import searchRoutes from './routes/search.js';
|
|
import aiRoutes from './routes/ai.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' });
|
|
}
|
|
});
|
|
|
|
/*
|
|
* Leseschnittstelle fuer das ICON CRM. Eigener Token statt JWT - der Aufrufer
|
|
* ist das ICON-Backend, kein angemeldeter Mensch. Nur GET.
|
|
*/
|
|
app.use('/api/integrations', integrationRoutes);
|
|
|
|
// Gegenrichtung: die Kontakte des ICON CRM lesend anzeigen. Nichts wird gespeichert.
|
|
app.use('/api/icon', iconCrmRoutes);
|
|
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/users', userRoutes);
|
|
app.use('/api/leads', leadRoutes);
|
|
// Globale Suche (Volltext) und die erweiterte Suche mit dem Modell des Scanners.
|
|
app.use('/api/search', searchRoutes);
|
|
// OpenAI-Schluessel und Modellwahl (Einstellungen -> KI, nur admin).
|
|
app.use('/api/ai', aiRoutes);
|
|
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);
|
|
|
|
// Auswertungen des Dashboards.
|
|
app.use('/api/dashboard', dashboardRoutes);
|
|
|
|
// 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);
|
|
}
|