Preview: Anzeige-Logik und API-Client
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
471c7a056d
commit
0f884e41e8
3 files changed
+185
No files matched your search
@@ -0,0 +1,56 @@
|
|||||||
|
import { account } from './appwrite'
|
||||||
|
|
||||||
|
const PROJECT_ADMIN_URL =
|
||||||
|
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'
|
||||||
|
|
||||||
|
// Wie kindsFetch in projectKindsApi.js: der Fehler trägt .status und .data
|
||||||
|
// (z. B. 409 { error: 'Preview-Runner ist nicht aktiv' }, 503 bei Überlast).
|
||||||
|
async function previewFetch(path, options = {}) {
|
||||||
|
const jwt = await account.createJWT()
|
||||||
|
const response = await fetch(`${PROJECT_ADMIN_URL}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt.jwt}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(data.error || `API-Fehler ${response.status}`)
|
||||||
|
error.status = response.status
|
||||||
|
error.data = data
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = (projectId) => `/api/admin/website-projects/${encodeURIComponent(projectId)}/preview`
|
||||||
|
|
||||||
|
export function fetchPreview(projectId) {
|
||||||
|
return previewFetch(base(projectId))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPreviewMode(projectId, mode) {
|
||||||
|
return previewFetch(base(projectId), { method: 'PATCH', body: JSON.stringify({ mode }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePreviewEnv(projectId, { set = {}, remove = [] } = {}) {
|
||||||
|
return previewFetch(`${base(projectId)}/env`, { method: 'PUT', body: JSON.stringify({ set, remove }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rebuildPreview(projectId) {
|
||||||
|
return previewFetch(`${base(projectId)}/rebuild`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startPreviewDev(projectId) {
|
||||||
|
return previewFetch(`${base(projectId)}/dev/start`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopPreviewDev(projectId) {
|
||||||
|
return previewFetch(`${base(projectId)}/dev/stop`, { method: 'POST' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPreviewLog(projectId, runFile) {
|
||||||
|
return previewFetch(`${base(projectId)}/logs/${encodeURIComponent(runFile)}`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Anzeige-Logik für den Block "Preview" (Status, Erkennung, Modi)
|
||||||
|
|
||||||
|
export const MODE_OPTIONS = [
|
||||||
|
{ value: 'auto', label: 'Automatisch' },
|
||||||
|
{ value: 'build', label: 'Build' },
|
||||||
|
{ value: 'dev', label: 'Dev-Server' },
|
||||||
|
{ value: 'off', label: 'Aus' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dateFormat = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
timeZone: 'Europe/Berlin',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23',
|
||||||
|
})
|
||||||
|
|
||||||
|
// "TT.MM. HH:MM" in Europe/Berlin
|
||||||
|
function formatBerlin(value) {
|
||||||
|
const d = new Date(value)
|
||||||
|
if (!value || Number.isNaN(d.getTime())) return ''
|
||||||
|
const parts = Object.fromEntries(dateFormat.formatToParts(d).map((p) => [p.type, p.value]))
|
||||||
|
return `${parts.day}.${parts.month}. ${parts.hour}:${parts.minute}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previewStatus(p) {
|
||||||
|
const state = p?.state || 'off'
|
||||||
|
switch (state) {
|
||||||
|
case 'online': {
|
||||||
|
const at = formatBerlin(p.builtAt)
|
||||||
|
const sha = p.builtSha ? String(p.builtSha).slice(0, 7) : ''
|
||||||
|
if (!at) return { color: 'green', text: 'Online' }
|
||||||
|
return { color: 'green', text: `Online seit ${at}${sha ? ` (Build ${sha})` : ''}` }
|
||||||
|
}
|
||||||
|
case 'building':
|
||||||
|
return { color: 'yellow', text: 'Baut gerade …' }
|
||||||
|
case 'starting':
|
||||||
|
return { color: 'yellow', text: 'Dev-Server startet …' }
|
||||||
|
case 'sleeping':
|
||||||
|
return { color: 'yellow', text: 'Dev-Server schläft, startet beim nächsten Aufruf' }
|
||||||
|
case 'failed':
|
||||||
|
return { color: 'red', text: 'Build fehlgeschlagen – letzte funktionierende Version ist online' }
|
||||||
|
default:
|
||||||
|
return { color: 'grey', text: 'Keine Preview' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'static' | 'build:<pm>' | 'dev:<pm>' | 'build:<framework>:<pm>' ⇒ Anzeigetext
|
||||||
|
export function detectedLabel(detected) {
|
||||||
|
if (!detected || typeof detected !== 'string') return ''
|
||||||
|
if (detected === 'static') return 'Statisch'
|
||||||
|
const [kind, ...rest] = detected.split(':')
|
||||||
|
const base = kind === 'build' ? 'Build' : kind === 'dev' ? 'Dev-Server' : ''
|
||||||
|
if (!base || !rest.length || rest.some((x) => !x)) return ''
|
||||||
|
const pm = rest[rest.length - 1]
|
||||||
|
const fw = rest.length > 1 ? rest[0] : ''
|
||||||
|
const fwLabel = fw ? fw.charAt(0).toUpperCase() + fw.slice(1) : ''
|
||||||
|
return `${base} (${fwLabel ? `${fwLabel}, ` : ''}${pm})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modeHint(mode, detected) {
|
||||||
|
if ((mode || 'auto') !== 'auto') return ''
|
||||||
|
const label = detectedLabel(detected)
|
||||||
|
return label ? `Automatisch → ${label}` : ''
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
|
const { previewStatus, detectedLabel, modeHint, MODE_OPTIONS } = await loadModule('src/lib/previewView.js')
|
||||||
|
|
||||||
|
test('previewStatus online: Datum in Europe/Berlin und sha7', () => {
|
||||||
|
const s = previewStatus({ state: 'online', builtAt: '2026-09-30T13:05:00Z', builtSha: 'abcdef1234567890' })
|
||||||
|
assert.deepEqual(s, { color: 'green', text: 'Online seit 30.09. 15:05 (Build abcdef1)' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('previewStatus online im Winter (MEZ)', () => {
|
||||||
|
const s = previewStatus({ state: 'online', builtAt: '2026-01-05T08:07:00Z', builtSha: 'abcdef1' })
|
||||||
|
assert.equal(s.text, 'Online seit 05.01. 09:07 (Build abcdef1)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('previewStatus online ohne Datum/sha bleibt lesbar', () => {
|
||||||
|
assert.deepEqual(previewStatus({ state: 'online' }), { color: 'green', text: 'Online' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('previewStatus weitere Zustände', () => {
|
||||||
|
assert.deepEqual(previewStatus({ state: 'building' }), { color: 'yellow', text: 'Baut gerade …' })
|
||||||
|
assert.deepEqual(previewStatus({ state: 'starting' }), { color: 'yellow', text: 'Dev-Server startet …' })
|
||||||
|
assert.deepEqual(previewStatus({ state: 'sleeping' }), { color: 'yellow', text: 'Dev-Server schläft, startet beim nächsten Aufruf' })
|
||||||
|
assert.deepEqual(previewStatus({ state: 'failed' }), { color: 'red', text: 'Build fehlgeschlagen – letzte funktionierende Version ist online' })
|
||||||
|
assert.deepEqual(previewStatus({ state: 'off' }), { color: 'grey', text: 'Keine Preview' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('previewStatus leer/unbekannt/null ⇒ grau', () => {
|
||||||
|
const grey = { color: 'grey', text: 'Keine Preview' }
|
||||||
|
assert.deepEqual(previewStatus({ state: '' }), grey)
|
||||||
|
assert.deepEqual(previewStatus({}), grey)
|
||||||
|
assert.deepEqual(previewStatus(null), grey)
|
||||||
|
assert.deepEqual(previewStatus({ state: 'xyz' }), grey)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('detectedLabel', () => {
|
||||||
|
assert.equal(detectedLabel('static'), 'Statisch')
|
||||||
|
assert.equal(detectedLabel('build:npm'), 'Build (npm)')
|
||||||
|
assert.equal(detectedLabel('dev:pnpm'), 'Dev-Server (pnpm)')
|
||||||
|
assert.equal(detectedLabel('build:vite:npm'), 'Build (Vite, npm)')
|
||||||
|
assert.equal(detectedLabel('dev:npm'), 'Dev-Server (npm)')
|
||||||
|
assert.equal(detectedLabel(''), '')
|
||||||
|
assert.equal(detectedLabel(null), '')
|
||||||
|
assert.equal(detectedLabel('foo:npm'), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('modeHint', () => {
|
||||||
|
assert.equal(modeHint('auto', 'build:npm'), 'Automatisch → Build (npm)')
|
||||||
|
assert.equal(modeHint('auto', 'static'), 'Automatisch → Statisch')
|
||||||
|
assert.equal(modeHint('', 'dev:pnpm'), 'Automatisch → Dev-Server (pnpm)')
|
||||||
|
assert.equal(modeHint('auto', ''), '')
|
||||||
|
assert.equal(modeHint('build', 'build:npm'), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('MODE_OPTIONS', () => {
|
||||||
|
assert.deepEqual(MODE_OPTIONS, [
|
||||||
|
{ value: 'auto', label: 'Automatisch' },
|
||||||
|
{ value: 'build', label: 'Build' },
|
||||||
|
{ value: 'dev', label: 'Dev-Server' },
|
||||||
|
{ value: 'off', label: 'Aus' },
|
||||||
|
])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user