turnstone/web/src/stores/search.ts
pyr0ball a45fa901dd feat: Vue 3 frontend and FastAPI REST layer
- 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)
2026-05-08 16:27:59 -07:00

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,
}
})