From e43c4a7f2a069170ce460a2bb189d7ba017c824d Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 19 Jun 2026 19:12:53 +0100 Subject: [PATCH 1/7] feat: automate virtual environment setup and use exec for gunicorn process management --- run.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/run.sh b/run.sh index 3ae4f0a..278b524 100755 --- a/run.sh +++ b/run.sh @@ -1,4 +1,16 @@ #!/bin/bash +if [ ! -d "venv" ]; then + echo "Creating virtual environment in 'venv'..." + python3 -m venv venv + source venv/bin/activate + if [ -f "requirements.txt" ]; then + echo "Installing requirements..." + pip install -r requirements.txt + fi +else + source venv/bin/activate +fi + cd frontend && npm run build && cd .. -gunicorn --bind 0.0.0.0:5000 --workers 1 --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker app:app --log-level info \ No newline at end of file +exec gunicorn --bind 0.0.0.0:5000 --workers 1 --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker app:app --log-level info \ No newline at end of file -- 2.47.3 From d81136fe5e259d751662f1fcf47702ebdf855d63 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 19 Jun 2026 19:29:44 +0100 Subject: [PATCH 2/7] feat: add toggleable SFTP panel --- .gitignore | 3 ++- frontend/src/App.vue | 13 +++++++++++++ frontend/src/components/TabContent.vue | 27 ++++++++++++++------------ run.sh | 11 ++++++++++- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 9f2f255..a1ff39d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ .env frontend/node_modules/ -static/dist/ \ No newline at end of file +static/dist/ +venv/ \ No newline at end of file diff --git a/frontend/src/App.vue b/frontend/src/App.vue index cf57aec..a755f4d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -49,6 +49,10 @@ const showFolderForm = ref(false); const showEditHost = ref(false); const showAuditLog = ref(false); const showApiKeys = ref(false); +const showSftpPanel = ref(false); +function toggleSftp() { + showSftpPanel.value = !showSftpPanel.value; +} const auditLoading = ref(false); const auditErr = ref(""); const auditRows = ref([]); @@ -705,6 +709,14 @@ async function deleteIdentityRow(id: number) {
+
diff --git a/frontend/src/components/TabContent.vue b/frontend/src/components/TabContent.vue index 46c4277..a3359bb 100644 --- a/frontend/src/components/TabContent.vue +++ b/frontend/src/components/TabContent.vue @@ -14,6 +14,7 @@ import SftpPanel from "./SftpPanel.vue"; const props = defineProps<{ hostId: number; visible: boolean; + showSftp: boolean; }>(); const termEl = ref(null); @@ -187,17 +188,19 @@ watch( class="h-full min-h-[320px] rounded-lg border border-slate-800 bg-[#0a0e12] p-1" /> - - + diff --git a/run.sh b/run.sh index 278b524..3d39c5f 100755 --- a/run.sh +++ b/run.sh @@ -12,5 +12,14 @@ else source venv/bin/activate fi -cd frontend && npm run build && cd .. +echo "Building frontend..." +( + cd frontend + if [ ! -d "node_modules" ]; then + echo "Installing frontend dependencies..." + npm install + fi + npm run build +) + exec gunicorn --bind 0.0.0.0:5000 --workers 1 --worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker app:app --log-level info \ No newline at end of file -- 2.47.3 From 59d216f697ff684c215ecf9496231d7ea9345e18 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 19 Jun 2026 19:34:58 +0100 Subject: [PATCH 3/7] chore: rename application title from JDB-NET SSH to SSH across UI components --- frontend/index.html | 2 +- frontend/src/App.vue | 2 +- frontend/src/components/LoginForm.vue | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 2313e17..c3c84b1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -16,7 +16,7 @@ href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> - JDB-NET SSH + SSH
diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a755f4d..8b2f430 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -697,7 +697,7 @@ async function deleteIdentityRow(id: number) {
- JDB-NET SSH + SSH

- JDB-NET SSH + SSH

Sign in to manage connections and open terminals. -- 2.47.3 From 053c9e8431489c3f3ef017b5c390a24790a07f90 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 19 Jun 2026 19:42:59 +0100 Subject: [PATCH 4/7] feat: add tab dragging and multi-pane split view support to App layout --- frontend/src/App.vue | 94 +++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8b2f430..08ca103 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -33,8 +33,42 @@ const breadcrumb = ref<{ id: number; label: string }[]>([]); const searchActive = ref(false); const currentFolderId = ref(null); const searchQuery = ref(""); -const tabs = ref([]); -const activeTabId = ref(null); +const tabs = ref<{ id: string; hostId: number; label: string }[]>([]); +const activePanes = ref([]); +const draggedTabId = ref(null); + +function onTabDragStart(id: string) { + draggedTabId.value = id; +} + +function onTabDrop(targetId: string) { + if (!draggedTabId.value || draggedTabId.value === targetId) return; + const from = tabs.value.findIndex((t) => t.id === draggedTabId.value); + const to = tabs.value.findIndex((t) => t.id === targetId); + if (from === -1 || to === -1) return; + const [t] = tabs.value.splice(from, 1); + tabs.value.splice(to, 0, t); + draggedTabId.value = null; +} + +function onTabDragEnd() { + draggedTabId.value = null; +} + +function onSplitDrop() { + if (!draggedTabId.value || activePanes.value.includes(draggedTabId.value)) { + draggedTabId.value = null; + return; + } + if (activePanes.value.length >= 2) { + activePanes.value = [activePanes.value[0], draggedTabId.value]; + } else if (activePanes.value.length === 1) { + activePanes.value.push(draggedTabId.value); + } else { + activePanes.value = [draggedTabId.value]; + } + draggedTabId.value = null; +} const loadErr = ref(""); const hostSortOrder = ref<"name" | "last_connected">("name"); /** Narrow viewports: slide-over hosts panel; md+ sidebar stays visible */ @@ -236,9 +270,10 @@ async function onLoggedIn() { async function logout() { await api.logout(); - tabs.value = []; - activeTabId.value = null; - loggedIn.value = false; + tabs.value = []; + activePanes.value = []; + allHosts.value = []; + loggedIn.value = false; } function fmtDate(ts: string | null): string { @@ -403,7 +438,7 @@ function fmtScopes(scopes: string[]): string { function openTab(h: HostRow) { const id = crypto.randomUUID(); tabs.value.push({ id, hostId: h.id, label: h.label }); - activeTabId.value = id; + activePanes.value = [id]; if (window.matchMedia("(max-width: 767px)").matches) { sidebarOpen.value = false; } @@ -415,8 +450,9 @@ function toggleSidebar() { function closeTab(id: string) { tabs.value = tabs.value.filter((t) => t.id !== id); - if (activeTabId.value === id) { - activeTabId.value = tabs.value.length ? tabs.value[tabs.value.length - 1].id : null; + activePanes.value = activePanes.value.filter((p) => p !== id); + if (activePanes.value.length === 0 && tabs.value.length) { + activePanes.value = [tabs.value[tabs.value.length - 1].id]; } } @@ -641,10 +677,9 @@ async function deleteHostRow(id: number) { await api.deleteHost(id); allHosts.value = allHosts.value.filter((h) => h.id !== id); tabs.value = tabs.value.filter((t) => t.hostId !== id); - if (!tabs.value.some((t) => t.id === activeTabId.value)) { - activeTabId.value = tabs.value.length - ? tabs.value[tabs.value.length - 1].id - : null; + activePanes.value = activePanes.value.filter(p => tabs.value.some(t => t.id === p)); + if (activePanes.value.length === 0 && tabs.value.length) { + activePanes.value = [tabs.value[tabs.value.length - 1].id]; } await refreshBrowse(); } @@ -1003,13 +1038,19 @@ async function deleteIdentityRow(id: number) { v-for="t in tabs" :key="t.id" type="button" - class="flex items-center gap-2 rounded-t-lg border border-b-0 px-3 py-2 text-sm" - :class=" - t.id === activeTabId + draggable="true" + @dragstart="onTabDragStart(t.id)" + @dragover.prevent + @drop="onTabDrop(t.id)" + @dragend="onTabDragEnd" + class="flex items-center gap-2 rounded-t-lg border border-b-0 px-3 py-2 text-sm transition-colors" + :class="[ + activePanes.includes(t.id) ? 'border-slate-700 bg-surface text-white' - : 'border-transparent bg-transparent text-slate-400 hover:text-white' - " - @click="activeTabId = t.id" + : 'border-transparent bg-transparent text-slate-400 hover:text-white', + draggedTabId && draggedTabId !== t.id ? 'hover:bg-slate-800/50' : '' + ]" + @click="activePanes = [t.id]" > {{ t.label }} Ă—

-
+
+ +
+ Drop to split side-by-side +
-- 2.47.3 From 790cfc581a9faafc5558eb9ccb11269e742b6929 Mon Sep 17 00:00:00 2001 From: Jamie Banks Date: Fri, 19 Jun 2026 19:48:56 +0100 Subject: [PATCH 5/7] style: update design system with new color palette and typography fonts --- frontend/index.html | 4 ++-- frontend/src/components/TabContent.vue | 11 ++++++----- frontend/tailwind.config.js | 24 +++++++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index c3c84b1..70c78e1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,12 +8,12 @@ href="https://assets.jdbnet.co.uk/projects/ssh.png" /> - + SSH diff --git a/frontend/src/components/TabContent.vue b/frontend/src/components/TabContent.vue index a3359bb..5a0ff33 100644 --- a/frontend/src/components/TabContent.vue +++ b/frontend/src/components/TabContent.vue @@ -79,12 +79,13 @@ onMounted(async () => { term = new Terminal({ cursorBlink: true, - fontFamily: "IBM Plex Mono, monospace", + fontFamily: "DM Mono, ui-monospace, monospace", fontSize: 14, theme: { - background: "#0a0e12", - foreground: "#e2e8f0", - cursor: "#3d9aed", + background: "#0d1117", + foreground: "#e6edf3", + cursor: "#1ebe8a", + selectionBackground: "rgba(30, 190, 138, 0.3)", }, }); fit = new FitAddon(); @@ -185,7 +186,7 @@ watch(