154 lines
3.8 KiB
Vue
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>
|