perf: streamline repository and deployment awareness
ForgeFlow quality gate / quality (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-08-12 15:26:53 +02:00
parent 38e221cbd1
commit 32ed4fcb5e
19 changed files with 351 additions and 73 deletions
+8 -1
View File
@@ -328,8 +328,15 @@ class DeploymentService {
async refreshActiveOperations() {
const active = this.store.data.operations.filter((item) => item.type === 'deployment' && !TERMINAL_STATUSES.has(item.status));
const queue = active.slice(0, 20);
const results = [];
for (const operation of active.slice(0, 20)) results.push(await this.refreshOperation(operation.id));
const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
while (queue.length) {
const operation = queue.shift();
results.push(await this.refreshOperation(operation.id));
}
});
await Promise.all(workers);
return results;
}
+2 -2
View File
@@ -6,8 +6,8 @@ function registerRepositoryIpc({
resolveRepository, cloneRepositoryInto, cloneDirectoryName,
matchRemoteToRepository, shell, dialog,
}) {
register("repositories:refresh", async () => {
const result = await repositories.refresh();
register("repositories:refresh", async ({ force = false }) => {
const result = await repositories.refresh({ force: force === true });
monitor?.setPaths(repositories.getWatchPaths());
return result;
});
+35 -4
View File
@@ -31,6 +31,10 @@ class RepositoryService {
this.lastKnownLocalPaths = [];
this.lastKnownRemoteRepositories = [];
this.lastSuccessfulRemoteRefreshAt = null;
this.lastRemoteRefreshAtMs = 0;
this.lastDiscoveredPaths = [];
this.lastDiscoveryAtMs = 0;
this.refreshPromise = null;
}
async discoverInRoot(root, maxDepth = 4) {
@@ -82,17 +86,27 @@ class RepositoryService {
return [...this.lastKnownLocalPaths];
}
async getRemoteRepositories() {
async getRemoteRepositories({ force = false } = {}) {
if (!this.store.data.gitea.baseUrl || !this.store.getToken()) {
this.lastKnownRemoteRepositories = [];
this.lastSuccessfulRemoteRefreshAt = null;
return { repositories: [], stale: false, error: null };
}
if (!force && this.lastSuccessfulRemoteRefreshAt && Date.now() - this.lastRemoteRefreshAtMs < 15_000) {
return {
repositories: this.lastKnownRemoteRepositories.map((repository) => ({ ...repository })),
stale: false,
error: null,
cached: true
};
}
try {
const repositories = await this.gitea.listRepositories();
this.lastKnownRemoteRepositories = repositories.map((repository) => ({ ...repository }));
this.lastSuccessfulRemoteRefreshAt = new Date().toISOString();
this.lastRemoteRefreshAtMs = Date.now();
return { repositories, stale: false, error: null };
} catch (error) {
if (!this.lastSuccessfulRemoteRefreshAt) throw error;
@@ -109,12 +123,28 @@ class RepositoryService {
}
}
async refresh() {
async getDiscoveredPaths({ force = false } = {}) {
if (!force && this.lastDiscoveryAtMs && Date.now() - this.lastDiscoveryAtMs < 30_000) {
return [...this.lastDiscoveredPaths];
}
const paths = await this.discoverAll(this.store.data.workspaceRoots);
this.lastDiscoveredPaths = [...paths];
this.lastDiscoveryAtMs = Date.now();
return paths;
}
async refresh(options = {}) {
if (this.refreshPromise) return this.refreshPromise;
this.refreshPromise = this.performRefresh(options).finally(() => { this.refreshPromise = null; });
return this.refreshPromise;
}
async performRefresh({ force = false } = {}) {
const started = Date.now();
const remoteResult = await this.getRemoteRepositories();
const remoteResult = await this.getRemoteRepositories({ force });
const remoteRepositories = remoteResult.repositories;
const discoveredPaths = await this.discoverAll(this.store.data.workspaceRoots);
const discoveredPaths = await this.getDiscoveredPaths({ force });
const mappedPaths = Object.values(this.store.data.repositoryMappings || {});
const localPaths = [...new Set([...discoveredPaths, ...mappedPaths])];
const localDescriptors = await this.getLocalDescriptors(localPaths);
@@ -179,6 +209,7 @@ class RepositoryService {
durationMs: Date.now() - started,
remoteCount: remoteRepositories.length,
remoteStale: remoteResult.stale,
remoteCached: remoteResult.cached === true,
discoveredCount: discoveredPaths.length,
linkedCount: sorted.filter((item) => item.localPath).length,
attentionCount: sorted.filter((item) => item.attention).length,
+97 -30
View File
@@ -34,18 +34,23 @@ function createUnraidInventoryMethods({
fi
if [ -n "$ids" ]; then
disappeared=0
while IFS= read -r container_id; do
[ -n "$container_id" ] || continue
# Use Docker's own JSON document instead of a Go template. Accessing an
# absent .State.Health map key makes the formatted Docker inspect fail for
# every container without a healthcheck, which previously made those
# containers look as if they disappeared during the scan.
if inspect=$(docker inspect "$container_id" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
disappeared=$((disappeared + 1))
fi
done <<< "$ids"
mapfile -t container_ids <<< "$ids"
# Docker accepts multiple IDs and returns one JSON array. This avoids one
# daemon round-trip per container on larger Unraid installations.
if inspect=$(docker inspect "\${container_ids[@]}" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
# A container can disappear between docker ps and inspect. Fall back to
# individual reads so the remaining inventory stays complete.
for container_id in "\${container_ids[@]}"; do
[ -n "$container_id" ] || continue
if inspect=$(docker inspect "$container_id" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
disappeared=$((disappeared + 1))
fi
done
fi
if [ "$disappeared" -gt 0 ]; then
printf 'W\\t%s\\n' "$(printf '%s' "$disappeared stale container reference(s) disappeared during inventory; current containers were still processed." | base64 | tr -d '\\r\\n')"
fi
@@ -99,26 +104,35 @@ function createUnraidInventoryMethods({
(
set -- -f "$primary"
files_text=$primary
has_override=false
for extra in "$dir/compose.override.yml" "$dir/compose.override.yaml" "$dir/docker-compose.override.yml" "$dir/docker-compose.override.yaml"; do
[ -f "$extra" ] || continue
has_override=true
set -- "$@" -f "$extra"
files_text="$files_text
$extra"
done
project_name=$(sed -n 's/^name:[[:space:]]*//p' "$primary" 2>/dev/null | head -n1 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | xargs 2>/dev/null || true)
[ -n "$project_name" ] || project_name=$(basename "$dir")
valid=false; services=''; images=''; compose_error=''
valid=false; services=''; compose_error=''
images=$(awk '
/^[[:space:]]*services:[[:space:]]*($|#)/ { in_services=1; next }
in_services && /^[^[:space:]]/ { exit }
in_services && /^[[:space:]]+image:[[:space:]]*/ {
line=$0; sub(/^[[:space:]]*image:[[:space:]]*/, "", line); sub(/[[:space:]]+#.*/, "", line); gsub(/"/, "", line); print line
}
' "$primary" 2>/dev/null || true)
if [ "$compose_ok" != true ]; then
compose_error='Docker Compose is unavailable; file metadata was still detected.'
elif [ "$compose_v2" = true ]; then
if services=$(cd "$dir" && docker compose "$@" config --services 2>&1); then
valid=true
images=$(cd "$dir" && docker compose "$@" config --images 2>/dev/null || true)
if [ "$has_override" = true ] || [ -z "$images" ] || printf '%s' "$images" | grep -q '\$'; then images=$(cd "$dir" && docker compose "$@" config --images 2>/dev/null || true); fi
else compose_error=$services; services=''; fi
else
if services=$(cd "$dir" && docker-compose "$@" config --services 2>&1); then
valid=true
images=$(cd "$dir" && docker-compose "$@" config --images 2>/dev/null || true)
if [ "$has_override" = true ] || [ -z "$images" ] || printf '%s' "$images" | grep -q '\$'; then images=$(cd "$dir" && docker-compose "$@" config --images 2>/dev/null || true); fi
else compose_error=$services; services=''; fi
fi
if [ -z "$services" ]; then
@@ -130,15 +144,6 @@ function createUnraidInventoryMethods({
}
' "$primary" 2>/dev/null || true)
fi
if [ -z "$images" ]; then
images=$(awk '
/^[[:space:]]*services:[[:space:]]*($|#)/ { in_services=1; next }
in_services && /^[^[:space:]]/ { exit }
in_services && /^[[:space:]]+image:[[:space:]]*/ {
line=$0; sub(/^[[:space:]]*image:[[:space:]]*/, "", line); sub(/[[:space:]]+#.*/, "", line); gsub(/"/, "", line); print line
}
' "$primary" 2>/dev/null || true)
fi
printf 'Y\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \\
"$(printf '%s' "$dir" | base64 | tr -d '\\r\\n')" \\
"$(printf '%s' "$files_text" | base64 | tr -d '\\r\\n')" \\
@@ -310,7 +315,7 @@ function createUnraidInventoryMethods({
};
}
async saveWorkloadState(profile, workload, server) {
async saveWorkloadState(profile, workload, server, { expectedGiteaSha = null, health = null } = {}) {
const candidateSha = String(workload.metadata?.liveRevision || "");
const previousState = this.store.getDeploymentState?.(profile.id) || {};
const observedLiveSha = /^[0-9a-f]{40,64}$/i.test(candidateSha) ? candidateSha.toLowerCase() : null;
@@ -318,11 +323,21 @@ function createUnraidInventoryMethods({
const profileRemote = inventoryRemoteIdentity(profile.cloneUrl);
const workloadRemote = inventoryRemoteIdentity(workload.metadata?.sourceRepository);
const repositoryMatches = Boolean(observedLiveSha && profileRemote && workloadRemote && profileRemote === workloadRemote);
const verifiedGiteaSha = /^[0-9a-f]{40}$/i.test(String(expectedGiteaSha || ""))
? String(expectedGiteaSha).toLowerCase()
: null;
const matchesGitea = Boolean(repositoryMatches && verifiedGiteaSha && observedLiveSha === verifiedGiteaSha);
const primary = workload.containers.find((container) => container.running) || workload.containers[0] || {};
const dockerHealthy = workload.runtime.health === "healthy" ? true : workload.runtime.health === "unhealthy" ? false : null;
const effectiveHealthy = workload.runtime.running === false
? false
: health?.configured ? health.healthy : dockerHealthy;
return this.store.saveDeploymentState(profile.id, {
liveSha,
healthy: workload.runtime.health === "healthy" ? true : workload.runtime.health === "unhealthy" ? false : null,
runtimeVerification: workload.runtime.health === "unverified" ? "running-unverified" : workload.runtime.health,
healthy: effectiveHealthy,
healthStatus: health?.status ?? null,
healthLatencyMs: health?.latencyMs ?? null,
runtimeVerification: workload.runtime.running === false ? "stopped" : health?.configured ? "desktop-healthcheck" : workload.runtime.health === "unverified" ? "running-unverified" : workload.runtime.health,
containerRunning: workload.runtime.running,
dockerHealth: primary.health || null,
containerName: primary.name || profile.containerName,
@@ -332,8 +347,8 @@ function createUnraidInventoryMethods({
composeProject: workload.compose?.project || null,
observedAt: workload.observedAt,
evidence: liveSha ? "container-provenance-label" : "runtime-only",
giteaSha: repositoryMatches ? observedLiveSha : previousState.giteaSha || null,
matchesGitea: repositoryMatches ? true : previousState.matchesGitea === true && previousState.liveSha === liveSha,
giteaSha: verifiedGiteaSha,
matchesGitea,
previousSha: previousState.previousSha || null,
});
}
@@ -408,6 +423,7 @@ function createUnraidInventoryMethods({
}
async scanServerInventory(serverId, repositories, { autoLink = false } = {}) {
const started = Date.now();
const { server, inventory, workloads } = await this.collectServerInventory(serverId, repositories);
let adopted = 0;
const adoptedLinks = [];
@@ -442,6 +458,7 @@ function createUnraidInventoryMethods({
adopted,
adoptedLinks,
readOnly: !autoLink,
durationMs: Date.now() - started,
});
return response;
}
@@ -609,7 +626,57 @@ function createUnraidInventoryMethods({
}
async discoverServerWorkloads(serverId, repositories) {
return this.scanServerInventory(serverId, repositories, { autoLink: true });
const started = Date.now();
const inventory = await this.scanServerInventory(serverId, repositories, { autoLink: true });
const server = this.store.getServer(serverId);
const queue = inventory.workloads.filter((workload) => workload.link?.profileId && workload.link?.repositoryFullName);
const refreshedProfileIds = [];
let giteaUnavailable = false;
let giteaFailureReported = false;
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
while (queue.length) {
const workload = queue.shift();
const repository = repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link.repositoryFullName).toLowerCase());
const profile = this.store.getDeploymentProfile?.(workload.link.repositoryFullName, workload.link.profileId)
|| this.store.getDeploymentProfiles?.(workload.link.repositoryFullName)?.find((item) => item.id === workload.link.profileId)
|| this.allSshProfiles().find((item) => item.id === workload.link.profileId);
if (!repository || !profile) continue;
let expectedGiteaSha = null;
const status = repository.localStatus;
if (status?.head && status.branch?.head === profile.branch && status.branch?.upstream && status.branch.ahead === 0 && status.branch.behind === 0) {
expectedGiteaSha = status.head;
} else if (!giteaUnavailable) {
try {
const [owner, repo] = String(repository.fullName).split("/");
const branch = await this.gitea.getBranch(owner, repo, profile.branch);
expectedGiteaSha = branch?.commit?.id || branch?.commit?.sha || null;
} catch (error) {
if (!error?.status || Number(error.status) >= 500) {
giteaUnavailable = true;
if (!giteaFailureReported) {
giteaFailureReported = true;
await this.diagnostics?.warning("unraid.workloads.gitea-verification-degraded", {
serverId,
message: error.message,
});
}
}
}
}
const health = workload.runtime.running
? await this.checkHealth(profile.healthcheckUrl)
: { configured: false, healthy: false, skipped: "container-stopped" };
await this.saveWorkloadState(profile, workload, server, { expectedGiteaSha, health });
refreshedProfileIds.push(profile.id);
}
});
await Promise.all(workers);
await this.diagnostics?.debug("unraid.workloads.states-refreshed", {
serverId,
profiles: refreshedProfileIds.length,
durationMs: Date.now() - started,
});
return { ...inventory, refreshedProfiles: refreshedProfileIds.length, refreshedProfileIds };
}
async linkServerWorkload({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) {
+20 -6
View File
@@ -59,18 +59,23 @@ function createUnraidStateMethods({ path, bash, shellQuote, inventoryRemoteIdent
return "";
}
};
const health = await this.checkHealth(profile.healthcheckUrl);
const containerRunning = fields.containerRunning === "true";
const health = containerRunning
? await this.checkHealth(profile.healthcheckUrl)
: { configured: false, healthy: false, skipped: "container-stopped" };
const dockerHealthy = fields.dockerHealth
? fields.dockerHealth === "healthy"
: null;
const effectiveHealthy = health.configured ? health.healthy : dockerHealthy;
const runtimeVerification = health.configured
const effectiveHealthy = !containerRunning ? false : health.configured ? health.healthy : dockerHealthy;
const runtimeVerification = !containerRunning
? "stopped"
: health.configured
? "desktop-healthcheck"
: dockerHealthy === true
? "docker-healthcheck"
: dockerHealthy === false
? "docker-unhealthy"
: fields.containerRunning === "true"
: containerRunning
? "running-unverified"
: "stopped";
return this.store.saveDeploymentState(profile.id, {
@@ -85,7 +90,7 @@ function createUnraidStateMethods({ path, bash, shellQuote, inventoryRemoteIdent
healthStatus: health.status,
healthLatencyMs: health.latencyMs,
containerName,
containerRunning: fields.containerRunning === "true",
containerRunning,
dockerHealth: fields.dockerHealth || null,
dockerMan: {
webUi: decode(fields.webUiLabel),
@@ -273,7 +278,16 @@ function createUnraidStateMethods({ path, bash, shellQuote, inventoryRemoteIdent
item.status,
),
);
return Promise.all(active.map((item) => this.refreshOperation(item.id)));
const queue = [...active];
const results = [];
const workers = Array.from({ length: Math.min(4, queue.length) }, async () => {
while (queue.length) {
const operation = queue.shift();
results.push(await this.refreshOperation(operation.id));
}
});
await Promise.all(workers);
return results;
}
}
return UnraidStateMethods.prototype;
+51 -3
View File
@@ -178,6 +178,10 @@ const ui = {
refreshError: null,
refreshWarning: null,
autoRefreshPending: false,
repositoryRefreshPromise: null,
repositoryRefreshRequest: null,
deploymentTruthPromise: null,
deploymentTruthRequest: null,
paletteQuery: "",
updateStatus: null,
updateChecking: false,
@@ -424,10 +428,32 @@ function scheduleAutoRefresh() {
}
async function refreshRepositories(withLoader = true, silent = false) {
ui.repositoryRefreshRequest = {
withLoader: ui.repositoryRefreshRequest?.withLoader === true || withLoader,
silent: ui.repositoryRefreshRequest ? ui.repositoryRefreshRequest.silent && silent : silent,
};
if (ui.repositoryRefreshPromise) return ui.repositoryRefreshPromise;
ui.repositoryRefreshPromise = (async () => {
let result;
while (ui.repositoryRefreshRequest) {
const request = ui.repositoryRefreshRequest;
ui.repositoryRefreshRequest = null;
result = await performRepositoryRefresh(request.withLoader, request.silent);
}
return result;
})();
try {
return await ui.repositoryRefreshPromise;
} finally {
ui.repositoryRefreshPromise = null;
}
}
async function performRepositoryRefresh(withLoader = true, silent = false) {
if (withLoader) setLoading(true, "Refreshing Local → Gitea → Server state…");
try {
const selectedId = ui.selectedRepoId;
ui.repositories = await window.forgeflow.refreshRepositories();
ui.repositories = await window.forgeflow.refreshRepositories({ force: withLoader });
ui.refreshError = null;
const staleRepository = ui.repositories.find(
(repository) => repository.remoteStale,
@@ -488,6 +514,25 @@ async function refreshActiveOperations(showErrors = true) {
}
async function refreshDeploymentTruth(showErrors = false) {
ui.deploymentTruthRequest = { showErrors: ui.deploymentTruthRequest?.showErrors === true || showErrors };
if (ui.deploymentTruthPromise) return ui.deploymentTruthPromise;
ui.deploymentTruthPromise = (async () => {
let result;
while (ui.deploymentTruthRequest) {
const request = ui.deploymentTruthRequest;
ui.deploymentTruthRequest = null;
result = await performDeploymentTruthRefresh(request.showErrors);
}
return result;
})();
try {
return await ui.deploymentTruthPromise;
} finally {
ui.deploymentTruthPromise = null;
}
}
async function performDeploymentTruthRefresh(showErrors = false) {
let discovery = [];
try {
discovery = (await window.forgeflow.discoverServerDeployments?.()) || [];
@@ -516,8 +561,11 @@ async function refreshDeploymentTruth(showErrors = false) {
);
if (!targets.length) return { checked: 0, failed: 0, discovery };
const inventoryRefreshedProfiles = new Set(discovery.flatMap((server) => server.refreshedProfileIds || []));
const pendingTargets = targets.filter(({ profile }) => !inventoryRefreshedProfiles.has(profile.id));
const failures = [];
const queue = [...targets];
const queue = [...pendingTargets];
const workers = Array.from(
{ length: Math.min(3, queue.length) },
async () => {
@@ -547,7 +595,7 @@ async function refreshDeploymentTruth(showErrors = false) {
"error",
);
}
return { checked: targets.length, failed: failures.length, discovery };
return { checked: targets.length, reusedInventory: targets.length - pendingTargets.length, failed: failures.length, discovery };
}
function selectRepository(id, shouldRender = true) {
+2
View File
@@ -241,6 +241,8 @@ function createMockDeploymentBridge(context) {
detected: 3,
adopted: 0,
verified: 1,
refreshedProfiles: 1,
refreshedProfileIds: ["profile-portfolio"],
linked: 2,
unmatched: 0,
needsReview: 2,
+1 -1
View File
@@ -5,7 +5,7 @@ function createMockRepositoryBridge(context) {
await wait(80);
snapshot();
return {
appVersion: "0.10.11-demo",
appVersion: "0.10.12-demo",
platform: "win32",
state: clone(state),
git: { available: true, version: "git version 2.47.3" },
+4
View File
@@ -357,6 +357,8 @@ select:focus-visible {
padding: 0 6px 10px;
}
.repo-row {
content-visibility: auto;
contain-intrinsic-size: auto 48px;
width: 100%;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
@@ -2706,6 +2708,8 @@ kbd {
background: linear-gradient(180deg, var(--primary), var(--success));
}
.server-inventory-panel .tool-row {
content-visibility: auto;
contain-intrinsic-size: auto 76px;
transition: background 150ms ease, transform 150ms ease;
}
.server-inventory-panel .tool-row:hover {