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", help: "Help center", "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)}
` : ""} ${ui.refreshWarning ? `
${icon("warning")} ${escapeHtml(ui.refreshWarning)}
` : ""}
${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" : `Local every ${ui.boot.state.preferences?.repositoryPollSeconds || 4}s · Gitea every ${ui.boot.state.preferences?.fetchIntervalMinutes || "manual"}${ui.boot.state.preferences?.fetchIntervalMinutes ? " min" : ""}`)}
`; } 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 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 || []; const status = repository.localStatus || {}; const branchRows = 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.

'; const stashRows = ui.stashes.length ? ui.stashes.map((stash) => `
${escapeHtml(stash.ref)}${escapeHtml(stash.subject)} · ${formatDate(stash.date)}
${stash.quarantined ? `Codex review required` : ``}
`).join("") : '

No stashes, or Git tools have not been loaded.

'; const recoveryBody = 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.

'; const syncState = status.counts?.changed ? `${status.counts.changed} local file${status.counts.changed === 1 ? "" : "s"} need protection` : status.branch?.ahead || status.branch?.behind ? `${status.branch.ahead || 0} ahead · ${status.branch.behind || 0} behind` : "Preview against Gitea before changing files"; return `

Branches

${branchRows}

Stashes

${stashRows}

Gitea workspace sync

Make tracked files match the current upstream branch exactly
${escapeHtml(syncState)}

Safe mirror, never silent overwrite

ForgeFlow fetches Gitea, previews additions, changes and deletions, then protects local Codex work before resetting. Local commits go to a recovery branch; modified and untracked files go to a stash.

${icon("shield")}Ignored runtime data such as .env, dependency folders and local databases is preserved. Background awareness only fetches; it never applies this sync automatically.
${escapeHtml(status.branch?.head || "No branch")} → ${escapeHtml(status.branch?.upstream || "No upstream")}

Repository troubleshooting

Safe, repository-specific recovery actions
${recoveryBody}
${repository.sshUrl && status.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 link${resolvedLinks === 1 ? "" : "s"}${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.

'}
`; } const HELP_TOPICS = [ { id: "getting-started", icon: "rocket", category: "Basics", title: "Start with a repository", summary: "Connect Gitea, discover local projects and understand the Local → Gitea → Server flow.", keywords: "setup connect token roots clone local remote overview", steps: [ "Open Settings and validate the Gitea URL and token.", "Add the parent folders that contain your projects, then save and rescan.", "Select a repository. The release rail shows local changes, Gitea parity and the linked server release.", "Use Changes to review work; use Git tools for branches, synchronization and pull requests.", ], note: "ForgeFlow does not modify a project merely because you opened it or refreshed the overview.", }, { id: "workspace-sync", icon: "refresh", category: "Git & Gitea", title: "Make a local project match Gitea", summary: "Clean tracked leftovers without losing local Codex work or ignored runtime data.", keywords: "sync mirror pull reset deleted files cleanup stash recovery codex upstream dirty", steps: [ "Open the repository, choose Git tools and select Preview Gitea sync.", "Review every file that will be added, changed or removed.", "Choose Protect local work & synchronize only when the preview matches your intention.", "ForgeFlow creates a recovery branch for local commits and a stash for modified or untracked files before matching the upstream commit.", ], note: "Ignored files such as .env, local databases and dependency folders remain untouched. Background awareness only fetches metadata and never applies a sync.", }, { id: "changes", icon: "file", category: "Git & Gitea", title: "Review, commit and publish changes", summary: "Keep intentional local changes separate from remote updates.", keywords: "changes stage hunk commit push branch pull request conflict", steps: [ "Review selected files or individual hunks in Changes.", "Enter a clear commit message and commit the reviewed selection.", "Fetch before publishing; if Gitea changed, synchronize or resolve divergence first.", "Push directly only when branch protection permits it, otherwise create a pull request.", ], note: "The action panel explains the current blocker and only enables operations that are safe for the selected state.", }, { id: "deployment-linking", icon: "link", category: "Deployments", title: "Link server workloads to repositories", summary: "Turn Docker, Compose and DockerMan evidence into one explicit repository deployment.", keywords: "server inventory docker compose dockerman container detect linked review reconcile", steps: [ "Open Deployments and scan the configured server.", "Review proposed matches. ForgeFlow uses Compose paths, Git provenance, labels and container metadata; names alone are not trusted.", "Confirm Review & link for a correct candidate, or choose the repository manually.", "Use Review reconciliation whenever a saved link conflicts with current server evidence.", ], note: "External and system containers remain monitoring-only until you explicitly link them. A linked workload also appears on the matching repository.", }, { id: "deployments", icon: "deploy", category: "Deployments", title: "Deploy an exact Gitea commit", summary: "Validate, pull, build and promote a release without damaging the live service.", keywords: "deploy release unraid preflight rollback health exact sha commit server pull", steps: [ "Open the repository Deployments tab and select the intended environment.", "Run preflight and repair blocking configuration before deployment.", "Deploy only the shown target commit. Server pull fetches that exact commit instead of an ambiguous latest branch state.", "Follow the run stages and health result. Failed promotion keeps or restores the previous release where supported.", ], note: "Commit parity means Local, Gitea and Server identify the same revision; a running container alone does not prove a correct release.", }, { id: "deploy-keys", icon: "key", category: "Deployments", title: "Repair repository deploy keys", summary: "Give the server read-only access to exactly the repository it must pull.", keywords: "ssh key deploy key gitea permission server pull fingerprint authentication", steps: [ "Run deployment preflight for the environment.", "Use the offered deploy-key repair when repository access is missing.", "ForgeFlow creates or reuses a repository-scoped key, registers the public key read-only in Gitea and verifies access from the server.", "Re-run preflight and deploy only after the server can resolve and fetch the target commit.", ], note: "The private key remains on the configured server. ForgeFlow does not copy your personal Gitea token into deployment commands.", }, { id: "git-validator", icon: "shield", category: "Quality", title: "Use Git Validator safely", summary: "Find repository hygiene issues and apply only reviewed repairs.", keywords: "validator hygiene gitignore readme license branch protection secrets large files fix repair", steps: [ "Open a repository and choose Git Validator.", "Select the policy that fits the repository and run a fresh scan.", "Open each finding to understand its evidence and recommended repair.", "Preview repairable findings before applying them, then rescan to verify the result.", ], note: "A score is guidance, not proof of correctness. Repairs that can alter repository policy or files always require an explicit action.", }, { id: "updates-diagnostics", icon: "update", category: "Maintenance", title: "Update or troubleshoot ForgeFlow", summary: "Install signed releases and collect useful diagnostics without exposing credentials.", keywords: "update installer signed release diagnostics logs support error updater restart", steps: [ "Open Settings and choose Check now under ForgeFlow updates.", "Download the signed packaged release, then choose Apply & restart.", "If an operation fails, open Diagnostics and run the troubleshooter.", "Export a diagnostic bundle when deeper inspection is needed; tokens, passwords and private keys are redacted.", ], note: "Binary auto-update is available only from a packaged installation. Source checkouts continue to use the normal development workflow.", }, ]; function renderHelp() { const query = String(ui.helpQuery || "").trim().toLowerCase(); const topics = HELP_TOPICS.filter((topic) => !query || `${topic.category} ${topic.title} ${topic.summary} ${topic.keywords} ${topic.steps.join(" ")} ${topic.note}`.toLowerCase().includes(query), ); const categories = [...new Set(HELP_TOPICS.map((topic) => topic.category))]; const topicMarkup = topics.map((topic) => { const isOpen = topic.id === ui.helpTopic || Boolean(query); return `
${icon(topic.icon)}${escapeHtml(topic.category)}${escapeHtml(topic.title)}${escapeHtml(topic.summary)}${icon("chevron", "help-chevron")}
    ${topic.steps.map((step) => `
  1. ${escapeHtml(step)}
  2. `).join("")}
${icon("shield")}${escapeHtml(topic.note)}
`; }).join(""); return `
ForgeFlow guide

How can we help?

Clear, practical instructions for repositories, Gitea synchronization, server deployments and maintenance.

${projectIllustration("flow")}

${query ? `Search results` : "Everything you need to operate ForgeFlow"}

${topics.length} topic${topics.length === 1 ? "" : "s"}${query ? ` matching “${escapeHtml(ui.helpQuery)}”` : ""}
${topicMarkup || `

No help topic found

Try a broader term such as sync, deployment, keys, validator or update.

`}
`; } 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

Signed packaged updates 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${update.packaged ? "" : " in the source repository"}` : `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.packaged && !update.downloaded ? `` : ""}${update?.downloaded ? `` : ""}
${icon("shield")}${update && !update.packaged ? "Source checkouts must be updated with Git after reviewing the exact commit. Integrated source replacement remains disabled until source archives are publisher-signed." : "Packaged updates require an Ed25519 publisher signature that binds the exact commit, artifact name, size and SHA-256 digest."}

SSH / Unraid servers

Credentials are encrypted locally; a new host fingerprint is shown before authentication.
${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

Read-only remote awareness. Use 0 to disable; fetching never changes project files.
${icon("shield")}Remote awareness only fetches branch metadata. ForgeFlow never resets, cleans or overwrites a workspace in the background.

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)}

` : ""}

Operational audit log

Append-only release, pull-request and recovery events
${ui.auditEvents.length ? `${ui.auditEvents.map((item) => ``).join("")}
TimeEventRepositoryResult
${formatDate(item.timestamp)}${escapeHtml(item.event)}${escapeHtml(item.details?.repository || "—")}${escapeHtml(item.details?.result || item.details?.note || "—")}
` : '

Load the operational audit log.

'}
`; } 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}
`; }