bfa-dryer-design/app.py
Richard Sauer 1831759563 V2: GrapesJS-based HMI design editor
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>
2026-04-08 17:06:01 +10:00

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)