diff --git a/backend/src/db.ts b/backend/src/db.ts index 0e1286a..73ab240 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,8 +1,9 @@ import Database from 'better-sqlite3'; import { DB_PATH } from './config'; import { logger } from './globals'; -import { readdir, readFile } from "fs/promises"; -import { join, dirname } from "path"; +import { readdir, readFile } from "fs/promises"; +import { existsSync, mkdirSync } from "fs"; +import { join, dirname } from "path"; export let FTS5Enabled = true; @@ -55,13 +56,25 @@ export const migrate = async () => { logger.info(`Migrations done`); } -logger.info(`Loading database at ${DB_PATH}`); - -export const db = new Database(DB_PATH); +logger.info(`Loading database at ${DB_PATH}`); + +// Ensure parent directory exists (avoid better-sqlite3 directory error) +try { + const dir = dirname(DB_PATH); + // Skip if dir is current directory or drive root-like (e.g., "C:") + const isTrivialDir = dir === '.' || dir === '' || /^[A-Za-z]:\\?$/.test(dir); + if (!isTrivialDir && !existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } +} catch (e) { + logger.warn(`Failed to ensure DB directory exists: ${e}`); +} + +export const db = new Database(DB_PATH); initializeDB(); -migrate(); \ No newline at end of file +migrate(); diff --git a/frontend-vue/src/components/base/BaseButton.vue b/frontend-vue/src/components/base/BaseButton.vue index da2360f..de82723 100644 --- a/frontend-vue/src/components/base/BaseButton.vue +++ b/frontend-vue/src/components/base/BaseButton.vue @@ -46,7 +46,9 @@ const emit = defineEmits<{ const handleKeydown = (event: KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() - emit('click', event as any) + const btn = event.currentTarget as HTMLButtonElement | null + // Trigger native click so type="submit" works and parent @click receives it + btn?.click() } } @@ -202,4 +204,4 @@ const handleKeydown = (event: KeyboardEvent) => { border-color: #6b7280; } } - \ No newline at end of file + diff --git a/frontend-vue/src/components/base/BaseDialog.vue b/frontend-vue/src/components/base/BaseDialog.vue index da50c04..f08dc2d 100644 --- a/frontend-vue/src/components/base/BaseDialog.vue +++ b/frontend-vue/src/components/base/BaseDialog.vue @@ -16,6 +16,7 @@ 'dialog', `dialog--${size}` ]" + tabindex="-1" @click.stop >
@@ -87,6 +88,12 @@ const handleOverlayClick = () => { let lastFocusedElement: HTMLElement | null = null const trapFocus = (event: KeyboardEvent) => { + // Close on Escape regardless of focused element when dialog is open + if (event.key === 'Escape') { + event.preventDefault() + handleClose() + return + } if (event.key !== 'Tab') return const focusableElements = dialogRef.value?.querySelectorAll( @@ -119,16 +126,24 @@ watch(() => props.show, async (isVisible) => { await nextTick() - // Focus first focusable element or the dialog itself - const firstFocusable = dialogRef.value?.querySelector( - 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' - ) as HTMLElement - + // Focus [autofocus] first, then first focusable, else the dialog itself + const root = dialogRef.value as HTMLElement | undefined + const selector = '[autofocus], button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + const firstFocusable = root?.querySelector(selector) as HTMLElement | null if (firstFocusable) { firstFocusable.focus() } else { - dialogRef.value?.focus() + root?.focus() } + + // Retry shortly after in case slotted children mount slightly later + setTimeout(() => { + if (!root) return + if (!root.contains(document.activeElement)) { + const retryTarget = (root.querySelector(selector) as HTMLElement) || root + retryTarget?.focus() + } + }, 0) } else { document.body.style.overflow = '' document.removeEventListener('keydown', trapFocus) @@ -275,4 +290,4 @@ watch(() => props.show, async (isVisible) => { color: rgba(255, 255, 255, 0.87); } } - \ No newline at end of file +