Archive site generations on Yandex Disk and document workflow, agent=codex

This commit is contained in:
german
2026-09-28 17:04:11 +03:00
parent 312f4c8755
commit 741c7935b6
5 changed files with 1484 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
# servaki.online — инструкции для агентов
Исходники и опубликованная сборка находятся в Gitea `german/site-servaki-online`; сайт размещён отдельно на Beget. Перед изменением подтянуть `main`, не включать в Git секреты, резервные копии и локальные зависимости.
После **каждой генерации для сайта** сохранять в Яндекс Диск все исходные медиа и варианты, промпты, оптимизированные файлы и готовую сборку. Использовать новую датированную папку внутри `D:\YandexDisk\servaki.online\лендинги\servaki.online site\`. Запустить `save_generations_yadisk.py` сначала без `--copy` для плана, затем с `--copy`; указать `--raw-dir` текущего генератора, `--target` папки выпуска и при необходимости `--source` рабочей копии. Проверить итоговый `MANIFEST.json` и число файлов. Не считать запись в локальную папку подтверждением завершённой облачной синхронизации без проверки клиента Яндекс Диска.
+2 -1
View File
@@ -10,6 +10,8 @@
Актуальные тарифы: «Старт» — 9 000 ₽/мес., «Бизнес» — 21 000 ₽/мес.,
«Премиум» — 45 000 ₽/мес. Публикация на Beget выполняется отдельно от Git.
Исходные генерации, варианты, промпты и готовый выпуск дополнительно сохранены в папке Яндекс Диска `servaki.online/лендинги/servaki.online site/2026-09-28-landing-generations/`. Контрольные суммы 251 файла — в [архивном манифесте](archive-manifests/2026-09-28-generations.json). Для следующих выпусков действует правило в [AGENTS.md](AGENTS.md).
## Сведения о прежней версии
**Сайт:** https://servaki.online
@@ -38,4 +40,3 @@
<noscript><div><img src="https://mc.yandex.ru/watch/110477494" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
```
+6
View File
@@ -65,3 +65,9 @@ python verify.py
- `deploy_beget.py reprice` меняет только главную: перед загрузкой проверяет, что шесть изменений касаются исключительно трёх видимых цен и трёх цен в JSON-LD. Предыдущая главная сохраняется локально в `release-backups/2026-09-28-before-tripled-prices/` и на Beget вне публичного каталога в `backup-2026-09-28-before-tripled-prices/`.
- `.htaccess`, DNS, почта и другие сайты не менялись. Исходный репозиторий сайта — `german/site-servaki-online` в Gitea; в `servaki-monitor` есть чужие незакоммиченные изменения, они не затрагивались. Публикация выполнялась напрямую на существующий Beget-хостинг, без автоматического деплоя из Gitea.
- Для отката вернуть из резервной копии прежний `index.html`; у нового релиза были добавлены `robots.txt`, `sitemap.xml` и каталоги `approach`, `services`, `industries`, `assets/editorial`. Удалять общие каталоги `img` и другие сайты нельзя.
## Хранение генераций на Яндекс Диске
По указанию владельца все результаты генерации для сайта сохраняются также в папке Яндекс Диска соответствующего выпуска: исходные изображения и варианты, промпты и манифесты, оптимизированные медиа и готовая сборка. Для выпуска 28.09.2026 архив находится в `D:\YandexDisk\servaki.online\лендинги\servaki.online site\2026-09-28-landing-generations\`: 251 файл, 173 297 271 байт. `MANIFEST.json` содержит SHA-256 каждого файла и связи исходных генераций с сохранёнными оригиналами; все файлы после копирования сверены повторно.
Для следующих выпусков запустить `save_generations_yadisk.py` сначала в режиме плана, затем с `--copy`, указав `--raw-dir` папки текущего генератора и новый `--target` внутри `servaki.online site`. Скрипт не заменяет существующий файл с отличающейся контрольной суммой. Копирование в локальную папку Яндекс Диска проверено; статус облачной синхронизации следует проверять отдельно в клиенте Яндекс Диска.
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
"""Archive every site image generation, prompt and published output to Yandex Disk.
Run with --plan first, then --copy. Existing files are verified, never replaced.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import uuid
ROOT = Path(__file__).resolve().parent
YANDEX_SITE = Path(r'D:\YandexDisk\servaki.online\лендинги\servaki.online site')
GROUPS = ('artwork', 'assets', 'content', 'public')
MEDIA_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.mp4', '.webm'}
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def planned_files(source: Path, raw: Path):
files = []
for dirname in GROUPS:
directory = source / dirname
if not directory.is_dir():
raise RuntimeError(f'Missing source directory: {directory}')
files.extend((path, path.relative_to(source)) for path in directory.rglob('*')
if path.is_file() and not path.is_symlink())
if not raw.is_dir():
raise RuntimeError(f'Missing image-generation directory: {raw}')
files.extend((path, Path('imagegen-raw') / path.name) for path in raw.iterdir()
if path.is_file() and not path.is_symlink()
and path.suffix.lower() in MEDIA_EXTENSIONS)
files.append((source / 'README.md', Path('SITE-BUILD.md')))
if len({relative.as_posix() for _, relative in files}) != len(files):
raise RuntimeError('Duplicate archive destination')
return sorted(files, key=lambda item: item[1].as_posix())
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--copy', action='store_true', help='copy and verify files')
parser.add_argument('--source', type=Path, default=ROOT)
parser.add_argument('--raw-dir', type=Path, required=True,
help='directory containing raw generated media for this release')
parser.add_argument('--target', type=Path, required=True,
help='new release folder under the Yandex Disk site directory')
args = parser.parse_args()
base = YANDEX_SITE.resolve()
target = args.target.resolve()
if not base.is_dir() or not target.is_relative_to(base) or target == base:
raise RuntimeError('Target must be a subfolder of the existing Yandex Disk site folder')
files = planned_files(args.source.resolve(), args.raw_dir.resolve())
total = sum(source.stat().st_size for source, _ in files)
counts = {group: sum(relative.parts[0] == group for _, relative in files)
for group in (*GROUPS, 'imagegen-raw')}
print(json.dumps({'target': str(target), 'files': len(files), 'bytes': total,
'groups': counts, 'mode': 'copy' if args.copy else 'plan'}, ensure_ascii=False))
if not args.copy:
return
target.mkdir(parents=True, exist_ok=True)
entries = []
artwork_hashes = {}
for source, relative in files:
expected = sha256(source)
if relative.parts[0] == 'artwork':
artwork_hashes.setdefault(expected, []).append(relative.as_posix())
destination = target / relative
destination.parent.mkdir(parents=True, exist_ok=True)
if destination.exists():
if sha256(destination) != expected:
raise RuntimeError(f'Archive conflict: {destination}')
else:
temporary = destination.with_name(destination.name + '.part-' + uuid.uuid4().hex[:8])
try:
shutil.copy2(source, temporary)
if sha256(temporary) != expected:
raise RuntimeError(f'Copy checksum mismatch: {relative}')
os.replace(temporary, destination)
finally:
if temporary.exists():
temporary.unlink()
entries.append({'path': relative.as_posix(), 'bytes': source.stat().st_size,
'sha256': expected})
raw_matches = {entry['path']: artwork_hashes.get(entry['sha256'], [])
for entry in entries if entry['path'].startswith('imagegen-raw/')}
manifest = {'site': 'servaki.online', 'release_date': '2026-09-28',
'files': entries, 'raw_matches_in_artwork': raw_matches}
destination = target / 'MANIFEST.json'
temporary = target / ('MANIFEST.json.part-' + uuid.uuid4().hex[:8])
temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
os.replace(temporary, destination)
for entry in entries:
if sha256(target / entry['path']) != entry['sha256']:
raise RuntimeError(f'Final checksum mismatch: {entry["path"]}')
print(json.dumps({'verified_files': len(entries), 'verified_bytes': total,
'raw_matched_to_artwork': sum(bool(paths) for paths in raw_matches.values())},
ensure_ascii=False))
if __name__ == '__main__':
main()