Archive site generations on Yandex Disk and document workflow, agent=codex
This commit is contained in:
@@ -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` и число файлов. Не считать запись в локальную папку подтверждением завершённой облачной синхронизации без проверки клиента Яндекс Диска.
|
||||
@@ -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 -->
|
||||
```
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user