async function runOperation( message, operation, successMessage, { refresh = true } = {}, ) { setLoading(true, message); try { const result = await operation(); if (successMessage) showToast("Done", successMessage, "success"); if (refresh) await refreshRepositories(false); return result; } catch (error) { const pushAfterCommit = error.code === "PUSH_AFTER_COMMIT_FAILED"; showToast( pushAfterCommit ? "Commit saved locally; push failed" : "Operation failed", error.message, "error", ); // Always reload the real Git state. A failed stage must keep changes visible, while a // failed push after a successful commit must immediately surface as an ahead branch. await refreshRepositories(false, true); if (pushAfterCommit) { ui.selectedFiles.clear(); ui.selectedFile = null; ui.diff = ""; ui.commitMessage = ""; render(); } return null; } finally { setLoading(false); } } async function executeDeployment(profileId) { const repository = selectedRepository(); const profile = repository?.deploymentProfiles?.find((item) => item.id === profileId) || selectedProfile(repository); if (!repository || !profile) return; const targetSha = deploymentTargetSha(repository, profile); if (!targetSha) { showToast("Refresh required", "Refresh Gitea and server truth before deploying this environment.", "error"); return; } const deploymentOptions = { note: document.querySelector("#deployment-note")?.value.trim() || "", override: document.querySelector("#deployment-override")?.checked === true, overrideReason: document.querySelector("#deployment-override-reason")?.value.trim() || "", }; ui.modal = null; setLoading( true, profile.provider === "ssh-unraid" ? `Deploying ${repository.name} to ${profile.remoteFolder} over SSH…` : `Dispatching ${profile.name} workflow…`, ); try { ui.activeDeployment = await window.forgeflow.deploy( repository, profile.id, targetSha, deploymentOptions, ); updateOperationInState(ui.activeDeployment); ui.currentView = "deployment-run"; showToast( "Deployment started", `${repository.name} ${shortSha(targetSha)} → ${profile.environment}`, "success", ); startOperationPolling(); } catch (error) { if (profile.provider === "ssh-unraid" && isSshCredentialError(error)) { ui.modal = { type: "server-password", serverId: profile.serverId, retry: { type: "deploy", repositoryFullName: repository.fullName, profileId: profile.id, }, }; showToast("SSH key rejected", "Enter the Unraid server password once; ForgeFlow will retry the direct desktop → Unraid connection.", "error"); render(); } else { showToast("Deployment failed to start", error.message, "error"); } } setLoading(false); } async function executeRollback(profileId) { const repository = selectedRepository(); const profile = repository?.deploymentProfiles?.find( (item) => item.id === profileId, ); const target = profile?.state?.previousSha; if (!repository || !profile || !target) return; ui.modal = null; setLoading( true, profile?.provider === "ssh-unraid" ? `Rolling back ${profile.remoteFolder} over SSH…` : `Dispatching rollback to ${shortSha(target)}…`, ); try { ui.activeDeployment = await window.forgeflow.rollback( repository, profile.id, target, ); updateOperationInState(ui.activeDeployment); ui.currentView = "deployment-run"; showToast( "Rollback requested", `${profile.environment} → ${shortSha(target)}`, "success", ); } catch (error) { showToast("Rollback failed to start", error.message, "error"); } setLoading(false); } async function loadGitTools(repository) { if (!repository?.localPath) return; setLoading(true, "Loading branches and stashes…"); try { [ui.branches, ui.stashes, ui.gitRecovery] = await Promise.all([ window.forgeflow.branches(repository.localPath), window.forgeflow.stashList(repository.localPath), window.forgeflow.gitRecoveryStatus(repository.localPath), ]); ui.repositoryTab = "gittools"; } catch (error) { showToast("Git tools unavailable", error.message, "error"); } setLoading(false); } function profileRepository(profileId) { return ui.repositories.find((repository) => repository.deploymentProfiles?.some((profile) => profile.id === profileId), ); } async function runSystemPreflight({ setup = false } = {}) { setLoading(true, "Checking local readiness…"); try { ui.systemPreflight = await window.forgeflow.setupPreflight({ baseUrl: ui.setupDraft.baseUrl, token: ui.setupDraft.token, roots: setup ? ui.setupDraft.roots : ui.boot.state.workspaceRoots, }); if (!setup) ui.diagnosticsStatus = await window.forgeflow.diagnosticsStatus(); showToast( ui.systemPreflight.summary.ready ? "Readiness checks passed" : "Readiness needs attention", ui.systemPreflight.summary.ready ? `${ui.systemPreflight.summary.counts.pass} checks passed.` : `${ui.systemPreflight.summary.blocking.length} blocking check(s) must be resolved.`, ui.systemPreflight.summary.ready ? "success" : "error", ); return ui.systemPreflight; } catch (error) { showToast("Readiness check failed", error.message, "error"); return null; } finally { setLoading(false); } } async function runDeploymentPreflight( repository, profileId, { showModal = true } = {}, ) { if (!repository || !profileId) return null; if (String(repository.id) !== String(ui.selectedRepoId)) selectRepository(repository.id, false); ui.selectedProfileId = profileId; ui.deploymentPreflight = null; setLoading(true, "Verifying repository, workflow and server…"); try { const report = await window.forgeflow.deploymentPreflight( repository, profileId, ); ui.deploymentPreflight = report; if (showModal) ui.modal = { type: "deployment-preflight", profileId, repositoryFullName: repository.fullName, }; return report; } catch (error) { showToast("Deployment preflight failed", error.message, "error"); return null; } finally { setLoading(false); } }