|
|
|
@@ -15,6 +15,7 @@ function renderTitlebar() {
|
|
|
|
|
deployments: "Deployments",
|
|
|
|
|
diagnostics: "Diagnostics",
|
|
|
|
|
settings: "Settings",
|
|
|
|
|
help: "Help center",
|
|
|
|
|
"deployment-run": "Deployment run",
|
|
|
|
|
}[ui.currentView] || "Workspace";
|
|
|
|
|
return `<header class="titlebar">
|
|
|
|
@@ -87,7 +88,7 @@ function renderSidebar() {
|
|
|
|
|
).length;
|
|
|
|
|
const rows = (list) => list.map(renderRepositoryRow).join("");
|
|
|
|
|
return `<aside class="sidebar">
|
|
|
|
|
<nav class="primary-nav">${navButton("overview", "Overview", "overview", attention || "")}${navButton("deployments", "Deployments", "deploy", operations().filter((item) => item.type === "deployment" && !isTerminalOperation(item.status)).length || "")}${navButton("diagnostics", "Diagnostics", "shield", ui.diagnosticsStatus?.lastWriteError ? "!" : "")}${navButton("settings", "Settings", "settings")}</nav>
|
|
|
|
|
<nav class="primary-nav">${navButton("overview", "Overview", "overview", attention || "")}${navButton("deployments", "Deployments", "deploy", operations().filter((item) => item.type === "deployment" && !isTerminalOperation(item.status)).length || "")}${navButton("diagnostics", "Diagnostics", "shield", ui.diagnosticsStatus?.lastWriteError ? "!" : "")}${navButton("settings", "Settings", "settings")}${navButton("help", "Help", "help")}</nav>
|
|
|
|
|
<div class="sidebar-section"><span>Repositories</span><button data-action="refresh" title="Refresh">${icon("refresh")}</button></div>
|
|
|
|
|
<input class="repo-filter" id="repo-filter" value="${attr(ui.repoSearch)}" placeholder="Filter projects" aria-label="Filter projects" />
|
|
|
|
|
<div class="repo-list">
|
|
|
|
@@ -380,7 +381,7 @@ function renderGitTools(repository) {
|
|
|
|
|
return `<div class="tab-page git-tools-grid">
|
|
|
|
|
<section class="panel"><div class="panel-header"><h2>Branches</h2><button class="button ghost" data-action="load-git-tools">${icon("refresh")}Refresh</button></div><div class="panel-body"><div class="inline-form"><input id="new-branch-name" class="input" placeholder="feature/name"/><button class="button" data-action="create-branch">${icon("plus")}Create & switch</button></div><div class="tool-list">${branchRows}</div></div></section>
|
|
|
|
|
<section class="panel"><div class="panel-header"><h2>Stashes</h2><button class="button" data-action="stash-changes" ${status.clean ? "disabled" : ""}>${icon("archive")}Stash changes</button></div><div class="panel-body"><div class="tool-list">${stashRows}</div></div></section>
|
|
|
|
|
<section class="panel workspace-sync-panel"><div class="panel-header"><div><h2>Gitea workspace sync</h2><span class="meta">Make tracked files match the current upstream branch exactly</span></div><span class="status-pill ${status.branch?.behind || status.branch?.ahead || status.counts?.changed ? "warning" : "success"}">${escapeHtml(syncState)}</span></div><div class="panel-body"><div class="workspace-sync-layout"><div><h3>Safe mirror, never silent overwrite</h3><p>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.</p><div class="notice">${icon("shield")}Ignored runtime data such as <span class="mono">.env</span>, dependency folders and local databases is preserved. Background awareness only fetches; it never applies this sync automatically.</div></div><div class="workspace-sync-actions"><span class="meta">${escapeHtml(status.branch?.head || "No branch")} → ${escapeHtml(status.branch?.upstream || "No upstream")}</span><button class="button primary" data-action="preview-workspace-sync">${icon("refresh")}Preview Gitea sync</button></div></div></div></section>
|
|
|
|
|
<section class="panel workspace-sync-panel"><div class="panel-header"><div><h2>Gitea workspace sync</h2><span class="meta">Make tracked files match the current upstream branch exactly</span></div><div class="panel-header-actions"><button class="button ghost" data-action="open-context-help" data-topic="workspace-sync">${icon("help")}How does this work?</button><span class="status-pill ${status.branch?.behind || status.branch?.ahead || status.counts?.changed ? "warning" : "success"}">${escapeHtml(syncState)}</span></div></div><div class="panel-body"><div class="workspace-sync-layout"><div><h3>Safe mirror, never silent overwrite</h3><p>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.</p><div class="notice">${icon("shield")}Ignored runtime data such as <span class="mono">.env</span>, dependency folders and local databases is preserved. Background awareness only fetches; it never applies this sync automatically.</div></div><div class="workspace-sync-actions"><span class="meta">${escapeHtml(status.branch?.head || "No branch")} → ${escapeHtml(status.branch?.upstream || "No upstream")}</span><button class="button primary" data-action="preview-workspace-sync">${icon("refresh")}Preview Gitea sync</button></div></div></div></section>
|
|
|
|
|
<section class="panel troubleshooting-panel"><div class="panel-header"><div><h2>Repository troubleshooting</h2><span class="meta">Safe, repository-specific recovery actions</span></div><button class="button primary" data-action="scan-git-recovery">${icon("pulse")}Scan</button></div><div class="panel-body">${recoveryBody}<div class="card-actions"><button class="button" data-action="repair-git-locks">${icon("wrench")}Repair proven stale locks</button><button class="button" data-action="reconcile-repository">${icon("refresh")}Refresh Git state</button>${repository.sshUrl && status.remoteUrl !== repository.sshUrl ? `<button class="button" data-action="repair-origin">${icon("link")}Repair origin</button>` : ""}</div><div class="notice warning">Lock repair refuses to run while a matching Git process is active. A force option is shown only when process detection itself is unavailable.</div></div></section>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
@@ -471,9 +472,11 @@ function renderRepositoryWorkspace(repository) {
|
|
|
|
|
}).join("")}</div><button class="button ghost" data-action="select-deployment-profile" data-profile-id="${attr(profile?.id || profiles[0].id)}">View all</button></div>`
|
|
|
|
|
: "";
|
|
|
|
|
return `<div class="repo-workspace"><header class="repo-header illustrated-repo-header"><div class="repo-heading"><h1><button class="favorite-button ${repository.favorite ? "active" : ""}" data-action="toggle-favorite" title="Toggle favorite">${icon("star")}</button>${escapeHtml(repository.fullName)}</h1><p>${escapeHtml(repository.localPath || "No local working tree linked")}</p></div>${projectIllustration("repo")}<div class="repo-header-actions"><button class="button" data-action="fetch" ${!repository.localPath ? "disabled" : ""}>${icon("refresh")}Fetch</button><button class="button" data-action="open-path" ${!repository.localPath ? "disabled" : ""}>${icon("folder")}Folder</button><button class="button" data-action="open-gitea" ${!repository.htmlUrl ? "disabled" : ""}>${icon("external")}Gitea</button></div></header>
|
|
|
|
|
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
|
|
|
|
|
<div class="release-rail">${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)}</div>
|
|
|
|
|
${deploymentLinks}
|
|
|
|
|
<div class="repo-context">
|
|
|
|
|
${repository.localPath ? `<div class="repo-quick-actions"><button class="button" data-action="open-editor">${icon("external")}Open in editor</button><button class="button" data-action="open-terminal">${icon("terminal")}Open terminal</button><button class="button" data-action="check-branch-protection">${icon("shield")}Check branch protection</button><button class="button primary" data-action="open-pull-request">${icon("git")}Create pull request</button>${ui.branchProtection ? `<span class="status-pill ${ui.branchProtection.protected ? "warning" : "success"}">${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}</span>` : ""}</div>` : ""}
|
|
|
|
|
<div class="release-rail">${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)}</div>
|
|
|
|
|
${deploymentLinks}
|
|
|
|
|
</div>
|
|
|
|
|
<nav class="tabs">${[
|
|
|
|
|
["changes", "Changes"],
|
|
|
|
|
["history", "History"],
|
|
|
|
@@ -615,6 +618,143 @@ function renderDeployments() {
|
|
|
|
|
);
|
|
|
|
|
return `<div class="page"><div class="page-header visual-page-header"><div><div class="eyebrow">Server releases</div><h1>Deployments</h1><p>Discover live Unraid workloads, verify them against Gitea and release an exact commit through a protected server pull.</p></div>${projectIllustration("deploy")}<div class="stack horizontal compact"><button class="button" data-action="refresh-operations">${icon("refresh")}Refresh runs & servers</button>${missingDockerMan.length ? `<button class="button primary" data-action="repair-missing-dockerman">${icon("wrench")}Repair ${missingDockerMan.length} managed integration${missingDockerMan.length === 1 ? "" : "s"}</button>` : ""}</div></div>${active.length ? `<div class="notice warning">${icon("pulse")} ${active.length} deployment operation${active.length === 1 ? " is" : "s are"} still active. ForgeFlow reconciles these against the live server automatically.</div>` : ""}${renderServerInventory()}<section class="section-block"><div class="section-heading"><div><h2>Linked deployment environments</h2><span class="meta">Stable Compose identity, live container health and exact Gitea commit parity</span></div></div><div class="deploy-card-grid">${cards.length ? cards.map(({ repository, profile }) => renderProfileCard(repository, profile, true)).join("") : '<div class="empty-state panel"><h3>No deployment environments configured</h3><p>Scan a server and link an existing workload, or open a repository and add an environment.</p></div>'}</div></section><section class="section-block"><div class="section-heading"><h2>All operations</h2><span class="meta">Newest first</span></div><div class="panel">${operations().length ? `<table class="data-table"><thead><tr><th>Repository</th><th>Action</th><th>Environment</th><th>Commit</th><th>Status</th><th>Updated</th><th></th></tr></thead><tbody>${operations().map((operation) => `<tr><td>${escapeHtml(operation.repository)}</td><td>${escapeHtml(operation.action || "deploy")}</td><td>${escapeHtml(operation.environment || "—")}</td><td class="mono">${escapeHtml(operation.shortSha || shortSha(operation.sha))}</td><td><span class="status-pill ${toneForStatus(operation.status)}">${escapeHtml(operation.status)}</span></td><td>${formatDate(operation.updatedAt || operation.createdAt)}</td><td><button class="button ghost" data-action="open-operation" data-operation-id="${attr(operation.id)}">Open</button></td></tr>`).join("")}</tbody></table>` : '<div class="empty-state compact"><p>No operations recorded.</p></div>'}</div></section></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 `<details class="help-topic" data-help-topic="${attr(topic.id)}" ${isOpen ? "open" : ""}><summary><span class="help-topic-icon">${icon(topic.icon)}</span><span><small>${escapeHtml(topic.category)}</small><strong>${escapeHtml(topic.title)}</strong><span>${escapeHtml(topic.summary)}</span></span>${icon("chevron", "help-chevron")}</summary><div class="help-topic-body"><ol>${topic.steps.map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol><div class="help-note">${icon("shield")}<span>${escapeHtml(topic.note)}</span></div></div></details>`;
|
|
|
|
|
}).join("");
|
|
|
|
|
return `<div class="page help-page"><header class="help-hero"><div><div class="eyebrow">ForgeFlow guide</div><h1>How can we help?</h1><p>Clear, practical instructions for repositories, Gitea synchronization, server deployments and maintenance.</p><label class="help-search">${icon("search")}<input id="help-search" value="${attr(ui.helpQuery)}" placeholder="Search sync, deploy keys, Git Validator…" aria-label="Search help"/><kbd>Ctrl F</kbd></label></div>${projectIllustration("flow")}</header><div class="help-layout"><aside class="help-navigation"><span class="eyebrow">Topics</span>${categories.map((category) => `<div class="help-category"><strong>${escapeHtml(category)}</strong>${HELP_TOPICS.filter((topic) => topic.category === category).map((topic) => `<button data-action="help-topic" data-topic="${attr(topic.id)}" class="${ui.helpTopic === topic.id ? "active" : ""}">${icon(topic.icon)}${escapeHtml(topic.title)}</button>`).join("")}</div>`).join("")}</aside><section class="help-results"><div class="help-results-header"><div><h2>${query ? `Search results` : "Everything you need to operate ForgeFlow"}</h2><span>${topics.length} topic${topics.length === 1 ? "" : "s"}${query ? ` matching “${escapeHtml(ui.helpQuery)}”` : ""}</span></div></div>${topicMarkup || `<div class="empty-state panel"><h3>No help topic found</h3><p>Try a broader term such as sync, deployment, keys, validator or update.</p><button class="button" data-action="clear-help-search">Clear search</button></div>`}</section></div></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSettings() {
|
|
|
|
|
const state = ui.boot.state;
|
|
|
|
|
const prefs = state.preferences || {};
|
|
|
|
|