From ec087ea63641417f7a505ca28590bbe2c42e21d5 Mon Sep 17 00:00:00 2001 From: Panfilov Konstantin Date: Wed, 21 Oct 2026 17:24:14 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B1=D1=80=D0=B5=D0=BD=D0=B4=20ZoomStage=20=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B8=D0=BC=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B2=D0=B0=D0=BB=20(refs=20#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy.yml | 39 ++++++++++++++------ AGENTS.md | 12 +++++++ CLAUDE.md | 12 +++++++ README.md | 12 +++++++ bin/brand-contract.php | 27 ++++++++++++++ bin/ensure-requisites-config.php | 60 ------------------------------- bin/release-assets.php | 35 +++++++----------- bin/release-brand.php | 26 ++++++++++++++ brands/zoomstage.online/dark.svg | 46 ++++++++++++++++++++++++ brands/zoomstage.online/light.svg | 46 ++++++++++++++++++++++++ components/asset-url.php | 4 +-- components/site-domain.php | 11 ++++++ docs/logo-alt.svg | 58 +++++++++++++++--------------- docs/logo.svg | 58 +++++++++++++++--------------- index.php | 1 + release/assets.json | 14 +++----- release/brands.json | 20 +++++++++++ release/requisites.json | 7 ---- requisites.html | 5 --- template.php | 37 +++---------------- tests/check-preland-footer.php | 20 +++++++++++ tests/check-release-assets.php | 18 ++++++++-- tests/check-requisites-config.php | 28 --------------- tests/render-preland.php | 6 ++++ 24 files changed, 365 insertions(+), 237 deletions(-) create mode 100644 bin/brand-contract.php delete mode 100644 bin/ensure-requisites-config.php create mode 100644 bin/release-brand.php create mode 100644 brands/zoomstage.online/dark.svg create mode 100644 brands/zoomstage.online/light.svg create mode 100644 components/site-domain.php create mode 100644 release/brands.json delete mode 100644 release/requisites.json delete mode 100644 requisites.html create mode 100644 tests/check-preland-footer.php delete mode 100644 tests/check-requisites-config.php create mode 100644 tests/render-preland.php diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index d9bbe36..840061b 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -65,16 +65,32 @@ jobs: 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 - + - name: Apply approved domain brand only if needed + 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 + status=0 + php bin/release-brand.php --status "$PRELAND_URL" || status=$? + case "$status" in 0) exit 0 ;; 10) ;; *) exit "$status" ;; esac + # Только точная ранее согласованная пара заменяется по preland#12. + # Обычные выпуски с правильным брендом сюда не доходят. + umask 077 + known_hosts=$(mktemp); batch=$(mktemp); files=$(mktemp) + trap 'rm -f "$known_hosts" "$batch" "$files"' EXIT + printf '%s\n' "$SFTP_KNOWN_HOSTS" > "$known_hosts" + php bin/release-brand.php --files "$PRELAND_URL" > "$files" + revision=$(git rev-parse HEAD) + while read -r source target; do + # Оборванный put не должен оставлять усечённый действующий SVG. + printf '%s\n' "put -p $source $REMOTE_DIR/$target.upload-$revision" >> "$batch" + printf '%s\n' "rename $REMOTE_DIR/$target.upload-$revision $REMOTE_DIR/$target" >> "$batch" + done < "$files" + SSHPASS="$SFTP_PASS" sshpass -e sftp -oBatchMode=no -oStrictHostKeyChecking=yes -oUserKnownHostsFile="$known_hosts" -b "$batch" "$SFTP_USER@$SFTP_HOST" + php bin/release-brand.php --status "$PRELAND_URL" - name: Upload over SFTP env: SFTP_HOST: ${{ secrets.ZOOMPIXEL_SFTP_HOST }} @@ -126,7 +142,7 @@ jobs: 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" + php bin/release-brand.php --status "$PRELAND_URL" - name: Remove previously published internal files env: @@ -141,6 +157,7 @@ jobs: 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 diff --git a/AGENTS.md b/AGENTS.md index 4c184b2..bf54da2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,3 +55,15 @@ - /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 продуктовый код не меняется. diff --git a/CLAUDE.md b/CLAUDE.md index f596a5f..78a5712 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,3 +52,15 @@ - /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 продуктовый код не меняется. diff --git a/README.md b/README.md index aaa2be0..beb5ba6 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,15 @@ Workflow `.gitea/workflows/deploy.yml` содержит единственную - 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. + +## Доменный бренд и минимальный подвал (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 продуктовый код не меняется. diff --git a/bin/brand-contract.php b/bin/brand-contract.php new file mode 100644 index 0000000..74d4ca0 --- /dev/null +++ b/bin/brand-contract.php @@ -0,0 +1,27 @@ + $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, '' . $brand['label'] . '')) 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; +} diff --git a/bin/ensure-requisites-config.php b/bin/ensure-requisites-config.php deleted file mode 100644 index d9fade1..0000000 --- a/bin/ensure-requisites-config.php +++ /dev/null @@ -1,60 +0,0 @@ - !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 index 0d3ea86..fbb187c 100644 --- a/bin/release-assets.php +++ b/bin/release-assets.php @@ -1,6 +1,7 @@ isFile()) $files[] = $path; } - } elseif (preg_match('/\.css$/', $name) || $name === 'requisites.html') { + } elseif (preg_match('/\.css$/', $name)) { $files[] = $name; } } @@ -32,7 +33,9 @@ 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']; + $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); } @@ -52,7 +55,7 @@ function validateReleaseAssets(string $root): array function releaseFiles(string $root): array { - $files = array_values(array_filter(releaseStaticFiles($root), static fn(string $path): bool => !preg_match('~^[0-9]+/~', $path))); + $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)); @@ -67,6 +70,10 @@ function releaseFiles(string $root): array 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; @@ -88,6 +95,7 @@ function verifyPrimaryAssets(string $root, array $files): void $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, '~') . '(?:\?[^"\']*)?)["\']~'; @@ -103,17 +111,7 @@ function verifyPrimaryAssets(string $root, array $files): void throw new RuntimeException('Основной сайт обновлён: синхронизируйте ' . $path . ' в Git перед выпуском'); } } - if (!preg_match('~]*class=["\'][^"\']*\bh5\b[^"\']*["\'][^>]*>\s*Наши реквизиты\s*(.*?)~su', $html, $block)) throw new RuntimeException('Не найдены текущие реквизиты основного сайта'); - preg_match_all('~]*>(.*?)

~su', $block[1], $paragraphs); - $actual = []; - foreach ($paragraphs[1] as $paragraph) { - $paragraph = preg_replace('/\s+/u', ' ', $paragraph); - $text = html_entity_decode(strip_tags(preg_replace('~]*>~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"); + fwrite(STDERR, "OK: primary PDF/favicon актуальны\n"); } if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { @@ -123,15 +121,6 @@ if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { $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('Неизвестный режим'); diff --git a/bin/release-brand.php b/bin/release-brand.php new file mode 100644 index 0000000..4b76c71 --- /dev/null +++ b/bin/release-brand.php @@ -0,0 +1,26 @@ + $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); } diff --git a/brands/zoomstage.online/dark.svg b/brands/zoomstage.online/dark.svg new file mode 100644 index 0000000..f2ecdb9 --- /dev/null +++ b/brands/zoomstage.online/dark.svg @@ -0,0 +1,46 @@ + +ZoomStage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brands/zoomstage.online/light.svg b/brands/zoomstage.online/light.svg new file mode 100644 index 0000000..3e11793 --- /dev/null +++ b/brands/zoomstage.online/light.svg @@ -0,0 +1,46 @@ + +ZoomStage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/asset-url.php b/components/asset-url.php index 7573c12..48bef5f 100644 --- a/components/asset-url.php +++ b/components/asset-url.php @@ -2,8 +2,8 @@ /** Версия логотипа по содержимому исключает старый бренд из браузерного кеша. */ function prelandAssetUrl(string $path): string { - global $assetsUriPrefix; $file = dirname(__DIR__) . '/' . $path; $version = is_file($file) ? substr(hash_file('sha256', $file), 0, 16) : 'missing'; - return htmlspecialchars(rtrim($assetsUriPrefix, '/') . '/' . $path . '?v=' . $version, ENT_QUOTES, 'UTF-8'); + // Бренд всегда принадлежит текущему преленду, даже если общая статика на CDN/API. + return htmlspecialchars('/' . $path . '?v=' . $version, ENT_QUOTES, 'UTF-8'); } diff --git a/components/site-domain.php b/components/site-domain.php new file mode 100644 index 0000000..a9e6f7a --- /dev/null +++ b/components/site-domain.php @@ -0,0 +1,11 @@ +]/', $candidate)) continue; + $host = strtolower(rtrim(preg_replace('/:\d+$/', '', $candidate), '.')); + if (strlen($host) <= 253 && preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/D', $host)) return $host; + } + return ''; +} diff --git a/docs/logo-alt.svg b/docs/logo-alt.svg index 9f292d3..f2ecdb9 100644 --- a/docs/logo-alt.svg +++ b/docs/logo-alt.svg @@ -1,46 +1,46 @@ +ZoomStage - + - - - - - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/docs/logo.svg b/docs/logo.svg index 3dceb74..3e11793 100644 --- a/docs/logo.svg +++ b/docs/logo.svg @@ -1,46 +1,46 @@ +ZoomStage - + - - - - - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + diff --git a/index.php b/index.php index 8cc9c90..a76b206 100644 --- a/index.php +++ b/index.php @@ -2,6 +2,7 @@ require_once __DIR__ . '/i18n.php'; require_once __DIR__ . '/components/asset-url.php'; +require_once __DIR__ . '/components/site-domain.php'; $configFile = __DIR__ . '/config.json'; $config = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; diff --git a/release/assets.json b/release/assets.json index 095ad5f..8c2ecd8 100644 --- a/release/assets.json +++ b/release/assets.json @@ -1,5 +1,5 @@ { - "issue": "anton/preland#11", + "issue": "anton/preland#12", "captured": "2026-10-21", "hashing": "SHA-256; SVG/CSS/HTML/webmanifest CRLF normalized to LF", "files": { @@ -612,12 +612,12 @@ "source": "https://zoomstage.online/bootstrap.min.css" }, "docs/logo-alt.svg": { - "sha256": "919b0a436e6ffea1ec833a28e5b8c6f4d26029944a7d555ca4d61da63ea1f86f", - "source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/logo-alt.svg" + "sha256": "bf31f25bd0cb636b0a4625747be4a0e4a7cd8f9d5c4e1fc43580556e039d32e8", + "source": "brands/zoomstage.online/dark.svg" }, "docs/logo.svg": { - "sha256": "1be54a2f9aa40b6d8740fd3367f045c5edf1cdea87c26d67b60cf663d4a2436f", - "source": "https://zoomzilla.ru/assets/6fd2ff6b/docs/logo-light.svg" + "sha256": "cce6e077430eab850b7f4409d1c42f95347459efe753d0d65415d65b6f3a5b01", + "source": "brands/zoomstage.online/light.svg" }, "docs/privacy_policy.pdf": { "sha256": "61b483911be875810a7920efa5ddb9c9a132633cfe1d5d9efeb8f9d487cacf83", @@ -723,10 +723,6 @@ "sha256": "36c7e1c142c883f746eb5851278a34249897ec16aadfbbb9d9bbf5b11336459b", "source": "https://zoomstage.online/images/sprite.svg" }, - "requisites.html": { - "sha256": "5d7d42da0029f2a90bebb2fc10bb507be3725a4be3fb8237f3afe21cfb485c88", - "source": "https://zoomzilla.ru/" - }, "style.css": { "sha256": "102aee00546d897dd94887d69b35fec0b3430448ef6a21e072f5b7d76964ce06", "source": "https://zoomstage.online/style.css" diff --git a/release/brands.json b/release/brands.json new file mode 100644 index 0000000..b4a3395 --- /dev/null +++ b/release/brands.json @@ -0,0 +1,20 @@ +{ + "zoomstage.online": { + "label": "ZoomStage", + "issue": 12, + "logos": { + "docs/logo.svg": { + "file": "brands/zoomstage.online/light.svg", + "sha256": "cce6e077430eab850b7f4409d1c42f95347459efe753d0d65415d65b6f3a5b01" + }, + "docs/logo-alt.svg": { + "file": "brands/zoomstage.online/dark.svg", + "sha256": "bf31f25bd0cb636b0a4625747be4a0e4a7cd8f9d5c4e1fc43580556e039d32e8" + } + }, + "replace_from": { + "docs/logo.svg": "1be54a2f9aa40b6d8740fd3367f045c5edf1cdea87c26d67b60cf663d4a2436f", + "docs/logo-alt.svg": "919b0a436e6ffea1ec833a28e5b8c6f4d26029944a7d555ca4d61da63ea1f86f" + } + } +} diff --git a/release/requisites.json b/release/requisites.json deleted file mode 100644 index 21b2ffb..0000000 --- a/release/requisites.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - "ИП Кардаполова Яна Виталийсовна", - "346886, Ростовская обл., г. Батайск,", - "ДНТ «Труд», ул. 11-я линия, д. 1195", - "ИНН: 616202304132,", - "ОГРНИП: 326619600205915" -] diff --git a/requisites.html b/requisites.html deleted file mode 100644 index 5e13dfe..0000000 --- a/requisites.html +++ /dev/null @@ -1,5 +0,0 @@ -

ИП Кардаполова Яна Виталийсовна

-

346886, Ростовская обл., г. Батайск,

-

ДНТ «Труд», ул. 11-я линия, д. 1195

-

ИНН: 616202304132,

-

ОГРНИП: 326619600205915

diff --git a/template.php b/template.php index 45e7dcc..81df815 100644 --- a/template.php +++ b/template.php @@ -57,8 +57,8 @@