mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 14:26:42 +10:00
Complete rewrite of the editor frontend using GrapesJS: - Full drag/drop visual editor with style manager - Custom HMI blocks: Temperature, Motor, Output, Burner, Automation, Gauge - Layout blocks: Page Container, Tab Bar, Top Bar, Label, Divider, Spacer - Style panel: font family/size/weight/color, background, border, layout, effects - Traits panel: active/inactive colors, animation, linked control, setpoints - Device manager: Tab5 (1280x720), Schneider HMIDT651 (1280x800), Desktop - Theme switcher: Dark Industrial, Light Industrial, High Contrast, Classic SCADA - Layers panel with z-index reorder and show/hide - Undo/redo, preview mode, canvas zoom - V1 layout preserved (saved as current.json), V2 uses current_v2.json - Login/users/comments backend unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
"""BFA Banana Dryer — HMI Design Collaboration Tool"""
|
|
import os
|
|
import json
|
|
import time
|
|
import uuid
|
|
from flask import Flask, render_template, request, jsonify, redirect, make_response
|
|
|
|
app = Flask(__name__)
|
|
LAYOUT_DIR = os.path.join(os.path.dirname(__file__), "layouts")
|
|
PHOTO_DIR = os.path.join(os.path.dirname(__file__), "static", "photos")
|
|
os.makedirs(LAYOUT_DIR, exist_ok=True)
|
|
os.makedirs(PHOTO_DIR, exist_ok=True)
|
|
|
|
CURRENT_LAYOUT = os.path.join(LAYOUT_DIR, "current.json")
|
|
|
|
def get_layout():
|
|
# V2: GrapesJS project JSON stored in current_v2.json
|
|
v2_path = os.path.join(LAYOUT_DIR, "current_v2.json")
|
|
if os.path.exists(v2_path):
|
|
with open(v2_path) as f:
|
|
return json.load(f)
|
|
# No V2 layout yet — return empty so GrapesJS starts fresh
|
|
return {}
|
|
|
|
def get_v1_layout():
|
|
"""Legacy V1 layout for reference"""
|
|
if os.path.exists(CURRENT_LAYOUT):
|
|
with open(CURRENT_LAYOUT) as f:
|
|
return json.load(f)
|
|
return {}
|
|
|
|
def save_layout(layout):
|
|
# V2: save GrapesJS project data
|
|
v2_path = os.path.join(LAYOUT_DIR, "current_v2.json")
|
|
with open(v2_path, "w") as f:
|
|
json.dump(layout, f, indent=2)
|
|
|
|
def save_v1_layout(layout):
|
|
"""Legacy V1 save"""
|
|
with open(CURRENT_LAYOUT, "w") as f:
|
|
json.dump(layout, f, indent=2)
|
|
|
|
def get_user():
|
|
return request.cookies.get("bfa_user", "")
|
|
|
|
@app.route("/")
|
|
def index():
|
|
if not get_user():
|
|
return redirect("login")
|
|
return render_template("editor.html", user=get_user())
|
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
|
def login():
|
|
layout = get_layout()
|
|
users = layout.get("users", ["Richard", "Rob", "Guido"])
|
|
if request.method == "POST":
|
|
name = request.form.get("user", "").strip()
|
|
if name:
|
|
resp = make_response(redirect("./"))
|
|
resp.set_cookie("bfa_user", name, max_age=86400*30)
|
|
# Add user to layout if new
|
|
if name not in users:
|
|
layout.setdefault("users", []).append(name)
|
|
save_layout(layout)
|
|
return resp
|
|
return render_template("login.html", users=users)
|
|
|
|
@app.route("/logout")
|
|
def logout():
|
|
resp = make_response(redirect("login"))
|
|
resp.delete_cookie("bfa_user")
|
|
return resp
|
|
|
|
@app.route("/api/layout", methods=["GET"])
|
|
def api_get_layout():
|
|
return jsonify(get_layout())
|
|
|
|
@app.route("/api/layout", methods=["POST"])
|
|
def api_save_layout():
|
|
layout = request.json
|
|
save_layout(layout)
|
|
return jsonify({"ok": True})
|
|
|
|
@app.route("/api/comment", methods=["POST"])
|
|
def api_add_comment():
|
|
data = request.json
|
|
layout = get_layout()
|
|
comment = {
|
|
"id": str(uuid.uuid4())[:8],
|
|
"target": data["target"],
|
|
"user": data.get("user", get_user()),
|
|
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
"text": data["text"]
|
|
}
|
|
layout.setdefault("comments", []).append(comment)
|
|
save_layout(layout)
|
|
return jsonify(comment)
|
|
|
|
@app.route("/api/users", methods=["POST"])
|
|
def api_add_user():
|
|
name = request.json.get("name", "").strip()
|
|
if not name:
|
|
return jsonify({"error": "empty name"}), 400
|
|
layout = get_layout()
|
|
if name not in layout.get("users", []):
|
|
layout.setdefault("users", []).append(name)
|
|
save_layout(layout)
|
|
return jsonify({"ok": True, "users": layout["users"]})
|
|
|
|
@app.route("/api/photo", methods=["POST"])
|
|
def api_upload_photo():
|
|
if "file" not in request.files:
|
|
return jsonify({"error": "no file"}), 400
|
|
f = request.files["file"]
|
|
fname = f"{int(time.time())}_{f.filename}"
|
|
f.save(os.path.join(PHOTO_DIR, fname))
|
|
return jsonify({"filename": fname, "url": f"static/photos/{fname}"})
|
|
|
|
@app.route("/api/photos", methods=["GET"])
|
|
def api_list_photos():
|
|
photos = sorted(os.listdir(PHOTO_DIR)) if os.path.exists(PHOTO_DIR) else []
|
|
return jsonify([{"filename": p, "url": f"static/photos/{p}"} for p in photos if not p.startswith(".")])
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=5001, debug=True)
|