Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.
This commit is contained in:
1 parent
4ddffbbeb1
commit
67a89d1794
45 files changed
+2856
-320
No files matched your search
+152
-13
@@ -5,7 +5,7 @@ import re
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -77,6 +77,9 @@ def create_app(cfg):
|
||||
app.config["MAX_CONTENT_LENGTH"] = 256 * 1024 * 1024
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
# Orca-App-Fenster schließt oft — Session-Cookie muss überleben (30 Tage, gleitend).
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30)
|
||||
app.config["SESSION_REFRESH_EACH_REQUEST"] = True
|
||||
if cfg.get("fqdn", "").startswith("https://"):
|
||||
app.config["SESSION_COOKIE_SECURE"] = True
|
||||
app.json.ensure_ascii = False
|
||||
@@ -421,6 +424,7 @@ def create_app(cfg):
|
||||
grotti_db.invalidate_user_count()
|
||||
store.touch_user_login(conn(), row["id"])
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session["user_id"] = row["id"]
|
||||
session["csrf"] = grotti_auth.new_csrf()
|
||||
store.add_event(conn(), "", "", "auth", "Admin %s angelegt" % username)
|
||||
@@ -442,6 +446,7 @@ def create_app(cfg):
|
||||
raise GrottiError("Benutzer oder Passwort falsch.", "auth", 401)
|
||||
store.touch_user_login(conn(), row["id"])
|
||||
session.clear()
|
||||
session.permanent = True
|
||||
session["user_id"] = row["id"]
|
||||
session["csrf"] = grotti_auth.new_csrf()
|
||||
store.add_event(conn(), "", "", "auth", "Login %s" % row["username"])
|
||||
@@ -633,7 +638,8 @@ def create_app(cfg):
|
||||
|
||||
@app.get("/kameras")
|
||||
def kameras():
|
||||
return page("kameras.html", "Kameras")
|
||||
"""Alte Links → Wand (Kameras sitzen als Snapshot-Thumbs auf den Karten)."""
|
||||
return redirect("/", code=301)
|
||||
|
||||
@app.get("/dateien")
|
||||
def dateien():
|
||||
@@ -641,7 +647,7 @@ def create_app(cfg):
|
||||
|
||||
@app.get("/projekte")
|
||||
def projekte():
|
||||
return redirect("/auftraege", code=301)
|
||||
return page("projekte.html", "Projekte")
|
||||
|
||||
@app.get("/auftraege")
|
||||
def auftraege_page():
|
||||
@@ -1049,7 +1055,27 @@ def create_app(cfg):
|
||||
status = data.get("status") or "idee"
|
||||
if status not in PROJECT_STATUS:
|
||||
raise GrottiError("Unbekannter Status.", "status")
|
||||
return jsonify(store.create_project(conn(), name, (data.get("notes") or "").strip(), status)), 201
|
||||
folder = (data.get("folder") or "").strip()
|
||||
if not folder:
|
||||
folder = library.default_project_folder(name)
|
||||
try:
|
||||
if cfg.get("watch_dir"):
|
||||
folder = library.ensure_project_folder(cfg, folder)
|
||||
else:
|
||||
folder = library.normalize_folder(folder)
|
||||
except GrottiError as exc:
|
||||
if exc.code in ("ordner", "watch"):
|
||||
raise GrottiError(
|
||||
"%s — Ordner relativ unter dem Slice-Pfad, z. B. Projekte/%s (kein D:\\…)."
|
||||
% (exc.message.rstrip("."), library.project_slug(name)),
|
||||
exc.code,
|
||||
getattr(exc, "status", 400) or 400,
|
||||
) from exc
|
||||
raise
|
||||
row = store.create_project(
|
||||
conn(), name, (data.get("notes") or "").strip(), status, folder=folder
|
||||
)
|
||||
return jsonify(row), 201
|
||||
|
||||
@app.get("/api/v1/projekte/<int:project_id>")
|
||||
def project_get(project_id):
|
||||
@@ -1073,6 +1099,13 @@ def create_app(cfg):
|
||||
if data["status"] not in PROJECT_STATUS:
|
||||
raise GrottiError("Unbekannter Status.", "status")
|
||||
fields["status"] = data["status"]
|
||||
if "folder" in data:
|
||||
folder = (data.get("folder") or "").strip()
|
||||
if folder and cfg.get("watch_dir"):
|
||||
folder = library.ensure_project_folder(cfg, folder)
|
||||
elif folder:
|
||||
folder = library.normalize_folder(folder)
|
||||
fields["folder"] = folder
|
||||
row = store.update_project(conn(), project_id, fields)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
@@ -1080,15 +1113,95 @@ def create_app(cfg):
|
||||
|
||||
@app.delete("/api/v1/projekte/<int:project_id>")
|
||||
def project_delete(project_id):
|
||||
row = store.get_project(conn(), project_id)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
# ?force=1 löscht den Projektordner inkl. Inhalt
|
||||
force = (request.args.get("force") or "").strip() in ("1", "true", "yes")
|
||||
folder = (row.get("folder") or "").strip()
|
||||
if folder and cfg.get("watch_dir"):
|
||||
library.delete_project_disk_folder(cfg, folder, force=force)
|
||||
if not store.delete_project(conn(), project_id):
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
return "", 204
|
||||
|
||||
@app.get("/api/v1/projekte/<int:project_id>/assets")
|
||||
def project_assets_list(project_id):
|
||||
row = store.get_project(conn(), project_id)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
folder = (row.get("folder") or "").strip()
|
||||
if not folder:
|
||||
return jsonify([])
|
||||
if not cfg.get("watch_dir"):
|
||||
return jsonify([])
|
||||
return jsonify(library.list_project_assets(cfg, folder))
|
||||
|
||||
@app.post("/api/v1/projekte/<int:project_id>/assets")
|
||||
def project_assets_upload(project_id):
|
||||
row = store.get_project(conn(), project_id)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
folder = (row.get("folder") or "").strip()
|
||||
if not folder:
|
||||
raise GrottiError("Das Projekt hat keinen Ordner.", "ordner")
|
||||
if not cfg.get("watch_dir"):
|
||||
raise GrottiError("Kein Slice-Ordner gesetzt — unter Einstellungen festlegen.", "watch")
|
||||
upload = request.files.get("file")
|
||||
if not upload or not upload.filename:
|
||||
raise GrottiError("Keine Datei angehängt.", "datei")
|
||||
saved = library.save_project_asset(cfg, folder, upload.filename, upload.read())
|
||||
return jsonify(saved), 201
|
||||
|
||||
@app.get("/api/v1/projekte/<int:project_id>/assets/<path:rel>")
|
||||
def project_assets_download(project_id, rel):
|
||||
row = store.get_project(conn(), project_id)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
folder = (row.get("folder") or "").strip()
|
||||
if not folder:
|
||||
raise GrottiError("Das Projekt hat keinen Ordner.", "ordner")
|
||||
path = library.resolve_project_asset(cfg, folder, rel)
|
||||
return send_file(path, as_attachment=True, download_name=path.name)
|
||||
|
||||
@app.delete("/api/v1/projekte/<int:project_id>/assets/<path:rel>")
|
||||
def project_assets_delete(project_id, rel):
|
||||
row = store.get_project(conn(), project_id)
|
||||
if not row:
|
||||
raise GrottiError("Das Projekt fehlt.", "projekt", 404)
|
||||
folder = (row.get("folder") or "").strip()
|
||||
if not folder:
|
||||
raise GrottiError("Das Projekt hat keinen Ordner.", "ordner")
|
||||
library.delete_project_asset(cfg, folder, rel)
|
||||
return "", 204
|
||||
|
||||
@app.get("/api/v1/dateien")
|
||||
def file_list():
|
||||
library.sync_watch(conn(), cfg, force=False)
|
||||
return jsonify(store.list_files(conn(), watch_dir=cfg.get("watch_dir")))
|
||||
|
||||
@app.post("/api/v1/dateien/<int:file_id>/meta")
|
||||
def file_meta_refresh(file_id):
|
||||
"""Eine Datei neu parsen — z. B. paint_index für ACE nach Orca-Deep-Link."""
|
||||
out = library.refresh_one_file(conn(), file_id, watch_dir=cfg.get("watch_dir"))
|
||||
if not out:
|
||||
raise GrottiError("Datei nicht gefunden.", "datei", 404)
|
||||
return jsonify(out)
|
||||
|
||||
@app.get("/api/v1/orca-fokus")
|
||||
def orca_focus():
|
||||
"""Zuletzt von orca_senden gesetzte Datei — offene Tabs schalten um statt neu zu öffnen."""
|
||||
path = Path(cfg["data_dir"]) / "orca-senden-focus.json"
|
||||
if not path.is_file():
|
||||
return jsonify({})
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return jsonify({})
|
||||
if not isinstance(data, dict) or data.get("file_id") is None:
|
||||
return jsonify({})
|
||||
return jsonify(data)
|
||||
|
||||
@app.post("/api/v1/dateien/neu-lesen")
|
||||
def file_rescan():
|
||||
result = library.sync_watch(conn(), cfg, force=True)
|
||||
@@ -1203,13 +1316,7 @@ def create_app(cfg):
|
||||
|
||||
@app.delete("/api/v1/dateien/<int:file_id>")
|
||||
def file_delete(file_id):
|
||||
row = store.get_file_row(conn(), file_id)
|
||||
if not row:
|
||||
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
||||
if row["source"] != "upload":
|
||||
raise GrottiError("Die Datei kommt aus dem Watch-Ordner und bleibt, solange sie dort liegt.", "watch")
|
||||
store.delete_file(conn(), file_id)
|
||||
Path(row["stored_path"]).unlink(missing_ok=True)
|
||||
library.delete_library_file(conn(), cfg, file_id)
|
||||
return "", 204
|
||||
|
||||
@app.post("/api/v1/senden")
|
||||
@@ -1575,6 +1682,17 @@ def create_app(cfg):
|
||||
except ValueError as exc:
|
||||
raise GrottiError(str(exc), "urteil") from exc
|
||||
run_json = store.set_verdict(c, run_row["id"], verdict, reason, ok_n, bad_n)
|
||||
# Live-Lauf schließen: Moonraker-History bleibt oft ohne Endzeit hängen,
|
||||
# sonst bleibt der Druck im Protokoll ewig „in_progress“ trotz Urteil.
|
||||
if run_row and (run_row.get("state") or "") in ("in_progress", "printing", "paused"):
|
||||
closed = store.complete_open_run(c, run_row["id"], state="completed")
|
||||
if closed:
|
||||
try:
|
||||
jobs.apply_deduction(c, closed, cfg["energy"])
|
||||
except Exception as exc:
|
||||
print("Abräumen Abzug %s: %s" % (host, exc), flush=True)
|
||||
row = store.get_run_row(c, closed["id"]) or closed
|
||||
run_json = store.run_out(row)
|
||||
reprint = None
|
||||
if bool(data.get("reprint")) and run_row and run_row.get("file_id"):
|
||||
reprint = store.enqueue_print(
|
||||
@@ -2098,11 +2216,27 @@ def create_app(cfg):
|
||||
|
||||
@app.patch("/api/v1/spulen/<int:spool_id>")
|
||||
def spool_patch(spool_id):
|
||||
row = store.update_spool(conn(), spool_id, spool_fields(body(), partial=True, spool_id=spool_id))
|
||||
data = body()
|
||||
fields = spool_fields(data, partial=True, spool_id=spool_id)
|
||||
if "archived" in data or "archived_at" in data:
|
||||
if data.get("archived") is False or data.get("archived_at") is None:
|
||||
fields["archived_at"] = None
|
||||
elif data.get("archived") is True or data.get("archived_at") in (True, "now"):
|
||||
fields["archived_at"] = "now"
|
||||
row = store.update_spool(conn(), spool_id, fields)
|
||||
if not row:
|
||||
raise GrottiError("Die Spule fehlt.", "spule", 404)
|
||||
return jsonify(row)
|
||||
|
||||
@app.post("/api/v1/spulen/<int:spool_id>/leer")
|
||||
def spool_empty(spool_id):
|
||||
"""Rest 0, Regal, archivieren — wenn der Rest falsch geschätzt war."""
|
||||
row = store.mark_spool_empty(conn(), spool_id)
|
||||
if not row:
|
||||
raise GrottiError("Die Spule fehlt.", "spule", 404)
|
||||
store.add_event(conn(), "", "", "spule", "Spule „%s“ als leer markiert" % row["name"])
|
||||
return jsonify(row)
|
||||
|
||||
@app.post("/api/v1/spulen/<int:spool_id>/einlegen")
|
||||
def spool_load(spool_id):
|
||||
"""Spule an Drucker/Platz legen; vorherige Rolle dort geht ins Regal."""
|
||||
@@ -2112,7 +2246,8 @@ def create_app(cfg):
|
||||
spool = store.get_spool_row(conn(), spool_id)
|
||||
if not spool:
|
||||
raise GrottiError("Die Spule fehlt.", "spule", 404)
|
||||
live_run = store.find_live_run(conn(), host)
|
||||
# Auch offene History-Jobs (Pause/Resume nach Moonraker-Split), nicht nur live:
|
||||
live_run = store.find_active_run(conn(), host)
|
||||
mm_now = float((live_run or {}).get("filament_used_mm") or 0)
|
||||
printing = bool(
|
||||
live_run and (live_run.get("state") or "") in ("in_progress", "printing", "paused")
|
||||
@@ -2161,6 +2296,8 @@ def create_app(cfg):
|
||||
updated = store.update_spool(conn(), spool_id, {"printer_host": host, "ace_slot": slot})
|
||||
if printing:
|
||||
store.open_segment(conn(), live_run["id"], spool_id, slot, mm_now)
|
||||
store.assign_run_spool(conn(), live_run["id"], spool_id)
|
||||
store.update_send_spool(conn(), host, live_run.get("filename") or "", spool_id)
|
||||
store.add_event(
|
||||
conn(),
|
||||
host,
|
||||
@@ -2462,6 +2599,7 @@ def create_app(cfg):
|
||||
host = (request.args.get("host") or "").strip()
|
||||
material = (request.args.get("material") or "").strip()
|
||||
brand = (request.args.get("brand") or "").strip()
|
||||
color = (request.args.get("color") or "").strip()
|
||||
project_id = optional_id(request.args.get("project_id"))
|
||||
return jsonify(store.statistics(
|
||||
conn(),
|
||||
@@ -2470,6 +2608,7 @@ def create_app(cfg):
|
||||
host=host,
|
||||
material=material or "",
|
||||
brand=brand or "",
|
||||
color=color or "",
|
||||
project_id=project_id,
|
||||
))
|
||||
|
||||
|
||||
Reference in new issue
Block a user