Init
This commit is contained in:
151
resources/js/Components/SystemMonitor.vue
Normal file
151
resources/js/Components/SystemMonitor.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<p class="text-[11px] text-muted leading-relaxed">
|
||||
durable timers · Continue-As-New · heartbeat · signal-driven stop · ~30 min long-running
|
||||
</p>
|
||||
<div class="flex items-center gap-3 text-[10px] text-muted/60">
|
||||
<span class="font-mono">app/Temporal/SystemMonitor/</span>
|
||||
<a href="https://docs.temporal.io/develop/php/timers#timers" target="_blank" class="text-accent/50 hover:text-accent transition-colors">Workflow::timer()</a>
|
||||
<a href="https://docs.temporal.io/develop/php/continue-as-new" target="_blank" class="text-accent/50 hover:text-accent transition-colors">Continue-As-New</a>
|
||||
<a href="https://docs.temporal.io/develop/php/message-passing#signals" target="_blank" class="text-accent/50 hover:text-accent transition-colors">signals</a>
|
||||
</div>
|
||||
|
||||
<SimulationControls v-model="simulation" />
|
||||
|
||||
<!-- Config sliders -->
|
||||
<div class="flex items-center gap-4 text-[10px] text-muted">
|
||||
<label class="flex items-center gap-1.5">
|
||||
<span class="text-label">Interval</span>
|
||||
<input type="range" v-model.number="intervalSeconds" min="5" max="120" step="5" class="w-20 accent-accent">
|
||||
<span class="text-value w-8 text-right">{{ intervalSeconds }}s</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5">
|
||||
<span class="text-label">Iterations</span>
|
||||
<input type="range" v-model.number="maxIterations" min="3" max="60" step="1" class="w-20 accent-accent">
|
||||
<span class="text-value w-6 text-right">{{ maxIterations }}</span>
|
||||
</label>
|
||||
<span class="text-muted/50">~{{ Math.round(intervalSeconds * maxIterations / 60) }} min</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="startMonitor"
|
||||
class="px-3 py-1 text-[11px] font-medium uppercase tracking-wider bg-accent/15 text-accent border border-accent/30 hover:bg-accent/25 hover:border-accent/50 transition-all rounded-sm cursor-pointer">
|
||||
Start Monitor
|
||||
</button>
|
||||
<button v-if="workflowId && !isTerminal" @click="stopMonitor"
|
||||
class="px-2 py-0.5 text-[10px] uppercase tracking-wider text-danger border border-danger/30 hover:bg-danger/10 transition-all rounded-sm cursor-pointer">
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="workflowId" class="bg-deep/50 border border-border rounded-sm p-3 space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full"
|
||||
:class="isTerminal ? 'bg-accent' : 'bg-accent animate-pulse-dot'"></span>
|
||||
<span class="text-[11px] text-value">{{ status.status || 'starting' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Iteration + health score -->
|
||||
<div class="flex items-center gap-4 text-[10px] text-muted">
|
||||
<span>iteration <span class="text-value">{{ status.iteration || 0 }}</span> / {{ status.totalIterations || maxIterations }}</span>
|
||||
<span>health <span class="font-medium" :class="scoreColor">{{ status.healthScore ?? '—' }}</span></span>
|
||||
<span v-if="status.startedAt">started <span class="text-value">{{ formatTime(status.startedAt) }}</span></span>
|
||||
<span v-if="status.lastCheckAt">last check <span class="text-value">{{ formatTime(status.lastCheckAt) }}</span></span>
|
||||
</div>
|
||||
|
||||
<!-- Health score bar -->
|
||||
<div class="h-1.5 bg-border/30 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500"
|
||||
:class="scoreBarColor"
|
||||
:style="{ width: (status.healthScore ?? 100) + '%' }">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check history -->
|
||||
<div v-if="(status.checkHistory || []).length" class="space-y-1 pt-1 border-t border-border/30">
|
||||
<div class="text-[10px] text-muted uppercase tracking-wider">Check History (last {{ status.checkHistory.length }})</div>
|
||||
<div v-for="(check, i) in [...(status.checkHistory || [])].reverse()" :key="i"
|
||||
class="flex items-center gap-3 text-[10px] border-b border-border/10 py-0.5">
|
||||
<span class="text-muted w-28">{{ formatTime(check.timestamp) }}</span>
|
||||
<span class="font-medium w-6 text-right" :class="check.healthScore >= 80 ? 'text-accent' : check.healthScore >= 50 ? 'text-warn' : 'text-danger'">{{ check.healthScore }}</span>
|
||||
<span v-if="check.checks" class="text-muted/60">
|
||||
{{ check.checks.product_count || 0 }} products ·
|
||||
{{ check.checks.order_count || 0 }} orders ·
|
||||
{{ check.checks.out_of_stock_products || 0 }} OOS
|
||||
</span>
|
||||
<span v-if="check.attempt > 1" class="text-warn text-[9px]">attempt {{ check.attempt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { usePolling } from '../composables/usePolling.js';
|
||||
import SimulationControls from './SimulationControls.vue';
|
||||
|
||||
const simulation = ref({
|
||||
failureRate: 0,
|
||||
latencyMs: 0,
|
||||
rateLimiting: { enabled: false, hitChance: 25, retryAfterMs: 2000 },
|
||||
});
|
||||
|
||||
const intervalSeconds = ref(60);
|
||||
const maxIterations = ref(30);
|
||||
const workflowId = ref(null);
|
||||
const status = ref({});
|
||||
|
||||
const isTerminal = computed(() =>
|
||||
['completed', 'stopped', 'failed'].includes(status.value.status) || !!status.value.error
|
||||
);
|
||||
|
||||
const scoreColor = computed(() => {
|
||||
const s = status.value.healthScore ?? 100;
|
||||
if (s >= 80) return 'text-accent';
|
||||
if (s >= 50) return 'text-warn';
|
||||
return 'text-danger';
|
||||
});
|
||||
|
||||
const scoreBarColor = computed(() => {
|
||||
const s = status.value.healthScore ?? 100;
|
||||
if (s >= 80) return 'bg-accent';
|
||||
if (s >= 50) return 'bg-warn';
|
||||
return 'bg-danger';
|
||||
});
|
||||
|
||||
function formatTime(iso) {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
return new Date(iso).toLocaleTimeString();
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
const { start: startPolling } = usePolling(async () => {
|
||||
const { data } = await axios.get(`/temporal/system-monitor/${workflowId.value}/status`);
|
||||
if (data.error && !data.status) {
|
||||
status.value = { ...status.value, status: 'completed' };
|
||||
return true;
|
||||
}
|
||||
status.value = data;
|
||||
return isTerminal.value;
|
||||
}, 3000);
|
||||
|
||||
async function startMonitor() {
|
||||
const { data } = await axios.post('/temporal/system-monitor/start', {
|
||||
interval_seconds: intervalSeconds.value,
|
||||
max_iterations: maxIterations.value,
|
||||
simulation: simulation.value,
|
||||
});
|
||||
workflowId.value = data.workflow_id;
|
||||
status.value = { status: 'starting' };
|
||||
startPolling();
|
||||
}
|
||||
|
||||
async function stopMonitor() {
|
||||
await axios.post(`/temporal/system-monitor/${workflowId.value}/stop`);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user