// R18: Abgleich der Preview-Zustaende mit dem Runner (alle 2 Minuten, nur mit PREVIEW_RUNNER=on). // (a) 'building' ohne laufenden Verfolger und aelter als 70 Minuten => 'failed' // (b) Dev-Projekte: Zustand laut Runner (laeuft => online/starting, sonst sleeping); Modus 'off' bleibt unberuehrt // (c) Dev-Spezifikationen, die der Runner nicht kennt (z. B. nach Neustart), erneut hinterlegen // Wirft nie: Fehler werden protokolliert und im Ergebnis gemeldet. import { config } from '../config.js' import { listDocuments, updateDocument, Query } from './appwriteAdmin.js' import * as runnerClient from './previewRunnerClient.js' import { hasLiveFollower, registerDevRound, isRunnerDev } from './previewDeploy.js' export const STALE_BUILD_MS = 70 * 60 * 1000 const STALE_TEXT = 'Status unbekannt – bitte neu bauen' export async function reconcilePreviews({ now = Date.now() } = {}) { if (!config.previewRunner.enabled) return { skipped: true } const result = { staleFailed: 0, stateUpdated: 0, reregistered: 0 } let docs try { docs = await listDocuments(config.collections.websiteProjects, [ Query.equal('previewState', ['building', 'sleeping', 'starting', 'online']), Query.limit(500), ]) } catch (err) { console.error('[previewReconcile] Projekte nicht lesbar:', err.message) return { ...result, error: err.message } } const patch = async (doc, data) => { try { await updateDocument(config.collections.websiteProjects, doc.$id, data); return true } catch (err) { console.error('[previewReconcile] Update fehlgeschlagen:', err.message) return false } } // (a) for (const doc of docs) { if (doc.previewState !== 'building' || !doc.repoFullName) continue if (hasLiveFollower(doc.repoFullName)) continue const since = Date.parse(doc.$updatedAt || doc.$createdAt || '') if (!Number.isFinite(since) || now - since <= STALE_BUILD_MS) continue if (await patch(doc, { previewState: 'failed', previewError: STALE_TEXT })) result.staleFailed++ } // (b) + (c) const devDocs = docs.filter((d) => isRunnerDev(d) && d.previewState !== 'building' && d.subdomain) if (!devDocs.length) return result let st try { st = await runnerClient.status() } catch (err) { console.error('[previewReconcile] Runner-Status nicht abrufbar:', err.message) return { ...result, error: err.message } } const entries = new Map((st?.dev || []).map((e) => [e.sub, e])) const known = new Set([...(st?.known || []), ...entries.keys()]) const unknown = [] for (const doc of devDocs) { const entry = entries.get(doc.subdomain) const want = entry?.state === 'online' ? 'online' : entry?.state === 'starting' ? 'starting' : 'sleeping' if (doc.previewState !== want && (await patch(doc, { previewState: want }))) result.stateUpdated++ if (!known.has(doc.subdomain)) unknown.push(doc) } if (unknown.length) { try { await registerDevRound(unknown); result.reregistered = unknown.length } catch (err) { console.error('[previewReconcile] Registrierung fehlgeschlagen:', err.message) } } return result } /** Fuer setInterval: nie werfen. */ export function runReconcile() { return reconcilePreviews().catch((err) => { console.error('[previewReconcile] Lauf fehlgeschlagen:', err.message) return { error: err.message } }) }