Files
ForgeFlow/src/renderer/mock-bridge.js
T
NuklearRabbit dec3b79793
Managed validation / full (pull_request) Successful in 27s
hygiene: prepare ForgeFlow for public release
2026-09-02 23:37:30 +02:00

590 lines
21 KiB
JavaScript

(() => {
if (window.forgeflow) return;
const wait = (ms = 180) => new Promise((resolve) => setTimeout(resolve, ms));
const clone = (value) => JSON.parse(JSON.stringify(value));
const iso = (offset = 0) => new Date(Date.now() + offset).toISOString();
const storage = {
get(key) {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
set(key, value) {
try {
localStorage.setItem(key, value);
} catch {}
},
};
const repositoryListeners = new Set();
const operationListeners = new Set();
const updateListeners = new Set();
const emitRepositories = () =>
repositoryListeners.forEach((listener) =>
listener({ reason: "demo-change" }),
);
const emitOperations = (operations) =>
operationListeners.forEach((listener) =>
listener({ operations: clone(operations) }),
);
const randomSha = () =>
`${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`
.padEnd(40, "a")
.slice(0, 40);
const makeStatus = ({
head,
branch = "main",
ahead = 0,
behind = 0,
upstream = `origin/${branch}`,
files = [],
}) => ({
branch: { oid: head, head: branch, upstream, ahead, behind },
files,
counts: {
changed: files.length,
staged: files.filter((item) => item.staged).length,
unstaged: files.filter((item) => item.unstaged).length,
conflicts: files.filter((item) => item.conflict).length,
untracked: files.filter((item) => item.untracked).length,
},
clean: files.length === 0,
root: "",
remoteUrl: "",
head,
shortHead: head.slice(0, 7),
fingerprint: `${head}:${branch}:${ahead}:${behind}:${files.map((item) => `${item.path}:${item.indexCode}${item.worktreeCode}`).join("|")}`,
});
const makeFile = (path, status = "modified", options = {}) => ({
path,
originalPath: options.originalPath || null,
indexCode: options.staged
? status === "added"
? "A"
: status === "deleted"
? "D"
: "M"
: ".",
worktreeCode: options.staged
? "."
: status === "untracked"
? "?"
: status === "deleted"
? "D"
: status === "conflict"
? "U"
: "M",
staged: Boolean(options.staged),
unstaged: !options.staged,
untracked: status === "untracked",
conflict: status === "conflict",
status,
});
const profile = (id, name, environment, options = {}) => ({
id,
name,
environment,
provider: "gitea-actions",
branch: options.branch || "main",
workflowFile: options.workflowFile || "deploy.yml",
rollbackWorkflowFile: options.rollbackWorkflowFile ?? "rollback.yml",
healthcheckUrl:
options.healthcheckUrl || `https://${environment}.internal/health`,
statusUrl:
options.statusUrl ||
`https://${environment}.internal/.well-known/forgeflow`,
confirmationRequired: options.confirmationRequired !== false,
inputs: {},
state: {
liveSha: options.liveSha || null,
previousSha: options.previousSha || null,
healthy: options.healthy ?? null,
healthConfigured: true,
statusConfigured: true,
healthStatus: options.healthy === false ? 503 : 200,
healthLatencyMs: 42,
checkedAt: options.checkedAt || iso(-120000),
},
});
const sshProfile = (id, name, environment, options = {}) => ({
id, name, environment, provider: "ssh-unraid", branch: options.branch || "main",
serverId: "demo-unraid", remoteFolder: options.remoteFolder || name,
deploymentMode: "server-git", composeFiles: ["compose.yml"],
composeProject: options.composeProject || String(options.remoteFolder || name).toLowerCase(),
composeServices: options.composeServices || [String(options.remoteFolder || name).toLowerCase()],
containerName: options.containerName || options.remoteFolder || name,
generatedCompose: false, adoptedFromServer: true, serverSourceOfTruth: true,
confirmationRequired: true,
serverGitAccess: { configured: options.accessConfigured !== false, keyFingerprint: "SHA256:demo", hostFingerprint: "SHA256:gitea", configuredAt: iso(-3600000) },
state: {
liveSha: options.liveSha || null, giteaSha: options.giteaSha || options.liveSha || null,
previousSha: options.previousSha || null, healthy: options.healthy ?? true,
containerRunning: true, runtimeVerification: "verified", matchesGitea: options.matchesGitea ?? true,
checkedAt: iso(-120000), dockerMan: { templateExists: true, webUi: true, icon: true },
},
});
const now = iso();
const defaultPreferences = {
autoRefresh: true,
repositoryPollSeconds: 4,
operationPollSeconds: 5,
fetchIntervalMinutes: 10,
preferredCloneProtocol: "https",
diagnosticsEnabled: true,
diagnosticLevel: "info",
logRetentionDays: 14,
maxLogFileMb: 8,
};
let state = {
schemaVersion: 8,
setupComplete: storage.get("forgeflow-demo-setup") !== "false",
appearance: storage.get("forgeflow-theme") || "dark",
gitea: {
baseUrl: "https://gitea.internal",
user: { login: "jens", full_name: "Jens" },
hasToken: true,
},
workspaceRoots: ["C:\\Development"],
repositoryMappings: {},
deploymentProfiles: {},
deploymentStates: {},
favorites: [
"jens/microsoft-cloud-operations-platform",
"jens/unraid-appops-gateway",
],
updates: {
owner: "Jens",
repo: "ForgeFlow",
branch: "main",
autoCheck: true,
lastCheckedAt: null,
},
servers: [
{
id: "server-unraid",
name: "Unraid",
host: "192.168.1.10",
port: 22,
username: "root",
authType: "privateKey",
basePath: "/mnt/user/appdata",
privateKeyPath: "C:\\Users\\your-name\\.ssh\\id_ed25519",
hostFingerprint: "SHA256:demo",
hasPassword: false,
hasPassphrase: false,
},
],
preferences: { ...defaultPreferences },
operations: [
{
id: "op-success",
type: "deployment",
action: "deploy",
status: "success",
repository: "jens/microsoft-cloud-operations-platform",
profileId: "profile-mcop-prod",
profileName: "Production",
environment: "production",
workflowFile: "deploy.yml",
branch: "main",
sha: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
shortSha: "b82f91a",
createdAt: now,
updatedAt: now,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "complete" },
{ id: "runner", label: "Runner execution", status: "complete" },
{ id: "healthcheck", label: "Healthcheck", status: "complete" },
{ id: "complete", label: "Complete", status: "complete" },
],
logs: [
"[info] Exact commit verified.",
"[job] deploy: success",
"[ok] Server reports b82f91a and healthcheck returned 200.",
],
run: {
id: 48,
runNumber: 48,
status: "completed",
conclusion: "success",
name: "ForgeFlow deployment",
},
runUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform/actions/runs/48",
},
{
id: "op-failed",
type: "deployment",
action: "deploy",
status: "failed",
repository: "jens/portfolio",
profileId: "profile-portfolio",
profileName: "Production",
environment: "production",
workflowFile: "deploy.yml",
branch: "main",
sha: "a7f2e1c1bb6147fc8b6633d2b08500c93402a719",
shortSha: "a7f2e1c",
createdAt: iso(-86400000),
updatedAt: iso(-86300000),
failure: { stage: "healthcheck", message: "Healthcheck returned 502." },
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "complete" },
{ id: "runner", label: "Runner execution", status: "complete" },
{ id: "healthcheck", label: "Healthcheck", status: "failed" },
{ id: "complete", label: "Complete", status: "failed" },
],
logs: ["[job] deploy: success", "[error] Healthcheck returned 502."],
},
],
};
let repositories = [
{
id: 1,
name: "microsoft-cloud-operations-platform",
fullName: "jens/microsoft-cloud-operations-platform",
owner: { login: "jens" },
description: "Tenant-aware Microsoft cloud operations console.",
private: true,
defaultBranch: "main",
htmlUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform",
cloneUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform.git",
sshUrl: "git@gitea.internal:jens/microsoft-cloud-operations-platform.git",
updatedAt: now,
localPath: "C:\\Development\\Microsoft-Cloud-Operations-Platform",
localStatus: makeStatus({
head: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-mcop-prod", "Production", "production", {
liveSha: "72bd10eb0173cd4346ca0f0f7dcc3e8182cc8fd0",
previousSha: "6ac991ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
healthy: true,
}),
profile("profile-mcop-stage", "Staging", "staging", {
liveSha: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
previousSha: "72bd10eb0173cd4346ca0f0f7dcc3e8182cc8fd0",
healthy: true,
confirmationRequired: false,
}),
],
},
{
id: 2,
name: "vacancyradar",
fullName: "jens/vacancyradar",
owner: { login: "jens" },
description: "Local-first vacancy intelligence cockpit.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/vacancyradar",
cloneUrl: "https://gitea.internal/jens/vacancyradar.git",
sshUrl: "git@gitea.internal:jens/vacancyradar.git",
updatedAt: now,
localPath: "C:\\Development\\VacancyRadar",
localStatus: makeStatus({
head: "c9182d0d28318c8cf0af109edc054732426aadf1",
branch: "feature/deployment-api",
files: [
makeFile("src/api/deploy.ts", "added", { staged: true }),
makeFile("src/main.tsx"),
makeFile("src/components/Sidebar.tsx"),
],
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-vr", "Production", "production", {
liveSha: "c117ab9d28318c8cf0af109edc054732426aadf1",
previousSha: "b1f57aad28318c8cf0af109edc054732426aadf1",
healthy: true,
}),
],
},
{
id: 3,
name: "unraid-appops-gateway",
fullName: "jens/unraid-appops-gateway",
owner: { login: "jens" },
description: "Safe operations gateway for Unraid and Portainer.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/unraid-appops-gateway",
cloneUrl: "https://gitea.internal/jens/unraid-appops-gateway.git",
sshUrl: "git@gitea.internal:jens/unraid-appops-gateway.git",
updatedAt: now,
localPath: "C:\\Development\\Unraid-AppOps-Gateway",
localStatus: makeStatus({
head: "f2d1e0a1bb6147fc8b6633d2b08500c93402a719",
ahead: 2,
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-appops", "Production", "production", {
liveSha: "8ac731b1bb6147fc8b6633d2b08500c93402a719",
previousSha: "7bc198a1bb6147fc8b6633d2b08500c93402a719",
healthy: true,
}),
],
},
{
id: 4,
name: "support-bundle-collector",
fullName: "jens/support-bundle-collector",
owner: { login: "jens" },
description: "Privacy-aware Windows support bundle collector.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/support-bundle-collector",
cloneUrl: "https://gitea.internal/jens/support-bundle-collector.git",
sshUrl: "git@gitea.internal:jens/support-bundle-collector.git",
updatedAt: now,
localPath: null,
localStatus: null,
linkState: "remote-only",
deploymentProfiles: [],
},
{
id: 5,
name: "portfolio",
fullName: "jens/portfolio",
owner: { login: "jens" },
description: "Professional infrastructure and automation portfolio.",
private: false,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/portfolio",
cloneUrl: "https://gitea.internal/jens/portfolio.git",
sshUrl: "git@gitea.internal:jens/portfolio.git",
updatedAt: now,
localPath: "C:\\Development\\portfolio",
localStatus: makeStatus({
head: "a7f2e1c1bb6147fc8b6633d2b08500c93402a719",
behind: 1,
}),
linkState: "linked",
deploymentProfiles: [
sshProfile("profile-portfolio", "Production", "production", {
remoteFolder: "Portfolio",
containerName: "Portfolio",
liveSha: "4c20dd11bb6147fc8b6633d2b08500c93402a719",
giteaSha: "a7f2e1c1bb6147fc8b6633d2b08500c93402a719",
previousSha: "31adfe11bb6147fc8b6633d2b08500c93402a719",
healthy: false,
matchesGitea: false,
}),
],
},
];
const diffs = {
"src/api/deploy.ts": `diff --git a/src/api/deploy.ts b/src/api/deploy.ts\nnew file mode 100644\n--- /dev/null\n+++ b/src/api/deploy.ts\n@@ -0,0 +1,18 @@\n+export interface DeploymentRequest {\n+ environment: 'staging' | 'production';\n+ commitSha: string;\n+}\n+\n+export async function deploy(request: DeploymentRequest) {\n+ return api.post('/deployments', request);\n+}`,
"src/main.tsx": `diff --git a/src/main.tsx b/src/main.tsx\nindex 45ad1a2..939fc17 100644\n--- a/src/main.tsx\n+++ b/src/main.tsx\n@@ -24,8 +24,9 @@ import { Router } from './routes';\n-const API_ENDPOINT = 'http://localhost:3000';\n+const API_ENDPOINT = process.env.VITE_API_URL || '/api';\n+const DEPLOY_VERSION = '1.0.4-rc1';`,
"src/components/Sidebar.tsx": `diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx\nindex a7bbd82..bf21e90 100644\n--- a/src/components/Sidebar.tsx\n+++ b/src/components/Sidebar.tsx\n@@ -31,6 +31,7 @@ export function Sidebar() {\n+ <NavItem to="/deployments">Deployments</NavItem>`,
};
const findRepo = (localPath) =>
repositories.find((item) => item.localPath === localPath);
const findProfileRepo = (profileId) =>
repositories.find((item) =>
item.deploymentProfiles.some((entry) => entry.id === profileId),
);
const syncState = () => {
state.deploymentProfiles = {};
state.deploymentStates = {};
state.repositoryMappings = {};
for (const repository of repositories) {
if (repository.localPath)
state.repositoryMappings[repository.fullName.toLowerCase()] =
repository.localPath;
state.deploymentProfiles[repository.fullName.toLowerCase()] =
repository.deploymentProfiles.map(
({ state: profileState, ...entry }) => entry,
);
for (const entry of repository.deploymentProfiles)
if (entry.state) state.deploymentStates[entry.id] = clone(entry.state);
}
};
const recompute = (repository) => {
const status = repository.localStatus;
if (status) {
status.counts = {
changed: status.files.length,
staged: status.files.filter((item) => item.staged).length,
unstaged: status.files.filter((item) => item.unstaged).length,
conflicts: status.files.filter((item) => item.conflict).length,
untracked: status.files.filter((item) => item.untracked).length,
};
status.clean = status.files.length === 0;
status.shortHead = status.head.slice(0, 7);
status.branch.oid = status.head;
}
repository.favorite = state.favorites.includes(
repository.fullName.toLowerCase(),
);
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,
),
);
repository.attention =
!repository.localPath ||
Boolean(
status?.counts.conflicts ||
status?.branch.behind ||
status?.branch.ahead ||
status?.counts.changed,
);
repository.attentionReason = !repository.localPath
? "No local folder linked"
: status?.counts.conflicts
? `${status.counts.conflicts} conflict(s)`
: status?.counts.changed
? `${status.counts.changed} local change(s)`
: status?.branch.behind
? `${status.branch.behind} commit(s) behind remote`
: status?.branch.ahead
? `${status.branch.ahead} unpushed commit(s)`
: null;
repository.preferredCloneUrl =
state.preferences.preferredCloneProtocol === "ssh"
? repository.sshUrl
: repository.cloneUrl;
};
const snapshot = () => {
repositories.forEach(recompute);
syncState();
return clone(repositories);
};
syncState();
const commitHistory = [
{
sha: "c9182d0d28318c8cf0af109edc054732426aadf1",
shortSha: "c9182d0",
author: "Jens",
date: now,
subject: "feat: add deployment provider contract",
},
{
sha: "1fa7399d28318c8cf0af109edc054732426aadf1",
shortSha: "1fa7399",
author: "Jens",
date: iso(-86400000),
subject: "refactor: consolidate repository state",
},
{
sha: "a251a11d28318c8cf0af109edc054732426aadf1",
shortSha: "a251a11",
author: "Jens",
date: iso(-172800000),
subject: "docs: define deployment safety gates",
},
];
const branchesByRepo = new Map();
const stashesByRepo = new Map();
function updateOperation(operation) {
state.operations = [
clone(operation),
...state.operations.filter((item) => item.id !== operation.id),
].slice(0, 250);
emitOperations([operation]);
return clone(operation);
}
function advanceOperation(operation) {
if (
!operation ||
["success", "failed", "cancelled", "rolled-back"].includes(
operation.status,
)
)
return operation;
operation.demoPolls = (operation.demoPolls || 0) + 1;
if (operation.demoPolls === 1) {
operation.status = "running";
operation.run = {
id: 81,
runNumber: 81,
status: "running",
conclusion: null,
name:
operation.action === "rollback"
? "ForgeFlow rollback"
: "ForgeFlow deployment",
};
operation.runUrl = `https://gitea.internal/${operation.repository}/actions/runs/81`;
operation.stages.find((item) => item.id === "queued").status = "complete";
operation.stages.find((item) => item.id === "runner").status = "active";
operation.jobs = [
{
id: 201,
name: operation.action === "rollback" ? "rollback" : "deploy",
status: "running",
conclusion: null,
},
];
operation.logs.push(`[job] ${operation.jobs[0].name}: running`);
} else if (operation.demoPolls >= 2) {
operation.status =
operation.action === "rollback" ? "rolled-back" : "success";
operation.stages.forEach((item) => {
item.status = "complete";
});
operation.jobs = [
{
id: 201,
name: operation.action === "rollback" ? "rollback" : "deploy",
status: "completed",
conclusion: "success",
},
];
operation.logs.push(
"[ok] Runner completed successfully.",
`[ok] Server status endpoint confirms ${operation.shortSha}.`,
);
const repository = repositories.find(
(item) => item.fullName === operation.repository,
);
const targetProfile = repository?.deploymentProfiles.find(
(item) => item.id === operation.profileId,
);
if (targetProfile) {
const oldLive = targetProfile.state.liveSha;
targetProfile.state.previousSha = oldLive;
targetProfile.state.liveSha = operation.sha;
targetProfile.state.healthy = true;
targetProfile.state.checkedAt = iso();
}
}
operation.updatedAt = iso();
return operation;
}
const bridgeContext = { wait, clone, iso, storage, repositoryListeners, operationListeners, updateListeners, emitRepositories, emitOperations, randomSha, now, profile, state, repositories, recompute, snapshot, commitHistory, advanceOperation, syncState, diffs, findRepo, findProfileRepo, branchesByRepo, stashesByRepo, updateOperation };
window.forgeflow = Object.freeze({
...createMockRepositoryBridge(bridgeContext),
...createMockDeploymentBridge(bridgeContext),
});
})();