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>
139 lines
5.2 KiB
JavaScript
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);
|
|
}
|