# ADR-061: Optional badge/subtitle + self-healing docker entrypoint (PR#144)

## Статус
Accepted (2026-07-29)

## Контекст
PR#134 (ADR-060) ввёл SVG template renderer для on-brand covers. При тестовом рендере выявлены три проблемы:

1. **Пустой badge рисует квадрат** — `renderSlotBackground()` вызывался для каждого слота с bg-спекой ДО проверки контента. Cover без `--slots badge=...` получал пустой `<rect fill="#121212" stroke="#ccff00">` в районе (780, 780).
2. **Subtitle оставляет пустой `<text>`** — `{{subtitle}}` заменялся на пустую строку безусловно, но `<text>` элемент оставался в DOM. Большинство cover'ов — title-only.
3. **Fresh clone = broken** — `node_modules/` (sharp, lucide-static) gitignored, Dockerfile не устанавливал deps для draw-image. После `docker compose up` → `Cannot find module 'sharp'`.

## Решение
1. **Slot bg conditional** — `renderSlotBackground()` перенесён внутрь `if (value)` блока в `buildSvg()`. bg/border рендерится ТОЛЬКО если слот заполнен контентом.
2. **Subtitle conditional** — если `args.subtitle` falsy, regex `/[^\n]*\{\{subtitle\}\}[^\n]*\n?/g` удаляет всю `<text>` строку с `{{subtitle}}` из шаблона ДО подстановки. При наличии subtitle — обычная подстановка `escapeXml(args.subtitle)`.
3. **Self-healing entrypoint** — `docker-entrypoint.sh` (POSIX `#!/bin/sh`) проверяет `node_modules/sharp`, при отсутствии запускает `npm ci --prefix` (continue-on-error: WARN в логах, контейнер стартует), затем `exec opencode "$@"`. Dockerfile `ENTRYPOINT` → entrypoint shim. `.dockerignore` исключает `app_data/`, `.git`, `**/node_modules` из build context.

## Альтернативы
- **Install draw-image deps в Dockerfile (RUN npm ci)** — отвергнуто: node_modules gitignored, COPY не сработает; `npm ci` в build-time требует COPY package.json + package-lock.json из `.opencode/draw-image/`, усложняет Dockerfile. Self-healing entrypoint — одноразовая задержка при первом старте, потом skip.
- **Pre-build hook (postinstall sync-lucide в Dockerfile)** — уже работает через entrypoint: `npm ci` запускает postinstall (`sync-lucide.mjs`), восстанавливает 2007 иконок.
- **Удалить `<text>` subtitle через DOM-парсер (xmldom)** — overkill для одной строки. Regex по строке — достаточно, шаблон контролируемый.
- **`set -euo pipefail` в entrypoint** — отвергнуто: `npm ci` failure (нет сети) должен быть continue-on-error, иначе контейнер не стартует. POSIX `#!/bin/sh` для портативности в slim-образах.