Analyse: Routen fuer Portal und WOMS

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 10:19:37 +00:00
1 parent 696f17b470
commit f4c8b291d7
7 files changed
+416 -16

No files matched your search

+126 -2
View File
@@ -14,18 +14,54 @@ import {
runActivation,
} from '../services/siteFeatures.js'
import {
ELIGIBILITY_MESSAGES,
activateAnalyse,
analyseDeps,
analyseDomains,
analyseEligibility,
deactivateAnalyse,
} from '../services/analyse.js'
const router = Router()
const DAY_MS = 24 * 60 * 60 * 1000
// Eigene Fehler (httpError) tragen einen Text fuer den Kunden. Fehler von Appwrite (err.type) oder
// unerwartete Fehler werden nur geloggt; der Kunde sieht einen verstaendlichen Hinweis statt der Rohmeldung.
function fail(res, err, fallback) {
const own = err.status >= 400 && err.status < 500 && !err.type
const own = ((err.status >= 400 && err.status < 500) || err.status === 503) && !err.type
if (own) return res.status(err.status).json({ error: err.message })
console.error('[funktionen]', err.status || '', err.type || '', err.message)
return res.status(500).json({ error: fallback })
}
const ANALYSE_OFF = 'Website-Analyse ist noch nicht freigeschaltet'
const ANALYSE_ERROR = 'Die Website-Analyse konnte gerade nicht geändert werden. Bitte versuch es später noch einmal oder schreib uns im Chat.'
async function analyseView(customerId, projectId) {
if (!config.analyse.enabled) return { comingSoon: true }
const [doc, eligibility] = await Promise.all([getFeatureDoc(customerId, projectId, 'analyse'), analyseEligibility(customerId, projectId)])
return {
state: doc?.state || 'off',
accentColor: doc?.accentColor || '#111111',
eligible: eligibility.ok,
reason: eligibility.reason || null,
reasonText: eligibility.reason ? ELIGIBILITY_MESSAGES[eligibility.reason] : null,
avvVersion: config.analyse.avvVersion,
}
}
// Schalter aus: alle Analyse-Routen 409
function requireAnalyseEnabled(_req, res, next) {
if (!config.analyse.enabled) return res.status(409).json({ error: ANALYSE_OFF })
next()
}
function analyseProjectId(value) {
if (typeof value !== 'string' || !value) throw Object.assign(new Error('Bitte wähle ein Projekt.'), { status: 400 })
return value
}
// Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet
router.get('/', requireSession, async (req, res) => {
try {
@@ -33,8 +69,10 @@ router.get('/', requireSession, async (req, res) => {
// Ein Fehler beim Freischalten (z. B. Ticketsystem) darf die Liste nicht verhindern
await runActivation({ customerId }).catch((err) => console.error('[funktionen] Freischalten fehlgeschlagen:', err.message))
await backfillInvoiceLinks(customerId)
const projects = await listCustomerFeatures(customerId)
for (const p of projects) p.analyse = await analyseView(customerId, p.projectId)
res.json({
projects: await listCustomerFeatures(customerId),
projects,
setupEstimate: config.funktionen.setupEstimate,
consentText: config.funktionen.consentText,
})
@@ -110,4 +148,90 @@ router.post('/news/checkout', requireSession, async (req, res) => {
}
})
const analyseGuard = [requireSession, requireAnalyseEnabled]
router.post('/analyse/activate', analyseGuard, async (req, res) => {
try {
if (req.session.viewAs) {
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nichts geändert werden.' })
}
const { projectId, accentColor, consent } = req.body || {}
const { doc } = await activateAnalyse({
customerId: getSessionCustomerId(req),
projectId: analyseProjectId(projectId),
accentColor,
consent,
by: 'customer',
})
res.json({ ok: true, state: doc.state })
} catch (err) {
fail(res, err, ANALYSE_ERROR)
}
})
router.post('/analyse/deactivate', analyseGuard, async (req, res) => {
try {
if (req.session.viewAs) {
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nichts geändert werden.' })
}
const { doc } = await deactivateAnalyse({
customerId: getSessionCustomerId(req),
projectId: analyseProjectId(req.body?.projectId),
by: 'customer',
})
res.json({ ok: true, state: doc?.state || 'off' })
} catch (err) {
fail(res, err, ANALYSE_ERROR)
}
})
// Eigenes Projekt und Analyse-Dokument mit siteKey (aktiv oder aus: Daten liegen noch bis zu 60 Tage)
async function ownAnalyseDoc(req, projectId) {
const customerId = getSessionCustomerId(req)
const project = await assertOwnProject(customerId, analyseProjectId(projectId))
const doc = await getFeatureDoc(customerId, projectId, 'analyse')
return { project, doc }
}
router.get('/analyse/stats', analyseGuard, async (req, res) => {
try {
const range = String(req.query.range || '30')
if (!['7', '30', '90'].includes(range)) return res.status(400).json({ error: 'Ungültiger Zeitraum' })
const { doc } = await ownAnalyseDoc(req, req.query.projectId)
if (!doc?.siteKey || !['active', 'off'].includes(doc.state)) {
return res.status(409).json({ error: 'Für dieses Projekt gibt es keine Auswertung.' })
}
let stats
try {
stats = await analyseDeps.client.getStats(doc.siteKey, Number(range))
} catch (err) {
console.error('[funktionen] Auswertung:', err.status || '', err.message)
return res.status(503).json({ error: 'Auswertung gerade nicht erreichbar' })
}
res.json(stats)
} catch (err) {
fail(res, err, 'Die Auswertung konnte gerade nicht geladen werden.')
}
})
router.post('/analyse/heatmap-link', analyseGuard, async (req, res) => {
try {
const { projectId, path, vp } = req.body || {}
if (typeof path !== 'string' || !path.startsWith('/') || path.length > 300 || /[?#]/.test(path)) {
return res.status(400).json({ error: 'Ungültiger Pfad' })
}
if (!['m', 't', 'd'].includes(vp)) return res.status(400).json({ error: 'Ungültige Ansicht' })
const { project, doc } = await ownAnalyseDoc(req, projectId)
if (!doc?.siteKey || doc.state !== 'active') {
return res.status(409).json({ error: 'Die Website-Analyse ist für dieses Projekt nicht aktiv.' })
}
const domains = analyseDomains(project)
const host = project.liveDomain ? domains[domains.length - 1] : domains[0]
const token = analyseDeps.client.heatToken({ siteKey: doc.siteKey, path, vp })
res.json({ url: `https://${host}${path}?__wk_heat=${encodeURIComponent(token)}` })
} catch (err) {
fail(res, err, 'Der Heatmap-Link konnte gerade nicht erstellt werden.')
}
})
export default router