monacousa-portal/src/routes/(app)/admin/cron-monitoring/+page.svelte

242 lines
7.8 KiB
Svelte
Raw Normal View History

<script lang="ts">
import { Timer, Play, CheckCircle, XCircle, Clock, RefreshCw, AlertTriangle } from 'lucide-svelte';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
let { data, form } = $props();
const cronLogs = $derived(data.cronLogs);
let runningJob = $state<string | null>(null);
const cronJobs = [
{
id: 'dues-reminders',
name: 'Dues Reminders',
description: 'Send payment reminders to members with upcoming or overdue dues',
schedule: 'Daily at 9:00 AM',
action: '?/runDuesReminders'
},
{
id: 'event-reminders',
name: 'Event Reminders',
description: 'Send reminder emails 24 hours before events',
schedule: 'Hourly',
action: '?/runEventReminders'
}
];
function getStatusBadge(status: string) {
switch (status) {
case 'completed':
return { icon: CheckCircle, color: 'text-green-600', bg: 'bg-green-50', label: 'Completed' };
case 'failed':
return { icon: XCircle, color: 'text-red-600', bg: 'bg-red-50', label: 'Failed' };
case 'running':
return { icon: RefreshCw, color: 'text-blue-600', bg: 'bg-blue-50', label: 'Running' };
default:
return { icon: Clock, color: 'text-slate-500', bg: 'bg-slate-50', label: status };
}
}
function formatDuration(ms: number | null): string {
if (!ms) return '-';
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function getLastRun(jobName: string) {
return cronLogs.find((log: any) => log.job_name === jobName);
}
</script>
<svelte:head>
<title>Cron Monitoring | Monaco USA</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-slate-900">Cron Job Monitoring</h1>
<p class="text-slate-500">Monitor and manually trigger scheduled tasks</p>
</div>
{#if form?.error}
<div class="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
<div class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4" />
{form.error}
</div>
</div>
{/if}
{#if form?.success}
<div class="rounded-lg bg-green-50 border border-green-200 p-4 text-sm text-green-600">
<div class="flex items-center gap-2">
<CheckCircle class="h-4 w-4" />
{form.success}
</div>
</div>
{/if}
<!-- Cron Jobs -->
<div class="grid gap-4 sm:grid-cols-2">
{#each cronJobs as job}
{@const lastRun = getLastRun(job.id)}
{@const lastStatus = lastRun ? getStatusBadge(lastRun.status) : null}
<div class="glass-card p-6">
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-monaco-100 p-2">
<Timer class="h-5 w-5 text-monaco-600" />
</div>
<div>
<h3 class="font-semibold text-slate-900">{job.name}</h3>
<p class="text-sm text-slate-500">{job.description}</p>
</div>
</div>
</div>
<div class="mt-4 space-y-2 text-sm">
<div class="flex items-center justify-between">
<span class="text-slate-500">Schedule:</span>
<span class="font-medium text-slate-700">{job.schedule}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-500">Last Run:</span>
{#if lastRun}
<span class="font-medium text-slate-700">{formatDate(lastRun.started_at)}</span>
{:else}
<span class="text-slate-400">Never</span>
{/if}
</div>
{#if lastStatus}
{@const StatusIcon = lastStatus.icon}
<div class="flex items-center justify-between">
<span class="text-slate-500">Status:</span>
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium {lastStatus.bg} {lastStatus.color}">
<StatusIcon class="h-3 w-3" />
{lastStatus.label}
</span>
</div>
{/if}
{#if lastRun?.duration_ms}
<div class="flex items-center justify-between">
<span class="text-slate-500">Duration:</span>
<span class="font-medium text-slate-700">{formatDuration(lastRun.duration_ms)}</span>
</div>
{/if}
</div>
<div class="mt-4 border-t border-slate-100 pt-4">
<form
method="POST"
action={job.action}
use:enhance={() => {
runningJob = job.id;
return async ({ update }) => {
await invalidateAll();
await update();
runningJob = null;
};
}}
>
<button
type="submit"
disabled={runningJob !== null}
class="flex w-full items-center justify-center gap-2 rounded-lg bg-monaco-600 px-4 py-2 text-sm font-medium text-white hover:bg-monaco-700 disabled:opacity-50"
>
{#if runningJob === job.id}
<RefreshCw class="h-4 w-4 animate-spin" />
Running...
{:else}
<Play class="h-4 w-4" />
Run Now
{/if}
</button>
</form>
</div>
</div>
{/each}
</div>
<!-- Execution History -->
<div class="glass-card p-6">
<h2 class="mb-4 text-lg font-semibold text-slate-900">Execution History</h2>
{#if cronLogs.length === 0}
<div class="py-12 text-center">
<Timer class="mx-auto h-12 w-12 text-slate-300" />
<p class="mt-4 text-slate-500">No cron executions recorded yet.</p>
<p class="mt-1 text-sm text-slate-400">Run a job above or wait for scheduled execution.</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-200 text-left">
<th class="pb-3 pr-4 font-medium text-slate-600">Job</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Status</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Started</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Duration</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Triggered By</th>
<th class="pb-3 font-medium text-slate-600">Details</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{#each cronLogs as log}
{@const statusBadge = getStatusBadge(log.status)}
{@const BadgeIcon = statusBadge.icon}
<tr class="hover:bg-slate-50">
<td class="py-3 pr-4 font-medium text-slate-900 whitespace-nowrap">
{log.job_name}
</td>
<td class="py-3 pr-4">
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium {statusBadge.bg} {statusBadge.color}">
<BadgeIcon class="h-3 w-3" />
{statusBadge.label}
</span>
</td>
<td class="py-3 pr-4 text-slate-500 whitespace-nowrap">
{formatDate(log.started_at)}
</td>
<td class="py-3 pr-4 text-slate-500">
{formatDuration(log.duration_ms)}
</td>
<td class="py-3 pr-4">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {log.triggered_by === 'admin-manual' ? 'bg-purple-50 text-purple-700' : 'bg-slate-100 text-slate-600'}">
{log.triggered_by === 'admin-manual' ? 'Manual' : 'Scheduled'}
</span>
</td>
<td class="py-3 text-slate-500">
{#if log.error_message}
<span class="text-red-600 text-xs">{log.error_message}</span>
{:else if log.result?.summary}
<span class="text-xs">
Sent: {log.result.summary.totalRemindersSent || 0},
Errors: {log.result.summary.totalErrors || 0}
</span>
{:else if log.result?.sent !== undefined}
<span class="text-xs">Sent: {log.result.sent}</span>
{:else}
<span class="text-slate-400">-</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>