Mercury/ui/src/views/admin/Cache.vue
Matthew L McPeak 0eeb2ed207
All checks were successful
ci / build-ui (push) Successful in 14s
ci / test (push) Successful in 2m57s
ci / publish (push) Successful in 2m26s
Initial Commit
2026-06-17 19:56:53 -04:00

154 lines
3.8 KiB
Vue

<template>
<div>
<div class="page-header">
<div class="page-title">
<h2>Query Cache</h2>
<span class="subtitle">In-memory LRU cache for compiled SQL queries inspect and flush as needed</span>
</div>
<div class="header-actions">
<NychButton @click="load">Refresh</NychButton>
<NychButton severity="danger" @click="flushCache">Flush Cache</NychButton>
</div>
</div>
<div class="stats-grid" v-if="stats">
<div class="stat-card">
<span class="stat-label">Entries</span>
<span class="stat-value">{{ stats.size }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Cache Hits</span>
<span class="stat-value success">{{ stats.hits }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Cache Misses</span>
<span class="stat-value muted">{{ stats.misses }}</span>
</div>
<div class="stat-card hit-rate-card">
<span class="stat-label">Hit Rate</span>
<span class="stat-value" :class="hitRateClass">{{ hitRate }}%</span>
<div class="hit-rate-bar">
<div class="hit-rate-fill" :style="{ width: hitRate + '%' }"></div>
</div>
</div>
</div>
<div class="no-stats" v-else>
<span class="empty-icon"></span>
<span class="empty-label">Cache stats unavailable</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '../../stores/auth'
const auth = useAuthStore()
const stats = ref<{ size: number; hits: number; misses: number } | null>(null)
const hitRate = computed(() => {
if (!stats.value) return 0
const total = stats.value.hits + stats.value.misses
return total === 0 ? 0 : Math.round((stats.value.hits / total) * 100)
})
const hitRateClass = computed(() => {
const r = hitRate.value
if (r >= 80) return 'good'
if (r >= 50) return 'ok'
return 'poor'
})
async function load() {
const res = await fetch('/api/admin/cache/stats', { headers: auth.authHeaders() })
stats.value = await res.json()
}
async function flushCache() {
if (!confirm('Flush the entire query cache?')) return
await fetch('/api/admin/cache/', { method: 'DELETE', headers: auth.authHeaders() })
load()
}
onMounted(load)
</script>
<style scoped>
.header-actions { display: flex; gap: 0.5rem; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.stat-card {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem 1.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.stat-label {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
}
.stat-value {
font-family: var(--font-mono);
font-size: 2.4rem;
font-weight: 500;
line-height: 1;
color: var(--primary);
}
.stat-value.success { color: var(--success); }
.stat-value.muted { color: var(--text-muted); }
.stat-value.good { color: var(--success); }
.stat-value.ok { color: var(--warn); }
.stat-value.poor { color: var(--danger); }
.hit-rate-card { gap: 0.75rem; }
.hit-rate-bar {
height: 4px;
background: var(--surface-2);
border-radius: 2px;
overflow: hidden;
margin-top: auto;
}
.hit-rate-fill {
height: 100%;
background: var(--primary);
border-radius: 2px;
transition: width 0.4s ease;
}
.no-stats {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 4rem;
color: var(--text-dim);
}
.no-stats .empty-icon {
font-size: 2.5rem;
opacity: 0.3;
}
.no-stats .empty-label {
font-family: var(--font-serif);
font-size: 1rem;
color: var(--text-muted);
}
</style>