loeschKandidaten liest eine Seite (Standard 100, aelteste Bezugszeit zuerst, optional je Funktion) ohne Gastfelder; die Frist steht in der Abfrage. arbeiteSeiten (Seite lesen, bearbeiten, neu lesen) endet bei leerer Seite, am Deckel (config.termine.loeschDeckel, 2000 je Status und Lauf, Rest an den Folgetagen, gezaehlt als gedeckelt) oder bei einer Seite ohne Fortschritt. verfallen, abgesagt und abgelehnt laufen darueber, unter der Buchungssperre werden Status und Bezugszeit frisch geprueft. taeglicherJob liefert gedeckelt getrennt vom Loeschzaehler. Neue Indizes idx_status_ends und idx_status_updated auf termineBookings (scripts/termine-setup.mjs legt sie an). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
415 lines
20 KiB
JavaScript
415 lines
20 KiB
JavaScript
import 'dotenv/config'
|
||
import path from 'node:path'
|
||
|
||
/** Einzige erlaubte Datenbank (Ticketsystem). Keine separate Portal-DB mehr. */
|
||
export const WOMS_DATABASE_ID = 'woms-database'
|
||
|
||
const DEPRECATED_DATABASE_IDS = new Set([
|
||
'6943bf0e0003291f8c35', // frühere Portal-/Legacy-DB
|
||
])
|
||
|
||
function resolveDatabaseId() {
|
||
const fromEnv = (process.env.APPWRITE_DATABASE_ID || '').trim()
|
||
if (!fromEnv || fromEnv === WOMS_DATABASE_ID) return WOMS_DATABASE_ID
|
||
if (DEPRECATED_DATABASE_IDS.has(fromEnv)) {
|
||
console.warn(
|
||
`[config] Veraltete APPWRITE_DATABASE_ID "${fromEnv}" – es wird nur ${WOMS_DATABASE_ID} verwendet.`
|
||
)
|
||
return WOMS_DATABASE_ID
|
||
}
|
||
console.warn(
|
||
`[config] APPWRITE_DATABASE_ID="${fromEnv}" ignoriert – Portal nutzt ausschließlich ${WOMS_DATABASE_ID}.`
|
||
)
|
||
return WOMS_DATABASE_ID
|
||
}
|
||
|
||
/** Zahl >= 0 aus einer Umgebungsvariablen; leer, keine Zahl oder negativ ergibt den Vorgabewert. */
|
||
function nonNegativeNumber(raw, fallback) {
|
||
const value = Number(raw)
|
||
return String(raw ?? '').trim() !== '' && Number.isFinite(value) && value >= 0 ? value : fallback
|
||
}
|
||
|
||
const termineEnabled = (process.env.TERMINE_ENABLED || '').trim().toLowerCase()
|
||
|
||
// Abo-Produkte (Spec 2026-09-27 "Verträge, Hosting-Abo und E-Mail-Zusatzspeicher"). Preise in Cent,
|
||
// Endpreise gemäß § 19 UStG. Texte, die Kunden sehen, stimmen mit den Vertragsbedingungen überein.
|
||
const products = {
|
||
wartung: {
|
||
key: 'wartung',
|
||
name: 'WEBklar Wartungs-Abo',
|
||
lookupKey: process.env.STRIPE_ABO_PRICE_LOOKUP_KEY || 'webklar_wartung_monatlich_5000',
|
||
amountCents: 5000,
|
||
// Version 4 (Rechtsrunde 28.09.2026): beliebig viele E-Mail-Postfächer statt 1 E-Mail-Postfach (F07;
|
||
// Fix round 1, item 11: ausgeschrieben "E-Mail-Postfächer" statt bloß "Postfächer")
|
||
description:
|
||
'Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland (Sicherheitsupdates, SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit), 1 Domain inklusive Verwaltung, beliebig viele E-Mail-Postfächer (zusammen 5 GB) und 1 Stunde Arbeitszeit pro Monat (12 Monate nutzbar). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
|
||
statementDescriptor: 'WEBKLAR WARTUNG',
|
||
priceNickname: 'Wartungs-Abo monatlich',
|
||
minTermMonths: 12,
|
||
minutesPerMonth: 60,
|
||
// Testrabatt: id passend zur amount_off-Regel (amountCents - 50), siehe global-constraints.md
|
||
testCouponId: 'WEBKLAR-TEST-050',
|
||
// Leistungen fuers Kundenportal (Vergleich, Bestellseite): [Titel, Detail], Spec 3.1 / Fassung 4
|
||
features: [
|
||
['Betrieb & Pflege Ihrer Website bzw. Web-Anwendung', 'Server in Deutschland, Updates, Sicherheit, tägliche Datensicherung, Überwachung'],
|
||
['1 Domain inklusive', '.de, .com, .eu, .net oder .org, Registrierung und DNS-Verwaltung'],
|
||
['beliebig viele Postfächer', 'zusammen 5 GB Speicher'],
|
||
['1 Stunde Arbeitszeit pro Monat', 'nicht genutzte Zeit bleibt 12 Monate verfügbar'],
|
||
],
|
||
},
|
||
hosting: {
|
||
key: 'hosting',
|
||
name: 'WEBklar Hosting-Abo',
|
||
lookupKey: 'webklar_hosting_monatlich_2000',
|
||
amountCents: 2000,
|
||
description:
|
||
'Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland (SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit, Updates von Betriebssystem und Server-Software), 1 Domain inklusive Verwaltung und 1 E-Mail-Postfach (5 GB). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
|
||
statementDescriptor: 'WEBKLAR HOSTING',
|
||
priceNickname: 'Hosting-Abo monatlich',
|
||
minTermMonths: 12,
|
||
minutesPerMonth: 0,
|
||
testCouponId: 'WEBKLAR-TEST-050-HOSTING',
|
||
// Fix round 1, Important 3: row 1 war wörtlich von Wartung kopiert und suggerierte Website-Pflege
|
||
// (die es im Hosting-Abo nicht gibt); row 4 nennt jetzt die Abrechnung in 15-Minuten-Schritten.
|
||
features: [
|
||
['Bereitstellung Ihrer statischen Website', 'Server in Deutschland, SSL, Updates von Betriebssystem und Server-Software, tägliche Datensicherung, Überwachung'],
|
||
['1 Domain inklusive', '.de, .com, .eu, .net oder .org, Registrierung und DNS-Verwaltung'],
|
||
['1 E-Mail-Postfach', 'mit Ihrer eigenen Domain, 5 GB Speicher'],
|
||
['Keine Pflege der Website', 'Pflege und Updates der Website nur im Wartungs-Abo'],
|
||
['Änderungen nur nach Freigabe', '100 € pro Stunde in 15-Minuten-Schritten (gemäß § 19 UStG ohne Umsatzsteuer)'],
|
||
],
|
||
},
|
||
}
|
||
|
||
// E-Mail-Zusatzspeicher: zweite Position (Stückzahl) im selben Stripe-Abo
|
||
const mailStorage = {
|
||
key: 'mailStorage',
|
||
name: 'WEBklar E-Mail-Zusatzspeicher 5 GB',
|
||
lookupKey: 'webklar_mailspeicher_5gb_monatlich_100',
|
||
amountCents: 100,
|
||
gbPerUnit: 5,
|
||
includedGb: 5,
|
||
maxUnits: 10,
|
||
priceNickname: 'E-Mail-Zusatzspeicher 5 GB monatlich',
|
||
// Hetzner-Paket (konsoleH), auf dem alle Postfaecher liegen: Gesamtgroesse und die Schwelle, ab der
|
||
// der Admin-Reiter "E-Mail-Speicher" rot warnt (Task 13)
|
||
hetznerPackage: { name: 'Webhosting M', gb: 50, warnGb: 40 },
|
||
}
|
||
|
||
// News-Pinnwand: einmalige Einrichtung (Einmalpreis, kein Abo)
|
||
const newsSetup = {
|
||
key: 'newsSetup',
|
||
name: 'News-Pinnwand – Einrichtung',
|
||
amountCents: 3000,
|
||
lookupKey: 'webklar_news_einrichtung_3000',
|
||
oneTime: true,
|
||
priceNickname: 'News-Einrichtung 30 €',
|
||
}
|
||
|
||
// Ticketsystem (WOMS); eigene Konstante, weil auch die Enable-Banking-Rueckkehradresse darauf aufbaut
|
||
const womsBaseUrl = (process.env.WOMS_PUBLIC_URL || 'https://ticket.webklar.com').replace(/\/$/, '')
|
||
|
||
const ENABLE_BANKING_API_BASE = 'https://api.enablebanking.com'
|
||
const enableBankingAppId = (process.env.ENABLE_BANKING_APP_ID || '').trim()
|
||
const enableBankingKeyPath = (process.env.ENABLE_BANKING_KEY_PATH || '').trim()
|
||
|
||
/**
|
||
* Basisadresse der Enable-Banking-API. ENABLE_BANKING_API_BASE (nur fuer Tests: Nachbau) gilt nur fuer
|
||
* 127.0.0.1 oder localhost; jeder andere Wert wird ignoriert, damit ein verirrter Wert den signierten
|
||
* Zugriff (JWT) nie an einen fremden Server schickt.
|
||
*/
|
||
export function resolveEnableBankingApiBase(value) {
|
||
const raw = String(value || '').trim()
|
||
if (!raw) return ENABLE_BANKING_API_BASE
|
||
try {
|
||
const url = new URL(raw)
|
||
if (['http:', 'https:'].includes(url.protocol) && ['127.0.0.1', 'localhost'].includes(url.hostname)) return url.origin
|
||
} catch {
|
||
// keine gueltige Adresse: Standard
|
||
}
|
||
return ENABLE_BANKING_API_BASE
|
||
}
|
||
|
||
export const config = {
|
||
port: Number(process.env.PORT) || 3000,
|
||
sessionSecret: process.env.SESSION_SECRET || '',
|
||
cookieName: process.env.SESSION_COOKIE_NAME || 'webklar_portal_session',
|
||
allowedCustomerStatuses: (process.env.ALLOWED_CUSTOMER_STATUSES || 'active')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
|
||
appwrite: {
|
||
endpoint: process.env.APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1',
|
||
projectId: process.env.APPWRITE_PROJECT_ID || '6a1058610003c5a13a05',
|
||
databaseId: resolveDatabaseId(),
|
||
apiKey: process.env.APPWRITE_API_KEY || '',
|
||
},
|
||
|
||
collections: {
|
||
customers: process.env.APPWRITE_COLLECTION_CUSTOMERS || 'customers',
|
||
customerPortalAccess: process.env.APPWRITE_COLLECTION_CUSTOMER_PORTAL_ACCESS || 'customerPortalAccess',
|
||
websiteProjects: process.env.APPWRITE_COLLECTION_WEBSITE_PROJECTS || 'websiteProjects',
|
||
portalFeatures: process.env.APPWRITE_COLLECTION_PORTAL_FEATURES || 'portalFeatures',
|
||
analyseConsents: process.env.APPWRITE_COLLECTION_ANALYSE_CONSENTS || 'analyseConsents',
|
||
workorders: process.env.APPWRITE_COLLECTION_WORKORDERS || 'workorders',
|
||
worksheets: process.env.APPWRITE_COLLECTION_WORKSHEETS || 'worksheets',
|
||
employees: process.env.APPWRITE_COLLECTION_EMPLOYEES || 'employees',
|
||
portalMessages: process.env.APPWRITE_COLLECTION_PORTAL_MESSAGES || 'portalMessages',
|
||
subscriptions: process.env.APPWRITE_COLLECTION_SUBSCRIPTIONS || 'subscriptions',
|
||
hourCredits: process.env.APPWRITE_COLLECTION_HOUR_CREDITS || 'hourCredits',
|
||
takeoverCredits: process.env.APPWRITE_COLLECTION_TAKEOVER_CREDITS || 'takeoverCredits',
|
||
aboOffers: process.env.APPWRITE_COLLECTION_ABO_OFFERS || 'aboOffers',
|
||
contractEvents: process.env.APPWRITE_COLLECTION_CONTRACT_EVENTS || 'contractEvents',
|
||
newsPosts: process.env.APPWRITE_COLLECTION_NEWS_POSTS || 'newsPosts',
|
||
customerViewLog: process.env.APPWRITE_COLLECTION_CUSTOMER_VIEW_LOG || 'customerViewLog',
|
||
// Finanzen (Bankkonto, Buchungen, Einstellungen, Fixkosten)
|
||
bankConnections: process.env.APPWRITE_COLLECTION_BANK_CONNECTIONS || 'bankConnections',
|
||
bankTransactions: process.env.APPWRITE_COLLECTION_BANK_TRANSACTIONS || 'bankTransactions',
|
||
financeSettings: process.env.APPWRITE_COLLECTION_FINANCE_SETTINGS || 'financeSettings',
|
||
financeFixedCosts: process.env.APPWRITE_COLLECTION_FINANCE_FIXED_COSTS || 'financeFixedCosts',
|
||
// Terminplaner (Einstellungen, Buchungen, Belegungszellen, Mail-Warteschlange; scripts/termine-setup.mjs)
|
||
termineConfig: process.env.APPWRITE_COLLECTION_TERMINE_CONFIG || 'termineConfig',
|
||
termineBookings: process.env.APPWRITE_COLLECTION_TERMINE_BOOKINGS || 'termineBookings',
|
||
termineClaims: process.env.APPWRITE_COLLECTION_TERMINE_CLAIMS || 'termineClaims',
|
||
termineOutbox: process.env.APPWRITE_COLLECTION_TERMINE_OUTBOX || 'termineOutbox',
|
||
},
|
||
|
||
publicBaseUrl: (process.env.PORTAL_PUBLIC_URL || 'https://project.webklar.com').replace(/\/$/, ''),
|
||
// Ticketsystem (WOMS), z. B. fuer den Link "Ticket oeffnen" in der Push-Zeitmail
|
||
womsBaseUrl,
|
||
|
||
// Push-Zeitmail: signierte Links zum Eintragen der Zeit eines Git-Pushes (ohne Login)
|
||
pushTime: {
|
||
secret: process.env.PUSH_TIME_SECRET || process.env.SESSION_SECRET || '',
|
||
ttlDays: 14,
|
||
},
|
||
|
||
products,
|
||
mailStorage,
|
||
newsSetup,
|
||
// Funktionen-Tab (News-Pinnwand): Zeiteinschaetzung auf der Buchungsseite und Wortlaut der Zustimmung,
|
||
// der mit Zeitstempel am Feature-Dokument gespeichert wird (Widerruf, § 356 Abs. 4 BGB)
|
||
funktionen: {
|
||
setupEstimate: 'meist innerhalb von 1–2 Wochen',
|
||
consentText:
|
||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).',
|
||
},
|
||
|
||
// Wartungs-Abo (Stripe Billing). Preis/Produkt legt der Server per lookup_key selbst an.
|
||
// Die Werte kommen aus dem Produktkatalog (products.wartung).
|
||
stripe: {
|
||
secretKey: process.env.STRIPE_SECRET_KEY || '',
|
||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||
// Nur für Tests: Stripe-Nachbau statt api.stripe.com (z. B. http://127.0.0.1:12111)
|
||
apiBase: process.env.STRIPE_API_BASE || '',
|
||
priceLookupKey: products.wartung.lookupKey,
|
||
priceAmountCents: products.wartung.amountCents,
|
||
productName: products.wartung.name,
|
||
statementDescriptor: products.wartung.statementDescriptor,
|
||
// Generisches Logo, allen drei Stripe-Produkten mitgegeben (ensurePrice); ein Katalogeintrag
|
||
// kann das mit einem eigenen "image"-Feld ueberschreiben
|
||
productImage: 'https://project.webklar.com/img/wartungs-abo-produkt.png',
|
||
// Kleinunternehmer: Pflichthinweis auf jeder Stripe-Rechnung
|
||
invoiceFooter:
|
||
process.env.STRIPE_INVOICE_FOOTER ||
|
||
'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet (Kleinunternehmerregelung). WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler',
|
||
},
|
||
|
||
// Finom-Geschaeftskonto lesend ueber Enable Banking (PSD2). Der private Schluessel liegt als Datei auf dem
|
||
// Server (nie im Repo); der Client liest ihn bei jedem Aufruf, ein Schluesseltausch wirkt ohne Neustart.
|
||
enableBanking: {
|
||
appId: enableBankingAppId,
|
||
keyPath: enableBankingKeyPath,
|
||
apiBase: resolveEnableBankingApiBase(process.env.ENABLE_BANKING_API_BASE),
|
||
redirectUrl: `${womsBaseUrl}/finanzen/bank-callback`,
|
||
aspsp: { name: 'Finom', country: 'DE' },
|
||
enabled: Boolean(enableBankingAppId && enableBankingKeyPath),
|
||
},
|
||
|
||
abo: {
|
||
// Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert)
|
||
termsVersion: '2026-09-30-v5',
|
||
// Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich
|
||
minTermMonths: products.wartung.minTermMonths,
|
||
minutesPerMonth: products.wartung.minutesPerMonth,
|
||
creditValidityMonths: 12,
|
||
usageRoundingMinutes: 15,
|
||
// Widerrufsfrist und "Beginn nach Ablauf der Widerrufsfrist": server/services/deadlines.js
|
||
// Pilotbetrieb: nur diese Kunden-E-Mails duerfen bestellen (leer = alle)
|
||
pilotEmails: (process.env.ABO_PILOT_EMAILS || '')
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
// Testkonten zahlen nur den Stripe-Mindestbetrag (0,50 €) ueber einen Rabatt auf den echten Preis
|
||
testEmails: (process.env.ABO_TEST_EMAILS || '')
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
// Fix round 1, Minor 8: einzige Quelle ist der Katalog (products.wartung.testCouponId); Hosting
|
||
// hat sein eigenes unter products.hosting.testCouponId
|
||
testCouponId: products.wartung.testCouponId,
|
||
},
|
||
|
||
// Individuelle Abo-Angebote (z. B. erste 2 Monate je 25 €): je Angebot ein einmal einloesbarer Stripe-Coupon
|
||
offers: {
|
||
maxMonths: 12,
|
||
defaultValidityDays: 30,
|
||
maxValidityDays: 366,
|
||
},
|
||
|
||
// Uebernahmegutschrift: beim bisherigen Anbieter bereits bezahlte Zeit wird mit den Abo-Gebuehren verrechnet
|
||
takeover: {
|
||
// Bei jeder Textaenderung an den Bedingungen der Gutschrift hochziehen (wird mit dem Antrag gespeichert).
|
||
// Entwurf vom 27.09.2026 (Gutschrift fuer beide Abos); das Freigabedatum wird beim Livegang eingetragen.
|
||
conditionsVersion: '2026-09-30',
|
||
// Nachweise liegen ausserhalb von public/ (gitignored, ueberlebt Deploys wie preview-data/)
|
||
uploadDir: process.env.TAKEOVER_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'takeover-credits'),
|
||
maxFiles: 3,
|
||
maxFileBytes: 8 * 1024 * 1024,
|
||
maxTotalBytes: 10 * 1024 * 1024,
|
||
// Plausibilitaetsgrenze fuer eine einzelne Gutschrift (Tippfehler abfangen)
|
||
maxAmountCents: 100000,
|
||
maxOpenPerCustomer: 3,
|
||
// Nachweise abgelehnter Antraege werden nach dieser Frist geloescht (Datenschutzerklaerung Abschnitt 7)
|
||
rejectedRetentionDays: 183,
|
||
},
|
||
|
||
// News-Pinnwand: Fotos liegen ausserhalb von public/ (gitignored, ueberleben Deploys)
|
||
news: {
|
||
uploadDir: process.env.NEWS_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'news'),
|
||
},
|
||
|
||
// Chat-Anhaenge (Bilder, Dateien, Kamerafotos, Sprachnachrichten): liegen ausserhalb von public/
|
||
// (gitignored, ueberleben Deploys wie preview-data/)
|
||
chat: {
|
||
uploadDir: process.env.CHAT_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'chat'),
|
||
maxFileBytes: 25 * 1024 * 1024,
|
||
maxFilesPerMessage: 10,
|
||
// Uploads werden im Speicher gepuffert: hoechstens so viele gleichzeitig, weitere warten
|
||
maxParallelUploads: 4,
|
||
// Missbrauchsschutz: Uploads je Kunde und Stunde, Gesamtspeicher je Kunde, freier Platz auf dem Server
|
||
maxUploadsPerHour: 200,
|
||
maxBytesPerCustomer: 1024 * 1024 * 1024,
|
||
minFreeDiskBytes: 2 * 1024 * 1024 * 1024,
|
||
// Hochgeladen, aber nie abgeschickt: nach dieser Zeit loeschen (Datenschutzerklaerung Abschnitt 6)
|
||
pendingTtlHours: 24,
|
||
},
|
||
|
||
smtp: {
|
||
host: process.env.SMTP_HOST || '',
|
||
port: Number(process.env.SMTP_PORT) || 587,
|
||
secure: (process.env.SMTP_SECURE || 'false') === 'true',
|
||
user: process.env.SMTP_USERNAME || '',
|
||
pass: process.env.SMTP_PASSWORD || '',
|
||
from: process.env.SMTP_FROM || process.env.SMTP_USERNAME || '',
|
||
// Antworten auf Kundenmails landen hier statt im Versandpostfach (noreply@)
|
||
replyTo: process.env.SMTP_REPLY_TO || '',
|
||
},
|
||
|
||
chatNotify: {
|
||
emails: (process.env.CHAT_NOTIFY_EMAILS || 'kenso@webklar.com,andrej@webklar.com')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
throttleMin: Number(process.env.CHAT_NOTIFY_THROTTLE_MIN) || 15,
|
||
},
|
||
|
||
gitea: {
|
||
webhookToken: process.env.GITEA_WEBHOOK_TOKEN || '',
|
||
baseUrl: process.env.GITEA_BASE_URL || 'https://git.webklar.com',
|
||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||
templateOwner: process.env.GITEA_TEMPLATE_OWNER || 'knso',
|
||
templateRepo: process.env.GITEA_TEMPLATE_REPO || 'webklar-preview-template',
|
||
defaultOwner: process.env.GITEA_DEFAULT_OWNER || 'knso',
|
||
// Ziel-Organisation des Umzugs (siehe docs/superpowers/plans/2026-09-27-gitea-organisation-1-kundenportal.md)
|
||
org: process.env.GITEA_ORG || 'WEBklar',
|
||
// Login des System-Kontos, das automatische Commits/Pushes ausfuehrt (kein Arbeitsblatt dafuer)
|
||
systemUser: process.env.GITEA_SYSTEM_USER || 'webklar-system',
|
||
// Interne Repos, die trotz Zuweisung nicht nach GITEA_ORG umziehen
|
||
keepInPlace: (
|
||
process.env.GITEA_KEEP_IN_PLACE ||
|
||
'ANDJ/Emailsorter,ANDJ/Steuerungstechnik,JUSN/tickte-system,JUSN/Webklar,knso/eship,knso/Webklar-Kundenbereich'
|
||
)
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
// Beim Zuweisen eines Projekts werden Pushes der letzten so-vielen Tage nachgetragen
|
||
backfillDays: Number(process.env.GITEA_BACKFILL_DAYS) || 30,
|
||
// Zeitlimit fuer jeden Gitea-API-Aufruf (giteaFetch, fetchPreviewConfig); verhindert ein
|
||
// haengendes Portal, wenn Gitea nicht antwortet (Controller-Ruling final-fix Punkt 1).
|
||
timeoutMs: Number(process.env.GITEA_TIMEOUT_MS) || 30000,
|
||
},
|
||
|
||
cors: {
|
||
allowedOrigins: (process.env.ADMIN_CORS_ORIGINS || 'https://ticket.webklar.com,http://localhost:5173')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
},
|
||
|
||
preview: {
|
||
baseHost: process.env.PREVIEW_BASE_HOST || 'project.webklar.com',
|
||
deployRoot: process.env.PREVIEW_DEPLOY_ROOT || '',
|
||
traefikDynamicDir: process.env.TRAEFIK_DYNAMIC_DIR || '',
|
||
sessionSecret:
|
||
process.env.PREVIEW_SESSION_SECRET || process.env.SESSION_SECRET || '',
|
||
cookieDomain: process.env.PREVIEW_COOKIE_DOMAIN || '.project.webklar.com',
|
||
sessionMaxAge: Number(process.env.PREVIEW_SESSION_MAX_AGE || 60 * 60 * 24 * 7),
|
||
},
|
||
|
||
previewRunner: {
|
||
enabled: process.env.PREVIEW_RUNNER === 'on',
|
||
url: process.env.PREVIEW_RUNNER_URL || 'http://webklar-preview-runner:8080',
|
||
token: process.env.PREVIEW_RUNNER_TOKEN || '',
|
||
logDir: process.env.PREVIEW_LOG_DIR || path.join(process.cwd(), 'upload-data', 'preview-logs'),
|
||
},
|
||
|
||
analyse: {
|
||
enabled: process.env.ANALYSE_ENABLED === 'on',
|
||
url: process.env.ANALYTICS_URL || 'http://webklar-analytics:8091',
|
||
adminToken: process.env.ANALYTICS_ADMIN_TOKEN || '',
|
||
heatSecret: process.env.ANALYTICS_HEAT_SECRET || '',
|
||
avvVersion: '2026-10-02',
|
||
},
|
||
|
||
// Terminplaner (Funktionsschluessel 'termine'). Wird bei jedem Aufruf gelesen (Tests aendern die Werte zur
|
||
// Laufzeit); Schalter siehe server/services/termine/schalter.js
|
||
termine: {
|
||
// off (Vorgabe) | pilot (nur TERMINE_PILOT_EMAILS) | on (alle Kunden)
|
||
mode: ['pilot', 'on'].includes(termineEnabled) ? termineEnabled : 'off',
|
||
// Kunden-E-Mails fuer den Pilotbetrieb; '*' = alle
|
||
pilotEmails: (process.env.TERMINE_PILOT_EMAILS || '')
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
holdMinutes: 30,
|
||
zelleMin: 5,
|
||
mailsProStunde: 200,
|
||
// Hoechstens so viele Buchungen loescht ein Schritt der Aufbewahrungs-Loeschung je Lauf (Tagesjob); der Rest folgt am naechsten Tag
|
||
loeschDeckel: 2000,
|
||
// Mindestzeit zwischen Laden und Absenden des Buchungsformulars (schneller = Bot)
|
||
formMinSekunden: nonNegativeNumber(process.env.TERMINE_FORM_MIN_SECONDS, 3),
|
||
// Kuerzeste Antwortzeit (ms) von POST /buchungen und den Widerrufs-POSTs der oeffentlichen Schnittstelle: verwischt, ob
|
||
// eine Adresse oder Buchung bekannt ist (Zeitunterschied). Tests setzen 0.
|
||
minAntwortMs: nonNegativeNumber(process.env.TERMINE_MIN_ANTWORT_MS, 300),
|
||
},
|
||
|
||
invoiceNinja: {
|
||
url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''),
|
||
apiToken: process.env.INVOICE_NINJA_API_TOKEN || '',
|
||
},
|
||
}
|
||
|
||
export function assertServerConfig() {
|
||
if (!config.sessionSecret || config.sessionSecret.length < 32) {
|
||
console.warn('[config] SESSION_SECRET fehlt oder ist zu kurz (min. 32 Zeichen).')
|
||
}
|
||
if (!config.pushTime.secret || config.pushTime.secret.length < 32) {
|
||
console.warn('[config] PUSH_TIME_SECRET (bzw. SESSION_SECRET) fehlt oder ist zu kurz – Zeit-Links aus Push-Mails funktionieren nicht.')
|
||
}
|
||
if (!config.appwrite.apiKey) {
|
||
console.warn('[config] APPWRITE_API_KEY fehlt – DB-Zugriff und Webhook schlagen fehl.')
|
||
}
|
||
}
|