<script setup>
// Лог доставок: последние N с фильтрами по каналу и статусу.
import { onMounted, ref } from "vue";
import {
GnBadge,
GnIconButton,
GnLoader,
GnPageHeader,
GnSelect,
GnStatusCard,
GnTable,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { api } from "../api.js";
import { badgeFor, fmtTime, th, truncate } from "../ui.js";
const toast = useToast();
const rows = ref([]);
const loading = ref(true);
const error = ref("");
const CHANNELS = [
{ value: "", label: "все" },
{ value: "telegram", label: "telegram" },
{ value: "email", label: "email" },
{ value: "s2s", label: "s2s" },
{ value: "internal_log", label: "internal_log" },
];
const STATUSES = [
{ value: "", label: "все" },
{ value: "pending", label: "pending" },
{ value: "delivered", label: "delivered" },
{ value: "failed", label: "failed" },
{ value: "skipped", label: "skipped" },
];
const channel = ref("");
const status = ref("");
async function load() {
loading.value = true;
error.value = "";
try {
const query = { limit: 100 };
if (channel.value) query.channel = channel.value;
if (status.value) query.status = status.value;
rows.value = await api.listDeliveries(query);
} catch (ex) {
error.value = ex.message;
toast.error(ex.message);
} finally {
loading.value = false;
}
}
onMounted(load);
</script>
<template>
<main class="container page">
<GnPageHeader
kicker="Synapse"
title="Доставки"
subtitle="Лог попыток: канал, цель, статус, ретраи и текст, реально ушедший в канал"
/>
<!-- Фильтры отдельным блоком, с подписями -->
<div class="filters">
<GnSelect v-model="status" label="Статус" :options="STATUSES" @update:model-value="load" />
<GnSelect v-model="channel" label="Канал" :options="CHANNELS" @update:model-value="load" />
<div class="refresh-slot">
<GnIconButton icon="ph-arrow-counter-clockwise" label="Обновить" @click="load" />
</div>
</div>
<GnLoader v-if="loading" />
<GnStatusCard v-else-if="error" variant="error" icon="ph-warning-circle" :text="error" />
<template v-else>
<GnTable
:rows="rows"
empty-text="Доставок ещё не было"
:columns="[
{ key: 'created_at', label: 'Время' },
{ key: 'event', label: th('Событие', 'Идентификатор события, породившего доставку') },
{ key: 'channel', label: 'Канал' },
{ key: 'target_name', label: 'Цель' },
{ key: 'status', label: th('Статус', 'delivered — доставлено; pending — ждёт отправки или ретрая; skipped — отсеял шумодав; failed — попытки кончились') },
{ key: 'attempts', label: th('Попытки', 'Сколько раз пытались отправить (ретраи 30с → 2мин → 10мин → 30мин)') },
{ key: 'last_error', label: 'Ошибка' },
{ key: 'rendered_message', label: 'Сообщение' },
]"
>
<template #cell-created_at="{ value }">{{ fmtTime(value) }}</template>
<template #cell-event="{ row }">
<code class="event-id">{{ truncate(row.event_id, 13) }}</code>
</template>
<template #cell-status="{ value }">
<GnBadge :variant="badgeFor(value).variant" outline>{{ value }}</GnBadge>
</template>
<template #cell-attempts="{ value }">{{ value ?? 0 }}</template>
<template #cell-last_error="{ value }">{{ truncate(value, 60) || "—" }}</template>
<template #cell-rendered_message="{ value }">
<span :title="value || ''">{{ truncate(value, 60) || "—" }}</span>
</template>
</GnTable>
</template>
</main>
</template>
<style scoped>
.page {
max-width: 1440px;
margin-inline: auto;
padding-block: var(--gn-space-6);
display: flex;
flex-direction: column;
gap: var(--gn-space-4);
}
.filters {
display: flex;
align-items: flex-end;
gap: var(--gn-space-4);
}
/* вход селекта сидит на 5px выше низа form-group (margin-bottom подписи
в ките) — тем же отступом поднимаем кнопку, центры совпадают */
.refresh-slot {
margin-bottom: var(--gn-space-5);
}
.event-id {
font-size: 0.78em;
}
</style>