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 ``;
+ }).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 ? `
| 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)} | |
`).join("")}
` : '
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 ? `
` : ""}${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 ? `
| 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)} | |
`).join("")}
` : '
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 `
${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}