const app = document.querySelector("#app"); const toastRoot = document.querySelector("#toast-root"); const icons = { overview: '', repository: '', deploy: '', settings: '', search: '', refresh: '', folder: '', git: '', branch: '', file: '', check: '', warning: '', error: '', arrowRight: '', arrowUp: '', arrowDown: '', external: '', play: '', terminal: '', clock: '', sun: '', moon: '', plus: '', trash: '', close: '', copy: '', chevron: '', link: '', cloud: '', pulse: '', history: '', more: '', star: '', archive: '', shield: '', rocket: '', layers: '', undo: '', menu: '', download: '', server: '', key: '', update: '', wrench: '', }; function icon(name, className = "") { return ``; } function escapeHtml(value) { return String(value ?? "").replace( /[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[ character ], ); } function attr(value) { return escapeHtml(value).replace(/`/g, "`"); } function formatDate(value) { if (!value) return "Unknown"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); const diff = Date.now() - date.getTime(); if (diff < 60_000) return "just now"; if (diff < 3_600_000) return `${Math.max(1, Math.floor(diff / 60_000))}m ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)}d ago`; return date.toLocaleDateString(undefined, { day: "2-digit", month: "short", year: date.getFullYear() !== new Date().getFullYear() ? "numeric" : undefined, }); } function truncate(value, length = 76) { const text = String(value || ""); return text.length > length ? `${text.slice(0, length - 1)}…` : text; } function shortSha(value) { return String(value || "").slice(0, 7) || "—"; } function defaultWorkspaceRoot() { return ui.boot?.state?.workspaceRoots?.[0] || null; } function safeCloneFolderName(repository) { return ( String(repository?.name || "repository") .replace(/\.git$/i, "") .replace(/[^a-zA-Z0-9._-]/g, "-") || "repository" ); } function displayCloneTarget(repository) { const root = defaultWorkspaceRoot(); if (!root) return null; const separator = ui.boot?.platform === "win32" ? "\\" : "/"; return `${String(root).replace(/[\\/]+$/, "")}${separator}${safeCloneFolderName(repository)}`; } function clonePrimaryLabel(repository) { return defaultWorkspaceRoot() ? `Clone to ${safeCloneFolderName(repository)}` : "Choose project root & clone"; } function isTerminalOperation(status) { return ["success", "failed", "cancelled", "rolled-back"].includes(status); } function toneForStatus(status) { if (["success", "healthy", "complete", "rolled-back"].includes(status)) return "success"; if (["failed", "failure", "unhealthy", "danger"].includes(status)) return "danger"; if (["queued", "running", "requested", "warning", "active"].includes(status)) return "warning"; return ""; } const ui = { boot: null, repositories: [], currentView: "overview", selectedRepoId: null, repositoryTab: "changes", selectedFile: null, selectedFiles: new Set(), selectedProfileId: null, diff: "", history: [], branches: [], stashes: [], search: "", repoSearch: "", commitMessage: "", loading: false, loadingMessage: "", modal: null, setupStep: 0, systemPreflight: null, deploymentPreflight: null, serverGitVerifications: {}, deployKeyLifecycle: null, inventoryReviewPlan: null, diagnosticsStatus: null, troubleshooter: null, deploymentDiscovery: null, serverDiscovery: [], lastDiagnosticBundle: null, setupDraft: { baseUrl: "https://", token: "", user: null, roots: [], discovered: [], }, setupValidation: null, activeDeployment: null, operationPollTimer: null, inputRenderTimer: null, isMock: false, refreshError: null, refreshWarning: null, autoRefreshPending: false, repositoryRefreshPromise: null, repositoryRefreshRequest: null, deploymentTruthPromise: null, deploymentTruthRequest: null, paletteQuery: "", updateStatus: null, updateChecking: false, servers: [], serverInspection: null, gitRecovery: null, gitValidation: null, diffHunks: null, conflictState: null, branchProtection: null, pullRequests: [], auditEvents: [], }; function scheduleInputRender(delay = 120) { if (ui.inputRenderTimer) clearTimeout(ui.inputRenderTimer); ui.inputRenderTimer = setTimeout(() => { ui.inputRenderTimer = null; render(); }, delay); } function selectedRepository() { return ( ui.repositories.find( (repository) => String(repository.id) === String(ui.selectedRepoId), ) || null ); } function selectedProfile(repository = selectedRepository()) { if (!repository?.deploymentProfiles?.length) return null; return ( repository.deploymentProfiles.find( (profile) => profile.id === ui.selectedProfileId, ) || repository.deploymentProfiles.find( (profile) => profile.branch === repository.localStatus?.branch.head, ) || repository.deploymentProfiles[0] ); } function canDirectPushDeploy(repository, profile = selectedProfile(repository)) { const status = repository?.localStatus; const mode = deploymentMode(profile); return Boolean( repository?.localPath && status?.head && !status?.counts?.changed && !status?.counts?.conflicts && profile && mode === "push-bundle" && profile.branch === status.branch?.head, ); } function deploymentMode(profile) { if (profile?.provider !== "ssh-unraid") return "gitea-actions"; return ["push-bundle", "server-git", "monitor-only"].includes(profile.deploymentMode) ? profile.deploymentMode : "push-bundle"; } function deploymentTargetSha(repository, profile = selectedProfile(repository)) { return deploymentMode(profile) === "server-git" ? profile?.state?.giteaSha || null : repository?.localStatus?.head || null; } function canServerGitDeploy(repository, profile = selectedProfile(repository)) { const target = deploymentTargetSha(repository, profile); return Boolean( profile && deploymentMode(profile) === "server-git" && target && profile.branch && !(profile.state?.liveSha === target && profile.state?.healthy !== false), ); } function canDeploy(repository, profile = selectedProfile(repository)) { const mode = deploymentMode(profile); if (mode === "server-git") return canServerGitDeploy(repository, profile); if (mode === "push-bundle") return canDirectPushDeploy(repository, profile); return profile?.provider === "gitea-actions" && repository?.readyToDeploy; } function operations() { return ui.boot?.state?.operations || []; } function repositoryOperations(repository) { return operations().filter( (operation) => operation.repository === repository?.fullName, ); } function applyTheme(appearance) { const resolved = appearance === "system" ? matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark" : appearance; document.documentElement.dataset.theme = resolved || "dark"; } function showToast(title, message, type = "info") { const toast = document.createElement("div"); toast.className = `toast ${type}`; toast.innerHTML = `${icon(type === "error" ? "error" : type === "success" ? "check" : "warning")}
${escapeHtml(title)}${escapeHtml(message)}
`; toastRoot.append(toast); setTimeout(() => toast.remove(), 5600); } function isSshCredentialError(error) { const code = String(error?.code || ""); const message = String(error?.message || ""); return ["SSH_PRIVATE_KEY_READ_FAILED", "SSH_PRIVATE_KEY_NOT_FOUND", "SSH_CONNECTION_FAILED"].includes(code) || /private key|publickey|authentication methods failed|permission denied|authentication failed/i.test(message); } function setLoading(loading, message = "") { ui.loading = loading; ui.loadingMessage = message; render(); } function updateOperationInState(operation) { if (!operation || !ui.boot) return; const list = operations(); ui.boot.state.operations = [ operation, ...list.filter((item) => item.id !== operation.id), ].slice(0, 250); if (ui.activeDeployment?.id === operation.id) ui.activeDeployment = operation; } function stopOperationPolling() { if (ui.operationPollTimer) clearTimeout(ui.operationPollTimer); ui.operationPollTimer = null; } function startOperationPolling() { stopOperationPolling(); const operationId = ui.activeDeployment?.id; if (!operationId || isTerminalOperation(ui.activeDeployment.status)) return; const seconds = Math.max( 2, Number(ui.boot?.state?.preferences?.operationPollSeconds) || 3, ); ui.operationPollTimer = setTimeout(async () => { try { const operation = await window.forgeflow.refreshOperations(operationId); if (operation) updateOperationInState(operation); render(); if (operation && !isTerminalOperation(operation.status)) startOperationPolling(); else stopOperationPolling(); } catch (error) { showToast("Deployment status refresh failed", error.message, "error"); stopOperationPolling(); } }, seconds * 1000); } async function bootstrap() { try { ui.boot = await window.forgeflow.bootstrap(); ui.isMock = String(ui.boot.appVersion).includes("demo"); ui.diagnosticsStatus = ui.boot.diagnostics || null; applyTheme(ui.boot.state.appearance); ui.setupDraft.roots = [...(ui.boot.state.workspaceRoots || [])]; if (ui.boot.state.setupComplete) { await refreshRepositories(false); const reconciled = await refreshActiveOperations(false); if ( (Array.isArray(reconciled) ? reconciled : []).some((operation) => isTerminalOperation(operation.status), ) ) await refreshRepositories(false); } window.forgeflow.onRepositoriesChanged?.(() => scheduleAutoRefresh()); window.forgeflow.onOperationsChanged?.((payload) => { const changed = payload?.operations || []; for (const operation of changed) updateOperationInState(operation); if (changed.some((operation) => isTerminalOperation(operation.status))) scheduleAutoRefresh(250); render(); }); window.forgeflow.onUpdatesChanged?.((payload) => { ui.updateStatus = payload; render(); if (payload?.available) showToast( "ForgeFlow update available", `Version ${payload.remoteVersion} is ready to download.`, "success", ); }); render(); const updateResult = ui.boot.updateResult; if (updateResult?.state === "success") { const restartNote = updateResult.restartLaunched ? "" : " Automatic restart was unavailable, but the update itself succeeded."; showToast( "ForgeFlow updated successfully", `Version ${updateResult.installedVersion || updateResult.expectedVersion || ui.boot.appVersion} is installed.${restartNote}`, "success", ); } else if (updateResult?.state === "rolled-back") { showToast( "ForgeFlow update rolled back", updateResult.message || "The update failed and the previous version was restored.", "error", ); } else if (updateResult?.state === "failed") { showToast( "ForgeFlow update failed", updateResult.message || "See the update log for technical details.", "error", ); } setTimeout(() => { void refreshDeploymentTruth(false); }, 500); } catch (error) { app.innerHTML = `
${icon("error")}ForgeFlow could not start${escapeHtml(error.message)}
`; } } function scheduleAutoRefresh() { if ( ui.loading || ui.autoRefreshPending || !ui.boot?.state?.preferences?.autoRefresh ) return; ui.autoRefreshPending = true; setTimeout(async () => { ui.autoRefreshPending = false; await refreshRepositories(false, true); }, 450); } async function refreshRepositories(withLoader = true, silent = false) { ui.repositoryRefreshRequest = { withLoader: ui.repositoryRefreshRequest?.withLoader === true || withLoader, silent: ui.repositoryRefreshRequest ? ui.repositoryRefreshRequest.silent && silent : silent, }; if (ui.repositoryRefreshPromise) return ui.repositoryRefreshPromise; ui.repositoryRefreshPromise = (async () => { let result; while (ui.repositoryRefreshRequest) { const request = ui.repositoryRefreshRequest; ui.repositoryRefreshRequest = null; result = await performRepositoryRefresh(request.withLoader, request.silent); } return result; })(); try { return await ui.repositoryRefreshPromise; } finally { ui.repositoryRefreshPromise = null; } } async function performRepositoryRefresh(withLoader = true, silent = false) { if (withLoader) setLoading(true, "Refreshing Local → Gitea → Server state…"); try { const selectedId = ui.selectedRepoId; ui.repositories = await window.forgeflow.refreshRepositories({ force: withLoader }); ui.refreshError = null; const staleRepository = ui.repositories.find( (repository) => repository.remoteStale, ); ui.refreshWarning = staleRepository ? `Gitea could not be reached. Showing repository data last refreshed ${formatDate(staleRepository.remoteLastRefreshedAt)} while local and server state continue to refresh.` : null; if (selectedId && !selectedRepository()) ui.selectedRepoId = null; const repository = selectedRepository(); if ( repository && !repository.deploymentProfiles.some( (profile) => profile.id === ui.selectedProfileId, ) ) ui.selectedProfileId = selectedProfile(repository)?.id || null; if (repository) { const availablePaths = new Set( (repository.localStatus?.files || []).map((file) => file.path), ); ui.selectedFiles = new Set( [...ui.selectedFiles].filter((filePath) => availablePaths.has(filePath), ), ); if (ui.selectedFile && !availablePaths.has(ui.selectedFile)) { ui.selectedFile = repository.localStatus?.files?.[0]?.path || null; ui.diff = ""; } } if ( !ui.selectedRepoId && ui.currentView === "repository" && ui.repositories.length ) selectRepository(ui.repositories[0].id, false); } catch (error) { ui.refreshError = error.message; ui.refreshWarning = null; if (!silent) showToast("Refresh failed", error.message, "error"); } finally { if (withLoader) setLoading(false); else render(); } } async function refreshActiveOperations(showErrors = true) { try { const updated = await window.forgeflow.refreshOperations(); for (const operation of Array.isArray(updated) ? updated : []) updateOperationInState(operation); return updated; } catch (error) { if (showErrors) showToast("Deployment status unavailable", error.message, "error"); return []; } } async function refreshDeploymentTruth(showErrors = false) { ui.deploymentTruthRequest = { showErrors: ui.deploymentTruthRequest?.showErrors === true || showErrors }; if (ui.deploymentTruthPromise) return ui.deploymentTruthPromise; ui.deploymentTruthPromise = (async () => { let result; while (ui.deploymentTruthRequest) { const request = ui.deploymentTruthRequest; ui.deploymentTruthRequest = null; result = await performDeploymentTruthRefresh(request.showErrors); } return result; })(); try { return await ui.deploymentTruthPromise; } finally { ui.deploymentTruthPromise = null; } } async function performDeploymentTruthRefresh(showErrors = false) { let discovery = []; try { discovery = (await window.forgeflow.discoverServerDeployments?.()) || []; ui.serverDiscovery = discovery; const adopted = discovery.reduce( (total, server) => total + Number(server.adopted || 0), 0, ); if (adopted > 0) { await refreshRepositories(false, true); showToast( "Server workloads discovered", `${adopted} workload${adopted === 1 ? " was" : "s were"} linked automatically from exact repository provenance.`, "success", ); } } catch (error) { if (showErrors) showToast("Server discovery unavailable", error.message, "error"); } const targets = ui.repositories.flatMap((repository) => (repository.deploymentProfiles || []).map((profile) => ({ repository, profile, })), ); if (!targets.length) return { checked: 0, failed: 0, discovery }; const inventoryRefreshedProfiles = new Set(discovery.flatMap((server) => server.refreshedProfileIds || [])); const pendingTargets = targets.filter(({ profile }) => !inventoryRefreshedProfiles.has(profile.id)); const failures = []; const queue = [...pendingTargets]; const workers = Array.from( { length: Math.min(3, queue.length) }, async () => { while (queue.length) { const target = queue.shift(); try { target.profile.state = await window.forgeflow.refreshProfileState( target.repository.fullName, target.profile.id, ); } catch (error) { failures.push({ repository: target.repository.fullName, profile: target.profile.name, message: error.message, }); } } }, ); await Promise.all(workers); await refreshRepositories(false); if (showErrors && failures.length) { showToast( "Some environments could not be checked", `${failures.length} profile${failures.length === 1 ? "" : "s"} could not be refreshed. Open Deployments for details.`, "error", ); } return { checked: targets.length, reusedInventory: targets.length - pendingTargets.length, failed: failures.length, discovery }; } function selectRepository(id, shouldRender = true) { ui.selectedRepoId = id; ui.currentView = "repository"; ui.repositoryTab = "changes"; ui.commitMessage = ""; ui.history = []; ui.branches = []; ui.stashes = []; ui.gitRecovery = null; ui.gitValidation = null; ui.branchProtection = null; const repository = selectedRepository(); ui.selectedProfileId = selectedProfile(repository)?.id || null; const files = repository?.localStatus?.files || []; ui.selectedFiles = new Set(files.map((file) => file.path)); ui.selectedFile = files[0]?.path || null; ui.diff = ""; if (ui.selectedFile && repository?.localPath) loadDiff(repository, ui.selectedFile); if (repository?.owner?.login && repository?.localStatus?.branch?.head) { window.forgeflow .branchProtection(repository.fullName, repository.localStatus.branch.head) .then((protection) => { if (String(ui.selectedRepoId) === String(id)) { ui.branchProtection = protection; render(); } }) .catch(() => {}); } if (shouldRender) render(); } async function loadDiff(repository, filePath) { ui.diff = "Loading diff…"; render(); try { const file = repository.localStatus?.files.find( (item) => item.path === filePath, ); ui.diff = await window.forgeflow.repositoryDiff( repository.localPath, filePath, Boolean(file?.staged && !file?.unstaged), ); ui.diffHunks = file?.unstaged ? await window.forgeflow .repositoryDiffHunks(repository.localPath, filePath) .catch(() => null) : null; } catch (error) { ui.diff = `Unable to load diff: ${error.message}`; } render(); } function repositoryAction(repository) { if (!repository.localPath) return { kind: "link", title: "Connect this repository", detail: "Link an existing local folder or clone it from Gitea.", }; const status = repository.localStatus; if (!status) return { kind: "error", title: "Local repository unavailable", detail: repository.attentionReason || "The linked folder could not be read.", }; if (status.counts.conflicts) return { kind: "conflict", title: "Resolve merge conflicts", detail: `${status.counts.conflicts} conflicted file${status.counts.conflicts === 1 ? "" : "s"} block deployment.`, }; if (status.counts.changed) return { kind: "commit", title: "Commit local changes", detail: `${status.counts.changed} changed file${status.counts.changed === 1 ? "" : "s"} detected.`, }; if (!repository.deploymentProfiles?.length) return { kind: "configure", title: "Configure deployment", detail: "Connect an Unraid server or a Gitea Actions workflow before deploying.", }; const profile = selectedProfile(repository); if (profile?.branch !== status.branch.head) return { kind: "branch-profile", title: "No deployment for this branch", detail: `The selected profile accepts ${profile.branch}; you are on ${status.branch.head}.`, }; if (canDirectPushDeploy(repository, profile)) return { kind: "deploy", title: "Ready for direct redeploy", detail: `ForgeFlow will copy committed HEAD ${status.shortHead} directly to ${profile.environment} over the configured desktop → Unraid connection.`, }; if (status.branch.behind && status.branch.ahead) return { kind: "diverged", title: "Branches have diverged", detail: `Local is ${status.branch.ahead} ahead and ${status.branch.behind} behind.`, }; if (status.branch.behind) return { kind: "pull", title: "Synchronize from Gitea", detail: `Local ${status.branch.head} is ${status.branch.behind} commit${status.branch.behind === 1 ? "" : "s"} behind.`, }; if (status.branch.ahead) return { kind: "push", title: "Push local commits", detail: `${status.branch.ahead} commit${status.branch.ahead === 1 ? "" : "s"} ready to push.`, }; if (repository.readyToDeploy) return { kind: "deploy", title: "Ready for deployment", detail: `Commit ${status.shortHead} can be released to ${profile.environment}.`, }; return { kind: "clean", title: "Repository synchronized", detail: "No local or remote action is required.", }; }