699 lines
28 KiB
JavaScript
699 lines
28 KiB
JavaScript
function createMockDeploymentBridge(context) {
|
|
const { 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 } = context;
|
|
return {
|
|
async saveDeploymentProfile(fullName, input) {
|
|
const repo = repositories.find((item) => item.fullName === fullName);
|
|
const existing = repo.deploymentProfiles.find(
|
|
(item) => item.id === input.id,
|
|
);
|
|
const saved = {
|
|
...(existing ||
|
|
profile(
|
|
input.id || `profile-${Date.now()}`,
|
|
input.name || input.environment,
|
|
input.environment || "production",
|
|
)),
|
|
...input,
|
|
id: input.id || `profile-${Date.now()}`,
|
|
provider: input.provider || existing?.provider || "gitea-actions",
|
|
inputs: existing?.inputs || {},
|
|
state: existing?.state || {
|
|
liveSha: null,
|
|
previousSha: null,
|
|
healthy: null,
|
|
healthConfigured: Boolean(input.healthcheckUrl),
|
|
statusConfigured: Boolean(input.statusUrl),
|
|
checkedAt: null,
|
|
},
|
|
};
|
|
repo.deploymentProfiles = [
|
|
...repo.deploymentProfiles.filter((item) => item.id !== saved.id),
|
|
saved,
|
|
];
|
|
snapshot();
|
|
return { profile: clone(saved), state: clone(state) };
|
|
},
|
|
async deleteDeploymentProfile(fullName, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === fullName);
|
|
repo.deploymentProfiles = repo.deploymentProfiles.filter(
|
|
(item) => item.id !== profileId,
|
|
);
|
|
snapshot();
|
|
return { profiles: clone(repo.deploymentProfiles), state: clone(state) };
|
|
},
|
|
async deploymentPreflight(repository, profileId) {
|
|
await wait(280);
|
|
const profile = repository.deploymentProfiles.find(
|
|
(item) => item.id === profileId,
|
|
);
|
|
const status = repository.localStatus;
|
|
const checks = [
|
|
{
|
|
id: "repository.linked",
|
|
label: "Local repository link",
|
|
status: repository.localPath ? "pass" : "fail",
|
|
detail: repository.localPath || "No local folder linked.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "git.branch",
|
|
label: "Allowed branch",
|
|
status: status?.branch.head === profile?.branch ? "pass" : "fail",
|
|
detail: `Current: ${status?.branch.head || "unknown"}; required: ${profile?.branch || "unknown"}.`,
|
|
required: true,
|
|
},
|
|
{
|
|
id: "git.clean",
|
|
label: "Clean working tree",
|
|
status: status?.clean ? "pass" : "fail",
|
|
detail: status?.clean
|
|
? "No uncommitted changes."
|
|
: `${status?.counts.changed || 0} changed file(s).`,
|
|
required: true,
|
|
},
|
|
{
|
|
id: "git.sync",
|
|
label: "Local and Gitea synchronized",
|
|
status:
|
|
!status?.branch.ahead && !status?.branch.behind ? "pass" : "fail",
|
|
detail: `${status?.branch.ahead || 0} ahead, ${status?.branch.behind || 0} behind.`,
|
|
required: true,
|
|
},
|
|
{
|
|
id: "workflow.deploy.remote",
|
|
label: "Deploy workflow on Gitea branch",
|
|
status: "pass",
|
|
detail: `${profile?.workflowFile || "deploy.yml"} exists on ${profile?.branch || "main"}.`,
|
|
required: true,
|
|
},
|
|
{
|
|
id: "gitea.actions",
|
|
label: "Gitea Actions API",
|
|
status: "pass",
|
|
detail: "The Actions runs endpoint is accessible.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "server.status",
|
|
label: "Server version endpoint",
|
|
status: profile?.statusUrl ? "pass" : "warning",
|
|
detail: profile?.statusUrl
|
|
? `Endpoint reachable; live ${profile.state?.liveSha?.slice(0, 7) || "unknown"}.`
|
|
: "No status URL configured.",
|
|
required: false,
|
|
},
|
|
{
|
|
id: "server.health",
|
|
label: "Application healthcheck",
|
|
status: profile?.healthcheckUrl ? "pass" : "warning",
|
|
detail: profile?.healthcheckUrl
|
|
? "HTTP 200 in 42 ms."
|
|
: "No healthcheck URL configured.",
|
|
required: false,
|
|
},
|
|
];
|
|
const blocking = checks
|
|
.filter((i) => i.required && i.status === "fail")
|
|
.map((i) => i.id);
|
|
return {
|
|
kind: "deployment",
|
|
repository: repository.fullName,
|
|
profileId,
|
|
startedAt: iso(-100),
|
|
completedAt: iso(),
|
|
checks,
|
|
summary: {
|
|
counts: {
|
|
pass: checks.filter((i) => i.status === "pass").length,
|
|
warning: checks.filter((i) => i.status === "warning").length,
|
|
fail: checks.filter((i) => i.status === "fail").length,
|
|
skipped: 0,
|
|
},
|
|
blocking,
|
|
ready: blocking.length === 0,
|
|
},
|
|
head: status?.head || null,
|
|
};
|
|
},
|
|
async deploy(repository, profileId, sha) {
|
|
await wait(320);
|
|
const selected = repository.deploymentProfiles.find(
|
|
(item) => item.id === profileId,
|
|
);
|
|
const operation = {
|
|
id: `deploy-${Date.now()}`,
|
|
type: "deployment",
|
|
action: "deploy",
|
|
status: "queued",
|
|
repository: repository.fullName,
|
|
profileId,
|
|
profileName: selected.name,
|
|
environment: selected.environment,
|
|
workflowFile: selected.workflowFile,
|
|
branch: selected.branch,
|
|
sha,
|
|
shortSha: sha.slice(0, 7),
|
|
dispatchedAt: iso(),
|
|
createdAt: iso(),
|
|
updatedAt: iso(),
|
|
demoPolls: 0,
|
|
stages: [
|
|
{ id: "requested", label: "Requested", status: "complete" },
|
|
{ id: "verified", label: "Verified", status: "complete" },
|
|
{ id: "queued", label: "Workflow queued", status: "active" },
|
|
{ id: "runner", label: "Runner execution", status: "pending" },
|
|
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
|
|
{ id: "complete", label: "Complete", status: "pending" },
|
|
],
|
|
logs: [
|
|
`[info] Verified clean ${selected.branch} at ${sha}`,
|
|
`[ok] Gitea accepted ${selected.workflowFile}.`,
|
|
],
|
|
};
|
|
return updateOperation(operation);
|
|
},
|
|
async rollback(repository, profileId, targetSha) {
|
|
await wait(320);
|
|
const selected = repository.deploymentProfiles.find(
|
|
(item) => item.id === profileId,
|
|
);
|
|
const operation = {
|
|
id: `rollback-${Date.now()}`,
|
|
type: "deployment",
|
|
action: "rollback",
|
|
status: "queued",
|
|
repository: repository.fullName,
|
|
profileId,
|
|
profileName: selected.name,
|
|
environment: selected.environment,
|
|
workflowFile: selected.rollbackWorkflowFile,
|
|
branch: selected.branch,
|
|
sha: targetSha,
|
|
shortSha: targetSha.slice(0, 7),
|
|
dispatchedAt: iso(),
|
|
createdAt: iso(),
|
|
updatedAt: iso(),
|
|
demoPolls: 0,
|
|
stages: [
|
|
{ id: "requested", label: "Requested", status: "complete" },
|
|
{ id: "verified", label: "Verified", status: "complete" },
|
|
{ id: "queued", label: "Workflow queued", status: "active" },
|
|
{ id: "runner", label: "Runner execution", status: "pending" },
|
|
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
|
|
{ id: "complete", label: "Complete", status: "pending" },
|
|
],
|
|
logs: [
|
|
`[warning] Rollback target verified: ${targetSha}`,
|
|
`[ok] Gitea accepted ${selected.rollbackWorkflowFile}.`,
|
|
],
|
|
};
|
|
return updateOperation(operation);
|
|
},
|
|
async healthcheck() {
|
|
await wait(160);
|
|
return { configured: true, healthy: true, status: 200, latencyMs: 42 };
|
|
},
|
|
async refreshProfileState(fullName, profileId) {
|
|
await wait(240);
|
|
const repo =
|
|
repositories.find((item) => item.fullName === fullName) ||
|
|
findProfileRepo(profileId);
|
|
const target = repo?.deploymentProfiles.find(
|
|
(item) => item.id === profileId,
|
|
);
|
|
if (!target) throw new Error("Deployment profile not found.");
|
|
target.state = {
|
|
...target.state,
|
|
checkedAt: iso(),
|
|
healthy: target.state.healthy !== false,
|
|
healthConfigured: Boolean(target.healthcheckUrl),
|
|
statusConfigured: Boolean(target.statusUrl),
|
|
};
|
|
syncState();
|
|
return clone(target.state);
|
|
},
|
|
async discoverServerDeployments() {
|
|
await wait(80);
|
|
return [
|
|
{
|
|
serverId: "server-unraid",
|
|
serverName: "Unraid",
|
|
detected: 3,
|
|
adopted: 0,
|
|
verified: 1,
|
|
linked: 2,
|
|
unmatched: 0,
|
|
needsReview: 2,
|
|
running: 3,
|
|
stopped: 0,
|
|
capabilities: {
|
|
docker: true,
|
|
dockerReady: true,
|
|
compose: true,
|
|
git: false,
|
|
tar: true,
|
|
checksum: true,
|
|
},
|
|
warnings: [],
|
|
workloads: [
|
|
{
|
|
workloadId: "workload-demo-linked",
|
|
displayName: "Portfolio",
|
|
status: "linked",
|
|
runtime: { running: true, health: "healthy" },
|
|
compose: {
|
|
project: "portfolio",
|
|
workingDir: "/mnt/user/appdata/portfolio",
|
|
configFiles: ["/mnt/user/appdata/portfolio/docker-compose.yml"],
|
|
services: ["web"],
|
|
},
|
|
containers: [{ name: "Portfolio", running: true }],
|
|
candidates: [],
|
|
link: {
|
|
profileId: "profile-portfolio",
|
|
repositoryFullName: "jens/portfolio",
|
|
source: "manual",
|
|
},
|
|
},
|
|
{
|
|
workloadId: "workload-demo-review",
|
|
displayName: "OmniRoute",
|
|
status: "suggested",
|
|
runtime: { running: true, health: "unverified" },
|
|
compose: {
|
|
project: "omniroute",
|
|
workingDir: "/mnt/user/appdata/OmniRoute",
|
|
configFiles: ["/mnt/user/appdata/OmniRoute/docker-compose.yml"],
|
|
services: ["omniroute"],
|
|
},
|
|
containers: [{ name: "omniroute", running: true }],
|
|
remoteFolderCandidate: "OmniRoute",
|
|
candidates: repositories.slice(0, 1).map((repository) => ({
|
|
repositoryFullName: repository.fullName,
|
|
repositoryName: repository.name,
|
|
score: 55,
|
|
exact: false,
|
|
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",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
},
|
|
async planServerReconciliation(serverId) {
|
|
await wait(90);
|
|
const id = "a".repeat(64);
|
|
return {
|
|
inventory: (await this.discoverServerDeployments()).find((item) => item.serverId === serverId),
|
|
plan: {
|
|
id,
|
|
serverId,
|
|
summary: { additions: 0, updates: 1, stale: 0, conflicts: 1 },
|
|
additions: [],
|
|
updates: [{ workloadId: "workload-demo-linked", profileId: "profile-portfolio", repositoryFullName: "jens/portfolio", impact: "Refresh detected Compose identity and observed deployment state" }],
|
|
stale: [],
|
|
conflicts: [{ workloadId: "workload-demo-review", displayName: "OmniRoute", status: "suggested", candidates: [{ repositoryFullName: repositories[0].fullName, score: 55, exact: false }] }],
|
|
},
|
|
};
|
|
},
|
|
async applyServerReconciliation(serverId, planId) {
|
|
await wait(120);
|
|
if (serverId !== "server-unraid" || planId !== "a".repeat(64)) throw new Error("The reconciliation plan is stale.");
|
|
return { adopted: 0, refreshed: 1, retired: 0, state: clone(state) };
|
|
},
|
|
async planInventoryReview(serverId, workloadId, action, reason = "", repositoryFullName = null) {
|
|
if (["ignore", "manual-exclude", "exclude-scan-root"].includes(action) && reason.length < 5) throw new Error("A meaningful review reason is required.");
|
|
return { id: "b".repeat(64), serverId, workloadId, action, reason, repositoryFullName, evidenceHash: "c".repeat(64), classification: "ambiguous", containersUnaffected: true, configurationChanges: [`Persist review decision ${action}`], recovery: "Remove the decision or rescan after evidence changes." };
|
|
},
|
|
async applyInventoryReview(serverId, workloadId, action, reason, repositoryFullName, planId) {
|
|
if (planId !== "b".repeat(64)) throw new Error("The inventory review plan is stale.");
|
|
const inventory = (await this.discoverServerDeployments()).find((item) => item.serverId === serverId);
|
|
const workload = inventory.workloads.find((item) => item.workloadId === workloadId);
|
|
if (workload) workload.reviewDecision = { action, reason, repositoryFullName, evidenceHash: "c".repeat(64) };
|
|
return { decision: workload?.reviewDecision, inventory, state: clone(state) };
|
|
},
|
|
async linkServerWorkload(repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "") {
|
|
await wait(120);
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName);
|
|
if (!repo) throw new Error("Repository not found.");
|
|
const id = `profile-${workloadId}`;
|
|
const saved = {
|
|
id,
|
|
name: `Unraid · ${remoteFolder || repo.name}`,
|
|
environment: "production",
|
|
provider: "ssh-unraid",
|
|
branch: repo.defaultBranch || "main",
|
|
serverId,
|
|
remoteFolder: remoteFolder || repo.name,
|
|
deploymentMode,
|
|
composeFile: "docker-compose.yml",
|
|
composeFiles: ["docker-compose.yml"],
|
|
composeProject: String(remoteFolder || repo.name).toLowerCase(),
|
|
composeService: String(remoteFolder || repo.name).toLowerCase(),
|
|
composeServices: [String(remoteFolder || repo.name).toLowerCase()],
|
|
containerName: remoteFolder || repo.name,
|
|
preservePaths: [".env", "appdata", "data", "logs", "config"],
|
|
generatedCompose: false,
|
|
adoptedFromServer: true,
|
|
serverSourceOfTruth: true,
|
|
manageDockerMan: false,
|
|
forceRecreate: false,
|
|
removeOrphans: false,
|
|
workloadIdentity: { workloadId, linkSource: "manual", linkedAt: iso() },
|
|
confirmationRequired: true,
|
|
state: {
|
|
liveSha: null,
|
|
healthy: null,
|
|
containerRunning: true,
|
|
runtimeVerification: "running-unverified",
|
|
checkedAt: iso(),
|
|
},
|
|
};
|
|
repo.deploymentProfiles = [
|
|
...repo.deploymentProfiles.filter((item) => item.id !== id),
|
|
saved,
|
|
];
|
|
syncState();
|
|
return { profile: clone(saved), state: clone(state) };
|
|
},
|
|
async configureServerGitAccess(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName);
|
|
const target = repo?.deploymentProfiles.find((item) => item.id === profileId);
|
|
if (!target) throw new Error("Deployment profile not found.");
|
|
target.deploymentMode = "server-git";
|
|
target.serverGitAccess = { configured: true, keyFingerprint: "SHA256:demo", hostFingerprint: "SHA256:gitea", configuredAt: iso() };
|
|
syncState();
|
|
return { profile: clone(target), created: true, remoteSha: target.state?.giteaSha || repo.localStatus?.head };
|
|
},
|
|
async verifyServerGitProfile(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName);
|
|
const target = repo?.deploymentProfiles.find((item) => item.id === profileId);
|
|
if (!target) throw new Error("Deployment profile not found.");
|
|
const branchSha = target.state?.giteaSha || repo.localStatus?.head || null;
|
|
const liveSha = target.state?.liveSha || null;
|
|
return {
|
|
readiness: branchSha && liveSha === branchSha ? "Ready" : "Commit mismatch",
|
|
ready: true,
|
|
checkedAt: iso(),
|
|
repository: repo.fullName,
|
|
profileId,
|
|
branchSha,
|
|
liveSha,
|
|
checks: [
|
|
{ id: "remote-branch", label: "Gitea branch", status: "pass", detail: "Exact branch resolved." },
|
|
{ id: "deploy-key-scope", label: "Repository deploy key", status: "pass", detail: "Repository-scoped and read-only." },
|
|
{ id: "server-git-access", label: "Unraid to Gitea", status: "pass", detail: "Pinned SSH access verified." },
|
|
],
|
|
};
|
|
},
|
|
async deployKeyInventory(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName);
|
|
const profile = repo?.deploymentProfiles.find((item) => item.id === profileId);
|
|
return { repository: repo.fullName, profileId, server: { id: profile.serverId, name: "Unraid" }, configuredKey: { id: profile.serverGitAccess?.deployKeyId || 17, readOnly: true }, serverKey: { privateKeyPresent: true, fingerprint: profile.serverGitAccess?.keyFingerprint || "SHA256:demo" }, stale: false, orphaned: [], shared: [], conflicts: [], ready: true, checkedAt: iso() };
|
|
},
|
|
async planDeployKeyRotation(repository, profileId) {
|
|
const evidence = await this.deployKeyInventory(repository, profileId);
|
|
return { id: `rotation-${profileId}`, operation: "rotate-deploy-key", impact: ["Generate a new server-side key", "Verify read-only access", "Switch atomically", "Revoke the previous key"], recovery: "Previous access remains recoverable until verification succeeds.", evidence };
|
|
},
|
|
async applyDeployKeyRotation(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
|
|
profile.serverGitAccess = { ...profile.serverGitAccess, configured: true, deployKeyId: 18, keyFingerprint: "SHA256:rotated", rotatedAt: iso() }; syncState(); return { profile: clone(profile), state: clone(state) };
|
|
},
|
|
async planDeployKeyRevocation(repository, profileId) {
|
|
const evidence = await this.deployKeyInventory(repository, profileId);
|
|
return { id: `revocation-${profileId}`, operation: "revoke-deploy-key", impact: ["Remove the repository key", "Disable server pull", "Preserve recovery material"], containersUnaffected: true, evidence };
|
|
},
|
|
async applyDeployKeyRevocation(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
|
|
profile.deploymentMode = "monitor-only"; profile.serverGitAccess = { ...profile.serverGitAccess, configured: false, revokedAt: iso(), recoveryAvailable: true }; syncState(); return { profile: clone(profile), state: clone(state) };
|
|
},
|
|
async restoreDeployKey(repository, profileId) {
|
|
const repo = repositories.find((item) => item.fullName === repository.fullName); const profile = repo.deploymentProfiles.find((item) => item.id === profileId);
|
|
profile.deploymentMode = "server-git"; profile.serverGitAccess = { ...profile.serverGitAccess, configured: true, deployKeyId: 19, keyFingerprint: "SHA256:restored", restoredAt: iso() }; syncState(); return { profile: clone(profile), state: clone(state), proof: { ready: true } };
|
|
},
|
|
async refreshOperations(operationId = null) {
|
|
await wait(300);
|
|
if (operationId) {
|
|
const operation = state.operations.find(
|
|
(item) => item.id === operationId,
|
|
);
|
|
if (!operation) throw new Error("Operation not found.");
|
|
return updateOperation(advanceOperation(operation));
|
|
}
|
|
const active = state.operations
|
|
.filter(
|
|
(item) =>
|
|
!["success", "failed", "cancelled", "rolled-back"].includes(
|
|
item.status,
|
|
),
|
|
)
|
|
.map(advanceOperation);
|
|
if (active.length) emitOperations(active);
|
|
state.operations = state.operations.map(
|
|
(item) => active.find((entry) => entry.id === item.id) || item,
|
|
);
|
|
return clone(active);
|
|
},
|
|
async getOperation(operationId) {
|
|
return clone(
|
|
state.operations.find((item) => item.id === operationId) || null,
|
|
);
|
|
},
|
|
async gitValidatorScan(fullName) {
|
|
await wait(260);
|
|
const policy = state.gitValidatorPolicy || { id: "standard", label: "Standard", requiredScore: 70 };
|
|
const activeWarnings = 3;
|
|
return {
|
|
repository: fullName,
|
|
checkedAt: iso(),
|
|
score: 78,
|
|
grade: "Good",
|
|
policy,
|
|
ready: 78 >= policy.requiredScore && (policy.id === "minimal" || activeWarnings === 0),
|
|
commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847",
|
|
trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] },
|
|
expiredSuppressions: [],
|
|
summary: { passed: 7, warnings: 3, errors: 0, repairable: 2 },
|
|
checks: [
|
|
{
|
|
id: "origin",
|
|
category: "Repository identity",
|
|
title: "Origin matches Gitea",
|
|
status: "pass",
|
|
detail: "The local origin resolves to this Gitea repository.",
|
|
weight: 15,
|
|
},
|
|
{
|
|
id: "default-branch-protection",
|
|
category: "Gitea governance",
|
|
title: "Default branch protection",
|
|
status: "warning",
|
|
detail: "main accepts unprotected direct changes.",
|
|
weight: 18,
|
|
fixAction: "protect-default-branch",
|
|
safe: false,
|
|
confirmation:
|
|
"Protect main on Gitea and block direct and force pushes?",
|
|
},
|
|
{
|
|
id: "force-push",
|
|
category: "Gitea governance",
|
|
title: "Force-push protection",
|
|
status: "pass",
|
|
detail: "Force pushes are blocked.",
|
|
weight: 8,
|
|
},
|
|
{
|
|
id: "upstream",
|
|
category: "Branch hygiene",
|
|
title: "Current branch has an upstream",
|
|
status: "pass",
|
|
detail: "main tracks origin/main.",
|
|
weight: 8,
|
|
},
|
|
{
|
|
id: "working-tree",
|
|
category: "Branch hygiene",
|
|
title: "Working tree is intentional",
|
|
status: "warning",
|
|
detail: "3 changed files require review, commit or stash.",
|
|
weight: 5,
|
|
},
|
|
{
|
|
id: "identity",
|
|
category: "Commit integrity",
|
|
title: "Repository author identity",
|
|
status: "pass",
|
|
detail: "Jens <jens@example.test>",
|
|
weight: 7,
|
|
},
|
|
{
|
|
id: "local-safety",
|
|
category: "Local configuration",
|
|
title: "Safe synchronization defaults",
|
|
status: "warning",
|
|
detail: "Recommended repository-local safeguards are incomplete.",
|
|
weight: 10,
|
|
fixAction: "configure-local-safety",
|
|
safe: true,
|
|
},
|
|
{
|
|
id: "readme",
|
|
category: "Repository documentation",
|
|
title: "README is versioned",
|
|
status: "pass",
|
|
detail: "Repository documentation is tracked.",
|
|
weight: 7,
|
|
},
|
|
{
|
|
id: "gitignore",
|
|
category: "Repository hygiene",
|
|
title: ".gitignore is versioned",
|
|
status: "pass",
|
|
detail: "Generated files are excluded centrally.",
|
|
weight: 8,
|
|
},
|
|
{
|
|
id: "tracked-secrets",
|
|
category: "Security",
|
|
title: "No secret-shaped files are tracked",
|
|
status: "pass",
|
|
detail:
|
|
"No tracked environment, key or credential filenames detected.",
|
|
weight: 22,
|
|
},
|
|
{
|
|
id: "large-files",
|
|
category: "Repository performance",
|
|
title: "No oversized tracked files",
|
|
status: "pass",
|
|
detail: "No tracked files above 10 MB were found.",
|
|
weight: 7,
|
|
},
|
|
],
|
|
};
|
|
},
|
|
async gitValidatorSetPolicy(_fullName, policy) {
|
|
const requiredScores = { minimal: 55, standard: 70, strict: 82, production: 90 };
|
|
state.gitValidatorPolicy = {
|
|
id: policy.id,
|
|
label: policy.id[0].toUpperCase() + policy.id.slice(1),
|
|
requiredScore: requiredScores[policy.id] || 70,
|
|
};
|
|
return clone(state.gitValidatorPolicy);
|
|
},
|
|
async gitValidatorSuppress(_fullName, suppression) {
|
|
return { ...suppression, id: `suppression-${Date.now()}`, createdAt: iso() };
|
|
},
|
|
async gitValidatorPreviewRepair(_fullName, check) {
|
|
return { checkId: check.id, action: check.fixAction, files: [".git/config"], diff: "+ reviewed configuration change\n", remoteMutation: check.fixAction === "protect-default-branch" };
|
|
},
|
|
async gitValidatorExport(fullName, format) {
|
|
return { extension: format === "markdown" ? "md" : format, mimeType: "text/plain", content: `# Git assurance — ${fullName}\n` };
|
|
},
|
|
async gitValidatorRepair() {
|
|
await wait(180);
|
|
return { repaired: true };
|
|
},
|
|
async diagnosticsStatus() {
|
|
return {
|
|
enabled: state.preferences.diagnosticsEnabled !== false,
|
|
level: state.preferences.diagnosticLevel,
|
|
retentionDays: state.preferences.logRetentionDays,
|
|
maxFileMb: state.preferences.maxLogFileMb,
|
|
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
|
|
fileCount: 2,
|
|
totalBytes: 18432,
|
|
totalSize: "18.0 KB",
|
|
latestAt: iso(-2000),
|
|
lastWriteError: null,
|
|
};
|
|
},
|
|
async exportConfigurationBackup() {
|
|
return {
|
|
filePath: "C:\\Downloads\\ForgeFlow-Configuration-demo.ffbackup",
|
|
};
|
|
},
|
|
async importConfigurationBackup() {
|
|
return { state: clone(state), exportedAt: iso(-86400000) };
|
|
},
|
|
async listAuditEvents() {
|
|
return [
|
|
{
|
|
id: "audit-1",
|
|
timestamp: iso(-60000),
|
|
event: "deployment.completed",
|
|
details: { repository: "Jens/ForgeFlow", result: "success" },
|
|
},
|
|
];
|
|
},
|
|
async exportAuditLog() {
|
|
return { filePath: "C:\\Downloads\\ForgeFlow-Audit-demo.json", count: 1 };
|
|
},
|
|
async clearDiagnostics() {
|
|
return {
|
|
enabled: true,
|
|
level: state.preferences.diagnosticLevel,
|
|
retentionDays: state.preferences.logRetentionDays,
|
|
maxFileMb: state.preferences.maxLogFileMb,
|
|
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
|
|
fileCount: 1,
|
|
totalBytes: 256,
|
|
totalSize: "256 B",
|
|
latestAt: iso(),
|
|
lastWriteError: null,
|
|
};
|
|
},
|
|
async openDiagnosticsFolder() {
|
|
return true;
|
|
},
|
|
async exportDiagnostics(privacyMode = "standard") {
|
|
await wait(500);
|
|
return {
|
|
path: `C:\Users\Jens\Downloads\ForgeFlow-Diagnostics-demo.zip`,
|
|
bytes: 38221,
|
|
size: "37.3 KB",
|
|
sha256: "b".repeat(64),
|
|
privacyMode,
|
|
generatedAt: iso(),
|
|
};
|
|
},
|
|
async showDiagnosticBundle() {
|
|
return true;
|
|
},
|
|
async reportRendererEvent() {
|
|
return true;
|
|
},
|
|
onRepositoriesChanged(listener) {
|
|
repositoryListeners.add(listener);
|
|
return () => repositoryListeners.delete(listener);
|
|
},
|
|
onOperationsChanged(listener) {
|
|
operationListeners.add(listener);
|
|
return () => operationListeners.delete(listener);
|
|
},
|
|
onUpdatesChanged(listener) {
|
|
updateListeners.add(listener);
|
|
return () => updateListeners.delete(listener);
|
|
},
|
|
async reset() {
|
|
state.setupComplete = false;
|
|
storage.set("forgeflow-demo-setup", "false");
|
|
return clone(state);
|
|
},
|
|
};
|
|
}
|