Author SHA1 Message Date
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
162 changed files with 4281 additions and 828 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
+93 -221
View File
@@ -1,118 +1,13 @@
name: deploy-preland name: deploy-preland
# После коммита в main сохраняется legacy-выпуск на Beget. Новые домены ZoomPixel # Решение владельца 19.10.2026, preland#4: только stage.
# доставляются отдельными SFTP job: staging — только на stage, main — только на production. # 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. Прочие файлы на хостинге не удаляются.
on: on:
push: push:
branches: branches:
- main
- staging - staging
jobs: 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: deploy-zoomstage:
name: deploy zoomstage.online name: deploy zoomstage.online
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'
@@ -128,9 +23,26 @@ jobs:
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
- name: Checkout staging branch - 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
# /N/ по-прежнему принадлежит stage. Любое расхождение требует
# сначала перенести актуальный снимок в Git по задаче.
php bin/release-assets.php --stage-sets "$PRELAND_URL"
- name: Apply approved domain brand only if needed
env: env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }} SFTP_USER: ${{ secrets.ZOOMPIXEL_SFTP_USER }}
@@ -138,22 +50,24 @@ jobs:
SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }} SFTP_KNOWN_HOSTS: ${{ secrets.ZOOMPIXEL_SFTP_KNOWN_HOSTS }}
run: | run: |
set -eu 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 umask 077
known_hosts=$(mktemp); batch=$(mktemp); config=$(mktemp) known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$config"' EXIT trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts"
printf '%s\n' "get -p $REMOTE_DIR/config.json $config" > "$batch" php bin/release-brand.php --files "$PRELAND_URL" > "$files"
SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" revision=$(git rev-parse HEAD)
changed=0 while read -r source target; do
php bin/ensure-focus-config.php "$config" || changed=$? # Оборванный put не должен оставлять усечённый действующий SVG.
case "$changed" in printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch"
0) ;; printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch"
10) printf '%s\n' "put -p $config $REMOTE_DIR/config.json" > "$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"
*) exit "$changed" ;; php bin/release-brand.php --status "$PRELAND_URL"
esac
- name: Upload over SFTP - name: Upload over SFTP
env: env:
SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }}
@@ -170,7 +84,11 @@ jobs:
dirs=$(mktemp) dirs=$(mktemp)
trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT trap 'rm -f "$known_hosts" "$batch" "$files" "$dirs"' EXIT
printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" 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 — редактируемый контент. Кодовая доставка их
# намеренно не перезаписывает: актуальное содержимое stage фиксируют в Git
# отдельной синхронизацией по задаче.
# Положительный список: 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 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" printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch"
while read -r file; do while read -r file; do
@@ -185,9 +103,36 @@ jobs:
done < "$files" done < "$files"
sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done 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" 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")" 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 - name: Check the page
run: | run: |
set -eu set -eu
@@ -198,8 +143,29 @@ jobs:
printf '%s' "$page" | grep -q 'claim?key=' || { echo "переход не на claim"; exit 1; } 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/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; } 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)") page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=abcd2efghi&nc=$(date +%s)")
printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; } printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; }
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 - name: Remove obsolete demo directories
env: env:
@@ -223,98 +189,4 @@ jobs:
done done
printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch" printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch"
done done
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"
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; }
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

+43 -1
View File
@@ -1,5 +1,18 @@
# Инструкции агентов для `preland` # Инструкции агентов для `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/`. Общие правила рабочей области находятся в Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей — [AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md). [в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
@@ -30,7 +43,11 @@
такого выпуска фактический stage-config следует забрать и зафиксировать в Git. такого выпуска фактический stage-config следует забрать и зафиксировать в Git.
`requisites.html` также живёт только на хостинге и не перезаписывается. Не `requisites.html` также живёт только на хостинге и не перезаписывается. Не
создавать target-specific конфиги или `config.json.example`. создавать target-specific конфиги или `config.json.example`.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert` - Корневые числовые наборы примеров `/N/` на `zoomstage.online` — редактируемый
контент stage. Обычный push в `staging` должен исключать `[0-9]+/` из SFTP-заливки:
актуальные примеры нельзя перезаписывать Git-версией. Снимок stage фиксируется в
Git отдельной синхронизацией по задаче.
- Коммит в `staging` запускает SFTP-автодеплой только на zoomstage.online; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению. также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост. - Проверки кода и браузера проводить в Docker без установки ПО на хост.
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
@@ -38,3 +55,28 @@
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат. политику и документацию не подключают 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/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- 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 продуктовый код не меняется.
+49 -5
View File
@@ -1,9 +1,22 @@
# Инструкции агентов для `preland` # Инструкции агентов для `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/`. Общие правила рабочей области находятся в Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей — [AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md). [в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
Этот файл и [AGENTS.md](AGENTS.md) содержат одинаковые локальные правила; обновлять их вместе. Этот файл и [CLAUDE.md](CLAUDE.md) содержат одинаковые локальные правила; обновлять их вместе.
## Перед работой ## Перед работой
@@ -21,10 +34,19 @@
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim. переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`. - `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
Не обходить серверные правила доступа к загрузке и результату обработки. Не обходить серверные правила доступа к загрузке и результату обработки.
- Рабочий `config.json` единственный для всех прилендов: это точная проверенная - Рабочий `config.json` на каждом хостинге настраивает владелец. Деплой не имеет
конфигурация `zoomstage.online`, и каждый FTP/SFTP-deploy обязан доставлять её на права перезаписывать его значений. При появлении новой обязательной UTM workflow
stage и production. `requisites.html` по-прежнему живёт только на хостинге и не получает текущий хостовый конфиг и меняет только ключи, которые прямо
перезаписывается. Не создавать target-specific конфиги или `config.json.example`. согласованы в задаче. Иные значения, включая уже настроенные 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-версией. Снимок stage фиксируется в
Git отдельной синхронизацией по задаче.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert` - Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению. также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост. - Проверки кода и браузера проводить в Docker без установки ПО на хост.
@@ -33,3 +55,25 @@
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат. политику и документацию не подключают 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/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- 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 продуктовый код не меняется.
+112 -56
View File
@@ -1,39 +1,78 @@
# Преленд zoomzilla # Преленд 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`): посетитель с рекламы выбирает фото прямо на Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото. преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы — Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
маленькая заглушка `Yii::t` в `i18n.php`. маленькая заглушка `Yii::t` в `i18n.php`.
Боевой экземпляр: `https://preland.anton.barbariskatest.ru` (хостинг beget). Пример ссылки входа: Активные production: `https://zoompixel.ru`, `https://zoompixel.online`; текущая цель выката — только `https://zoomstage.online`. Пример ссылки входа:
`https://preland.anton.barbariskatest.ru/?indpic=1&theme=dark&degr=2&registration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`. `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 |
Ненастроенные параметры (`utm_source`, `keyword`, `subid` и любые другие) проходят как есть. Перед переходом `config.json` хранит публичные числовые `weights/default`. Внутри PHP сохраняются прежние semantic enum. Runtime-конфиг мигрируется `bin/ensure-utm-config.php`: только согласованные UTM, с сохранением весов, default и JSON-типов соседних полей. Скрипт идемпотентен; старые focus/cons patch-скрипты больше не вызываются доставкой.
Preland всегда формирует полный resolved-набор своих восьми меток: явное допустимое значение остаётся, `rnd`
разрешается по весам, отсутствующее получает `default`. Вместе с внешними рекламными параметрами этот набор
уходит в `claim`, а затем сайт переносит его в `upscale`; ни один из семи параметров не должен пропадать только
потому, что был назначен default.
| Метка | Что задаёт | Где | В api#327 публичное `chob` заменено на `banch`; внутренний `bchoice` сохраняется. Мигратор переименовывает текущий host-ключ токенно, сохраняя все остальные байты, включая веса и default. Одновременные `banch/chob/bchoice` блокируют patch без записи. До миграции код читает прежний host-config; сохранённые recovery-записи с `utmDigits.chob` или `redirectUtm` продолжают выбранный вариант и выпускают уже `banch`.
|---|---|---|
| `indpic` | картинка-пример на преленде | корневой каталог сета `/1/`, `/2/` и т. д. | Перед каждым переходом Preland создаёт для всех восьми меток новый шум из10 ASCII букв/цифр, с уже выбранной цифрой в пятой позиции. Retry и recovery меняют только шум; вариант повторно не выбирается. Выбранные цифры сохраняются в upload record. Старые записи IndexedDB адаптируются только на пути recovery, без публичных alias старого URL.
| `theme` | `light` / `dark` | преленд и сайт |
| `degr` | пресет ухудшения превью «До» | сайт (#42) | Чужие параметры (`utm_source`, `keyword`, `subid` и любые другие) сохраняют значения после обычного URL-кодирования. Технические `key/id/stage/shown_theme` не маскируются; внешний `key` не может подменить одноразовый ключ загрузки. `shown_theme` берётся из фактически показанной темы непосредственно при переходе.
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
| `focus` | первоначальная область лупы Join: `center`/`face` | сайт, cpanel после `tp` |
### Загрузка и переход на сайт ### Загрузка и переход на сайт
@@ -76,7 +115,7 @@ Preland всегда формирует полный resolved-набор сво
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» | | `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) | | `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` | | `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) | | `logoAltExt` | устарело | После preland#11 обе темы используют проверенные SVG Zoomzilla; расширение не меняется runtime-конфигом. |
| `title` | нет | заголовок страницы («Улучшение изображения») | | `title` | нет | заголовок страницы («Улучшение изображения») |
| `default_language` | нет | сейчас не действует: язык всегда `ru` | | `default_language` | нет | сейчас не действует: язык всегда `ru` |
@@ -99,10 +138,16 @@ Preland хранит наборы ровно как Index, поэтому их
N/05_... N/05_...
``` ```
`indpic=N` выбирает корневой каталог `/N/`. Набор копируется с Index в корень `indpic=abcdNefghi` выбирает корневой каталог `/N/`. Набор копируется с Index в корень
домена целиком: без переименования, вложения в `images/` и промежуточных домена целиком: без переименования, вложения в `images/` и промежуточных
каталогов. Устаревшие `images/demo*` не являются частью поставки. каталогов. Устаревшие `images/demo*` не являются частью поставки.
Для `zoomstage.online` обычный deploy ветки `staging` намеренно **не загружает**
корневые числовые каталоги `/N/`: это редактируемый контент stage, а не артефакт
кодовой доставки. Workflow продолжает проверять их доступность HTTP-smoke. Чтобы
зафиксировать обновлённый набор, сначала синхронизируйте его со stage в Git по
отдельной задаче; следующая заливка не перетрёт его старой ревизией.
## Деплой на произвольный домен ## Деплой на произвольный домен
**Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**: **Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**:
@@ -114,10 +159,7 @@ N/05_...
git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент # дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
``` ```
3. **Конфиг и реквизиты.** Рабочий `config.json` уже лежит в репозитории и обязателен к доставке на каждый 3. **Конфиг и реквизиты.** Рабочий `config.json` на хостинге — источник истины. При первичном создании домена его настраивают отдельно; обычный deploy не заменяет его репозиторным снимком и не перезаписывает `requisites.html`. Менять можно только явно согласованные ключи после чтения текущего файла.
приленд. Его не заменяют локальным вариантом и не исключают из синхронизации. Реквизиты задаются ключом
`requisites` в нём либо runtime-файлом `requisites.html`. 2026-10-05 конфиг пропал после заливки,
и преленд слал загрузку на собственный домен; это больше не допускается.
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`): 4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
```nginx ```nginx
server { server {
@@ -142,43 +184,57 @@ N/05_...
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x' curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
# 302 на https://<host>/ — точка передачи на сайте есть # 302 на https://<host>/ — точка передачи на сайте есть
``` ```
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на В браузере: открыть `https://<домен>/?indpic=abcd1efghi&theme=abcd1efghi&subid=TEST`, выбрать фото — адрес сменится на
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка. `https://<host>/public/upscale?id=…&indpic=<новый шум с1>&theme=<новый шум с1>&subid=TEST…` (без `key=`), пойдёт обработка.
Это настоящая загрузка и обработка на сайте. Это настоящая загрузка и обработка на сайте.
## Автодеплой (Gitea Actions) ## Автодеплой (Gitea Actions)
`.gitea/workflows/deploy.yml`: после каждого коммита в `main` раннер заливает файлы репозитория на хостинги **всех Решение владельца от 19.10.2026, задача preland#4: сейчас выкатываем **только stage**.
целей** по FTP с TLS и проверяет страницу каждой. Цель — домен со своим хостингом; список целей — `matrix` в workflow:
```yaml | Экземпляр | Назначение | Выкат |
- name: barbariskatest # имя цели (видно в списке прогонов: deploy barbariskatest) |---|---|---|
url: https://preland.anton.barbariskatest.ru # адрес преленда — для проверки после заливки | https://zoomstage.online | Тестовый стенд | Push в staging, SFTP |
prefix: PRELAND # секреты PRELAND_FTP_HOST, PRELAND_FTP_USER, PRELAND_FTP_PASS | https://zoompixel.ru | Production | Отключён до отдельного поручения |
dir: "" # каталог сайта на FTP: "" — корень FTP (beget), иначе, например, public_html | https://zoompixel.online | Production | Отключён до отдельного поручения |
```
Цели выкатываются независимо (`fail-fast: false`): сбой одного хостинга не останавливает остальные. Прежний preland.anton.barbariskatest.ru выведен из использования и исключён из workflow.
Публикация изменений самого workflow в main не запускает доставку: триггер оставлен только для staging.
**Новый домен на своём хостинге:** Workflow `.gitea/workflows/deploy.yml` содержит единственную job deploy-zoomstage на
1. Домен и HTTPS — как в «Деплой на произвольный домен», шаг 1. выделенном runner zoomzilla-production. Production возвращается только отдельной задачей
2. Секреты `<ПРЕФИКС>_FTP_HOST`, `<ПРЕФИКС>_FTP_USER`, `<ПРЕФИКС>_FTP_PASS` (Gitea → настройки репозитория → и прямым поручением владельца. Ошибки старого FTP больше не являются блокером выпуска.
Actions → секреты; порт 21, FTP с TLS). В коде реквизитов нет.
3. Никаких ручных конфигов не создавать: единственный `config.json` приезжает вместе с кодом.
4. Запись в `matrix` — коммит в `main`, автодеплой выкатит все цели вместе с конфигом.
**Что заливается.** Каждый deploy получает файлы репозитория (`git ls-files`), кроме служебных Рабочие config.json/requisites.html на хостинге не перезаписываются. Корневые наборы
(`.gitea/`, `README.md`, `.gitignore`) и живущего на хостинге `requisites.html`. Единственный /N/ принадлежат stage и исключены из кодовой доставки. Родительские каталоги создаются
версионируемый `config.json` всегда доставляется вместе с кодом. штатным SFTP-генератором; smoke проверяет страницу и assets наборов1/2.
На хостинге ничего не удаляется: файлы, убранные из репозитория, удалять вручную. Откат — отдельный согласованный revert в staging, который обновляет только stage.
**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет ## Android: фото и файлы (2026-10-19)
`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает. Основная кнопка выбирает 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/ по-прежнему редактируется на stage и не перезаписывается обычной кодовой доставкой; все150файлов перед выкатом сверяются с Git. Drift блокирует выпуск до импорта актуального набора. При полном переносе репозитория набор уже должен быть правильным.
- 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/*`) с хостинга; вернуть их той же Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put.
загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход
по ключу отдаст 404. ## Доменный бренд и минимальный подвал (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, который рамочный тест не замечал.
+27
View File
@@ -0,0 +1,27 @@
<?php
/** Готовые доменные SVG, без генерации логотипа при запросе страницы. */
declare(strict_types=1);
function prelandBrand(string $root, string $domain): array
{
$catalog = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
if (!isset($catalog[$domain])) throw new RuntimeException('Для домена не утверждён бренд: ' . $domain);
$brand = $catalog[$domain];
if (array_keys($brand['logos']) !== ['docs/logo.svg', 'docs/logo-alt.svg']) throw new RuntimeException('Нужна полная пара SVG');
foreach ($brand['logos'] as $target => $record) {
if (!preg_match('~^brands/' . preg_quote($domain, '~') . '/(?:light|dark)\.svg$~D', $record['file'])) throw new RuntimeException('Некорректный путь бренда');
$body = str_replace("\r\n", "\n", file_get_contents($root . '/' . $record['file']));
if (hash('sha256', $body) !== $record['sha256'] || str_contains($body, '<text') || !str_contains($body, '<title>' . $brand['label'] . '</title>')) throw new RuntimeException('Не совпал утверждённый готовый SVG: ' . $target);
}
return $brand;
}
function prelandBrandState(array $actual, array $brand): int
{
$expected = array_map(static fn(array $record): string => $record['sha256'], $brand['logos']);
if ($actual === $expected) return 0;
if (array_keys($actual) !== array_keys($expected)) return 1;
foreach ($expected as $path => $hash) {
if ($actual[$path] !== $hash && $actual[$path] !== ($brand['replace_from'][$path] ?? null)) return 1;
}
return 10;
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/** preland#17: миграция только utm.chb → utm.cons; остальные значения владельца сохраняются. */
declare(strict_types=1);
if ($argc !== 2) {
fwrite(STDERR, "Использование: php bin/ensure-cons-config.php /path/to/config.json\n");
exit(2);
}
try {
$path = $argv[1];
$raw = @file_get_contents($path);
if ($raw === false) throw new RuntimeException('Не удалось прочитать config.json.');
// Object decode сохраняет типы соседних {} и объектов с числовыми ключами.
$config = json_decode($raw, false, 512, JSON_THROW_ON_ERROR);
if (!$config instanceof stdClass || !($config->utm ?? null) instanceof stdClass) throw new RuntimeException('В config.json отсутствует объект utm.');
$hasCons = property_exists($config->utm, 'cons');
$cons = $hasCons ? $config->utm->cons : (object) ['weights' => (object) ['1' => 100, '2' => 0], 'default' => '2'];
if (!$cons instanceof stdClass || !(is_array($cons->weights ?? null) || ($cons->weights ?? null) instanceof stdClass)
|| !(is_string($cons->default ?? null) || is_int($cons->default ?? null))
|| !preg_match('/^[1-9][0-9]*$/D', (string) $cons->default)
|| (float) $cons->default > 2147483647) {
throw new RuntimeException('Существующий utm.cons некорректен; автоматическая перезапись запрещена.');
}
if (!property_exists($config->utm, 'chb') && $hasCons) {
fwrite(STDOUT, "cons уже настроен: config не изменён.\n");
exit(0);
}
unset($config->utm->chb);
$config->utm->cons = $cons;
$encoded = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
if (file_put_contents($path, $encoded, LOCK_EX) === false) throw new RuntimeException('Не удалось записать config.json.');
fwrite(STDOUT, "Мигрированы только utm.chb/cons; остальные поля и существующие настройки cons сохранены.\n");
exit(10);
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage() . "\n");
exit(2);
}
+96
View File
@@ -0,0 +1,96 @@
<?php
/** api#326: заменяет только согласованные JSON-токены, сохраняя прочие байты. */
declare(strict_types=1);
/** Карта диапазонов исходного JSON; числовые токены никогда не сериализуются. */
function utmConfigNode(string $raw, int &$offset): array
{
$length=strlen($raw);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$start=$offset; $char=$raw[$offset] ?? '';
if ($char==='"') {
$offset++;
while ($offset<$length) {
if ($raw[$offset]==='\\') { $offset+=2; continue; }
if ($raw[$offset++]==='"') break;
}
return ['start'=>$start,'end'=>$offset,'type'=>'string','value'=>json_decode(substr($raw,$start,$offset-$start),true,512,JSON_THROW_ON_ERROR)];
}
if ($char==='{' || $char==='[') {
$object=$char==='{'; $close=$object ? '}' : ']'; $offset++; $properties=[];
while (true) {
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===$close) { $offset++; break; }
if ($object) {
$key=utmConfigNode($raw,$offset);
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
$offset++; // Двоеточие уже проверено полным JSON validation.
$value=utmConfigNode($raw,$offset);
$properties[]=['key'=>$key,'value'=>$value];
} else { utmConfigNode($raw,$offset); }
while ($offset<$length && ctype_space($raw[$offset])) $offset++;
if (($raw[$offset] ?? '')===',') { $offset++; continue; }
$offset++; break;
}
return ['start'=>$start,'end'=>$offset,'type'=>$object ? 'object' : 'array','properties'=>$properties];
}
while ($offset<$length && !str_contains(",]} \t\r\n",$raw[$offset])) $offset++;
return ['start'=>$start,'end'=>$offset,'type'=>'scalar'];
}
/** Выбирает единственное свойство; неоднозначные управляющие поля не мигрирует. */
function utmConfigProperty(array $node, string $key): ?array
{
$found=null;
foreach ($node['properties'] ?? [] as $property) {
if ($property['key']['value']!==$key) continue;
if ($found!==null) throw new RuntimeException('Повторное управляющее поле: '.$key);
$found=$property;
}
return $found;
}
if ($argc!==2) { fwrite(STDERR,"Использование: php bin/ensure-utm-config.php config.json\n"); exit(2); }
try {
$path=$argv[1]; $raw=file_get_contents($path);
// Валидация всей грамматики без последующей сериализации decoded данных.
json_decode($raw,false,512,JSON_THROW_ON_ERROR|JSON_BIGINT_AS_STRING);
$offset=0; $root=utmConfigNode($raw,$offset);
$utm=utmConfigProperty($root,'utm')['value'] ?? null;
if (($root['type'] ?? null)!=='object' || ($utm['type'] ?? null)!=='object') throw new RuntimeException('Отсутствует объект utm');
$replacements=[];
$replace=static function(array $node,string $value) use (&$replacements): void {
$replacements[]=['start'=>$node['start'],'end'=>$node['end'],'text'=>json_encode($value,JSON_THROW_ON_ERROR)];
};
foreach (['theme'=>['dark'=>'1','light'=>'2'],'reg'=>['before'=>'1','after'=>'2'],'banch'=>['b'=>'1','a'=>'2'],'focus'=>['face'=>'1','center'=>'2']] as $key=>$map) {
$property=utmConfigProperty($utm,$key);
$aliases=['reg'=>['registration'],'banch'=>['chob','bchoice']][$key] ?? [];
foreach ($aliases as $old) {
$legacy=utmConfigProperty($utm,$old);
if ($property && $legacy) throw new RuntimeException('Конфликт старого и нового UTM: '.$key);
if ($legacy) { $replace($legacy['key'],$key); $property=$legacy; }
}
if (!$property) continue;
$setting=$property['value'];
$weights=utmConfigProperty($setting,'weights')['value'] ?? null;
$default=utmConfigProperty($setting,'default')['value'] ?? null;
if (($setting['type'] ?? null)!=='object' || ($weights['type'] ?? null)!=='object' || !$default) throw new RuntimeException('Некорректный UTM: '.$key);
$seen=[];
foreach ($weights['properties'] as $variant) {
$name=$variant['key']['value']; $digit=$map[$name] ?? $name;
if (!in_array($digit,['1','2'],true) || isset($seen[$digit])) throw new RuntimeException('Неоднозначный вариант UTM: '.$key);
$seen[$digit]=true;
if ($digit!==$name) $replace($variant['key'],$digit);
}
$value=json_decode(substr($raw,$default['start'],$default['end']-$default['start']),false,512,JSON_THROW_ON_ERROR);
if (!(is_string($value)||is_int($value))) throw new RuntimeException('Некорректный default UTM: '.$key);
$digit=$map[$value] ?? $value;
if (!in_array((string)$digit,['1','2'],true)) throw new RuntimeException('Некорректный default UTM: '.$key);
if ($digit!==$value) $replace($default,$digit);
}
if (!$replacements) { echo "Числовые UTM уже настроены; config не изменён.\n"; exit(0); }
usort($replacements,static fn(array $a,array $b): int=>$b['start']<=>$a['start']);
foreach ($replacements as $replacement) $raw=substr_replace($raw,$replacement['text'],$replacement['start'],$replacement['end']-$replacement['start']);
if (file_put_contents($path,$raw,LOCK_EX)===false) throw new RuntimeException('Не удалось записать config');
echo "Мигрированы только согласованные имена и enum UTM; остальные байты сохранены.\n"; exit(10);
} catch(Throwable $error) { fwrite(STDERR,$error->getMessage()."\n"); exit(2); }
+130
View File
@@ -0,0 +1,130 @@
<?php
/** preland#11: проверенный состав ресурсов; исполняется до SFTP. */
declare(strict_types=1);
require_once __DIR__ . '/brand-contract.php';
function releaseAssetBytes(string $path, string $bytes): string
{
return preg_match('/\.(svg|css|html|webmanifest)$/', $path) ? str_replace("\r\n", "\n", $bytes) : $bytes;
}
function releaseStaticFiles(string $root): array
{
$files = [];
foreach (scandir($root) as $name) {
if (in_array($name, ['docs', 'favicon', 'fonts', 'images'], true) || ctype_digit($name)) {
$directory = $root . '/' . $name;
if (!is_dir($directory)) continue;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $entry) {
$path = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
if ($name === 'docs' && !preg_match('~^docs/[^/]+\.(svg|pdf)$~', $path)) continue;
if ($entry->isFile()) $files[] = $path;
}
} elseif (preg_match('/\.css$/', $name)) {
$files[] = $name;
}
}
sort($files, SORT_STRING);
return $files;
}
function validateReleaseAssets(string $root): array
{
$manifest = json_decode((string) file_get_contents($root . '/release/assets.json'), true, 512, JSON_THROW_ON_ERROR);
$files = $manifest['files'] ?? [];
$brands = json_decode(file_get_contents($root . '/release/brands.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (array_keys($brands) as $domain) prelandBrand($root, $domain);
$required = ['docs/logo.svg', 'docs/logo-alt.svg', 'docs/privacy_policy.pdf', 'docs/public_offer.pdf', 'docs/tariffs.pdf', 'docs/user_agreement.pdf'];
foreach ($required as $path) {
if (!isset($files[$path])) throw new RuntimeException('В manifest отсутствует обязательный ресурс ' . $path);
}
$names = array_keys($files); sort($names, SORT_STRING);
if ($names !== releaseStaticFiles($root)) throw new RuntimeException('Состав статических ресурсов отличается от manifest; синхронизируйте актуальный набор');
foreach ($files as $path => $record) {
if (!preg_match('~^[A-Za-z0-9._/-]+$~', $path) || str_contains($path, '..')) throw new RuntimeException('Недопустимый путь ресурса');
$bytes = releaseAssetBytes($path, (string) file_get_contents($root . '/' . $path));
$blob = sha1('blob ' . strlen($bytes) . chr(0) . $bytes);
if ($blob === '91267982e1ad373b6ebd5fb2b761b34fe2a0c2a5' || preg_match('/WBE\s*70/i', $bytes)) {
throw new RuntimeException('Запрещённая заглушка бренда: ' . $path);
}
if (($record['sha256'] ?? '') !== hash('sha256', $bytes)) throw new RuntimeException('Ресурс не соответствует согласованному снимку: ' . $path);
}
return $files;
}
function releaseFiles(string $root): array
{
$files = array_values(array_filter(releaseStaticFiles($root), static fn(string $path): bool => !preg_match('~^(?:[0-9]+/|docs/logo(?:-alt)?\.svg$)~', $path)));
foreach (['.htaccess', 'index.php', 'i18n.php', 'template.php'] as $path) $files[] = $path;
foreach (['components', 'messages'] as $directory) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/' . $directory, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $entry) if ($entry->isFile() && $entry->getExtension() === 'php') {
$files[] = str_replace('\\', '/', substr($entry->getPathname(), strlen($root) + 1));
}
}
sort($files, SORT_STRING);
return $files;
}
function verifyRemoteReleaseAssets(array $files, string $base, bool $setsOnly): void
{
if (!preg_match('~^https://[A-Za-z0-9.-]+$~', $base)) throw new RuntimeException('Требуется HTTPS origin');
if (!$setsOnly) {
$brand = prelandBrand(dirname(__DIR__), (string) parse_url($base, PHP_URL_HOST));
foreach ($brand['logos'] as $path => $record) $files[$path]['sha256'] = $record['sha256'];
}
$count = 0;
foreach ($files as $path => $record) {
if ($setsOnly && !preg_match('~^[0-9]+/~', $path)) continue;
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]);
$bytes = @file_get_contents($base . '/' . $path . '?release_sha=' . $record['sha256'], false, $context);
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
throw new RuntimeException('Хост отличается от Git: ' . $path . '. Сначала синхронизируйте актуальный снимок по задаче.');
}
$count++;
}
fwrite(STDERR, "OK: remote {$count} resources\n");
}
/** Сверка с текущей страницей основного сайта, а не с оставшимся старым asset URL. */
function verifyPrimaryAssets(string $root, array $files): void
{
$origin = 'https://zoomzilla.ru';
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
$html = @file_get_contents($origin . '/?theme=light&preland_asset_check=1', false, $context);
if ($html === false) throw new RuntimeException('Основной сайт недоступен для сверки ресурсов');
foreach ($files as $path => $record) {
if (str_starts_with($path, 'docs/logo')) continue;
if (!str_starts_with($path, 'docs/') && !str_starts_with($path, 'favicon/')) continue;
$name = basename($path === 'docs/logo.svg' ? 'logo-light.svg' : $path);
$pattern = '~(?:src|href)=["\']([^"\']*/' . preg_quote($name, '~') . '(?:\?[^"\']*)?)["\']~';
if (!preg_match($pattern, $html, $match)) {
// Часть размеров favicon ссылается только из manifest; общий каталог тот же.
if (!str_starts_with($path, 'favicon/') || !preg_match('~href=["\']([^"\']*/favicon\.ico)["\']~', $html, $base)) throw new RuntimeException('Текущая ссылка основного сайта не найдена: ' . $path);
$url = dirname($base[1]) . '/' . $name;
} else $url = html_entity_decode($match[1], ENT_QUOTES, 'UTF-8');
if (str_starts_with($url, '/')) $url = $origin . $url;
if (!str_starts_with($url, $origin . '/')) throw new RuntimeException('Неожиданный origin ресурса: ' . $path);
$bytes = @file_get_contents($url, false, $context);
if ($bytes === false || hash('sha256', releaseAssetBytes($path, $bytes)) !== $record['sha256']) {
throw new RuntimeException('Основной сайт обновлён: синхронизируйте ' . $path . ' в Git перед выпуском');
}
}
fwrite(STDERR, "OK: primary PDF/favicon актуальны\n");
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
try {
$root = dirname(__DIR__);
$files = validateReleaseAssets($root);
$mode = $argv[1] ?? '--check';
if ($mode === '--files') echo implode("\n", releaseFiles($root)), "\n";
elseif ($mode === '--primary') verifyPrimaryAssets($root, $files);
elseif ($mode === '--remote' || $mode === '--stage-sets') verifyRemoteReleaseAssets($files, $argv[2] ?? '', $mode === '--stage-sets');
elseif ($mode === '--check') echo 'OK: release assets ', count($files), "\n";
else throw new RuntimeException('Неизвестный режим');
} catch (Throwable $error) {
fwrite(STDERR, $error->getMessage() . "\n"); exit(1);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/** Отдельная явно согласованная миграция бренда. Обычная code-поставка SVG не пишет. */
declare(strict_types=1);
require __DIR__ . '/brand-contract.php';
try {
$mode = $argv[1] ?? ''; $url = $argv[2] ?? '';
if (!preg_match('~^https://([a-z0-9.-]+)$~D', $url, $match)) throw new RuntimeException('Требуется HTTPS origin цели');
$brand = prelandBrand(dirname(__DIR__), $match[1]);
if ($mode === '--files') {
foreach ($brand['logos'] as $target => $record) echo $record['file'], ' ', $target, "\n";
exit(0);
}
if ($mode !== '--status') throw new RuntimeException('Неизвестный режим бренда');
$actual = []; $expected = [];
foreach ($brand['logos'] as $target => $record) {
$context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]);
$body = @file_get_contents($url . '/' . $target . '?brand_check=' . $record['sha256'], false, $context);
if ($body === false) throw new RuntimeException('Не удалось прочитать бренд цели: ' . $target);
$actual[$target] = hash('sha256', str_replace("\r\n", "\n", $body));
$expected[$target] = $record['sha256'];
}
$state = prelandBrandState($actual, $brand);
if ($state === 0) { echo "OK: доменный бренд совпал; SVG не перезаписываются\n"; exit(0); }
if ($state === 10) { echo "Разрешена точная миграция бренда по preland#12\n"; exit(10); }
throw new RuntimeException('На хосте другой бренд: обычный deploy не имеет права его менять');
} catch (Throwable $error) { fwrite(STDERR, $error->getMessage() . "\n"); exit(1); }
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff. Show more