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
8 changed files with 130 additions and 44 deletions

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@ name: deploy-preland
# После коммита в main — заливка преленда на хостинги всех целей (FTP с TLS) и проверка страницы каждой цели. # После коммита в main — заливка преленда на хостинги всех целей (FTP с TLS) и проверка страницы каждой цели.
# Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория <prefix>_FTP_HOST, # Цель — домен со своим хостингом (список — matrix ниже): реквизиты — секреты репозитория <prefix>_FTP_HOST,
# <prefix>_FTP_USER, <prefix>_FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает. # <prefix>_FTP_USER, <prefix>_FTP_PASS (порт 21). Цели независимы: сбой одного хостинга остальные не останавливает.
# Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, .gitignore) и тех, что живут # Заливаются файлы репозитория (git ls-files), кроме служебных (.gitea/, README.md, AGENTS.md, CLAUDE.md, .gitignore) и тех, что живут
# только на хостинге: config.json (в git его нет) и requisites.html (реквизиты заполняют на месте — пустой файл # только на хостинге: config.json (в git его нет) и requisites.html (реквизиты заполняют на месте — пустой файл
# из git их бы затёр). На хостинге ничего не удаляется: удалённое из репозитория убирать вручную. # из git их бы затёр). На хостинге ничего не удаляется: удалённое из репозитория убирать вручную.
@@ -58,7 +58,7 @@ jobs:
netrc=$(mktemp); list=$(mktemp) netrc=$(mktemp); list=$(mktemp)
trap 'rm -f "$netrc" "$list"' EXIT trap 'rm -f "$netrc" "$list"' EXIT
printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc" printf 'machine %s login %s password %s\n' "$FTP_HOST" "$FTP_USER" "$FTP_PASS" > "$netrc"
git ls-files | grep -vE '^(\.gitea/|README\.md$|\.gitignore$|config\.json$|requisites\.html$)' > files.txt git ls-files | grep -vE '^(\.gitea/|README\.md$|AGENTS\.md$|CLAUDE\.md$|\.gitignore$|config\.json$|requisites\.html$)' > files.txt
# имя уходит в адрес ftp:// как есть — пробелы и прочее экранирование не поддерживаем # имя уходит в адрес ftp:// как есть — пробелы и прочее экранирование не поддерживаем
if grep -q '[^A-Za-z0-9._/-]' files.txt; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' files.txt; exit 1; fi if grep -q '[^A-Za-z0-9._/-]' files.txt; then echo "имена с недопустимыми символами:"; grep '[^A-Za-z0-9._/-]' files.txt; exit 1; fi
# один curl на все файлы — одно соединение FTP+TLS вместо соединения на файл # один curl на все файлы — одно соединение FTP+TLS вместо соединения на файл
+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 и не дают разрешения на публикацию или выкат.
+2 -2
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");
@@ -310,4 +310,4 @@ $resultOrig = $getAssetUrl('1/result.avif');
} else { } else {
initStrictImagePipeline(); initStrictImagePipeline();
} }
</script> </script>
+37 -38
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;
@@ -265,4 +264,4 @@ $redirectUrlUtm = $redirect_url_utm ?? '';
fileInput.value = ''; fileInput.value = '';
} }
})(); })();
</script> </script>
+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...',
+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";