All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Final re-skin batch: admin ops (Console/FileManager[VueFinder preserved]/WipeCalendar/WipeHistory/Changelog/Migration), platform-admin (Dashboard/Licenses/Servers/Subscriptions/Users), public product pages (ServerInfo/StatusPage/StoreView) + PublicLayout, WarpEditor, ErrorBoundary. All logic/store/router/WebSocket/handlers preserved. Marketing views (Landing/Pricing/FAQ/HowItWorks/Roadmap/EarlyAccess + MarketingLayout) intentionally deferred to the dedicated marketing-site redesign. Build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
243 lines
7.0 KiB
Vue
243 lines
7.0 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useWipeStore } from '@/stores/wipe'
|
|
import Panel from '@/components/ds/data/Panel.vue'
|
|
import Button from '@/components/ds/core/Button.vue'
|
|
import Badge from '@/components/ds/core/Badge.vue'
|
|
import Icon from '@/components/ds/core/Icon.vue'
|
|
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
|
|
|
|
const wipeStore = useWipeStore()
|
|
|
|
const currentMonth = ref(new Date())
|
|
|
|
const monthLabel = computed(() => {
|
|
return currentMonth.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
|
|
})
|
|
|
|
const calendarDays = computed(() => {
|
|
const year = currentMonth.value.getFullYear()
|
|
const month = currentMonth.value.getMonth()
|
|
const firstDay = new Date(year, month, 1).getDay()
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate()
|
|
|
|
const days: { date: number; inMonth: boolean; hasWipe: boolean; wipeType?: string }[] = []
|
|
|
|
// Padding before
|
|
for (let i = 0; i < firstDay; i++) {
|
|
days.push({ date: 0, inMonth: false, hasWipe: false })
|
|
}
|
|
|
|
// Days of month
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
const wipe = wipeStore.history.find(w =>
|
|
w.started_at?.startsWith(dateStr) || w.completed_at?.startsWith(dateStr)
|
|
)
|
|
days.push({
|
|
date: d,
|
|
inMonth: true,
|
|
hasWipe: !!wipe,
|
|
wipeType: wipe?.wipe_type,
|
|
})
|
|
}
|
|
|
|
return days
|
|
})
|
|
|
|
function prevMonth() {
|
|
const d = new Date(currentMonth.value)
|
|
d.setMonth(d.getMonth() - 1)
|
|
currentMonth.value = d
|
|
}
|
|
|
|
function nextMonth() {
|
|
const d = new Date(currentMonth.value)
|
|
d.setMonth(d.getMonth() + 1)
|
|
currentMonth.value = d
|
|
}
|
|
|
|
const activeSchedules = computed(() => wipeStore.schedules.filter(s => s.is_active))
|
|
|
|
onMounted(() => {
|
|
wipeStore.fetchHistory()
|
|
wipeStore.fetchSchedules()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="wc">
|
|
<!-- Page head -->
|
|
<div class="wc__head">
|
|
<div class="wc__head-id">
|
|
<div class="wc__head-chip">
|
|
<Icon name="calendar" :size="20" :stroke-width="2" />
|
|
</div>
|
|
<div>
|
|
<div class="t-eyebrow">Auto-wiper</div>
|
|
<h1 class="wc__title">Wipe calendar</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendar panel -->
|
|
<Panel :flush-body="true" title="Wipe calendar">
|
|
<template #actions>
|
|
<Button variant="ghost" size="sm" icon="chevron-left" @click="prevMonth" />
|
|
<span class="wc__month-label">{{ monthLabel }}</span>
|
|
<Button variant="ghost" size="sm" icon="chevron-right" @click="nextMonth" />
|
|
</template>
|
|
|
|
<!-- Day-of-week headers -->
|
|
<div class="wc__dow-row">
|
|
<div
|
|
v-for="day in ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']"
|
|
:key="day"
|
|
class="wc__dow"
|
|
>{{ day }}</div>
|
|
</div>
|
|
|
|
<!-- Day cells -->
|
|
<div class="wc__grid">
|
|
<div
|
|
v-for="(day, i) in calendarDays"
|
|
:key="i"
|
|
class="wc__cell"
|
|
:class="{ 'wc__cell--out': !day.inMonth }"
|
|
>
|
|
<template v-if="day.inMonth">
|
|
<span class="wc__date" :class="{ 'wc__date--wipe': day.hasWipe }">{{ day.date }}</span>
|
|
<Badge
|
|
v-if="day.hasWipe"
|
|
tone="warn"
|
|
size="md"
|
|
class="wc__wipe-badge"
|
|
>{{ day.wipeType }}</Badge>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</Panel>
|
|
|
|
<!-- Active schedules panel -->
|
|
<Panel title="Active schedules" subtitle="Cron schedules currently enabled">
|
|
<EmptyState
|
|
v-if="activeSchedules.length === 0"
|
|
icon="calendar-clock"
|
|
title="No active schedules"
|
|
description="Activate a schedule in the auto-wiper to see it here."
|
|
/>
|
|
<div v-else class="wc__sched-list">
|
|
<div
|
|
v-for="schedule in activeSchedules"
|
|
:key="schedule.id"
|
|
class="wc__sched-row"
|
|
>
|
|
<div class="wc__sched-info">
|
|
<div class="wc__sched-name">{{ schedule.schedule_name }}</div>
|
|
<div class="wc__sched-meta">
|
|
<span class="wc__mono">{{ schedule.cron_expression }}</span>
|
|
· {{ schedule.timezone }}
|
|
</div>
|
|
</div>
|
|
<div class="wc__sched-next">
|
|
Next:
|
|
<span class="wc__mono">
|
|
{{ schedule.next_scheduled_run ? new Date(schedule.next_scheduled_run).toLocaleDateString() : 'TBD' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.wc {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* Page head */
|
|
.wc__head { display: flex; align-items: center; }
|
|
.wc__head-id { display: flex; align-items: center; gap: 12px; }
|
|
.wc__head-chip {
|
|
width: 40px; height: 40px; flex: none; border-radius: var(--radius-md);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--accent); background: var(--accent-soft);
|
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
|
}
|
|
.wc__title {
|
|
font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.02em;
|
|
color: var(--text-primary); margin-top: 3px;
|
|
}
|
|
.wc__month-label {
|
|
font-size: var(--text-sm); font-weight: 600; color: var(--text-primary);
|
|
padding: 0 4px; white-space: nowrap;
|
|
}
|
|
|
|
/* Calendar grid */
|
|
.wc__dow-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
background: var(--surface-inset);
|
|
}
|
|
.wc__dow {
|
|
padding: 8px 0;
|
|
text-align: center;
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
color: var(--text-tertiary);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--tracking-wider);
|
|
}
|
|
|
|
.wc__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
}
|
|
.wc__cell {
|
|
min-height: 80px;
|
|
padding: 8px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
border-right: 1px solid var(--border-subtle);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.wc__cell:nth-child(7n) { border-right: 0; }
|
|
.wc__cell--out { background: var(--surface-inset); }
|
|
|
|
.wc__date {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-tertiary);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.wc__date--wipe { color: var(--accent-text); font-weight: 700; }
|
|
.wc__wipe-badge { align-self: flex-start; }
|
|
|
|
/* Active schedules list */
|
|
.wc__sched-list { display: flex; flex-direction: column; }
|
|
.wc__sched-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 2px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
.wc__sched-row:last-child { border-bottom: 0; }
|
|
.wc__sched-info { flex: 1; min-width: 0; }
|
|
.wc__sched-name { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
|
|
.wc__sched-meta { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
|
|
.wc__sched-next { font-size: var(--text-xs); color: var(--text-tertiary); flex: none; }
|
|
.wc__mono { font-family: var(--font-mono); }
|
|
|
|
@media (max-width: 600px) {
|
|
.wc__cell { min-height: 52px; padding: 4px; }
|
|
}
|
|
</style>
|