diff --git a/src/components/AnalyseRow.jsx b/src/components/AnalyseRow.jsx
new file mode 100644
index 0000000..096976e
--- /dev/null
+++ b/src/components/AnalyseRow.jsx
@@ -0,0 +1,83 @@
+import { useState, useEffect } from 'react'
+import { fetchAnalyse, setAnalyse } from '../lib/analyseApi'
+import { analyseStatus, avvLabel } from '../lib/analyseView'
+
+const COLORS = { green: '#10b981', red: '#ef4444', grey: '#a0aec0' }
+
+// Kompakte Zeile "Website-Analyse" je Projekt (lädt beim Einbinden; mit key=Projekt-ID verwenden)
+export default function AnalyseRow({ projectId }) {
+ const [data, setData] = useState(null)
+ const [loadError, setLoadError] = useState('')
+ const [mode, setMode] = useState(null) // null | 'on' | 'off'
+ const [confirmed, setConfirmed] = useState(false)
+ const [busy, setBusy] = useState(false)
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ let cancelled = false
+ fetchAnalyse(projectId)
+ .then((r) => { if (!cancelled) setData(r) })
+ .catch((err) => { if (!cancelled) setLoadError(err.message) })
+ return () => { cancelled = true }
+ }, [projectId])
+
+ const apply = async (on) => {
+ setBusy(true)
+ setError('')
+ try {
+ await setAnalyse(projectId, { on, avvConfirmedVerbally: on })
+ setData(await fetchAnalyse(projectId))
+ setMode(null)
+ setConfirmed(false)
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ const cancel = () => { setMode(null); setConfirmed(false); setError('') }
+ const status = analyseStatus(data)
+
+ return (
+
+
+ Website-Analyse:
+ {loadError ? (
+ {loadError}
+ ) : data ? (
+ {status.text}
+ ) : (
+ Lädt …
+ )}
+ {status.canEnable && !mode && (
+
+ )}
+ {status.canDisable && !mode && (
+
+ )}
+
+
+ {mode === 'on' && data && (
+
+
+
+
+
+ )}
+
+ {mode === 'off' && (
+
+ Website-Analyse wirklich ausschalten?
+
+
+
+ )}
+
+ {error &&
{error}
}
+
+ )
+}
diff --git a/src/components/AssignedProjectCard.jsx b/src/components/AssignedProjectCard.jsx
index b68023a..923d485 100644
--- a/src/components/AssignedProjectCard.jsx
+++ b/src/components/AssignedProjectCard.jsx
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { FaCodeBranch } from 'react-icons/fa'
import { FaLinkSlash } from 'react-icons/fa6'
import PreviewBlock from './PreviewBlock'
+import AnalyseRow from './AnalyseRow'
import GiteaLinkButton from './GiteaLinkButton'
import PreviewLinkButton from './PreviewLinkButton'
import { suggestSubdomain } from '../lib/subdomain'
@@ -112,6 +113,8 @@ export default function AssignedProjectCard({ project, onUnassign, showActions =
+ {project.subdomain && }
+
{showActions && project.subdomain && (
{[p.customerName, p.previewUrl && 'Preview'].filter(Boolean).join(' · ') || 'Intern'}
diff --git a/tests/unit/analyseView.test.mjs b/tests/unit/analyseView.test.mjs
new file mode 100644
index 0000000..5e19bbb
--- /dev/null
+++ b/tests/unit/analyseView.test.mjs
@@ -0,0 +1,49 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { loadModule } from './load.mjs'
+
+const { analyseStatus, avvLabel } = await loadModule('src/lib/analyseView.js')
+
+test('analyseStatus: Schalter aus => noch nicht freigeschaltet, kein Button', () => {
+ const s = analyseStatus({ enabled: false })
+ assert.equal(s.text, 'Noch nicht freigeschaltet')
+ assert.equal(s.canEnable, false)
+ assert.equal(s.canDisable, false)
+})
+
+test('analyseStatus: an seit Datum (Europe/Berlin)', () => {
+ const s = analyseStatus({ enabled: true, state: 'active', eligible: true, activatedAt: '2026-10-01T22:30:00Z' })
+ assert.equal(s.text, 'An seit 02.10.2026')
+ assert.equal(s.canDisable, true)
+ assert.equal(s.canEnable, false)
+})
+
+test('analyseStatus: an ohne Datum bleibt lesbar', () => {
+ assert.equal(analyseStatus({ enabled: true, state: 'active', eligible: true }).text, 'An')
+})
+
+test('analyseStatus: aus und berechtigt => Einschalten moeglich', () => {
+ const s = analyseStatus({ enabled: true, state: 'off', eligible: true })
+ assert.equal(s.text, 'Aus')
+ assert.equal(s.canEnable, true)
+ assert.equal(analyseStatus({ enabled: true, state: null, eligible: true }).text, 'Aus')
+})
+
+test('analyseStatus: gesperrt', () => {
+ const s = analyseStatus({ enabled: true, state: 'disabled', eligible: true })
+ assert.equal(s.text, 'Gesperrt')
+})
+
+test('analyseStatus: nicht berechtigt => Grund, kein Button', () => {
+ const s = analyseStatus({ enabled: true, state: null, eligible: false, reasonText: 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.' })
+ assert.equal(s.text, 'Nicht verfügbar: Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.')
+ assert.equal(s.canEnable, false)
+})
+
+test('analyseStatus: robust bei leerer Antwort', () => {
+ assert.equal(analyseStatus(null).text, '')
+})
+
+test('avvLabel nennt die Fassung', () => {
+ assert.equal(avvLabel('2026-10-01'), 'Der Kunde hat den Auftragsverarbeitungsvertrag (Fassung 2026-10-01) bestätigt')
+})
diff --git a/tests/unit/servicesView.test.mjs b/tests/unit/servicesView.test.mjs
index ef9762e..513b285 100644
--- a/tests/unit/servicesView.test.mjs
+++ b/tests/unit/servicesView.test.mjs
@@ -38,3 +38,8 @@ test('servicesCards: robust bei leerer Antwort und unbekannter Funktion', () =>
const cards = servicesCards({ kinds: [{ key: 'x', label: 'X', features: ['foo'] }] })
assert.deepEqual(cards[0].features, [{ label: 'foo', statusLabel: '' }])
})
+
+test('servicesCards: Status "available" => "Verfügbar"', () => {
+ const cards = servicesCards({ kinds, features: { analyse: { label: 'Website-Analyse', status: 'available' } }, groups: {} })
+ assert.deepEqual(cards[0].features, [{ label: 'Website-Analyse', statusLabel: 'Verfügbar' }])
+})