Три новых раздела admin-панели: браузер базы данных, управление системой и пользователи

База данных
До этого, если нужно было посмотреть или поправить данные в таблице — открываешь pgAdmin, вводишь хост, пользователя, пароль, ждёшь подключения. Или пишешь одноразовый SELECT прямо в консоли. Оба пути медленные и неудобные, особенно когда нужно просто глянуть значение одного поля или исправить опечатку.
Я сделал браузер баз данных прямо внутри admin-панели.
Навигация по схемам и таблицам

Слева — дерево: сначала загружаются схемы PostgreSQL. Клик по схеме раскрывает список её таблиц — запрос идёт только при первом открытии, не при загрузке страницы. Клик по таблице открывает её содержимое справа.
Активная таблица подсвечивается в дереве. Переключение между таблицами мгновенное — сбрасывает сортировку, фильтры и пагинацию.
Браузер таблицы
Открытая таблица показывает структуру и данные. Структура свёрнута по умолчанию — разворачивается кликом по строке «Структура (N колонок)». Внутри — таблица с именем, типом данных, nullable и default для каждой колонки.
Данные — 50 строк на страницу с пагинацией. Под таблицей: кнопки ‹ и ›, счётчик «1–50 из 247». Заголовки колонок кликабельны — сортировка по любому полю, повторный клик меняет направление, стрелка ↑↓ показывает текущее состояние.
Над таблицей — фильтр: выпадающий список колонок и поле значения. Ввёл и нажал Enter или «Найти» — данные перезагружаются с WHERE. Кнопка «✕ сброс» снимает фильтр.
Правый столбец таблицы с кнопками ✏ и ✕ закреплён: при горизонтальном скролле (а в широких таблицах он неизбежен) кнопки остаются видны. Реализовано через position: sticky; right: 0 с тенью, которая обозначает границу закреплённой части.
Редактирование отдельной ячейки

Клик по значению в ячейке открывает модальное окно с textarea. Там полное значение — без обрезки через ellipsis. Отредактировал — нажал «Сохранить», изменение применяется через PUT, таблица перезагружается.
В SQL-редакторе клик по ячейке тоже открывает это окно, но только для чтения — там нет привязки к строке, поэтому редактировать нечего.
Добавление и редактирование записей

Кнопка «+ Строка» в шапке открывает модальное окно со всеми колонками таблицы. Каждое поле подписано именем, типом данных, флагами nullable и auto. Тип данных влияет на то, какой элемент управления появляется:
boolean— выпадающий списокtrue/false, плюс— nullесли поле nullablejson/jsonb— многострочное textareainteger,numericи прочие числовые —<input type="number">- Всё остальное — текстовое поле
Поля с nextval(...) в default помечены бейджем auto и при создании остаются пустыми — бэкенд оставляет их на усмотрение базы.
При редактировании открывается то же самое окно, но уже с заполненными значениями из выбранной строки.
Идентификация строк без первичного ключа
Удаление и обновление строк работают через системный столбец PostgreSQL ctid — физический адрес строки в файле таблицы. Бэкенд добавляет ctid::text as "__ctid" к каждому SELECT и использует его в WHERE ctid = ?::tid. Это работает для любой таблицы независимо от структуры первичного ключа.
SQL-редактор

В верхней части сайдбара — кнопка «⌨ SQL Editor». Открывает textarea с моноширинным шрифтом. Ctrl+Enter выполняет запрос. Tab вставляет два пробела вместо смены фокуса.
SELECT-запросы возвращают таблицу с результатами и счётчиком строк. INSERT / UPDATE / DELETE показывают «OK — затронуто строк: N». Ошибки отображаются с полным текстом от PostgreSQL.
История последних 15 запросов внизу — клик по строке вставляет запрос в редактор.
Система

Раздел «Система» объединяет несколько несвязанных по природе вещей, которые нужно иметь под рукой при работе с сервером.
Информация о процессе и статус сервисов
Два блока вверху страницы.
Первый — сетка плиток: uptime сервера в читаемом виде (2д 3ч 15м), версия Node.js, RSS-память, heap использованный, heap выделенный, платформа. Всё из process.memoryUsage() и process.uptime().
Второй — статус сервисов. Для PostgreSQL endpoint делает sequelize.authenticate() и измеряет время ответа. Результат — цветная точка с glow-эффектом (зелёная если ok, красная при ошибке) и latency в миллисекундах рядом с хостом. Если DB_HOST не задан — статус unknown, попытки подключиться нет.
Это полезно именно в разделе «Система», а не только на дашборде: когда идёшь сюда что-то менять или перезапускать сервер, сразу видишь состояние процесса и базы без переключения вкладок.
Фоновые задачи
Задачи делятся на два типа.
Системные задачи — встроенные в приложение. Их нельзя удалить, но можно включить/выключить и изменить интервал. Таблица показывает имя, описание, интервал в человекочитаемом виде (каждые 5 мин), время последнего запуска, статус (✓ ok / ✗ ошибка / выполняется), длительность. Кнопка ▶ запускает задачу немедленно, не дожидаясь следующего тика. Кнопка ⏸ останавливает или возобновляет. Кнопка ⚙ открывает настройки.

Webhook-задачи — пользовательские, хранятся в базе данных. По расписанию делают HTTP-запрос на указанный URL. Настраиваются: название, описание, URL, метод, заголовки (JSON), тело запроса, интервал в миллисекундах. Создание — через кнопку «+ Создать», полный CRUD. Задачи можно включать и выключать, не удаляя конфигурацию.
Оба списка обновляются автоматически каждые 10 секунд. Если только что запустил задачу вручную — увидишь результат без перезагрузки страницы.
Практический случай: нужно периодически дёргать внешний эндпоинт для прогрева кэша или синхронизации данных. Раньше — отдельный скрипт в crontab на сервере. Сейчас — webhook-задача в интерфейсе, интервал меняется на лету без деплоя.
Переменные окружения
Список всех переменных процесса, кроме системных (npm_, HOME, PATH и т.п.). Чувствительные переменные — те, в имени которых есть TOKEN, SECRET, PASSWORD, KEY и похожие — маскируются: видны первые четыре символа и многоточие. Кнопка 🔒 рядом с такой переменной делает отдельный запрос к серверу и раскрывает полное значение. Кнопка 🔓 скрывает обратно.
Поле фильтрации над таблицей — мгновенная фильтрация по имени без запросов к серверу.
Перезапуск сервера
Кнопка «Перезапустить сервер» с двухшаговым подтверждением. После клика появляется inline-подтверждение «Продолжить?» и кнопки «Да, перезапустить» / «Отмена». Подтвердил — сервер отвечает 200 и через 300 мс вызывает process.exit(0). Docker перезапустит контейнер автоматически.
Интерфейс переходит в состояние «Перезапуск… ожидаем сервер» и начинает поллинг /health раз в секунду. Когда сервер снова отвечает — показывает «✓ Сервер перезапущен» и через 4 секунды возвращается в исходное состояние.
Пользователи

До этого доступ в admin-панель контролировался двумя переменными окружения: ADMIN_UI_USERNAME и ADMIN_UI_PASSWORD. Одна учётная запись, только через переменные окружения, смена пароля — через редактирование .env и перезапуск контейнера.
Таблица admin_users и авторизация
В схеме api_admin появилась таблица admin_users: id, name, login, password_hash, created_at, updated_at. Пароли хранятся только в виде bcrypt-хешей — bcrypt.hash при создании, bcrypt.compare при логине. updated_at обновляется триггером.
Логика авторизации при логине:
- Если
DB_HOSTзадан и в таблице есть хотя бы одна запись — авторизация только через базу. - Если таблица пустая или база недоступна — fallback на
ADMIN_UI_USERNAME/ADMIN_UI_PASSWORDиз env. - Если env тоже не заданы — 503.
Это позволяет запустить сервис с нуля: при первом старте, если заданы ADMIN_UI_USERNAME и ADMIN_UI_PASSWORD, сервер создаёт запись в admin_users через seedAdminUser — один раз, только если пользователя с таким логином ещё нет. Дальше работает только база, переменные окружения игнорируются.
Такой подход даёт плавную миграцию: существующая конфигурация продолжает работать без изменений, но сразу создаёт полноценную запись в базе.
JWT и актуальность данных пользователя
JWT-токен живёт 30 дней. За это время имя или логин администратора может измениться в базе — токен при этом будет нести старые данные. Перевыдавать токен при каждом изменении профиля — лишняя сложность.
Решение: endpoint GET /api/admin/me. Декодирует JWT, берёт sub (числовой id пользователя) и делает SELECT из admin_users. Клиент всегда получает актуальные данные. Если sub — строка (env-fallback токен), данные берутся из payload.
Интерфейс везде использует данные с сервера, а не из локально декодированного JWT: имя в заголовке страницы, блок «Мой аккаунт».
Список пользователей и CRUD
Таблица: имя, логин, дата создания, дата последнего обновления. Справа в каждой строке — кнопки ✏ и ✕.

Создание — модальное окно с полями: имя, логин, пароль, подтверждение пароля. Все четыре обязательны. Валидация совпадения паролей на клиенте — до отправки запроса.

Редактирование — то же окно с заполненными именем и логином. Поля пароля пустые: если оставить их пустыми — password_hash в базе не меняется. Нужно поменять только имя — пароль вводить не нужно.
Удаление — с подтверждением через confirm() с именем и логином пользователя в тексте. Деструктивное действие, которое выполняется редко — нативный диалог здесь достаточен и надёжен.
Мой аккаунт и смена пароля
В начале страницы — блок «Мой аккаунт» с именем и логином текущего пользователя. Данные берутся с сервера через /api/admin/me, не из JWT.
Кнопка «Сменить пароль» открывает модальное окно с тремя полями: текущий пароль, новый пароль, подтверждение. Логика смены:
- Верификация текущего пароля — повторный логин через
POST /api/admin/loginс текущим паролем. - Если логин успешен —
PUT /api/admin/users/:idс новым паролем.
Отдельного endpoint для смены пароля нет. Используется тот же путь, что и при обычном логине — те же правила проверки, никакого отдельного кода, который нужно поддерживать.
Все три раздела работают под тем же adminUiAuth middleware. Отдельного права «только для суперадмина» нет — любой пользователь из admin_users имеет полный доступ ко всему, включая управление другими пользователями. Для проекта с небольшой командой это нормально; если потребуются роли — их можно добавить позже без изменения архитектуры.