function navButton(view, label, iconName, count = "") { return ``; } function renderTitlebar() { const state = ui.boot?.state; const user = state?.gitea?.user; const connected = Boolean(state?.gitea?.hasToken); const repository = selectedRepository(); const title = ui.currentView === "repository" && repository ? repository.fullName : { overview: "Release overview", deployments: "Deployments", diagnostics: "Diagnostics", settings: "Settings", "deployment-run": "Deployment run", }[ui.currentView] || "Workspace"; return `
ForgeFlowby ITWorx.tech
${escapeHtml(title)}
${icon("search")}
${connected ? escapeHtml(user?.login || "Gitea") : "Offline"}
`; } function renderRepositoryRow(repository) { const status = repository.localStatus; const profiles = repository.deploymentProfiles || []; const workloads = linkedWorkloadsForRepository(repository); const runningWorkloads = workloads.filter((workload) => workload.runtime?.running); const badges = []; if (status?.counts.conflicts) badges.push('!'); else if (status?.counts.changed) badges.push( `${status.counts.changed}`, ); if (status?.branch.ahead) badges.push( `↑${status.branch.ahead}`, ); if (status?.branch.behind) badges.push( `↓${status.branch.behind}`, ); if (repository.readyToDeploy) badges.push( '', ); if (profiles.length) badges.push( `S${profiles.length}`, ); if (!repository.localPath) badges.push(''); const branch = status?.branch.head || repository.defaultBranch || "remote"; return ``; } function renderSidebar() { const query = `${ui.search} ${ui.repoSearch}`.trim().toLowerCase(); const repositories = ui.repositories.filter( (repository) => !query || `${repository.name} ${repository.fullName} ${repository.description}` .toLowerCase() .includes(query), ); const favorites = repositories.filter((repository) => repository.favorite); const others = repositories.filter((repository) => !repository.favorite); const attention = ui.repositories.filter( (repository) => repository.attention || repository.localStatus?.counts.changed || repository.localStatus?.branch.ahead || repository.readyToDeploy, ).length; const rows = (list) => list.map(renderRepositoryRow).join(""); return ``; } function renderSummaryCard(label, value, note, iconName, tone = "") { return `
${icon(iconName)}
${label}
${value}
${note}
`; } function queueActionFor(repository) { const action = repositoryAction(repository); const mapping = { link: ["folder", "Link folder", "Local project is not connected", ""], error: ["error", "Inspect problem", action.detail, "danger"], conflict: ["warning", "Resolve conflicts", action.detail, "danger"], commit: ["file", "Review & commit", action.detail, "warning"], diverged: ["warning", "Resolve divergence", action.detail, "danger"], pull: ["arrowDown", "Synchronize", action.detail, "warning"], push: ["arrowUp", "Push commits", action.detail, ""], configure: ["settings", "Configure deploy", action.detail, ""], "branch-profile": ["branch", "Select profile", action.detail, ""], deploy: ["rocket", "Deploy release", action.detail, "success"], clean: ["check", "Synchronized", action.detail, "success"], }; return mapping[action.kind] || mapping.clean; } function projectIllustration(kind = "flow") { return ``; } function renderOverview() { const changed = ui.repositories.filter( (repository) => repository.localStatus?.counts.changed, ).length; const unpushed = ui.repositories.filter( (repository) => repository.localStatus?.branch.ahead, ).length; const deployable = ui.repositories.filter( (repository) => repository.readyToDeploy, ).length; const unhealthy = ui.repositories .flatMap((repository) => repository.deploymentProfiles || []) .filter((profile) => profile.state?.healthy === false).length; const queue = ui.repositories .filter((repository) => repositoryAction(repository).kind !== "clean") .slice(0, 8); const recent = operations().slice(0, 7); const active = recent.filter( (operation) => !isTerminalOperation(operation.status), ); return `
${ui.refreshError ? `
${icon("error")} ${escapeHtml(ui.refreshError)}
` : ""}
${renderSummaryCard("Local work", changed, changed === 1 ? "repository has changes" : "repositories have changes", "file", changed ? "warning" : "success")} ${renderSummaryCard("Unpushed", unpushed, "repositories ahead of Gitea", "arrowUp", unpushed ? "warning" : "success")} ${renderSummaryCard("Ready", deployable, "exact commits ready to deploy", "rocket", deployable ? "success" : "")} ${renderSummaryCard("Health", unhealthy || active.length, unhealthy ? "unhealthy environments" : active.length ? "operations in progress" : "all checked environments healthy", "pulse", unhealthy ? "danger" : active.length ? "warning" : "success")}

Action queue

Sorted by required attention
${ queue.length ? queue .map((repository) => { const [iconName, label, reason, tone] = queueActionFor(repository); return `
${icon(iconName)}
${escapeHtml(repository.name)}
${escapeHtml(repository.localStatus?.branch.head || repository.defaultBranch || "remote")} ${repository.localStatus?.shortHead ? `• ${repository.localStatus.shortHead}` : ""}
${escapeHtml(label)}${escapeHtml(reason)}
`; }) .join("") : '

Everything is synchronized

No repository needs immediate attention.

' }

Recent deployments

${recent.length ? recent.map((operation) => `
${escapeHtml(operation.repository)} → ${escapeHtml(operation.environment || "environment")}
${escapeHtml(operation.action === "rollback" ? "Rollback" : "Deploy")} ${escapeHtml(operation.shortSha || shortSha(operation.sha))} · ${escapeHtml(operation.status)}
${formatDate(operation.updatedAt || operation.createdAt)}
`).join("") : '

No deployment history yet.

'}

Workspace readiness

${readinessRow("Git executable", ui.boot.git.available, ui.boot.git.version || ui.boot.git.error)} ${readinessRow("Gitea connection", ui.boot.state.gitea.hasToken, ui.boot.state.gitea.baseUrl || "Not configured")} ${readinessRow("Workspace folders", ui.boot.state.workspaceRoots.length > 0, `${ui.boot.state.workspaceRoots.length} configured`)} ${readinessRow("Automatic awareness", ui.boot.state.preferences?.autoRefresh !== false, ui.boot.state.preferences?.autoRefresh === false ? "Manual refresh only" : `Every ${ui.boot.state.preferences?.repositoryPollSeconds || 4}s`)}
`; } function readinessRow(label, ok, detail) { return `
${escapeHtml(label)}${escapeHtml(detail)}
`; } function releaseNode(label, value, description, tone = "") { return `
${label}
${escapeHtml(value)}${escapeHtml(description)}
`; } function linkedWorkloadsForRepository(repository) { const fullName = String(repository?.fullName || "").toLowerCase(); if (!fullName) return []; return (ui.serverDiscovery || []).flatMap((server) => (server.workloads || []) .filter((workload) => String(workload.link?.repositoryFullName || "").toLowerCase() === fullName) .map((workload) => ({ ...workload, serverId: server.serverId, serverName: server.serverName || server.server?.name || "Server" })), ); } function diffAtmosphere(diff) { if (!ui.selectedFile) return ""; const lines = String(diff || "").split("\n"); const additions = lines.filter( (line) => line.startsWith("+") && !line.startsWith("+++"), ).length; const removals = lines.filter( (line) => line.startsWith("-") && !line.startsWith("---"), ).length; const extension = String(ui.selectedFile).split(".").pop()?.slice(0, 8).toUpperCase() || "FILE"; return ``; } function renderDiff(diff) { if (!diff) return '

No textual diff

Select another file or open the project folder for binary changes.

'; const rendered = escapeHtml(diff) .split("\n") .map((line) => { const type = line.startsWith("+") && !line.startsWith("+++") ? "add" : line.startsWith("-") && !line.startsWith("---") ? "remove" : line.startsWith("@@") ? "hunk" : ""; return `${line || " "}`; }) .join(""); return `${rendered}${diffAtmosphere(diff)}`; } function fileStatusCode(file) { if (file.conflict) return "U"; if (file.untracked) return "?"; return ( { modified: "M", added: "A", deleted: "D", renamed: "R", copied: "C", "type-changed": "T", }[file.status] || "M" ); } function renderChanges(repository) { const status = repository.localStatus; if (!repository.localPath) { const target = displayCloneTarget(repository); return `
${icon("link")}

Connect a local project

Clone directly into your default project root, or link an existing working tree.

${target ? `
${icon("folder")}Automatic destination${escapeHtml(target)}
` : '
No default project root is configured. ForgeFlow will ask for one.
'}
`; } if (!status) return `
${icon("error")}

Repository unavailable

${escapeHtml(repository.attentionReason || "The local working tree could not be read.")}

`; if (!status.files.length) return `
${icon("check")}

Working tree clean

Local ${escapeHtml(status.branch.head)} is at ${escapeHtml(status.shortHead)} with no uncommitted files.

`; const selected = status.files.find((file) => file.path === ui.selectedFile); const conflictActions = selected?.conflict ? `
Conflicted file

Choose one side, or edit the file and mark it resolved.

` : ""; return `
${ui.selectedFiles.size} selected · ${status.counts.changed} changed · ${status.counts.staged} staged
${status.files.map((file) => `
${fileStatusCode(file)}${escapeHtml(file.path)}${file.staged ? "●" : "○"}
`).join("")}
${status.counts.conflicts ? `
` : ""}
${conflictActions}
${escapeHtml(ui.selectedFile || "Select a file")}
${ui.diffHunks?.partialSupported ? `` : ""}${ui.selectedFile ? `` : ""}${ui.selectedFile ? escapeHtml(selected?.status || "") : ""}
${renderDiff(ui.diff)}
`; } function renderHistory(repository) { if (!repository.localPath) return '

Link a local repository to view commit history.

'; if (!ui.history.length) return `
${icon("history")}

Load local commit history

Review the last commits from this working tree.

`; return `
${ui.history.map((commit) => ``).join("")}
CommitMessageAuthorDate
${escapeHtml(commit.shortSha)}${escapeHtml(commit.subject)}${escapeHtml(commit.author)}${formatDate(commit.date)}
`; } function environmentState(profile) { const state = profile.state || {}; if (state.healthy === false) return { label: "Unhealthy", tone: "danger" }; if (state.healthy === true) return { label: "Healthy", tone: "success" }; if (state.containerRunning === true) return { label: "Running · unverified", tone: "warning" }; if (state.containerRunning === false) return { label: "Stopped", tone: "danger" }; if (profile.provider === "ssh-unraid" || state.statusConfigured || state.healthConfigured) return { label: "Not checked", tone: "" }; return { label: "Status not configured", tone: "" }; } function dockerManIntegration(profile) { const state = profile.state || {}; const iconMode = profile.iconMode || (profile.iconFilePath ? "upload" : profile.iconUrl ? "url" : "builtin"); const webUiExpected = Boolean(profile.webUiUrl || profile.hostPort); const iconExpected = iconMode !== "none"; const templateReady = Boolean(state.dockerMan?.templateExists); const webUiReady = !webUiExpected || Boolean(state.dockerMan?.webUi) || templateReady; const iconReady = !iconExpected || Boolean(state.dockerMan?.icon) || templateReady; return { iconMode, templateReady, webUiReady, iconReady, ready: Boolean(state.containerRunning && webUiReady && iconReady), }; } function deploymentIdentity(profile, repository) { const name = String( profile.state?.containerName || profile.containerName || profile.remoteFolder || repository.name || "container", ); let hash = 0; for (const character of name) hash = (hash * 31 + character.charCodeAt(0)) >>> 0; return { name, initial: name.slice(0, 1).toUpperCase(), accent: hash % 6 }; } function renderProfileCard(repository, profile, compact = false) { const state = profile.state || {}; const health = environmentState(profile); const isSsh = profile.provider === "ssh-unraid"; const mode = deploymentMode(profile); const verification = ui.serverGitVerifications[profile.id]; const targetSha = deploymentTargetSha(repository, profile); const ready = canDeploy(repository, profile); const modeLabel = { "push-bundle": "Direct copy", "server-git": "Server pull from Gitea", "monitor-only": "Monitor only", }[mode] || mode; const providerDetail = isSsh ? `SSH / Unraid · ${modeLabel} · ${profile.remoteFolder || repository.name} · ${profile.branch}${profile.adoptedFromServer ? " · server-linked" : ""}` : `${profile.workflowFile} · ${profile.branch}`; const rollbackConfigured = (isSsh && mode !== "monitor-only") || Boolean(profile.rollbackWorkflowFile); const dockerMan = dockerManIntegration(profile); const { templateReady, webUiReady, iconReady } = dockerMan; const dockerManReady = dockerMan.ready; const managesDockerMan = isSsh && profile.manageDockerMan === true; const webUi = profile.webUiUrl || state.webUiUrl || state.dockerMan?.webUi || ""; const identity = deploymentIdentity(profile, repository); const syncLabel = isSsh ? state.matchesGitea ? `${icon("check")}Live = Gitea · ${shortSha(state.liveSha)}` : state.liveSha && state.giteaSha ? `Live ${shortSha(state.liveSha)} · Gitea ${shortSha(state.giteaSha)}` : state.liveSha ? `${icon("check")}Live · ${shortSha(state.liveSha)}` : "" : state.matchesGitea ? `${icon("check")}Live = Gitea · ${shortSha(state.liveSha)}` : state.giteaSha && state.liveSha ? `Live ${shortSha(state.liveSha)} · Gitea ${shortSha(state.giteaSha)}` : ""; const dockerManLabel = managesDockerMan ? dockerManReady ? templateReady ? "Managed labels/template active" : "Managed labels active" : `Managed · WebUI ${webUiReady ? "ready" : "missing"} · icon ${iconReady ? "ready" : "missing"}` : "Existing DockerMan template preserved"; const sourceLabel = isSsh ? mode === "server-git" ? `Gitea ${state.giteaSha ? shortSha(state.giteaSha) : "refresh required"}` : "Committed local HEAD" : state.giteaSha ? shortSha(state.giteaSha) : "Refresh to compare"; const serverAccessAction = isSsh && mode === "server-git" ? `` : ""; return `
${escapeHtml(identity.initial)}
Container${escapeHtml(identity.name)}${escapeHtml(repository.fullName)} · ${escapeHtml(profile.environment)}
${syncLabel}
${escapeHtml(isSsh ? "SSH / UNRAID" : "GITEA ACTIONS")}

${escapeHtml(profile.name)}

${escapeHtml(providerDetail)}

${health.label}
${isSsh ? `` : ""}${serverAccessAction}${webUi ? `` : ""}${managesDockerMan ? `` : ""}${ready ? `` : ""}${state.previousSha && rollbackConfigured ? `` : ""}
`; } function renderRepositoryDeployments(repository) { const profiles = repository.deploymentProfiles || []; const workloads = linkedWorkloadsForRepository(repository); const profileIds = new Set(profiles.map((profile) => profile.id)); const workloadRows = workloads.map((workload) => { const containers = (workload.containers || []).map((container) => container.name).filter(Boolean); const profileResolved = Boolean(workload.link?.profileId && profileIds.has(workload.link.profileId)); return `
${escapeHtml(workload.displayName || containers[0] || "Server workload")}${escapeHtml(workload.serverName)} · ${containers.length ? escapeHtml(containers.join(", ")) : "container identity unavailable"} · ${workload.runtime?.running ? "running" : "stopped"}${escapeHtml(workload.compose?.project ? `Compose ${workload.compose.project}` : workload.remoteFolderCandidate || "Docker workload")}
${profileResolved ? "Repository linked" : "Link needs reconciliation"}${profileResolved ? `` : ``}
`; }).join(""); const repoOps = repositoryOperations(repository).slice(0, 10); return `

Deployment environments

${profiles.length} configured profile${profiles.length === 1 ? "" : "s"} · ${workloads.length} server workload${workloads.length === 1 ? "" : "s"} linked to this repository
${workloads.length ? `

Detected on server

Live Docker / Compose identities resolved back to this repository
${workloadRows}
` : ""}${profiles.length ? `
${profiles.map((profile) => renderProfileCard(repository, profile)).join("")}
` : '

No deployment profile

Connect a Gitea Actions workflow or a trusted SSH / Unraid server.

'}

Release history

${repoOps.length ? `${repoOps.map((operation) => ``).join("")}
ActionEnvironmentCommitStatusUpdated
${escapeHtml(operation.action || "deploy")}${escapeHtml(operation.environment)}${escapeHtml(operation.shortSha || shortSha(operation.sha))}${escapeHtml(operation.status)}${formatDate(operation.updatedAt || operation.createdAt)}
` : '

No releases for this repository yet.

'}
`; } function renderGitTools(repository) { if (!repository.localPath) return '

Link a local repository to manage branches and stashes.

'; const recovery = ui.gitRecovery; const locks = recovery?.lockReport?.locks || []; const activeProcesses = recovery?.lockReport?.processes?.active || []; const recommendations = recovery?.recommendations || []; return `

Branches

${ui.branches.length ? ui.branches.map((branch) => `
${escapeHtml(branch.name)}${escapeHtml(branch.shortSha)}${branch.upstream ? ` · ${escapeHtml(branch.upstream)}` : " · unpublished"}
${branch.current ? 'Current' : ``}
`).join("") : '

Load branch information.

'}

Stashes

${ui.stashes.length ? ui.stashes.map((stash) => `
${escapeHtml(stash.ref)}${escapeHtml(stash.subject)} · ${formatDate(stash.date)}
`).join("") : '

No stashes, or Git tools have not been loaded.

'}

Repository troubleshooting

Safe, repository-specific recovery actions
${recovery ? `
${locks.length ? `${locks.length} lock${locks.length === 1 ? "" : "s"}` : "No Git locks"}${activeProcesses.length ? `${activeProcesses.length} active Git process(es)` : "No matching active Git process detected"}
${locks.length ? `
${locks.map((lock) => `
${escapeHtml(lock.name)}${Math.round(lock.ageMs / 1000)}s old · ${escapeHtml(lock.modifiedAt)}
`).join("")}
` : ""}${recommendations.length ? `
${recommendations.map((item) => `
${escapeHtml(item.label)}${item.safe ? "Safe automated action" : item.action ? "Creates a safety branch before changing history" : "Review required"}
${item.action ? `` : ""}
`).join("")}
` : ""}` : '

Scan before repairing. ForgeFlow checks every .lock file in the actual Git directory, not only index.lock.

'}
${repository.sshUrl && repository.localStatus?.remoteUrl !== repository.sshUrl ? `` : ""}
Lock repair refuses to run while a matching Git process is active. A force option is shown only when process detection itself is unavailable.
`; } function renderRepositorySettings(repository) { const automaticTarget = displayCloneTarget(repository); const currentOrigin = repository.localStatus?.remoteUrl || "Unavailable"; const desiredOrigin = repository.sshUrl || repository.preferredCloneUrl || ""; const originNeedsRepair = Boolean( repository.localPath && desiredOrigin && currentOrigin !== desiredOrigin, ); const pullRequests = ui.pullRequests || []; return `

Repository identity

${desiredOrigin ? `
` : ""}
${originNeedsRepair ? `` : ""}${repository.localPath ? `` : ``}

Open pull requests

Live from Gitea
${pullRequests.length ? `
${pullRequests.map((pull) => `
#${pull.number} · ${escapeHtml(pull.title)}${escapeHtml(pull.head?.ref || pull.head?.label || "source")} → ${escapeHtml(pull.base?.ref || pull.base?.label || "target")} · ${formatDate(pull.updated_at || pull.created_at)}
`).join("")}
` : '

No open pull requests.

'}

Repository behavior

${icon("shield")}Origin repair changes only the Git remote URL. Git health scans the actual Git directory, repairs only proven stale lock files and never changes source files or commits.
`; } function renderGitValidator(repository) { const report = ui.gitValidation; if (!report) return `
${projectIllustration("diagnostics")}
Repository assurance

Validate Git best practices

Inspect repository identity, branch governance, tracked secrets, file hygiene and safe local synchronization settings.

`; const tone = report.score >= 90 ? "success" : report.score >= 70 ? "warning" : "danger"; const groups = report.checks.reduce((grouped, check) => { (grouped[check.category] ||= []).push(check); return grouped; }, {}); const trend = report.trend || {}; return `
${report.score}/ 100
${escapeHtml(report.policy?.label || "Standard")} policy · ${report.ready ? "release-ready" : "review required"}

${escapeHtml(report.grade)}

${report.summary.passed} passed · ${report.summary.warnings} recommendations · ${report.summary.errors} critical

${trend.newlyFound?.length || 0} new · ${trend.resolved?.length || 0} resolved · ${trend.regressions?.length || 0} regressions · ${report.expiredSuppressions?.length || 0} expired exceptions

${projectIllustration("diagnostics")}
${Object.entries( groups, ) .map( ([category, checks]) => `

${escapeHtml(category)}

${checks.filter((check) => check.status === "pass").length}/${checks.length} passed
${checks .map((check) => { const checkIndex = report.checks.indexOf(check); return `
${icon(check.status === "pass" ? "check" : check.status === "error" ? "error" : "warning")}
${escapeHtml(check.title)}

${escapeHtml(check.detail)}

${check.suppressed ? `Suppressed until ${formatDate(check.suppression.expiresAt)} · ${escapeHtml(check.suppression.reason)}` : check.expiredSuppression ? `Exception expired; finding is active again.` : ""}
${check.fixAction ? `` : check.status !== "pass" && !check.suppressed ? `` : `${check.suppressed ? "Suppressed" : check.status === "pass" ? "Best practice" : "Review"}`}
`; }) .join("")}
`, ) .join("")}
`; } function renderRepositoryWorkspace(repository) { const status = repository.localStatus; const profiles = repository.deploymentProfiles || []; const linkedWorkloads = linkedWorkloadsForRepository(repository); const profile = selectedProfile(repository); const profileWorkload = linkedWorkloads.find((workload) => workload.link?.profileId === profile?.id); const serverState = profile?.state || {}; const localTone = status?.counts.conflicts ? "danger" : status?.counts.changed ? "warning" : status ? "success" : ""; const remoteTone = status?.branch.behind ? "danger" : status?.branch.ahead ? "warning" : status?.branch.upstream ? "success" : ""; const serverTone = serverState.healthy === false ? "danger" : serverState.healthy === true ? "success" : profileWorkload?.runtime?.running ? "success" : ""; const content = ( { changes: renderChanges, history: renderHistory, deployments: renderRepositoryDeployments, gittools: renderGitTools, validator: renderGitValidator, settings: renderRepositorySettings, }[ui.repositoryTab] || renderChanges )(repository); const deploymentLinks = profiles.length ? `
${icon("server")}Linked deployments
${profiles.map((item) => { const workload = linkedWorkloads.find((candidate) => candidate.link?.profileId === item.id); const itemState = item.state || {}; const tone = itemState.healthy === false ? "danger" : itemState.healthy === true ? "success" : workload?.runtime?.running ? "success" : "warning"; const identity = workload?.displayName || item.containerName || item.remoteFolder || item.environment; return ``; }).join("")}
` : ""; return `

${escapeHtml(repository.fullName)}

${escapeHtml(repository.localPath || "No local working tree linked")}

${projectIllustration("repo")}
${repository.localPath ? `
${ui.branchProtection ? `${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}` : ""}
` : ""}
${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : profile ? "Linked" : "Unknown", profileWorkload ? `${profileWorkload.displayName || profile.containerName || "Container"} · ${profileWorkload.runtime?.running ? "running" : "stopped"} on ${profileWorkload.serverName}` : profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "profile linked · awaiting live scan") : "No deployment profile", serverTone)}
${deploymentLinks}
${content}
`; } function renderActionPanel(repository) { const action = repositoryAction(repository); const status = repository.localStatus; const profile = selectedProfile(repository); let body = ""; if (action.kind === "link") { const target = displayCloneTarget(repository); body = `
${icon("link")}

${action.title}

${action.detail}

${target ? `
Project root${escapeHtml(defaultWorkspaceRoot())}New folder${escapeHtml(safeCloneFolderName(repository))}
` : '
No default project root is configured yet.
'}
`; } else if (action.kind === "commit") { const hasStagedSelection = status.counts.staged > 0; const commitReady = Boolean( (ui.selectedFiles.size || hasStagedSelection) && ui.commitMessage.trim(), ); const commitBlocker = !ui.selectedFiles.size && !hasStagedSelection ? "Select files or stage one or more hunks." : !ui.commitMessage.trim() ? "Enter a commit message to enable commit and push." : ui.selectedFiles.size ? "Ready to commit. ForgeFlow stages the selected files automatically." : "Ready to commit only the reviewed staged hunks."; body = `
${ui.selectedFiles.size ? `${ui.selectedFiles.size} of ${status.counts.changed} files selected` : `${status.counts.staged} staged file(s)`}Ctrl+Enter
${icon(commitReady ? "check" : "warning")}${escapeHtml(commitBlocker)}
Partial hunk staging is preserved when no complete files are selected.
`; } else if (action.kind === "pull") body = `
${icon("arrowDown")}

${action.title}

${action.detail}

`; else if (action.kind === "push") body = `
${icon("arrowUp")}

${action.title}

${action.detail}

`; else if ( action.kind === "diverged" || action.kind === "conflict" || action.kind === "error" ) body = `
${icon("error")}

${action.title}

${action.detail}

${action.kind === "diverged" ? `` : ""}
`; else if (action.kind === "configure") body = `
${icon("settings")}

${action.title}

${action.detail}

`; else if (action.kind === "branch-profile") body = `
${icon("branch")}

${action.title}

${action.detail}

${repository.deploymentProfiles.length > 1 ? `` : ""}
`; else if (action.kind === "deploy") body = `
${icon("rocket")}

Release ${escapeHtml(status.shortHead)}

${escapeHtml(profile.name)} will deploy the exact commit from ${escapeHtml(profile.branch)} to ${escapeHtml(profile.environment)}.

${repository.deploymentProfiles.length > 1 ? `` : ""}
Local${escapeHtml(status.shortHead)}Gitea${escapeHtml(status.shortHead)}Target${escapeHtml(profile.environment)}
${profile.state?.previousSha && profile.rollbackWorkflowFile ? `` : ""}
`; else body = `
${icon("check")}

${action.title}

${action.detail}

${profile ? `` : ""}
`; return ``; } function renderServerInventory() { const servers = ui.serverDiscovery || []; const configuredServers = ui.boot?.state?.servers || []; const hiddenClassifications = new Set(["backup", "release-folder", "system-container", "manually-excluded"]); const visibleForServer = (server) => (server.workloads || []).filter((workload) => workload.reviewDecisionStale || workload.classification?.type === "duplicate" || (!hiddenClassifications.has(workload.classification?.type) && (workload.link || workload.runtime?.running || ["ambiguous", "orphan-container", "stopped-application", "historical-compose", "stale-link", "monitor-only"].includes(workload.classification?.type))), ); const reviewCount = servers.reduce((total, server) => total + visibleForServer(server).filter((workload) => !workload.link || workload.classification?.type === "stale-link" || workload.reviewDecisionStale).length, 0); const serverCards = servers.map((server) => { const capabilities = server.capabilities || {}; const capabilityText = [ capabilities.docker ? "Docker" : "Docker missing", capabilities.compose ? "Compose" : "Compose missing", capabilities.git ? "Git available" : "Git optional", capabilities.tar && capabilities.checksum ? "Push ready" : "Push tools incomplete", ].join(" · "); const errorBlock = server.error ? `
${icon("error")}
Server scan failed

${escapeHtml(server.error)}

` : ""; const warnings = (server.warnings || []).map((warning) => `
${icon("warning")}${escapeHtml(warning)}
`).join(""); const visibleWorkloads = visibleForServer(server); const hiddenCount = Math.max(0, (server.workloads || []).length - visibleWorkloads.length); const workloads = visibleWorkloads.length ? visibleWorkloads.map((workload) => { const containers = (workload.containers || []).map((container) => container.name).filter(Boolean).join(", "); const topCandidate = workload.candidates?.[0]; const linkedRepository = ui.repositories.find((repository) => String(repository.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase()); const linkedProfile = linkedRepository?.deploymentProfiles?.find((profile) => profile.id === workload.link?.profileId); const claimsLink = workload.status === "linked" || Boolean(workload.link); const linked = Boolean(claimsLink && linkedRepository && linkedProfile) && workload.classification?.type !== "stale-link"; const inconsistentLink = claimsLink && !linked; const classification = workload.classification?.type || workload.status || "review"; const statusTone = linked && !workload.reviewDecisionStale ? "success" : inconsistentLink || ["ambiguous", "duplicate", "orphan-container"].includes(classification) || workload.reviewDecisionStale ? "danger" : "warning"; const detail = workload.compose?.project ? `Compose ${workload.compose.project} · ${(workload.compose.services || []).join(", ") || "services unknown"}` : workload.dockerMan?.templatePath ? `DockerMan ${workload.dockerMan.name || workload.displayName} · ${containers || "template only"}` : `Container installation · ${containers || "unnamed"}`; const candidate = linked ? `Linked to ${workload.link?.repositoryFullName || "repository"}` : inconsistentLink ? `Stored link cannot be resolved to a loaded repository profile` : topCandidate ? `${topCandidate.repositoryFullName} suggested · ${topCandidate.confidence || topCandidate.status || "review required"}` : "No repository candidate; select one manually"; const canQuickLink = !linked && topCandidate && ["exact", "strong"].includes(topCandidate.confidence) && Boolean(workload.remoteFolderCandidate); const linkButton = canQuickLink ? `` : ``; const evidenceNote = workload.reviewDecisionStale ? "Saved decision is stale because server evidence changed" : workload.classification?.reason || "Awaiting review"; return `
${escapeHtml(workload.displayName)}${escapeHtml(detail)} · ${workload.runtime?.running ? "running" : "stopped"}${escapeHtml(candidate)}${escapeHtml(inconsistentLink ? "Reconcile this inventory link before deployment" : evidenceNote)}${workload.metadata?.composeDefinitionError ? `Compose file found; validation warning: ${escapeHtml(workload.metadata.composeDefinitionError)}` : ""}
${escapeHtml(workload.reviewDecisionStale ? "Decision stale" : linked ? "Linked" : inconsistentLink ? "Link unresolved" : classification)}${linked ? `` : inconsistentLink ? `` : linkButton}
`; }).join("") : `

${server.error ? "No inventory could be read until the SSH connection works." : "Docker returned no containers, Compose projects or DockerMan templates."}

`; const resolvedLinks = visibleWorkloads.filter((workload) => { const repository = ui.repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase()); return repository?.deploymentProfiles?.some((profile) => profile.id === workload.link?.profileId); }).length; const unresolvedLinks = visibleWorkloads.filter((workload) => { if (!(workload.status === "linked" || workload.link)) return false; const repository = ui.repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link?.repositoryFullName || "").toLowerCase()); return !repository?.deploymentProfiles?.some((profile) => profile.id === workload.link?.profileId); }).length; return `

${escapeHtml(server.serverName || server.server?.name || server.serverId)}

${server.running || 0} running · ${resolvedLinks} visible repository links${unresolvedLinks ? ` · ${unresolvedLinks} unresolved` : ""} · ${visibleWorkloads.filter((workload) => !workload.link).length} to review${hiddenCount ? ` · ${hiddenCount} unrelated/system workloads hidden` : ""}
${server.error ? "Scan failed" : escapeHtml(capabilityText)}${server.error ? "" : ``}
${errorBlock}${warnings}
${workloads}
`; }).join(""); const empty = configuredServers.length ? `

Server inventory has not completed

ForgeFlow will query Docker directly. A failed connection is shown explicitly instead of being reported as zero deployments.

` : `

No Unraid server configured

Add the server with password authentication and ForgeFlow can copy and deploy projects directly.

`; return `

Server inventory

Live Docker, Compose and DockerMan discovery, linked to Gitea
${servers.length ? `
${serverCards}
` : empty}
${icon("shield")}Server pull fetches an exact Gitea commit through a repository-scoped read-only deploy key, validates Compose and only then promotes the release. Direct copy remains an explicit fallback.
`; } function renderDeployments() { const cards = ui.repositories.flatMap((repository) => (repository.deploymentProfiles || []).map((profile) => ({ repository, profile })), ); const active = operations().filter((operation) => !isTerminalOperation(operation.status)); const missingDockerMan = cards.filter(({ profile }) => profile.provider === "ssh-unraid" && profile.manageDockerMan === true && profile.state?.containerRunning && !dockerManIntegration(profile).ready, ); return `
${active.length ? `
${icon("pulse")} ${active.length} deployment operation${active.length === 1 ? " is" : "s are"} still active. ForgeFlow reconciles these against the live server automatically.
` : ""}${renderServerInventory()}

Linked deployment environments

Stable Compose identity, live container health and exact Gitea commit parity
${cards.length ? cards.map(({ repository, profile }) => renderProfileCard(repository, profile, true)).join("") : '

No deployment environments configured

Scan a server and link an existing workload, or open a repository and add an environment.

'}

All operations

Newest first
${operations().length ? `${operations().map((operation) => ``).join("")}
RepositoryActionEnvironmentCommitStatusUpdated
${escapeHtml(operation.repository)}${escapeHtml(operation.action || "deploy")}${escapeHtml(operation.environment || "—")}${escapeHtml(operation.shortSha || shortSha(operation.sha))}${escapeHtml(operation.status)}${formatDate(operation.updatedAt || operation.createdAt)}
` : '

No operations recorded.

'}
`; } function renderSettings() { const state = ui.boot.state; const prefs = state.preferences || {}; const update = ui.updateStatus; const servers = state.servers || []; return `

Gitea connection

${state.gitea.hasToken ? `Connected as ${escapeHtml(state.gitea.user?.login || "user")}` : "Not connected"}
${escapeHtml(state.gitea.baseUrl || "No Gitea instance configured")}

ForgeFlow updates

Secure source update from ${escapeHtml(state.updates?.owner || "Jens")}/${escapeHtml(state.updates?.repo || "ForgeFlow")}
${icon(update?.available ? "download" : "check")}${update ? (update.available ? `ForgeFlow ${escapeHtml(update.remoteVersion)} is available` : `ForgeFlow ${escapeHtml(update.currentVersion)} is up to date`) : `Current version ${escapeHtml(ui.boot.appVersion)}`}${update ? `Branch ${escapeHtml(update.branch)} · commit ${escapeHtml(update.shortSha)} · checked ${formatDate(update.checkedAt)}` : "No update check in this session."}
${update?.available && !update.downloaded ? `` : ""}${update?.downloaded ? `` : ""}
${icon("shield")}The updater downloads an authenticated ZIP for the exact remote commit, verifies its SHA-256 checksum, runs the complete quality gate and restores the previous source version if validation fails.

SSH / Unraid servers

Credentials are entered locally and encrypted with the Windows credential protection used by Electron.
${servers.length ? `
${servers.map((server) => `
${icon("server")}
${escapeHtml(server.name)}${escapeHtml(server.username)}@${escapeHtml(server.host)}:${escapeHtml(server.port)} · ${escapeHtml(server.basePath)}${server.hostFingerprint ? `Trusted ${escapeHtml(server.hostFingerprint)}` : "Host identity not trusted yet"}
`).join("")}
` : '

No SSH server configured. Add your Unraid server before creating an SSH deployment profile.

'}

Git remote maintenance

Standardize linked repositories to the current Gitea SSH URLs.

This replaces legacy aliases and renamed owners only after an explicit click. Local commits and files are not changed.

Project roots

The first folder is the default clone destination. ForgeFlow automatically creates one subfolder per repository.

${state.workspaceRoots.map((root, index) => `
${index === 0 ? 'Default' : ""}
`).join("")}

Background awareness

Desktop integration

Separate arguments with |. Placeholders: {path}, {file}, {line}

Encrypted configuration backup

Repository mappings, servers, deployment profiles and preferences are encrypted. Tokens, passwords, passphrases and operation history are never exported.

Appearance

Danger zone

Reset removes local ForgeFlow configuration, repository links, profiles and operation history. It does not modify Git repositories or Gitea.

`; } function preflightTone(status) { return status === "pass" ? "success" : status === "fail" ? "danger" : status === "warning" ? "warning" : ""; } function renderPreflightChecks( report, emptyMessage = "Run the preflight to verify this configuration.", ) { if (!report?.checks?.length) return `

${escapeHtml(emptyMessage)}

`; return `
${report.checks.map((item) => `
${item.status === "pass" ? icon("check") : item.status === "fail" ? icon("error") : icon("warning")}
${escapeHtml(item.label)}${escapeHtml(item.detail)}${item.help ? `${escapeHtml(item.help)}` : ""}${item.repairAction ? `` : ""}
${escapeHtml(item.status)}
`).join("")}
`; } function renderDiagnostics() { const prefs = ui.boot.state.preferences || {}; const status = ui.diagnosticsStatus || ui.boot.diagnostics || {}; const report = ui.systemPreflight; const trouble = ui.troubleshooter; const troubleRows = trouble?.issues ?.map( (item, index) => `
${icon(item.severity === "error" ? "error" : "warning")}
${escapeHtml(item.title)}${escapeHtml(item.repository || "System")} · ${escapeHtml(item.detail)}
${item.repairable ? `` : 'Manual review'}
`, ) .join("") || ""; return `
${icon("shield")}Credentials are never added to the diagnostic bundle. Known runtime secrets are redacted again during export. You can inspect the ZIP before sharing it.

Log storage

${status.lastWriteError ? "Write error" : status.enabled ? "Recording" : "Disabled"}
Files${escapeHtml(status.fileCount ?? "—")}
Total size${escapeHtml(status.totalSize || "—")}
Latest event${status.latestAt ? formatDate(status.latestAt) : "None"}
Retention${escapeHtml(status.retentionDays || prefs.logRetentionDays || 14)} days
Location${escapeHtml(status.directory || "Unavailable")}
Level${escapeHtml(status.level || prefs.diagnosticLevel || "info")}
${status.lastWriteError ? `
Error${escapeHtml(status.lastWriteError)}
` : ""}

Recording policy

One-click troubleshooter

Git locks, interrupted operations, branch synchronization and deployment/server inconsistencies
${trouble?.issues?.some((item) => item.repairable && item.safe) ? `` : ""}
${trouble ? `${trouble.summary.total ? `${trouble.summary.total} issue(s)` : "Healthy"}${trouble.summary.errors} errors · ${trouble.summary.warnings} warnings · ${trouble.summary.repairable} repairable` : "Run the troubleshooter to inspect all linked repositories and deployments."}
${troubleRows || '

No problems detected.

'}

System preflight

Git, writable storage, credential protection, folders and Gitea
${report ? `${report.summary.ready ? "Ready" : `${report.summary.blocking.length} blocking`}${report.summary.counts.pass} passed · ${report.summary.counts.warning} warnings · ${report.summary.counts.fail} failed` : "Not run in this session"}
${renderPreflightChecks(report)}

Export support bundle

Configuration summary, repository states, operations, preflight and redacted JSONL logs
${ui.lastDiagnosticBundle ? `
${icon("check")}
${escapeHtml(ui.lastDiagnosticBundle.size)} bundle created

SHA-256 ${escapeHtml(ui.lastDiagnosticBundle.sha256)}

` : ""}
`; } function renderPipelineView() { const operation = ui.activeDeployment; if (!operation) return '

No deployment operation selected.

'; const logs = (operation.logs || []).join("\n"); return `

${escapeHtml(operation.status)}

${escapeHtml(operation.profileName || operation.workflowFile || "")} · ${escapeHtml(operation.shortSha || shortSha(operation.sha))}

${escapeHtml(operation.status)}
${(operation.stages || []).map((stage) => `
${stage.status === "complete" ? icon("check") : stage.status === "failed" ? icon("error") : stage.status === "active" ? icon("pulse") : icon("clock")}${escapeHtml(stage.label)}
`).join("")}
${operation.jobs?.length ? `

Runner jobs

${operation.jobs.map((job) => ``).join("")}
JobStatusStartedCompleted
${escapeHtml(job.name)}${escapeHtml(job.conclusion || job.status)}${job.startedAt ? formatDate(job.startedAt) : "—"}${job.completedAt ? formatDate(job.completedAt) : "—"}
` : ""}
Deployment output
${escapeHtml(logs || "Waiting for operation output…")}
${operation.failure ? `
${icon("error")}
${escapeHtml(operation.failure.stage)}

${escapeHtml(operation.failure.message)}

` : ""}
`; } function renderStatusbar() { const state = ui.boot?.state; const repository = selectedRepository(); const active = operations().filter( (operation) => !isTerminalOperation(operation.status), ).length; return ``; } function renderSetup() { const steps = ["Readiness", "Gitea", "Folders", "Discovery", "Ready"]; let body = ""; if (ui.setupStep === 0) { body = `

Check this computer

ForgeFlow verifies Git, writable storage and protected credential support before you enter any connection details.

${icon("shield")}Your Gitea token is entered only inside this local desktop application. It is never included in diagnostic logs or support bundles.
${renderPreflightChecks(ui.systemPreflight, "Run the readiness check to verify this computer.")}
Available even before Gitea is connected.
`; } else if (ui.setupStep === 1) { body = `

Connect your Gitea instance

Enter the URL and a personal access token created on your own Gitea server. ForgeFlow validates it locally and stores it using operating-system encryption when available.

${ui.setupValidation ? `
${icon("check")}Connected as ${escapeHtml(ui.setupValidation.user.login)} · ${ui.setupValidation.repositoryCount} repositories · Gitea ${escapeHtml(ui.setupValidation.version || "version unknown")}
` : `
${icon("shield")}Use the narrowest permissions that allow repository reads and Actions workflow dispatch. The setup guide explains this without requiring you to share the token.
`}
`; } else if (ui.setupStep === 2) { body = `

Select development folders

Choose parent folders. ForgeFlow discovers Git working trees below them and matches their origin to Gitea.

${ui.setupDraft.roots.map((root, index) => `
`).join("")}
`; } else if (ui.setupStep === 3) { body = `

Discovering repositories

Inspecting local Git metadata. Generated folders and nested dependency trees are skipped.

Scanning configured folders…
`; } else { body = `

ForgeFlow is ready

${ui.setupDraft.discovered.length} local repositories were found. You can add deployment environments after opening a repository.

Gitea connected${escapeHtml(ui.setupDraft.baseUrl)} · ${escapeHtml(ui.setupDraft.user?.login || "user")}
Workspace discovery${ui.setupDraft.roots.length} root folder(s), ${ui.setupDraft.discovered.length} repository/repositories
Safe diagnosticsStructured local logs with credential redaction are enabled by default.
${ ui.setupDraft.discovered.length ? ui.setupDraft.discovered .slice(0, 8) .map( (item) => `
${icon(item.error ? "error" : "git")}
${escapeHtml(item.localPath.split(/[\\/]/).pop())}${escapeHtml(item.localPath)}
${item.error ? "Unreadable" : "Ready"}
`, ) .join("") : '

No repositories found. You can link or clone repositories later.

' }
`; } const nextAction = ui.setupStep === 0 ? ui.systemPreflight?.summary?.ready ? '' : '' : ui.setupStep === 1 ? '' : ui.setupStep === 2 ? `` : ui.setupStep === 4 ? '' : ""; return `
${body}
`; }