- Fix TEST_ORACLE_USER quotes in .env.test for shell source compatibility - Format 14 frontend files with Prettier (stores, views, utils) - All 122 tests passing (77 telegram + 35 backend + 10 E2E) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
450 lines
11 KiB
Vue
450 lines
11 KiB
Vue
<template>
|
|
<div class="cache-stats-view">
|
|
<div class="stats-header">
|
|
<h1>Cache Statistics</h1>
|
|
<div class="actions">
|
|
<Button
|
|
label="Clear Cache"
|
|
icon="pi pi-trash"
|
|
severity="danger"
|
|
@click="showClearDialog = true"
|
|
:loading="loading"
|
|
/>
|
|
<Button
|
|
label="Refresh"
|
|
icon="pi pi-refresh"
|
|
@click="loadStats"
|
|
:loading="loading"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Message v-if="error" severity="error" :closable="true" @close="clearError">
|
|
{{ error }}
|
|
</Message>
|
|
|
|
<div v-if="!loading && stats" class="stats-grid">
|
|
<!-- Cache Status -->
|
|
<Card class="status-card">
|
|
<template #title>Cache Status</template>
|
|
<template #content>
|
|
<div class="status-content">
|
|
<div class="status-item">
|
|
<label>Global Status:</label>
|
|
<Tag
|
|
:value="stats.global_enabled ? 'ENABLED' : 'DISABLED'"
|
|
:severity="stats.global_enabled ? 'success' : 'danger'"
|
|
/>
|
|
</div>
|
|
<div class="status-item">
|
|
<label>Your Setting:</label>
|
|
<InputSwitch
|
|
v-model="userCacheEnabled"
|
|
@change="toggleUserCache"
|
|
/>
|
|
<span>{{ userCacheEnabled ? "ON" : "OFF" }}</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<label>Auto-Invalidation:</label>
|
|
<Tag
|
|
:value="stats.auto_invalidate ? 'ENABLED' : 'DISABLED'"
|
|
:severity="stats.auto_invalidate ? 'success' : 'warning'"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Performance Metrics -->
|
|
<Card class="metrics-card">
|
|
<template #title>Performance Metrics</template>
|
|
<template #content>
|
|
<div class="hit-rate">
|
|
<h3>Hit Rate: {{ stats.hit_rate?.toFixed(1) }}%</h3>
|
|
<p>
|
|
{{ stats.total_hits }} hits /
|
|
{{ stats.total_hits + stats.total_misses }} total requests
|
|
</p>
|
|
<ProgressBar :value="stats.hit_rate" />
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Queries Saved -->
|
|
<Card class="queries-card">
|
|
<template #title>Queries Saved</template>
|
|
<template #content>
|
|
<ul class="queries-list">
|
|
<li>
|
|
Today:
|
|
<strong>{{
|
|
stats.queries_saved?.today?.toLocaleString()
|
|
}}</strong>
|
|
queries avoided
|
|
</li>
|
|
<li>
|
|
This week:
|
|
<strong>{{ stats.queries_saved?.week?.toLocaleString() }}</strong>
|
|
queries avoided
|
|
</li>
|
|
<li>
|
|
All time:
|
|
<strong>{{
|
|
stats.queries_saved?.total?.toLocaleString()
|
|
}}</strong>
|
|
queries avoided
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Response Times -->
|
|
<Card class="response-times-card">
|
|
<template #title>Response Time Comparison</template>
|
|
<template #content>
|
|
<DataTable :value="responseTimesTable" class="p-datatable-sm">
|
|
<Column field="endpoint" header="Endpoint" />
|
|
<Column field="cached" header="With Cache">
|
|
<template #body="{ data }">{{ data.cached }} ms</template>
|
|
</Column>
|
|
<Column field="oracle" header="Without Cache">
|
|
<template #body="{ data }">{{ data.oracle }} ms</template>
|
|
</Column>
|
|
<Column field="improvement" header="Improvement">
|
|
<template #body="{ data }">
|
|
<Tag :value="`${data.improvement}% ↓`" severity="success" />
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
<div v-if="overallAvg" class="average-row">
|
|
<strong>Overall Average:</strong>
|
|
{{ overallAvg.cached }} ms vs {{ overallAvg.oracle }} ms ({{
|
|
overallAvg.improvement
|
|
}}% faster)
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Cache Details -->
|
|
<Card class="details-card">
|
|
<template #title>Cache Details</template>
|
|
<template #content>
|
|
<ul class="details-list">
|
|
<li>
|
|
Memory entries:
|
|
<strong>{{ stats.cache_size?.memory?.toLocaleString() }}</strong>
|
|
</li>
|
|
<li>
|
|
SQLite entries:
|
|
<strong>{{ stats.cache_size?.sqlite?.toLocaleString() }}</strong>
|
|
</li>
|
|
<li>
|
|
Cache type: <strong>{{ stats.cache_type }}</strong>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Clear Cache Dialog -->
|
|
<Dialog
|
|
v-model:visible="showClearDialog"
|
|
header="Clear Cache"
|
|
:modal="true"
|
|
:style="{ width: '450px' }"
|
|
>
|
|
<p>Are you sure you want to clear the cache?</p>
|
|
<div class="clear-options">
|
|
<div class="p-field-radiobutton">
|
|
<RadioButton id="clear_all" v-model="clearScope" value="all" />
|
|
<label for="clear_all">All companies</label>
|
|
</div>
|
|
<div class="p-field-radiobutton">
|
|
<RadioButton
|
|
id="clear_current"
|
|
v-model="clearScope"
|
|
value="current"
|
|
/>
|
|
<label for="clear_current">Current company only</label>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<Button label="Cancel" text @click="showClearDialog = false" />
|
|
<Button
|
|
label="Clear"
|
|
severity="danger"
|
|
@click="clearCache"
|
|
:loading="loading"
|
|
/>
|
|
</template>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from "vue";
|
|
import { useCacheStore } from "@/stores/cacheStore";
|
|
import { useCompanyStore } from "@/stores/companies";
|
|
import { useToast } from "primevue/usetoast";
|
|
import Button from "primevue/button";
|
|
import Card from "primevue/card";
|
|
import DataTable from "primevue/datatable";
|
|
import Column from "primevue/column";
|
|
import Tag from "primevue/tag";
|
|
import ProgressBar from "primevue/progressbar";
|
|
import InputSwitch from "primevue/inputswitch";
|
|
import Dialog from "primevue/dialog";
|
|
import RadioButton from "primevue/radiobutton";
|
|
import Message from "primevue/message";
|
|
|
|
const cacheStore = useCacheStore();
|
|
const companyStore = useCompanyStore();
|
|
const toast = useToast();
|
|
|
|
const loading = computed(() => cacheStore.isLoading);
|
|
const error = computed(() => cacheStore.error);
|
|
const stats = computed(() => cacheStore.stats);
|
|
|
|
const userCacheEnabled = ref(true);
|
|
const showClearDialog = ref(false);
|
|
const clearScope = ref("current");
|
|
|
|
const responseTimesTable = computed(() => {
|
|
if (!stats.value?.response_times) return [];
|
|
|
|
return Object.entries(stats.value.response_times).map(([key, data]) => ({
|
|
endpoint: formatEndpointName(key),
|
|
cached: data.cached,
|
|
oracle: data.oracle,
|
|
improvement: data.improvement,
|
|
}));
|
|
});
|
|
|
|
const overallAvg = computed(() => {
|
|
const times = Object.values(stats.value?.response_times || {});
|
|
if (times.length === 0) return null;
|
|
|
|
const avgCached = times.reduce((sum, t) => sum + t.cached, 0) / times.length;
|
|
const avgOracle = times.reduce((sum, t) => sum + t.oracle, 0) / times.length;
|
|
const improvement = (((avgOracle - avgCached) / avgOracle) * 100).toFixed(0);
|
|
|
|
return {
|
|
cached: avgCached.toFixed(0),
|
|
oracle: avgOracle.toFixed(0),
|
|
improvement,
|
|
};
|
|
});
|
|
|
|
async function loadStats() {
|
|
try {
|
|
await cacheStore.getStats();
|
|
userCacheEnabled.value = stats.value?.user_enabled ?? true;
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error",
|
|
detail: "Failed to load cache statistics",
|
|
life: 3000,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function toggleUserCache() {
|
|
try {
|
|
await cacheStore.toggleUserCache(userCacheEnabled.value);
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Success",
|
|
detail: `Cache ${userCacheEnabled.value ? "enabled" : "disabled"} for you`,
|
|
life: 3000,
|
|
});
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error",
|
|
detail: "Failed to toggle cache",
|
|
life: 3000,
|
|
});
|
|
// Revert toggle
|
|
userCacheEnabled.value = !userCacheEnabled.value;
|
|
}
|
|
}
|
|
|
|
async function clearCache() {
|
|
try {
|
|
const companyId =
|
|
clearScope.value === "current"
|
|
? companyStore.currentCompany?.id_firma
|
|
: null;
|
|
await cacheStore.invalidateCache(companyId, null);
|
|
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Success",
|
|
detail: "Cache cleared successfully",
|
|
life: 3000,
|
|
});
|
|
|
|
showClearDialog.value = false;
|
|
await loadStats();
|
|
} catch (error) {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error",
|
|
detail: "Failed to clear cache",
|
|
life: 3000,
|
|
});
|
|
}
|
|
}
|
|
|
|
function formatEndpointName(key) {
|
|
const names = {
|
|
schema: "Schema Lookup",
|
|
dashboard_summary: "Dashboard",
|
|
dashboard_trends: "Dashboard Trends",
|
|
companies: "Companies List",
|
|
invoices: "Invoices",
|
|
treasury: "Treasury",
|
|
};
|
|
return names[key] || key;
|
|
}
|
|
|
|
function clearError() {
|
|
cacheStore.clearError();
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadStats();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Container - Uses global .app-container pattern */
|
|
.cache-stats-view {
|
|
padding: 2rem;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Header - Uses global .page-header pattern */
|
|
.stats-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stats-header h1 {
|
|
margin: 0;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.status-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.status-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.status-item label {
|
|
font-weight: 600;
|
|
min-width: 140px;
|
|
}
|
|
|
|
/* Hit Rate - Uses global metric patterns */
|
|
.hit-rate {
|
|
text-align: center;
|
|
}
|
|
|
|
.hit-rate h3 {
|
|
margin: 0 0 0.5rem 0;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.hit-rate p {
|
|
margin: 0 0 1rem 0;
|
|
color: var(--text-color-secondary);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.queries-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.queries-list li {
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid var(--surface-border);
|
|
}
|
|
|
|
.queries-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.average-row {
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 2px solid var(--surface-border);
|
|
text-align: center;
|
|
}
|
|
|
|
.details-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.details-list li {
|
|
padding: 0.5rem 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.clear-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.p-field-radiobutton {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.response-times-card {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
/* Responsive - Cache stats specific adjustments */
|
|
@media (max-width: 768px) {
|
|
.stats-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|