feat: Telegram settings in DB with UI edit and test send
Store notification_channels (migration 004), effective config DB over env, PUT/test API endpoints, Settings form, and pass db session into ingest notify. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,40 +2,71 @@
|
||||
<div class="settings-page">
|
||||
<h1>Настройки</h1>
|
||||
<p class="settings-intro">
|
||||
Каналы оповещений SAC. Сейчас конфигурация читается из <code>sac-api.env</code> на сервере;
|
||||
редактирование через UI — в разработке (<code>notif-12</code>).
|
||||
Каналы оповещений SAC. Настройки Telegram сохраняются в БД (источник <code>db</code>) или берутся из
|
||||
<code>sac-api.env</code> (<code>env</code>), пока запись не создана.
|
||||
</p>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="success" class="success">{{ success }}</p>
|
||||
<p v-else-if="loading">Загрузка…</p>
|
||||
|
||||
<section v-else-if="data" class="card settings-card">
|
||||
<section v-else class="card settings-card">
|
||||
<h2>Telegram</h2>
|
||||
<dl class="settings-dl">
|
||||
<dt>Включён</dt>
|
||||
<dd>{{ data.telegram.enabled ? "да" : "нет" }}</dd>
|
||||
<dt>Настроен (token + chat)</dt>
|
||||
<dd>{{ data.telegram.configured ? "да" : "нет" }}</dd>
|
||||
<dt>Мин. severity</dt>
|
||||
<dd><code>{{ data.telegram.min_severity }}</code></dd>
|
||||
<dt>Bot token</dt>
|
||||
<dd>{{ data.telegram.bot_token_hint ?? "—" }}</dd>
|
||||
<dt>Chat ID</dt>
|
||||
<dd>{{ data.telegram.chat_id_hint ?? "—" }}</dd>
|
||||
<dt>Источник</dt>
|
||||
<dd><code>{{ data.telegram.source }}</code></dd>
|
||||
</dl>
|
||||
|
||||
<form class="settings-form" @submit.prevent="save">
|
||||
<label class="settings-field">
|
||||
<input v-model="form.enabled" type="checkbox" />
|
||||
Включить Telegram
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
Мин. severity
|
||||
<select v-model="form.min_severity">
|
||||
<option value="info">info</option>
|
||||
<option value="warning">warning</option>
|
||||
<option value="high">high</option>
|
||||
<option value="critical">critical</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
Bot token
|
||||
<input
|
||||
v-model="form.bot_token"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:placeholder="tokenPlaceholder"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
Chat ID
|
||||
<input v-model="form.chat_id" type="text" autocomplete="off" :placeholder="chatPlaceholder" />
|
||||
</label>
|
||||
|
||||
<p v-if="loaded" class="settings-meta">
|
||||
Источник: <code>{{ loaded.source }}</code>
|
||||
· настроен: {{ loaded.configured ? "да" : "нет" }}
|
||||
</p>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button type="submit" :disabled="saving">{{ saving ? "Сохранение…" : "Сохранить" }}</button>
|
||||
<button type="button" class="secondary" :disabled="testing" @click="testTelegram">
|
||||
{{ testing ? "Отправка…" : "Проверить Telegram" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="settings-hint">
|
||||
Для <strong>UseSAC=exclusive</strong> на агентах задайте
|
||||
<code>TELEGRAM_ENABLED=true</code> и <code>TELEGRAM_MIN_SEVERITY=warning</code> в
|
||||
<code>sac-api.env</code>, затем перезапустите <code>sac-api</code>.
|
||||
Для <strong>UseSAC=exclusive</strong> рекомендуется <code>warning</code> — failed login и problems, без
|
||||
успешных RDP (<code>info</code>).
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { apiFetch } from "../api";
|
||||
|
||||
interface TelegramSettings {
|
||||
@@ -47,23 +78,88 @@ interface TelegramSettings {
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface NotificationSettings {
|
||||
telegram: TelegramSettings;
|
||||
}
|
||||
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const testing = ref(false);
|
||||
const error = ref("");
|
||||
const data = ref<NotificationSettings | null>(null);
|
||||
const success = ref("");
|
||||
const loaded = ref<TelegramSettings | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
const form = reactive({
|
||||
enabled: false,
|
||||
min_severity: "warning",
|
||||
bot_token: "",
|
||||
chat_id: "",
|
||||
});
|
||||
|
||||
const tokenPlaceholder = computed(() =>
|
||||
loaded.value?.bot_token_hint ? `оставить ${loaded.value.bot_token_hint}` : "обязателен при первой настройке",
|
||||
);
|
||||
const chatPlaceholder = computed(() =>
|
||||
loaded.value?.chat_id_hint ? `оставить ${loaded.value.chat_id_hint}` : "обязателен при первой настройке",
|
||||
);
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
data.value = await apiFetch<NotificationSettings>("/api/v1/settings/notifications");
|
||||
const data = await apiFetch<{ telegram: TelegramSettings }>("/api/v1/settings/notifications");
|
||||
loaded.value = data.telegram;
|
||||
form.enabled = data.telegram.enabled;
|
||||
form.min_severity = data.telegram.min_severity;
|
||||
form.bot_token = "";
|
||||
form.chat_id = "";
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка загрузки";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
error.value = "";
|
||||
success.value = "";
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
enabled: form.enabled,
|
||||
min_severity: form.min_severity,
|
||||
};
|
||||
if (form.bot_token.trim()) body.bot_token = form.bot_token.trim();
|
||||
if (form.chat_id.trim()) body.chat_id = form.chat_id.trim();
|
||||
|
||||
const updated = await apiFetch<TelegramSettings>("/api/v1/settings/notifications/telegram", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
loaded.value = updated;
|
||||
form.bot_token = "";
|
||||
form.chat_id = "";
|
||||
success.value = "Настройки Telegram сохранены.";
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка сохранения";
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testTelegram() {
|
||||
testing.value = true;
|
||||
error.value = "";
|
||||
success.value = "";
|
||||
try {
|
||||
const res = await apiFetch<{ message: string }>("/api/v1/settings/notifications/telegram/test", {
|
||||
method: "POST",
|
||||
});
|
||||
success.value = res.message;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка отправки";
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -80,19 +176,40 @@ onMounted(async () => {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.settings-dl {
|
||||
display: grid;
|
||||
grid-template-columns: 11rem 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
margin: 0 0 1rem;
|
||||
.settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-dl dt {
|
||||
.settings-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.settings-field input[type="text"],
|
||||
.settings-field input[type="password"],
|
||||
.settings-field select {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.settings-meta {
|
||||
font-size: 0.85rem;
|
||||
color: #9aa4b2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-dl dd {
|
||||
margin: 0;
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-actions button.secondary {
|
||||
background: #2a3441;
|
||||
color: #c5d0dc;
|
||||
}
|
||||
|
||||
.settings-hint {
|
||||
@@ -100,4 +217,8 @@ onMounted(async () => {
|
||||
color: #9aa4b2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #6dd196;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user