diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b434703..d9bbe36 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -23,7 +23,24 @@ jobs: 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 - 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: 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: Preserve host config and add focus if absent env: @@ -39,13 +56,22 @@ jobs: 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" + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" 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" ;; + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" ;; + *) exit "$changed" ;; + esac + # preland#11: отдельное прямое разрешение21.10 на актуальные реквизиты. + changed=0 + php bin/ensure-requisites-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" -b "$batch" "$SFTP_USER@$SFTP_HOST" ;; *) exit "$changed" ;; esac @@ -68,7 +94,8 @@ jobs: # Наборы `/N/` на stage — редактируемый контент. Кодовая доставка их # намеренно не перезаписывает: актуальное содержимое stage фиксируют в Git # отдельной синхронизацией по задаче. - git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|requisites\.html$|config\.json$|[0-9]+/)' > "$files" + # Положительный список: tests/bin/планы не публикуются. + php bin/release-assets.php --files | grep -vE '^(config\.json$|[0-9]+/)' > "$files" if grep -q '[^A-Za-z0-9._/-]' "$files"; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' "$files"; exit 1; fi printf '%s\n' "-mkdir $REMOTE_DIR" > "$batch" while read -r file; do @@ -83,7 +110,7 @@ jobs: done < "$files" sort -u "$dirs" | while read -r dir; do printf '%s\n' "-mkdir $REMOTE_DIR/$dir" >> "$batch"; done while read -r file; do printf '%s\n' "put -p $file $REMOTE_DIR/$file" >> "$batch"; done < "$files" - SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" echo "залито файлов: $(wc -l < "$files")" - name: Check the page @@ -98,6 +125,26 @@ jobs: curl -fsS --max-time 30 "$PRELAND_URL/2/01_desk_1-source.avif" -o /dev/null || { echo "не опубликован второй набор примеров"; exit 1; } page2=$(curl -fsS --max-time 30 "$PRELAND_URL/?indpic=2&nc=$(date +%s)") printf '%s' "$page2" | grep -q '2/01_mob_1-source.avif' || { echo "indpic=2 не использует второй набор примеров"; exit 1; } + php bin/release-assets.php --remote "$PRELAND_URL" + php bin/release-assets.php --stage-requisites "$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, без рекурсии. + git ls-files tests bin docs/superpowers | while read -r file; do + printf '%s\n' "-rm $REMOTE_DIR/$file" >> "$batch" + done + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" - name: Remove obsolete demo directories env: @@ -121,4 +168,4 @@ jobs: done printf '%s\n' "-rmdir $REMOTE_DIR/images/$set" >> "$batch" done - SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" "$SFTP_USER@$SFTP_HOST" < "$batch" + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" diff --git a/AGENTS.md b/AGENTS.md index 517a545..4c184b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,3 +45,13 @@ ## Актуальная маршрутизация (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. diff --git a/CLAUDE.md b/CLAUDE.md index 07a7714..f596a5f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,3 +42,13 @@ План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на политику и документацию не подключают 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. diff --git a/README.md b/README.md index ac27f20..53c8f13 100644 --- a/README.md +++ b/README.md @@ -173,3 +173,15 @@ Workflow `.gitea/workflows/deploy.yml` содержит единственную ## Android: фото и файлы (2026-10-19) Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются. + +## Актуальные ресурсы (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. + +Повторная проверка выпуска: php bin/release-assets.php --stage-requisites https://zoomstage.online проверяет именно runtime config; успешная загрузка requisites.html сама по себе недостаточна. Все обязательные SFTP-команды выполняются через batch -b и прекращают выкат при ошибке put. diff --git a/bin/ensure-requisites-config.php b/bin/ensure-requisites-config.php new file mode 100644 index 0000000..d9fade1 --- /dev/null +++ b/bin/ensure-requisites-config.php @@ -0,0 +1,60 @@ + !is_string($line) || trim($line) === '')) throw new RuntimeException('Некорректный согласованный источник requisites'); + if (($config['requisites'] ?? null) === $expected) { echo "requisites актуальны: конфиг не изменён\n"; exit(0); } + $patched = replaceRequisites($raw, json_encode($expected, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); + $actual = json_decode($patched, true, 512, JSON_THROW_ON_ERROR); + $config['requisites'] = $expected; + // JSON-порядок ключей не меняет контракт, остальные значения должны совпасть. + if ($actual != $config) throw new RuntimeException('Изменились несогласованные поля'); + if (file_put_contents($path, $patched, LOCK_EX) === false) throw new RuntimeException('Запись config не удалась'); + echo "Изменён только requisites по решению владельца preland#11\n"; exit(10); +} catch (Throwable $error) { fwrite(STDERR, $error->getMessage() . "\n"); exit(2); } diff --git a/bin/release-assets.php b/bin/release-assets.php new file mode 100644 index 0000000..0d3ea86 --- /dev/null +++ b/bin/release-assets.php @@ -0,0 +1,141 @@ +getPathname(), strlen($root) + 1)); + if ($name === 'docs' && !preg_match('~^docs/[^/]+\.(svg|pdf)$~', $path)) continue; + if ($entry->isFile()) $files[] = $path; + } + } elseif (preg_match('/\.css$/', $name) || $name === 'requisites.html') { + $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'] ?? []; + $required = ['docs/logo.svg', 'docs/logo-alt.svg', 'docs/privacy_policy.pdf', 'docs/public_offer.pdf', 'docs/tariffs.pdf', 'docs/user_agreement.pdf', 'requisites.html']; + 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]+/~', $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'); + $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/') && !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 перед выпуском'); + } + } + if (!preg_match('~
]*>(.*?)
~su', $block[1], $paragraphs); + $actual = []; + foreach ($paragraphs[1] as $paragraph) { + $paragraph = preg_replace('/\s+/u', ' ', $paragraph); + $text = html_entity_decode(strip_tags(preg_replace('~?br\b[^>]*>~i', "\n", $paragraph)), ENT_QUOTES, 'UTF-8'); + foreach (explode("\n", $text) as $line) if (trim($line) !== '') $actual[] = trim($line); + } + $expected = json_decode(file_get_contents($root . '/release/requisites.json'), true, 512, JSON_THROW_ON_ERROR); + if ($actual !== $expected) throw new RuntimeException('Основной сайт обновил реквизиты; сначала синхронизируйте Git'); + fwrite(STDERR, "OK: primary logos/PDF/favicon/requisites актуальны\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 === '--stage-requisites') { + $base = $argv[2] ?? ''; + if (!preg_match('~^https://[A-Za-z0-9.-]+$~', $base)) throw new RuntimeException('Требуется HTTPS origin'); + $context = stream_context_create(['http' => ['timeout' => 30, 'header' => "Cache-Control: no-cache\r\n"]]); + $actual = json_decode((string) file_get_contents($base . '/config.json?requisites_check=1', false, $context), true, 512, JSON_THROW_ON_ERROR); + $expected = json_decode(file_get_contents($root . '/release/requisites.json'), true, 512, JSON_THROW_ON_ERROR); + if (($actual['requisites'] ?? null) !== $expected) throw new RuntimeException('Host config не получил актуальные requisites'); + echo "OK: runtime requisites актуальны\n"; + } + 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); + } +} diff --git a/components/asset-url.php b/components/asset-url.php new file mode 100644 index 0000000..7573c12 --- /dev/null +++ b/components/asset-url.php @@ -0,0 +1,9 @@ + - -346886, Ростовская обл., г. Батайск,
+ДНТ «Труд», ул. 11-я линия, д. 1195
+ИНН: 616202304132,
+ОГРНИП: 326619600205915
diff --git a/style.css b/style.css index fca555f..0e4b87a 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ body { font-family: "Inter", sans-serif; - /* fix WBE70 */ + /* Корневые отступы страницы задаёт общий layout. */ padding: unset !important; overflow: auto !important; } @@ -2910,7 +2910,7 @@ body { isolation: isolate; } padding-top: 0; padding-bottom: 0; } - :is(.index-mobile-fit, .tour-processing) #preview { height: auto; aspect-ratio: 1696 / 920; } + :is(.index-mobile-fit, .tour-processing) #preview { height: 100%; aspect-ratio: auto; } } @media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } } /* /api#298 */ @@ -2939,3 +2939,6 @@ body { isolation: isolate; } html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 225px; } html[data-upload-device=android] .index-mobile-fit .dropzone-form-action { min-height: 90px; } } + +/* preland#11: общий с API адаптивный платёжный бейдж подвала. */ +.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;} diff --git a/style.min.c.css b/style.min.c.css index 5a21363..d80cc76 100644 --- a/style.min.c.css +++ b/style.min.c.css @@ -841,3 +841,6 @@ padding-top: 2svh !important; @media(max-width:767.98px){.result-download-title{font-size:24px;line-height:1.2;}} @media (min-width:768px){.image-compare .icv__label{top:10px;width:92px;height:36px;display:flex;align-items:center;justify-content:center;padding:4px 10px;font:400 16px/26px Inter,sans-serif;color:#fff;backdrop-filter:none;-webkit-backdrop-filter:none;background:rgb(0 0 0 / 25%);border:1px solid rgb(255 255 255 / 80%);box-shadow:none;text-shadow:none;}} .upload-loading-dots{display:inline-grid;text-align:left;}.upload-loading-dots::before{content:'.....';visibility:hidden;grid-area:1 / 1;}.upload-loading-dots > span{grid-area:1 / 1;} + +/* preland#11: общий с API адаптивный платёжный бейдж подвала. */ +.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;} diff --git a/style.min.css b/style.min.css index 2fac99e..3030fe0 100644 --- a/style.min.css +++ b/style.min.css @@ -111,7 +111,7 @@ body { isolation: isolate; } padding-top: 0; padding-bottom: 0; } - :is(.index-mobile-fit, .tour-processing) #preview { height: auto; aspect-ratio: 1696 / 920; } + :is(.index-mobile-fit, .tour-processing) #preview { height: 100%; aspect-ratio: auto; } } @media (max-width: 767.98px) { .wrapper { --site-header-height: 68px; } } /* /api#298 */ @@ -140,3 +140,6 @@ body { isolation: isolate; } html[data-upload-device=mobile] .index-mobile-fit #dropzone-form { min-height: 225px; } html[data-upload-device=android] .index-mobile-fit .dropzone-form-action { min-height: 90px; } } + +/* preland#11: общий с API адаптивный платёжный бейдж подвала. */ +.footer-main img[src$="/images/payment.svg"]{max-width:100%;height:auto;} diff --git a/template.php b/template.php index fd432a1..45e7dcc 100644 --- a/template.php +++ b/template.php @@ -57,8 +57,8 @@