diff --git a/app/context/wizard.py b/app/context/wizard.py
index a741968..d323f4c 100644
--- a/app/context/wizard.py
+++ b/app/context/wizard.py
@@ -115,7 +115,10 @@ def apply_session(db_path: Path, session: dict[str, Any]) -> dict[str, Any]:
syslog_answer = str(answers.get("syslog") or "No")
if syslog_answer.startswith("Yes"):
- port = int(answers.get("syslog_port") or 514)
+ try:
+ port = int(answers.get("syslog_port") or 514)
+ except (ValueError, TypeError):
+ port = 514
sources.append({"type": "syslog", "id": f"syslog:{hostname}", "port": port})
return {
diff --git a/web/src/components/WizardOverlay.vue b/web/src/components/WizardOverlay.vue
index bdf0cb0..521c2a4 100644
--- a/web/src/components/WizardOverlay.vue
+++ b/web/src/components/WizardOverlay.vue
@@ -4,6 +4,7 @@
aria-modal="true"
:aria-labelledby="`wiz-heading-${currentStep?.step ?? 1}`"
class="fixed inset-0 z-50 bg-surface overflow-y-auto"
+ :ref="(el) => { dialogRef = el as HTMLElement | null }"
>
@@ -94,7 +95,7 @@
>
{{ applying ? 'Saving…' : `Save ${factCount} facts and apply source config` }}
-
@@ -133,6 +134,7 @@ const applying = ref(false)
const applyError = ref(null)
const headingRef = ref(null)
const summaryRef = ref(null)
+const dialogRef = ref(null)
const totalSteps = computed(() => schema.value.length)
const currentStep = computed(() =>
@@ -207,6 +209,19 @@ async function applyWizard() {
}
function onKeydown(e: KeyboardEvent) {
+ if (e.key === 'Tab') {
+ const focusable = dialogRef.value?.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ )
+ if (!focusable || focusable.length === 0) return
+ const first = focusable[0]!
+ const last = focusable[focusable.length - 1]!
+ if (e.shiftKey) {
+ if (document.activeElement === first) { e.preventDefault(); last.focus() }
+ } else {
+ if (document.activeElement === last) { e.preventDefault(); first.focus() }
+ }
+ }
if (e.key === 'Escape') emit('close')
}
diff --git a/web/src/views/ContextView.vue b/web/src/views/ContextView.vue
index 0847320..b8893d2 100644
--- a/web/src/views/ContextView.vue
+++ b/web/src/views/ContextView.vue
@@ -21,7 +21,7 @@
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, nextTick } from 'vue'
import DocUploadZone from '@/components/DocUploadZone.vue'
import WizardOverlay from '@/components/WizardOverlay.vue'
@@ -199,6 +201,19 @@ interface Doc { id: string; filename: string; doc_type: string; file_size: numbe
interface Fact { id: string; category: string; key: string; value: string; source: string | null; created_at: string }
const activeTab = ref('Documents')
+const tabRefs = ref([])
+const ctxTabs = ['Documents', 'Facts']
+
+function handleTabKey(e: KeyboardEvent, currentTab: string) {
+ const idx = ctxTabs.indexOf(currentTab)
+ let next = idx
+ if (e.key === 'ArrowRight') next = (idx + 1) % ctxTabs.length
+ else if (e.key === 'ArrowLeft') next = (idx - 1 + ctxTabs.length) % ctxTabs.length
+ else return
+ e.preventDefault()
+ activeTab.value = ctxTabs[next]!
+ nextTick(() => tabRefs.value[next]?.focus())
+}
const showWizard = ref(false)
const docs = ref([])
const facts = ref([])
@@ -224,30 +239,36 @@ async function loadFacts() {
}
async function doDelete(id: string) {
- const r = await fetch(`${BASE}/api/context/docs/${id}`, { method: 'DELETE' })
- if (r.ok) { confirmDelete.value = null; await loadDocs() }
- else error.value = 'Failed to delete document'
+ try {
+ const r = await fetch(`${BASE}/api/context/docs/${id}`, { method: 'DELETE' })
+ if (r.ok) { confirmDelete.value = null; await loadDocs() }
+ else error.value = 'Failed to delete document'
+ } catch { error.value = 'Could not reach server' }
}
async function doDeleteFact(id: string) {
- const r = await fetch(`${BASE}/api/context/facts/${id}`, { method: 'DELETE' })
- if (r.ok) { confirmDeleteFact.value = null; await loadFacts() }
- else error.value = 'Failed to delete fact'
+ try {
+ const r = await fetch(`${BASE}/api/context/facts/${id}`, { method: 'DELETE' })
+ if (r.ok) { confirmDeleteFact.value = null; await loadFacts() }
+ else error.value = 'Failed to delete fact'
+ } catch { error.value = 'Could not reach server' }
}
async function addFact() {
if (!newFact.value.key.trim() || !newFact.value.value.trim()) return
- const r = await fetch(`${BASE}/api/context/facts`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ...newFact.value, source: 'manual' }),
- })
- if (r.ok) {
- newFact.value = { category: 'service', key: '', value: '' }
- await loadFacts()
- } else {
- error.value = 'Failed to add fact'
- }
+ try {
+ const r = await fetch(`${BASE}/api/context/facts`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ...newFact.value, source: 'manual' }),
+ })
+ if (r.ok) {
+ newFact.value = { category: 'service', key: '', value: '' }
+ await loadFacts()
+ } else {
+ error.value = 'Failed to add fact'
+ }
+ } catch { error.value = 'Could not reach server' }
}
function onWizardComplete() {
diff --git a/web/src/views/DiagnoseView.vue b/web/src/views/DiagnoseView.vue
index 80f2e64..fada17f 100644
--- a/web/src/views/DiagnoseView.vue
+++ b/web/src/views/DiagnoseView.vue
@@ -11,14 +11,16 @@
-import { ref, onMounted, watch } from 'vue'
+import { ref, onMounted, watch, nextTick } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
import QuickCapture from '@/components/QuickCapture.vue'
import IncidentForm from '@/components/IncidentForm.vue'
@@ -72,6 +74,19 @@ const tabs: { key: 'quick' | 'structured'; label: string }[] = [
]
const activeTab = ref<'quick' | 'structured'>('quick')
const createdLabel = ref('')
+const tabRefs = ref([])
+
+function handleTabKey(e: KeyboardEvent, currentKey: 'quick' | 'structured') {
+ const keys = tabs.map(t => t.key)
+ const idx = keys.indexOf(currentKey)
+ let next = idx
+ if (e.key === 'ArrowRight') next = (idx + 1) % keys.length
+ else if (e.key === 'ArrowLeft') next = (idx - 1 + keys.length) % keys.length
+ else return
+ e.preventDefault()
+ activeTab.value = keys[next] as 'quick' | 'structured'
+ nextTick(() => tabRefs.value[next]?.focus())
+}
onMounted(() => {
if (route.query.tab === 'structured') activeTab.value = 'structured'