import { useState, useEffect, useCallback, useRef } from 'react' import { modeOptionsFor, previewStatus, modeHint, isDevUnavailableError, NO_DEV_HINT } from '../lib/previewView' import { isValidEnvName, buildEnvPayload, isDevPreview, envValueTooLong, MAX_ENV_VARS } from '../lib/previewEnv' import { fetchPreview, setPreviewMode, savePreviewEnv, rebuildPreview, startPreviewDev, stopPreviewDev, fetchPreviewLog, } from '../lib/previewApi' import { Button, Badge } from './ui' import { previewColorTone } from './projectTones' const NOT_ACTIVE = 'Preview-Runner ist nicht aktiv' const logDate = (at) => { const d = new Date(at) if (!at || Number.isNaN(d.getTime())) return String(at || '') return d.toLocaleString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) } export default function PreviewBlock({ projectId }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [inactive, setInactive] = useState(false) const [error, setError] = useState('') const [busy, setBusy] = useState(false) const [logText, setLogText] = useState('') const [logFile, setLogFile] = useState('') const [showLogs, setShowLogs] = useState(false) // Variablen: Änderungen werden gesammelt und mit "Variablen speichern" übertragen const [pendingSet, setPendingSet] = useState({}) const [pendingRemove, setPendingRemove] = useState([]) const [newName, setNewName] = useState('') const [newValue, setNewValue] = useState('') const [editName, setEditName] = useState('') const [editValue, setEditValue] = useState('') const [envError, setEnvError] = useState('') const alive = useRef(true) useEffect(() => { alive.current = true return () => { alive.current = false } }, []) const handleError = useCallback((err) => { if (!alive.current) return if (err?.status === 409 && err.message === NOT_ACTIVE) { setInactive(true) return } setError(err?.message || 'Unbekannter Fehler') }, []) const load = useCallback(async () => { try { const r = await fetchPreview(projectId) if (!alive.current) return setData(r) setInactive(false) setError('') } catch (err) { handleError(err) } finally { if (alive.current) setLoading(false) } }, [projectId, handleError]) useEffect(() => { load() }, [load]) // Solange gebaut/gestartet wird: alle 5 s nachladen const state = data?.state useEffect(() => { if (state !== 'building' && state !== 'starting') return undefined const t = setInterval(load, 5000) return () => clearInterval(t) }, [state, load]) const run = async (fn) => { setBusy(true) setError('') try { await fn() await load() } catch (err) { handleError(err) } finally { if (alive.current) setBusy(false) } } const openLog = (runFile) => { setLogFile(runFile) setLogText('') if (!runFile) return setBusy(true) setError('') fetchPreviewLog(projectId, runFile) .then((r) => { if (alive.current) setLogText(Array.isArray(r.lines) ? r.lines.join('\n') : String(r.log ?? r.text ?? '')) }) .catch((err) => { handleError(err) if (alive.current) setLogText(`Fehler: ${err?.message || 'Protokoll konnte nicht geladen werden'}`) }) .finally(() => { if (alive.current) setBusy(false) }) } const addPending = (name, value) => { setPendingSet((p) => ({ ...p, [name]: value })) setPendingRemove((r) => r.filter((x) => x !== name)) } const handleAdd = () => { if (!isValidEnvName(newName)) { setEnvError('Name nur mit A–Z, 0–9 und _ (nicht mit einer Ziffer beginnen).') return } if (!newValue) { setEnvError('Bitte einen Wert eingeben.') return } if (envValueTooLong(newValue)) { setEnvError('Der Wert ist zu lang (max. 4096 Byte).') return } if (!names.includes(newName) && names.length >= MAX_ENV_VARS) { setEnvError(`Maximal ${MAX_ENV_VARS} Variablen möglich.`) return } setEnvError('') addPending(newName, newValue) setNewName('') setNewValue('') } const handleRemove = (name) => { setPendingSet((p) => { const { [name]: _drop, ...rest } = p return rest }) if ((data?.envNames || []).includes(name)) setPendingRemove((r) => [...new Set([...r, name])]) } const handleChange = () => { if (!editValue) { setEnvError('Bitte einen neuen Wert eingeben.') return } if (envValueTooLong(editValue)) { setEnvError('Der Wert ist zu lang (max. 4096 Byte).') return } setEnvError('') addPending(editName, editValue) setEditName('') setEditValue('') } const handleSaveEnv = () => run(async () => { await savePreviewEnv(projectId, buildEnvPayload(pendingSet, pendingRemove)) setPendingSet({}) setPendingRemove([]) }) const enter = (fn) => (e) => { if (e.key === 'Enter') { e.preventDefault() fn() } } if (loading) return

Lädt …

const view = data ? { ...data } : { state: 'off' } const status = previewStatus(view) const mode = data?.mode || 'auto' const hint = modeHint(mode, data?.detected) const names = [...new Set([...(data?.envNames || []), ...Object.keys(pendingSet)])].filter((n) => !pendingRemove.includes(n)) const dirty = Object.keys(pendingSet).length > 0 || pendingRemove.length > 0 const dev = isDevPreview(data) const logs = [...(data?.logs || [])].sort((a, b) => new Date(b.at) - new Date(a.at)).slice(0, 5) return (
{data && ( <>
{data.devAvailable === false &&

{NO_DEV_HINT}

} {hint &&

{hint}

}
{status.text}
{data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && (

{data.error}

)} )} {inactive &&

{NOT_ACTIVE}.

} {error &&

{error}

} {data && !inactive && ( <>
{dev && ( <> )}
{showLogs && ( <> {logFile &&
{logText || '…'}
} )}

Variablen

{names.length === 0 &&

Keine Variablen.

} {names.map((n) => (
{n} ••• {editName === n ? ( <> setEditValue(e.target.value)} /> ) : ( )}
))}
setNewName(e.target.value)} /> setNewValue(e.target.value)} />
{envError &&

{envError}

} {dirty && (
)}

Alles mit VITE_ / NEXT_PUBLIC_ ist später im Browser sichtbar.

)}
) }