From 0d98e16f29673a1165e85a3c15353d7da03acd45 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 26 Jun 2026 18:36:04 +0100 Subject: [PATCH 1/2] feat: implement SFTP file upload progress tracking using XHR in API and UI components --- frontend/src/api.ts | 37 ++++++++++++++++++++++----- frontend/src/components/SftpPanel.vue | 22 ++++++++++++++-- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 39fd84c..b074cec 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -318,16 +318,41 @@ export const api = { await handle(res); }, - async sftpUpload(connId: string, path: string, file: File): Promise { + async sftpUpload( + connId: string, + path: string, + file: File, + onProgress?: (loaded: number, total: number) => void + ): Promise { const fd = new FormData(); fd.set("path", path); fd.set("file", file); - const res = await fetch(`/api/sftp/${connId}/upload`, { - method: "POST", - credentials: "include", - body: fd, + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", `/api/sftp/${connId}/upload`); + xhr.withCredentials = true; + if (onProgress) { + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + onProgress(e.loaded, e.total); + } + }; + } + xhr.onload = () => { + if (xhr.status === 401) return reject(new Error("unauthorized")); + let data: any = {}; + try { + data = JSON.parse(xhr.responseText); + } catch (e) {} + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(data.error || xhr.statusText)); + } + }; + xhr.onerror = () => reject(new Error("Network Error")); + xhr.send(fd); }); - await handle(res); }, sftpDownloadUrl(connId: string, path: string): string { diff --git a/frontend/src/components/SftpPanel.vue b/frontend/src/components/SftpPanel.vue index 9de810a..b3dead7 100644 --- a/frontend/src/components/SftpPanel.vue +++ b/frontend/src/components/SftpPanel.vue @@ -11,6 +11,7 @@ const err = ref(""); const busy = ref(false); const renameTarget = ref(null); const newName = ref(""); +const uploadProgress = ref(null); function isDir(m: number): boolean { return (m & 0o170000) === 0o040000; @@ -66,11 +67,16 @@ async function onUpload(ev: Event) { input.value = ""; if (!file) return; err.value = ""; + uploadProgress.value = 0; try { - await api.sftpUpload(props.connId, path.value, file); + await api.sftpUpload(props.connId, path.value, file, (loaded, total) => { + uploadProgress.value = Math.round((loaded / total) * 100); + }); await load(); } catch (e) { err.value = e instanceof Error ? e.message : "Upload failed"; + } finally { + uploadProgress.value = null; } } @@ -186,7 +192,19 @@ function fmtSize(n: number): string {

{{ err }}

-

Loading…

+
+
+ Uploading... + {{ uploadProgress }}% +
+
+
+
+
+

Loading…

  • Date: Fri, 26 Jun 2026 18:40:25 +0100 Subject: [PATCH 2/2] feat: add toggleable audit log functionality based on server environment variable --- app.py | 11 +++++++++-- frontend/src/App.vue | 5 +++++ frontend/src/api.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index f84d5e8..85eeb1c 100644 --- a/app.py +++ b/app.py @@ -883,7 +883,13 @@ def _connect_with_jump_chain(host_id: int) -> tuple[paramiko.SSHClient, paramiko raise RuntimeError("failed to build jump chain") +def is_audit_log_enabled() -> bool: + return os.getenv("ENABLE_AUDIT_LOG", "true").lower() in ("1", "true", "yes") + + def _insert_connection_audit(host_row: dict[str, Any]) -> int | None: + if not is_audit_log_enabled(): + return None try: with db_cursor() as (_, cur): cur.execute( @@ -964,9 +970,10 @@ def api_logout(): @app.route("/api/me", methods=["GET"]) def api_me(): version = app.config.get("VERSION", "unknown") + audit_enabled = is_audit_log_enabled() if session.get("logged_in"): - return jsonify({"logged_in": True, "app_version": version}) - return jsonify({"logged_in": False, "app_version": version}) + return jsonify({"logged_in": True, "app_version": version, "audit_log_enabled": audit_enabled}) + return jsonify({"logged_in": False, "app_version": version, "audit_log_enabled": audit_enabled}) @app.route("/api/identities", methods=["GET"]) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 1e4f5e5..a77b002 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -25,6 +25,7 @@ interface TabItem { const loggedIn = ref(false); const checking = ref(true); const appVersion = ref("unknown"); +const auditLogEnabled = ref(true); const identities = ref([]); const allHosts = ref([]); const allFolders = ref([]); @@ -347,6 +348,9 @@ onMounted(async () => { if (m.app_version) { appVersion.value = m.app_version; } + if (m.audit_log_enabled !== undefined) { + auditLogEnabled.value = m.audit_log_enabled; + } if (loggedIn.value) await refreshData(); } catch { loggedIn.value = false; @@ -918,6 +922,7 @@ async function deleteIdentityRow(id: number) { API keys