feat: add contextual help and stabilize repository layout
This commit is contained in:
1 parent
d47c7b5e41
commit
e882656e85
17 files changed
+660
-82
No files matched your search
@@ -8,6 +8,19 @@ async function handleShellActions(event, target, action, repository) {
|
||||
await refreshDeploymentTruth(true);
|
||||
setLoading(false);
|
||||
}
|
||||
} else if (action === "open-context-help" || action === "help-topic") {
|
||||
ui.helpTopic = target.dataset.topic || "getting-started";
|
||||
ui.helpQuery = "";
|
||||
ui.currentView = "help";
|
||||
ui.modal = null;
|
||||
render();
|
||||
requestAnimationFrame(() =>
|
||||
document.querySelector(`[data-help-topic="${ui.helpTopic}"]`)?.scrollIntoView({ block: "start", behavior: "smooth" }),
|
||||
);
|
||||
} else if (action === "clear-help-search") {
|
||||
ui.helpQuery = "";
|
||||
render();
|
||||
requestAnimationFrame(() => document.querySelector("#help-search")?.focus());
|
||||
} else if (action === "select-repo") selectRepository(target.dataset.id);
|
||||
else if (action === "open-deployment-link") {
|
||||
if (!repository) return true;
|
||||
|
||||
@@ -59,6 +59,8 @@ const icons = {
|
||||
'<path d="M21 12a9 9 0 0 1-15.3 6.4L3 16"/><path d="M3 21v-5h5"/><path d="M3 12A9 9 0 0 1 18.3 5.6L21 8"/><path d="M21 3v5h-5"/>',
|
||||
wrench:
|
||||
'<path d="M14.7 6.3a4 4 0 0 0-5-5l2.1 2.1-2.8 2.8-2.1-2.1a4 4 0 0 0 5 5L20 17.2 17.2 20l-8.1-8.1a4 4 0 0 0-5-5l2.1 2.1-2.8 2.8-2.1-2.1a4 4 0 0 0 5 5"/>',
|
||||
help:
|
||||
'<circle cx="12" cy="12" r="9"/><path d="M9.7 9a2.5 2.5 0 1 1 3.8 2.1c-.9.5-1.5 1.1-1.5 2.4M12 17.5h.01"/>',
|
||||
};
|
||||
|
||||
function icon(name, className = "") {
|
||||
@@ -183,6 +185,8 @@ const ui = {
|
||||
deploymentTruthPromise: null,
|
||||
deploymentTruthRequest: null,
|
||||
paletteQuery: "",
|
||||
helpQuery: "",
|
||||
helpTopic: "getting-started",
|
||||
updateStatus: null,
|
||||
updateChecking: false,
|
||||
servers: [],
|
||||
|
||||
@@ -404,6 +404,8 @@ function render() {
|
||||
? renderOverview()
|
||||
: ui.currentView === "deployments"
|
||||
? renderDeployments()
|
||||
: ui.currentView === "help"
|
||||
? renderHelp()
|
||||
: ui.currentView === "settings"
|
||||
? renderSettings()
|
||||
: ui.currentView === "diagnostics"
|
||||
|
||||
@@ -51,6 +51,9 @@ app.addEventListener("input", (event) => {
|
||||
else if (event.target.id === "palette-input") {
|
||||
ui.paletteQuery = event.target.value;
|
||||
scheduleInputRender(60);
|
||||
} else if (event.target.id === "help-search") {
|
||||
ui.helpQuery = event.target.value;
|
||||
scheduleInputRender(60);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,6 +97,11 @@ document.addEventListener("keydown", (event) => {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && ui.currentView === "help") {
|
||||
event.preventDefault();
|
||||
document.querySelector("#help-search")?.focus();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "Enter" &&
|
||||
|
||||
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
|
||||
await wait(80);
|
||||
snapshot();
|
||||
return {
|
||||
appVersion: "0.10.13-demo",
|
||||
appVersion: "0.10.14-demo",
|
||||
platform: "win32",
|
||||
state: clone(state),
|
||||
git: { available: true, version: "git version 2.47.3" },
|
||||
|
||||
+309
-1
@@ -823,7 +823,11 @@ select:focus-visible {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto 39px minmax(0, 1fr);
|
||||
grid-template-rows: auto auto 39px minmax(0, 1fr);
|
||||
}
|
||||
.repo-context {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.repo-header {
|
||||
padding: 16px 18px 13px;
|
||||
@@ -995,8 +999,14 @@ select:focus-visible {
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface-1);
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.tab {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
background: transparent;
|
||||
@@ -4026,3 +4036,301 @@ html[data-theme="light"] .visual-page-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Help center and compact repository-shell safeguards */
|
||||
.panel-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.help-page {
|
||||
max-width: 1500px;
|
||||
}
|
||||
.help-hero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
padding: 30px 34px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(circle at 78% 15%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 32%),
|
||||
linear-gradient(125deg, color-mix(in srgb, var(--surface-2) 92%, transparent), var(--surface-1));
|
||||
box-shadow: 0 22px 65px -48px color-mix(in srgb, var(--accent) 65%, transparent);
|
||||
}
|
||||
.help-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(color-mix(in srgb, var(--text-faint) 20%, transparent) 0.8px, transparent 0.8px);
|
||||
background-size: 18px 18px;
|
||||
mask-image: linear-gradient(90deg, transparent, black 65%, transparent);
|
||||
opacity: 0.42;
|
||||
}
|
||||
.help-hero > div:first-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 750px;
|
||||
}
|
||||
.help-hero h1 {
|
||||
margin: 8px 0 6px;
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
.help-hero p {
|
||||
max-width: 680px;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.help-search {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: 680px;
|
||||
margin-top: 22px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--line));
|
||||
border-radius: 11px;
|
||||
background: color-mix(in srgb, var(--surface-0) 90%, transparent);
|
||||
box-shadow: 0 12px 36px -30px var(--accent);
|
||||
}
|
||||
.help-search:focus-within {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
}
|
||||
.help-search > .icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
.help-search input {
|
||||
min-width: 0;
|
||||
height: 46px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
.help-search kbd {
|
||||
padding: 3px 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
background: var(--surface-2);
|
||||
color: var(--text-faint);
|
||||
font: 10px var(--font-mono);
|
||||
}
|
||||
.help-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(190px, 240px) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
}
|
||||
.help-navigation {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: color-mix(in srgb, var(--surface-1) 94%, transparent);
|
||||
}
|
||||
.help-category {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
.help-category > strong {
|
||||
padding: 4px 8px;
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.help-category button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.help-category button:hover,
|
||||
.help-category button.active {
|
||||
background: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
|
||||
color: var(--text);
|
||||
}
|
||||
.help-category button.active {
|
||||
box-shadow: inset 2px 0 var(--primary);
|
||||
}
|
||||
.help-category button .icon {
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: var(--primary);
|
||||
}
|
||||
.help-results {
|
||||
min-width: 0;
|
||||
}
|
||||
.help-results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
min-height: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.help-results-header h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.help-results-header span {
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
.help-topic {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 13px;
|
||||
background: var(--surface-1);
|
||||
transition: border-color 150ms ease, transform 150ms ease, box-shadow 150ms ease;
|
||||
scroll-margin-top: 14px;
|
||||
}
|
||||
.help-topic:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 34%, var(--line));
|
||||
}
|
||||
.help-topic[open] {
|
||||
border-color: color-mix(in srgb, var(--accent) 26%, var(--line));
|
||||
box-shadow: 0 18px 42px -40px var(--accent);
|
||||
}
|
||||
.help-topic summary {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.help-topic summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.help-topic summary > span:nth-child(2) {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.help-topic summary small {
|
||||
color: var(--primary);
|
||||
font-size: 9px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.help-topic summary strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
.help-topic summary span span {
|
||||
overflow: hidden;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.help-topic-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line));
|
||||
border-radius: 11px;
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--surface-2));
|
||||
color: var(--primary);
|
||||
}
|
||||
.help-topic-icon .icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
.help-chevron {
|
||||
color: var(--text-faint);
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
.help-topic[open] .help-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.help-topic-body {
|
||||
padding: 0 18px 18px 70px;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
}
|
||||
.help-topic-body ol {
|
||||
display: grid;
|
||||
gap: 11px;
|
||||
margin: 16px 0;
|
||||
padding-left: 22px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.help-topic-body li::marker {
|
||||
color: var(--primary);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
}
|
||||
.help-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--success) 20%, var(--line));
|
||||
border-radius: 9px;
|
||||
background: color-mix(in srgb, var(--success) 6%, var(--surface-0));
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.help-note .icon {
|
||||
flex: 0 0 auto;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.help-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.help-navigation {
|
||||
position: static;
|
||||
display: none;
|
||||
}
|
||||
.help-hero .project-illustration {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.help-hero {
|
||||
padding: 22px;
|
||||
}
|
||||
.help-search kbd {
|
||||
display: none;
|
||||
}
|
||||
.help-search {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
.help-topic-body {
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
+145
-5
@@ -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 || {};
|
||||
|
||||
Reference in new issue
Block a user