From 1f32ce4620649b46440da2febaf7889d835ebf20 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Fri, 13 Mar 2026 10:31:39 -0400 Subject: [PATCH] fix: credential picker in host modal, fix xterm dimensions crash - Added credential dropdown to New Host modal (loads from vault API) - Fixed xterm.js "Cannot read dimensions" crash by guarding fitAddon.fit() with requestAnimationFrame and container dimension checks - Added WebGL context loss handler - credentialId now passed when creating hosts Co-Authored-By: Claude Opus 4.6 --- frontend/composables/useTerminal.ts | 16 ++++++++++++--- frontend/pages/index.vue | 31 +++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/composables/useTerminal.ts b/frontend/composables/useTerminal.ts index 882a36b..e72e95a 100644 --- a/frontend/composables/useTerminal.ts +++ b/frontend/composables/useTerminal.ts @@ -33,13 +33,23 @@ export function useTerminal() { term.open(container) try { - term.loadAddon(new WebglAddon()) + const webgl = new WebglAddon() + webgl.onContextLoss(() => { webgl.dispose() }) + term.loadAddon(webgl) } catch { // WebGL not available, fall back to canvas } - fitAddon.fit() - const resizeObserver = new ResizeObserver(() => fitAddon.fit()) + // Delay fit until container has layout dimensions + const safeFit = () => { + try { + if (container.offsetWidth > 0 && container.offsetHeight > 0) { + fitAddon.fit() + } + } catch { /* ignore fit errors on unmounted containers */ } + } + requestAnimationFrame(safeFit) + const resizeObserver = new ResizeObserver(() => safeFit()) resizeObserver.observe(container) return { term, fitAddon, searchAddon, resizeObserver } diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index 14acdb1..e444572 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -17,6 +17,10 @@ const selectedHost = ref(null) const hostCredential = ref(null) const loadingCredential = ref(false) +// Credentials list for host modal dropdown +const allCredentials = ref([]) +const allSshKeys = ref([]) + // Terminal composable for connect-on-click const { createTerminal, connectToHost } = useTerminal() @@ -24,9 +28,18 @@ onMounted(async () => { await Promise.all([connections.fetchHosts(), connections.fetchTree()]) }) -function openNewHost(groupId?: number) { +async function openNewHost(groupId?: number) { editingHost.value = groupId ? { groupId } : null - inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '' } + inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '', credentialId: null } + // Load credentials for dropdown + try { + const [creds, keys] = await Promise.all([ + vault.listCredentials() as Promise, + vault.listKeys() as Promise, + ]) + allCredentials.value = creds + allSshKeys.value = keys + } catch { /* ignore */ } showHostDialog.value = true } @@ -113,7 +126,7 @@ function dismissSavePrompt() { } // Inline modal state -const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '' }) +const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '', credentialId: null as number | null }) async function createGroupInline() { const nameEl = document.getElementById('grp-name') as HTMLInputElement @@ -134,10 +147,11 @@ async function createHostInline() { port: inlineHost.value.port, protocol: inlineHost.value.protocol, groupId: inlineHost.value.groupId, + credentialId: inlineHost.value.credentialId, tags, }) showHostDialog.value = false - inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '' } + inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '', credentialId: null } await connections.fetchTree() } @@ -419,6 +433,15 @@ async function deleteSelectedHost() { +
+ + +