function navButton(view, label, iconName, count = "") {
return `${icon(iconName)}${label} ${count !== "" ? `${count} ` : ""} `;
}
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 `
ForgeFlow by ITWorx.tech ${escapeHtml(title)}
`;
}
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 `
${repository.favorite ? icon("star") : icon(repository.localPath ? "git" : "cloud")}
${escapeHtml(repository.name)} ${escapeHtml(branch)} ${status?.shortHead ? `• ${escapeHtml(status.shortHead)} ` : ""}
${badges.join("")}
`;
}
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 `
${kind === "deploy" ? "Live release topology" : kind === "repo" ? "Project signal" : "Release flow"}
`;
}
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)}
Open ${icon("arrowRight")} `;
})
.join("")
: '
✓
Everything is synchronized No repository needs immediate attention.
'
}
${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.
'}
${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 `${escapeHtml(extension)} change map +${additions} −${removals}
`;
}
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.
'}
${icon("cloud")}${escapeHtml(clonePrimaryLabel(repository))} ${icon("link")}Link existing folder Choose another location
`;
}
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.
${icon("refresh")}Fetch remote state ${icon("folder")}Open project
`;
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.
Use ours Use theirs Mark resolved
`
: "";
return `${ui.selectedFiles.size} selected · ${status.counts.changed} changed · ${status.counts.staged} staged${ui.selectedFiles.size === status.files.length ? "Clear" : "Select all"}
${status.files.map((file) => `
${fileStatusCode(file)} ${escapeHtml(file.path)} ${file.staged ? "●" : "○"}
`).join("")}
${status.counts.conflicts ? `${icon("warning")}Conflict guide
` : ""}${conflictActions}${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.
Load history `;
return `Commit Message Author Date ${ui.history.map((commit) => `${escapeHtml(commit.shortSha)} ${escapeHtml(commit.subject)} ${escapeHtml(commit.author)} ${formatDate(commit.date)} `).join("")}
`;
}
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"
? `${icon("shield")}Verify server pull ${icon("key")}Deploy key lifecycle ${icon("key")}Configure Gitea access `
: "";
return `${escapeHtml(identity.initial)} Container ${escapeHtml(identity.name)} ${escapeHtml(repository.fullName)} · ${escapeHtml(profile.environment)}
${syncLabel}
Live commit ${state.liveSha ? shortSha(state.liveSha) : "Unknown"} Deploy source ${escapeHtml(sourceLabel)} Previous version ${state.previousSha ? shortSha(state.previousSha) : "Unknown"} Last checked ${state.checkedAt ? formatDate(state.checkedAt) : "Never"} ${isSsh ? `Deployment mode ${escapeHtml(modeLabel)} Compose project ${escapeHtml(profile.composeProject || "ForgeFlow-generated identity")} Runtime ${state.containerRunning === false ? "Stopped" : state.containerRunning ? state.runtimeVerification === "running-unverified" ? "Running · unverified" : "Running" : "Unknown"} DockerMan ${escapeHtml(dockerManLabel)} ` : ""}Rollback ${rollbackConfigured ? "Available after first deploy" : "Not configured"}
${icon("shield")}Preflight ${isSsh ? `${icon("wrench")}Check / fix write access ` : ""}${serverAccessAction}${icon("refresh")}Refresh truth ${webUi ? `${icon("external")}Open Web UI ` : ""}${managesDockerMan ? `${icon("wrench")}${dockerManReady ? "Reapply DockerMan integration" : "Repair DockerMan integration"} ` : ""}${ready ? `${icon("rocket")}Deploy ${escapeHtml(shortSha(targetSha))} ` : ""}Edit ${state.previousSha && rollbackConfigured ? `${icon("undo")}Rollback ` : ""}
`;
}
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 ``;
}).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 ${icon("plus")}Add environment ${workloads.length ? `
` : ""}${profiles.length ? `
${profiles.map((profile) => renderProfileCard(repository, profile)).join("")}
` : '
↗
No deployment profile Connect a Gitea Actions workflow or a trusted SSH / Unraid server.
Configure deployment '}
Release history ${repoOps.length ? `
Action Environment Commit Status Updated ${repoOps.map((operation) => `${escapeHtml(operation.action || "deploy")} ${escapeHtml(operation.environment)} ${escapeHtml(operation.shortSha || shortSha(operation.sha))} ${escapeHtml(operation.status)} ${formatDate(operation.updatedAt || operation.createdAt)} Open `).join("")}
` : '
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 `${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 ? `
` : ""}${recommendations.length ? `
` : ""}` : '
Scan before repairing. ForgeFlow checks every .lock file in the actual Git directory, not only index.lock.
'}
${icon("wrench")}Repair proven stale locks ${icon("refresh")}Refresh Git state ${repository.sshUrl && repository.localStatus?.remoteUrl !== repository.sshUrl ? `${icon("link")}Repair origin ` : ""}
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 ${icon("folder")}${repository.localPath ? "Open project folder" : "Link local folder"} ${originNeedsRepair ? `${icon("link")}Use current Gitea origin ` : ""}${repository.localPath ? `${icon("pulse")}Scan Git health ${icon("link")}Remove link ` : `${icon("cloud")}${escapeHtml(clonePrimaryLabel(repository))} Choose another location `}
Open pull requests Live from Gitea ${icon("refresh")}Refresh ${pullRequests.length ? `` : ''}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.
${icon("shield")}Run Git Validator `;
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")}Assurance policy ${["minimal", "standard", "strict", "production"].map((policy) => `${policy[0].toUpperCase() + policy.slice(1)} `).join("")} ${icon("refresh")}Scan again Export report
${Object.entries(
groups,
)
.map(
([category, checks]) =>
`
${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 ? `${icon("wrench")}Preview fix ` : check.status !== "pass" && !check.suppressed ? `Document exception ` : `${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 `${escapeHtml(identity)} ${escapeHtml(item.environment)}${workload?.serverName ? ` · ${escapeHtml(workload.serverName)}` : ""} `;
}).join("")}
View all `
: "";
return `
${repository.localPath ? `
${icon("external")}Open in editor ${icon("terminal")}Open terminal ${icon("shield")}Check branch protection ${icon("git")}Create pull request ${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}
${[
["changes", "Changes"],
["history", "History"],
["deployments", "Deployments"],
["gittools", "Git tools"],
["validator", "Git Validator"],
["settings", "Project settings"],
]
.map(
([id, label]) =>
`${label} `,
)
.join("")} ${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.
'}
${icon("cloud")}${escapeHtml(clonePrimaryLabel(repository))} Link existing folder Choose another clone location `;
} 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 = `Commit message required ${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)}
${icon("arrowUp")}${ui.selectedFiles.size ? "Commit selected" : "Commit staged hunks"} & push to Gitea ${icon("git")}${ui.selectedFiles.size ? "Commit selected locally" : "Commit staged hunks locally"} Partial hunk staging is preserved when no complete files are selected.
Stage selected files Unstage selected files ${icon("archive")}Stash all changes
`;
} else if (action.kind === "pull")
body = `${icon("arrowDown")}
${action.title} ${action.detail}
Fast-forward from Gitea `;
else if (action.kind === "push")
body = `${icon("arrowUp")}
${action.title} ${action.detail}
Push ${status.branch.ahead} commit${status.branch.ahead === 1 ? "" : "s"} `;
else if (
action.kind === "diverged" ||
action.kind === "conflict" ||
action.kind === "error"
)
body = `${icon("error")}
${action.title} ${action.detail}
${action.kind === "diverged" ? `
${icon("wrench")}Open guided repository repair ` : ""}
Open project folder Refresh status `;
else if (action.kind === "configure")
body = `${icon("settings")}
${action.title} ${action.detail}
Configure first environment `;
else if (action.kind === "branch-profile")
body = `${icon("branch")}
${action.title} ${action.detail}
${repository.deploymentProfiles.length > 1 ? `
Deployment profile ${repository.deploymentProfiles.map((item) => `${escapeHtml(item.name)} · ${escapeHtml(item.branch)} `).join("")} ` : ""}
Edit profile `;
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 ? `
Environment ${repository.deploymentProfiles.map((item) => `${escapeHtml(item.name)} · ${escapeHtml(item.environment)} `).join("")} ` : ""}
Local ${escapeHtml(status.shortHead)} Gitea ${escapeHtml(status.shortHead)} Target ${escapeHtml(profile.environment)}
${icon("rocket")}Deploy ${escapeHtml(status.shortHead)} → ${escapeHtml(profile.environment)} ${profile.state?.previousSha && profile.rollbackWorkflowFile ? `
${icon("undo")}Rollback to ${shortSha(profile.state.previousSha)} ` : ""}
`;
else
body = `${icon("check")}
${action.title} ${action.detail}
${profile ? `
${icon("pulse")}Check ${escapeHtml(profile.environment)} ` : ""}
`;
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)}
Use server password instead Test connection
`
: "";
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
? `${icon("link")}Link to ${escapeHtml(topCandidate.repositoryName || topCandidate.repositoryFullName)} `
: `${icon("link")}Review & link `;
const evidenceNote = workload.reviewDecisionStale ? "Saved decision is stale because server evidence changed" : workload.classification?.reason || "Awaiting review";
return ``;
}).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 `${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.
Scan servers now `
: `No Unraid server configured Add the server with password authentication and ForgeFlow can copy and deploy projects directly.
Add server `;
return `
Server inventory Live Docker, Compose and DockerMan discovery, linked to Gitea ${icon("refresh")}Scan servers ${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 ? `
Repository Action Environment Commit Status Updated ${operations().map((operation) => `${escapeHtml(operation.repository)} ${escapeHtml(operation.action || "deploy")} ${escapeHtml(operation.environment || "—")} ${escapeHtml(operation.shortSha || shortSha(operation.sha))} ${escapeHtml(operation.status)} ${formatDate(operation.updatedAt || operation.createdAt)} Open `).join("")}
` : '
'}
`;
}
function renderSettings() {
const state = ui.boot.state;
const prefs = state.preferences || {};
const update = ui.updateStatus;
const servers = state.servers || [];
return `${icon("settings")}General ${icon("update")}Updates ${icon("server")}Servers ${icon("trash")}Reset setup
Gitea connection ${state.gitea.hasToken ? `Connected as ${escapeHtml(state.gitea.user?.login || "user")}` : "Not connected"} ${escapeHtml(state.gitea.baseUrl || "No Gitea instance configured")}
Validate & save
ForgeFlow updates Secure source update from ${escapeHtml(state.updates?.owner || "Jens")}/${escapeHtml(state.updates?.repo || "ForgeFlow")} ${icon("update")}${ui.updateChecking ? "Checking…" : "Check now"} ${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 ? `${icon("download")}Download update ` : ""}${update?.downloaded ? `${icon("update")}Apply & restart ` : ""}Save update settings
${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. ${icon("plus")}Add server ${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"}
Test & trust Edit ${icon("trash")}
`).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. ${icon("link")}Normalize all origins 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 ' : ""}${icon("trash")}
`).join("")}
${icon("plus")}Add project root Save folders & rescan
Background awareness Save awareness settings
Desktop integration Save desktop integration
Appearance Color theme Dark Light Follow system
Danger zone Reset removes local ForgeFlow configuration, repository links, profiles and operation history. It does not modify Git repositories or Gitea.
Reset ForgeFlow
`;
}
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 ? `${icon("wrench")}${escapeHtml(item.repairLabel || "Repair")} ` : ""}
${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 ? `
${icon("wrench")}${item.safe ? "Repair" : "Review & repair"} ` : '
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.
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)}
` : ""}
${icon("folder")}Open logs ${icon("trash")}Clear logs
One-click troubleshooter Git locks, interrupted operations, branch synchronization and deployment/server inconsistencies ${icon("pulse")}Scan everything ${trouble?.issues?.some((item) => item.repairable && item.safe) ? `${icon("wrench")}Repair ${trouble.issues.filter((item) => item.repairable && item.safe).length} safe issue(s) ` : ""}
${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 || '
'}
System preflight Git, writable storage, credential protection, folders and Gitea ${icon("shield")}Run checks ${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 ${icon("archive")}Create diagnostic ZIP
${ui.lastDiagnosticBundle ? `
${icon("check")}
${escapeHtml(ui.lastDiagnosticBundle.size)} bundle created SHA-256 ${escapeHtml(ui.lastDiagnosticBundle.sha256)}
Show file ` : ""}
`;
}
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 Job Status Started Completed ${operation.jobs.map((job) => `${escapeHtml(job.name)} ${escapeHtml(job.conclusion || job.status)} ${job.startedAt ? formatDate(job.startedAt) : "—"} ${job.completedAt ? formatDate(job.completedAt) : "—"} `).join("")}
` : ""}
Deployment output ${icon("copy")}Copy
${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 `${icon("git")}${escapeHtml(ui.boot?.git?.version || "Git unavailable")} ${icon("folder")}${state?.workspaceRoots?.length || 0} roots ${repository?.localStatus ? `${icon("branch")}${escapeHtml(repository.localStatus.branch.head)} ` : ""}
${ui.autoRefreshPending ? `${icon("refresh")}Change detected ` : ""}${active ? `${icon("pulse")}${active} active ` : ""}ForgeFlow ${escapeHtml(ui.boot?.appVersion || "")}
`;
}
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.")}
${icon("archive")}Export setup diagnostics 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) => `
${icon("trash")}
`).join("")}
${icon("plus")}Add development folder `;
} 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 diagnostics Structured 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
? 'Continue '
: 'Run readiness check '
: ui.setupStep === 1
? 'Validate & continue '
: ui.setupStep === 2
? `Scan folders `
: ui.setupStep === 4
? 'Enter ForgeFlow '
: "";
return ``;
}