feat: add Quick/Structured tabs to DiagnoseView
This commit is contained in:
parent
a5c4e4453b
commit
24f46df8e7
1 changed files with 46 additions and 85 deletions
|
|
@ -1,108 +1,69 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 max-w-4xl mx-auto">
|
<div class="p-6 max-w-4xl mx-auto">
|
||||||
<div class="mb-6">
|
<div class="mb-5">
|
||||||
<h1 class="text-text-primary text-xl font-semibold mb-1">Diagnose</h1>
|
<h1 class="text-text-primary text-xl font-semibold mb-1">Diagnose</h1>
|
||||||
<p class="text-text-dim text-sm">
|
<p class="text-text-dim text-sm">
|
||||||
Describe a symptom or service name. Turnstone runs layered searches — broad relevance,
|
Quick: describe a symptom to surface log evidence.
|
||||||
then CRITICAL/ERROR — and returns deduplicated evidence sorted by time.
|
Structured: tag a timestamped incident record.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 mb-6">
|
<!-- Tab toggle -->
|
||||||
<input
|
<div class="flex gap-1 mb-6 border-b border-surface-border">
|
||||||
v-model="symptom"
|
|
||||||
type="text"
|
|
||||||
placeholder="e.g. 'plex EAE audio' or 'ssh authentication failed'"
|
|
||||||
class="flex-1 bg-surface-raised border border-surface-border rounded px-4 py-2.5 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
|
|
||||||
@keydown.enter="run()"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
class="px-6 py-2.5 rounded bg-accent text-white text-sm font-semibold hover:bg-blue-400 transition-colors disabled:opacity-50"
|
v-for="t in tabs"
|
||||||
:disabled="loading || !symptom.trim()"
|
:key="t.key"
|
||||||
@click="run()"
|
@click="activeTab = t.key"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 text-sm transition-colors border-b-2 -mb-px',
|
||||||
|
activeTab === t.key
|
||||||
|
? 'border-accent text-accent'
|
||||||
|
: 'border-transparent text-text-muted hover:text-text-primary'
|
||||||
|
]"
|
||||||
|
>{{ t.label }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick tab -->
|
||||||
|
<QuickCapture v-if="activeTab === 'quick'" />
|
||||||
|
|
||||||
|
<!-- Structured tab -->
|
||||||
|
<template v-else>
|
||||||
|
<IncidentForm @created="onCreated" />
|
||||||
|
<div
|
||||||
|
v-if="createdLabel"
|
||||||
|
class="mt-4 p-3 rounded bg-green-900/30 border border-green-700/40 text-green-400 text-sm"
|
||||||
>
|
>
|
||||||
<span v-if="loading">Diagnosing…</span>
|
Incident "{{ createdLabel }}" saved —
|
||||||
<span v-else>Diagnose</span>
|
<RouterLink to="/incidents" class="underline underline-offset-2">view in Incidents</RouterLink>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error -->
|
|
||||||
<div v-if="error" class="mb-4 p-3 rounded bg-red-900/30 border border-red-700/40 text-sev-error text-sm">
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results -->
|
|
||||||
<template v-if="entries.length">
|
|
||||||
<div class="mb-3 text-text-dim text-xs">
|
|
||||||
{{ entries.length }} relevant log{{ entries.length !== 1 ? 's' : '' }} — sorted chronologically
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plain-text summary (pre-formatted for LLM context) -->
|
|
||||||
<div v-if="formatted" class="mb-4 rounded border border-surface-border bg-surface-raised p-4 overflow-x-auto">
|
|
||||||
<p class="text-text-dim text-xs uppercase tracking-widest mb-2">Formatted summary</p>
|
|
||||||
<pre class="text-text-muted text-xs whitespace-pre-wrap leading-relaxed font-mono">{{ formatted }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded border border-surface-border overflow-hidden">
|
|
||||||
<LogEntryRow
|
|
||||||
v-for="entry in entries"
|
|
||||||
:key="entry.entry_id"
|
|
||||||
:entry="entry"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Zero state after run -->
|
|
||||||
<div v-else-if="ranOnce && !loading" class="text-center text-text-dim py-12">
|
|
||||||
<p class="mb-1">No log evidence found for "{{ lastQuery }}"</p>
|
|
||||||
<p class="text-sm">Check the Sources tab to confirm data is ingested, or try a broader description.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, RouterLink } from 'vue-router'
|
||||||
import type { LogEntry } from '@/stores/search'
|
import QuickCapture from '@/components/QuickCapture.vue'
|
||||||
import LogEntryRow from '@/components/LogEntryRow.vue'
|
import IncidentForm from '@/components/IncidentForm.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const symptom = ref('')
|
const tabs: { key: 'quick' | 'structured'; label: string }[] = [
|
||||||
const entries = ref<LogEntry[]>([])
|
{ key: 'quick', label: 'Quick' },
|
||||||
const formatted = ref('')
|
{ key: 'structured', label: 'Structured' },
|
||||||
const loading = ref(false)
|
]
|
||||||
const error = ref<string | null>(null)
|
const activeTab = ref<'quick' | 'structured'>('quick')
|
||||||
const ranOnce = ref(false)
|
const createdLabel = ref('')
|
||||||
const lastQuery = ref('')
|
|
||||||
|
|
||||||
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const q = route.query.q
|
if (route.query.tab === 'structured') activeTab.value = 'structured'
|
||||||
if (typeof q === 'string' && q.trim()) {
|
|
||||||
symptom.value = q
|
|
||||||
run()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function run() {
|
watch(() => route.query.tab, (tab) => {
|
||||||
if (!symptom.value.trim()) return
|
if (tab === 'structured' || tab === 'quick') activeTab.value = tab
|
||||||
loading.value = true
|
})
|
||||||
error.value = null
|
|
||||||
lastQuery.value = symptom.value
|
function onCreated(label: string) {
|
||||||
try {
|
createdLabel.value = label
|
||||||
const url = new URL(`${BASE}/api/diagnose`, window.location.origin)
|
setTimeout(() => { createdLabel.value = '' }, 4000)
|
||||||
url.searchParams.set('q', symptom.value)
|
|
||||||
const res = await fetch(url.toString())
|
|
||||||
if (!res.ok) throw new Error(`API returned ${res.status}`)
|
|
||||||
const data = await res.json()
|
|
||||||
entries.value = data.results
|
|
||||||
formatted.value = data.formatted ?? ''
|
|
||||||
ranOnce.value = true
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e instanceof Error ? e.message : String(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue