import React, { useState, useEffect, useRef } from 'react'; const AppDetail = ({ app, onBack }) => { const [formData, setFormData] = useState({ domain: '', dbPassword: '', port: app.vhost?.proxyPort || '' }); const [installing, setInstalling] = useState(false); const [logs, setLogs] = useState([]); const [status, setStatus] = useState(null); // 'success' | 'error' const logEndRef = useRef(null); const scrollToBottom = () => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [logs]); const handleInstall = async (e) => { e.preventDefault(); setInstalling(true); setLogs([]); setStatus(null); try { // Use fetch with stream reader to handle POST with SSE-like response const response = await fetch(`/api/v1/apps/${app.id}/install`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); lines.forEach(line => { if (line.trim().startsWith('data: ')) { try { const data = JSON.parse(line.trim().slice(6)); if (data.log) { setLogs(prev => [...prev, data.log]); } if (data.completed) { setStatus(data.success ? 'success' : 'error'); if (data.error) setLogs(prev => [...prev, `ERROR: ${data.error}`]); } } catch (e) { console.error('Error parsing SSE line', e); } } }); } } catch (err) { setLogs(prev => [...prev, `FATAL: ${err.message}`]); setStatus('error'); } finally { setInstalling(false); } }; return (
{/* Header */}
{app.category}
{app.id === 'forgejo' ? '🏗️' : '📦'}

{app.name}

v{app.version}

{app.description}

{app.installed ? (

Application Installed

Deployed to {app.installedInfo?.domain} on {new Date(app.installedInfo?.installedAt).toLocaleDateString()}

Open Application
) : (
{/* Install Form */}

Installation Settings

setFormData({ ...formData, domain: e.target.value })} disabled={installing} className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 transition-colors" />
{app.database && (
setFormData({ ...formData, dbPassword: e.target.value })} disabled={installing} className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 transition-colors" />
)} {app.vhost && (
setFormData({ ...formData, port: e.target.value })} disabled={installing} className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 transition-colors" />
)}
{/* Live Logs */}

Installation Logs

{logs.length === 0 && !installing && (
Logs will appear here during installation...
)} {logs.map((log, i) => (
{log}
))}
{status === 'success' && (
Installation Complete!
)} {status === 'error' && (
Installation Failed.
)}
)}
); }; export default AppDetail;