- Messskript: data-variant="eigen" zeigt eine eigene Banner-Fassung (Sie-Form, WEBklar misst selbst, BANNER_VERSION_EIGEN 2026-10-17); Kunden-Websites bleiben bei der Fassung 2026-10-02. - config.analyse.eigeneWebsite: fester Schlüssel und Domains von webklar.com; ensureEigeneWebsite zieht den Eintrag im Messdienst täglich nach (aktiv: false schaltet ab). Kein Kunde, kein Abo, keine Tickets. - /api/admin/analyse-eigen (jeder Mitarbeiter): Kennzahlen 7/30/90 Tage und Heatmap-Link auf webklar.com. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
410 lines
23 KiB
JavaScript
410 lines
23 KiB
JavaScript
import { runRepoAssignJob } from './services/repoAssignJob.js'
|
|
import express from 'express'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { config, assertServerConfig, WOMS_DATABASE_ID } from './config.js'
|
|
import { verifyDatabaseAccess } from './services/appwriteAdmin.js'
|
|
import { syncPreviewTlsOnStartup } from './services/previewDeploy.js'
|
|
import { runReconcile } from './services/previewReconcile.js'
|
|
import { checkBankConsent, shouldRunStartupSync, syncBank } from './services/finance/bankSync.js'
|
|
import { getConnection } from './services/finance/bankStore.js'
|
|
import { requireSession, sessionMiddleware } from './middleware/session.js'
|
|
import authRoutes from './routes/auth.js'
|
|
import projectsRoutes from './routes/projects.js'
|
|
import featuresRoutes from './routes/features.js'
|
|
import customerDataRoutes from './routes/customerData.js'
|
|
import chatRoutes from './routes/chat.js'
|
|
import aboRoutes from './routes/abo.js'
|
|
import funktionenRoutes from './routes/funktionen.js'
|
|
import fristenRoutes from './routes/fristen.js'
|
|
import termineRoutes from './routes/termine.js'
|
|
import shopRoutes from './routes/shop.js'
|
|
import shopWebhookRoutes from './routes/shopWebhook.js'
|
|
import { shopSeiteRouter } from './routes/shopSeite.js'
|
|
import unterkunftRoutes from './routes/unterkunft.js'
|
|
import { unterkunftSeiteRouter } from './routes/unterkunftSeite.js'
|
|
import zeitenRoutes from './routes/zeiten.js'
|
|
import publicAuftragRoutes from './routes/auftrag.js'
|
|
import kundeAuftraegeRoutes from './routes/auftraegeKunde.js'
|
|
import takeoverCreditRoutes from './routes/takeoverCredits.js'
|
|
import { purgeExpiredUploads } from './services/takeoverCredits.js'
|
|
import { purgePendingChatUploads } from './services/chatAttachments.js'
|
|
import { purgeViewLog } from './services/customerViewLog.js'
|
|
import { purgeUnclaimedNewsImages } from './services/newsImages.js'
|
|
import { runActivation } from './services/siteFeatures.js'
|
|
import { runAboCheck, runAnalyseReconcile } from './services/analyse.js'
|
|
import { ensureEigeneWebsite } from './services/analyseEigen.js'
|
|
import { aboFunktionenTagesJob } from './services/aboFunktionenAbgleich.js'
|
|
import { fuenfMinutenJob, minutenJob, tagesbericht, taeglicherJob } from './services/termine/jobs.js'
|
|
import { minutenJob as unterkunftMinutenJob, viertelstundenJob as unterkunftViertelstundenJob, taeglicherJob as unterkunftTaeglicherJob } from './services/unterkunft/jobs.js'
|
|
import { minutenJob as shopMinutenJob, stuendlicherJob as shopStuendlicherJob, taeglicherJob as shopTaeglicherJob } from './services/shop/jobs.js'
|
|
import { zeitenAboJob, zeitenErinnerungsJob } from './services/zeiten/jobs.js'
|
|
import giteaWebhookRoutes from './routes/webhook/gitea.js'
|
|
import pushTimeRoutes from './routes/pushTime.js'
|
|
import adminGiteaRoutes from './routes/admin/gitea.js'
|
|
import adminPreviewRoutes from './routes/admin/preview.js'
|
|
import adminWebsiteProjectsRoutes from './routes/admin/websiteProjects.js'
|
|
import adminServicesRoutes from './routes/admin/services.js'
|
|
import adminCustomersRoutes from './routes/admin/customers.js'
|
|
import adminEmployeesRoutes from './routes/admin/employees.js'
|
|
import adminInvoiceClientsRoutes from './routes/admin/invoiceClients.js'
|
|
import newsRoutes from './routes/news.js'
|
|
import publicNewsRoutes from './routes/publicNews.js'
|
|
import publicTermineRoutes from './routes/publicTermine.js'
|
|
import publicShopRoutes from './routes/publicShop.js'
|
|
import publicUnterkunftRoutes from './routes/publicUnterkunft.js'
|
|
import publicZeitenRoutes from './routes/publicZeiten.js'
|
|
import adminAnalyseRoutes from './routes/admin/analyse.js'
|
|
import adminAnalyseEigenRoutes from './routes/admin/analyseEigen.js'
|
|
import { legalRouter } from './services/legalDocs.js'
|
|
import adminFinanzenRoutes from './routes/admin/finanzen.js'
|
|
import adminProjektkontenRoutes from './routes/admin/projektkonten.js'
|
|
import adminAuftraegeRoutes from './routes/admin/auftraege.js'
|
|
import teamRoutes from './routes/team/index.js'
|
|
import legacyAdminRedirect from './routes/legacyAdminRedirect.js'
|
|
import { corsMiddleware } from './middleware/cors.js'
|
|
import { kundenansichtGuard } from './middleware/kundenansichtGuard.js'
|
|
import { dashboardWeiche, neuDirektWeiche, oeffentlicheSeiteWeiche } from './services/kundenansicht/dashboardWeiche.js'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const publicDir = path.join(__dirname, '..', 'public')
|
|
|
|
assertServerConfig()
|
|
|
|
const app = express()
|
|
app.set('trust proxy', 1)
|
|
app.use(sessionMiddleware())
|
|
// Kundenansicht (Mitarbeiter im Portal eines Kunden): Ablauf, Verwechslungsschutz und Schreibsperre vor allen
|
|
// Kunden-Routen, auch vor dem 15-MB-Parser der Gutschriften
|
|
app.use(kundenansichtGuard)
|
|
// Oeffentliche Schnittstelle des Terminplaners (Gaeste ohne Session): bringt Parser (16 KB) und Fehlerbehandlung mit und
|
|
// haengt deshalb VOR dem globalen Parser (2 MB), der ihre Rumpfe sonst zuerst lesen wuerde
|
|
app.use('/api/public/termine/v1', publicTermineRoutes)
|
|
// Oeffentliche Schnittstelle des Webshops (Kaeufer ohne Session): wie die des Terminplaners mit eigenem Parser (16 KB), daher ebenfalls VOR dem globalen Parser
|
|
app.use('/api/public/shop/v1', publicShopRoutes)
|
|
// Oeffentliche Schnittstelle des Buchungskalenders (Gaeste ohne Session): ebenfalls mit eigenem Parser VOR dem globalen
|
|
app.use('/api/public/unterkunft/v1', publicUnterkunftRoutes)
|
|
// Uebernahmegutschrift: Nachweise kommen als Base64 im JSON (max. 10 MB Dateien) -> eigenes Limit,
|
|
// muss vor dem globalen Parser stehen (der ueberspringt bereits geparste Bodies). Login-Pruefung
|
|
// zuerst, damit niemand ohne Session grosse Bodies parsen laesst.
|
|
app.use('/api/abo/takeover-credits', requireSession, express.json({ limit: '15mb' }))
|
|
// rawBody wird fuer die HMAC-Verifikation des Gitea-Webhooks benoetigt
|
|
app.use(express.json({ limit: '2mb', verify: (req, _res, buf) => { req.rawBody = buf } }))
|
|
|
|
app.use(corsMiddleware(config.cors.allowedOrigins))
|
|
|
|
app.use('/api/auth', authRoutes)
|
|
app.use('/api/projects', projectsRoutes)
|
|
app.use('/api/features', featuresRoutes)
|
|
app.use('/api/public/news', publicNewsRoutes)
|
|
app.use('/api/public/zeiten/v1', publicZeitenRoutes)
|
|
app.use('/api/news', newsRoutes)
|
|
app.use('/api', customerDataRoutes)
|
|
app.use('/api/chat', chatRoutes)
|
|
app.use('/api/abo/takeover-credits', takeoverCreditRoutes)
|
|
app.use('/api/abo', aboRoutes)
|
|
app.use('/api/funktionen', funktionenRoutes)
|
|
app.use('/api/fristen', fristenRoutes)
|
|
app.use('/api/termine', termineRoutes)
|
|
// Stripe-Connect-Webhook des Webshops (ohne Sitzung): vor dem Router, braucht rawBody des globalen Parsers
|
|
app.use('/api/shop/stripe-webhook', shopWebhookRoutes)
|
|
app.use('/api/shop', shopRoutes)
|
|
app.use('/api/unterkunft', unterkunftRoutes)
|
|
app.use('/api/zeiten', zeitenRoutes)
|
|
// Auftragsbestaetigung: oeffentliche Bestaetigung per Token (ohne Sitzung) und die Auftraege des angemeldeten Kunden
|
|
app.use('/api/auftrag', publicAuftragRoutes)
|
|
app.use('/api/auftraege', kundeAuftraegeRoutes)
|
|
app.use('/api/team', teamRoutes)
|
|
app.use('/api/admin/gitea', adminGiteaRoutes)
|
|
app.use('/api/admin/website-projects', adminPreviewRoutes)
|
|
app.use('/api/admin/website-projects', adminWebsiteProjectsRoutes)
|
|
app.use('/api/admin/services', adminServicesRoutes)
|
|
app.use('/api/admin/customers', adminCustomersRoutes)
|
|
app.use('/api/admin/employees', adminEmployeesRoutes)
|
|
app.use('/api/admin/invoice-clients', adminInvoiceClientsRoutes)
|
|
app.use('/api/admin/analyse', adminAnalyseRoutes)
|
|
// Website-Analyse der eigenen Website webklar.com (WOMS-Seite Website-Statistik)
|
|
app.use('/api/admin/analyse-eigen', adminAnalyseEigenRoutes)
|
|
// Projektkonten und Auftraege (Admin): vor dem Finanzen-Router, dessen requireAppwriteStaff sonst jede Anfrage darunter zuerst prueft
|
|
app.use('/api/admin/finanzen/projektkonten', adminProjektkontenRoutes)
|
|
app.use('/api/admin/finanzen/auftraege', adminAuftraegeRoutes)
|
|
app.use('/api/admin/finanzen', adminFinanzenRoutes)
|
|
app.use('/webhook', giteaWebhookRoutes)
|
|
// Push-Zeitmail: Zeit eines Git-Pushes per Link aus der Mail eintragen (ohne Login, signierter Token)
|
|
app.use('/zeit', express.urlencoded({ extended: false, limit: '4kb' }), pushTimeRoutes)
|
|
|
|
app.get('/api/health', async (_req, res) => {
|
|
const dbAccess = await verifyDatabaseAccess()
|
|
res.json({
|
|
ok: dbAccess.ok,
|
|
service: 'webklar-kundenbereich',
|
|
databaseId: WOMS_DATABASE_ID,
|
|
endpoint: config.appwrite.endpoint,
|
|
appwriteDbAccess: dbAccess,
|
|
})
|
|
})
|
|
|
|
// Einbau-Skript fuer Kundenwebsites: darf von fremden Seiten geladen werden, 5 Minuten Cache
|
|
app.use('/embed/', (_req, res, next) => {
|
|
res.set('Cache-Control', 'public, max-age=300')
|
|
res.set('Access-Control-Allow-Origin', '*')
|
|
next()
|
|
})
|
|
// Adressen ohne Fassung: aktuelle Fassung der Rechtstexte (Analyse und Terminplaner; vor express.static)
|
|
app.use('/rechtliches', legalRouter())
|
|
// Webshop-Seiten fuer Kaeufer: /shop/:siteKey und /shop/:siteKey/widerruf (vor express.static)
|
|
app.use(shopSeiteRouter())
|
|
// Buchungskalender-Seiten fuer Gaeste: /unterkunft/:siteKey und /unterkunft/:siteKey/buchung/:id (vor express.static)
|
|
app.use(unterkunftSeiteRouter())
|
|
// Abgeschaltetes Admin-Panel und Rechnungsanalyse: Links aus alten Team-Mails ins Ticketsystem
|
|
app.use('/admin.html', legacyAdminRedirect.admin)
|
|
app.use('/kalkulation.html', legacyAdminRedirect.kalkulation)
|
|
// Kundenansicht neu: je Sitzung die neue oder die alte Seite (Schalter KUNDENANSICHT_NEU), vor den statischen Dateien
|
|
app.get('/dashboard.html', dashboardWeiche({ publicDir }))
|
|
app.get('/neu/dashboard.html', neuDirektWeiche())
|
|
// Kuendigen und Widerrufen: im Pilot die neuen Seiten aus public/neu/oeffentlich/, sobald die Dateien dort liegen
|
|
app.get(['/kuendigen.html', '/widerrufen.html'], oeffentlicheSeiteWeiche({ publicDir }))
|
|
app.use(express.static(publicDir))
|
|
|
|
app.get('/dashboard.html', (req, res, next) => {
|
|
const raw = req.signedCookies?.[config.cookieName]
|
|
if (!raw) {
|
|
return res.redirect('/login.html')
|
|
}
|
|
next()
|
|
})
|
|
|
|
app.get('/', (_req, res) => {
|
|
res.redirect('/login.html')
|
|
})
|
|
|
|
app.use((err, _req, res, _next) => {
|
|
console.error('[server] Unbehandelter Fehler:', err)
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: err.message || 'Interner Serverfehler' })
|
|
}
|
|
})
|
|
|
|
const server = app.listen(config.port, '0.0.0.0', () => {
|
|
console.log(`Webklar Kundenbereich läuft auf Port ${config.port}`)
|
|
syncPreviewTlsOnStartup()
|
|
verifyDatabaseAccess().then((result) => {
|
|
if (result.ok) return
|
|
console.error(
|
|
'[startup] APPWRITE_API_KEY: Kein Lesezugriff auf woms-database. In Appwrite Console neuen API-Key mit Scopes databases.read + databases.write anlegen und in .env eintragen.'
|
|
)
|
|
if (result.reason) console.error(`[startup] Appwrite: ${result.reason}`)
|
|
})
|
|
|
|
// Nachweise abgelehnter Uebernahmegutschriften nach Ablauf der Aufbewahrungsfrist loeschen (taeglich)
|
|
const purge = () =>
|
|
purgeExpiredUploads()
|
|
.then((r) => r.purged && console.log(`[takeover] Nachweise von ${r.purged} abgelehnten Anträgen gelöscht`))
|
|
.catch((err) => console.warn('[takeover] Aufräumen fehlgeschlagen:', err.message))
|
|
setTimeout(purge, 60 * 1000).unref()
|
|
setInterval(purge, 24 * 60 * 60 * 1000).unref()
|
|
|
|
// Protokoll der Kundenansicht: Eintraege aelter als 12 Monate loeschen (taeglich)
|
|
const purgeViewLogJob = () =>
|
|
purgeViewLog()
|
|
.then((r) => r.deleted && console.log(`[kundenansicht] ${r.deleted} Protokolleinträge gelöscht`))
|
|
.catch((err) => console.warn('[kundenansicht] Aufräumen fehlgeschlagen:', err.message))
|
|
setTimeout(purgeViewLogJob, 150 * 1000).unref()
|
|
setInterval(purgeViewLogJob, 24 * 60 * 60 * 1000).unref()
|
|
|
|
// Chat-Anhaenge, die nie abgeschickt wurden, nach config.chat.pendingTtlHours loeschen (stuendlich)
|
|
const purgeChat = () =>
|
|
purgePendingChatUploads()
|
|
.then((r) => r.purged && console.log(`[chat] ${r.purged} nicht gesendete Anhänge gelöscht`))
|
|
.catch((err) => console.warn('[chat] Aufräumen fehlgeschlagen:', err.message))
|
|
setTimeout(purgeChat, 90 * 1000).unref()
|
|
setInterval(purgeChat, 60 * 60 * 1000).unref()
|
|
|
|
// News-Fotos, die nie an einen Beitrag gebunden wurden, nach 24 Stunden loeschen (stuendlich)
|
|
const purgeNewsImages = () =>
|
|
purgeUnclaimedNewsImages()
|
|
.then((n) => n && console.log(`[news] ${n} nicht verwendete Fotos gelöscht`))
|
|
.catch((err) => console.warn('[news] Aufräumen fehlgeschlagen:', err.message))
|
|
setTimeout(purgeNewsImages, 120 * 1000).unref()
|
|
setInterval(purgeNewsImages, 60 * 60 * 1000).unref()
|
|
|
|
// Funktionen (News-Pinnwand): geschlossene Einrichtungs-Tickets freischalten und den Kunden benachrichtigen (alle 5 Minuten)
|
|
const activateFeatures = () =>
|
|
runActivation()
|
|
.then((done) => done.length && console.log(`[funktionen] ${done.length} Funktion(en) freigeschaltet`))
|
|
.catch((err) => console.warn('[funktionen] Freischalten fehlgeschlagen:', err.message))
|
|
setInterval(activateFeatures, 5 * 60 * 1000).unref()
|
|
|
|
// Website-Analyse: aktive Analysen ohne laufendes Wartungs-Abo beenden (taeglich, 2 Minuten nach Start beginnend).
|
|
// Laeuft unabhaengig vom Schalter ANALYSE_ENABLED: er liest nur Analyse-Dokumente und ist ohne solche wirkungslos.
|
|
const analyseAboCheck = () =>
|
|
runAboCheck({ now: new Date() })
|
|
.then((r) => r.deactivated.length && console.log(`[analyse] ${r.deactivated.length} Analyse(n) ohne Abo beendet`))
|
|
.catch((err) => console.warn('[analyse] Abo-Prüfung fehlgeschlagen:', err.message))
|
|
// danach der Abgleich: gelöschte Projekte/Kunden, geänderte Domains, Messdienst für beendete Analysen, fehlende Tickets
|
|
.then(() => runAnalyseReconcile({ now: new Date() }))
|
|
.then((r) => r.ended.length && console.log(`[analyse] Abgleich: ${r.ended.length} Analyse(n) beendet`))
|
|
.catch((err) => console.warn('[analyse] Abgleich fehlgeschlagen:', err.message))
|
|
// eigene Website webklar.com: Eintrag im Messdienst nachziehen (ohne Kunde und Abo, config.analyse.eigeneWebsite)
|
|
.then(() => ensureEigeneWebsite())
|
|
.catch((err) => console.warn('[analyse] Eintrag für webklar.com fehlgeschlagen:', err.message))
|
|
setTimeout(analyseAboCheck, 2 * 60 * 1000).unref()
|
|
setInterval(analyseAboCheck, 24 * 60 * 60 * 1000).unref()
|
|
|
|
// Funktionen des Abo-Modells (Kundenansicht neu, P6): taeglich der Abo-Ende-Abgleich fuer News, Terminplaner, Webshop und
|
|
// Buchungskalender (150 Sekunden nach dem Start beginnend, VOR dem Terminplaner-Tagesjob bei 180 s und den Loeschjobs von Shop
|
|
// und Buchungskalender, damit ein zurueckgekehrtes Abo nicht mehr geloescht wird). Laeuft unabhaengig von den Funktionsschaltern: ohne
|
|
// Dokumente des Abo-Modells kostet er eine Abfrage. Ein noch laufender Lauf wird nicht ueberlappt; im Log stehen keine Kundendaten.
|
|
const aboFunktionenAbgleich = () =>
|
|
aboFunktionenTagesJob(new Date())
|
|
.then((r) => {
|
|
if (r.abgeschaltet || r.wiederEin || r.geloescht || r.mails || r.fehler) {
|
|
console.log(`[funktionen-abo] ${r.abgeschaltet ?? 0} abgeschaltet, ${r.wiederEin ?? 0} wieder eingeschaltet, ${r.geloescht ?? 0} gelöscht, ${r.mails ?? 0} Mail(s), ${r.fehler ?? 0} Fehler`)
|
|
}
|
|
})
|
|
.catch((err) => console.warn('[funktionen-abo] Abgleich fehlgeschlagen:', err.message))
|
|
setTimeout(aboFunktionenAbgleich, 150 * 1000).unref()
|
|
setInterval(aboFunktionenAbgleich, 24 * 60 * 60 * 1000).unref()
|
|
|
|
// Terminplaner (Modus off: jeder Lauf kehrt sofort zurueck, ohne Abfrage; ein noch laufender Job wird nicht ueberlappt):
|
|
// jede Minute abgelaufene Haltungen verfallen lassen und die Mail-Warteschlange senden, alle 5 Minuten Erinnerungen, erledigte
|
|
// Termine und vergangene Anfragen, taeglich (3 Minuten nach dem Start beginnend) Loeschfristen und Aufraeumen. Die Jobs melden
|
|
// einzelne Fehler selbst; hier faengt der Rahmen alles Unerwartete ab. Im Log stehen keine Gastdaten.
|
|
const termineMinutenJob = () =>
|
|
minutenJob(new Date()).catch((err) => console.warn('[termine] Minutenjob fehlgeschlagen:', err.message))
|
|
setInterval(termineMinutenJob, 60 * 1000).unref()
|
|
const termineFuenfMinutenJob = () =>
|
|
fuenfMinutenJob(new Date()).catch((err) => console.warn('[termine] Fünf-Minuten-Job fehlgeschlagen:', err.message))
|
|
setInterval(termineFuenfMinutenJob, 5 * 60 * 1000).unref()
|
|
// Nach dem Tagesjob steht eine Zeile mit den Zahlen der Loeschungen im Log (Spur unwiderruflicher Loeschungen, ohne Personendaten)
|
|
const termineTagesJob = () =>
|
|
taeglicherJob(new Date())
|
|
.then((ergebnis) => {
|
|
const zeile = tagesbericht(ergebnis)
|
|
if (zeile) console.log(zeile)
|
|
})
|
|
.catch((err) => console.warn('[termine] Tagesjob fehlgeschlagen:', err.message))
|
|
setTimeout(termineTagesJob, 180 * 1000).unref()
|
|
setInterval(termineTagesJob, 24 * 60 * 60 * 1000).unref()
|
|
|
|
// Webshop: jede Minute offene Bestellungen mit verstrichener Frist bei Stripe abgleichen (75 s nach dem Start beginnend), stuendlich nie
|
|
// beanspruchte Artikelbilder aufraeumen (130 s), taeglich Kaeuferdaten anonymisieren, unbezahlte Bestellungen und lange abgeschaltete
|
|
// Shops loeschen (200 s). Bei SHOP_ENABLED=off kehren Minuten- und Stundenjob sofort zurueck; der Tagesjob laeuft dann nur, wenn es
|
|
// noch Shop-Dokumente gibt. Die Jobs melden Einzelfehler selbst (ohne Kaeuferdaten); hier faengt der Rahmen alles Unerwartete ab.
|
|
const shopMinuten = () => shopMinutenJob(new Date()).catch((err) => console.warn('[shop] Minutenjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
shopMinuten()
|
|
setInterval(shopMinuten, 60 * 1000).unref()
|
|
}, 75 * 1000).unref()
|
|
const shopStunden = () => shopStuendlicherJob(new Date()).catch((err) => console.warn('[shop] Stundenjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
shopStunden()
|
|
setInterval(shopStunden, 60 * 60 * 1000).unref()
|
|
}, 130 * 1000).unref()
|
|
const shopTages = () =>
|
|
shopTaeglicherJob(new Date())
|
|
.then((r) => r && !r.aus && !r.uebersprungen && console.log(`[shop] Tagesjob: ${JSON.stringify(r)}`))
|
|
.catch((err) => console.warn('[shop] Tagesjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
shopTages()
|
|
setInterval(shopTages, 24 * 60 * 60 * 1000).unref()
|
|
}, 200 * 1000).unref()
|
|
|
|
// Buchungskalender: jede Minute Fristen ablaufen lassen (80 s nach dem Start beginnend), alle 15 Minuten iCal-Importe (140 s),
|
|
// taeglich Aufbewahrung (210 s). Bei UNTERKUNFT_ENABLED=off kehrt jeder Lauf sofort zurueck. Die Jobs melden Einzelfehler selbst
|
|
// (ohne Gastdaten); hier faengt der Rahmen alles Unerwartete ab.
|
|
const unterkunftMinuten = () => unterkunftMinutenJob(new Date()).catch((err) => console.warn('[unterkunft] Minutenjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
unterkunftMinuten()
|
|
setInterval(unterkunftMinuten, 60 * 1000).unref()
|
|
}, 80 * 1000).unref()
|
|
const unterkunftViertelstunden = () =>
|
|
unterkunftViertelstundenJob(new Date()).catch((err) => console.warn('[unterkunft] Viertelstundenjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
unterkunftViertelstunden()
|
|
setInterval(unterkunftViertelstunden, 15 * 60 * 1000).unref()
|
|
}, 140 * 1000).unref()
|
|
const unterkunftTages = () =>
|
|
unterkunftTaeglicherJob(new Date())
|
|
.then((r) => r && !r.aus && !r.uebersprungen && console.log(`[unterkunft] Tagesjob: ${JSON.stringify(r)}`))
|
|
.catch((err) => console.warn('[unterkunft] Tagesjob fehlgeschlagen:', err.message))
|
|
setTimeout(() => {
|
|
unterkunftTages()
|
|
setInterval(unterkunftTages, 24 * 60 * 60 * 1000).unref()
|
|
}, 210 * 1000).unref()
|
|
|
|
// Oeffnungszeiten live: taeglich der Abo-Abgleich (4 Minuten nach dem Start beginnend; laeuft unabhaengig vom Schalter ZEITEN_ENABLED,
|
|
// ohne Zeiten-Dokumente kostet er eine Abfrage) und stuendlich die Feiertags-Erinnerung (5 Minuten nach dem Start beginnend; nur bei
|
|
// Schalter pilot/on und nur 08:00 bis 20:00 Berliner Zeit, sonst kehrt jeder Lauf sofort zurueck). Beide Jobs ueberlappen nicht und
|
|
// werfen nie; hier faengt der Rahmen trotzdem alles Unerwartete ab. Im Log stehen keine Kundendaten.
|
|
const zeitenAbo = () =>
|
|
zeitenAboJob(new Date())
|
|
.then((r) => {
|
|
if (r.aboEnde || r.abgeschaltet || r.wiederEin || r.geloescht || r.mails || r.fehler) {
|
|
console.log(`[zeiten] Abo-Abgleich: ${r.aboEnde ?? 0} Abo-Ende erkannt, ${r.abgeschaltet ?? 0} abgeschaltet, ${r.wiederEin ?? 0} wieder eingeschaltet, ${r.geloescht ?? 0} gelöscht, ${r.mails ?? 0} Mail(s), ${r.fehler ?? 0} Fehler`)
|
|
}
|
|
})
|
|
.catch((err) => console.warn('[zeiten] Abo-Abgleich fehlgeschlagen:', err.message))
|
|
setTimeout(zeitenAbo, 240 * 1000).unref()
|
|
setInterval(zeitenAbo, 24 * 60 * 60 * 1000).unref()
|
|
const zeitenErinnerung = () =>
|
|
zeitenErinnerungsJob(new Date())
|
|
.then((r) => {
|
|
if (r.merker || r.mails || r.fehler) console.log(`[zeiten] Feiertags-Erinnerung: ${r.merker ?? 0} Tag(e) vorgemerkt, ${r.mails ?? 0} Mail(s), ${r.fehler ?? 0} Fehler`)
|
|
})
|
|
.catch((err) => console.warn('[zeiten] Feiertags-Erinnerung fehlgeschlagen:', err.message))
|
|
setTimeout(zeitenErinnerung, 300 * 1000).unref()
|
|
setInterval(zeitenErinnerung, 60 * 60 * 1000).unref()
|
|
|
|
// Neue Gitea-Repos erkennen, Zuordnungs-Mail senden, Erinnerungen/Wiederholungen (alle 10 Minuten)
|
|
const repoAssign = () =>
|
|
runRepoAssignJob({ now: new Date() }).catch((err) =>
|
|
console.warn('[repoAssign] Lauf fehlgeschlagen:', err.message)
|
|
)
|
|
setTimeout(repoAssign, 60 * 1000).unref()
|
|
setInterval(repoAssign, 10 * 60 * 1000).unref()
|
|
|
|
// Finanzen (nur wenn Enable Banking eingerichtet ist): Bankabruf 2 Minuten nach dem Start (nur wenn der letzte
|
|
// nicht frisch war) und alle 6 Stunden, Warnung vor dem Ablauf des Bankzugangs 3 Minuten nach dem Start und
|
|
// taeglich. syncBank wirft nie, meldet einen Fehlschlag aber im Ergebnis; die Meldung enthaelt keine Sitzungs-
|
|
// oder Schluesselangaben.
|
|
if (config.enableBanking.enabled) {
|
|
const bankSync = () =>
|
|
syncBank()
|
|
.then((r) => !r.ok && console.warn('[finanzen] Bankabruf fehlgeschlagen:', r.error))
|
|
.catch((err) => console.warn('[finanzen] Bankabruf fehlgeschlagen:', err.message))
|
|
// Beim Start nur abrufen, wenn der letzte Abruf nicht frisch war: Deployments starten den Server oft neu und
|
|
// wuerden sonst das Tageslimit der Bank aufbrauchen
|
|
const startupBankSync = () =>
|
|
getConnection()
|
|
.then((connection) => {
|
|
if (shouldRunStartupSync(connection, new Date())) return bankSync()
|
|
console.log('[finanzen] Bankabruf beim Start übersprungen (letzter Abruf ist frisch)')
|
|
})
|
|
.catch((err) => console.warn('[finanzen] Bankabruf fehlgeschlagen:', err.message))
|
|
setTimeout(startupBankSync, 2 * 60 * 1000).unref()
|
|
setInterval(bankSync, 6 * 60 * 60 * 1000).unref()
|
|
|
|
const bankConsent = () =>
|
|
checkBankConsent().catch((err) => console.warn('[finanzen] Prüfung des Bankzugangs fehlgeschlagen:', err.message))
|
|
setTimeout(bankConsent, 3 * 60 * 1000).unref()
|
|
setInterval(bankConsent, 24 * 60 * 60 * 1000).unref()
|
|
}
|
|
|
|
// Preview-Runner: Zustaende mit dem Runner abgleichen (alle 2 Minuten, nur wenn eingeschaltet)
|
|
if (config.previewRunner.enabled) {
|
|
setTimeout(runReconcile, 30 * 1000).unref()
|
|
setInterval(runReconcile, 2 * 60 * 1000).unref()
|
|
}
|
|
})
|
|
|
|
server.on('error', (err) => {
|
|
if (err.code === 'EADDRINUSE') {
|
|
console.error(
|
|
`[server] Port ${config.port} ist bereits belegt. Windows: netstat -ano | findstr :${config.port} → taskkill /PID <PID> /F. Oder in .env einen anderen PORT setzen (z. B. PORT=3002).`
|
|
)
|
|
process.exit(1)
|
|
}
|
|
throw err
|
|
})
|