Versions: take a version off the Frame (its gallery folder and backup copy, nothing else)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
thunderheronightandClaude Opus 5.5 committed 2026-10-14 14:32:10 +02:00
1 parent 7e64f039d6
commit dfaa6a4a1c
4 files changed
+96

No files matched your search

+37
View File
@@ -44,6 +44,7 @@ STATE = STATE_DIR / "state.json"
FRAME = CFG.frame_host
FRAME_VIDEOS = "Videos/ArcturusVisionCamera"
FRAME_SCENES = ".local/share/Steam/steamapps/common/ArcturusVisionGaussianXR/Scenes"
FRAME_BACKUP = "Documents/GaussianSplats" # to-frame.sh keeps a copy of every scene here
SSH = ["ssh", "-o", "ConnectTimeout=4", "-o", "BatchMode=yes", FRAME]
HOSTNAME = socket.gethostname().lower()
# Names the server may be reached under (protects against DNS rebinding); IP addresses always work
@@ -818,6 +819,40 @@ def start_versions(p: dict):
return None
def remove_from_frame(slug: str, version: str):
"""Take a version off the Frame: its gallery folder and the backup copy. Only a folder this version
got from Splat Workshop (remembered, or named exactly <NN>_<scene>-<version>) – never anything else."""
if not FRAME:
return err("err.no_frame")
d = scene_dir(slug) if slug and not slug.startswith(".") else None
if not d or not version or version.startswith(".") or Path(version).name != version:
return err("err.no_version")
meta = read_meta(slug)
folders = meta.get("version_folders") or {}
folder = folders.get(version)
fr = frame_listing(force=True)
if fr is None:
return err("err.frame_offline")
if not folder:
folder = next((x for x in fr["scenes"] if re.fullmatch(r"\d\d_" + re.escape(f"{slug}-{version}"), x)), None)
if not folder or not re.fullmatch(r"\d\d_[A-Za-z0-9._-]+", folder) or folder not in fr["scenes"]:
return err("err.not_on_frame_version")
if (u := uploads.get(f"{slug}#{version}")) and u["proc"].poll() is None:
return err("err.upload_busy")
try:
r = subprocess.run(SSH + [f"rm -rf -- {FRAME_SCENES}/{shq(folder)} {FRAME_BACKUP}/{shq(folder)}"],
capture_output=True, text=True, timeout=30)
except subprocess.TimeoutExpired:
return err("err.frame_offline")
if r.returncode != 0:
return err("err.frame_failed", detail=(r.stderr.strip().splitlines() or [""])[-1])
folders.pop(version, None)
write_meta(slug, version_folders=folders)
uploads.pop(f"{slug}#{version}", None)
frame_cache["t"] = 0
return None
def delete_version(slug: str, key: str):
d = scene_dir(slug) if slug and not slug.startswith(".") else None
vdir = d / "versions" / Path(key).name if d and key and not key.startswith(".") else None
@@ -1747,6 +1782,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
e = start_upload(p.get("slug", ""), p.get("version", ""))
elif path == "/api/versions":
e = start_versions(p)
elif path == "/api/frame/remove":
e = remove_from_frame(p.get("slug", ""), p.get("version", ""))
elif path == "/api/version/delete":
e = delete_version(p.get("slug", ""), p.get("version", ""))
elif path == "/api/pick":
+43
View File
@@ -132,3 +132,46 @@ def test_one_job_at_a_time():
assert server.start_job({"path": str(rec), "name": "Other", "quality": "fast", "upload": False}) is None
time.sleep(1)
assert server.start_versions({"slug": "Room", "sizes": "10000"})["code"] == "err.job_busy"
def upload(api, slug, key):
assert api.post("/api/upload", {"slug": slug, "version": key})[0] == 200
end = time.time() + 60
while (u := server.upload_status(f"{slug}#{key}")) and u["running"] and time.time() < end:
time.sleep(0.2)
assert server.upload_status(f"{slug}#{key}")["ok"]
def test_taking_a_version_off_the_frame_removes_only_that(api, frame):
room()
make("Room", "10000,20000")
upload(api, "Room", "0.01m")
upload(api, "Room", "0.02m")
scenes, backup = frame / server.FRAME_SCENES, frame / server.FRAME_BACKUP
(scenes / "04_BurtLakeHouse").mkdir() # an example scene of Gaussian XR
(scenes / "30_Room-0.01m-old").mkdir() # someone else's folder with a similar name
assert api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})[0] == 200
assert sorted(p.name for p in scenes.iterdir()) == ["04_BurtLakeHouse", "21_Room-0.02m", "30_Room-0.01m-old"]
assert not (backup / "20_Room-0.01m").exists() and (backup / "21_Room-0.02m").exists()
assert "0.01m" not in json.loads((RUNS / "Room.ui.json").read_text())["version_folders"]
v = {x["key"]: x for x in server.versions_of("Room")}
assert v["0.01m"]["on_frame"] is False and v["0.02m"]["on_frame"] # still on the PC, off the Frame
status, r = api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})
assert status == 400 and r["error"]["code"] == "err.not_on_frame_version"
@pytest.mark.parametrize("version", ["", "..", "../0.02m", ".hidden"])
def test_taking_off_the_frame_only_with_a_proper_version(api, frame, version):
room()
assert api.post("/api/frame/remove", {"slug": "Room", "version": version})[0] == 400
def test_taking_off_a_frame_that_is_off(api, frame):
room()
make("Room", "10000")
upload(api, "Room", "0.01m")
os.environ["FAKE_FRAME_OFF"] = "1"
status, r = api.post("/api/frame/remove", {"slug": "Room", "version": "0.01m"})
assert status == 400 and r["error"]["code"] == "err.frame_offline"
os.environ.pop("FAKE_FRAME_OFF")
assert (frame / server.FRAME_SCENES / "20_Room-0.01m").exists()
+8
View File
@@ -201,6 +201,10 @@ const I18N = {
"ver.confirm": "Move the version “{name}” ({size}) to the trash? A copy on the Frame is not touched.",
"job.sub.versions": "{n} versions · for {time}", "err.no_version": "This version does not exist",
"err.no_versions": "Pick at least one size",
"ver.unframe": "Off the Frame", "ver.unframe.title": "Remove this version from the Frame's gallery (it stays on the PC)",
"ver.unframe.confirm": "Remove “{name}” from the Frame? Its folder in the gallery and the backup copy on the Frame are deleted; the version stays on this PC.",
"ver.unframed": "“{name}” removed from the Frame", "err.not_on_frame_version": "This version is not on the Frame",
"err.frame_offline": "The Frame is not reachable", "err.frame_failed": "The Frame refused",
"err.plan_failed": "The floor plan could not be drawn", "sc.replacing": "Being computed again – stays until the new one is done", "err.plan_busy": "Other floor plans are being drawn right now, try again in a moment",
"plan.measure": "📏 Measure", "plan.measure.title": "Measure distances in the floor plan",
"plan.measure.hint": "Click to set points · right-click removes the last one · horizontal distances, about ±5 cm",
@@ -411,6 +415,10 @@ const I18N = {
"ver.confirm": "Fassung „{name}“ ({size}) in den Papierkorb verschieben? Eine Kopie auf der Frame bleibt unberührt.",
"job.sub.versions": "{n} Fassungen · seit {time}", "err.no_version": "Diese Fassung gibt es nicht",
"err.no_versions": "Mindestens eine Größe wählen",
"ver.unframe": "Von der Frame nehmen", "ver.unframe.title": "Diese Fassung aus der Galerie der Frame entfernen (auf dem PC bleibt sie)",
"ver.unframe.confirm": "„{name}“ von der Frame nehmen? Ihr Ordner in der Galerie und die Sicherungskopie auf der Frame werden gelöscht; auf dem PC bleibt die Fassung.",
"ver.unframed": "„{name}“ von der Frame genommen", "err.not_on_frame_version": "Diese Fassung liegt nicht auf der Frame",
"err.frame_offline": "Die Frame ist nicht erreichbar", "err.frame_failed": "Die Frame hat abgelehnt",
"err.plan_failed": "Der Grundriss konnte nicht gezeichnet werden", "sc.replacing": "Wird neu gerechnet – bleibt, bis die neue fertig ist", "err.plan_busy": "Gerade werden andere Grundrisse gezeichnet, gleich noch einmal versuchen",
"plan.measure": "📏 Messen", "plan.measure.title": "Abstände im Grundriss messen",
"plan.measure.hint": "Klicken setzt Punkte · Rechtsklick nimmt den letzten zurück · waagerechte Abstände, etwa ±5 cm genau",
+8
View File
@@ -1102,6 +1102,7 @@ async function drawVersions(slug) {
<span class="muted" style="font-size:12.5px">${tr("million", {n: num(v.splats / 1e6, 2)})} · ${mb(v.size)}${v.stale ? " · " + tr("ver.stale") : ""}</span>
<span class="sp"></span>${frameState({upload: v.upload, on_frame: v.on_frame})}
${S.frameConfigured ? `<button class="small" data-vup="${esc(v.key)}" ${v.upload && v.upload.running ? "disabled" : ""}>${tr(v.on_frame ? "sc.reupload" : "sc.toFrame")}</button>` : ""}
${v.on_frame ? `<button class="small" data-vunframe="${esc(v.key)}" title="${tr("ver.unframe.title")}">${tr("ver.unframe")}</button>` : ""}
<button class="small" data-vdel="${esc(v.key)}" title="${tr("ver.delete")}" aria-label="${tr("ver.delete")}">${tr("sc.delete")}</button>
</div>`).join("") : `<p class="faint" style="font-size:13px;margin:0">${tr("ver.none")}</p>`;
$("#verList").querySelectorAll("[data-vup]").forEach(b => b.onclick = async () => {
@@ -1109,6 +1110,13 @@ async function drawVersions(slug) {
if (!res.ok) { toast(trErr(res.error)); return; }
toast(tr("sc.uploadStarted")); setTimeout(() => drawVersions(slug), 1500);
});
$("#verList").querySelectorAll("[data-vunframe]").forEach(b => b.onclick = async () => {
const v = list.find(x => x.key === b.dataset.vunframe);
if (!confirm(tr("ver.unframe.confirm", {name: v.label}))) return;
const res = await api("/api/frame/remove", {slug, version: v.key});
if (!res.ok) { toast(trErr(res.error)); return; }
toast(tr("ver.unframed", {name: v.label})); drawVersions(slug);
});
$("#verList").querySelectorAll("[data-vdel]").forEach(b => b.onclick = async () => {
const v = list.find(x => x.key === b.dataset.vdel);
if (!confirm(tr("ver.confirm", {name: v.label, size: mb(v.size)}))) return;