Release ForgeFlow 0.8.3 premium deployment UX
This commit is contained in:
+21
-1
@@ -852,6 +852,20 @@ function dockerManIntegration(profile) {
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -868,7 +882,13 @@ function renderProfileCard(repository, profile, compact = false) {
|
||||
const dockerManReady = dockerMan.ready;
|
||||
const webUi =
|
||||
profile.webUiUrl || state.webUiUrl || state.dockerMan?.webUi || "";
|
||||
return `<article class="deploy-card ${compact ? "compact-card" : ""}"><div class="deploy-card-header"><div><div class="eyebrow">${escapeHtml(profile.environment)}</div><h3>${escapeHtml(profile.name)}</h3><p>${escapeHtml(providerDetail)}</p></div><span class="status-pill ${health.tone}"><span class="state-dot ${health.tone}"></span>${health.label}</span></div><div class="deploy-card-body"><div class="deploy-metadata"><span>Provider</span><strong>${isSsh ? "SSH / Unraid" : "Gitea Actions"}</strong><span>Live version</span><strong>${state.liveSha ? shortSha(state.liveSha) : "Unknown"}</strong><span>Previous version</span><strong>${state.previousSha ? shortSha(state.previousSha) : "Unknown"}</strong><span>Last checked</span><strong>${state.checkedAt ? formatDate(state.checkedAt) : "Never"}</strong>${isSsh ? `<span>Container</span><strong>${escapeHtml(state.containerName || profile.containerName || profile.remoteFolder || repository.name)}${state.containerRunning === false ? " · stopped" : state.containerRunning ? " · running" : ""}</strong><span>DockerMan</span><strong class="${dockerManReady ? "text-success" : "text-warning"}">${dockerManReady ? (templateReady ? "Labels/template active" : "WebUI/icon labels active") : `WebUI ${webUiReady ? "ready" : "missing"} · icon ${iconReady ? "ready" : "missing"}`}</strong>` : ""}<span>Rollback</span><strong>${rollbackConfigured ? "Available after first deploy" : "Not configured"}</strong></div><div class="card-actions"><button class="button" data-action="run-deployment-preflight" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("shield")}Preflight</button><button class="button" data-action="reconcile-deployment" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("refresh")}Reconcile</button>${webUi ? `<button class="button" data-action="open-profile-webui" data-url="${attr(webUi)}">${icon("external")}Open Web UI</button>` : ""}${isSsh ? `<button class="button ${dockerManReady ? "ghost" : ""}" data-action="apply-dockerman-metadata" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("wrench")}${dockerManReady ? "Reapply DockerMan metadata" : "Repair DockerMan integration"}</button>` : ""}${ready ? `<button class="button primary" data-action="deploy-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("rocket")}Deploy ${escapeHtml(repository.localStatus.shortHead)}</button>` : ""}<button class="button ghost" data-action="edit-deployment-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">Edit</button>${state.previousSha && rollbackConfigured ? `<button class="button danger" data-action="rollback-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("undo")}Rollback</button>` : ""}</div></div></article>`;
|
||||
const identity = deploymentIdentity(profile, repository);
|
||||
const syncLabel = state.matchesGitea
|
||||
? `<span class="sync-proof success">${icon("check")}Live = Gitea · ${shortSha(state.liveSha)}</span>`
|
||||
: state.giteaSha && state.liveSha
|
||||
? `<span class="sync-proof warning">Live ${shortSha(state.liveSha)} · Gitea ${shortSha(state.giteaSha)}</span>`
|
||||
: "";
|
||||
return `<article class="deploy-card accent-${identity.accent} ${compact ? "compact-card" : ""}"><div class="container-identity"><span class="container-avatar">${escapeHtml(identity.initial)}</span><div><span>Container</span><strong>${escapeHtml(identity.name)}</strong><small>${escapeHtml(repository.fullName)} · ${escapeHtml(profile.environment)}</small></div>${syncLabel}</div><div class="deploy-card-header"><div><div class="eyebrow">${escapeHtml(isSsh ? "SSH / UNRAID" : "GITEA ACTIONS")}</div><h3>${escapeHtml(profile.name)}</h3><p>${escapeHtml(providerDetail)}</p></div><span class="status-pill ${health.tone}"><span class="state-dot ${health.tone}"></span>${health.label}</span></div><div class="deploy-card-body"><div class="deploy-metadata"><span>Live commit</span><strong>${state.liveSha ? shortSha(state.liveSha) : "Unknown"}</strong><span>Gitea commit</span><strong>${state.giteaSha ? shortSha(state.giteaSha) : "Refresh to compare"}</strong><span>Previous version</span><strong>${state.previousSha ? shortSha(state.previousSha) : "Unknown"}</strong><span>Last checked</span><strong>${state.checkedAt ? formatDate(state.checkedAt) : "Never"}</strong>${isSsh ? `<span>Runtime</span><strong>${state.containerRunning === false ? "Stopped" : state.containerRunning ? "Running" : "Unknown"}</strong><span>DockerMan</span><strong class="${dockerManReady ? "text-success" : "text-warning"}">${dockerManReady ? (templateReady ? "Labels/template active" : "WebUI/icon labels active") : `WebUI ${webUiReady ? "ready" : "missing"} · icon ${iconReady ? "ready" : "missing"}`}</strong>` : ""}<span>Rollback</span><strong>${rollbackConfigured ? "Available after first deploy" : "Not configured"}</strong></div><div class="card-actions"><button class="button" data-action="run-deployment-preflight" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("shield")}Preflight</button><button class="button" data-action="reconcile-deployment" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("refresh")}Refresh truth</button>${webUi ? `<button class="button" data-action="open-profile-webui" data-url="${attr(webUi)}">${icon("external")}Open Web UI</button>` : ""}${isSsh ? `<button class="button ${dockerManReady ? "ghost" : ""}" data-action="apply-dockerman-metadata" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("wrench")}${dockerManReady ? "Reapply DockerMan metadata" : "Repair DockerMan integration"}</button>` : ""}${ready ? `<button class="button primary" data-action="deploy-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("rocket")}Deploy ${escapeHtml(repository.localStatus.shortHead)}</button>` : ""}<button class="button ghost" data-action="edit-deployment-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">Edit</button>${state.previousSha && rollbackConfigured ? `<button class="button danger" data-action="rollback-profile" data-repository-id="${attr(repository.id)}" data-profile-id="${attr(profile.id)}">${icon("undo")}Rollback</button>` : ""}</div></div></article>`;
|
||||
}
|
||||
|
||||
function renderRepositoryDeployments(repository) {
|
||||
|
||||
+11
-11
@@ -416,21 +416,21 @@
|
||||
);
|
||||
repository.readyToDeploy = Boolean(
|
||||
repository.localPath &&
|
||||
status?.clean &&
|
||||
status.branch.upstream &&
|
||||
status.branch.ahead === 0 &&
|
||||
status.branch.behind === 0 &&
|
||||
repository.deploymentProfiles.some(
|
||||
(entry) => entry.branch === status.branch.head,
|
||||
),
|
||||
status?.clean &&
|
||||
status.branch.upstream &&
|
||||
status.branch.ahead === 0 &&
|
||||
status.branch.behind === 0 &&
|
||||
repository.deploymentProfiles.some(
|
||||
(entry) => entry.branch === status.branch.head,
|
||||
),
|
||||
);
|
||||
repository.attention =
|
||||
!repository.localPath ||
|
||||
Boolean(
|
||||
status?.counts.conflicts ||
|
||||
status?.branch.behind ||
|
||||
status?.branch.ahead ||
|
||||
status?.counts.changed,
|
||||
status?.branch.behind ||
|
||||
status?.branch.ahead ||
|
||||
status?.counts.changed,
|
||||
);
|
||||
repository.attentionReason = !repository.localPath
|
||||
? "No local folder linked"
|
||||
@@ -564,7 +564,7 @@
|
||||
await wait(80);
|
||||
snapshot();
|
||||
return {
|
||||
appVersion: "0.8.2-demo",
|
||||
appVersion: "0.8.3-demo",
|
||||
platform: "win32",
|
||||
state: clone(state),
|
||||
git: { available: true, version: "git version 2.47.3" },
|
||||
|
||||
+148
-8
@@ -32,27 +32,67 @@
|
||||
|
||||
html[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #eef2f7;
|
||||
--surface-0: #f7f9fc;
|
||||
--bg: #e9eef7;
|
||||
--surface-0: #f2f6fc;
|
||||
--surface-1: #ffffff;
|
||||
--surface-2: #f3f6fa;
|
||||
--surface-3: #e8edf4;
|
||||
--surface-hover: #edf2f8;
|
||||
--line: #cdd5e1;
|
||||
--line-soft: #e1e6ee;
|
||||
--line: #c5d0df;
|
||||
--line-soft: #dce4ef;
|
||||
--text: #172033;
|
||||
--text-muted: #526078;
|
||||
--text-faint: #7b879a;
|
||||
--primary: #295fca;
|
||||
--primary-strong: #326ee0;
|
||||
--primary-soft: rgba(50, 110, 224, 0.1);
|
||||
--primary: #2857bf;
|
||||
--primary-strong: #346ee8;
|
||||
--primary-soft: rgba(52, 110, 232, 0.13);
|
||||
--success: #087a57;
|
||||
--success-soft: rgba(8, 122, 87, 0.1);
|
||||
--warning: #9b5b00;
|
||||
--warning-soft: rgba(155, 91, 0, 0.1);
|
||||
--danger: #c73737;
|
||||
--danger-soft: rgba(199, 55, 55, 0.1);
|
||||
--shadow: 0 18px 70px rgba(43, 55, 77, 0.15);
|
||||
--shadow: 0 18px 54px rgba(31, 55, 94, 0.14);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body,
|
||||
html[data-theme="light"] .app-shell {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 82% 2%,
|
||||
rgba(67, 123, 235, 0.13),
|
||||
transparent 31%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 20% 100%,
|
||||
rgba(31, 170, 141, 0.08),
|
||||
transparent 33%
|
||||
),
|
||||
var(--bg);
|
||||
}
|
||||
html[data-theme="light"] .titlebar,
|
||||
html[data-theme="light"] .sidebar {
|
||||
background: rgba(250, 252, 255, 0.92);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
html[data-theme="light"] .panel,
|
||||
html[data-theme="light"] .summary-card,
|
||||
html[data-theme="light"] .deploy-card,
|
||||
html[data-theme="light"] .settings-group,
|
||||
html[data-theme="light"] .pipeline-card {
|
||||
border-color: rgba(151, 169, 197, 0.48);
|
||||
box-shadow: 0 8px 28px rgba(49, 75, 116, 0.08);
|
||||
}
|
||||
html[data-theme="light"] .nav-button.active {
|
||||
background: linear-gradient(
|
||||
105deg,
|
||||
rgba(52, 110, 232, 0.16),
|
||||
rgba(48, 181, 151, 0.08)
|
||||
);
|
||||
box-shadow: inset 3px 0 var(--primary-strong);
|
||||
}
|
||||
html[data-theme="light"] .button.primary {
|
||||
box-shadow: 0 7px 18px rgba(52, 110, 232, 0.22);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -1309,10 +1349,110 @@ html[data-theme="light"] .diff-line.remove {
|
||||
gap: 12px;
|
||||
}
|
||||
.deploy-card {
|
||||
--card-accent: var(--primary-strong);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-1);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.1);
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
.deploy-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in srgb, var(--card-accent) 42%, var(--line));
|
||||
box-shadow: 0 16px 42px
|
||||
color-mix(in srgb, var(--card-accent) 12%, transparent);
|
||||
}
|
||||
.deploy-card.accent-0 {
|
||||
--card-accent: #4d7df3;
|
||||
}
|
||||
.deploy-card.accent-1 {
|
||||
--card-accent: #8b5cf6;
|
||||
}
|
||||
.deploy-card.accent-2 {
|
||||
--card-accent: #0ea5a0;
|
||||
}
|
||||
.deploy-card.accent-3 {
|
||||
--card-accent: #e2783f;
|
||||
}
|
||||
.deploy-card.accent-4 {
|
||||
--card-accent: #d24e83;
|
||||
}
|
||||
.deploy-card.accent-5 {
|
||||
--card-accent: #4b9b55;
|
||||
}
|
||||
.container-identity {
|
||||
min-height: 78px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 13px 14px;
|
||||
border-top: 4px solid var(--card-accent);
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
color-mix(in srgb, var(--card-accent) 15%, var(--surface-1)),
|
||||
var(--surface-1) 68%
|
||||
);
|
||||
}
|
||||
.container-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
color-mix(in srgb, var(--card-accent) 72%, #fff),
|
||||
var(--card-accent)
|
||||
);
|
||||
box-shadow: 0 8px 20px color-mix(in srgb, var(--card-accent) 28%, transparent);
|
||||
font-size: 19px;
|
||||
font-weight: 780;
|
||||
}
|
||||
.container-identity > div {
|
||||
min-width: 0;
|
||||
}
|
||||
.container-identity span:not(.container-avatar):not(.sync-proof),
|
||||
.container-identity small {
|
||||
display: block;
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
.container-identity strong {
|
||||
display: block;
|
||||
margin: 2px 0 3px;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sync-proof {
|
||||
margin-left: auto;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sync-proof.success {
|
||||
color: var(--success);
|
||||
background: var(--success-soft);
|
||||
}
|
||||
.sync-proof.warning {
|
||||
color: var(--warning);
|
||||
background: var(--warning-soft);
|
||||
}
|
||||
.sync-proof .icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
.deploy-card-header {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user