// These three sections used to be pushed into the DOM after render() had already // written the shell. Keeping them in the markup makes the rendered output the // single source of truth, so an unchanged render can be skipped safely. function renderDeploymentPolicyFields(policy) { const windows = (policy.maintenanceWindows || []) .map((window) => `${window.days.join(",")}:${window.start}-${window.end}`) .join(" | "); return `

Deployment policy

Day 0 is Sunday. Separate windows with |.
`; } function renderReleaseNoteFields(profile) { return `
`; } function renderWorkloadClassificationFields(workload) { const type = workload?.classification?.type || "ambiguous"; const recommended = type === "duplicate" ? "select-authoritative" : type === "stale-link" ? "archive-link" : type === "historical-compose" ? "mark-historical" : type === "orphan-container" ? "monitor-only" : "manual-link"; const actions = [["manual-link", "Confirm selected repository match"], ["select-authoritative", "Select as authoritative instance"], ["mark-historical", "Mark historical definition"], ["archive-link", "Archive stale link"], ["monitor-only", "Keep for monitoring only"], ["manual-exclude", "Exclude this workload"], ["ignore", "Ignore with reason"]]; const options = actions.map(([value, label]) => ``).join(""); return `

Classify without touching containers

${icon("info")}
${escapeHtml(type)}

${escapeHtml(workload?.classification?.reason || "ForgeFlow needs an explicit decision for this workload.")}

The decision is tied to current evidence and becomes stale automatically when server truth changes.

`; } function renderModal() { if (!ui.modal) return ""; const repository = selectedRepository() || ui.repositories.find( (repo) => repo.fullName === ui.modal.repositoryFullName, ); if (ui.modal.type === "server-password") { const server = (ui.boot?.state?.servers || []).find((item) => item.id === ui.modal.serverId); if (!server) return ``; const retryText = ui.modal.retry?.type === "deploy" ? "Save password & redeploy" : "Save password & rescan"; return ``; } if (ui.modal.type === "server-reconciliation-plan") { const plan = ui.modal.result?.plan || {}; const summary = plan.summary || {}; const rows = [ ...(plan.additions || []).map((item) => ({ tone: "success", title: `Link ${item.repositoryFullName}`, detail: `${item.evidence} · ${item.impact}` })), ...(plan.updates || []).map((item) => ({ tone: "", title: `Refresh ${item.repositoryFullName}`, detail: item.impact })), ...(plan.stale || []).map((item) => ({ tone: "warning", title: `Review stale link ${item.repositoryFullName}`, detail: `${item.reason} · no automatic removal` })), ...(plan.conflicts || []).map((item) => ({ tone: "danger", title: `Manual review: ${item.displayName}`, detail: `${item.status} · ${(item.candidates || []).map((candidate) => candidate.repositoryFullName).join(", ") || "no unique repository"}` })), ]; return ``; } if (ui.modal.type === "workspace-sync") { const plan = ui.workspaceSyncPlan; if (!plan) return ""; const summary = plan.summary || {}; const blocked = Boolean(plan.blockers?.length); const changeRows = (plan.changes || []).map((change) => `
${escapeHtml(change.path)}${change.originalPath ? `${escapeHtml(change.originalPath)} → ` : ""}${escapeHtml(change.status)}
${escapeHtml(change.code)}
`).join(""); const recoveryRows = [ plan.recovery?.safetyBranch ? "Local commits → recovery branch" : "No local commits require a recovery branch", plan.recovery?.stash ? "Modified and untracked files → named Git stash" : "No working-tree files require a stash", plan.recovery?.untrackedCleanup ? "Untracked files are removed after they are stashed" : "No untracked cleanup required", "Ignored runtime files remain in place", ]; return ``; } if (ui.modal.type === "workload-link") { const serverResult = (ui.serverDiscovery || []).find( (item) => item.serverId === ui.modal.serverId, ); const workload = serverResult?.workloads?.find( (item) => item.workloadId === ui.modal.workloadId, ); if (!workload) { return ``; } const availableRepositories = ui.repositories.filter((item) => item.fullName); const suggestedRepository = ui.modal.repositoryFullName || workload.candidates?.[0]?.repositoryFullName || selectedRepository()?.fullName || availableRepositories[0]?.fullName || ""; const selectedLinkRepository = availableRepositories.find( (item) => item.fullName === suggestedRepository, ); const remoteFolder = ui.modal.remoteFolder || workload.remoteFolderCandidate || safeCloneFolderName(selectedLinkRepository); const candidateSummary = workload.candidates?.length ? workload.candidates .slice(0, 4) .map( (candidate) => `
${escapeHtml(candidate.repositoryFullName)}${escapeHtml(candidate.exact ? "Exact provenance" : `${candidate.score} confidence`)} · ${escapeHtml((candidate.reasons || []).join(", ") || "name similarity")}
`, ) .join("") : '
Repository candidatesNo confident match; choose manually.
'; const containerNames = (workload.containers || []) .map((container) => container.name) .filter(Boolean) .join(", "); return ``; } if (ui.modal.type === "deployment-config") { const storedProfile = repository?.deploymentProfiles?.find( (profile) => profile.id === ui.modal.profileId, ) || {}; const discovery = ui.deploymentDiscovery?.repository === repository?.fullName ? ui.deploymentDiscovery : null; const existing = { ...storedProfile, ...(discovery?.profile || {}) }; if (discovery?.provenance) existing.provenance = discovery.provenance; const servers = ui.boot.state.servers || []; const provider = ui.modal.provider || existing.provider || (servers.length ? "ssh-unraid" : "gitea-actions"); const ssh = provider === "ssh-unraid"; const remoteFolder = existing.remoteFolder || safeCloneFolderName(repository); return ``; } if (ui.modal.type === "inventory-review-plan") { const plan = ui.inventoryReviewPlan; return ``; } if (ui.modal.type === "deploy-key-lifecycle") { const lifecycle = ui.deployKeyLifecycle; const inventory = lifecycle?.inventory; const rotation = lifecycle?.rotation; const revocation = lifecycle?.revocation; return ``; } if (ui.modal.type === "deployment-preflight") { const profile = repository?.deploymentProfiles?.find( (item) => item.id === ui.modal.profileId, ) || selectedProfile(repository); const report = ui.deploymentPreflight; return ``; } if (ui.modal.type === "deploy-confirm") { const profile = repository?.deploymentProfiles?.find( (item) => item.id === ui.modal.profileId, ) || selectedProfile(repository); const targetSha = deploymentTargetSha(repository, profile); return ``; } if (ui.modal.type === "rollback-confirm") { const profile = repository?.deploymentProfiles?.find( (item) => item.id === ui.modal.profileId, ); const target = profile?.state?.previousSha; return ``; } if (ui.modal.type === "server-config") { const server = (ui.boot.state.servers || []).find( (item) => item.id === ui.modal.serverId, ) || {}; const authType = ui.modal.authType || server.authType || "password"; return ``; } if (ui.modal.type === "hunk-staging") { const hunks = ui.diffHunks?.hunks || []; return ``; } if (ui.modal.type === "pull-request") { return ``; } if (ui.modal.type === "conflict-guide") { const state = ui.conflictState || {}; return ``; } if (ui.modal.type === "command-palette") return renderCommandPalette(); return ""; } function paletteCommands() { const repository = selectedRepository(); return [ { id: "overview", label: "Go to release overview", detail: "Workspace", icon: "overview", enabled: true, }, { id: "refresh", label: "Refresh all repositories", detail: "Local and Gitea", icon: "refresh", enabled: true, }, { id: "deployments", label: "Open deployments", detail: "Release history", icon: "deploy", enabled: true, }, { id: "diagnostics", label: "Open diagnostics", detail: "Logs, preflight and support bundle", icon: "shield", enabled: true, }, { id: "settings", label: "Open settings", detail: "Connections and awareness", icon: "settings", enabled: true, }, { id: "open-folder", label: "Open selected project folder", detail: repository?.name || "No repository selected", icon: "folder", enabled: Boolean(repository?.localPath), }, { id: "git-tools", label: "Open branch and stash tools", detail: repository?.name || "No repository selected", icon: "branch", enabled: Boolean(repository?.localPath), }, { id: "deploy-selected", label: "Deploy selected repository", detail: canDeploy(repository) ? `${repository.name} ${shortSha(deploymentTargetSha(repository))}` : "Not ready", icon: "rocket", enabled: canDeploy(repository), }, ]; } function renderCommandPalette() { const query = ui.paletteQuery.toLowerCase(); const commands = paletteCommands().filter( (command) => !query || `${command.label} ${command.detail}`.toLowerCase().includes(query), ); return ``; } function enhanceRenderedUi() { document.querySelectorAll("button.icon-button:not([aria-label])").forEach((button) => { const action = String(button.title || button.dataset.action || "Action").replaceAll("-", " "); button.setAttribute("aria-label", action.charAt(0).toUpperCase() + action.slice(1)); }); document.querySelectorAll(".field > label:not([for])").forEach((label, index) => { const control = label.parentElement?.querySelector("input, select, textarea"); if (!control) return; if (!control.id) control.id = `forgeflow-field-${index}`; label.htmlFor = control.id; }); document.querySelectorAll("input:not([aria-label]), select:not([aria-label]), textarea:not([aria-label])").forEach((control) => { if (control.labels?.length) return; const fallback = String(control.placeholder || control.name || control.id || "Form control").replaceAll("-", " ").trim(); control.setAttribute("aria-label", fallback.charAt(0).toUpperCase() + fallback.slice(1)); }); } // A render replaces the complete application shell. Without this, a background // repository poll or deployment poll destroys the element the user is typing in, // discarding the caret position and every scroll offset on screen. function elementRenderPath(element) { const parts = []; let node = element; while (node && node !== app) { const parent = node.parentElement; if (!parent) return null; parts.push(`${node.tagName}.${Array.prototype.indexOf.call(parent.children, node)}`); node = parent; } return node === app ? parts.reverse().join(">") : null; } function elementAtRenderPath(renderPath) { let node = app; for (const part of renderPath.split(">")) { const separator = part.lastIndexOf("."); node = node?.children?.[Number(part.slice(separator + 1))]; // The shell can be structurally different after a view change, in which case // the old offset belongs to an unrelated element and must be dropped. if (!node || node.tagName !== part.slice(0, separator)) return null; } return node; } // enhanceRenderedUi() re-injects these controls empty on every render, so a // background refresh would otherwise discard a release note or review reason // while the user is still writing it. const INJECTED_FIELD_IDS = [ "deployment-note", "deployment-override", "deployment-override-reason", "inventory-review-action", "inventory-review-reason", "profile-policy-frozen", "profile-policy-note", "profile-policy-freeze-reason", "profile-policy-windows", ]; function captureInjectedFieldValues() { const values = []; for (const id of INJECTED_FIELD_IDS) { const element = document.getElementById(id); if (!element) continue; if (element.type === "checkbox") values.push({ id, checked: element.checked }); else if (element.value) values.push({ id, value: element.value }); } return values; } function restoreInjectedFieldValues(values) { for (const entry of values) { const element = document.getElementById(entry.id); if (!element) continue; // Never overwrite a value the freshly rendered control already carries; only // fill back in what the injection left empty. if ("checked" in entry) { if (!element.checked) element.checked = entry.checked; } else if (!element.value) element.value = entry.value; } } function captureInteractionState() { const scroll = []; for (const element of app.querySelectorAll("*")) { if (!element.scrollTop && !element.scrollLeft) continue; const renderPath = elementRenderPath(element); if (renderPath) scroll.push({ renderPath, top: element.scrollTop, left: element.scrollLeft }); } const injectedFields = captureInjectedFieldValues(); const active = document.activeElement; if (!active?.id || !app.contains(active)) return { scroll, injectedFields, focus: null }; const focus = { id: active.id, start: null, end: null, direction: "none" }; try { focus.start = active.selectionStart; focus.end = active.selectionEnd; focus.direction = active.selectionDirection || "none"; } catch {} return { scroll, injectedFields, focus }; } function restoreInteractionState(state) { restoreInjectedFieldValues(state.injectedFields); for (const entry of state.scroll) { const element = elementAtRenderPath(entry.renderPath); if (!element) continue; element.scrollTop = entry.top; element.scrollLeft = entry.left; } if (!state.focus) return; const element = document.getElementById(state.focus.id); if (!element || !app.contains(element)) return; element.focus({ preventScroll: true }); if (state.focus.start === null) return; try { element.setSelectionRange(state.focus.start, state.focus.end, state.focus.direction); } catch {} } let lastRenderedMarkup = null; function render() { if (!ui.boot) return; const repository = selectedRepository(); const main = ui.currentView === "overview" ? renderOverview() : ui.currentView === "deployments" ? renderDeployments() : ui.currentView === "help" ? renderHelp() : ui.currentView === "settings" ? renderSettings() : ui.currentView === "diagnostics" ? renderDiagnostics() : ui.currentView === "deployment-run" ? renderPipelineView() : repository ? renderRepositoryWorkspace(repository) : renderOverview(); const withPanel = ui.currentView === "repository" && repository; const markup = `
${renderTitlebar()}
${renderSidebar()}
${main}
${withPanel ? renderActionPanel(repository) : ""}${ui.loading ? `
${escapeHtml(ui.loadingMessage || "Working…")}
` : ""}
${renderStatusbar()}
${ui.boot.state.setupComplete ? "" : renderSetup()}${renderModal()}`; // Most renders are triggered by a poll that found nothing new. Rebuilding an // identical shell would only cost layout work and interrupt the user. The // markup is the complete rendered state, so comparing it is sufficient: // enhanceRenderedUi() only derives labels and ids from what is already there. if (markup === lastRenderedMarkup) return; const interaction = captureInteractionState(); app.innerHTML = markup; enhanceRenderedUi(); restoreInteractionState(interaction); lastRenderedMarkup = markup; if (ui.modal?.type === "command-palette") requestAnimationFrame(() => document.querySelector("#palette-input")?.focus(), ); }