feat: Jetzt-hosten-Button + Deploy-Status auf Projektkarten

- Karten zeigen fuer Hosting-Typen (Preview/Website/Vorlage) einen Status:
  gehostet (ready/deployed) vs nicht deployt (synced etc.)
- nicht-deployte Projekte: Button "Jetzt hosten" -> deployt via PATCH-Route
  (setzt status=deployed/ready), danach funktioniert die Preview
- deployte Projekte zeigen weiterhin den Preview-Link

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-12 09:32:04 +00:00
parent 9e4c60fdaf
commit 612063455e

View File

@@ -75,6 +75,16 @@ function typeOf(project) {
return 'project'
}
// Typen mit Hosting (Subdomain + Deploy)
const HOSTED_TYPES = new Set(['preview', 'website', 'template'])
// Gleiche Logik wie isPreviewProjectReady im Backend (auth.js):
// nur 'ready'/'deployed' gelten als tatsaechlich gehostet.
function isProjectReady(project) {
const st = String(project.provisioningStatus || project.status || '').toLowerCase()
return ['ready', 'deployed'].includes(st)
}
export default function ProjectsPage() {
const { projects, loading, error, fetchAllProjects, assignProjects, unassignProject } = useWebsiteProjects()
const { customers } = useCustomers()
@@ -93,6 +103,7 @@ export default function ProjectsPage() {
const [syncLoading, setSyncLoading] = useState(false)
const [notice, setNotice] = useState('')
const [assignBusyId, setAssignBusyId] = useState('')
const [hostBusyId, setHostBusyId] = useState('')
// Bearbeiten-Modal
const [editProject, setEditProject] = useState(null)
@@ -194,6 +205,25 @@ export default function ProjectsPage() {
setAssignBusyId('')
}
const handleHostNow = async (project) => {
setHostBusyId(project.$id)
setNotice('')
try {
// Deploy ueber die vorhandene PATCH-Route anstossen (Typ bleibt gleich)
const r = await updateWebsiteProject(project.$id, { projectType: typeOf(project) })
setNotice(
r.deploy?.deployed
? `"${project.projectName}" wird jetzt gehostet${r.previewUrl ? ': ' + r.previewUrl : ''}`
: `Deploy angestossen, aber nicht bestaetigt: ${r.deploy?.error || 'siehe Server-Logs'}`
)
await fetchAllProjects()
} catch (err) {
setNotice('Hosten fehlgeschlagen: ' + (err.message || 'unbekannt'))
} finally {
setHostBusyId('')
}
}
const openEdit = (project) => {
setEditError('')
setEditProject(project)
@@ -324,7 +354,11 @@ export default function ProjectsPage() {
}}
>
{filteredProjects.map((project) => {
const t = TYPES[typeOf(project)]
const type = typeOf(project)
const t = TYPES[type]
const hosted = HOSTED_TYPES.has(type)
const ready = isProjectReady(project)
const needsHosting = hosted && !ready && !project.archived
return (
<div
key={project.$id}
@@ -342,9 +376,16 @@ export default function ProjectsPage() {
</div>
)}
</div>
<span className={`badge ${t.badge}`} style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
{t.icon} {t.short}
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'flex-end', flexShrink: 0 }}>
<span className={`badge ${t.badge}`} style={{ whiteSpace: 'nowrap' }}>
{t.icon} {t.short}
</span>
{hosted && !project.archived && (
<span className={`badge ${ready ? 'badge-ok' : 'badge-warn'}`} style={{ whiteSpace: 'nowrap' }}>
{ready ? 'gehostet' : 'nicht deployt'}
</span>
)}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '13px' }}>
@@ -389,7 +430,20 @@ export default function ProjectsPage() {
>
<FaPen /> Bearbeiten
</button>
<PreviewLinkButton href={project.previewUrl} />
{needsHosting ? (
<button
type="button"
className="btn btn-teal"
style={{ padding: '6px 12px', fontSize: '13px' }}
onClick={() => handleHostNow(project)}
disabled={hostBusyId === project.$id}
title="Projekt jetzt deployen/hosten"
>
<FaGlobe /> {hostBusyId === project.$id ? 'Hosten…' : 'Jetzt hosten'}
</button>
) : (
<PreviewLinkButton href={project.previewUrl} />
)}
<GiteaLinkButton href={project.giteaRepoUrl} />
</div>
</div>