## Design ### Роут в web/app.py После `/bundle/...` (в конце файла) добавить: ```python from fastapi.responses import FileResponse MEDIA_DIRS = [ BASE_DIR / "media", # свежие: media/ BASE_DIR / "media" / "media" # старые: media/media/ ] @app.get("/media/{filename}") def media(request: Request, filename: str): """Отдаёт медиа-файл поста (из media/ или media/media/). Авторизация.""" _require_auth(request) name = os.path.basename(filename) # защита от path traversal if not name: return HTMLResponse("bad filename", status_code=400) for d in MEDIA_DIRS: f = (d / name).resolve() if f.exists() and f.is_file(): # отдаём с корректным MIME по расширению return FileResponse(f) return HTMLResponse("not found", status_code=404) ``` Примечание: `FileResponse` уже есть в fastapi.responses (импортировать). `MEDIA_DIRS` можно вынести в константы рядом с `STATIC_DIR`. ### Шаблоны В `candidates.html` (и `published.html`), заменить блок бейджа: ```html {% if p.media_path %} 🖼 медиа {# маленькое превью: изображение или видео #} {% set media_src = '/media/' ~ p.media_path.split('/')[-1] %} {% if p.media_path.lower().endswith(('.jpg','.jpeg','.png','.gif','.webp')) %} медиа {% elif p.media_path.lower().endswith(('.mp4','.webm','.mov')) %} {% endif %} {% endif %} ``` В `candidates.html` строка 49: заменить бейдж на блок с превью. В `published.html` — аналогично (там сейчас бейдж медиа? проверить). ### Верификация - `openspec validate web-media-preview` — чисто. - Перезапуск веба: `sudo systemctl restart vesti-web`. - Открыть /candidates — у поста с media_path видно изображение/видео. - `/media/LinuxMastery_1079.jpg` — 200 (файл есть). - Старый пост с media/media/ — тоже 200. - Несуществующий файл — 404.