Grotti 3.6.0: Lauf-Identität nach Filament-Pause, robustere Queue, Projekte/Aufträge list-first und API-Polish.
This commit is contained in:
1 parent
d1eb87ba25
commit
2b1e36634a
53 files changed
+4254
-636
No files matched your search
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Alt-Paare und verwaiste Live-Läufe finden. Standard: Probelauf.
|
||||
|
||||
python scripts/laeufe_reparieren.py
|
||||
python scripts/laeufe_reparieren.py --apply
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from grotti import jobs, store # noqa: E402
|
||||
from grotti.config import load # noqa: E402
|
||||
from grotti.db import connect, prepare # noqa: E402
|
||||
|
||||
|
||||
def find_pairs(conn):
|
||||
return conn.execute(
|
||||
"""
|
||||
SELECT a.id AS pred_id, b.id AS succ_id,
|
||||
a.printer_host, a.filename,
|
||||
a.state AS pred_state, b.state AS succ_state,
|
||||
a.duration_s AS pred_dur, b.duration_s AS succ_dur,
|
||||
a.ended_at, b.started_at,
|
||||
a.deducted AS pred_deducted, b.deducted AS succ_deducted,
|
||||
a.chargen_nr AS pred_chargen, b.chargen_nr AS succ_chargen
|
||||
FROM runs a
|
||||
JOIN runs b
|
||||
ON a.printer_host = b.printer_host
|
||||
AND a.filename = b.filename
|
||||
AND a.id <> b.id
|
||||
AND b.started_at BETWEEN a.ended_at - interval '30 seconds'
|
||||
AND a.ended_at + interval '5 seconds'
|
||||
WHERE a.state IN ('cancelled', 'interrupted', 'error')
|
||||
AND a.ended_at IS NOT NULL
|
||||
AND a.moonraker_job_id NOT LIKE 'live:%%'
|
||||
AND b.moonraker_job_id NOT LIKE 'live:%%'
|
||||
AND COALESCE(b.duration_s, 0) > 60
|
||||
AND COALESCE(b.duration_s, 0) >= COALESCE(a.duration_s, 0) - 30
|
||||
ORDER BY a.ended_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
|
||||
def find_orphan_live(conn):
|
||||
return conn.execute(
|
||||
"""
|
||||
SELECT id, printer_host, filename, state, started_at, ended_at, duration_s, chargen_nr
|
||||
FROM runs
|
||||
WHERE moonraker_job_id LIKE 'live:%%'
|
||||
AND state IN ('interrupted', 'completed', 'cancelled')
|
||||
ORDER BY COALESCE(ended_at, started_at) DESC
|
||||
LIMIT 100
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Läufe nach Filament-Resume reparieren")
|
||||
parser.add_argument("--apply", action="store_true", help="Wirklich zusammenlegen")
|
||||
args = parser.parse_args()
|
||||
cfg = load()
|
||||
prepare(cfg["database_url"])
|
||||
with connect(cfg["database_url"]) as conn:
|
||||
pairs = find_pairs(conn)
|
||||
orphans = find_orphan_live(conn)
|
||||
print("=== Verdachtspaare (Beweis: Dauer fortgesetzt, Lücke ≤ 5 s) ===")
|
||||
for row in pairs:
|
||||
print(json.dumps({k: str(v) for k, v in dict(row).items()}, ensure_ascii=False))
|
||||
print("%d Paare" % len(pairs))
|
||||
print("=== Verwaiste live:-Läufe ===")
|
||||
for row in orphans:
|
||||
print(json.dumps({k: str(v) for k, v in dict(row).items()}, ensure_ascii=False))
|
||||
print("%d orphan" % len(orphans))
|
||||
if not args.apply:
|
||||
print("Probelauf. Mit --apply werden Paare verbunden.")
|
||||
conn.rollback()
|
||||
return
|
||||
merged = 0
|
||||
for row in pairs:
|
||||
try:
|
||||
jobs.manual_merge_runs(
|
||||
conn, row["pred_id"], row["succ_id"], confirm=True
|
||||
)
|
||||
merged += 1
|
||||
print("verbunden", row["pred_id"], "+", row["succ_id"])
|
||||
except Exception as exc:
|
||||
print("Fehler", row["pred_id"], row["succ_id"], exc)
|
||||
conn.rollback()
|
||||
# Orphans nur an History derselben Datei hängen (kein Host-Fallback).
|
||||
# adopt_live_run nimmt geschlossene live: nur ≤ 2 h — ältere bleiben liegen.
|
||||
cleaned = 0
|
||||
skipped = 0
|
||||
for row in orphans:
|
||||
fname = (row.get("filename") or "").strip()
|
||||
if not fname:
|
||||
skipped += 1
|
||||
continue
|
||||
hist = conn.execute(
|
||||
"""
|
||||
SELECT * FROM runs
|
||||
WHERE printer_host = %s
|
||||
AND filename = %s
|
||||
AND moonraker_job_id NOT LIKE 'live:%%'
|
||||
AND state IN ('in_progress', 'printing', 'paused')
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(row["printer_host"], fname),
|
||||
).fetchone()
|
||||
if not hist:
|
||||
skipped += 1
|
||||
continue
|
||||
store.adopt_live_run(conn, row["printer_host"], fname, hist["id"])
|
||||
still = conn.execute(
|
||||
"SELECT 1 FROM runs WHERE id = %s", (row["id"],)
|
||||
).fetchone()
|
||||
if still:
|
||||
skipped += 1
|
||||
else:
|
||||
cleaned += 1
|
||||
conn.commit()
|
||||
print(
|
||||
"verbunden=%d orphan_adopted=%d orphan_skipped=%d"
|
||||
% (merged, cleaned, skipped)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Absolute Windows-Pfade in files.stored_path und projects.folder auf Linux umschreiben.
|
||||
|
||||
Beispiel (Dry-Run):
|
||||
python scripts/pfade_umziehen.py \\
|
||||
--database-url postgresql:///grotti?host=/var/run/postgresql \\
|
||||
--from "D:/Projekte/3DP/Slice" --to /var/lib/grotti/slice
|
||||
|
||||
Ohne --apply nur Anzeige. Mit --apply werden die Zeilen geschrieben.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
def normalize_slash(path: str) -> str:
|
||||
return (path or "").replace("\\", "/").rstrip("/")
|
||||
|
||||
|
||||
def rewrite(value: str, old: str, new: str) -> str | None:
|
||||
if not value:
|
||||
return None
|
||||
cur = value.replace("\\", "/")
|
||||
old_n = normalize_slash(old)
|
||||
new_n = normalize_slash(new)
|
||||
# case-insensitive für Windows-Laufwerke
|
||||
if cur.lower().startswith(old_n.lower()):
|
||||
rest = cur[len(old_n) :]
|
||||
if rest and not rest.startswith("/"):
|
||||
rest = "/" + rest
|
||||
return new_n + rest
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Slice-Pfade Windows → Linux umschreiben")
|
||||
parser.add_argument("--database-url", required=True)
|
||||
parser.add_argument("--from", dest="old", required=True, help="Alter Prefix (Windows)")
|
||||
parser.add_argument("--to", dest="new", required=True, help="Neuer Prefix (Linux)")
|
||||
parser.add_argument("--apply", action="store_true", help="Wirklich schreiben")
|
||||
args = parser.parse_args()
|
||||
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
old = args.old
|
||||
new = args.new
|
||||
changed_files = 0
|
||||
changed_projects = 0
|
||||
|
||||
with psycopg.connect(args.database_url, row_factory=dict_row) as conn:
|
||||
files = conn.execute("SELECT id, stored_path FROM files ORDER BY id").fetchall()
|
||||
for row in files:
|
||||
nxt = rewrite(row["stored_path"] or "", old, new)
|
||||
if nxt is None or nxt == row["stored_path"]:
|
||||
continue
|
||||
changed_files += 1
|
||||
print("file %s: %s → %s" % (row["id"], row["stored_path"], nxt))
|
||||
if args.apply:
|
||||
conn.execute("UPDATE files SET stored_path = %s WHERE id = %s", (nxt, row["id"]))
|
||||
|
||||
projects = conn.execute("SELECT id, folder FROM projects ORDER BY id").fetchall()
|
||||
for row in projects:
|
||||
folder = row["folder"] or ""
|
||||
# folder ist relativ — nur absolute Reste bereinigen
|
||||
nxt = rewrite(folder, old, new)
|
||||
if nxt is None:
|
||||
continue
|
||||
# relativ zum neuen Slice-Root speichern, falls möglich
|
||||
new_n = normalize_slash(new)
|
||||
if nxt.lower().startswith(new_n.lower()):
|
||||
rel = nxt[len(new_n) :].lstrip("/")
|
||||
nxt = rel
|
||||
changed_projects += 1
|
||||
print("project %s: %s → %s" % (row["id"], folder, nxt))
|
||||
if args.apply:
|
||||
conn.execute("UPDATE projects SET folder = %s WHERE id = %s", (nxt, row["id"]))
|
||||
|
||||
if args.apply:
|
||||
conn.commit()
|
||||
print("Geschrieben: %s Dateien, %s Projekte." % (changed_files, changed_projects))
|
||||
else:
|
||||
print(
|
||||
"Dry-Run: %s Dateien, %s Projekte würden sich ändern. Mit --apply ausführen."
|
||||
% (changed_files, changed_projects)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user