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() { +
+ + +