Files
preland/admin/README.md
T

274 lines
30 KiB
Markdown
Raw 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.
# Админка прелендингов — 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 редактируются в соответствующей колонке. Веса хранятся в процентах:
0..100 с двумя знаками; сумма группы — 100% либо все веса — 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 ссылки, выбор и сохранённые метки остаются.
Неизвестный в текущем config номер сопровождается предупреждением;
конструктор не исправляет конфигурацию автоматически.
## Компактная сводка и заметки — 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-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. Изменить основной сайт или UTM в нужной колонке, проверить проценты и default,
нажать «Сохранить» под колонкой. Остальные поля сохраняются; ошибка оставляет черновик.
2. Для копирования выбрать источник и получателей под таблицей. Сначала сохранить
либо отменить их черновики, затем нажать кнопку с числом получателей.
3. Проверить итог каждой цели в колонках и журнале. При конфликте явно перечитать
сохранённые настройки; неизвестный результат требует проверки журнала и фактического состояния.
4. Раздел indpic имеет собственные «Открытый домен» и checkbox целей ZIP/проверки.
Выбор копирования настроек на эти цели не влияет. Резерв относится к открытому домену indpic.
5. Загрузить ZIP или проверить SFTP-папку, при необходимости назначить новый default
в основной таблице: его номер должен присутствовать в весах, допускается вес0.
Номера — 1–9, ZIP содержит папку номера и ровно90 изображений установленных форматов/
размеров; лимит8МиБ на файл,64МиБ распакованного набора. При нулевой сумме весов rnd
использует первый ключ weights по порядку config.json; только cons использует default.
Имена numeric/legacy UTM адаптируются отдельно для каждой цели, конфликтующие aliases
блокируются. Удаление набора из реестра оставляет файлы; используемый номер удалить нельзя.
## Ручные варианты и независимость — preland#30
Во всех девяти UTM ссылка «Добавить вариант» создаёт пустую строку: номер, вес и
неотмеченный default. До нажатия пустых строк нет. Существующие номера показаны
текстом; новые вводятся вручную (1–9, для part только 1 или 2). После сохранения новый номер тоже становится
обычным текстом. Нулевой вес сохраняется. Отсутствующую группу можно создать через
ту же ссылку; для неё default выбирается явно. «Отмена» возвращает последний прочитанный конфиг.
Редактор не обращается к каталогу основной админки и не проверяет реализацию номера
на сайте: можно записать theme=3 или indpic=9 независимо от наличия темы/файлов.
Соответствие номера содержимому сайта обеспечивает пользователь. Формат номера,
дубликаты, aliases, проценты и CAS проверяются; конфиг не нормализуется автоматически.
Сохранение, копирование и восстановление одинаково независимы от каталога/наборов.
Новый сет загружается отдельно: выбрать домены, вручную указать номер, выбрать ZIP,
нажать «Загрузить сет». Для существующего номера нужно подтвердить замену.
Проверка ZIP и изображений сохраняется, как и проверка файлов, загруженных по SFTP.
Загрузка не меняет config.json. После неё пользователь сам добавляет вариант indpic,
заполняет вес/default и сохраняет настройки. Автоматической связи нет.
## Приватное состояние и публикация наборов
Резервы и 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 проверки: `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, а не только видимость рабочей области.
## Номера вариантов и чтение с хостинга (#29)
В строках UTM редактируются только вес и default; номера отображаются текстом,
добавление/удаление вариантов и создание отсутствующей группы из таблицы убраны.
Копирование сохранённых групп остаётся доступно. API config возвращает read_at UTC
как метаданные успешного Transport-чтения, не записывая его в config.json.
Колонка показывает время последнего успешного чтения МСК с секундами, отдельные
loading/error/retry состояния и «Записано на хостинг» после подтверждённых Save/Copy.
Ошибка сохраняет прошлую дату; ввод во время чтения сохраняет черновик, но обновляет
метаданные чтения. Сохранение блокируется до завершения чтения.
## Процентный контракт (#28)
В `config.json` веса известных UTM хранятся непосредственно в процентах: числа
0–100 с точностью до двух знаков; сумма группы — 100 либо все значения равны 0.
Поле «Вес, %» показывает эти же числа, без пересчёта и перераспределения ввода.
Номер варианта и default не меняются при изменении веса. Промежуточная неверная
сумма допускается в черновике и блокирует сохранение с понятной причиной.
Config/Transport проверяют единый контракт для Save, Copy, Restore и проверки
нового подключения. Старые относительные конфиги и резервные копии не публикуются;
сами архивы сохраняются. Совместимость со старой единицей весов не поддерживается
по прямому решению владельца. Не путать это с неизменными alias/defaulttypes.
Публичный выбор варианта остаётся прежним: масштабирование до процентов сохраняет
доли; поведение полностью нулевой группы не заменяется новым fallback.
`PercentMigration` применяется только явно при миграции: заменяет числовые токены
известных weights, сохраняя остальные байты. Доли округляются независимо до сотых;
если сумма округлений не равна 100, операция останавливается до записи всех целей.
Остаток автоматически не распределяется. Приватный CLI с планом/receipt/CAS делает
резервные копии и проверяет точные bytes после каждой записи; в public его нет.
## Независимые UTM-наборы — preland#32
Дискета перед глазиком создаёт снимок текущих UTM-полей таблицы, включая ещё не опубликованные правки. Сохранение набора не пишет на хостинг. Обязательно собственное название; дубликат без учёта регистра отклоняется. Приватный utm-presets.json содержит только id/name/utm: ни домен, ни ID подключения, ни происхождение не сохраняются. Host, заметки, реквизиты и файлы сетов не входят в набор.
GET/POST/DELETE /api/utm-presets требуют вход; записи требуют CSRF. POST принимает только name и utm (канонические группы, только default/weights). DELETE принимает только id. Копирование POST /api/copy принимает preset и targets с ID/версией каждого получателя; прежний источник-домен больше не поддерживается. Очередь фиксирует собственный UTM-снимок, поэтому удаление набора или любого подключения-источника не влияет на задание. Получателем может быть любой доступный домен. Основной сайт и заметки сохраняются, отсутствующая группа набора не удаляет группу получателя. CAS, резерв и подтверждённое перечитывание сохранены.
Проверки: tests/utm-presets.php, обновлённый tests/copy.php и ui-utm-presets.cjs; helper preset-ui-helper.cjs адаптирует прежние copy-регрессии к независимому источнику. UI покрывает черновик без публикации, имена/дубликаты, два получателя, reload, гонку list GET/delete, 401/relogin, отмену удаления и API-удаление, мобильную ширину. Тестовые удаления выполняются только над созданными этим тестом наборами.
api#347: part — девятая UTM, пятый символ1/2, default1. Поддерживается редактором, именованными UTM-наборами, копированием, заметками и конструктором. Preland только передаёт метку, ссылка «Для бизнеса» там не добавляется. Runtime config автоматически не дополняется. Старые сохранённые ссылки получают независимый part1 без изменения остальных query-шумов; recovery использует общий приоритет сохранённого snapshot перед текущим resolved fallback.