aecedd9fe4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3.5 KiB
Vue
110 lines
3.5 KiB
Vue
<template>
|
|
<v-table density="compact" class="fc-backup-table">
|
|
<thead>
|
|
<tr>
|
|
<th>When</th>
|
|
<th>Status</th>
|
|
<th class="text-right">Duration</th>
|
|
<th class="text-right">Size</th>
|
|
<th>Tag</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="r in runs" :key="r.id">
|
|
<td class="fc-tabular" :title="r.started_at">
|
|
{{ formatRelative(r.started_at) }}
|
|
</td>
|
|
<td>
|
|
<v-icon size="small" :color="statusColor(r.status)">
|
|
{{ statusIcon(r.status) }}
|
|
</v-icon>
|
|
{{ r.status }}
|
|
</td>
|
|
<td class="text-right fc-tabular">{{ formatDuration(r) }}</td>
|
|
<td class="text-right fc-tabular">{{ formatBytes(r.size_bytes) }}</td>
|
|
<td>
|
|
<v-chip
|
|
v-if="r.tag" size="x-small" color="accent" variant="tonal"
|
|
>{{ r.tag }}</v-chip>
|
|
<span v-else class="fc-muted">—</span>
|
|
</td>
|
|
<td class="text-right">
|
|
<v-menu>
|
|
<template #activator="{ props: act }">
|
|
<v-btn icon="mdi-dots-vertical" size="x-small" variant="text" v-bind="act" />
|
|
</template>
|
|
<v-list density="compact">
|
|
<v-list-item
|
|
:title="r.tag ? 'Untag' : 'Tag…'"
|
|
@click="$emit('tag', r)"
|
|
/>
|
|
<v-list-item
|
|
title="Restore…" :disabled="r.status !== 'ok'"
|
|
@click="$emit('restore', r)"
|
|
/>
|
|
<v-list-item
|
|
title="Delete…"
|
|
@click="$emit('delete', r)"
|
|
/>
|
|
</v-list>
|
|
</v-menu>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!runs.length">
|
|
<td colspan="6" class="text-center fc-muted py-4">No backups yet.</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</template>
|
|
|
|
<script setup>
|
|
defineProps({ runs: { type: Array, default: () => [] } })
|
|
defineEmits(['restore', 'delete', 'tag'])
|
|
|
|
function statusIcon(s) {
|
|
return {
|
|
ok: 'mdi-check-circle', error: 'mdi-close-circle',
|
|
running: 'mdi-timer-sand', restoring: 'mdi-restore-clock',
|
|
restored: 'mdi-restore', pending: 'mdi-clock-outline',
|
|
}[s] || 'mdi-help-circle'
|
|
}
|
|
function statusColor(s) {
|
|
return {
|
|
ok: 'success', error: 'error',
|
|
running: 'accent', restoring: 'info',
|
|
restored: 'info', pending: 'on-surface-variant',
|
|
}[s] || 'on-surface-variant'
|
|
}
|
|
function formatDuration(r) {
|
|
if (r.duration_seconds == null) return '—'
|
|
const s = r.duration_seconds
|
|
if (s < 60) return `${s}s`
|
|
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
|
|
const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60)
|
|
return `${h}h ${m}m`
|
|
}
|
|
function formatBytes(b) {
|
|
if (b == null) return '—'
|
|
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
|
|
let i = 0; let v = b
|
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
|
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
|
|
}
|
|
function formatRelative(iso) {
|
|
if (!iso) return '—'
|
|
const then = new Date(iso).getTime()
|
|
const diff = Math.max(0, (Date.now() - then) / 1000)
|
|
if (diff < 60) return `${Math.floor(diff)}s ago`
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
|
return `${Math.floor(diff / 86400)}d ago`
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.fc-backup-table { background: transparent; }
|
|
.fc-tabular { font-variant-numeric: tabular-nums; }
|
|
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
|
</style>
|