refactor: split renderer ipc and unraid domains
This commit is contained in:
@@ -0,0 +1,660 @@
|
||||
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: 2,
|
||||
adopted: 0,
|
||||
verified: 1,
|
||||
linked: 1,
|
||||
unmatched: 0,
|
||||
needsReview: 1,
|
||||
running: 2,
|
||||
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"],
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
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);
|
||||
return {
|
||||
repository: fullName,
|
||||
checkedAt: iso(),
|
||||
score: 78,
|
||||
grade: "Good",
|
||||
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 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user