Документация
CONTEXT.md — шпаргалка
Читать первым перед сессией. Навигация + правила + грабли. Статус —
STATE.md. ТЗ —ARCHITECTURE.md. История —CHANGES.md.
0. Два проекта
| Проект | Где | Роль | Git |
|---|---|---|---|
web/ |
D:\content_shop\web\ → VPS |
Сайт-витрина: клиент, корзина, оплата, выдача | ✅ свой репо (43 коммита) |
znymkari-data/ |
D:\content_shop\znymkari-data\ → ноут |
Лаборатория: импорт, sanitize, превью, push | ✅ свой репо (создан 2026-10-15-б) |
Общее между ними:
- Модели catalog (Event, Block, Category, Performance, Media, Organizer, TeamMember, ProgramScan, Expense, ExtraIncome).
- import_service.py, media_metadata.py, sanitize.py, generate_video_posters.
- Одна схема БД на обоих. Транспорт — JSON (django.core.serializers) + ORM-merge, не pg_dump.
- Organizer / TeamMember — справочники живут только на VPS. На ноуте не заполняются и не показываются в админке.
Что НЕ берём в znymkari-data: cart, favorites, telegram, issues, orders, клиентские views, templates сайта, static сайта.
1. Что где лежит
Локально (D:\content_shop\)
D:\content_shop\ ← локальный корень (БЕЗ git)
├── web\ ← сайт (git repo #1)
│ ├── .git\ ← история (43 коммита, после реструктуризации)
│ ├── .gitignore
│ ├── apps\
│ │ ├── accounts\ ← UserProfile, TelegramLinkToken
│ │ ├── catalog\ ← Event, Media, Performance, ...
│ │ ├── issues\ ← трекер замечаний
│ │ ├── orders\ ← пусто, Этап 3
│ │ └── telegram\ ← бот, webhook, 5 языков
│ ├── config\ ← settings, urls, wsgi, asgi
│ │ ├── settings.py ← общие
│ │ ├── settings_local.py ← dev (не в git)
│ │ └── settings_local_prod.py ← шаблон прод
│ ├── docs\ ← доки (этот файл и др.)
│ ├── media\ ← Znymkari logo, QR
│ ├── static\
│ │ ├── css\common.css, css\lightbox.css
│ │ ├── js\common.js, favorites.js, cart.js, lightbox.js, merge-prompt.js
│ │ └── favicon.ico
│ ├── manage.py, db.sqlite3, requirements.txt
│ ├── start_all.bat ← локальный сервер + бот + туннель
│ ├── start_shell.bat ← Django shell
│ └── test.bat ← тесты
├── deploy.bat ← деплой сайта на VPS
├── .gitignore ← старый корневой, ни к чему не привязан
├── desktop.ini ← мусор Windows
└── znymkari-data\ ← цех (git repo #2, создан 2026-10-15-б)
Мусор в корне и web/
- ✅ Удалены 2026-10-15:
content_shop.db,docs_upload.tar.gz,web_upload.tar.gz,fix_docs.py,find_cyr.py,__pycache__(146 шт.), всеdesktop.iniвweb/. - ✅ Удалены 2026-10-16: старый мир (
cli/,domain/,scripts/,services/,storage/), корневой.gitignore,start_shell.bat.
Документация (D:\content_shop\web\docs\)
CONTEXT.md— этот файл.STATE.md— статус.ARCHITECTURE.md— ТЗ.CHANGES.md— журнал (последние 3 сессии).PRODUCTION.md— чек-лист до прода.SETUP_HOME.md— ноут Олега.SETUP_VPS.md— покупка VPS + домена.COMMANDS.md— шпаргалка по командам.
DOCS_ROOT = BASE_DIR / "docs" в settings.py. Читалка: /catalog/docs/<slug>/, @staff_member_required.
Management-команды
В web/apps/catalog/management/commands/ (VPS):
- ✅ receive_event <slug> — мягкий merge JSON в базу + превью в /srv/previews/.
- ✅ assign_media_authors <event_id> — эвристика автора по ролям команды.
В znymkari-data/apps/importer/management/commands/ (ноут):
- ✅ import_media <event_id> <festival_name> — импорт фестиваля.
- ✅ generate_video_posters <event_id> [--force] — добор постеров.
- ✅ pack_event <event_id> — JSON + tar превью + meta.json → dist/.
- ✅ push_event <event_id> — paramiko SFTP + запуск receive_event по SSH.
Шаблоны сайта (web/apps/catalog/templates/catalog/)
home.html,event.html,cart.html,favorites.html.events_list.html,prices.html,markup.html,cameras.html.dev_links.html,docs.html.includes/:footer.html,sitemap.html,header_actions.html,buy_modal.html.
Статика (web/static/)
CSS: common.css (фон, футер, .page-top, app-shell), lightbox.css.
JS — порядок подключения:
common.js → lightbox.js → favorites.js → cart.js → merge-prompt.js.
favorites.js перед cart.js — иначе у анонима cart.js не найдёт FavoritesModule.
Модули:
- common.js — боке, софиты, back-link, sitemap, BuyModal.
- lightbox.js — window.Lightbox.
- cart.js — window.CartModule.
- favorites.js — window.FavoritesModule.
- merge-prompt.js — модалка «До входа вы положили N медиа…».
Бэкенд (web/apps/)
catalog/:
- models.py — Event, Block, Category, Performance, Media, Organizer, TeamMember, ProgramScan, Expense, ExtraIncome, Favorite, CartItem, UserEventDiscount.
- admin.py, views.py, urls.py, api.py, client_api.py, apps.py, tests.py.
- services/media_metadata.py — локальная копия.
- migrations/ — 12 штук.
- management/commands/ — receive_event, assign_media_authors.
accounts/:
- models.py — UserProfile, TelegramLinkToken.
- views.py, views_telegram.py, urls.py, forms.py, tokens.py, signals.py, admin.py, tests.py.
- migrations/ — 4 штуки.
- templates/accounts/profile.html.
issues/: models.py (Issue), admin.py, tests/, migrations/.
telegram/:
- api.py (Bot API wrapper), services.py (токены), messages.py (5 языков), keyboards.py, preview_photo.py, bot.py, views.py (webhook).
- handlers/ — по файлу на команду.
- management/commands/ — run_telegram_bot, set_telegram_webhook.
- models.py, admin.py, apps.py.
orders/ — пусто, Этап 3.
Конфиг (web/config/)
settings.py— общие настройки.INSTALLED_APPS: admin, auth, contenttypes, sessions, messages, staticfiles, catalog, accounts, issues, telegram, orders.PROJECT_VERSION = "0.15".urls.py— корневые роуты +/telegram/webhook/<secret>/.context_processors.py—version_infoдля футера.settings_local.py— dev (Postgres или SQLite).settings_local_prod.py— шаблон прод-конфига. НЕ в git. На VPS —settings_local.py= содержимоеsettings_local_prod.py.
Одна схема — две конфигурации:
- VPS: Postgres znymkari_db, юзер znymkari_user, DEBUG=False, ALLOWED_HOSTS=['znymkari.com', 'www.znymkari.com'].
- Ноут: Postgres znymkari_local, юзер znymkari_local_user, DEBUG=False, ALLOWED_HOSTS=['*'] (LAN).
2. URL'ы, которые легко перепутать
| URL | Что |
|---|---|
/ |
Главная |
/event/<id>/ |
Клиентская страница ивента (без /catalog/) |
/catalog/cart/ |
Корзина. Только залогиненным. Анонима редиректит на /catalog/favorites/ |
/catalog/favorites/ |
Избранное. Для всех |
/catalog/prices/ |
Прайс |
/catalog/events/ |
Список ивентов |
/catalog/event/<id>/markup/ |
Разметка ивента (staff) |
/catalog/event/<id>/cameras/ |
Часы камер (staff) |
/catalog/docs/<slug>/ |
Читалка доков (staff). 8 табов |
/accounts/signup/, /accounts/login/, /accounts/logout/ |
Авторизация |
/accounts/profile/ |
Личный кабинет |
/accounts/update-profile/ |
Обновление (POST JSON) |
/telegram/webhook/<secret>/ |
Webhook Telegram-бота |
/admin/ |
Django-админка |
/admin/issues/issue/ |
Замечания |
URL'ы в znymkari-data:
| URL | Что |
|---|---|
/ |
Домашняя: кнопки Импорт · Отправка · Хранилище · Чистка · Настройки |
/import/ |
Импорт: таблица папок и ивентов |
/send/ |
Отправка архивов на VPS |
/cleanup/ |
Чистка мусора (сироты, пустые ивенты, логи) |
/storage/ |
Инвентарь хранилища |
/settings/ |
Пути, VPS, ключи |
/admin/ |
Django-админка |
3. Правила работы
- Всегда указывать имя файла и путь в правках.
- Если пользователь пишет «стоп» — правки из последнего сообщения НЕ применены, кроме тех, что он отметит.
- Если пользователь пишет «весь файл» — давать правки целым файлом.
- Правки только «Найти → Заменить» — конкретный кусок. Не описывать прозой.
- После каждой правки — подсказать, как проверить результат.
- Периодически предлагать покрыть тестами.
- Если файла не видел или не помнишь — попросить заново, не гадать.
- Не переделывать работающее без обсуждения.
- Многофайловые правки — разделять на «Файл 1 / Файл 2 / …».
- Внутри
<script>и<style>— никаких{% %}и{{ }}в комментариях. Django парсит шаблон целиком. - Многострочный комментарий в Django-шаблоне — только блок подряд идущих однострочных
{# ... #}. Ни{# ... \n ... #}, ни{% comment %}. -
Деплой — двумя путями.
- VPS: правки кода — только локально, потом
deploy.bat. На VPS руками код не трогаем (единственное исключение —settings_local.py). Перед деплоем — три стоп-крана: 1. Тестыpython manage.py test -v 2→ 77 OK. 2.tar -tzf web_upload.tar.gz | findstr settings_local→ пусто. 3. Изменённый файл присутствует в архиве. - Ноут Олега: отдельный деплой. Через SFTP ключом
id_ed25519(пользовательdeploy,193.176.249.24:22022).settings_local.py— свой, не из git.
Импорт: полностью локальный на ноуте. VPS-БД не трогается до момента
push_event/receive_event. - VPS: правки кода — только локально, потом
-
Формат сессий в
CHANGES.md— только даты.2026-10-14,2026-10-14-б,2026-10-14-в. НикакихС21,С22. В остальных доках — без номеров сессий вообще. -
Стоп-кран
tar— перед каждым деплоем проверять, чтоsettings_local.pyне попал в архив. Это единственный файл, который живёт на сервере и НЕ должен перезаписываться.
4. Грабли — по темам
Деплой VPS
tarбез--excludeзатирает боевойsettings_local.py. Dev-файл локально перезаписывает VPS-версию →DEBUG=True,ALLOWED_HOSTS=[], SQLite вместо Postgres. Симптом:DisallowedHost at /(жёлтая страница). Восстановление:cp settings_local_prod.py settings_local.py- дописать security-блок +
TELEGRAM_WEBHOOK_SECRET. Фикс: всегда--exclude="web/config/settings_local.py"+ проверкаtar -tzf web_upload.tar.gz | findstr settings_local→ пусто. tarна Windows → Linux — «Ignoring unknown extended header keyword 'SCHILY.fflags'». Предупреждение, не ошибка. Лечится флагом--warning=no-unknown-keyword.- Windows
sshзависает в батниках — если в конце скрипта есть вывод (особенно| headилиstatus). Лечится флагами-T,< NULили перенаправлением2>&1. - Hetzner IPv6 не работает для
requests. HTTP-запрос висит 35 сек → ReadTimeout, аcurlотвечает мгновенно. Фикс:/etc/gai.conf→precedence ::ffff:0:0/96 100. tarбез-C parentсхлопывает структуру. Правильно:tar -czf web_upload.tar.gz -C D:\content_shop web.ufw enableна Ubuntu 24.04 — крашитсяUnicodeDecodeError. Обход:sudo ufw --force enable.createsuperuserчерез SSH из Windows — тот жеUnicodeDecodeError. Обход:DJANGO_SUPERUSER_USERNAME=... DJANGO_SUPERUSER_PASSWORD=... --noinput.- 502 Bad Gateway —
www-dataне проходит по/home/deploy(750). Фикс:usermod -aG deploy www-data,chmod 750,systemctl restart nginx. - Sudo NOPASSWD:
/etc/sudoers.d/deployсdeploy ALL=(ALL) NOPASSWD: ALL, права440, проверкаvisudo -c.
Django-шаблоны
{# ... #}работает ТОЛЬКО в одну строку. Многострочный — Django выведет как текст.{% url %}/{{ }}/{% if %}внутри<script>и<style>— парсятся Django. Даже в JS-комментариях. Правило 10 — внутри<script>избегать{% %}вообще.
База данных / поиск
- SQLite
icontainsрегистронезависим ТОЛЬКО для ASCII. Кириллица — учитывает регистр.search_eventsфильтрует в Python черезstr.lower(). likes_count—PositiveIntegerField, дефолт 0. Порог ∞ = 100.- Postgres 15+ требует
GRANT ALL ON SCHEMA public TO <user>— иначе Django не создаст таблицы.
Модели каталога — реальность (не то, что в старой доке)
Category.block— FK наBlockесть.null=True,on_delete=SET_NULL.Mediaимеет иstorage_path(отSTORAGE_ROOT), иrelative_path(папка от корня ивента).storage_path— то, что используется для доступа к файлу.Event.external_id/slug— есть в коде (с 2026-10-15-б).Media.width/height— нет. Только план.Media.media_type—photo/video/edited_video.Event.old_data_deleted— булев, медиа удалены по сроку.Event.award_on_main_stage— награда в подарок при полном пакете.
Клиентские модули (JS)
window.CartModule— корзина. Аноним →localStorage('znymkari:cart'), залогинен → сервер.window.FavoritesModule— избранное. То же.localStorage('znymkari:favorites').- События:
cart:change,favorites:changeнаwindow. - Счётчик
count === -1— маркер «перечитай сервер». - Закладка-ленточка — показывается, если медиа в избранном. Красная — аноним, зелёная — залогинен.
window.CURRENT_USER— inline-скрипт ДО подключения модулей. Иначе модуль стартует как аноним.
Общий лайтбокс
window.Lightbox.open({items, index, showLike, note, buttons, onLike, onClose}).- Кнопки: массив или функция от item. CSS-классы
lb-btn-danger/lb-btn-success. - Правило: если действие убирает фото из контекста — лайтбокс закрывается.
CSS / вёрстка
- App-shell:
body.app-shell+.app-scroll. .page-top— fixed-слой.pointer-events: noneна контейнер,autoна детей.- Grid + скролл:
overflow: auto+align-content: stretchтянет ряды. Лечитсяgrid-auto-rows: max-content; align-items: start; align-content: start. visibility: hiddenна FontAwesome-иконке — схлопывается. Использоватьopacity: 0.
Главная (home.html)
- Не выходить из IIFE при пустой БД.
if (events.length === 0) return;убиваетinitAuth→ZnymkariAuthModal=undefined→ «Войти» не работает. ФлагhasEvents, а неreturn.
Telegram
- Webhook, не long polling.
/telegram/webhook/<secret>/. Long polling выключен (run_telegram_bot.service→disabled). - Backoff после ~15 минут ошибок webhook. Telegram перестаёт слать новые апдейты.
Лечение:
set_telegram_webhook --delete→set_telegram_webhook. _send_surpriseблокирует воркер. Если/srv/previewsпуст, висит 35 сек. Фикс:threading.Thread(daemon=True).allowed_updates: ['message', 'callback_query'].- Emoji-флага Crimean Tatar не существует. Используем 🟦.
window.openдля Telegram — вызывать синхронно (доawait fetch).- Токен и
TELEGRAM_WEBHOOK_SECRET— вsettings_local.py, в git не попадают.
ffmpeg / видео
-anвырезает звук. Для сохранения:-c:a aac -b:a 96k -ac 2.- Автоплей со звуком заблокирован —
muted = true, поplaying— снять. -ssдо-i— быстро, но чёрный кадр.-ssпосле — точнее, медленнее.- Постер — из 70% длины. На коротких (≤12 сек) — с 1-й секунды.
Soft-delete / реактивация
delete_account_view= толькоis_active=False.username,email,telegram_idостаются.- Telegram-вход реактивирует. Telegram верифицирует личность.
IntegrityErrorнаtg_<id>. ЕслиUserProfile.telegram_idпотерян — fallback поusername.- Email soft-delete — пока не лечим (SMTP не настроен). Техдолг.
Django settings
ALLOWED_HOSTS—.domain.com(с точкой), не*.domain.com.DOCS_ROOT = BASE_DIR / "docs".SECRET_KEY,TELEGRAM_SHARED_SECRET,TELEGRAM_WEBHOOK_SECRET— только вsettings_local.py.SECRET_KEY—openssl rand -base64 48.TELEGRAM_WEBHOOK_SECRET—openssl rand -hex 32.requirements.txt— не включатьfolium,googlemaps,numpy,python-dotenv.MAILERS— Django 6.1 использует новую настройку вместоEMAIL_BACKEND. Пока console backend.apps/catalog/services/media_metadata.py— локальная копия из старого мира (перенесена 2026-10-16). Больше ничего из старого мира не тянется.
Dev
- Два
runserverна порт 8000 = глюки.taskkill /F /IM python.exe.
Ноут Олега
- Пользователи —
olegarh(uid 1000),deploy(uid 1001, sudo NOPASSWD). - Имя хоста —
oleg-home. - ОС — Ubuntu Server 24.04 LTS, без GUI. Ядро 7.0.0-38.
- Сеть — Wi-Fi
wlp8s0, LAN192.168.31.87(резерв в роутере сделан). Ethernetenp9s0сломан. - Внешний SSH —
193.176.249.24:22022→192.168.31.87:22. - SSH-ключ —
id_ed25519, залит вauthorized_keysдляolegarhиdeploy. - Крышка —
HandleLidSwitch=ignore. Sleep/suspend/hibernate замаскированы. - Ноут = мини-UPS. Батарея 2–4 ч.
- Postgres локальный — 18,
znymkari_local, юзерznymkari_local_user, порт5432,uk_UA.UTF-8. - Python — 3.14. nginx — 1.28. ffmpeg — 8.0.1.
- Django на ноуте — планируется
/home/deploy/znymkari-data/, venv рядом,0.0.0.0:8000, systemdgunicorn-oleg.service. - Доступ к сайту ноута —
http://192.168.31.87:8000/. - WinSCP — локально
192.168.31.87:22, не22022. NAT loopback не работает. - HDD — NTFS,
/mnt/hdd, FUSEntfs-3g.chown/chmodигнорируются. - STORAGE —
/mnt/hdd/znymkari-storage. Рабочая копия. - FESTIVALS —
/mnt/hdd/zny-festivals. Оригиналы, источник импорта. - HEAP —
/mnt/hdd/zny-heap. Свалка, вне импорта. - Симлинки —
/home/olegarh/HDD → /mnt/hdd,/home/olegarh/STORE → /mnt/hdd/znymkari-storage. deployне видит/home/olegarh/без sudo — это нормально.- NAT loopback на Xiaomi — не работает. Изнутри только
192.168.31.87:22, снаружи193.176.249.24:22022.
Samba (ноут Олега)
- Шары:
zny-festivals(источник импорта) +zny-heap(свалка, вне импорта). Обе на/mnt/hdd. Рабочая копияznymkari-storageне шарится. - Юзер:
olegarh. Пароль: отдельный SMB (smbpasswd -a), не системный. - Порт 445, только LAN
192.168.31.0/24. Наружу не пробрасывать. - Правило Windows: перед выдёргиванием USB HDD — «Безопасное извлечение». Иначе NTFS «грязный».
- Fast Startup Windows — выключить. Иначе NTFS на HDD не «закрывается» корректно.
ufwне включён на ноуте. Правило для 445 добавлено. Техдолг.System error 1219приnet use— Windows не любит две SMB-сессии к одному хосту. Решение:net use * /delete /y→ перезагрузка → через Проводник.$RECYCLE.BIN,System Volume Information— не трогать.found.000/001/003— хлам отchkdsk, если появятся — не трогать.
Unicode-кавычки в именах папок
- В именах встречаются скрытые символы (
'U+2019, вариантый).lsпоказывает имя,mvс копипастой — не находит. - Решение:
Tab-автодополнение в bash, или bash-итерация по ФС (for item in *), где имена берутся из самой ФС.
Импорт медиа
- Импорт полностью локальный. VPS-БД не трогается до
push_event. - Копирование, не перемещение. Оригинал в
zyn-festivals/<название>/остаётся как бэкап. Олег сам решает вручную, когда удалять. import_service(вznymkari-data) копирует изFESTIVALS_ROOTвSTORAGE_ROOT/<slug>/с проверкой размера. Копия вweb/удалена 2026-10-16.media_metadata.py— локальная копия вapps/importer/(ноут) и вapps/catalog/services/(web). Старого мира больше нет.sanitize.py— есть вznymkari-data/apps/importer/sanitize.py(с 2026-10-15-б).- Slug ивента —
{id:04d}_{sanitized_title}. Уникально навсегда. Media.relative_path— путь внутри ивента. Абсолютных путей в БД нет.- Идемпотентность merge — по
Event.external_id(Event),(event, title)(Block / Category),(event, storage_path)(Media). Проверено на живом цикле. - Хозяин поля — правило merge: при создании ставим всё из JSON, при
обновлении — только белый список. Лайки, покупки, заморозка, разметка,
автор, прайс,
organizer,staff_on_site— не трогаются никогда. _apply_qr_watermark—quality=100, не 85. Оптимизирован: накладывается в памяти, без повторного открытия файла.shutil.copy2на NTFS — вызывает дикие тормоза (пытается сохранить метаданные Windows). Используемshutil.copyfile.- Видео-постеры — сначала генерируется 10-сек превью, а постер вынимается из него (
ffmpeg -i preview.mp4), а не из гигабайтного оригинала. Экономит половину времени. - Broadcom BCM4352 — не работает из коробки. Лечится
broadcom-sta-dkms. После обновления ядра проверять пересборкуwl. - NTFS-3G vs NTFS3 —
findmnt --verifyругается. Техдолг →ntfs3.
Хранилище
- Единое хранилище на HDD:
/mnt/hdd/znymkari-storage/. - Оригиналы —
/mnt/hdd/zny-festivals/(источник импорта). - Раскладка:
<STORAGE_ROOT>/<id>_<slug>/<relative_path>. - HOT/COLD отменены 2026-10-16.
VPS / Hetzner
- Hetzner CPX22 — €19.49/мес + бэкапы €3.90 + IPv4 €0.50 = €23.89/мес.
- Командный доступ: Hetzner → Security → Members → роль Member.
- SSH-ключ:
ssh-keygen -t ed25519 -C "znymkari". - Имя сервера:
znymkari. IP:128.140.81.213. Location: Falkenstein. - Домен:
znymkari.com(в аккаунте Олегаkonsolch). - DNS:
A @иA www→128.140.81.213. - Пользователь:
deploy, sudo NOPASSWD через/etc/sudoers.d/deploy. - Проект:
/home/deploy/content_shop, venv —/home/deploy/content_shop/venv. - gunicorn: systemd
gunicorn-znymkari.service, сокетgunicorn.sock. - nginx:
/etc/nginx/sites-available/znymkari. - Postgres: база
znymkari_db, юзерznymkari_user. - SSL:
certbot --nginx. Автообновлениеcertbot.timer.
5. Стиль общения
- Пользователь — русскоязычный, общается кратко.
- Обращение на «ты».
- Примеры кода — с русскими комментариями.
- Не перегружать. Мелкая правка — мелко.
- Код — в тройных бэктиках. Если внутри бэктики — четверные.
- Инструкции — строго исчерпывающие, в одном сообщении (без «смотри выше»). Указывать Где, Что набрать, Что ожидать.
- Правки кода (сайт/шаблоны) — всегда: ЧТО НАЙТИ → ЧТО ЗАМЕНИТЬ.
- Правки конфигов (CLI) — всегда: ВМЕСТО ... → ЗАМЕНИ НА .... Не начинать со слов «выполни на ноуте» — сначала сказать, как туда попасть. Никаких паст из 10 команд без запроса.
- Правки — только парами «Найти блок → Заменить на».
6. Что не сделано
См. STATE.md — не дублируем.