Files
remnawave-minishop/frontend/src/admin/ConfigAlertsBanner.svelte
T
3252a8 6bb2709244 feat: add telemetry build provenance
Stamp official Docker builds with a low-cardinality provenance marker and report build_provenance/image_modified in anonymous telemetry. Local and fork builds default to custom, while official GitHub/GitLab release paths mark images as official.
2026-06-10 15:18:32 +03:00

128 lines
6.0 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { getContext } from "svelte";
import { RefreshCw, TriangleAlert } from "$components/ui/icons.js";
import { AdminButton } from "$components/patterns/admin/index.js";
export let at = (key, _params = {}, fallback = "") => fallback || key;
export let section = "stats";
export let onNavigate = () => {};
const healthStore = getContext("healthStore");
const MESSAGE_FALLBACKS = {
data_dir_missing:
"Каталог data ({path}) не найден. Проверьте, что том data смонтирован в контейнер.",
data_dir_not_writable:
"Нет прав на запись в {path} — бэкапы, тарифы, логотипы и переводы не сохранятся.",
backups_dir_not_writable: "Каталог бэкапов {path} недоступен для записи.",
tariffs_config_invalid: "Файл тарифов {path} не читается: {error}",
locale_overrides_invalid: "Файл переводов {path} повреждён: {error}",
subscription_page_config_invalid: "Конфиг гайдов подписки не читается: {error}",
provider_not_configured:
"Провайдер {provider} включён, но не настроен — оплата через него не работает.",
provider_webhook_needs_base_url:
"Провайдеру {provider} нужен WEBHOOK_BASE_URL для приёма вебхуков, а он не задан.",
no_payment_methods: "Не включён ни один способ оплаты.",
mini_app_url_missing:
"SUBSCRIPTION_MINI_APP_URL не задан — кнопка Mini App в боте не появится.",
mini_app_url_not_https:
"SUBSCRIPTION_MINI_APP_URL должен начинаться с https:// (сейчас {url}).",
redis_not_configured:
"REDIS_URL не задан — состояния диалогов бота и кэш не переживут перезапуск.",
smtp_incomplete: "SMTP настроен не полностью — вход по email работать не будет.",
proxy_not_trusted:
"Запросы приходят через прокси {remote}, которого нет в TRUSTED_PROXIES — вебхуки платёжных провайдеров могут отклоняться по IP.",
bot_token_invalid: "Telegram отверг BOT_TOKEN — бот не работает.",
telegram_api_error: "Не удалось обратиться к Telegram API: {error}",
telegram_webhook_missing: "Вебхук Telegram не установлен — бот не получает обновления.",
telegram_webhook_mismatch: "Вебхук Telegram указывает на {actual}, ожидается {expected}.",
telegram_webhook_error: "Telegram сообщает об ошибке доставки вебхука: {error}",
telegram_webhook_pending: "В очереди Telegram скопилось {count} необработанных обновлений.",
panel_api_not_configured:
"PANEL_API_URL и PANEL_API_KEY не заданы — синхронизация и выдача подписок не работают.",
panel_api_unreachable: "Панель Remnawave недоступна по адресу {url}.",
};
const SECTION_FALLBACK_LABELS = {
settings: "Настройки",
payments: "Платежи",
backups: "Бэкапы",
tariffs: "Тарифы",
appearance: "Внешний вид",
translations: "Переводы",
users: "Пользователи",
};
function interpolate(template, params = {}) {
return String(template || "").replace(/\{(\w+)\}/g, (match, key) =>
params[key] !== undefined && params[key] !== null ? String(params[key]) : match
);
}
function alertText(alert) {
const fallback = interpolate(
MESSAGE_FALLBACKS[alert.message_key] || alert.message_key,
alert.params
);
return at(`health_${alert.message_key}`, alert.params || {}, fallback);
}
function sectionLabel(id) {
return at(`nav_${id}`, {}, SECTION_FALLBACK_LABELS[id] || id);
}
$: alerts = $healthStore?.alerts || [];
$: healthLoading = $healthStore?.healthLoading;
$: isDashboard = section === "stats";
$: visibleAlerts = isDashboard
? alerts
: alerts.filter((alert) => (alert.sections || []).includes(section));
$: errorCount = visibleAlerts.filter((alert) => alert.severity === "error").length;
</script>
{#if visibleAlerts.length}
<div
class="admin-config-alerts"
class:admin-config-alerts-error={errorCount > 0}
role="alert"
aria-live="polite"
>
<div class="admin-config-alerts-head">
<span class="admin-config-alerts-title">
<TriangleAlert size={15} />
{at("health_title", {}, "Проблемы конфигурации")}
</span>
{#if isDashboard}
<AdminButton
onclick={() => healthStore.loadHealth({ refresh: true })}
disabled={healthLoading}
>
<RefreshCw size={13} />
{at("health_refresh", {}, "Проверить снова")}
</AdminButton>
{/if}
</div>
<ul class="admin-config-alerts-list">
{#each visibleAlerts as alert (alert.id)}
<li class="admin-config-alert admin-config-alert-{alert.severity}">
<span class="admin-config-alert-dot" aria-hidden="true"></span>
<span class="admin-config-alert-text">{alertText(alert)}</span>
{#if isDashboard && (alert.sections || []).length}
<span class="admin-config-alert-links">
{#each alert.sections as sectionId (sectionId)}
<button
type="button"
class="admin-config-alert-link"
on:click={() => onNavigate(sectionId)}
>
{sectionLabel(sectionId)}
</button>
{/each}
</span>
{/if}
</li>
{/each}
</ul>
</div>
{/if}