23 KiB
Админка прелендингов — preland#22
Отдельное PHP-приложение для admin.zoomstage.online. Формы редактируют фактические
config.json в общей таблице; сохранённые настройки можно скопировать в выбранные домены. ZIP indpic
проверяется по контракту CPanel и доставляется на выбранные хостинги. Изменение контента
не требует Git/push/deploy; admin/ не входит в публичную поставку прелендингов.
Реквизиты и устаревший utm.chb скрыты и защищены от записи. Неизвестные поля показаны только для чтения и сохраняются.
Спецификация и план — docs/superpowers/{specs,plans}/2026-10-25-preland22-* в репозитории,
копии и фактический статус — в базе знаний, основной реестр — preland#22.
Сводка и конструктор ссылки
Первый экран — редактор фактических конфигов всех подключений. Основной сайт, варианты, веса и default редактируются в соответствующей колонке. Доля вычисляется автоматически:3:1 →75%/25%; нулевые веса дают0%. Default выбирается radio обычного начертания. Отсутствующая группа добавляется только явно. «Сохранить» и «Отмена» работают отдельно для каждого домена; ошибка и конфликт сохраняют черновик. Перечитывание другой колонки сохраняет DOM, фокус и курсор. После конфликта нужно явно загрузить сохранённое либо отменить черновик. Версии CAS остаются внутренними; даты заметок и версии конфигов в таблице не показываются.
Под таблицей — копирование сохранённых host и восьми канонических UTM из одного
источника в несколько получателей. Источник исключён из получателей. Черновики
источника и выбранных получателей сначала нужно сохранить или отменить. Неизвестные
поля, реквизиты, заметки и файлы ZIP не копируются. Отсутствующая группа источника
оставляет группу получателя. Источник и каждая цель проверяются по своей версии;
сохранение/копирование считаются завершёнными после результата задания и перечитывания.
Ошибки отдельных целей видны в колонке и журнале; успешные цели не повторяются.
Конструктор работает отдельно: значения или «Рандом» в восьми списках, затем
«Сформировать и сохранить». Последний выбор и точный 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 группа сопровождается предупреждением;
конструктор не исправляет конфигурацию автоматически.
Компактная сводка и заметки — preland#23 / #24
С версии preland#25 ячейки содержат inline-редактор, описанный выше.
В блоке каждой UTM верхняя строка содержит название один раз и общую заметку
в объединённой ячейке доменов. Ниже — пустая первая ячейка и значения доменов.
Устаревший utm.chb скрыт в сводке и редакторе; остальные неизвестные поля видны.
Редактор заметки —
растягиваемая по высоте textarea на всю ширину объединённой ячейки доменов; сохранение явное.
Черновик сохраняется при обновлении колонок или ошибке запроса. При конфликте
сохранения нужно явно загрузить серверный текст; чужой текст молча не заменяется.
GET /api/utm-notes читает восемь заметок, POST /api/utm-notes принимает
{marker,text,version}. Авторизация/CSRF общие; версия отдельная у каждой заметки,
запись защищена CAS. Plain text до10000 Unicode-символов, пустой текст очищает
заметку. Файл state/utm-notes.json приватный0600, хранится независимо от
config.json, подключений, конструктора ссылок и заданий. Заметки не доставляются
на хостинги и сохраняются после замены контейнера.
Состав и доступы
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-sessions0700, файлы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 не нужен. Замена реквизитов сбрасывает проверку подключения.
Работа через формы
- Изменить основной сайт или UTM в нужной колонке, проверить проценты и default, нажать «Сохранить» под колонкой. Остальные поля сохраняются; ошибка оставляет черновик.
- Для копирования выбрать источник и получателей под таблицей. Сначала сохранить либо отменить их черновики, затем нажать кнопку с числом получателей.
- Проверить итог каждой цели в колонках и журнале. При конфликте явно перечитать сохранённые настройки; неизвестный результат требует проверки журнала и фактического состояния.
- Раздел indpic имеет собственные «Открытый домен» и checkbox целей ZIP/проверки. Выбор копирования настроек на эти цели не влияет. Резерв относится к открытому домену indpic.
- Загрузить ZIP или проверить SFTP-папку, при необходимости назначить новый default в основной таблице: его номер должен присутствовать в весах, допускается вес0. Номера — 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 восстановление нельзя подтвердить.
Сборка, проверки и выпуск
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 сам не доказывает завершение выпуска.
Browser проверки preland#25
ui-matrix.cjs выполняет полный цикл на четырёх disposable local-целях a/b/c/d:
inline input/default/add/remove/missing, Save/Cancel, параллельные черновики,
задержанный GET, настоящий CAS-конфликт и offline/recovery, copy двух получателей,
partial failure/source changed, aliases/unknown/requisites, независимые ZIP-цели,
ошибочный ZIP, конструктор/заметки и1440/390. Скрипту нужны writable mount fixture
в /fixture и output в /output; он возвращает исходные config-файлы в finally.
ui-regression.cjs — совместимая точка входа этого же сценария, отдельно повторять её
не требуется. ui-utm.cjs отдельно проверяет настоящую ZIP-доставку в a+b, variant3
с весом0/default и обратное восстановление через controls; нужен VALID_ZIP набора3.
ui-notes.cjs, ui-link-builder.cjs, ui-verified-refresh.cjs, ui-remember.cjs,
ui-requisites.cjs и ui-smoke.cjs сохраняют отдельные сценарии своих подсистем.
Ни один browser write-test не предназначен для production.
Отдельный ui-session-jobs.cjs удерживает ответ настоящего завершённого Save/Copy
через выход и повторный вход: старое продолжение не сбрасывает новый черновик,
не подменяет результаты/notice и не снимает busy новой операции. Серверные задания
при выходе продолжаются; отменяются только устаревшие UI-продолжения.
Скрытие, удаление и порядок доменов (preland#26)
Значок перечёркнутого глаза справа от домена временно убирает его из таблицы и рабочих списков. Подключение, секрет, проверка соединения, сайт, config.json и наборы сохраняются. В «Скрытых доменах» доступны возврат и удаление подключения. Сервер запрещает удалять видимый домен и домен с незавершённым заданием; сайт и файлы удаление подключения не затрагивает. История сохраняется, результаты скрытых целей не выводятся в рабочих списках.
Маркер слева от названия позволяет перетащить колонку мышью; стрелки на маркере позволяют изменить порядок с клавиатуры. Порядок автоматически сохраняется на сервере и применяется к таблице и спискам выбора для всех входов. Скрытый домен сохраняет свою позицию, новый добавляется в конец. Черновики настроек и заметок при перестановке сохраняются; перед скрытием изменённого домена нужно сохранить или отменить его правки.
API с обычными авторизацией и CSRF: POST /api/targets/visibility принимает
{id,hidden} (hidden — boolean), POST /api/targets/order принимает {ids} —
ровно все видимые ID в нужном порядке; оба возвращают актуальные targets.
Неверный/устаревший набор ID отвергается, скрытые позиции не перезаписываются.
GET /api/state содержит все подключения, включая hidden; интерфейс отдельно
фильтрует рабочие списки. Скрытие не отменяет уже запущенное задание.
Ответы API401 не отправляют Set-Cookie: задержанный ответ старого запроса не может
перезаписать cookie нового успешного входа. Истёкший сеанс отзывается на сервере,
а явный выход (200) удаляет cookie. UI также отбрасывает ответы прежнего сеанса.
Проверка ui-target-login-race.cjs включает следующий авторизованный запрос и
сохранение порядка после задержанного401, а не только видимость рабочей области.