Author SHA1 Message Date
Panfilov Konstantin b4c4a2ca27 документация: обновить адрес Gitea 2026-11-03 23:55:31 +03:00
Panfilov Konstantin ba33a76a34 feat: неблокирующий сбор User Flow и гостевых ошибок (api#376 api#377)
deploy-preland / deploy zoomstage.online (push) Successful in 1m1s
2026-10-31 13:29:45 +03:00
Panfilov Konstantin 791761a782 fix: тематическое окно ошибки загрузки (#33)
deploy-preland / deploy zoomstage.online (push) Successful in 1m7s
2026-10-28 09:07:00 +03:00
Panfilov Konstantin 4a6683f335 test: учитывать текущие UTM defaults владельца в stage CI #347
deploy-preland / deploy zoomstage.online (push) Successful in 1m0s
2026-10-28 05:34:19 +03:00
Panfilov Konstantin 2970f990e0 docs: сохранить фактический stage config без изменения runtime #347
deploy-preland / deploy zoomstage.online (push) Failing after 35s
2026-10-28 05:15:31 +03:00
Panfilov Konstantin 875549a5af fix(admin): уточнить ошибку допустимого значения part #347 2026-10-28 05:11:28 +03:00
Panfilov Konstantin 01f83375a7 docs: уточнить допустимые варианты part #347 2026-10-28 05:10:43 +03:00
Panfilov Konstantin 469bec1595 api#347: обновить полный перечень девяти UTM 2026-10-28 05:06:53 +03:00
Panfilov Konstantin f7d1792e23 api#347: сохранить part в именованных наборах и процентных контрактах 2026-10-28 05:06:11 +03:00
Panfilov Konstantin 1993f7b4b1 api#347: проверить старые recovery snapshots с default2 2026-10-28 05:04:08 +03:00
Panfilov Konstantin 0d350ed14b api#347: передавать и настраивать UTM part без ссылки в Preland 2026-10-28 05:04:08 +03:00
Panfilov Konstantin 94619432a3 feat(admin): независимые именованные UTM-наборы 2026-10-26 18:38:11 +03:00
Panfilov Konstantin 941a4f01aa feat(admin): ручные варианты UTM и независимый редактор config (#30) 2026-10-26 05:42:03 +03:00
Panfilov Konstantin 47405b27d8 feat(admin): единые процентные веса без старого формата (#28) 2026-10-26 04:56:32 +03:00
Panfilov Konstantin a22519e754 fix(admin): номера UTM и дата чтения с хостинга (#29) 2026-10-26 04:42:40 +03:00
Panfilov Konstantin 00fbef9ede feat(admin): скрытие доменов и сохраняемый порядок колонок (#26) 2026-10-26 04:11:44 +03:00
Panfilov Konstantin dd6d221dd7 feat(admin): edit domain settings inline and copy saved configurations
deploy-preland / deploy zoomstage.online (push) Successful in 57s
2026-10-26 03:06:11 +03:00
Panfilov Konstantin 84f70b1c71 fix(admin): align UTM notes and block obsolete chb publication
deploy-preland / deploy zoomstage.online (push) Successful in 54s
2026-10-25 23:16:35 +03:00
Panfilov Konstantin f2097d4800 feat(admin): compact UTM percentages and shared notes
deploy-preland / deploy zoomstage.online (push) Successful in 1m1s
2026-10-25 22:59:37 +03:00
Panfilov Konstantin 4fc4fd8162 fix(admin): route multi-megabyte sets to SFTP before shell escaping
deploy-preland / deploy zoomstage.online (push) Successful in 1m3s
2026-10-25 22:14:15 +03:00
Panfilov Konstantin 6ec813bc99 fix(admin): refresh verified connection state without losing drafts
deploy-preland / deploy zoomstage.online (push) Successful in 56s
2026-10-25 19:27:09 +03:00
Panfilov Konstantin 522aaf8554 feat(admin): remember login for 90 days and support REG SSH limits
deploy-preland / deploy zoomstage.online (push) Successful in 57s
2026-10-25 19:17:51 +03:00
Panfilov Konstantin d04b40519e fix(admin): persist private sessions across container replacement
deploy-preland / deploy zoomstage.online (push) Successful in 58s
2026-10-25 18:26:41 +03:00
Panfilov Konstantin 2cfa2e2d2e feat(admin): add runtime overview and independent saved UTM link builder
deploy-preland / deploy zoomstage.online (push) Successful in 57s
2026-10-25 18:17:23 +03:00
Panfilov Konstantin 48c71f3300 fix(admin): show verified catalog groups in UTM forms
deploy-preland / deploy zoomstage.online (push) Successful in 1m3s
2026-10-25 11:53:20 +03:00
Panfilov Konstantin 9515c1c62b feat: единая админка runtime-конфигов и indpic (#22)
deploy-preland / deploy zoomstage.online (push) Successful in 1m1s
2026-10-25 11:33:56 +03:00
Panfilov Konstantin 5b6b2d2053 docs: план выполнения админки прелендингов (#22) 2026-10-25 10:46:16 +03:00
Panfilov Konstantin b3b14ec193 docs: проект единой админки конфигов и indpic (#22) 2026-10-25 10:35:35 +03:00
Panfilov Konstantin 847c642ca6 preland#21: закрепить три боевых .ru и отключение остальных .online 2026-10-25 09:24:40 +03:00
Panfilov Konstantin a1aa25671f preland#21: готовые доменные SVG Zoomika, Zoomila и ZoomPixel 2026-10-25 09:09:18 +03:00
Panfilov Konstantin 965eb2e6a5 fix(utm): заменить публичную chob на banch (#327)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-25 05:09:55 +03:00
Panfilov Konstantin 08febba368 api#326 Сохранить прежний random и чужие JSON токены
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-25 02:10:45 +03:00
Panfilov Konstantin 81e189ca67 api#326 Числовые UTM и новый шум каждого перехода 2026-10-25 02:05:48 +03:00
Panfilov Konstantin f30742c2fb preland#18 #19: popup ошибки загрузки и однострочный ИИ-заголовок
deploy-preland / deploy zoomstage.online (push) Successful in 55s
2026-10-24 10:59:20 +03:00
Panfilov Konstantin f4ce5afdc4 docs: snapshot verified stage consent runtime config (#17)
deploy-preland / deploy zoomstage.online (push) Successful in 43s
2026-10-24 05:05:45 +03:00
Panfilov Konstantin 89cc16eb4a feat: replace chb with consent groups and preserve runtime config (api#317, #17)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-24 04:35:44 +03:00
Panfilov Konstantin 188b78a115 fix: desktop-кнопка «Выберите файл» (api#315)
deploy-preland / deploy zoomstage.online (push) Successful in 41s
2026-10-23 23:36:34 +03:00
Panfilov Konstantin f87bef2443 fix: резервные форматы примеров и stage snapshot (#16)
deploy-preland / deploy zoomstage.online (push) Successful in 42s
2026-10-23 20:10:51 +03:00
Panfilov Konstantin 0e6611da73 fix(ui): ограничить fallback высоты первым экраном #16 2026-10-23 19:46:41 +03:00
Panfilov Konstantin 198ba573f9 fix(ui): согласовать первый экран и pending #16 2026-10-23 19:40:47 +03:00
Panfilov Konstantin 01933546a7 fix(theme): mirror bounded cookie propagation reconciliation
deploy-preland / deploy zoomstage.online (push) Successful in 39s
2026-10-21 22:05:02 +03:00
Panfilov Konstantin ad8c8eb849 build(theme): сохранять LF общих partial при export (#13) 2026-10-21 20:55:28 +03:00
Panfilov Konstantin 5ffb0f37e9 fix(theme): зеркалить canonical cookie при первом boot (#13) 2026-10-21 20:50:25 +03:00
Panfilov Konstantin c6b2b227c0 fix(theme): сохранять текущий выбор без storage (#13) 2026-10-21 20:46:23 +03:00
Panfilov Konstantin 9c06977e03 feat(theme): локальный выбор и показанная тема handoff (#13) 2026-10-21 20:42:28 +03:00
Panfilov Konstantin 3740839321 fix: совпадение видимого логотипа Preland и Tour (refs #12)
deploy-preland / deploy zoomstage.online (push) Successful in 40s
2026-10-21 17:35:07 +03:00
Panfilov Konstantin ec087ea636 feat: готовый бренд ZoomStage и минимальный доменный подвал (refs #12)
deploy-preland / deploy zoomstage.online (push) Successful in 44s
2026-10-21 17:24:14 +03:00
Panfilov Konstantin a2c3197a16 merge: сохранить историю локального Android-коммита без изменения проверенного дерева (refs #11)
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-21 02:30:12 +03:00
Panfilov Konstantin 57196e32a9 docs: уточнить канонический SVG-контракт (refs #11) 2026-10-21 02:30:12 +03:00
Panfilov Konstantin f2ad7ecb4c merge: синхронизировать main с проверенным staging без старого кода (refs #11)
deploy-preland / deploy zoomstage.online (push) Successful in 45s
2026-10-21 02:27:08 +03:00
Panfilov Konstantin e9a2a0d568 fix: актуальные ресурсы Zoomzilla и проверяемый выпуск (refs #11) 2026-10-21 02:27:08 +03:00
Panfilov Konstantin 144dda6d88 fix: стабильная область кнопки при смене шрифта (api#301)
deploy-preland / deploy zoomstage.online (push) Successful in 34s
2026-10-20 20:11:13 +03:00
Panfilov Konstantin 0c9a406d68 fix: единая загрузка примеров и стабильный первый кадр (api#301)
deploy-preland / deploy zoomstage.online (push) Successful in 33s
2026-10-20 19:52:30 +03:00
Panfilov Konstantin 75ccf14889 Показывать ошибку после восстановления истории (#9)
deploy-preland / deploy zoomstage.online (push) Successful in 36s
2026-10-20 18:52:35 +03:00
Panfilov Konstantin 6a2532cb87 Обработать browser storage ошибки и показывать восстановленную форму (#9) 2026-10-20 18:43:25 +03:00
Panfilov Konstantin 4c5d3de7bc Merge remote-tracking branch 'origin/staging' into fix/issue299-resume 2026-10-20 18:33:12 +03:00
Panfilov Konstantin 5afa71ec84 Изолировать callbacks отменённой загрузки (#9) 2026-10-20 18:33:12 +03:00
Panfilov Konstantin d70d396670 Восстанавливать загрузку30мин после Reload и удерживать Back (#9) 2026-10-20 18:31:43 +03:00
Panfilov Konstantin 1a20ce17d5 Согласовать каркас Preland с Tour (api#298)
deploy-preland / deploy zoomstage.online (push) Successful in 36s
2026-10-20 18:16:35 +03:00
Panfilov Konstantin f2e6877013 fix(ui): загрузка и общий контракт сравнения (refs #8 #7)
deploy-preland / deploy zoomstage.online (push) Successful in 32s
2026-10-20 01:41:30 +03:00
Panfilov Konstantin 7eaf1c12b0 feat(ui): равные desktop плашки сравнения как в Index (refs #6)
deploy-preland / deploy zoomstage.online (push) Successful in 30s
2026-10-20 00:18:11 +03:00
Panfilov Konstantin c30e3239a0 feat(ui): общий бренд и компактная мобильная обработка (refs #5)
deploy-preland / deploy zoomstage.online (push) Successful in 30s
2026-10-19 22:41:49 +03:00
Panfilov Konstantin 8274946610 chore(ci): отключить production и legacy автодеплой (refs #4) 2026-10-19 14:57:39 +03:00
Panfilov Konstantin d2637e8f61 chore(ci): только stage, Барбариска выведена (refs #4)
deploy-preland / deploy zoomstage.online (push) Successful in 35s
2026-10-19 14:57:39 +03:00
Panfilov Konstantin 0f06a18622 feat: компактный Index и крупные миниатюры (refs anton/api#283) 2026-10-19 14:53:57 +03:00
Panfilov Konstantin 0274d5d921 fix: стабильные размеры Index при скрытии тулбаров 2026-10-19 14:53:57 +03:00
Panfilov Konstantin dcb914a429 fix(upload): кликабельная мобильная кнопка 2026-10-19 14:53:57 +03:00
Panfilov Konstantin ab84dc1629 feat(upload): фото и файлы отдельными путями на Android 2026-10-19 14:53:57 +03:00
Panfilov Konstantin 029cd16d46 feat: компактный Index и крупные миниатюры (refs anton/api#283)
deploy-preland / deploy barbariskatest (push) Failing after 1m36s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 38s
deploy-preland / deploy zoompixel.ru (push) Successful in 37s
2026-10-19 14:38:31 +03:00
Panfilov Konstantin 3319499d5e fix: стабильные размеры Index при скрытии тулбаров
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 33s
deploy-preland / deploy zoompixel.ru (push) Successful in 34s
deploy-preland / deploy barbariskatest (push) Failing after 1m47s
2026-10-19 13:48:44 +03:00
Panfilov Konstantin 953c7dd8f2 fix(upload): кликабельная мобильная кнопка
deploy-preland / deploy barbariskatest (push) Successful in 57s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 37s
deploy-preland / deploy zoompixel.ru (push) Successful in 34s
2026-10-19 12:26:21 +03:00
Panfilov Konstantin 21f8fbb6a4 feat(upload): фото и файлы отдельными путями на Android 2026-10-19 12:20:22 +03:00
Panfilov Konstantin 3ad7be0db3 feat(upload): фото и файлы отдельными путями на Android 2026-10-19 12:19:33 +03:00
Panfilov Konstantin b4dedcfea5 fix: preserve stage example sets during deploy
deploy-preland / deploy barbariskatest (push) Skipped
deploy-preland / deploy zoomstage.online (push) Successful in 33s
deploy-preland / deploy zoompixel.online (push) Skipped
deploy-preland / deploy zoompixel.ru (push) Skipped
2026-10-18 04:28:06 +03:00
Panfilov Konstantin 19547d1797 fix(ci): deploy only changed legacy application files
deploy-preland / deploy barbariskatest (push) Successful in 32s
deploy-preland / deploy zoompixel.online (push) Successful in 37s
deploy-preland / deploy zoompixel.ru (push) Successful in 33s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 01:04:06 +03:00
Panfilov Konstantin 56456bb0cb fix(ci): tolerate existing legacy FTP directories
deploy-preland / deploy zoompixel.online (push) Successful in 35s
deploy-preland / deploy zoompixel.ru (push) Successful in 52s
deploy-preland / deploy barbariskatest (push) Failing after 1m49s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:57:20 +03:00
Panfilov Konstantin 78b24c91f8 fix(ci): skip root directory creation on legacy FTP
deploy-preland / deploy barbariskatest (push) Failing after 30s
deploy-preland / deploy zoompixel.online (push) Successful in 28s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.ru (push) Successful in 32s
2026-10-18 00:53:41 +03:00
Panfilov Konstantin be030f25a0 fix(ci): source legacy FTPS command file
deploy-preland / deploy barbariskatest (push) Failing after 35s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
2026-10-18 00:50:00 +03:00
Panfilov Konstantin 498e2df7b9 fix(ci): use FTPS client for legacy assets
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.ru (push) Successful in 29s
deploy-preland / deploy barbariskatest (push) Failing after 30s
deploy-preland / deploy zoompixel.online (push) Successful in 33s
2026-10-18 00:46:02 +03:00
Panfilov Konstantin 97fe9646b7 fix(ci): verify legacy FTP retry payloads
deploy-preland / deploy zoompixel.ru (push) Successful in 28s
deploy-preland / deploy barbariskatest (push) Failing after 44s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
2026-10-18 00:40:02 +03:00
Panfilov Konstantin d646f98aa7 fix(ci): upload legacy files independently
deploy-preland / deploy barbariskatest (push) Failing after 45s
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 33s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
2026-10-18 00:34:31 +03:00
Panfilov Konstantin 35b6a334ba fix(ci): clone legacy release without node checkout
deploy-preland / deploy zoompixel.online (push) Successful in 32s
deploy-preland / deploy zoompixel.ru (push) Successful in 32s
deploy-preland / deploy barbariskatest (push) Failing after 43s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:28:31 +03:00
Panfilov Konstantin a0dee79b22 fix(ci): run legacy release on production runner
deploy-preland / deploy zoompixel.online (push) Successful in 35s
deploy-preland / deploy zoompixel.ru (push) Successful in 33s
deploy-preland / deploy barbariskatest (push) Failing after 32s
deploy-preland / deploy zoomstage.online (push) Skipped
2026-10-18 00:19:05 +03:00
Panfilov Konstantin 730042385d fix(ci): use Alpine php83 for host config
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 34s
deploy-preland / deploy zoompixel.ru (push) Successful in 31s
deploy-preland / deploy barbariskatest (push) Canceled after 9m51s
2026-10-18 00:08:46 +03:00
Panfilov Konstantin a86978a499 chore(config): snapshot stage focus defaults
deploy-preland / deploy zoomstage.online (push) Skipped
deploy-preland / deploy zoompixel.online (push) Successful in 31s
deploy-preland / deploy zoompixel.ru (push) Successful in 37s
deploy-preland / deploy barbariskatest (push) Failing after 5m1s
2026-10-17 20:51:44 +03:00
Panfilov Konstantin ac4cc21361 fix(config): enable explicit face focus 2026-10-17 20:51:44 +03:00
Panfilov Konstantin f14ce88cc6 fix(deploy): preserve host preland config 2026-10-17 20:48:52 +03:00
Panfilov Konstantin 9835bb0ef8 feat(utm): propagate join focus mode 2026-10-17 20:46:33 +03:00
Panfilov Konstantin 2180d9e37b chore(config): snapshot stage focus defaults
deploy-preland / deploy zoompixel.ru (push) Skipped
deploy-preland / deploy barbariskatest (push) Skipped
deploy-preland / deploy zoomstage.online (push) Successful in 33s
deploy-preland / deploy zoompixel.online (push) Skipped
2026-10-17 20:40:46 +03:00
242 changed files with 9078 additions and 888 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
components/theme-client.php text eol=lf
components/theme-switch.php text eol=lf
components/theme-style.php text eol=lf
+94 -225
View File
@@ -1,118 +1,13 @@
name: deploy-preland
# После коммита в main сохраняется legacy-выпуск на Beget. Новые домены ZoomPixel
# доставляются отдельными SFTP job: staging — только на stage, main — только на production.
# Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория <prefix>_FTP_HOST,
# <prefix>_FTP_USER, <prefix>_FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает.
# Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, AGENTS.md, CLAUDE.md, .gitignore) и тех, что живут
# только на хостинге: requisites.html (реквизиты заполняют на месте — пустой файл из git их бы затёр).
# Рабочий config.json настраивается владельцем на каждом хостинге. Деплой не
# перезаписывает его: он добавляет только отсутствующие обязательные UTM-ключи.
# Единственное контролируемое удаление — завершённая миграция legacy images/demo* после
# smoke новых корневых наборов /1, /2. Прочие файлы на хостинге не удаляются.
# Решение владельца 19.10.2026, preland#4: только stage.
# Production выкаты временно отключены; Барбариска больше не используется.
on:
push:
branches:
- main
- staging
jobs:
deploy:
name: deploy ${{ matrix.name }}
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# name — имя цели, url — адрес преленда (для проверки), prefix — префикс секретов <prefix>_FTP_*,
# dir — каталог сайта на FTP ("" — корень FTP, как на beget). Новый домен — новая запись и три секрета;
# requisites.html создают на хостинге цели. Рабочий config.json приходит из Git.
- name: barbariskatest
url: https://preland.anton.barbariskatest.ru
prefix: PRELAND
dir: ""
defaults:
run:
shell: sh
env:
PRELAND_URL: ${{ matrix.url }}
REMOTE_DIR: ${{ matrix.dir }}
steps:
- name: Install tools (Alpine)
run: apk add --no-cache git curl php83-cli
- name: Checkout repository
uses: actions/checkout@v4
- name: Preserve host config and add focus if absent
env:
FTP_HOST: ${{ secrets[format('{0}_FTP_HOST', matrix.prefix)] }}
FTP_USER: ${{ secrets[format('{0}_FTP_USER', matrix.prefix)] }}
FTP_PASS: ${{ secrets[format('{0}_FTP_PASS', matrix.prefix)] }}
run: |
set -eu
[ -n "$FTP_HOST" ] && [ -n "$FTP_USER" ] && [ -n "$FTP_PASS" ] || { echo "нет секретов ${{ matrix.prefix }}_FTP_*"; exit 1; }
dir=$(printf '%s' "$REMOTE_DIR" | sed 's#^/*##; s#/*$##')
umask 077
netrc=$(mktemp); config=$(mktemp)
trap 'rm -f "$netrc" "$config"' EXIT
printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc"
curl -sS --ssl-reqd --netrc-file "$netrc" --fail --retry 3 \
"ftp://$FTP_HOST/${dir:+$dir/}config.json" -o "$config"
changed=0
php bin/ensure-focus-config.php "$config" || changed=$?
case "$changed" in
0) ;;
10) curl -sS --ssl-reqd --netrc-file "$netrc" --ftp-create-dirs --retry 3 --fail \
-T "$config" "ftp://$FTP_HOST/${dir:+$dir/}config.json" ;;
*) exit "$changed" ;;
esac
- name: Upload over FTP (TLS)
env:
FTP_HOST: ${{ secrets[format('{0}_FTP_HOST', matrix.prefix)] }}
FTP_USER: ${{ secrets[format('{0}_FTP_USER', matrix.prefix)] }}
FTP_PASS: ${{ secrets[format('{0}_FTP_PASS', matrix.prefix)] }}
run: |
set -eu
[ -n "$FTP_HOST" ] && [ -n "$FTP_USER" ] && [ -n "$FTP_PASS" ] || { echo "нет секретов ${{ matrix.prefix }}_FTP_*"; exit 1; }
dir=$(printf '%s' "$REMOTE_DIR" | sed 's#^/*##; s#/*$##')
case "$dir" in *[!A-Za-z0-9._/-]*) echo "недопустимый dir: $dir"; exit 1 ;; esac
umask 077
netrc=$(mktemp); list=$(mktemp)
trap 'rm -f "$netrc" "$list"' EXIT
printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc"
git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > files.txt
# имя уходит в адрес ftp:// как есть — пробелы и прочее экранирование не поддерживаем
if grep -q '[^A-Za-z0-9._/-]' files.txt; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' files.txt; exit 1; fi
# один curl на все файлы — одно соединение FTP+TLS вместо соединения на файл
while read -r f; do printf 'upload-file = "%s"\nurl = "ftp://%s/%s%s"\n' "$f" "$FTP_HOST" "${dir:+$dir/}" "$f"; done < files.txt > "$list"
curl -sS --ssl-reqd --netrc-file "$netrc" --ftp-create-dirs --retry 3 --fail-early -K "$list"
echo "залито файлов: $(wc -l < files.txt)"
- name: Check the page
run: |
set -eu
page=$(curl -fsS --max-time 30 "$PRELAND_URL/?nc=$(date +%s)")
api=$(printf '%s' "$page" | grep -o 'const createApiUrl = "[^"]*"' || true)
echo "$api"
case "$api" in
*'\/api\/images\/public\/create'*) ;;
*) echo "на странице нет адреса загрузки"; exit 1 ;;
esac
own=$(printf '%s' "$PRELAND_URL" | sed -E 's#^https?://([^/]+).*#\1#')
case "$api" in
*"$own"*) echo "загрузка уходит на сам преленд: на хостинге нет config.json с host"; exit 1 ;;
esac
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim — залит старый код?"; exit 1; }
echo "страница в порядке"
deploy-zoomstage:
name: deploy zoomstage.online
if: github.ref == 'refs/heads/staging'
@@ -125,12 +20,29 @@ jobs:
REMOTE_DIR: domains/zoomstage.online
steps:
- name: Install tools
run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli
run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli php-imagick
- name: Checkout staging branch
run: git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git .
env:
RELEASE_SHA: ${{ github.sha }}
run: |
set -eu
git clone --depth 1 --branch staging http://git.dira89kv.beget.tech:3000/anton/preland.git .
git fetch --depth 1 origin "$RELEASE_SHA"
git checkout --detach "$RELEASE_SHA"
[ "$(git rev-parse HEAD)" = "$RELEASE_SHA" ]
- name: Preserve host config and add focus if absent
- name: Validate release and current stage sets before upload
run: |
set -eu
php bin/release-assets.php --check
for test in tests/check-*.php; do php "$test"; done
php bin/release-assets.php --primary
# preland#22: /N/ — runtime. Проверяем config и декодируем все 90
# обязательных изображений используемых наборов, не сверяем с Git SHA.
php bin/release-assets.php --stage-sets "$PRELAND_URL"
- name: Apply approved domain brand only if needed
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
@@ -138,22 +50,24 @@ jobs:
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
[ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; }
status=0
php bin/release-brand.php --status "$PRELAND_URL" || status=$?
case "$status" in 0) exit 0 ;; 10) ;; *) exit "$status" ;; esac
# Только точная ранее согласованная пара заменяется по preland#12.
# Обычные выпуски с правильным брендом сюда не доходят.
umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
changed=0
php bin/ensure-focus-config.php "$config" || changed=$?
case "$changed" in
0) ;;
10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" ;;
*) exit "$changed" ;;
esac
php bin/release-brand.php --files "$PRELAND_URL" > "$files"
revision=$(git rev-parse HEAD)
while read -r source target; do
# Оборванный put не должен оставлять усечённый действующий SVG.
printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch"
printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch"
done < "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
php bin/release-brand.php --status "$PRELAND_URL"
- name: Upload over SFTP
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
@@ -170,7 +84,10 @@ jobs:
dirs=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > "$files"
# Наборы `/N/` на stage — редактируемый контент. Кодовая доставка их
# намеренно не перезаписывает. Обновление набора через админку не требует Git.
# Положительный список: admin/tests/bin/планы/секреты не публикуются.
php bin/release-assets.php --files | grep -vE '^(config\.json$|[0-9]+/)' > "$files"
if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi
printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch"
while read -r file; do
@@ -185,9 +102,36 @@ jobs:
done < "$files"
sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done
while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
echo "залито файлов: $(wc -l < "$files")"
# api#326/#327: сначала совместимый PHP-код, затем точечная миграция UTM.
# chob→banch меняет только токен имени; веса/default владельца сохраняются.
# Остальные поля владельца и их JSON-типы сохраняются.
- name: Migrate numeric UTM in preserved host config
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
changed=0
php bin/ensure-utm-config.php "$config" || changed=$?
case "$changed" in
0) ;;
10) revision=$(git rev-parse HEAD)
printf '%s\n' "put -p $config $REMOTE_DIR/config.json.upload-$revision" "rename $REMOTE_DIR/config.json.upload-$revision $REMOTE_DIR/config.json" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" ;;
*) exit "$changed" ;;
esac
- name: Check the page
run: |
set -eu
@@ -196,10 +140,29 @@ jobs:
echo "$api"
case "$api" in *'zoomzilla.ru\/api\/images\/public\/create'*) ;; *) echo "на странице нет адреса загрузки Zoomzilla"; exit 1 ;; esac
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; }
curl -fsS --max-time 30 "$PRELAND_URL/1/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован первый набор примеров"; exit 1; }
curl -fsS --max-time 30 "$PRELAND_URL/2/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован второй набор примеров"; exit 1; }
page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=2&nc=$(date +%s)")
printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; }
# Список наборов берётся из runtime config, а не фиксируется как /1/ и /2/.
# Полная проверка изображений ниже; одиночный HTTP200 недостаточен.
php bin/release-assets.php --remote "$PRELAND_URL"
php bin/release-brand.php --status "$PRELAND_URL"
- name: Remove previously published internal files
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
umask 077
known_hosts=$(mktemp); batch=$(mktemp)
trap 'rm -f "$known_hosts" "$batch"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
# Только точно известные служебные файлы прежнего deploy, без рекурсии.
printf '%s\n' "-rm $REMOTE_DIR/requisites.html" > "$batch"
git ls-files tests bin docs/superpowers | while read -r file; do
printf '%s\n' "-rm $REMOTE_DIR/$file" >> "$batch"
done
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
- name: Remove obsolete demo directories
env:
@@ -223,98 +186,4 @@ jobs:
done
printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch"
done
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
deploy-zoompixel-production:
name: deploy ${{ matrix.name }}
if: github.ref == 'refs/heads/main'
runs-on: zoomzilla-production
strategy:
fail-fast: false
matrix:
include:
- name: zoompixel.ru
url: https://zoompixel.ru
dir: domains/zoompixel.ru
- name: zoompixel.online
url: https://zoompixel.online
dir: domains/zoompixel.online
defaults:
run:
shell: sh
env:
PRELAND_URL: ${{ matrix.url }}
REMOTE_DIR: ${{ matrix.dir }}
steps:
- name: Install tools
run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git curl openssh-client sshpass php-cli
- name: Checkout production branch
run: git clone --depth 1 --branch main http://git.dira89kv.beget.tech:3000/anton/preland.git .
- name: Preserve host config and add focus if absent
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
[ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; }
umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
changed=0
php bin/ensure-focus-config.php "$config" || changed=$?
case "$changed" in
0) ;;
10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$batch"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" ;;
*) exit "$changed" ;;
esac
- name: Upload over SFTP
env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
SFTP_PASS: ${{ secrets.ZOOMPIXEL_SFTP_PASS }}
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: |
set -eu
[ -n "$SFTP_HOST" ] && [ -n "$SFTP_USER" ] && [ -n "$SFTP_PASS" ] && [ -n "$SFTP_KNOWN_HOSTS" ] || { echo "не заданы секреты ZOOMPIXEL_SFTP_*"; exit 1; }
umask 077
known_hosts=$(mktemp)
batch=$(mktemp)
files=$(mktemp)
dirs=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$)' > "$files"
if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi
printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch"
while read -r file; do
case "$file" in
*/*) dir=${file%/*} ;;
*) continue ;;
esac
while [ -n "$dir" ]; do
printf '%s\n' "$dir" >> "$dirs"
case "$dir" in */*) dir=${dir%/*} ;; *) break ;; esac
done
done < "$files"
sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done
while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch"
echo "залито файлов: $(wc -l < "$files")"
- name: Check the page
run: |
set -eu
page=$(curl -fsS --max-time 30 "$PRELAND_URL/?nc=$(date +%s)")
api=$(printf '%s' "$page" | grep -o 'const createApiUrl = "[^"]*"' || true)
echo "$api"
case "$api" in *'zoomzilla.ru\/api\/images\/public\/create'*) ;; *) echo "на странице нет адреса загрузки Zoomzilla"; exit 1 ;; esac
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; }
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 KiB

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+72 -1
View File
@@ -1,5 +1,18 @@
# Инструкции агентов для `preland`
## Поручение 25.10.2026 — preland#21
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
разделами ниже действует это датированное решение и задача #21.
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
@@ -30,7 +43,11 @@
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
создавать target-specific конфиги или `config.json.example`.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
- Корневые числовые наборы примеров `/N/` на `zoomstage.online` — редактируемый
контент stage. Обычный push в `staging` должен исключать `[0-9]+/` из SFTP-заливки:
актуальные примеры нельзя перезаписывать Git-версией. Снимок можно сохранить в
Git по задаче; #22 не требует этого для обычного изменения runtime-набора.
- Коммит в `staging` запускает SFTP-автодеплой только на zoomstage.online; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
@@ -38,3 +55,57 @@
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
## Актуальная маршрутизация (19.10.2026, preland#4)
До отдельного поручения владельца выкатывать только staging → zoomstage.online. Production zoompixel.ru/.online не обновлять. preland.anton.barbariskatest.ru больше не используется, не является целью и не должен упоминаться как требующий выпуска. CI запускает только stage. Сохранение runtime config и stage-наборов /N/ обязательно. Работа и решения — через Gitea.
## Актуальные ресурсы (21.10.2026, preland#11)
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ не перезаписывается кодовой доставкой. Требование #11 о сверке runtime-наборов с Git заменено решением #22 от 25.10.2026 (см. ниже).
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
## Runtime indpic и отдельная админка (25.10.2026, preland#22)
Это решение явно заменяет требование #11 о предварительном импорте изменённых
наборов `/N/` в Git. Кодовая доставка сохраняет runtime `config.json` и `/N/`;
обновление контента админкой не требует commit/push. Исторические записи `/N/`
в `release/assets.json` остаются снимком, не источником истины runtime.
`php bin/release-assets.php --check` сохраняет строгую проверку состава и SHA
поставляемой статики, включая запрет старого бренда. `--stage-sets URL` и
`--remote URL` читают фактический config и проверяют default, номера/веса indpic,
затем декодируют все 90 обязательных изображений каждого используемого набора,
проверяют формат и размеры по контракту API `IndpicSetStorage` (591a67cc8302).
Проверяются также ссылки с нулевым весом: их разрешает явный UTM и legacy random.
Отсутствующий indpic/default использует штатный fallback `/1/`; явно неверный
номер/default блокируется. Конфиг перечитывается после проверки для обнаружения
его изменения. Одиночный HTTP200 не считается подтверждением валидного набора.
HTTP не предоставляет inventory каталога: лишние файлы удалённого набора этим
режимом не исключаются. Для строгой проверки ровно 90 файлов без симлинков,
вложенных каталогов и лишних элементов выполнить на фактическом webroot
`php bin/check-runtime-sets.php /path/to/webroot` либо проверку через транспорт
админки. Локальная проверка и HTTP требуют PHP Imagick с AVIF/WebP/JPEG.
`releaseFiles()` — положительный список публичных кодовых/статических ресурсов.
`admin/`, его состояние/ключи/резервы, `bin/`, тесты и планы не публикуются обычной
доставкой прелендинга. Админка имеет отдельный образ и процедуру выпуска.
Проверки перед выпуском: все `tests/check-*.php`, manifest `--check`, `--primary`,
`--stage-sets`; после — `--remote` и проверка бренда. Не ослаблять guard ради выпуска.
+78 -5
View File
@@ -1,9 +1,22 @@
# Инструкции агентов для `preland`
## Поручение 25.10.2026 — preland#21
Боевые цели теперь `zoompixel.ru` на основном сервере и `zoomila.ru`/`zoomika.ru`
на двух REG.RU-хостингах. Владелец явно разрешил первоначальное размещение этих трёх
целей; прежняя заморозка #4 для этой задачи снята. Единственный активный `.online` —
`zoomstage.online` (stage). `zoompixel.online`, `zoomika.online`, `zoomila.online` отключены,
каталоги убраны из рабочих FTP-папок с резервным сохранением; не включать без поручения.
На обоих REG.RU-хостингах владельцу нужен SFTP или FTP; существующие доступы описаны
в актуальном разделе README, пароли остаются в REG.RU. Доменные готовые SVG — в
`brands/`, существующий runtime-config сохранять. Обычный автодеплой остаётся только
на stage; новое поручение не включает production-автодеплой. При расхождении со старыми
разделами ниже действует это датированное решение и задача #21.
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
Этот файл и [AGENTS.md](AGENTS.md) содержат одинаковые локальные правила; обновлять их вместе.
Этот файл и [CLAUDE.md](CLAUDE.md) содержат одинаковые локальные правила; обновлять их вместе.
## Перед работой
@@ -21,10 +34,19 @@
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
Не обходить серверные правила доступа к загрузке и результату обработки.
- Рабочий `config.json` единственный для всех прилендов: это точная проверенная
конфигурация `zoomstage.online`, и каждый FTP/SFTP-deploy обязан доставлять её на
stage и production. `requisites.html` по-прежнему живёт только на хостинге и не
перезаписывается. Не создавать target-specific конфиги или `config.json.example`.
- Рабочий `config.json` на каждом хостинге настраивает владелец. Деплой не имеет
права перезаписывать его значений. При появлении новой обязательной UTM workflow
получает текущий хостовый конфиг и меняет только ключи, которые прямо
согласованы в задаче. Иные значения, включая уже настроенные UTM, остаются
байт-в-байт прежними. Для #274 `bin/ensure-focus-config.php` — явное
исключение: он задаёт только новый `focus=face` (default и rnd). После каждого
такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
`requisites.html` также живёт только на хостинге и не перезаписывается. Не
создавать target-specific конфиги или `config.json.example`.
- Корневые числовые наборы примеров `/N/` на `zoomstage.online` — редактируемый
контент stage. Обычный push в `staging` должен исключать `[0-9]+/` из SFTP-заливки:
актуальные примеры нельзя перезаписывать Git-версией. Снимок можно сохранить в
Git по задаче; #22 не требует этого для обычного изменения runtime-набора.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
@@ -33,3 +55,54 @@
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
## Актуальные ресурсы (21.10.2026, preland#11)
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ не перезаписывается кодовой доставкой. Требование #11 о сверке runtime-наборов с Git заменено решением #22 от 25.10.2026 (см. ниже).
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
## Runtime indpic и отдельная админка (25.10.2026, preland#22)
Это решение явно заменяет требование #11 о предварительном импорте изменённых
наборов `/N/` в Git. Кодовая доставка сохраняет runtime `config.json` и `/N/`;
обновление контента админкой не требует commit/push. Исторические записи `/N/`
в `release/assets.json` остаются снимком, не источником истины runtime.
`php bin/release-assets.php --check` сохраняет строгую проверку состава и SHA
поставляемой статики, включая запрет старого бренда. `--stage-sets URL` и
`--remote URL` читают фактический config и проверяют default, номера/веса indpic,
затем декодируют все 90 обязательных изображений каждого используемого набора,
проверяют формат и размеры по контракту API `IndpicSetStorage` (591a67cc8302).
Проверяются также ссылки с нулевым весом: их разрешает явный UTM и legacy random.
Отсутствующий indpic/default использует штатный fallback `/1/`; явно неверный
номер/default блокируется. Конфиг перечитывается после проверки для обнаружения
его изменения. Одиночный HTTP200 не считается подтверждением валидного набора.
HTTP не предоставляет inventory каталога: лишние файлы удалённого набора этим
режимом не исключаются. Для строгой проверки ровно 90 файлов без симлинков,
вложенных каталогов и лишних элементов выполнить на фактическом webroot
`php bin/check-runtime-sets.php /path/to/webroot` либо проверку через транспорт
админки. Локальная проверка и HTTP требуют PHP Imagick с AVIF/WebP/JPEG.
`releaseFiles()` — положительный список публичных кодовых/статических ресурсов.
`admin/`, его состояние/ключи/резервы, `bin/`, тесты и планы не публикуются обычной
доставкой прелендинга. Админка имеет отдельный образ и процедуру выпуска.
Проверки перед выпуском: все `tests/check-*.php`, manifest `--check`, `--primary`,
`--stage-sets`; после — `--remote` и проверка бренда. Не ослаблять guard ради выпуска.
+145 -57
View File
@@ -1,39 +1,79 @@
# Преленд zoomzilla
## Боевые домены — поручение 25.10.2026, preland#21
Три боевые цели: `zoompixel.ru` на основном сервере, `zoomila.ru` и `zoomika.ru`
на двух отдельных хостингах REG.RU. Это отдельное разрешение владельца на первоначальное
размещение/обновление этих целей после заморозки #4. Единственный активный `.online` —
`zoomstage.online`, staging прелендингов. `zoompixel.online`, `zoomika.online` и
`zoomila.online` отключены по дополнительному решению владельца; их регистрация сохранена.
Рабочие FTP/SFTP-каталоги не должны содержать директории отключённых `.online`.
| Домен | Размещение | Пользователь SFTP/FTP | Каталог сайта |
|---|---|---|---|
| `zoomika.ru` | `server197.hosting.reg.ru`, `31.31.196.183` | `u3695919` | `/www/zoomika.ru` относительно домашнего каталога |
| `zoomila.ru` | `server124.hosting.reg.ru`, `37.140.192.195` | `u3695920` | `/www/zoomila.ru` относительно домашнего каталога |
| `zoompixel.ru` | основной сервер, `/srv/preland/domains/zoompixel.ru` | существующий административный доступ | отдельный контейнер `preland-zoompixel-ru-1` |
REG.RU выдаёт SFTP/SSH и FTP отдельно: SFTP использует порт 22 и пароль из блока
«Доступ в панель управления, SSH, SFTP»; FTP — порт 21 и отдельный пароль из блока
«Доступ к FTP». Пароли смотреть в кабинете REG.RU → нужный хостинг → «Доступы»;
не помещать в Git и журналы. Оба аккаунта уже созданы провайдером, сбрасывать пароли
для размещения не требуется. Полные домашние каталоги — `/var/www/<пользователь>/data`.
В `www` каждого REG.RU-аккаунта оставлен только соответствующий `.ru`. Сайты `.online`
выключены через ispmanager, их исходные файлы и установочные архивы сохранены вне webroot
в скрытом `.preland21-backup/` (каталог `site`, файлы `release.zip`, `initial-index.html`).
На основном сервере `zoompixel.online` отвечает HTTP/HTTPS 410; его контейнер удалён,
сервис исключён из Compose, исходники сохранены в `/srv/preland/backups/preland21-disabled-online/site`.
Логотипы для обеих тем — готовые SVG в `brands/<домен>/`, реестр — `release/brands.json`.
При первоначальном размещении копировать только положительный список `releaseFiles()`
и проверенные статические ресурсы (включая `/1`, `/2`), затем доменную пару в
`docs/logo.svg`/`docs/logo-alt.svg`. Служебные `bin/`, `tests/`, `.git/`, планы и
установочные архивы в webroot не входят. Архивы REG.RU хранить вне `/www`.
У новых пустых сайтов исходный config — проверенный stage-снимок; существующий
`zoompixel.ru/config.json` сохраняется байт-в-байт, старые UTM нормализуются в памяти.
Перед трафиком обязательны доверенный TLS (самоподписанный сертификат REG.RU не подходит),
HTTP→HTTPS, проверка двух тем/ширин, примеров, upload→claim→Tour→Join с UTM,
видимой ошибки и повторной попытки. Точный состав, коммиты, результаты и ограничения — #21.
Обычная автоматическая доставка по-прежнему только `staging` → `zoomstage.online`;
данное первоначальное размещение не включает production-автодеплой по push `main`.
Исторические разделы ниже описывают прежние цели; при расхождении действует этот раздел.
Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
маленькая заглушка `Yii::t` в `i18n.php`.
Боевой экземпляр: `https://preland.anton.barbariskatest.ru` (хостинг beget). Пример ссылки входа:
`https://preland.anton.barbariskatest.ru/?indpic=1&theme=dark&degr=2&registration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`.
Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа:
`https://zoomstage.online/?indpic=e9st1mm12b&theme=e9st1mm12b&degr=e9st2mm12b&reg=e9st2mm12b&banch=e9st2mm12b&tp=e9st1mm12b&utm_source=test&subid=CLICK1`.
## Как работает
### Метки
### Метки (api#326)
`index.php` разбирает параметры адреса по разделу `utm` из `config.json`. Для каждой настроенной метки:
Из публичного значения учитывается только пятый символ: цифра1–9 выбирает вариант,0 на Preland выбирает его по прежним `weights`. Отсутствующий, короткий, массивный и невалидный ввод получает `default`. Открытые `1`, `rnd`, `dark`, старые имена `registration/bchoice/chob/chb` больше не выбирают варианты. Точная длина входной строки не ограничена; Unicode-шум не изменяет позицию управляющего символа.
- значение из адреса принимается, если оно есть в `weights`;
- `rnd` — случайное значение по весам (`weights`), так делится трафик между вариантами;
- неизвестное значение отбрасывается, отсутствующее — заменяется на `default`.
| Метка | Числовые значения |
|---|---|
| `indpic` | номер настроенного набора1–9 (`/1/`, `/2/` и т.д.) |
| `theme` |1 dark,2 light |
| `degr` | номер настроенной группы1–9 |
| `reg` |1 before (PIN до оплаты),2 after |
| `banch` |1 без попапа банка,2 с попапом |
| `tp` | номер настроенной тарифной группы1–9 |
| `cons` | номер группы согласий1–9; существование проверяет API |
| `focus` |1 face,2 center |
| `part` |1 показывать ссылку «Для бизнеса» на сайте,2 скрывать; Preland ссылку не показывает |
Ненастроенные параметры (`utm_source`, `keyword`, `subid` и любые другие) проходят как есть. Перед переходом
Preland всегда формирует полный resolved-набор своих восьми меток: явное допустимое значение остаётся, `rnd`
разрешается по весам, отсутствующее получает `default`. Вместе с внешними рекламными параметрами этот набор
уходит в `claim`, а затем сайт переносит его в `upscale`; ни один из семи параметров не должен пропадать только
потому, что был назначен default.
`config.json` хранит публичные числовые `weights/default`. Внутри PHP сохраняются прежние semantic enum. Runtime-конфиг мигрируется `bin/ensure-utm-config.php`: только согласованные UTM, с сохранением весов, default и JSON-типов соседних полей. Скрипт идемпотентен; старые focus/cons patch-скрипты больше не вызываются доставкой.
| Метка | Что задаёт | Где |
|---|---|---|
| `indpic` | картинка-пример на преленде | корневой каталог сета `/1/`, `/2/` и т. д. |
| `theme` | `light` / `dark` | преленд и сайт |
| `degr` | пресет ухудшения превью «До» | сайт (#42) |
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
| `focus` | первоначальная область лупы Join: `center`/`face` | сайт, cpanel после `tp` |
В api#327 публичное `chob` заменено на `banch`; внутренний `bchoice` сохраняется. Мигратор переименовывает текущий host-ключ токенно, сохраняя все остальные байты, включая веса и default. Одновременные `banch/chob/bchoice` блокируют patch без записи. До миграции код читает прежний host-config; сохранённые recovery-записи с `utmDigits.chob` или `redirectUtm` продолжают выбранный вариант и выпускают уже `banch`.
Перед каждым переходом Preland создаёт для всех девяти меток новый шум из10 ASCII букв/цифр, с уже выбранной цифрой в пятой позиции. Retry и recovery меняют только шум; вариант повторно не выбирается. Выбранные цифры сохраняются в upload record. Старые записи IndexedDB адаптируются только на пути recovery, без публичных alias старого URL.
Чужие параметры (`utm_source`, `keyword`, `subid` и любые другие) сохраняют значения после обычного URL-кодирования. Технические `key/id/stage/shown_theme` не маскируются; внешний `key` не может подменить одноразовый ключ загрузки. `shown_theme` берётся из фактически показанной темы непосредственно при переходе.
### Загрузка и переход на сайт
@@ -76,7 +116,7 @@ Preland всегда формирует полный resolved-набор сво
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) |
| `logoAltExt` | устарело | После preland#11 обе темы используют проверенные SVG Zoomzilla; расширение не меняется runtime-конфигом. |
| `title` | нет | заголовок страницы («Улучшение изображения») |
| `default_language` | нет | сейчас не действует: язык всегда `ru` |
@@ -99,10 +139,16 @@ Preland хранит наборы ровно как Index, поэтому их
N/05_...
```
`indpic=N` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
`indpic=abcdNefghi` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
домена целиком: без переименования, вложения в `images/` и промежуточных
каталогов. Устаревшие `images/demo*` не являются частью поставки.
Для `zoomstage.online` обычный deploy ветки `staging` намеренно **не загружает**
корневые числовые каталоги `/N/`: это редактируемый контент stage, а не артефакт
кодовой доставки. По #22 workflow проверяет их полный контракт по фактическому
config через runtime guard. Импорт изменённого набора в Git не требуется;
следующая заливка не перетрёт его старой ревизией.
## Деплой на произвольный домен
**Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**:
@@ -111,13 +157,10 @@ N/05_...
1. **Домен.** Запись A (или CNAME) на хостинг, сертификат (Let's Encrypt или от хостинга); открыть `https://<домен>/`.
2. **Файлы.** Содержимое репозитория — в корень сайта, без `.git` и служебного (`README.md`, `.gitea/`):
```bash
git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland
git clone https://git.zoomzilla.ru/anton/preland.git && cd preland
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
```
3. **Конфиг и реквизиты.** Рабочий `config.json` уже лежит в репозитории и обязателен к доставке на каждый
приленд. Его не заменяют локальным вариантом и не исключают из синхронизации. Реквизиты задаются ключом
`requisites` в нём либо runtime-файлом `requisites.html`. 2026-10-05 конфиг пропал после заливки,
и преленд слал загрузку на собственный домен; это больше не допускается.
3. **Конфиг и реквизиты.** Рабочий `config.json` на хостинге — источник истины. При первичном создании домена его настраивают отдельно; обычный deploy не заменяет его репозиторным снимком и не перезаписывает `requisites.html`. Менять можно только явно согласованные ключи после чтения текущего файла.
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
```nginx
server {
@@ -142,43 +185,88 @@ N/05_...
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
# 302 на https://<host>/ — точка передачи на сайте есть
```
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка.
В браузере: открыть `https://<домен>/?indpic=abcd1efghi&theme=abcd1efghi&subid=TEST`, выбрать фото — адрес сменится на
`https://<host>/public/upscale?id=…&indpic=<новый шум с1>&theme=<новый шум с1>&subid=TEST…` (без `key=`), пойдёт обработка.
Это настоящая загрузка и обработка на сайте.
## Автодеплой (Gitea Actions)
`.gitea/workflows/deploy.yml`: после каждого коммита в `main` раннер заливает файлы репозитория на хостинги **всех
целей** по FTP с TLS и проверяет страницу каждой. Цель — домен со своим хостингом; список целей — `matrix` в workflow:
Решение владельца от 19.10.2026, задача preland#4: сейчас выкатываем **только stage**.
```yaml
- name: barbariskatest # имя цели (видно в списке прогонов: deploy barbariskatest)
url: https://preland.anton.barbariskatest.ru # адрес преленда — для проверки после заливки
prefix: PRELAND # секреты PRELAND_FTP_HOST, PRELAND_FTP_USER, PRELAND_FTP_PASS
dir: "" # каталог сайта на FTP: "" — корень FTP (beget), иначе, например, public_html
```
| Экземпляр | Назначение | Выкат |
|---|---|---|
| https://zoomstage.online | Тестовый стенд | Push в staging, SFTP |
| https://zoompixel.ru | Production | Отключён до отдельного поручения |
| https://zoompixel.online | Production | Отключён до отдельного поручения |
Цели выкатываются независимо (`fail-fast: false`): сбой одного хостинга не останавливает остальные.
Прежний preland.anton.barbariskatest.ru выведен из использования и исключён из workflow.
Публикация изменений самого workflow в main не запускает доставку: триггер оставлен только для staging.
**Новый домен на своём хостинге:**
1. Домен и HTTPS — как в «Деплой на произвольный домен», шаг 1.
2. Секреты `<ПРЕФИКС>_FTP_HOST`, `<ПРЕФИКС>_FTP_USER`, `<ПРЕФИКС>_FTP_PASS` (Gitea → настройки репозитория →
Actions → секреты; порт 21, FTP с TLS). В коде реквизитов нет.
3. Никаких ручных конфигов не создавать: единственный `config.json` приезжает вместе с кодом.
4. Запись в `matrix` — коммит в `main`, автодеплой выкатит все цели вместе с конфигом.
Workflow `.gitea/workflows/deploy.yml` содержит единственную job deploy-zoomstage на
выделенном runner zoomzilla-production. Production возвращается только отдельной задачей
и прямым поручением владельца. Ошибки старого FTP больше не являются блокером выпуска.
**Что заливается.** Каждый deploy получает файлы репозитория (`git ls-files`), кроме служебных
(`.gitea/`, `README.md`, `.gitignore`) и живущего на хостинге `requisites.html`. Единственный
версионируемый `config.json` всегда доставляется вместе с кодом.
На хостинге ничего не удаляется: файлы, убранные из репозитория, удалять вручную.
Рабочие config.json/requisites.html на хостинге не перезаписываются. Корневые наборы
/N/ принадлежат stage и исключены из кодовой доставки. Родительские каталоги создаются
штатным SFTP-генератором; smoke проверяет страницу и assets наборов1/2.
Откат — отдельный согласованный revert в staging, который обновляет только stage.
**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет
`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает.
## Android: фото и файлы (2026-10-19)
Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются.
**Откат** — `git revert` проблемного коммита в `main`: автодеплой зальёт прежнюю версию на все цели.
## Актуальные ресурсы (21.10.2026, preland#11)
**SFTP.** Сейчас только FTP с TLS. Хостинг без FTP — поле `protocol` в цели и `lftp` вместо curl в шаге заливки.
- Git main и staging содержат одно проверенное дерево. Источники: текущая zoomzilla.ru — два логотипа Zoomzilla, четыре PDF, favicon и реквизиты; zoomstage.online — все файлы наборов /1 и /2. История Git не переписывается; известная старая заглушка запрещена в рабочей поставке.
- release/assets.json фиксирует SHA-256 всех статических ресурсов (текстовые SVG/CSS/HTML/webmanifest нормализуют CRLF→LF). До выпуска выполнить девять tests/check-*.php, php bin/release-assets.php --check, --primary и --stage-sets https://zoomstage.online. Если источник изменился, сначала синхронизировать актуальные файлы и manifest по задаче, затем повторить проверки. Не отключать guard ради выката.
- --primary читает именно текущую страницу основного сайта и проверяет её актуальные URL/реквизиты: старый сохранённый asset URL не доказывает актуальность. Новые логотипы должны пройти визуальную проверку обеих тем/шапки/подвала. Cache version в components/asset-url.php зависит от содержимого.
- По прямому выбору владельца21.10 requisites.html теперь входит в каноническую поставку. Единственное новое разрешённое изменение runtime-config — ключ requisites из release/requisites.json через bin/ensure-requisites-config.php; остальные байты полей сохраняются, повторный запуск ничего не меняет. Это явное исключение к прежнему host-only правилу реквизитов, а не разрешение заменять весь config.json. Git config.json хранит согласованный снимок; после выпуска сверять runtime в памяти, не сохранять потенциальные секреты в логи.
- /N/ не перезаписывается кодовой доставкой. Требование #11 о сверке runtime-наборов с Git заменено решением #22 от 25.10.2026 (см. ниже).
- SFTP берёт положительный список из bin/release-assets.php --files: никакие tests/bin/release/plans не публикуются. Старые известные служебные файлы удаляются только по точному списку git ls-files, без рекурсивной очистки. После выпуска --remote сверяет все184ресурса.
- Production-преленды остаются замороженными по preland#4. Push main ничего не публикует; staging обновляет только zoomstage.online из точного event SHA.
**Откат вручную.** Перед заменой файлов — копия изменяемых файлов (`index.php`, `components/*`) с хостинга; вернуть их той же
загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход
по ключу отдаст 404.
Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put.
## Доменный бренд и минимальный подвал (21.10.2026, preland#12)
Новое поручение владельца заменяет часть требований #11: в подвале остаются логотип и копирайт, без реквизитов и платёжных значков. Копирайт использует текущий HTTP_HOST без порта/trailing-dot, отдельно от config.host API; непроверенные Host и X-Forwarded-Host не выводятся.
Логотипы — готовые SVG-контуры, не текст на лету. Для zoomstage.online утверждено имя ZoomStage, слитно без зоны, обе темы. Источники лежат в brands/zoomstage.online/light.svg и dark.svg; release/brands.json содержит явную цель, имя и SHA. В docs/logo.svg и logo-alt.svg хранятся правильные canonical stage-копии для полного переноса. Их размеры182×47 совпадают с viewBox, чтобы загрузка SVG не меняла геометрию.
Обычный bin/release-assets.php --files исключает оба logo-alias из записи; runtime cache-version рассчитывается по реальному файлу хоста. Отдельный шаг release-brand.php --status разрешает только заранее согласованную миграцию #12:0 — оба SVG уже правильные, записи нет;10 — каждый файл имеет свой известный старый или новый SHA, можно закончить миграцию (включая частичный put);1 — неизвестный домен/хеш, остановка без подмены. --files выдаёт ровно утверждённые source→target. Иные логотипы не перезаписывать общим выкатом. Новые production-домены требуют отдельного добавления готовых пар и явного первоначального размещения; текущий stage-процесс их не активирует.
primary guard теперь сверяет PDF/favicon, а не заставляет возвращать Zoomzilla и реквизиты на преленд. Реквизиты не отображаются и больше автоматически не синхронизируются; хостовый config и его существующие значения не меняются этим поручением. Устаревшие requisites.html и patch-скрипт удалены из поставки. Статический manifest183, отдельный каталог брендов проверяется по собственным SHA. История #11 сохранена; её прежние инструкции о видимом бренде/реквизитах заменены этим датированным решением.
Перед выпуском: девять tests/check-*.php и lint, --check/--primary/--stage-sets, оба бренда шапки/подвала на desktop/mobile, перенос Host, отсутствие карт/реквизитов, холодная/медленная загрузка, реальные локальные Preland→Tour→Join/error/retry и геометрия. Production-преленды по #4 не включать; выпускать только staging→zoomstage.online. API продуктовый код не меняется.
Дополнение приёмки #12: tests/e2e/brand-footer.cjs проверяет именно видимый SVG по центру шапки, а не только headerbox. PUBLISHED=1 использует zoomstage.online (или PRELAND_URL); без него — изолированный pre301.localhost QA proxy. Запускать в имеющемся Docker Playwright с /candidate=репозиторий,/qa=артефакты; POST/PUT блокируются. Пропущенное при compact CSS правило Bootstrap navbar>.container восстановлено во всех3CSS: API уже имеет его из полного Bootstrap. Это устраняет переход logo y0→13.5px, который рамочный тест не замечал.
## Runtime indpic и отдельная админка (25.10.2026, preland#22)
Это решение явно заменяет требование #11 о предварительном импорте изменённых
наборов `/N/` в Git. Кодовая доставка сохраняет runtime `config.json` и `/N/`;
обновление контента админкой не требует commit/push. Исторические записи `/N/`
в `release/assets.json` остаются снимком, не источником истины runtime.
`php bin/release-assets.php --check` сохраняет строгую проверку состава и SHA
поставляемой статики, включая запрет старого бренда. `--stage-sets URL` и
`--remote URL` читают фактический config и проверяют default, номера/веса indpic,
затем декодируют все 90 обязательных изображений каждого используемого набора,
проверяют формат и размеры по контракту API `IndpicSetStorage` (591a67cc8302).
Проверяются также ссылки с нулевым весом: их разрешает явный UTM и legacy random.
Отсутствующий indpic/default использует штатный fallback `/1/`; явно неверный
номер/default блокируется. Конфиг перечитывается после проверки для обнаружения
его изменения. Одиночный HTTP200 не считается подтверждением валидного набора.
HTTP не предоставляет inventory каталога: лишние файлы удалённого набора этим
режимом не исключаются. Для строгой проверки ровно 90 файлов без симлинков,
вложенных каталогов и лишних элементов выполнить на фактическом webroot
`php bin/check-runtime-sets.php /path/to/webroot` либо проверку через транспорт
админки. Локальная проверка и HTTP требуют PHP Imagick с AVIF/WebP/JPEG.
`releaseFiles()` — положительный список публичных кодовых/статических ресурсов.
`admin/`, его состояние/ключи/резервы, `bin/`, тесты и планы не публикуются обычной
доставкой прелендинга. Админка имеет отдельный образ и процедуру выпуска.
Проверки перед выпуском: все `tests/check-*.php`, manifest `--check`, `--primary`,
`--stage-sets`; после — `--remote` и проверка бренда. Не ослаблять guard ради выпуска.
api#347: part — девятая UTM, пятый символ1/2, default1. Preland только передаёт её на сайт и никогда не показывает ссылку «Для бизнеса». Отсутствующая группа/веса сохраняют fallback1; случайный выбор использует существующий порядок и настроенные веса. Runtime config автоматически не дополняется. Старые сохранённые ссылки конструктора получают part1 без изменения шума остальных меток; старые заметки читаются с пустой заметкой part.
+6
View File
@@ -0,0 +1,6 @@
tests
vendor
state
secret.env
.env
*.log
+5
View File
@@ -0,0 +1,5 @@
/vendor/
/state/
/secret.env
/.env
/*.log
+23
View File
@@ -0,0 +1,23 @@
FROM php:8.3-apache-bookworm@sha256:cbd3b3d9011668443d54ab40ec31db56c73d91b4c8198935d639aa80d2d815c1
RUN apt-get update \
&& apt-get install -y --no-install-recommends libmagickwand-dev libzip-dev unzip ca-certificates git \
&& docker-php-ext-install zip \
&& printf '\n' | pecl install imagick-3.8.0 \
&& docker-php-ext-enable imagick \
&& a2enmod rewrite headers \
&& groupmod -g 987 www-data && usermod -u 997 -g 987 www-data \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2@sha256:b6dcd72ca98fa19d0e5b8476de179859704891fad06dd9e0b96833d4c430a5fc /usr/bin/composer /usr/local/bin/composer
RUN docker-php-ext-install pcntl
WORKDIR /app
COPY composer.* ./
RUN COMPOSER_IPRESOLVE=4 composer install --no-dev --no-interaction --prefer-dist --no-progress --optimize-autoloader
COPY . /app
COPY deploy/apache.conf /etc/apache2/sites-available/000-default.conf
COPY deploy/php.ini /usr/local/etc/php/conf.d/preland-admin.ini
RUN mkdir -p /var/lib/preland-admin && chown 997:987 /var/lib/preland-admin
ENV ADMIN_STATE_DIR=/var/lib/preland-admin
EXPOSE 80
CMD ["apache2-foreground"]
+273
View File
@@ -0,0 +1,273 @@
# Админка прелендингов — 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.
+4
View File
@@ -0,0 +1,4 @@
<?php
declare(strict_types=1);
// Пароль читается только из stdin; не передавать его через аргументы командной строки.
$password=rtrim(stream_get_contents(STDIN),"\r\n");if(strlen($password)<12){fwrite(STDERR,"Пароль должен содержать не менее 12 символов.\n");exit(1);}echo password_hash($password,PASSWORD_DEFAULT)."\n";
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more