From 58d361bbabd9088441795755eddd5f194142d7b4 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:05:03 +0200 Subject: [PATCH] fix: align deployment links across repository views --- SOURCE_MANIFEST.txt | 10 ++--- src/renderer/actions/shell.js | 12 ++++++ src/renderer/mock-deployment-bridge.js | 21 ++++++++-- src/renderer/styles.css | 58 ++++++++++++++++++++++++++ src/renderer/views.js | 54 +++++++++++++++++++++--- tests/browser/forgeflow.spec.mjs | 11 +++++ 6 files changed, 152 insertions(+), 14 deletions(-) diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index a5b8b7e..49a2541 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -165,7 +165,7 @@ d0bf607dd1de9d55f2947d0adf0997cd3ca5c269d10a5362cc1d8bc4d1a2a8ae 6706 48bed91dd2a85bb51ee7307f7acc3b79c881ce8cf63b22ba79d5d079b265eb4b 7785 src/renderer/actions/inventory.js 4227a05a20580a31127d2c929640defc3d36e8e3e89d6be830aab1940da81082 12267 src/renderer/actions/recovery.js cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 src/renderer/actions/setup-and-settings.js -5d8110918b2957889047e38eb4ab2953b2b4476394d9328bd40ae6e4246e65ef 18584 src/renderer/actions/shell.js +a980f2e86d8286ea605a7259b9e9adcf3fda4f657b8d54a5d2d8765a7bbbec13 19065 src/renderer/actions/shell.js edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 src/renderer/app.js 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 src/renderer/assets/itworx-mark.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png @@ -175,11 +175,11 @@ edcfa0585af4b11ec3b7458969132bdf215830505859f1519f1635dc111ce48a 24333 eef2f269ba4fbb76bf66ad328d481b461255d0acb753b30878dd4d4eaac57dc6 6924 src/renderer/events.js a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js -9fa522dad0088e2981c4ca5c392e93d6c92e04ff23fd1083d9040a3aa52d2b55 28101 src/renderer/mock-deployment-bridge.js +5a2c0616a8a0ba23c182a8928abdf40a69612ce34c4cba47b1fd5f21fe5843d4 28617 src/renderer/mock-deployment-bridge.js 123518a4cfd47b7e510ffab9102a2fd82edc93754aaabd7bc0259d51dfce5d63 20032 src/renderer/mock-repository-bridge.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js -6776e0adb690b8f36274bfc5b31e3140054893c46ba64621c4f56a0d91b52fc2 78323 src/renderer/styles.css -1703e64533b7e2717b27c5776296c7dd76331e6f97e8005aea9fd688f1aee3ae 94834 src/renderer/views.js +1fc4c28859f7283aa2b771a50f1fd26addc8862b97a09250f806930ab4483f7a 79874 src/renderer/styles.css +5230c238acb87381303ec30026906cf8a7163c33380859d351b268adfc26b4f4 100196 src/renderer/views.js 0a1e9d9d6cd4d190eb7f85dbc6668d80600b1cf2749cc0c2c51cc428f506f20d 1121 src/shared/clone-target.cjs 5d425d5c2f939d0f6beebee7ebb0c77146cb7e318535ba7286ec7081a4dc2269 2497 src/shared/deployment-policy.cjs 029e600229714d033c28e2dcb77817aa8269847001782ae0012960e83ffd183f 3057 src/shared/git-status.cjs @@ -193,7 +193,7 @@ f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1 f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs -2d57a66bb2a6461e4c3266f14998d505ba0582135b0bbcf8fce9f1542a70007a 10479 tests/browser/forgeflow.spec.mjs +b46c9624c20bae767e50b4ab85e6b0ea6a9ee4a5c5c30f55554217f25134f1e7 11218 tests/browser/forgeflow.spec.mjs 454edeaccb2bd41043bc918d3e3a6127db14339031d6a1c1562ac855e90455d2 4318 tests/clone-target.test.mjs ac17f8bbe9e388b80abef7792c8b184a1fd482c93f13d23a478e433961020f75 17214 tests/config-store.test.mjs f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs diff --git a/src/renderer/actions/shell.js b/src/renderer/actions/shell.js index e0f6954..604eae7 100644 --- a/src/renderer/actions/shell.js +++ b/src/renderer/actions/shell.js @@ -9,6 +9,18 @@ async function handleShellActions(event, target, action, repository) { setLoading(false); } } else if (action === "select-repo") selectRepository(target.dataset.id); + else if (action === "open-deployment-link") { + if (!repository) return true; + selectRepository(repository.id, false); + ui.selectedProfileId = target.dataset.profileId || selectedProfile(repository)?.id || null; + ui.repositoryTab = "deployments"; + ui.currentView = "repository"; + render(); + } else if (action === "select-deployment-profile") { + ui.selectedProfileId = target.dataset.profileId || null; + ui.repositoryTab = "deployments"; + render(); + } else if (action === "refresh") { await refreshRepositories(true); await refreshActiveOperations(false); diff --git a/src/renderer/mock-deployment-bridge.js b/src/renderer/mock-deployment-bridge.js index a6cdf58..12db00a 100644 --- a/src/renderer/mock-deployment-bridge.js +++ b/src/renderer/mock-deployment-bridge.js @@ -238,13 +238,13 @@ function createMockDeploymentBridge(context) { { serverId: "server-unraid", serverName: "Unraid", - detected: 2, + detected: 3, adopted: 0, verified: 1, - linked: 1, + linked: 2, unmatched: 0, - needsReview: 1, - running: 2, + needsReview: 2, + running: 3, stopped: 0, capabilities: { docker: true, @@ -296,6 +296,19 @@ function createMockDeploymentBridge(context) { reasons: ["container and repository names are similar"], })), }, + { + workloadId: "workload-demo-unresolved", + displayName: "Legacy Worker", + status: "linked", + runtime: { running: true, health: "healthy" }, + containers: [{ name: "legacy-worker", running: true }], + candidates: [], + link: { + profileId: "profile-that-no-longer-exists", + repositoryFullName: "jens/removed-repository", + source: "manual", + }, + }, ], }, ]; diff --git a/src/renderer/styles.css b/src/renderer/styles.css index aab56c6..c2c76b1 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -849,6 +849,64 @@ select:focus-visible { .release-node:last-child { border-right: 0; } +.repository-deployment-summary { + display: flex; + align-items: center; + gap: 10px; + min-height: 48px; + padding: 7px 12px; + border-bottom: 1px solid var(--line); + background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 7%, var(--surface-1)), var(--surface-1) 42%); +} +.repository-deployment-summary-label { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; + color: var(--text-muted); + font-size: 10px; + font-weight: 760; + letter-spacing: 0.06em; + text-transform: uppercase; +} +.repository-deployment-summary-label svg { + width: 14px; + height: 14px; + color: var(--accent); +} +.repository-deployment-chips { + display: flex; + gap: 7px; + min-width: 0; + flex: 1; + overflow-x: auto; + scrollbar-width: thin; +} +.repository-deployment-chip { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; + max-width: 280px; + padding: 6px 9px; + border: 1px solid var(--line); + border-radius: 8px; + background: color-mix(in srgb, var(--surface-2) 88%, transparent); + color: var(--text); + cursor: pointer; +} +.repository-deployment-chip:hover { + border-color: color-mix(in srgb, var(--accent) 48%, var(--line)); + background: color-mix(in srgb, var(--accent) 10%, var(--surface-2)); +} +.repository-deployment-chip strong, +.repository-deployment-chip span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.repository-deployment-chip strong { font-size: 11px; } +.repository-deployment-chip span:last-child { color: var(--text-muted); font-size: 10px; } .release-node:not(:last-child)::after { content: "›"; position: absolute; diff --git a/src/renderer/views.js b/src/renderer/views.js index c87ed03..ecbd1f4 100644 --- a/src/renderer/views.js +++ b/src/renderer/views.js @@ -188,6 +188,16 @@ 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"); @@ -359,8 +369,15 @@ function renderProfileCard(repository, profile, compact = false) { } function renderRepositoryDeployments(repository) { const profiles = repository.deploymentProfiles || []; + const workloads = linkedWorkloadsForRepository(repository); + const profileIds = new Set(profiles.map((profile) => profile.id)); + const workloadRows = workloads.map((workload) => { + const containers = (workload.containers || []).map((container) => container.name).filter(Boolean); + const profileResolved = Boolean(workload.link?.profileId && profileIds.has(workload.link.profileId)); + return `
${escapeHtml(workload.displayName || containers[0] || "Server workload")}${escapeHtml(workload.serverName)} · ${containers.length ? escapeHtml(containers.join(", ")) : "container identity unavailable"} · ${workload.runtime?.running ? "running" : "stopped"}${escapeHtml(workload.compose?.project ? `Compose ${workload.compose.project}` : workload.remoteFolderCandidate || "Docker workload")}
${profileResolved ? "Repository linked" : "Link needs reconciliation"}${profileResolved ? `` : ``}
`; + }).join(""); const repoOps = repositoryOperations(repository).slice(0, 10); - return `

Deployment environments

Exact-commit Gitea Actions or pinned SSH / Unraid deployments
${profiles.length ? `
${profiles.map((profile) => renderProfileCard(repository, profile)).join("")}
` : '

No deployment profile

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

'}

Release history

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

No releases for this repository yet.

'}
`; + return `

Deployment environments

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

Detected on server

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

No deployment profile

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

'}

Release history

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

No releases for this repository yet.

'}
`; } function renderGitTools(repository) { @@ -412,6 +429,8 @@ function renderGitValidator(repository) { function renderRepositoryWorkspace(repository) { const status = repository.localStatus; + const profiles = repository.deploymentProfiles || []; + const linkedWorkloads = linkedWorkloadsForRepository(repository); const profile = selectedProfile(repository); const serverState = profile?.state || {}; const localTone = status?.counts.conflicts @@ -444,9 +463,19 @@ function renderRepositoryWorkspace(repository) { settings: renderRepositorySettings, }[ui.repositoryTab] || renderChanges )(repository); + const deploymentLinks = profiles.length + ? `
${icon("server")}Linked deployments
${profiles.map((item) => { + const workload = linkedWorkloads.find((candidate) => candidate.link?.profileId === item.id); + const itemState = item.state || {}; + const tone = itemState.healthy === false ? "danger" : itemState.healthy === true ? "success" : workload?.runtime?.running ? "success" : "warning"; + const identity = workload?.displayName || item.containerName || item.remoteFolder || item.environment; + return ``; + }).join("")}
` + : ""; return `

${escapeHtml(repository.fullName)}

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

${projectIllustration("repo")}
${repository.localPath ? `
${ui.branchProtection ? `${ui.branchProtection.protected ? `Protected · ${ui.branchProtection.requiredApprovals || 0} approval(s)` : "Direct pushes allowed"}` : ""}
` : ""}
${releaseNode("Local", status?.shortHead || "Not linked", status ? `${status.counts.changed} changes · ${status.branch.head}` : "No working tree", localTone)}${releaseNode("Gitea", status?.shortHead || "Unknown", status?.branch.upstream ? `${status.branch.ahead} ahead · ${status.branch.behind} behind` : "Branch not published", remoteTone)}${releaseNode(`Server${profile ? ` · ${profile.environment}` : ""}`, serverState.liveSha ? shortSha(serverState.liveSha) : "Unknown", profile ? (serverState.checkedAt ? `checked ${formatDate(serverState.checkedAt)}` : "not checked") : "No deployment profile", serverTone)}
+ ${deploymentLinks}