(() => { 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+ Deployments`, }; 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), }); })();