From fd56ed4049c721667310d1fe75258c60ad9084fb Mon Sep 17 00:00:00 2001 From: nearxos Date: Sun, 22 Feb 2026 16:29:14 +0200 Subject: [PATCH] Add file editing functionality to dashboard Implement API endpoints for retrieving and saving file content, allowing users to edit supported file types directly from the dashboard. Introduce a modal editor interface with syntax highlighting for various file formats. Update the HTML template to include the editor overlay and associated JavaScript for handling file operations, enhancing user experience and interactivity in managing portal files. --- emmc-provisioning/dashboard/app.py | 90 +++++++++++++++++++ .../dashboard/templates/portal_files.html | 74 ++++++++++++++- 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index 39f2d3a..8f6f06b 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -1063,6 +1063,96 @@ def api_portal_files_upload(): return jsonify({"ok": False, "error": str(e)}), 500 +# Text extensions allowed for editor (others are read-only or binary) +_PORTAL_EDITABLE_EXTENSIONS = frozenset( + ".sh .bash .conf .cfg .ini .desktop .json .py .yml .yaml .md .txt .plymouth .script".split() +) + + +def _portal_language_for_path(name): + """Return Ace/editor language mode from filename.""" + n = (name or "").lower() + if n.endswith(".sh") or n.endswith(".bash"): + return "sh" + if n.endswith(".json"): + return "json" + if n.endswith(".py"): + return "python" + if n.endswith(".yml") or n.endswith(".yaml"): + return "yaml" + if n.endswith(".md"): + return "markdown" + if n.endswith(".conf") or n.endswith(".cfg") or n.endswith(".ini") or n.endswith(".desktop") or n.endswith(".plymouth"): + return "ini" + if n.endswith(".script"): + return "sh" + return "plain_text" + + +@app.route("/api/portal-files/content", methods=["GET"]) +@require_admin +def api_portal_file_content_get(): + """Return file content as text for the editor. path= query param. Rejects binary.""" + path_arg = (request.args.get("path") or "").strip() + if not path_arg or ".." in path_arg or "\\" in path_arg: + return jsonify({"ok": False, "error": "invalid path"}), 400 + path = (PORTAL_FILES_DIR / path_arg).resolve() + try: + path.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"ok": False, "error": "invalid path"}), 400 + if not path.is_file(): + return jsonify({"ok": False, "error": "not found"}), 404 + ext = path.suffix.lower() if path.suffix else "" + if ext and ext not in _PORTAL_EDITABLE_EXTENSIONS: + return jsonify({"ok": False, "error": "binary or unsupported file type for editing"}), 400 + try: + raw = path.read_bytes() + text = raw.decode("utf-8") + except UnicodeDecodeError: + return jsonify({"ok": False, "error": "binary or unsupported encoding"}), 400 + except OSError as e: + return jsonify({"ok": False, "error": str(e)}), 500 + return jsonify({ + "ok": True, + "path": path_arg, + "content": text, + "language": _portal_language_for_path(path.name), + }) + + +@app.route("/api/portal-files/content", methods=["PUT"]) +@require_admin +def api_portal_file_content_put(): + """Write file content from editor. JSON body: { \"path\": \"...\", \"content\": \"...\" }.""" + data = request.get_json(silent=True) or {} + path_arg = (data.get("path") or "").strip() + content = data.get("content") + if not path_arg or ".." in path_arg or "\\" in path_arg: + return jsonify({"ok": False, "error": "invalid path"}), 400 + if content is None: + return jsonify({"ok": False, "error": "content required"}), 400 + path = (PORTAL_FILES_DIR / path_arg).resolve() + try: + path.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"ok": False, "error": "invalid path"}), 400 + if path.is_dir(): + return jsonify({"ok": False, "error": "cannot edit a folder"}), 400 + ext = path.suffix.lower() if path.suffix else "" + if path.exists() and ext and ext not in _PORTAL_EDITABLE_EXTENSIONS: + return jsonify({"ok": False, "error": "unsupported file type for editing"}), 400 + if not isinstance(content, str): + content = str(content) + try: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + admin_log("portal_edit", path_arg) + return jsonify({"ok": True, "path": path_arg}) + except OSError as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + @app.route("/api/portal-files/", methods=["DELETE"]) @require_admin def api_portal_file_delete(name): diff --git a/emmc-provisioning/dashboard/templates/portal_files.html b/emmc-provisioning/dashboard/templates/portal_files.html index eacb8f5..90ea12e 100644 --- a/emmc-provisioning/dashboard/templates/portal_files.html +++ b/emmc-provisioning/dashboard/templates/portal_files.html @@ -47,6 +47,15 @@ .desc-input { width: 100%; max-width: 280px; font-size: 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.3rem 0.5rem; border-radius: 4px; } .folder-name { font-weight: 500; } input[type="text"] { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.35rem 0.5rem; border-radius: 4px; font: inherit; } + /* Editor modal */ + .editor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 1rem; } + .editor-overlay.visible { display: flex; } + .editor-modal { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); width: 100%; max-width: 900px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4); } + .editor-header { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; } + .editor-title { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: var(--text); } + .editor-actions { display: flex; gap: 0.5rem; } + .editor-body { flex: 1; min-height: 400px; overflow: hidden; } + #editorHost { width: 100%; height: 100%; min-height: 420px; } @@ -80,8 +89,24 @@ + +
+
+
+ +
+ + +
+
+
+
+
+
+
+