Оба механизма — второй шаг многопользовательской волны (2026-10-03):
.env, оверрайды через админку (хранятся в БД, применяются без перезапуска контейнера). Секреты — write-only: значение записывается в БД, но никогда не возвращается API.manifest.webmanifest + service worker sw.js), канал push доставляет уведомления в браузеры пользователя через VAPID web-push.app/settings_registry.py — единственное место, где описан редактируемый параметр:
SettingDef(key, section, label, kind, options=(), help="")
kind: str | int | bool | secret (секрет — password-поле UI). Сейчас в реестре:
| section | ключ | kind | назначение |
|---|---|---|---|
| routing | routing_match_mode |
str(all/first) | режим матчинга правил |
| routing | dedup_window_seconds |
int 1..10^6 | окно дедупликации |
| routing | retention_days |
int 0..366 | окно ретеншна (0 — выкл) |
| push | vapid_public_key |
str | публичный VAPID-ключ |
| push | vapid_private_key |
secret | приватный VAPID-ключ |
| push | push_subject |
str | контакты отправителя (mailto:…) |
| telegram | telegram_bot_token |
secret | токен бота (канал #29 не готов) |
| smtp | smtp_host / smtp_port / smtp_user |
str/int | SMTP-конфиг (канал #29 не готов) |
| smtp | smtp_password |
secret | пароль SMTP |
| smtp | smtp_from |
str | From-адрес |
Новая настройка = запись в EDITABLE; чтения — в settings_store.get_setting(db, key). Ключи реестра, совпадающие с полями app.config.Settings (routing_match_mode, dedup_window_seconds, retention_days), получают дефолт из .env; ключи, которых в config нет (vapid_*, smtp_*, telegram_*) задаются только через UI.
app_settings(key PK, value, updated_at); строка — установить, удалить строку — вернуть дефолт.settings_store.get_setting(db, key) — env-дефолт + оверрайд, коэрсия по типу реестра (int → int(raw), bool → true/1). Без кэша: чтения редкие (приём, ingest, expire-прогон), один SELECT по PK.settings_store.get_setting_row(db, key) — сырое значение (для секретов в воркере: send_push).set_setting(db, key, None) — удалить оверрайд.GET /api/v1/admin/settings → список из реестра: {key, section, label, kind, help, options, value, set}. Для секрета value всегда null, факт наличия дефолта/оверрайда — set.PUT /api/v1/admin/settings body {"values": {key: "строка" | null}}:
422;422;"" — «не менять» (UI не отправляет пустое), непустая строка — set, null — сброс;null — вернуть дефолт .env.Секреты читает только воркер (send_push) — API их не отдаёт. Это осознанное отступление от правила «секретам не жить в БД»: секреты должны быть редактируемы из UI, хранилище БД write-only с ограничением чтения уровнем кода (владелец согласил 2026-10-03). Прочие секреты (gauth client secret и т.п.) — по-прежнему только .env + gnexus-creds.
/settings — один раздел для всех ролей: у пользователя личный блок web-push, у admin+ ещё секции конфига (Маршрутизация / Web Push / Telegram / SMTP). Секрет — password-поле с плейсхолдером «••••• задано — оставить пустым, чтобы не менять»; кнопка «Перебить дефолт» шлёт только изменённые значения (пустое поле открытой настройки = null = сброс оверрайда).
frontend/public/manifest.webmanifest — name/short_name Synapse, start_url: "/", display: standalone, theme #16161E, иконки 192/512 (+512 maskable) из gnexus-mark.svg.frontend/public/sw.js — pass-through fetch (никаких оффлайн-кэшей: API-приложение), обработчики push (showNotification, payload JSON {title, body, event_id, url}) и notificationclick (фокус существующего окна / openWindow на url).index.html — <link rel="manifest"> + theme-color; файлы отдаёт spa_fallback из dist контейнера API.main.js после mount; ошибки — console.warn, приложение не блокируется.Решает половину открытого вопроса №3: привязка «user_id ↔ адрес идентичности канала» для push создается самим пользователем — браузер подписывается, Synapse хранит только (user_id, endpoint, keys).
Каналы API (app/api/me_routes.py, любой аутентифицированный):
GET /api/v1/me/push → {configured, public_key} (public из настроек; configured=false — VAPID не заданы);GET /api/v1/me/push/subscriptions — свои устройства;POST /api/v1/me/push/subscriptions {endpoint, keys{p256dh,auth}, ua} — 201, upsert по endpoint (endpoint уникален: подписка «переезжает» новому пользователю, если браузер сменил аккаунт);DELETE /api/v1/me/push/subscriptions/{id} — 204 (только свой, чужой 404).Клиентская логика — frontend/src/push.js: разрешение браузера, pushManager.subscribe с applicationServerKey, база64url → Uint8Array.
pushCHANNELS += "push" — в правило ставится действие {"channel": "push"} (цель не задаётся — как у user).payload.user_id: без валидного sub → skipped с причиной в /admin/deliveries; без VAPID или без подписок → pending с ошибкой и ретраями.app/worker/senders.py: send_push): на каждую подписку pywebpush(..., vapid_private_key, vapid_claims={sub: push_subject}); ответ 404/410 (браузер отписался/endpoint истёк) — подписка удаляется; успех — когда все живые подписки отправлены (доставок нет — тоже ошибка аудитора, не тихий пропуск).Генерация пары P-256-ключей и значения для UI (проверено в контейнере):
python3 - <<'PY'
import base64
from cryptography.hazmat.primitives.serialization import (
Encoding, NoEncryption, PublicFormat, PrivateFormat,
)
from py_vapid import Vapid
def b64u(b):
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
v = Vapid()
v.generate_keys()
print("public :", b64u(v.public_key.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)))
print("private:", b64u(v.private_key.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
PY
public (base64url без-padding) — в «Настройки → Web Push → VAPID public key», private (DER в base64url) — в VAPID private key (write-only); воркер передаёт её через pywebpush → Vapid.from_string (raw/DER в base64url — сам pywebpush PEM-строки в этом формате не разбирает). push_subject — mailto:you@gnexus.space.
Пара секретная: сразу после генерации скопировать оба ключа в gnexus-creds (правило «секреты — в .env + gnexus-creds»; из UI приватный ключ назад не читается). Развёртывание на проде — шаг 6 рантбука docs/08.
⚠️ Смена приватного VAPID-ключа инвалидирует все подписки браузеров — после смены пользователи перезакажут подписку тумблером в разделе «Настройки».
/tmp/setpush.py (47 проверок, ALL OK): маска секретов, PUT-валидация, эффективные чтения оверрайдов, me/push*, CRUD подписок (upsert/чужой 404/смена владельца endpoint), routing push (skipped/pending), send_push (мок: 3 подписки delivered, 410-подписка удаляется)./tmp/mecheck.py, /tmp/retcheck.py — ALL OK./tmp/navcheck/setpush.js (17 проверок, ALL OK): user — личный блок и отсутствие админ-секций; user при не-настроенном VAPID — карточка «Сервер ещё не настроен»; admin — секции, маска секретов, GnSelect; nav-пункт «Настройки» у обеих ролей.