2026-06-02 21:11:57 +08:00
{% extends "base.html" %}
{% block title %}RSS → ntfy{% endblock %}
{% block body %}
< header class = "topbar" >
< div class = "brand" > < span class = "logo" > 📡< / span > RSS → ntfy< / div >
< nav class = "tabs" >
< button class = "tab active" data-tab = "feeds" data-i18n = "nav.feeds" > Ленты< / button >
2026-06-03 20:47:46 +08:00
< button class = "tab" data-tab = "reader" data-i18n = "nav.reader" > Чтение< / button >
2026-06-02 21:11:57 +08:00
< button class = "tab" data-tab = "history" data-i18n = "nav.history" > История< / button >
2026-06-03 20:47:46 +08:00
< button class = "tab admin-only" data-tab = "categories" data-i18n = "nav.categories" > Категории< / button >
2026-06-02 21:11:57 +08:00
< button class = "tab admin-only" data-tab = "users" data-i18n = "nav.users" > Пользователи< / button >
< button class = "tab admin-only" data-tab = "settings" data-i18n = "nav.settings" > Настройки< / button >
< / nav >
< div class = "topbar-actions" >
< span id = "whoami" class = "muted" > < / span >
< select id = "lang-select" class = "lang-select" >
< option value = "ru" > RU< / option >
< option value = "en" > EN< / option >
< / select >
< button class = "icon-btn" id = "theme-btn" data-i18n-title = "theme.toggle" > 🌓< / button >
< a class = "btn ghost" href = "/logout" id = "logout-btn" data-i18n = "topbar.logout" style = "display:none" > Выйти< / a >
< / div >
< / header >
< main class = "container" >
<!-- ===================== FEEDS ===================== -->
< section id = "tab-feeds" class = "tab-panel active" >
< div id = "stats" class = "stats" > < / div >
< div id = "chart-wrap" class = "chart-card hidden" >
< div class = "chart-head" >
< span data-i18n = "chart.title" > Активность за 14 дней< / span >
< span class = "chart-legend" >
< i class = "lg sent" > < / i > < span data-i18n = "chart.sent" > Отправлено< / span >
< i class = "lg failed" > < / i > < span data-i18n = "chart.failed" > Сбои< / span >
< / span >
< / div >
< div id = "chart" > < / div >
< / div >
< div class = "panel-head" >
< h2 data-i18n = "feeds.heading" > RSS-ленты< / h2 >
< div class = "panel-head-actions" >
< button class = "btn ghost" id = "check-all" data-i18n = "feeds.checkAll" > ↻ Проверить все< / button >
< button class = "btn ghost admin-only" id = "import-btn" data-i18n = "feeds.import" > ⬆ Импорт OPML< / button >
2026-06-03 20:47:46 +08:00
< button class = "btn ghost admin-only" id = "backup-btn" data-i18n = "feeds.backup" > 💾 Бэкап< / button >
< button class = "btn ghost admin-only" id = "restore-btn" data-i18n = "feeds.restore" > 📥 Восстановить< / button >
< input type = "file" id = "restore-file" accept = ".db" hidden >
2026-06-02 21:11:57 +08:00
< button class = "btn ghost" id = "export-btn" data-i18n = "feeds.export" > ⬇ Экспорт OPML< / button >
< button class = "btn primary admin-only" id = "add-feed" data-i18n = "feeds.add" > + Добавить ленту< / button >
< input type = "file" id = "opml-file" accept = ".opml,.xml,text/xml" hidden >
< / div >
< / div >
< div id = "feeds-list" class = "cards" > < / div >
< div id = "feeds-empty" class = "empty hidden" >
< div class = "empty-icon" > 🗞️< / div >
< p data-i18n = "feeds.empty" > < / p >
< / div >
< / section >
2026-06-03 20:47:46 +08:00
<!-- ===================== READER ===================== -->
< section id = "tab-reader" class = "tab-panel" >
< div class = "reader-layout" >
< aside class = "reader-sidebar" >
< div class = "reader-cats" id = "reader-cat-list" >
< div class = "reader-cat active" data-cat = "all" data-i18n = "reader.all" > Все< / div >
< / div >
< / aside >
< div class = "reader-main" >
< div class = "panel-head" >
< h2 data-i18n = "nav.reader" > Чтение< / h2 >
< button class = "btn ghost" id = "reader-mark-all" data-i18n = "reader.markAll" > Отметить все прочитанными< / button >
< / div >
< div id = "reader-list" > < / div >
< div id = "reader-detail" class = "hidden" > < / div >
< div id = "reader-empty" class = "empty hidden" >
< div class = "empty-icon" > 📖< / div >
< p data-i18n = "reader.empty" > Статей пока нет. Добавьте ленты, чтобы начать читать.< / p >
< / div >
< / div >
< / div >
< / section >
<!-- ===================== HISTORY ===================== -->
2026-06-02 21:11:57 +08:00
< section id = "tab-history" class = "tab-panel" >
< div class = "panel-head" >
< h2 data-i18n = "history.heading" > История уведомлений< / h2 >
< div class = "panel-head-actions" >
< button class = "btn ghost" id = "history-refresh" data-i18n = "history.refresh" > ↻ Обновить< / button >
< button class = "btn danger admin-only" id = "history-clear" data-i18n = "history.clear" > Очистить< / button >
< / div >
< / div >
< div class = "history-toolbar" >
< input type = "search" id = "history-search" data-i18n-ph = "history.search" placeholder = "Поиск…" >
< label class = "check-inline" > < input type = "checkbox" id = "history-errors" >
< span data-i18n = "history.onlyErrors" > Только ошибки< / span > < / label >
< / div >
< div id = "history-list" class = "history" > < / div >
< div id = "history-empty" class = "empty hidden" >
< div class = "empty-icon" > 📭< / div >
< p data-i18n = "history.empty" > История пуста.< / p >
< / div >
< / section >
2026-06-03 20:47:46 +08:00
<!-- ===================== CATEGORIES ===================== -->
< section id = "tab-categories" class = "tab-panel" >
< div class = "panel-head" >
< h2 data-i18n = "categories.heading" > Категории< / h2 >
< button class = "btn primary" id = "add-category" data-i18n = "categories.add" > + Добавить категорию< / button >
< / div >
< div id = "categories-list" class = "cards" > < / div >
< div id = "categories-empty" class = "empty hidden" >
< div class = "empty-icon" > 🏷️< / div >
< p data-i18n = "categories.empty" > Категорий пока нет.< / p >
< / div >
< / section >
<!-- ===================== USERS ===================== -->
2026-06-02 21:11:57 +08:00
< section id = "tab-users" class = "tab-panel" >
< div class = "panel-head" >
< h2 data-i18n = "users.heading" > Пользователи< / h2 >
< button class = "btn primary" id = "add-user" data-i18n = "users.add" > + Добавить пользователя< / button >
< / div >
< div id = "users-list" class = "cards" > < / div >
< / section >
<!-- ===================== SETTINGS ===================== -->
< section id = "tab-settings" class = "tab-panel" >
< div class = "panel-head" > < h2 data-i18n = "settings.heading" > Настройки< / h2 > < / div >
< form id = "settings-form" class = "settings-card" >
< h3 data-i18n = "settings.ntfy" > ntfy< / h3 >
< label > < span data-i18n = "settings.defaultServer" > Сервер ntfy по умолчанию< / span >
< input type = "text" name = "default_ntfy_server" placeholder = "https://ntfy.sh" >
< small class = "muted" data-i18n = "settings.defaultServerHint" > < / small >
< / label >
2026-06-02 21:47:12 +08:00
< details class = "adv" >
< summary data-i18n = "settings.ntfyAuth" > Авторизация на сервере ntfy< / summary >
< label > < span data-i18n = "feed.token" > Access token< / span > < small class = "muted" data-i18n = "feed.tokenHint" > < / small >
< input type = "text" name = "default_ntfy_token" placeholder = "tk_..." > < / label >
< div class = "grid-2" >
< label > < span data-i18n = "feed.login" > Логин< / span >
< input type = "text" name = "default_ntfy_username" autocomplete = "off" > < / label >
< label > < span data-i18n = "feed.password" > Пароль< / span >
< input type = "password" name = "default_ntfy_password" autocomplete = "new-password" > < / label >
< / div >
< small class = "muted" data-i18n = "settings.ntfyAuthHint" > < / small >
< / details >
2026-06-02 21:11:57 +08:00
< div class = "inline-test" >
< input type = "text" id = "test-topic" data-i18n-ph = "settings.testPh" >
< button type = "button" class = "btn ghost" id = "test-btn" data-i18n = "settings.testBtn" > Отправить тест< / button >
< / div >
2026-06-03 20:47:46 +08:00
< h3 data-i18n = "settings.check" > Проверка< / h3 >
< label > < span data-i18n = "settings.proxyUrl" > URL прокси< / span > < small class = "muted" data-i18n = "settings.proxyHint" > < / small >
< input type = "text" name = "proxy_url" placeholder = "http://proxy:8080" > < / label >
< h3 data-i18n = "settings.template" > Шаблон уведомлений< / h3 >
< label > < span data-i18n = "settings.templateHint" > Переменные: {title}, {body}, {link}, {source}, {image_url}< / span >
< textarea name = "notification_template" rows = "3" style = "width:100%;margin-top:4px;padding:8px;background:var(--bg-soft);border:1px solid var(--border);border-radius:8px;color:var(--text);font-family:monospace;font-size:13px;resize:vertical" > < / textarea > < / label >
2026-06-02 21:11:57 +08:00
< h3 data-i18n = "settings.check" > Проверка< / h3 >
< label > < span data-i18n = "settings.interval" > Интервал проверки по умолчанию (минуты)< / span >
< input type = "number" name = "check_interval" min = "1" value = "5" >
< small class = "muted" data-i18n = "settings.intervalHint" > < / small >
< / label >
2026-06-03 20:47:46 +08:00
< h3 data-i18n = "settings.feedDefaults" > Значения по умолчанию для новых лент< / h3 >
< div class = "grid-2" >
< label > < span data-i18n = "feed.priority" > Приоритет< / span >
< select name = "default_priority" >
< option value = "1" data-i18n = "feed.p1" > < / option >
< option value = "2" data-i18n = "feed.p2" > < / option >
< option value = "3" selected data-i18n = "feed.p3" > < / option >
< option value = "4" data-i18n = "feed.p4" > < / option >
< option value = "5" data-i18n = "feed.p5" > < / option >
< / select >
< / label >
< label > < span data-i18n = "feed.intervalMin" > Интервал, мин< / span > < small class = "muted" data-i18n = "feed.intervalHint" > < / small >
< input type = "number" name = "default_interval" min = "0" value = "0" > < / label >
< / div >
< label > < span data-i18n = "feed.tags" > Теги / эмодзи< / span > < small class = "muted" data-i18n = "feed.commaHint" > < / small >
< input type = "text" name = "default_tags" placeholder = "newspaper,fire" > < / label >
< label class = "switch-row" > < span data-i18n = "feed.attach" > Прикреплять картинку< / span >
< input type = "checkbox" name = "default_attach_image" class = "switch" checked > < / label >
< h3 data-i18n = "settings.telegram" > Telegram< / h3 >
2026-06-02 21:11:57 +08:00
< label class = "switch-row" > < span data-i18n = "settings.tgEnable" > Включить доставку в Telegram< / span >
< input type = "checkbox" name = "telegram_enabled" class = "switch" > < / label >
< div class = "grid-2" >
< label > < span data-i18n = "settings.tgToken" > Bot Token< / span >
< input type = "text" name = "telegram_token" placeholder = "123456:ABC-..." > < / label >
< label > < span data-i18n = "settings.tgChat" > Chat ID< / span >
< input type = "text" name = "telegram_chat_id" placeholder = "-1001234567890" > < / label >
< / div >
< small class = "muted" data-i18n = "settings.tgHint" > < / small >
< h3 data-i18n = "settings.webhook" > Webhook< / h3 >
< label class = "switch-row" > < span data-i18n = "settings.whEnable" > Включить доставку через webhook< / span >
< input type = "checkbox" name = "webhook_enabled" class = "switch" > < / label >
< label > < span data-i18n = "settings.whUrl" > URL webhook< / span >
< input type = "text" name = "webhook_url" placeholder = "https://example.com/hook" >
< small class = "muted" data-i18n = "settings.whHint" > < / small > < / label >
< h3 data-i18n = "settings.alerts" > Оповещения администратора< / h3 >
< label class = "switch-row" > < span data-i18n = "settings.alertEnable" > Уведомлять, если лента «упала»< / span >
< input type = "checkbox" name = "alerts_enabled" class = "switch" > < / label >
< div class = "grid-2" >
< label > < span data-i18n = "settings.alertTopic" > Тема ntfy для алертов< / span >
< input type = "text" name = "alert_topic" placeholder = "rss-alerts" > < / label >
< label > < span data-i18n = "settings.alertThreshold" > Порог (ошибок подряд)< / span >
< input type = "number" name = "alert_threshold" min = "1" value = "3" > < / label >
< / div >
< h3 data-i18n = "settings.auth" > Авторизация< / h3 >
< label class = "switch-row" > < span data-i18n = "settings.authRequire" > Требовать вход в веб-панель< / span >
< input type = "checkbox" name = "auth_enabled" class = "switch" > < / label >
< small class = "muted" data-i18n = "settings.authHint" > < / small >
< div class = "form-actions" >
< button type = "submit" class = "btn primary" data-i18n = "settings.save" > Сохранить настройки< / button >
< / div >
< / form >
< / section >
< / main >
<!-- ===================== FEED MODAL ===================== -->
< div id = "modal" class = "modal-backdrop hidden" >
< form class = "modal" id = "feed-form" >
< div class = "modal-head" >
< h3 id = "modal-title" data-i18n = "modal.addFeed" > Добавить ленту< / h3 >
< button type = "button" class = "icon-btn" id = "modal-close" > ✕< / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" name = "id" >
< label > < span data-i18n = "feed.url" > URL ленты *< / span >
< input type = "url" name = "url" placeholder = "https://example.com/feed.xml" required >
< / label >
< label > < span data-i18n = "feed.title" > Название< / span > < small class = "muted" data-i18n = "feed.titleOpt" > < / small >
< input type = "text" name = "title" >
< / label >
2026-06-03 20:47:46 +08:00
< label > < span data-i18n = "feed.category" > Категория< / span >
< select name = "category_id" >
< option value = "" data-i18n = "feed.categoryNone" > — без категории —< / option >
< / select >
< / label >
2026-06-02 21:11:57 +08:00
< div class = "grid-2" >
< label > < span data-i18n = "feed.server" > Сервер ntfy< / span > < small class = "muted" data-i18n = "feed.serverHint" > < / small >
< input type = "text" name = "ntfy_server" placeholder = "https://ntfy.sh" > < / label >
< label > < span data-i18n = "feed.topic" > Тема ntfy< / span >
< input type = "text" name = "ntfy_topic" placeholder = "my-news" > < / label >
< / div >
< details class = "adv" >
< summary data-i18n = "feed.priv" > Приватный ntfy-сервер (авторизация)< / summary >
< label > < span data-i18n = "feed.token" > Access token< / span > < small class = "muted" data-i18n = "feed.tokenHint" > < / small >
< input type = "text" name = "ntfy_token" placeholder = "tk_..." > < / label >
< div class = "grid-2" >
< label > < span data-i18n = "feed.login" > Логин< / span >
< input type = "text" name = "ntfy_username" autocomplete = "off" > < / label >
< label > < span data-i18n = "feed.password" > Пароль< / span >
< input type = "password" name = "ntfy_password" autocomplete = "new-password" > < / label >
< / div >
< / details >
< div class = "grid-2" >
< label > < span data-i18n = "feed.priority" > Приоритет< / span >
< select name = "priority" >
< option value = "1" data-i18n = "feed.p1" > < / option >
< option value = "2" data-i18n = "feed.p2" > < / option >
< option value = "3" selected data-i18n = "feed.p3" > < / option >
< option value = "4" data-i18n = "feed.p4" > < / option >
< option value = "5" data-i18n = "feed.p5" > < / option >
< / select >
< / label >
< label > < span data-i18n = "feed.intervalMin" > Интервал, мин< / span > < small class = "muted" data-i18n = "feed.intervalHint" > < / small >
< input type = "number" name = "interval" min = "0" value = "0" > < / label >
< / div >
2026-06-03 20:47:46 +08:00
< details class = "adv" >
< summary data-i18n = "feed.digest" > Дайджест< / summary >
< label class = "switch-row" > < span data-i18n = "feed.digestEnable" > Накапливать записи (дайджест)< / span >
< input type = "checkbox" name = "digest_enabled" class = "switch" > < / label >
< label > < span data-i18n = "feed.digestPeriod" > Период дайджеста (часы)< / span >
< input type = "number" name = "digest_period_hours" min = "1" value = "24" > < / label >
< / details >
2026-06-02 21:11:57 +08:00
< label > < span data-i18n = "feed.tags" > Теги / эмодзи< / span > < small class = "muted" data-i18n = "feed.commaHint" > < / small >
< input type = "text" name = "tags" placeholder = "newspaper,fire" > < / label >
< div class = "grid-2" >
< label > < span data-i18n = "feed.filterInc" > Фильтр: только с этими словами< / span > < small class = "muted" data-i18n = "feed.commaHint" > < / small >
< input type = "text" name = "filter_include" placeholder = "python, ai" > < / label >
< label > < span data-i18n = "feed.filterExc" > Фильтр: исключить слова< / span > < small class = "muted" data-i18n = "feed.commaHint" > < / small >
< input type = "text" name = "filter_exclude" placeholder = "sponsored" > < / label >
< / div >
< div class = "switch-grid" >
< label class = "switch-row" > < span data-i18n = "feed.attach" > Прикреплять картинку< / span >
< input type = "checkbox" name = "attach_image" class = "switch" checked > < / label >
2026-06-03 20:47:46 +08:00
< label class = "switch-row" > < span data-i18n = "feed.fetchArticle" > Загружать полную статью (trafilatura)< / span >
< input type = "checkbox" name = "fetch_full_article" class = "switch" > < / label >
< small class = "muted" data-i18n = "feed.fetchArticleHint" style = "margin-top:-8px;display:block" > Загружает страницу статьи и извлекает основной текст.< / small >
< label class = "switch-row" > < span data-i18n = "feed.fullContent" > Отправлять полный контент< / span >
< input type = "checkbox" name = "send_full_content" class = "switch" > < / label >
< small class = "muted" data-i18n = "feed.fullContentHint" style = "margin-top:-8px;display:block" > Весь текст, все картинки и видео. Для ntfy — Markdown.< / small >
2026-06-02 21:11:57 +08:00
< label class = "switch-row" > < span data-i18n = "feed.dupTg" > Дублировать в Telegram< / span >
< input type = "checkbox" name = "to_telegram" class = "switch" > < / label >
< label class = "switch-row" > < span data-i18n = "feed.toWebhook" > Отправлять в webhook< / span >
< input type = "checkbox" name = "to_webhook" class = "switch" > < / label >
< label class = "switch-row" > < span data-i18n = "feed.enabled" > Лента включена< / span >
< input type = "checkbox" name = "enabled" class = "switch" checked > < / label >
< / div >
< div class = "preview-block" >
< button type = "button" class = "btn ghost small" id = "preview-btn" data-i18n = "feed.preview" > 👁 Предпросмотр< / button >
< div id = "preview-area" > < / div >
< / div >
< / div >
< div class = "modal-foot" >
< button type = "button" class = "btn ghost" id = "modal-cancel" data-i18n = "modal.cancel" > Отмена< / button >
< button type = "submit" class = "btn primary" data-i18n = "modal.save" > Сохранить< / button >
< / div >
< / form >
< / div >
2026-06-03 20:47:46 +08:00
<!-- ===================== CATEGORY MODAL ===================== -->
< div id = "cat-modal" class = "modal-backdrop hidden" >
< form class = "modal" id = "cat-form" >
< div class = "modal-head" >
< h3 id = "cat-modal-title" data-i18n = "cat.addTitle" > Добавить категорию< / h3 >
< button type = "button" class = "icon-btn" id = "cat-modal-close" > ✕< / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" name = "id" >
< label > < span data-i18n = "cat.name" > Название *< / span >
< input type = "text" name = "name" required > < / label >
< label > < span data-i18n = "cat.sortOrder" > Порядок сортировки< / span >
< input type = "number" name = "sort_order" min = "0" value = "0" > < / label >
< / div >
< div class = "modal-foot" >
< button type = "button" class = "btn ghost" id = "cat-modal-cancel" data-i18n = "modal.cancel" > Отмена< / button >
< button type = "submit" class = "btn primary" data-i18n = "modal.save" > Сохранить< / button >
< / div >
< / form >
< / div >
2026-06-02 21:11:57 +08:00
<!-- ===================== USER MODAL ===================== -->
< div id = "user-modal" class = "modal-backdrop hidden" >
< form class = "modal" id = "user-form" >
< div class = "modal-head" >
< h3 id = "user-modal-title" data-i18n = "user.addTitle" > Добавить пользователя< / h3 >
< button type = "button" class = "icon-btn" id = "user-modal-close" > ✕< / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" name = "id" >
< label > < span data-i18n = "user.login" > Логин *< / span >
< input type = "text" name = "username" autocomplete = "off" required > < / label >
< label > < span data-i18n = "user.password" > Пароль< / span > < small class = "muted" id = "pw-hint" > < / small >
< input type = "password" name = "password" autocomplete = "new-password" > < / label >
< label > < span data-i18n = "user.role" > Роль< / span >
< select name = "role" >
< option value = "admin" data-i18n = "user.roleAdmin" > < / option >
< option value = "viewer" data-i18n = "user.roleViewer" > < / option >
< / select > < / label >
< / div >
< div class = "modal-foot" >
< button type = "button" class = "btn ghost" id = "user-modal-cancel" data-i18n = "modal.cancel" > Отмена< / button >
< button type = "submit" class = "btn primary" data-i18n = "modal.save" > Сохранить< / button >
< / div >
< / form >
< / div >
< div id = "toast" class = "toast hidden" > < / div >
{% endblock %}
{% block scripts %}< script src = "/static/app.js" > < / script > {% endblock %}