- app/rest.py: FastAPI app wrapping search/diagnose/sources with CORS - web/: Vue 3 + Vite + UnoCSS + Pinia frontend at port 8535 - LogSearchView: sidebar filters (source, severity, limit) + FTS search - DiagnoseView: layered symptom investigation matching MCP diagnose tool - SourcesView: corpus table with entry count, error count, time range - LogEntryRow: severity badge, pattern chips, repeat count, timestamp - StatusDot: live API health indicator in nav - scripts/start_dev.sh: launch FastAPI (:8534) + Vite dev server (:8535) - .gitignore: add web/node_modules/ and web/dist/ - Caddy: /turnstone* route added to menagerie.circuitforge.tech block (API → :8534 with /turnstone strip, SPA fallback → :8535)
103 lines
2.7 KiB
TypeScript
103 lines
2.7 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
|
|
export interface LogEntry {
|
|
entry_id: string
|
|
source_id: string
|
|
sequence: number
|
|
timestamp_iso: string | null
|
|
severity: string | null
|
|
repeat_count: number
|
|
out_of_order: boolean
|
|
matched_patterns: string[]
|
|
text: string
|
|
rank: number
|
|
}
|
|
|
|
export interface LogSource {
|
|
source_id: string
|
|
entry_count: number
|
|
earliest: string | null
|
|
latest: string | null
|
|
error_count: number
|
|
}
|
|
|
|
const BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
|
|
async function apiFetch<T>(path: string, params?: Record<string, string | undefined>): Promise<T> {
|
|
const url = new URL(`${BASE}${path}`, window.location.origin)
|
|
if (params) {
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v !== undefined && v !== '') url.searchParams.set(k, v)
|
|
}
|
|
}
|
|
const res = await fetch(url.toString())
|
|
if (!res.ok) throw new Error(`API ${path} returned ${res.status}`)
|
|
return res.json()
|
|
}
|
|
|
|
export const useSearchStore = defineStore('search', () => {
|
|
const query = ref('')
|
|
const sourceFilter = ref<string | undefined>()
|
|
const severityFilter = ref<string | undefined>()
|
|
const sinceFilter = ref<string | undefined>()
|
|
const limit = ref(50)
|
|
|
|
const results = ref<LogEntry[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const total = ref(0)
|
|
|
|
const sources = ref<LogSource[]>([])
|
|
const sourcesLoaded = ref(false)
|
|
|
|
const severityOptions = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL']
|
|
|
|
const hasResults = computed(() => results.value.length > 0)
|
|
|
|
async function runSearch() {
|
|
if (!query.value.trim()) return
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const data = await apiFetch<{ count: number; results: LogEntry[] }>('/api/search', {
|
|
q: query.value,
|
|
source: sourceFilter.value,
|
|
severity: severityFilter.value,
|
|
since: sinceFilter.value,
|
|
limit: String(limit.value),
|
|
})
|
|
results.value = data.results
|
|
total.value = data.count
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : String(e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadSources() {
|
|
if (sourcesLoaded.value) return
|
|
try {
|
|
const data = await apiFetch<{ sources: LogSource[] }>('/api/sources')
|
|
sources.value = data.sources
|
|
sourcesLoaded.value = true
|
|
} catch {
|
|
// non-fatal
|
|
}
|
|
}
|
|
|
|
function clearFilters() {
|
|
sourceFilter.value = undefined
|
|
severityFilter.value = undefined
|
|
sinceFilter.value = undefined
|
|
limit.value = 50
|
|
}
|
|
|
|
return {
|
|
query, sourceFilter, severityFilter, sinceFilter, limit,
|
|
results, loading, error, total, sources, sourcesLoaded,
|
|
severityOptions, hasResults,
|
|
runSearch, loadSources, clearFilters,
|
|
}
|
|
})
|