142 lines
14 KiB
Markdown
142 lines
14 KiB
Markdown
# Админка прелендингов — preland#22
|
||
|
||
Отдельное PHP-приложение для `admin.zoomstage.online`. Формы редактируют фактические
|
||
`config.json`, выбранные поля применяются к одному или нескольким доменам. ZIP indpic
|
||
проверяется по контракту CPanel и доставляется на выбранные хостинги. Изменение контента
|
||
не требует Git/push/deploy; `admin/` не входит в публичную поставку прелендингов.
|
||
|
||
Редактирование реквизитов отключено по поручению владельца: `requisites` видны
|
||
в сводке, но не создают полей редактора и не входят в «Выбрать все поля».
|
||
|
||
Спецификация и план — `docs/superpowers/{specs,plans}/2026-10-25-preland22-*` в репозитории,
|
||
копии и фактический статус — в базе знаний, основной реестр — preland#22.
|
||
|
||
## Сводка и конструктор ссылки
|
||
|
||
Первый экран читает сохранённые конфиги всех подключений: основной сайт, реквизиты,
|
||
дополнительные поля, default и все веса UTM. У каждой колонки своё время чтения,
|
||
версия, обновление и ошибка; при отказе прежний снимок явно помечается. Обновление
|
||
сводки не сбрасывает черновик редактора и не меняет его версию для CAS.
|
||
|
||
Конструктор работает отдельно: значения или «Рандом» в восьми списках, затем
|
||
«Сформировать и сохранить». Последний выбор и точный URL сохраняются на сервере
|
||
отдельно по ID домена, восстанавливаются после перезагрузки. Изменение списка скрывает
|
||
старую ссылку до явного сохранения. Значащая цифра стоит на пятой позиции метки,
|
||
`0` означает выбор по весам. Конструктор не меняет config, default, веса или наборы,
|
||
не создаёт задания; сохранённый выбор доступен и при недоступном хостинге.
|
||
|
||
`GET /api/link-builder?target=ID` читает выбор, `POST /api/link-builder` сохраняет
|
||
`{target,selection}` с ровно восемью каноническими UTM-ключами. Действуют общий вход
|
||
и CSRF. Состояние находится в приватном `state/link-builder-ID.json` с режимом0600.
|
||
При переименовании домена меняется origin ссылки, выбор и сохранённые метки остаются.
|
||
Неизвестная в текущем runtime/catalog группа сопровождается предупреждением;
|
||
конструктор не исправляет конфигурацию автоматически.
|
||
|
||
## Состав и доступы
|
||
|
||
`compose.yaml` запускает web и отдельный worker из одного неизменяемого образа. Нужны
|
||
PHP8.3, ZIP, Imagick с AVIF/WebP/JPEG и phpseclib3 из зафиксированного Composer lock.
|
||
На Windows ничего устанавливать не требуется: сборка и проверки выполняются Docker.
|
||
|
||
На сервере `/srv/preland-admin`: `compose.yaml`, `.env` с `ADMIN_IMAGE`, закрытый
|
||
`secret.env`, `state/`, `secrets/` и `deploy/`. В `secret.env` задаются `ADMIN_USERNAME`
|
||
и `ADMIN_PASSWORD_HASH`; bcrypt-значение заключается в одинарные кавычки, чтобы Compose
|
||
не интерполировал `$`. Пароль хешируется через `bin/password-hash.php` со stdin.
|
||
В HTTP Secure-cookie включена по умолчанию; `ADMIN_COOKIE_SECURE=0` разрешена только
|
||
на изолированном HTTP-стенде.
|
||
|
||
PHP-сессии хранятся в `state/sessions` с режимом0700, файлы имеют режим0600.
|
||
Пересоздание web-контейнера сохраняет вход; срок бездействия остаётся3600секунд.
|
||
Хранилище сессий не допускает относительный путь, symlink или публичный каталог.
|
||
При первом переходе со старого ephemeral backing существующие session-файлы
|
||
нужно перенести приватно до пересоздания; сам код не мигрирует старый `/tmp`.
|
||
|
||
Необязательная галочка «Запомнить меня на 90 дней» создаёт persistent cookie
|
||
`preland_admin` с HttpOnly/Secure/SameSite=Strict. Срок фиксируется при входе,
|
||
чтения его не продлевают. Данные такого входа лежат отдельно в
|
||
`state/remember-sessions`0700, файлы0600. Без галочки остаются session-only cookie
|
||
и прежний час бездействия. Анонимные обращения не создают долгоживущих файлов.
|
||
«Выйти» удаляет серверный сеанс и cookie немедленно; пароль не сохраняется.
|
||
|
||
UID/GID приложения — 997:987. Приложение получает приватный state и ровно два local
|
||
mount: `/targets/zoompixel.ru`, `/targets/zoomstage.online`. Весь `/srv/preland`, Docker
|
||
socket, root SSH key и база основного сайта в приложение не передаются. Права публикации
|
||
на каждом корне проверяются реальным временным файлом/rename при проверке подключения;
|
||
нельзя включать запись до успешной проверки.
|
||
|
||
REG.RU ограничивает размер SSH exec-команды: длинный inline helper разрывал
|
||
соединение после успешного входа. Доверенный helper передаётся сжатым zlib;
|
||
финальная команда ограничена7000байт, большие payload идут в закрытый SFTP-inbox.
|
||
На удалённом PHP требуется `gzinflate`; отсутствие возвращает понятную ошибку.
|
||
Содержимое inbox удаляется и при нормальном завершении, и резервно при обрыве.
|
||
|
||
Два REG.RU-хостинга подключаются SFTP/SSH с их обычными ограниченными учётными записями.
|
||
Нужны абсолютный корень, host key либо SHA256 fingerprint, пароль или приватный ключ,
|
||
доступный через stdin CLI `bin/target-import.php`. Секрет хранится 0600 в приватном state,
|
||
по API не возвращается. На удалённом хосте нужен `php` CLI >=8.1 с `fsync`; удалённый
|
||
Imagick не нужен. Замена реквизитов сбрасывает проверку подключения.
|
||
|
||
## Работа через формы
|
||
|
||
1. Выбрать исходный домен: данные и SHA-версия читаются с его хостинга.
|
||
2. Изменить поля. Отмечаются только введённые поля; можно явно выбрать группу/всю форму.
|
||
3. Отметить домены и нажать «Применить к выбранным». Каждый домен использует собственную
|
||
прочитанную версию и сохраняет свои остальные значения. После сохранения перечитать домен.
|
||
4. В журнале проверить отдельные статусы. Успешные цели не повторяются. Конфликт требует
|
||
перечитать данные; после неизвестного результата повтор сначала сверяет фактический хеш.
|
||
5. В indpic выбрать ZIP либо проверить уже загруженную SFTP-папку. Новый набор сам не
|
||
меняет default/веса. Чтобы назначить новый номер indpic default, он должен присутствовать
|
||
в таблице весов — можно с весом0. Это действующее правило resolver, а не вес трафика.
|
||
|
||
Номера — 1–9, ZIP содержит папку номера и ровно90 изображений установленных форматов/
|
||
размеров; лимит8МиБ на файл,64МиБ распакованного набора. При нулевой сумме весов rnd
|
||
использует первый ключ weights по порядку config.json; только cons использует default.
|
||
Имена numeric/legacy UTM адаптируются отдельно для каждой цели, конфликтующие aliases
|
||
блокируются. Удаление набора из реестра оставляет файлы; используемый номер удалить нельзя.
|
||
|
||
## Каталог групп и приватная публикация
|
||
|
||
`deploy/export-catalog.php` выполняется внутри существующего API-контейнера минимальным
|
||
Yii bootstrap, в read-only DB-транзакции. Он экспортирует только номера/названия degr/tp/cons,
|
||
не выдавая приложению доступ к БД. `deploy/refresh-catalog.sh` запускается host cron раз
|
||
в минуту от root. Подготовка/проверка/chown происходят в root-only `secrets/`, затем
|
||
`mv -fT` заменяет фиксированный `state/catalog.json` без следования ссылке назначения.
|
||
Снимок действует180секунд только для совпадающего основного host. Недоступный/старый
|
||
каталог даёт явную пометку непроверенного существования групп, а не выдуманный список.
|
||
|
||
Резервы и journal локального транспорта хранятся в приватном state. Для exact bind mount
|
||
папка подготовки `.preland-admin-staging` создаётся внутри корня с0700; публичный веб-сервер
|
||
другого UID обязан получать403 и через внешний origin, и через прямой loopback Apache.
|
||
При наличии nginx-правила deny оно служит дополнительной защитой. Одной надежды на
|
||
`.htaccess` недостаточно: AllowOverride может быть выключен. Каталог активного `/N/` —0755,
|
||
файлы0644. SFTP использует приватную sibling-папку вне webroot.
|
||
|
||
Переключение каталогов имеет два rename и durable journal. При аварии upload-child
|
||
supervisor сразу пытается восстановить полный прежний/новый набор под тем же lock;
|
||
при полном перезапуске это делает следующая операция транспорта. Неподтверждённый
|
||
результат остаётся unknown. Это восстановление после прерывания, а не обещание отсутствия
|
||
краткого окна между двумя rename. При недоступном SSH восстановление нельзя подтвердить.
|
||
|
||
## Сборка, проверки и выпуск
|
||
|
||
```sh
|
||
docker build -t preland-admin:<git-sha> -f admin/Dockerfile admin
|
||
docker run --rm --entrypoint composer -v "$PWD:/work:ro" -w /work/admin preland-admin:<git-sha> test
|
||
docker run --rm --entrypoint bash -v "$PWD:/work:ro" preland-admin:<git-sha> -c 'sed "s/\r$//" /work/admin/tests/catalog-publish.sh | bash'
|
||
```
|
||
|
||
Дополнительные Docker проверки: `tests/backend-sftp.Dockerfile` — настоящий SSH/SFTP,
|
||
`tests/backend-worker.php` — цикл с паузой65секунд, `tests/ui-*.cjs` — реальные browser
|
||
сценарии. Последние выполняются только на disposable целях; они изменяют и восстанавливают
|
||
настройки. `NODE_PATH=/e2e/node_modules`, `CHROMIUM_PATH` задаёт браузер имеющегося e2e-образа.
|
||
|
||
Для production подготовить отдельный TLS/nginx по `deploy/nginx.conf`, импортировать
|
||
проверенные подключения, сохранить резерв состояния, указать точный image в `.env`, затем
|
||
`docker compose up -d`. Проверить web/worker health, вход, четыре чтения, безопасный реальный
|
||
цикл сохранения/перечитывания, ZIP и публичные изображения. Не заменять runtime-конфиги
|
||
Git-снимком. Для отката приложения вернуть прежний `ADMIN_IMAGE`; state и контент сохраняются.
|
||
Возврат контента выполняется отдельно через резерв и CAS, не через старый Docker-образ.
|
||
|
||
Фактические production-команды, версии, ограничения, свидетельства UI и статус подключения
|
||
четырёх доменов фиксируются в preland#22 и KB; этот README сам не доказывает завершение выпуска.
|