<script setup>
// Список целей каналов. Config (endpoint, chat_id, ...) — на странице цели,
// там он читаем и подсвечен; секреты только в .env.
import { onMounted, ref } from "vue";
import {
GnButton,
GnConfirmDialog,
GnInput,
GnLoader,
GnModal,
GnPageHeader,
GnSelect,
GnStatusCard,
GnTable,
GnTextarea,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
import { useRouter } from "vue-router";
import { api } from "../api.js";
import { badgeChannel, fmtTime, th } from "../ui.js";
const toast = useToast();
const router = useRouter();
const rows = ref([]);
const loading = ref(true);
const error = ref("");
const CHANNELS = ["telegram", "email", "s2s", "internal_log"];
async function load() {
loading.value = true;
error.value = "";
try {
rows.value = await api.listTargets();
} catch (ex) {
error.value = ex.message;
} finally {
loading.value = false;
}
}
const showCreate = ref(false);
const form = ref({ channel: "telegram", name: "", description: "", config_text: "{}" });
const busy = ref(false);
const formError = ref("");
async function createTarget() {
formError.value = "";
let config = {};
try {
config = JSON.parse(form.value.config_text || "{}");
} catch {
formError.value = "Config — не корректный JSON";
return;
}
busy.value = true;
try {
await api.createTarget({
channel: form.value.channel,
name: form.value.name.trim(),
description: form.value.description,
config,
});
showCreate.value = false;
form.value = { channel: "telegram", name: "", description: "", config_text: "{}" };
toast.success("Цель создана");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
busy.value = false;
}
}
const confirmDelete = ref(null);
async function doDelete() {
try {
await api.deleteTarget(confirmDelete.value.id);
toast.success("Цель удалена");
await load();
} catch (ex) {
toast.error(ex.message);
} finally {
confirmDelete.value = null;
}
}
onMounted(load);
</script>
<template>
<main class="container page">
<GnPageHeader
kicker="Synapse"
title="Цели"
subtitle="Куда доставлять: TG-чаты, SMTP, s2s-точки. Креды каналов только в .env (gnexus-creds)"
>
<template #actions>
<GnButton variant="primary" icon="ph-plus" @click="showCreate = true">Цель</GnButton>
</template>
</GnPageHeader>
<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="Целей нет — например, создайте цель для internal_log"
:columns="[
{ key: 'channel', label: 'Канал' },
{ key: 'name', label: 'Название' },
{ key: 'created_at', label: 'Создана' },
{ key: 'actions', label: '' },
]"
>
<template #cell-channel="{ value }">
<GnBadge :variant="badgeChannel(value)" outline>{{ value }}</GnBadge>
</template>
<template #cell-created_at="{ value }">{{ fmtTime(value) }}</template>
<template #cell-actions="{ row }">
<span class="row-actions">
<GnIconButton icon="ph-eye" size="sm" label="Открыть" @click="router.push(`/targets/${row.id}`)" />
<GnIconButton icon="ph-trash" size="sm" label="Удалить" @click="confirmDelete = row" />
</span>
</template>
</GnTable>
</template>
<!-- Создание цели -->
<GnModal v-model:open="showCreate" title="Новая цель">
<div class="form">
<GnSelect v-model="form.channel" label="Канал" :options="CHANNELS.map((c) => ({ value: c, label: c }))" />
<GnInput v-model="form.name" label="Название" icon="ph-tag" help="Например infra-alerts" />
<GnTextarea v-model="form.description" label="Описание" help="Для чего эта цель (для ИИ-агента и человека)" />
<GnInput
v-model="form.config_text"
label="Config (JSON)"
icon="ph-code"
help='Для s2s: {"endpoint":"http://…","token_ref":"navi-rei"}; для TG: {"chat_id":"@…"}'
state="error"
:help="formError || undefined"
/>
</div>
<template #actions>
<GnButton variant="secondary" @click="showCreate = false">Отмена</GnButton>
<GnButton variant="primary" :loading="busy" :disabled="!form.name.trim()" @click="createTarget">
Создать
</GnButton>
</template>
</GnModal>
<!-- Удаление цели -->
<GnConfirmDialog
:open="confirmDelete !== null" @update:open="(v) => (v ? null : (confirmDelete = null))"
:key="confirmDelete?.id"
title="Удалить цель?"
:message="confirmDelete ? `Цель «${confirmDelete.name}» (${confirmDelete.channel}) будет удалена. Ссылки из правил и доставок обнулятся, история останется.` : ''"
confirm-variant="danger"
confirm-text="Удалить"
cancel-text="Отмена"
@confirm="doDelete"
/>
</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);
}
.row-actions {
display: flex;
gap: var(--gn-space-2);
}
.form {
display: flex;
flex-direction: column;
gap: var(--gn-space-4);
}
</style>