Author SHA1 Message Date
Panfilov Konstantin eac2489a49 fix: defer upload format gate to API 2026-10-15 05:14:55 +03:00
Panfilov Konstantin e7328d9d2b ui: compact visible format list
deploy-preland / deploy barbariskatest (push) Successful in 19s
2026-10-15 00:03:01 +03:00
Panfilov Konstantin 3eee288f78 feat: sync preland pdf picker policy
deploy-preland / deploy barbariskatest (push) Successful in 19s
2026-10-14 21:03:46 +03:00
Panfilov Konstantin 98db1c2a16 feat: defer preland format validation to api gate 2026-10-14 21:03:46 +03:00
yury 4fcb8164df #258: изменить подпись примеров на Preland Index
deploy-preland / deploy barbariskatest (push) Successful in 20s
Точечная текстовая правка. Проверки локально пройдены.
2026-10-13 19:05:30 +00:00
Panfilov Konstantin d489bbcd59 #258 Изменить подпись примеров на Index 2026-10-13 22:03:09 +03:00
Panfilov Konstantin 85e27ff2f2 docs: добавить инструкции агентов
deploy-preland / deploy barbariskatest (push) Failing after 4s
2026-10-08 13:41:52 +03:00
Panfilov Konstantin ff09b43996 Merge feature/requisites-config: реквизиты из config.json, автодеплой на несколько доменов
deploy-preland / deploy barbariskatest (push) Successful in 19s
2026-10-05 20:09:00 +03:00
Panfilov KonstantinandClaude Opus 5.5 98a82fa123 Автодеплой на несколько доменов с разными хостингами: цели — matrix (url, префикс секретов, каталог на FTP)
Цель — домен со своим хостингом: секреты <prefix>_FTP_HOST/USER/PASS ищутся по имени (проверено пробой на раннере),
dir — каталог сайта на FTP ("" — корень). Цели независимы (fail-fast: false), проверка страницы — у каждой своя.
config.json и requisites.html живут на хостинге каждой цели (в репозитории — только config.json.example), деплой их
не трогает; без config.json проверка новой цели падает. README: как добавить домен.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:08:33 +03:00
Panfilov KonstantinandClaude Opus 5.5 6cd01f8714 Реквизиты в подвале — из config.json (requisites), затем requisites.html, затем заглушка
Ключ requisites — список строк, каждая отдельным абзацем, текст экранируется. Без ключа — как было: requisites.html,
а если он пуст — заглушка «ИП Иванов» (решение заказчика). config.json.example — формат ключа; README.

Проверка — .extras/checks-preland-claim/t_requisites.sh (index.php в php:8.3-cli): до правки FAIL=3, после 6/6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:44:53 +03:00
Panfilov KonstantinandClaude Opus 5.5 01b5a03b8e Автодеплой: после коммита в main — заливка на хостинг преленда по FTP с TLS и проверка страницы
deploy-preland / deploy (push) Successful in 18s
Реквизиты — секреты репозитория PRELAND_FTP_HOST/USER/PASS. Не заливаются служебные файлы и живущие только на
хостинге config.json и requisites.html; на хостинге ничего не удаляется. После заливки страница проверяется: адрес
загрузки — на сайте, а не на самом преленде, переход — на claim. Один вызов curl на все файлы (одно соединение).
README: раздел «Автодеплой».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:37:25 +03:00
Panfilov KonstantinandClaude Opus 5.5 59e88ae964 README: что это, как работает (метки, загрузка, передача на сайт через claim), файлы, config.json, деплой на произвольный домен
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:34:06 +03:00
11 changed files with 384 additions and 41 deletions

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
name: deploy-preland
# После коммита в main — заливка преленда на хостинги всех целей (FTP с TLS) и проверка страницы каждой цели.
# Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория <prefix>_FTP_HOST,
# <prefix>_FTP_USER, <prefix>_FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает.
# Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, AGENTS.md, CLAUDE.md, .gitignore) и тех, что живут
# только на хостинге: config.json (в git его нет) и requisites.html (реквизиты заполняют на месте — пустой файл
# из git их бы затёр). На хостинге ничего не удаляется: удалённое из репозитория убирать вручную.
on:
push:
branches:
- main
jobs:
deploy:
name: deploy ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# name — имя цели, url — адрес преленда (для проверки), prefix — префикс секретов <prefix>_FTP_*,
# dir — каталог сайта на FTP ("" — корень FTP, как на beget). Новый домен — новая запись и три секрета;
# config.json (и requisites.html) создают на хостинге цели — без него проверка страницы не пройдёт.
- 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
- name: Checkout repository
uses: actions/checkout@v4
- 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$|config\.json$|requisites\.html$)' > 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 "страница в порядке"
+33
View File
@@ -0,0 +1,33 @@
# Инструкции агентов для `preland`
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
Этот файл и [CLAUDE.md](CLAUDE.md) содержат одинаковые локальные правила; обновлять их вместе.
## Перед работой
- Прочитать [README.md](README.md), [заметку проекта](../.known-base/Проекты/Преленд%20—%20anton-preland.md),
а для выката — `.gitea/workflows/deploy.yml` и `config.json.example`.
- Проверить состояние ветки и чужие изменения в `preland/`; изменения API сайта согласовать
с его контрактом и инструкциями `api/`.
## Локальные ограничения
- Преленд — PHP без фреймворка и сборки; `Yii::t` реализован локальной заглушкой `i18n.php`.
Сохранять существующие компоненты и передачу меток трафика на сайт.
- Загрузка идёт на `POST /api/images/public/create` с `file` и случайным `client_key`,
переход — через `/api/images/public/claim?key=…`. Ключ одноразовый, срок 30 минут;
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
Не обходить серверные правила доступа к загрузке и результату обработки.
- Рабочий `config.json` каждой цели и `requisites.html` живут на хостинге: не добавлять
их в git, не перезаписывать и не удалять при деплое. В репозитории — только образец конфига.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
в согласованном окружении, не публиковать ключи загрузок и FTP-секреты.
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
+33
View File
@@ -0,0 +1,33 @@
# Инструкции агентов для `preland`
Область действия — репозиторий `preland/`. Общие правила рабочей области находятся в
[AGENTS.md](../AGENTS.md) и [CLAUDE.md](../CLAUDE.md); канонический процесс и каталог ролей —
[в общей политике](../.known-base/Соглашения/Процесс%20командной%20разработки%20и%20каталог%20ролей.md).
Этот файл и [AGENTS.md](AGENTS.md) содержат одинаковые локальные правила; обновлять их вместе.
## Перед работой
- Прочитать [README.md](README.md), [заметку проекта](../.known-base/Проекты/Преленд%20—%20anton-preland.md),
а для выката — `.gitea/workflows/deploy.yml` и `config.json.example`.
- Проверить состояние ветки и чужие изменения в `preland/`; изменения API сайта согласовать
с его контрактом и инструкциями `api/`.
## Локальные ограничения
- Преленд — PHP без фреймворка и сборки; `Yii::t` реализован локальной заглушкой `i18n.php`.
Сохранять существующие компоненты и передачу меток трафика на сайт.
- Загрузка идёт на `POST /api/images/public/create` с `file` и случайным `client_key`,
переход — через `/api/images/public/claim?key=…`. Ключ одноразовый, срок 30 минут;
переход напрямую на `/public/upscale` не заменяет передачу владельца через claim.
- `host` задаёт сайт назначения; HTTPS необходим для `crypto.randomUUID()`.
Не обходить серверные правила доступа к загрузке и результату обработки.
- Рабочий `config.json` каждой цели и `requisites.html` живут на хостинге: не добавлять
их в git, не перезаписывать и не удалять при деплое. В репозитории — только образец конфига.
- Коммит в `main` запускает FTP-автодеплой на все цели workflow; откат через `git revert`
также запускает выкат. Коммит, push и изменения хостинга — только по прямому поручению.
- Проверки кода и браузера проводить в Docker без установки ПО на хост.
Проверка загрузки по боевому адресу вызывает реальную обработку; выполнять её только
в согласованном окружении, не публиковать ключи загрузок и FTP-секреты.
План, журнал и база знаний ведутся в корне рабочей области по общим правилам. Ссылки на
политику и документацию не подключают MCP и не дают разрешения на публикацию или выкат.
+160
View File
@@ -0,0 +1,160 @@
# Преленд zoomzilla
Одностраничный лендинг перед основным сайтом (`zoomzilla.ru`): посетитель с рекламы выбирает фото прямо на
преленде, файл сразу уходит в API сайта, а сам посетитель переходит на сайт — на страницу обработки этого фото.
Варианты страницы и сайта (A/B) задаются метками в адресе. Код — обычный PHP без фреймворка и сборки; переводы —
маленькая заглушка `Yii::t` в `i18n.php`.
Боевой экземпляр: `https://preland.anton.barbariskatest.ru` (хостинг beget). Пример ссылки входа:
`https://preland.anton.barbariskatest.ru/?indpic=1&theme=dark&degr=2&registration=after&bchoice=a&tp=1&utm_source=test&subid=CLICK1`.
## Как работает
### Метки
`index.php` разбирает параметры адреса по разделу `utm` из `config.json`. Для каждой настроенной метки:
- значение из адреса принимается, если оно есть в `weights`;
- `rnd` — случайное значение по весам (`weights`), так делится трафик между вариантами;
- неизвестное значение отбрасывается, отсутствующее — заменяется на `default`.
Ненастроенные параметры (`utm_source`, `keyword`, `subid` и любые другие) проходят как есть. Все метки уходят дальше
в адрес сайта: сайт сохраняет их на воронку и шлёт в Keitaro.
| Метка | Что задаёт | Где |
|---|---|---|
| `indpic` | картинка-пример на преленде | `images/demo/<indpic>/` |
| `theme` | `light` / `dark` | преленд и сайт |
| `degr` | пресет ухудшения превью «До» | сайт (#42) |
| `registration` | `before` — PIN до оплаты, `after` — после | сайт, страница результата |
| `bchoice` | попап выбора банка (`a` — показать) | сайт, оплата (#44) |
| `chb` | галочки согласия на странице оплаты (`no` — не обязательны) | сайт, оплата |
| `tp` | группа тарифов | сайт, cpanel «Группы тарифов (tp)» |
### Загрузка и переход на сайт
1. Браузер проверяет файл: изображение, не больше 6000 px по большей стороне.
2. XHR `POST https://<host>/api/images/public/create` (`multipart/form-data`: `file` и `client_key` — случайный UUID
загрузки, `crypto.randomUUID()`), `Accept: application/json`. Ответ — JSON с id обработки.
3. Браузер переходит на `https://<host>/api/images/public/claim?key=<client_key>&<метки>`. Сайт кладёт загрузку
в сессию посетителя и отвечает 302 на `/public/upscale?id=<обработка>&<метки>` (уже без ключа); страница сама
запускает обработку.
Почему не сразу `/public/upscale`: XHR с преленда кросс-доменный, и сессия, в которой сайт запомнил загрузку,
посетителю на сайте недоступна — браузеры изолируют или не принимают куки третьей стороны. Страница обработки
пускает только владельца загрузки, поэтому без передачи по ключу посетитель видел 404. Ключ одноразовый и годен
30 минут после загрузки.
`<host>` — домен сайта из `config.json`. API сайта принимает загрузку с любого домена (CORS отражает `Origin`), так
что для нового домена преленда на сайте ничего настраивать не нужно.
### Файлы
| Путь | Назначение |
|---|---|
| `index.php` | вход: конфиг, разбор меток, адреса API и перехода, общие переменные, `renderComponent()` |
| `template.php` | разметка страницы: шапка, блок загрузки, примеры, подвал с реквизитами |
| `components/form.php` | форма выбора файла и загрузка (XHR, `client_key`, переход на `claim`) |
| `components/preview.php`, `process.php`, `examples.php` | превью файла, индикатор загрузки, примеры «до/после» |
| `i18n.php`, `messages/{ru,en}/` | заглушка `Yii::t` и переводы; язык сейчас всегда `ru` (определение языка закомментировано) |
| `docs/` | `logo.svg`, `logo-alt.svg` и документы в PDF: тарифы, оферта, соглашение, политика |
| `requisites.html` | реквизиты для подвала (HTML), если в `config.json` нет `requisites`. Без обоих — **заглушка «ИП Иванов Иван Иванович»** из `template.php` |
| `images/`, `fonts/`, `favicon/`, `*.css` | статика; `style.min.css` встраивается в страницу |
| `.htaccess` | только сроки кэширования статики (Apache) |
| `config.json.example` | образец конфига; рабочий `config.json` в git не хранится |
### `config.json`
| Ключ | Обязательный | Что задаёт |
|---|:---:|---|
| `host` | **да** | домен сайта, куда идут загрузка и переход (`zoomzilla.ru`). Без него берётся домен самого преленда — запросы уходят на преленд и получают 404 |
| `utm` | да | метки: `weights` (значение → доля трафика для `rnd`) и `default` |
| `requisites` | нет | реквизиты в подвале — список строк, каждая отдельным абзацем (текст экранируется). Важнее `requisites.html`; без обоих — заглушка «ИП Иванов» |
| `assetsUriPrefix` | нет | откуда брать статику. Пусто — из своего каталога; если на хостинге нет каталога `images`, статика берётся с сайта (`https://<host>`) |
| `fontsUrl`, `faviconUrl`, `spritePath` | нет | адреса шрифтов, иконок и SVG-спрайта, если они не в `assetsUriPrefix` |
| `logoAltExt` | нет | расширение второго логотипа (`svg` по умолчанию) |
| `title` | нет | заголовок страницы («Улучшение изображения») |
| `default_language` | нет | сейчас не действует: язык всегда `ru` |
## Деплой на произвольный домен
**Требования.** PHP 8.0+ (боевой хостинг — 8.2), веб-сервер с `index.php` в роли индексного файла и **HTTPS**:
`crypto.randomUUID()` работает только в защищённом контексте, а сайт отвечает по https.
1. **Домен.** Запись A (или CNAME) на хостинг, сертификат (Let's Encrypt или от хостинга); открыть `https://<домен>/`.
2. **Файлы.** Содержимое репозитория — в корень сайта, без `.git` и служебного (`README.md`, `.gitea/`):
```bash
git clone http://git.dira89kv.beget.tech:3000/anton/preland.git && cd preland
# дальше — загрузка на хостинг: rsync/scp по SSH или FTP-клиент
```
3. **Конфиг и реквизиты.** На сервере, рядом с `index.php`; реквизиты — ключ `requisites` (иначе в подвале заглушка):
```bash
cp config.json.example config.json # host — домен сайта, метки — по кампании
```
`config.json` не в git: при обновлении файлов **не удалять** его (не синхронизировать каталог «зеркалом» без
исключения `config.json`). 2026-10-05 после заливки он пропал, и преленд слал загрузку на собственный домен.
4. **nginx без Apache** (если хостинг не обрабатывает `.htaccess`):
```nginx
server {
server_name <домен>;
root /var/www/<домен>;
index index.php;
location ~ /(\.git|config\.json|README\.md) { deny all; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ~* \.(avif|webp|jpe?g|png|svg|ico|woff2?|css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; }
}
```
5. **Сайт (по желанию).** Домен преленда сайт определяет сам — по `Referer` первого захода. Свои умолчания меток
для этого домена: cpanel → «Метки (UTM)» → «Умолчания по домену прелендинга».
6. **Проверка.**
```bash
curl -s 'https://<домен>/?nc=1' | grep -o 'const createApiUrl = [^;]*;\|const redirectUrlBase = [^;]*;'
# оба адреса — https://<host>/api/images/public/…, а не домен преленда
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'https://<host>/api/images/public/claim?key=x'
# 302 на https://<host>/ — точка передачи на сайте есть
```
В браузере: открыть `https://<домен>/?indpic=1&theme=dark&subid=TEST`, выбрать фото — адрес сменится на
`https://<host>/public/upscale?id=…&indpic=1&theme=dark&subid=TEST…` (без `key=`), пойдёт обработка.
Это настоящая загрузка и обработка на сайте.
## Автодеплой (Gitea Actions)
`.gitea/workflows/deploy.yml`: после каждого коммита в `main` раннер заливает файлы репозитория на хостинги **всех
целей** по FTP с TLS и проверяет страницу каждой. Цель — домен со своим хостингом; список целей — `matrix` в workflow:
```yaml
- name: barbariskatest # имя цели (видно в списке прогонов: deploy barbariskatest)
url: https://preland.anton.barbariskatest.ru # адрес преленда — для проверки после заливки
prefix: PRELAND # секреты PRELAND_FTP_HOST, PRELAND_FTP_USER, PRELAND_FTP_PASS
dir: "" # каталог сайта на FTP: "" — корень FTP (beget), иначе, например, public_html
```
Цели выкатываются независимо (`fail-fast: false`): сбой одного хостинга не останавливает остальные.
**Новый домен на своём хостинге:**
1. Домен и HTTPS — как в «Деплой на произвольный домен», шаг 1.
2. Секреты `<ПРЕФИКС>_FTP_HOST`, `<ПРЕФИКС>_FTP_USER`, `<ПРЕФИКС>_FTP_PASS` (Gitea → настройки репозитория →
Actions → секреты; порт 21, FTP с TLS). В коде реквизитов нет.
3. На хостинге, в каталоге сайта, — `config.json` из `config.json.example`: свой `host`, метки, `requisites`.
В репозитории только образец: `config.json` у каждого домена свой и живёт на его хостинге.
4. Запись в `matrix` — коммит в `main`, автодеплой выкатит все цели. Пока на новом хостинге нет `config.json`,
его проверка падает: загрузка ушла бы на сам преленд.
**Что заливается.** Файлы репозитория (`git ls-files`), кроме служебных (`.gitea/`, `README.md`, `.gitignore`)
и живущих на хостинге `config.json` и `requisites.html`. На хостинге ничего не удаляется: файлы, убранные
из репозитория, удалять вручную.
**Проверка после заливки.** На странице цели должен быть адрес загрузки на сайте, а не на самом преленде (иначе нет
`config.json` с `host`), и переход на `claim`. Иначе прогон этой цели падает.
**Откат** — `git revert` проблемного коммита в `main`: автодеплой зальёт прежнюю версию на все цели.
**SFTP.** Сейчас только FTP с TLS. Хостинг без FTP — поле `protocol` в цели и `lftp` вместо curl в шаге заливки.
**Откат вручную.** Перед заменой файлов — копия изменяемых файлов (`index.php`, `components/*`) с хостинга; вернуть их той же
загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход
по ключу отдаст 404.
+1 -1
View File
@@ -45,7 +45,7 @@ $resultOrig = $getAssetUrl('1/result.avif');
</div> </div>
<div class="image-examples"> <div class="image-examples">
<div class="image-examples-label"><?= Yii::t('app', 'Примеры изображений') ?></div> <div class="image-examples-label"><?= Yii::t('app', 'Примеры улучшения') ?></div>
<div class="image-examples-list"> <div class="image-examples-list">
<?php for ($i = 1; $i <= 5; $i++): <?php for ($i = 1; $i <= 5; $i++):
$thumbMob = $getAssetUrl("{$i}/thumb_mob.avif"); $thumbMob = $getAssetUrl("{$i}/thumb_mob.avif");
+36 -37
View File
@@ -1,14 +1,10 @@
<?php <?php
$maxSide = 6000;
$errorSizeMessage = Yii::t('app', 'Изображение слишком большое ({width}x{height}px). Максимальный размер — {maxSide}px по большей стороне.', [
'maxSide' => $maxSide,
]);
$errorMessage = Yii::t('app', 'Возникла ошибка'); $errorMessage = Yii::t('app', 'Возникла ошибка');
$utmJson = json_encode($UTM ?? [], JSON_UNESCAPED_UNICODE); $utmJson = json_encode($UTM ?? [], JSON_UNESCAPED_UNICODE);
$createApiUrl = ($apiHost ?? '') . '/api/images/public/create'; $createApiUrl = ($apiHost ?? '') . '/api/images/public/create';
$uploadPolicyApiUrl = ($apiHost ?? '') . '/api/images/public/upload-policy';
$redirectUrlBase = $redirect_url_base ?? ''; $redirectUrlBase = $redirect_url_base ?? '';
$redirectUrlUtm = $redirect_url_utm ?? ''; $redirectUrlUtm = $redirect_url_utm ?? '';
@@ -24,11 +20,12 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
<div class="dropzone-form-format"> <div class="dropzone-form-format">
<div class="dropzone-form-format-label"><?= Yii::t('app', 'Поддерживаемые форматы') ?>:</div> <div class="dropzone-form-format-label"><?= Yii::t('app', 'Поддерживаемые форматы') ?>:</div>
<div class="dropzone-form-format-list"> <div class="dropzone-form-format-list">
<div class="dropzone-form-format-item">png</div> <div class="dropzone-form-format-item">JPG</div>
<div class="dropzone-form-format-item">jpeg</div> <div class="dropzone-form-format-item">PNG</div>
<div class="dropzone-form-format-item">jpg</div> <div class="dropzone-form-format-item">HEIC</div>
<div class="dropzone-form-format-item">webp</div> <div class="dropzone-form-format-item">WebP</div>
<div class="dropzone-form-format-item">heic</div> <div class="dropzone-form-format-item">GIF</div>
<div class="dropzone-form-format-item">+много</div>
</div> </div>
</div> </div>
<div class="dropzone-form-action"> <div class="dropzone-form-action">
@@ -41,7 +38,7 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
</div> </div>
</div> </div>
<input type="file" id="dropzone-file-input" name="file" accept="image/jpeg,image/png,image/jpg,image/webp,image/heic,.heic" style="display: none;"> <input type="file" id="dropzone-file-input" name="file" style="display: none;">
</form> </form>
<script> <script>
@@ -66,8 +63,8 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
const redirectUrlBase = <?= json_encode($redirectUrlBase, JSON_UNESCAPED_UNICODE) ?>; const redirectUrlBase = <?= json_encode($redirectUrlBase, JSON_UNESCAPED_UNICODE) ?>;
const redirectUrlUtm = <?= json_encode($redirectUrlUtm, JSON_UNESCAPED_UNICODE) ?>; const redirectUrlUtm = <?= json_encode($redirectUrlUtm, JSON_UNESCAPED_UNICODE) ?>;
const createApiUrl = <?= json_encode($createApiUrl, JSON_UNESCAPED_UNICODE) ?>; const createApiUrl = <?= json_encode($createApiUrl, JSON_UNESCAPED_UNICODE) ?>;
const uploadPolicyApiUrl = <?= json_encode($uploadPolicyApiUrl, JSON_UNESCAPED_UNICODE) ?>;
const errorSizeTpl = <?= json_encode($errorSizeMessage, JSON_UNESCAPED_UNICODE) ?>;
const genericErrorMsg = <?= json_encode($errorMessage, JSON_UNESCAPED_UNICODE) ?>; const genericErrorMsg = <?= json_encode($errorMessage, JSON_UNESCAPED_UNICODE) ?>;
(function initNativeUploader() { (function initNativeUploader() {
@@ -75,6 +72,19 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
const fileInput = document.getElementById('dropzone-file-input'); const fileInput = document.getElementById('dropzone-file-input');
if (!formEl || !fileInput) return; if (!formEl || !fileInput) return;
// `accept` — только удобство picker. Преленд не дублирует Settings:
// разрешённый для наблюдения PDF добавляется неблокирующим запросом к API.
if (uploadPolicyApiUrl && window.fetch) {
fetch(uploadPolicyApiUrl, { credentials: 'omit', cache: 'no-store' })
.then(response => response.ok ? response.json() : null)
.then(policy => {
if (policy?.pdf_observation_enabled) {
fileInput.accept += ',.pdf';
}
})
.catch(() => {});
}
let columnsSwapped = false; let columnsSwapped = false;
formEl.addEventListener('click', (e) => { formEl.addEventListener('click', (e) => {
@@ -126,7 +136,6 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
} }
function handleSelectedFile(file) { function handleSelectedFile(file) {
const maxSide = 6000;
const objectUrl = URL.createObjectURL(file); const objectUrl = URL.createObjectURL(file);
const img = new Image(); const img = new Image();
@@ -134,26 +143,6 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
const width = img.naturalWidth; const width = img.naturalWidth;
const height = img.naturalHeight; const height = img.naturalHeight;
if (width > maxSide || height > maxSide) {
URL.revokeObjectURL(objectUrl);
fileInput.value = '';
const msg = errorSizeTpl
.replace('{width}', width)
.replace('{height}', height);
form.show();
if (typeof preview !== 'undefined' && preview) {
const col7 = document.querySelector('.col-md-7');
if (col7) col7.appendChild(preview.el);
preview.show();
preview.showError(msg);
}
if (typeof process !== 'undefined' && process) process.hide();
if (typeof examples !== 'undefined' && examples) examples.hide();
return;
}
form.hide(); form.hide();
swapColumns(); swapColumns();
@@ -184,11 +173,21 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
}; };
img.onerror = () => { img.onerror = () => {
URL.revokeObjectURL(objectUrl); // Browser не обязан декодировать допустимый контейнер. Не отказываем локально:
if (typeof preview !== 'undefined' && preview) { // серверный Tour gate проверит байты и при необходимости даст JPEG-preview.
preview.show(); form.hide();
preview.showError(genericErrorMsg); swapColumns();
if (typeof process !== 'undefined' && process) {
const col5 = document.querySelector('.col-md-5');
if (col5) col5.appendChild(process.el);
process.setFilename(file.name);
process.setSize(file.size);
process.setDimensions(0, 0);
process.setStage(0);
process.setStatus(0);
process.show();
} }
uploadFile(file);
}; };
img.src = objectUrl; img.src = objectUrl;
+7
View File
@@ -1,6 +1,13 @@
{ {
"host": "zoomzilla.ru", "host": "zoomzilla.ru",
"requisites": [
"ИП Иванов Иван Иванович",
"123123, г. Москва, Пресненская наб., 12",
"ИНН: 123404385406",
"ОГРНИП: 123420000019175"
],
"utm": { "utm": {
"indpic": { "indpic": {
"weights": { "weights": {
+1 -1
View File
@@ -265,7 +265,7 @@ return [
'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.' => 'After setup, you can submit images for processing through the API. Pricing is based on the upscale factor.', 'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.' => 'After setup, you can submit images for processing through the API. Pricing is based on the upscale factor.',
'Привяжите карту' => 'Add a Payment Method', 'Привяжите карту' => 'Add a Payment Method',
'Привязать карту' => 'Add a Payment Method', 'Привязать карту' => 'Add a Payment Method',
'Примеры изображений' => 'Image Examples', 'Примеры улучшения' => 'Enhancement examples',
'Проверьте папку "Спам" и "Корзина".' => 'Check your Spam and Trash folders.', 'Проверьте папку "Спам" и "Корзина".' => 'Check your Spam and Trash folders.',
'Проверяем PIN и выполняем оплату...' => 'Verifying the code and processing your payment...', 'Проверяем PIN и выполняем оплату...' => 'Verifying the code and processing your payment...',
'Проверяем email...' => 'Checking your email...', 'Проверяем email...' => 'Checking your email...',
+1 -1
View File
@@ -275,7 +275,7 @@ return [
'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.' => 'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.', 'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.' => 'После подключения можно передавать изображения на обработку по API. Стоимость считается по масштабу улучшения.',
'Привяжите карту' => 'Привяжите карту', 'Привяжите карту' => 'Привяжите карту',
'Привязать карту' => 'Привязать карту', 'Привязать карту' => 'Привязать карту',
'Примеры изображений' => 'Примеры изображений', 'Примеры улучшения' => 'Примеры улучшения',
'Проверьте папку "Спам" и "Корзина".' => 'Проверьте папку "Спам" и "Корзина".', 'Проверьте папку "Спам" и "Корзина".' => 'Проверьте папку "Спам" и "Корзина".',
'Проверяем PIN и выполняем оплату...' => 'Проверяем PIN и выполняем оплату...', 'Проверяем PIN и выполняем оплату...' => 'Проверяем PIN и выполняем оплату...',
'Проверяем email...' => 'Проверяем email...', 'Проверяем email...' => 'Проверяем email...',
+7 -1
View File
@@ -104,8 +104,14 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="h5 mb-3"><?= Yii::t('app', 'Наши реквизиты') ?></div> <div class="h5 mb-3"><?= Yii::t('app', 'Наши реквизиты') ?></div>
<?php <?php
// реквизиты: config.json (requisites — строки) → requisites.html → образец
$requisites = array_filter((array) ($config['requisites'] ?? []), fn ($line) => is_scalar($line) && trim((string) $line) !== '');
$requisitesFile = __DIR__ . '/requisites.html'; $requisitesFile = __DIR__ . '/requisites.html';
if (file_exists($requisitesFile) && filesize($requisitesFile) > 0) { if ($requisites) {
foreach ($requisites as $line) {
?><p class="mb-0"><?= htmlspecialchars((string) $line, ENT_QUOTES, 'UTF-8') ?></p>
<?php }
} elseif (file_exists($requisitesFile) && filesize($requisitesFile) > 0) {
echo file_get_contents($requisitesFile); echo file_get_contents($requisitesFile);
} else { } else {
?> ?>
+21
View File
@@ -0,0 +1,21 @@
<?php
/**
* Регрессия видимого списка форматов Preland.
* Список не управляет приёмом файла: он только кратко объясняет пользователю
* поддерживаемые типы и не должен разрастаться в несколько строк.
*/
$form = file_get_contents(dirname(__DIR__) . '/components/form.php');
if ($form === false) {
fwrite(STDERR, "FAIL: не удалось прочитать компонент формы Preland.\n");
exit(1);
}
preg_match_all('/<div class="dropzone-form-format-item">([^<]+)<\\/div>/', $form, $formatMatches);
$visibleFormats = ['JPG', 'PNG', 'HEIC', 'WebP', 'GIF', '+много'];
if ($formatMatches[1] !== $visibleFormats) {
fwrite(STDERR, "FAIL: Preland не показывает утверждённый компактный список форматов в нужном порядке.\n");
exit(1);
}
echo "OK: Preland показывает утверждённый компактный список форматов\n";