Add git version control panel and LAN hosting support
- Git modal (⎇ Git button): commit all diagrams as JSON to diagrams/ folder,
push to remote, view history, restore any diagram from any past commit
- Diagrams exported to diagrams/{id}_{name}.json on each commit so diffs are
human-readable and individual diagrams can be restored independently
- backend/run.py: changed host to 0.0.0.0 for LAN access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy import text, inspect as sa_inspect
|
||||
|
||||
from .database import Base, engine, SessionLocal
|
||||
from .routers import bundles, connectors, diagrams, devices, export, octopart, views, wires
|
||||
from .routers import bundles, connectors, diagrams, devices, export, git_mgr, octopart, views, wires
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -70,6 +70,7 @@ app.include_router(views.router, prefix="/api/views")
|
||||
app.include_router(octopart.router, prefix="/api/octopart")
|
||||
app.include_router(export.router, prefix="/api/export")
|
||||
app.include_router(connectors.router, prefix="/api/connectors")
|
||||
app.include_router(git_mgr.router, prefix="/api/git")
|
||||
|
||||
FRONTEND_DIR = os.path.normpath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "frontend")
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import models
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(tags=["git"])
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def _run(*args, cwd: Path) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
capture_output=True, text=True, cwd=str(cwd), timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
r = _run("rev-parse", "--show-toplevel", cwd=backend_dir)
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(500, "Not in a git repository")
|
||||
return Path(r.stdout.strip())
|
||||
|
||||
|
||||
def _diagrams_dir(repo_root: Path) -> Path:
|
||||
d = repo_root / "diagrams"
|
||||
d.mkdir(exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _safe_name(diagram_id: int, name: str) -> str:
|
||||
safe = re.sub(r"[^\w\-]", "_", name)
|
||||
return f"{diagram_id:04d}_{safe}.json"
|
||||
|
||||
|
||||
def _diagram_to_dict(diagram, db: Session) -> dict:
|
||||
return {
|
||||
"id": diagram.id,
|
||||
"name": diagram.name,
|
||||
"description": diagram.description or "",
|
||||
"devices": [
|
||||
{
|
||||
"id": d.id, "device_type": d.device_type, "label": d.label,
|
||||
"reference": d.reference, "x": d.x, "y": d.y,
|
||||
"width": d.width, "height": d.height,
|
||||
"properties": d.properties, "pins": d.pins,
|
||||
}
|
||||
for d in diagram.devices
|
||||
],
|
||||
"wires": [
|
||||
{
|
||||
"id": w.id, "label": w.label,
|
||||
"from_device_id": w.from_device_id, "from_pin": w.from_pin,
|
||||
"to_device_id": w.to_device_id, "to_pin": w.to_pin,
|
||||
"color_primary": w.color_primary, "color_stripe": w.color_stripe,
|
||||
"gauge": w.gauge, "length": w.length, "length_unit": w.length_unit,
|
||||
"notes": w.notes, "waypoints": w.waypoints or [],
|
||||
"twisted_pair": bool(w.twisted_pair), "twist_pitch": w.twist_pitch,
|
||||
"shielded": bool(w.shielded), "show_size_label": bool(w.show_size_label),
|
||||
}
|
||||
for w in diagram.wires
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _import_data(data: dict, db: Session, name_override: Optional[str] = None) -> models.Diagram:
|
||||
new_diag = models.Diagram(
|
||||
name=name_override or data.get("name", "Restored Diagram"),
|
||||
description=data.get("description", ""),
|
||||
)
|
||||
db.add(new_diag)
|
||||
db.flush()
|
||||
id_map = {}
|
||||
for dev in data.get("devices", []):
|
||||
nd = models.Device(
|
||||
diagram_id=new_diag.id,
|
||||
device_type=dev.get("device_type", "generic"),
|
||||
label=dev.get("label", ""),
|
||||
reference=dev.get("reference", ""),
|
||||
x=dev.get("x", 100), y=dev.get("y", 100),
|
||||
width=dev.get("width", 120), height=dev.get("height", 60),
|
||||
properties=dev.get("properties", {}),
|
||||
pins=dev.get("pins", []),
|
||||
)
|
||||
db.add(nd)
|
||||
db.flush()
|
||||
if "id" in dev:
|
||||
id_map[dev["id"]] = nd.id
|
||||
for w in data.get("wires", []):
|
||||
nw = models.Wire(
|
||||
diagram_id=new_diag.id,
|
||||
label=w.get("label", ""),
|
||||
from_device_id=id_map.get(w.get("from_device_id")),
|
||||
from_pin=w.get("from_pin", ""),
|
||||
to_device_id=id_map.get(w.get("to_device_id")),
|
||||
to_pin=w.get("to_pin", ""),
|
||||
color_primary=w.get("color_primary", "#FF0000"),
|
||||
color_stripe=w.get("color_stripe"),
|
||||
gauge=w.get("gauge", "18 AWG"),
|
||||
length=w.get("length"),
|
||||
length_unit=w.get("length_unit", "in"),
|
||||
notes=w.get("notes", ""),
|
||||
waypoints=w.get("waypoints", []),
|
||||
twisted_pair=w.get("twisted_pair", False),
|
||||
twist_pitch=w.get("twist_pitch", 16.0),
|
||||
shielded=w.get("shielded", False),
|
||||
show_size_label=w.get("show_size_label", False),
|
||||
)
|
||||
db.add(nw)
|
||||
db.commit()
|
||||
db.refresh(new_diag)
|
||||
return new_diag
|
||||
|
||||
|
||||
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/status")
|
||||
def git_status():
|
||||
repo_root = _repo_root()
|
||||
|
||||
branch_r = _run("rev-parse", "--abbrev-ref", "HEAD", cwd=repo_root)
|
||||
branch = branch_r.stdout.strip() if branch_r.returncode == 0 else "unknown"
|
||||
|
||||
status_r = _run("status", "--porcelain", cwd=repo_root)
|
||||
changed = [l for l in status_r.stdout.splitlines() if l.strip()]
|
||||
|
||||
ahead_r = _run("rev-list", "--count", "@{u}..HEAD", cwd=repo_root)
|
||||
ahead = int(ahead_r.stdout.strip()) if ahead_r.returncode == 0 and ahead_r.stdout.strip().isdigit() else 0
|
||||
|
||||
return {
|
||||
"branch": branch,
|
||||
"is_clean": len(changed) == 0,
|
||||
"changed_count": len(changed),
|
||||
"commits_ahead": ahead,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/log")
|
||||
def git_log():
|
||||
repo_root = _repo_root()
|
||||
r = _run("log", "--pretty=format:%H|%h|%s|%an|%ai", "-30", cwd=repo_root)
|
||||
if r.returncode != 0:
|
||||
return []
|
||||
entries = []
|
||||
for line in r.stdout.splitlines():
|
||||
parts = line.split("|", 4)
|
||||
if len(parts) == 5:
|
||||
entries.append({
|
||||
"hash": parts[0], "short": parts[1],
|
||||
"message": parts[2], "author": parts[3], "date": parts[4],
|
||||
})
|
||||
return entries
|
||||
|
||||
|
||||
class CommitBody(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
@router.post("/commit")
|
||||
def git_commit(body: CommitBody, db: Session = Depends(get_db)):
|
||||
if not body.message.strip():
|
||||
raise HTTPException(400, "Commit message required")
|
||||
repo_root = _repo_root()
|
||||
diagrams_dir = _diagrams_dir(repo_root)
|
||||
|
||||
# Export all diagrams to diagrams/ folder
|
||||
diagrams = db.query(models.Diagram).all()
|
||||
for diagram in diagrams:
|
||||
data = _diagram_to_dict(diagram, db)
|
||||
fname = _safe_name(diagram.id, diagram.name)
|
||||
(diagrams_dir / fname).write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
add_r = _run("add", "diagrams/", cwd=repo_root)
|
||||
if add_r.returncode != 0:
|
||||
raise HTTPException(500, f"git add failed: {add_r.stderr}")
|
||||
|
||||
commit_r = _run("commit", "-m", body.message.strip(), cwd=repo_root)
|
||||
if commit_r.returncode != 0:
|
||||
out = commit_r.stdout + commit_r.stderr
|
||||
if "nothing to commit" in out:
|
||||
return {"status": "nothing_to_commit", "output": "Nothing to commit — diagrams unchanged since last commit"}
|
||||
raise HTTPException(500, f"git commit failed: {commit_r.stderr}")
|
||||
|
||||
return {"status": "committed", "output": commit_r.stdout.strip()}
|
||||
|
||||
|
||||
@router.post("/push")
|
||||
def git_push():
|
||||
repo_root = _repo_root()
|
||||
r = _run("push", cwd=repo_root)
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(500, f"git push failed: {r.stderr.strip()}")
|
||||
return {"status": "pushed", "output": (r.stdout + r.stderr).strip()}
|
||||
|
||||
|
||||
@router.get("/history/{commit_hash}")
|
||||
def git_history_files(commit_hash: str):
|
||||
repo_root = _repo_root()
|
||||
r = _run("ls-tree", "--name-only", commit_hash, "diagrams/", cwd=repo_root)
|
||||
if r.returncode != 0:
|
||||
return {"files": []}
|
||||
files = [f for f in r.stdout.splitlines() if f.endswith(".json")]
|
||||
return {"files": files}
|
||||
|
||||
|
||||
class RestoreBody(BaseModel):
|
||||
commit_hash: str
|
||||
filepath: str # e.g. "diagrams/0001_My_Diagram.json"
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/restore")
|
||||
def git_restore(body: RestoreBody, db: Session = Depends(get_db)):
|
||||
repo_root = _repo_root()
|
||||
r = _run("show", f"{body.commit_hash}:{body.filepath}", cwd=repo_root)
|
||||
if r.returncode != 0:
|
||||
raise HTTPException(404, f"File not found in that commit")
|
||||
|
||||
try:
|
||||
data = json.loads(r.stdout)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(500, "Invalid JSON in git history")
|
||||
|
||||
short_hash = body.commit_hash[:7]
|
||||
name = body.name or f"{data.get('name', 'Diagram')} (restored {short_hash})"
|
||||
new_diag = _import_data(data, db, name_override=name)
|
||||
return {"id": new_diag.id, "name": new_diag.name}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=True)
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
@@ -304,6 +304,35 @@ button:active { background: #1a1a3a; }
|
||||
.drc-row:last-child { border-bottom: none; }
|
||||
.drc-none { color: #444466; font-style: italic; }
|
||||
|
||||
/* ── Git modal ────────────────────────────────────────────────────────────── */
|
||||
.git-badge {
|
||||
font-size: 11px; padding: 2px 8px; border-radius: 10px;
|
||||
background: #1a1a30; color: #8899bb; border: 1px solid #2a2a44;
|
||||
}
|
||||
.git-badge.git-clean { color: #88cc88; border-color: #336633; }
|
||||
.git-badge.git-dirty { color: #ddaa44; border-color: #554422; }
|
||||
.git-section { display: flex; flex-direction: column; gap: 5px; }
|
||||
.git-section-title {
|
||||
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: .06em; color: #555577;
|
||||
}
|
||||
.git-log-row {
|
||||
display: grid; grid-template-columns: 52px 1fr 80px auto;
|
||||
align-items: center; gap: 8px;
|
||||
padding: 4px 6px; border-radius: 3px; background: #0d0d20;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.git-log-row:hover { border-color: #2a2a44; }
|
||||
.git-hash { font-family: monospace; color: #6688cc; font-size: 11px; }
|
||||
.git-msg { color: #bbc; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.git-date { color: #445; font-size: 10px; text-align: right; }
|
||||
.git-restore-btn {
|
||||
font-size: 10px; padding: 2px 6px;
|
||||
background: #1a2840; border: 1px solid #334466; border-radius: 3px;
|
||||
color: #88aadd; cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.git-restore-btn:hover { background: #223355; color: #aaccff; }
|
||||
|
||||
.cm-label {
|
||||
display: block; font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: .06em; color: #555577; margin-bottom: 4px;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<button id="btn-git" title="Git version control">⎇ Git</button>
|
||||
<button id="btn-drc" title="Run design-rule check">⚠ DRC</button>
|
||||
<div id="saved-indicator">✓ Saved</div>
|
||||
<div class="export-wrap">
|
||||
@@ -419,6 +420,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="git-modal" style="display:none">
|
||||
<div class="modal-box" style="min-width:520px;max-width:700px">
|
||||
<div class="modal-header">
|
||||
<h3>⎇ Git Version Control</h3>
|
||||
<span id="git-status-badge" class="git-badge"></span>
|
||||
</div>
|
||||
<div class="modal-body" style="padding:12px;gap:10px;display:flex;flex-direction:column">
|
||||
|
||||
<!-- Commit section -->
|
||||
<div class="git-section">
|
||||
<label class="git-section-title">Commit all diagrams</label>
|
||||
<div style="display:flex;gap:6px;align-items:flex-start">
|
||||
<textarea id="git-commit-msg" placeholder="Describe what changed…" rows="2"
|
||||
style="flex:1;resize:vertical;font-size:12px;padding:5px;background:#0d0d20;color:#ccd;border:1px solid #2a2a44;border-radius:4px"></textarea>
|
||||
<div style="display:flex;flex-direction:column;gap:4px">
|
||||
<button id="btn-git-commit" class="btn-primary" style="white-space:nowrap">⊕ Commit</button>
|
||||
<button id="btn-git-push" style="white-space:nowrap">⇧ Push</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="git-op-status" style="font-size:11px;color:#88cc88;min-height:16px"></div>
|
||||
</div>
|
||||
|
||||
<!-- History section -->
|
||||
<div class="git-section" style="flex:1">
|
||||
<label class="git-section-title">Commit history</label>
|
||||
<div id="git-log-list" style="max-height:320px;overflow-y:auto;font-size:11px;display:flex;flex-direction:column;gap:1px"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div style="flex:1"></div>
|
||||
<button id="git-close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/deviceTypes.js"></script>
|
||||
|
||||
@@ -60,4 +60,12 @@ const api = {
|
||||
jsonUrl: (id) => `${API_BASE}/export/${id}/json`,
|
||||
formboardUrl: (id) => `${API_BASE}/export/${id}/formboard`,
|
||||
},
|
||||
git: {
|
||||
status: () => apiFetch("/git/status"),
|
||||
log: () => apiFetch("/git/log"),
|
||||
commit: (message) => apiFetch("/git/commit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }) }),
|
||||
push: () => apiFetch("/git/push", { method: "POST" }),
|
||||
history: (hash) => apiFetch(`/git/history/${hash}`),
|
||||
restore: (commit_hash, filepath, name) => apiFetch("/git/restore", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ commit_hash, filepath, name }) }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -455,6 +455,7 @@ class WiringApp {
|
||||
on("btn-json", () => this._export("json"));
|
||||
on("btn-img", () => { const n = document.getElementById("diagram-name")?.value || "diagram"; this.canvas.exportImage(n); });
|
||||
on("btn-formboard", () => this._export("formboard"));
|
||||
on("btn-git", () => this._openGitModal());
|
||||
on("btn-drc", () => this._runDrc());
|
||||
on("btn-import", () => document.getElementById("import-file-input")?.click());
|
||||
document.getElementById("import-file-input")?.addEventListener("change", (e) => {
|
||||
@@ -2001,6 +2002,143 @@ class WiringApp {
|
||||
}
|
||||
this.openConnectorModal(connData);
|
||||
}
|
||||
|
||||
// ── Git version control ───────────────────────────────────────────────────
|
||||
|
||||
async _openGitModal() {
|
||||
const modal = document.getElementById("git-modal");
|
||||
if (!modal) return;
|
||||
modal.style.display = "flex";
|
||||
|
||||
// Wire close button once
|
||||
const closeBtn = document.getElementById("git-close");
|
||||
if (!closeBtn._gitBound) {
|
||||
closeBtn._gitBound = true;
|
||||
closeBtn.addEventListener("click", () => { modal.style.display = "none"; });
|
||||
modal.addEventListener("click", (e) => { if (e.target === modal) modal.style.display = "none"; });
|
||||
}
|
||||
|
||||
// Wire action buttons once
|
||||
const commitBtn = document.getElementById("btn-git-commit");
|
||||
if (!commitBtn._gitBound) {
|
||||
commitBtn._gitBound = true;
|
||||
commitBtn.addEventListener("click", () => this._gitCommit());
|
||||
document.getElementById("btn-git-push").addEventListener("click", () => this._gitPush());
|
||||
}
|
||||
|
||||
await this._gitRefresh();
|
||||
}
|
||||
|
||||
async _gitRefresh() {
|
||||
await Promise.all([this._gitLoadStatus(), this._gitLoadLog()]);
|
||||
}
|
||||
|
||||
async _gitLoadStatus() {
|
||||
const badge = document.getElementById("git-status-badge");
|
||||
if (!badge) return;
|
||||
try {
|
||||
const s = await api.git.status();
|
||||
const ahead = s.commits_ahead > 0 ? ` · ${s.commits_ahead} ahead` : "";
|
||||
const dirty = s.is_clean ? "clean" : `${s.changed_count} changed`;
|
||||
badge.textContent = `${s.branch} ${dirty}${ahead}`;
|
||||
badge.className = "git-badge " + (s.is_clean ? "git-clean" : "git-dirty");
|
||||
} catch {
|
||||
badge.textContent = "git unavailable";
|
||||
badge.className = "git-badge";
|
||||
}
|
||||
}
|
||||
|
||||
async _gitLoadLog() {
|
||||
const list = document.getElementById("git-log-list");
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div style="color:#556;padding:4px">Loading…</div>';
|
||||
try {
|
||||
const entries = await api.git.log();
|
||||
if (!entries.length) {
|
||||
list.innerHTML = '<div style="color:#556;padding:4px">No commits yet</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = "";
|
||||
entries.forEach(e => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "git-log-row";
|
||||
row.innerHTML = `
|
||||
<span class="git-hash">${e.short}</span>
|
||||
<span class="git-msg">${this._esc(e.message)}</span>
|
||||
<span class="git-date">${e.date.slice(0, 10)}</span>
|
||||
<button class="git-restore-btn" data-hash="${e.hash}" title="Restore a diagram from this commit">↩ Restore</button>
|
||||
`;
|
||||
row.querySelector(".git-restore-btn").addEventListener("click", () => this._gitRestoreFlow(e));
|
||||
list.appendChild(row);
|
||||
});
|
||||
} catch {
|
||||
list.innerHTML = '<div style="color:#e06c6c;padding:4px">Failed to load log</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async _gitCommit() {
|
||||
const msg = document.getElementById("git-commit-msg")?.value?.trim();
|
||||
if (!msg) { alert("Enter a commit message first."); return; }
|
||||
const status = document.getElementById("git-op-status");
|
||||
status.textContent = "Committing…"; status.style.color = "#aabb88";
|
||||
try {
|
||||
const r = await api.git.commit(msg);
|
||||
if (r.status === "nothing_to_commit") {
|
||||
status.textContent = "Nothing to commit — diagrams unchanged.";
|
||||
status.style.color = "#8899bb";
|
||||
} else {
|
||||
status.textContent = "✓ Committed: " + r.output.split("\n")[0];
|
||||
status.style.color = "#88cc88";
|
||||
document.getElementById("git-commit-msg").value = "";
|
||||
await this._gitRefresh();
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = "✗ " + (e.message || "Commit failed");
|
||||
status.style.color = "#e06c6c";
|
||||
}
|
||||
}
|
||||
|
||||
async _gitPush() {
|
||||
const status = document.getElementById("git-op-status");
|
||||
status.textContent = "Pushing…"; status.style.color = "#aabb88";
|
||||
try {
|
||||
const r = await api.git.push();
|
||||
status.textContent = "✓ Pushed. " + (r.output || "").split("\n")[0];
|
||||
status.style.color = "#88cc88";
|
||||
await this._gitLoadStatus();
|
||||
} catch (e) {
|
||||
status.textContent = "✗ Push failed: " + (e.message || "unknown error");
|
||||
status.style.color = "#e06c6c";
|
||||
}
|
||||
}
|
||||
|
||||
async _gitRestoreFlow(entry) {
|
||||
const status = document.getElementById("git-op-status");
|
||||
status.textContent = `Loading files from ${entry.short}…`; status.style.color = "#aabb88";
|
||||
try {
|
||||
const { files } = await api.git.history(entry.hash);
|
||||
if (!files.length) { status.textContent = "No diagrams in that commit."; status.style.color = "#8899bb"; return; }
|
||||
|
||||
// Build a small picker inline
|
||||
const pick = files.map((f, i) => `${i + 1}. ${f.replace("diagrams/", "").replace(/\.json$/, "").replace(/^\d{4}_/, "").replace(/_/g, " ")}`).join("\n");
|
||||
const choice = prompt(`Diagrams in commit ${entry.short} — "${entry.message}"\n\nEnter number to restore:\n${pick}`);
|
||||
if (!choice) { status.textContent = ""; return; }
|
||||
const idx = parseInt(choice) - 1;
|
||||
if (isNaN(idx) || idx < 0 || idx >= files.length) { status.textContent = "Invalid selection."; return; }
|
||||
|
||||
const filepath = files[idx];
|
||||
status.textContent = `Restoring ${filepath}…`; status.style.color = "#aabb88";
|
||||
const r = await api.git.restore(entry.hash, filepath);
|
||||
status.textContent = `✓ Restored as "${r.name}" (id ${r.id})`;
|
||||
status.style.color = "#88cc88";
|
||||
await this.loadDiagramList();
|
||||
} catch (e) {
|
||||
status.textContent = "✗ Restore failed: " + (e.message || "unknown");
|
||||
status.style.color = "#e06c6c";
|
||||
}
|
||||
}
|
||||
|
||||
_esc(s) { return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => { window.app = new WiringApp(); });
|
||||
|
||||
Reference in New Issue
Block a user