Files
pzmanager/README.md
T
mrleo1nidandClaude Opus 5 ae8c511a9a
CI / test (push) Successful in 24s
Release / release (push) Successful in 33s
Импорт пресета не терял моды: полный набор и активный профиль
В пакете сборки рядом с комплектом сервера лежит пресет песочницы одиночной
игры. Наборы сортировались по имени, и пресет песочницы часто оказывался
первым — панель предлагала его по умолчанию, а модов и карт в нём нет.
Теперь первым идёт набор с конфигом сервера, а в списке выбора видно, что
внутри каждого.

Вкладки показывают активный профиль, поэтому импорт в новый выглядел так,
будто ничего не подтянулось. Появилась галочка «Сделать этот профиль
активным»; если её снять, панель говорит, где искать импортированное.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:21:41 +03:00

551 lines
36 KiB
Markdown
Raw Permalink 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.
# PZ Manager
[![release](https://img.shields.io/gitea/v/release/mrleo1nid/pzmanager?gitea_url=https%3A%2F%2Fgitea.hsrv.site&logo=gitea&logoColor=white&label=релиз)](https://gitea.hsrv.site/mrleo1nid/pzmanager/releases/latest)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Go](https://img.shields.io/badge/Go-1.24%2B-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![Platform](https://img.shields.io/badge/platform-Linux%20%2F%20Ubuntu-EEEEEE?logo=ubuntu&logoColor=white&labelColor=555555)](https://ubuntu.com/)
[![last commit](https://img.shields.io/gitea/last-commit/mrleo1nid/pzmanager?gitea_url=https%3A%2F%2Fgitea.hsrv.site&logo=gitea&logoColor=white)](https://gitea.hsrv.site/mrleo1nid/pzmanager/commits/branch/main)
Веб-панель для управления выделенным сервером Project Zomboid на Ubuntu.
Один бинарник без зависимостей: веб-интерфейс вшит внутрь, Node.js и Python на
сервере не нужны.
## Что умеет
- **Профили серверов** — несколько независимых сборок (свой мир, конфиг,
песочница, моды и лимит памяти) с переключением одной кнопкой.
- **Управление сервером** — запуск, остановка (через команду `quit`, мир
сохраняется), перезапуск, автоподъём после падения, автозапуск вместе с
менеджером. «Работает» панель показывает не раньше, чем сервер напечатает
`*** SERVER STARTED ****`: пока идёт загрузка мира, состояние остаётся
«Запускается».
- **Живая консоль** — вывод сервера в реальном времени и отправка любых команд
(`players`, `save`, `servermsg`, `kickuser`, …).
- **Мониторинг** — состояние, аптайм, CPU и память процесса сервера, свободные
ОЗУ и диск на хосте, список игроков онлайн.
- **Редактор конфигов** — `servertest.ini` и `SandboxVars.lua` прямо в
браузере: параметры разложены по разделам, у каждого видно описание из
самого конфига, поиск идёт и по названию, и по описанию.
- **Мод-менеджер** — моды списком с галочками и перетаскиванием порядка
загрузки, добавление по ссылке и разворачивание коллекций Steam целиком.
- **Импорт пресетов сборок** — ZIP-пакет сборки загружается прямо в панель:
конфиг, песочница и точки появления попадают в новый или существующий
профиль, свои порты и пароли при этом сохраняются. Отдельно принимаются
пресет песочницы одиночной игры (`.cfg`) и строка со списком модов из
игровой кнопки «Поделиться».
- **Бэкапы** — ручные и по расписанию, отдельно по каждому профилю, с
ротацией, скачиванием и восстановлением (перед восстановлением
автоматически снимается страховочная копия).
- **Два языка** — русский и английский, с выбором в шапке и автоопределением
по языку браузера.
- **Установка и обновление** сервера через SteamCMD прямо из панели.
- **Обновление самой панели** — кнопкой из интерфейса или одной командой в
терминале.
## Как это устроено
Менеджер запускает `start-server.sh` как дочерний процесс и держит его stdin,
stdout и stderr. Отсюда следует всё остальное: команды консоли пишутся прямо в
stdin (RCON не нужен), логи стримятся в браузер через SSE, а остановка идёт по
цепочке `quit``SIGTERM``SIGKILL` для всей группы процессов.
Сам менеджер живёт под systemd, который его и перезапускает.
```
systemd ──> pzmanager ──> start-server.sh ──> java (GameServer)
├── HTTP + SSE ──> браузер
└── SteamCMD, бэкапы, конфиги
```
## Быстрый старт
На чистом Ubuntu/Debian всё ставится одной командой — она возьмёт последний
релиз, проверит контрольные суммы и настроит systemd:
```bash
curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/get.sh | sudo bash
```
По умолчанию панель слушает `127.0.0.1:8080` — только с самой машины. Чтобы
она была доступна в локальной сети, укажите адрес сервера:
```bash
curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/get.sh \
| sudo PZ_LISTEN=10.10.1.142:8080 bash
```
Переменные: `PZ_LISTEN` (адрес панели), `PZ_HOME` (каталог, по умолчанию
`/opt/pzmanager`), `PZ_USER` (системный пользователь), `PZ_VERSION`
(конкретный тег вместо последнего).
Скрипт запускается от root, так что перед первым запуском его разумно
прочитать: [deploy/get.sh](deploy/get.sh).
Установщик поставит зависимости (SteamCMD, JRE, SDL2), заведёт пользователя
`pzserver` без shell, положит бинарник в `/usr/local/bin` и создаст структуру:
```
/opt/pzmanager/
├── config.yaml настройки менеджера
├── data/ учётки панели и скачанный SteamCMD
├── server/ серверные файлы Project Zomboid
├── Zomboid/ миры, конфиги и логи игры
└── backups/ архивы
```
**Обновление** — та же команда или кнопка в панели (вкладка «Настройки» →
«Версия панели»). Конфиг, миры и учётные записи не трогаются. Повторный запуск
с той же версией службу не трогает вовсе: если бинарник не изменился,
установщик не станет ради него ронять игровой сервер.
Дальше:
1. Откройте панель в браузере и создайте администратора, введя код первичной
настройки — установщик печатает его последним сообщением. Если панель
слушает localhost, пробросьте порт:
```bash
ssh -L 8080:127.0.0.1:8080 пользователь@сервер
```
Код одноразовый и перестаёт работать сразу после создания администратора.
Если вывод установщика потерялся, код есть в журнале:
```bash
journalctl -u pzmanager -n 30 --no-pager
```
2. Во вкладке «Обзор» нажмите **Установить / обновить через SteamCMD** и
дождитесь окончания — прогресс виден во вкладке «Консоль».
3. Запустите сервер. При первом запуске PZ создаст `Zomboid/Server/*.ini` —
после этого станут доступны вкладки «Конфиг сервера», «Песочница» и «Моды».
Порты, которые нужно открыть:
```bash
sudo ufw allow 8080/tcp # веб-панель, если она слушает не localhost
sudo ufw allow 16261/udp # игровой порт
sudo ufw allow 16262/udp # второй игровой порт
```
### Установка из исходников
Если релизам предпочитаете свою сборку (нужен Go 1.24+, собирать можно на
любой ОС, включая Windows):
```bash
make build-linux
```
Скопируйте `pzmanager` и папку `deploy/` на сервер и запустите установщик:
```bash
sudo PZ_LISTEN=10.10.1.142:8080 ./deploy/install.sh
```
### Удаление
Убрать панель, оставив миры и настройки:
```bash
sudo /opt/pzmanager/install.sh --uninstall
```
Удалить всё вместе с мирами, бэкапами и учётными записями:
```bash
sudo /opt/pzmanager/install.sh --uninstall --purge
```
`--purge` спрашивает подтверждение; в неинтерактивном запуске нужен ещё и
`--yes`. Каталог сносится, только если похож на установку панели — иначе
опечатка в `PZ_HOME` стоила бы чужих файлов.
Установщик кладёт свою копию в `/opt/pzmanager/install.sh`, так что для
удаления ничего скачивать не нужно. Если копии нет, те же аргументы принимает
и установочная команда:
```bash
curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/get.sh | sudo bash -s -- --uninstall --purge
```
## Язык интерфейса
Панель говорит по-русски и по-английски. Язык выбирается списком в шапке (и на
экране входа, до того как вошли), а до первого выбора берётся из настроек
браузера: русский — русским, всем остальным — английский. Выбор запоминается в
браузере, поэтому у каждого, кто заходит в панель, он свой.
Переводится всё, что показывает панель: интерфейс, подписи и описания всех
параметров `servertest.ini` и `SandboxVars.lua`, ошибки и служебные сообщения
менеджера в консоли. Вывод самого игрового сервера остаётся как есть — он
приходит от Project Zomboid.
## Импорт пресетов сборок
Крупные сборки модов раздают готовый комплект настроек архивом: внутри лежат
папка `Server` с `<имя>.ini`, `<имя>_SandboxVars.lua` и
`<имя>_spawnregions.lua`, а рядом — файлы для одиночной игры, которые серверу
не нужны.
На вкладке «Профили» есть «Импорт пресета сборки»: выберите ZIP, и панель
покажет, что в нём нашла — название сервера, сколько модов и пакетов
мастерской, какие карты, сколько параметров конфига и песочницы. Ничего не
записывается, пока вы не нажмёте «Импортировать».
Если наборов в архиве несколько, появится список выбора. Первым идёт самый
полный набор — тот, где есть конфиг сервера: только в нём лежат моды и карты.
Рядом с именем написано, что внутри, чтобы случайно не импортировать пресет
песочницы вместо комплекта сервера.
Импортировать можно в новый профиль (идентификатор предлагается по имени
файлов из архива) или поверх существующего. Прежние конфиги остаются рядом с
расширением `.bak`, мир не трогается. Профиль, который сейчас работает,
импорт не принимает — сначала остановите сервер, иначе PZ перезапишет файлы
своими при выходе.
Галочка «Сделать этот профиль активным» включена по умолчанию: вкладки «Моды»
и «Конфиг сервера» показывают активный профиль, и без переключения кажется,
что импорт ничего не сделал. Если галочку снять, панель напомнит, под каким
профилем искать импортированное.
Галочки «Оставить свои значения» переносят в импортированный конфиг то, что
относится к вашей машине, а не к сборке:
| группа | параметры |
| --- | --- |
| Порты и RCON | `DefaultPort`, `UDPPort`, `RCONPort`, `RCONPassword` |
| Пароль сервера и объявляемый IP | `Password`, `server_browser_announced_ip` |
| Имя и описание сервера | `PublicName`, `PublicDescription` |
Для нового профиля эти значения берутся из активного: порты и пароли на
машине общие. Всё остальное — списки модов, карты, правила и песочница —
приходит из пресета.
Посторонние `.ini` из архива (настройки модов, например) не принимаются за
конфиг сервера: панель опознаёт его по знакомым параметрам.
### Пресеты песочницы одиночной игры
Сборки нередко раздают только пресет для одиночной игры — файл `.cfg` из папки
`Sandbox Presets`. Панель принимает и его: как отдельный файл, и как часть
архива. Внутри это плоский список `Параметр=значение` с точками вместо
вложенности (`ZombieLore.Speed=2`), а серверу нужна таблица Lua, поэтому
значения переносятся по одному.
Если у профиля уже есть свой `SandboxVars.lua`, панель меняет в нём только
значения — комментарии игры, порядок и остальные параметры остаются на месте.
Параметры, которых в файле нет (обычно это настройки модов, пока моды не
установлены и сервер ни разу не запускался), пропускаются, и панель говорит,
сколько перенесла и сколько пропустила. Если файла ещё нет, он создаётся
целиком из пресета.
### Список модов из кнопки «Поделиться»
Игра умеет отдавать набор модов одной строкой: `Название:ModA;ModB;ModC`.
На вкладке «Моды» есть поле «Импорт списка модов» — вставьте её туда, и
список включённых модов заменится на этот, в том же порядке загрузки. Список
попадает в конфиг только после кнопки «Сохранить», так что до неё всё можно
отменить, просто переключив вкладку.
В такой строке нет идентификаторов мастерской, поэтому сама по себе она не
заставит сервер что-то скачать: панель добавит пакеты только тех модов, чьи
файлы уже лежат на диске, и скажет, для скольких модов файлов не нашлось.
Пакеты для них добавляются коллекцией на той же вкладке.
## Моды
Project Zomboid хранит моды в двух строках конфига: `WorkshopItems` — что
скачать из мастерской, `Mods` — что включить и в каком порядке. Панель
показывает их одним списком, где каждая строка — мод:
- **галочка** включает мод (добавляет его Mod ID в `Mods`);
- **перетаскивание** (или стрелки) задаёт порядок загрузки — для модов,
которые переопределяют одни и те же файлы, он решает, чья версия победит;
- **«Убрать»** выбрасывает пакет из `WorkshopItems` вместе со всеми его модами.
Добавить мод можно ссылкой на страницу мастерской или голым ID. Кнопка
**«Добавить коллекцию»** разворачивает коллекцию в список входящих в неё модов
— порядок берётся тот же, что на странице коллекции.
Mod ID, который нужен серверу, лежит внутри самого мода (`mod.info`), поэтому
панель читает уже скачанные моды с диска:
```
<server_dir>/steamapps/workshop/content/108600/<workshop id>/mods/<мод>/mod.info
~/Zomboid/mods/<мод>/mod.info # моды, положенные вручную
```
Пока сервер не скачал мод, его строка помечена «ещё не скачан» и галочка
недоступна: Mod ID неизвестен. Файлы загружает сам сервер при запуске по
списку `WorkshopItems` — то есть достаточно добавить моды, сохранить и
запустить сервер, после чего они станут доступны для включения.
Один пакет мастерской может содержать несколько модов — в списке они идут
отдельными строками, и включать их можно по отдельности.
## Редактор конфигов
Вкладки «Конфиг сервера» и «Песочница» показывают параметры не сплошным
списком, а разделами: «Основное», «PVP и урон», «Убежища», «Античит» и так
далее. Разделы сворачиваются, а поиск сверху раскрывает те, где нашлось
совпадение. Ключи, которых панель не знает (в новой версии игры их всегда
добавляют), не теряются — они собираются в раздел «Прочее».
Подписи и описания у панели свои, на языке интерфейса: игра пишет свои
комментарии на языке сервера, а на Linux он почти всегда английский. Расшифровку
конкретных значений (что означает 1, 2, 3) панель добавляет из самого конфига.
Полный текст виден при наведении, начало — прямо под полем.
Правка меняет ровно ту строку, которую поправили: комментарии, порядок и
переводы строк файла сохраняются, а рядом остаётся копия прежней версии с
расширением `.bak`.
## Профили серверов
Профиль — это отдельный сервер: свой мир, свой `*.ini`, свои настройки
песочницы, свой список модов и свой лимит памяти. Project Zomboid различает их
по параметру `-servername`, поэтому профили не мешают друг другу:
```
~/Zomboid/Server/vanilla.ini ~/Zomboid/Saves/Multiplayer/vanilla/
~/Zomboid/Server/vanilla_SandboxVars.lua
~/Zomboid/Server/modded.ini ~/Zomboid/Saves/Multiplayer/modded/
~/Zomboid/Server/modded_SandboxVars.lua
```
Работает всегда один профиль — активный: серверные файлы, игровые порты и файл
запуска JVM общие. Переключить профиль можно на остановленном сервере, выбрав
его в шапке панели или на вкладке «Профили»; после этого все вкладки —
конфиг, песочница, моды, бэкапы — относятся уже к нему.
При создании профиля можно скопировать `.ini` и настройки песочницы из
существующего: удобно, когда нужен тот же набор правил, но чистый мир.
### Память JVM
`java_memory` — это `-Xmx`, верхняя граница кучи. `java_memory_min` (`-Xms`)
по умолчанию пуст, и это осознанно: равные значения избавляют от роста кучи на
ходу, но заставляют JVM занять весь объём сразу при старте. На машине, где
столько свободной памяти не набирается, сервер тогда не поднимется вовсе —
`Failed to allocate initial Java heap`. Задавайте минимум, только если памяти
заведомо хватает.
Сервер, упавший сразу после запуска три раза подряд, панель перестаёт
поднимать автоматически: обычно так проявляется нехватка памяти или битый
конфиг, и перезапуск делу не помогает. Пока идёт отсчёт до автоподъёма, его
можно отменить кнопкой «Отменить перезапуск».
Удаление профиля убирает его только из панели — мир и конфиги остаются на
диске. Чтобы вернуть профиль, создайте его с тем же идентификатором.
Обновление с версии, где сервер был один, проходит само: старые поля
`server_name`, `admin_password` и `java_memory` превращаются в первый профиль
при первом запуске.
## Обновление
Панель показывает свою версию и следит за релизами в репозитории: вкладка
«Настройки» → «Версия панели». Если вышла новая версия, там появляется кнопка
**«Обновить до …»**.
Обновление из интерфейса устроено так. Сама панель работает от
непривилегированного пользователя и не может ни заменить свой бинарник в
`/usr/local/bin`, ни дёрнуть `systemctl`. Поэтому установщик кладёт рядом
маленький root-скрипт `/usr/local/bin/pzmanager-admin` и разрешает сервисному
пользователю ровно его одну команду:
```
pzserver ALL=(root) NOPASSWD: /usr/local/bin/pzmanager-admin
```
Скрипт не повторяет логику установки, а забирает из релиза `get.sh` (сверив
его sha256) и отдаёт работу ему — поэтому кнопка обновляет всё сразу: бинарник,
скрипты установки и systemd-юнит. Панель к этим файлам доступа не имеет, она
может только попросить выполнить `update` или `restart`.
Работа уходит в отдельный разовый юнит (`systemd-run --unit=pzmanager-update`).
Иначе обновление не пережило бы само себя: панель вызывает скрипт через sudo,
тот живёт в cgroup её юнита, и остановка службы убила бы его на середине.
Побочный эффект — вывод идёт не в консоль панели, а в свой журнал:
```bash
journalctl -u pzmanager-update -f
```
Если правило sudo не установлено (панель поставлена вручную или в системе нет
`sudo`), кнопка не появится, а рядом будет показана команда для терминала:
```bash
curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/get.sh | sudo bash
```
Обновление и перезапуск панели останавливают игровой сервер — панель делает
это сама, штатной командой `quit`, чтобы мир успел сохраниться.
## Доступ снаружи
Панель управляет сервером, поэтому наружу её лучше не выставлять голой. Два
разумных варианта:
**SSH-туннель (по умолчанию)** — ничего настраивать не нужно, панель слушает
только localhost.
**Доверенная локальная сеть** — `PZ_LISTEN=<ip>:8080` при установке или
`listen` в конфиге. Учтите, что HTTP не шифруется: пароль от панели идёт по
сети открытым текстом, так что для доступа через интернет этот вариант не
годится.
**HTTPS через reverse-proxy** — если панель нужна с телефона. Настройте nginx с
сертификатом (например, от Let's Encrypt):
```nginx
server {
listen 443 ssl;
server_name pz.example.com;
ssl_certificate /etc/letsencrypt/live/pz.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pz.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# Живая консоль работает через Server-Sent Events.
proxy_buffering off;
proxy_read_timeout 24h;
}
}
```
И запустите менеджер с флагом `--secure-cookies`, чтобы cookie сессии
передавалась только по HTTPS (добавьте флаг в `ExecStart` юнита).
## Команды
```bash
pzmanager serve # запустить панель (команда по умолчанию)
pzmanager useradd <логин> # создать пользователя панели
pzmanager passwd <логин> # сменить пароль
pzmanager users # список пользователей
```
Флаги: `--config <путь>`, `--listen <адрес>`, `--secure-cookies`.
## Конфигурация
Файл `/opt/pzmanager/config.yaml` создаёт установщик (при запуске без него
менеджер возьмёт `~/.config/pzmanager/config.yaml`). Всё, что в нём есть,
правится и через вкладку «Настройки».
```yaml
listen: 127.0.0.1:8080 # адрес веб-панели; 0.0.0.0 или IP — доступ по сети
data_dir: /opt/pzmanager/data
server_dir: /opt/pzmanager/server # куда SteamCMD ставит сервер
zomboid_dir: /opt/pzmanager/Zomboid # миры, конфиги и логи игры
steamcmd_path: "" # пусто — найти в PATH или скачать
profiles: # серверы: у каждого свой мир и настройки
- id: vanilla # vanilla -> vanilla.ini, Saves/Multiplayer/vanilla
title: Ванильный мир # как профиль подписан в панели
admin_password: "" # пароль админа игрового сервера
java_memory: 4g # -Xmx: верхняя граница кучи JVM
java_memory_min: "" # -Xms: пусто — JVM берёт память по мере надобности
extra_args: [] # доп. аргументы start-server.sh
- id: modded
title: Сборка с модами
java_memory: 8g
active_profile: vanilla # какой профиль запускается и настраивается
autostart: false # поднимать сервер вместе с менеджером
autorestart: true # поднимать сервер после падения
stop_timeout: 1m30s # сколько ждать после quit до SIGTERM
log_buffer_lines: 5000 # строк консоли в памяти
backup:
dir: /opt/pzmanager/backups
schedule: 6h # 0 — отключить автобэкапы
keep: 20 # архивов на профиль
stop_server: false # останавливать сервер на время бэкапа
```
Изменения `listen` применяются после `systemctl restart pzmanager`; остальные —
при следующем запуске игрового сервера.
## Бэкапы
В архив попадают мир активного профиля и его серверные конфиги. Имя файла —
`pz-профиль-ГГГГММДД-ЧЧММСС[-пометка].tar.gz`, так что архивы разных сборок не
путаются, а ротация (`backup.keep`) считается по каждому профилю отдельно.
Архивы, снятые до появления профилей, панель тоже читает и восстанавливает.
Бэкап на живом сервере технически корректен, но снимок может отставать от
состояния мира в памяти. Если хочется гарантированной согласованности —
включите `backup.stop_server` (игроков на время бэкапа выкинет) или отправляйте
`save` в консоль перед ручным бэкапом.
Восстановление требует остановленного сервера. Перед распаковкой менеджер сам
делает страховочную копию с пометкой `pre-restore`.
## Безопасность
- Пароли пользователей панели хранятся в виде bcrypt-хешей в
`data_dir/users.json` (права 0600).
- Сессии живут только в памяти: перезапуск менеджера разлогинивает всех.
- Первого администратора можно создать только с одноразовым кодом из журнала.
- Изменяющие запросы требуют собственный заголовок — это защита от CSRF.
- Панель не выполняет произвольные команды хоста: всё, что она умеет, — это
управление процессом сервера, SteamCMD и файлами в своих директориях.
Тем не менее доступ к панели равносилен доступу к игровому серверу, так что
пароль стоит выбрать не короче того, что вы поставили бы на SSH.
## Сборка и релизы
Репозиторий собирается Gitea Actions ([`.gitea/workflows`](.gitea/workflows)):
- **CI** — на каждый push и pull request в `main`: `gofmt`, `go vet`, тесты,
проверка сборки под Linux и синтаксиса скриптов установки.
- **Release** — на тег `v*`: собирает бинарники под `linux/amd64` и
`linux/arm64`, считает контрольные суммы и публикует релиз с ними и
скриптами установки.
Выпуск новой версии:
```bash
git tag -a v1.2.3 -m "Описание релиза" && git push origin v1.2.3
```
Версия попадает в бинарник через `-ldflags -X main.version=` и видна в
`pzmanager version` и в панели на вкладке «Настройки».
Workflow не зависят от github.com и от Node внутри контейнера: код забирается
через `git`, а релиз публикуется прямо в API Gitea. Токен Actions выдаёт сам;
если в настройках инстанса его права урезаны, заведите секрет `RELEASE_TOKEN`
с доступом на запись в репозиторий.
Для работы Actions нужен зарегистрированный runner с меткой `ubuntu-latest`,
умеющий запускать контейнеры (`docker`).
## Разработка
```bash
make test # тесты
make vet # go vet
make run # локальный запуск с конфигом в ./dev
```
Веб-интерфейс — обычные HTML, CSS и JS в `web/static`, без сборщика; при
`go build` они вшиваются в бинарник через `embed`.
Переводы лежат рядом: строки интерфейса — в `web/static/i18n.js`, подписи
параметров конфигов — в `web/static/hints-ru.json` и `hints-en.json`
(загружается только нужный язык), сообщения самого менеджера — в
`internal/i18n/catalog.go`, где ключом служит русская фраза из кода. Тесты в
`web` и `internal/i18n` следят, чтобы словари не разъезжались.
## Лицензия
[MIT](LICENSE) — делайте с кодом что угодно, но без каких-либо гарантий.