---
pr: 144
title: "fix(draw-image): optional badge/subtitle + self-healing docker entrypoint"
---

## Что сделано
Три фикса для draw-image (PR#134 follow-up):

1. **Пустой badge не рисует квадрат** — `renderSlotBackground()` перенесён внутрь `if (value)` блока в `buildSvg()` (`.opencode/draw-image/src/render.ts:33-41`). bg/border `<rect>` рендерится ТОЛЬКО если слот заполнен.
2. **Subtitle опционален** — если `args.subtitle` falsy, вся `<text>` строка с `{{subtitle}}` удаляется из шаблона (`render.ts:50-54`). Без subtitle — чистый вывод (только title).
3. **Docker entrypoint (self-healing)** — новый `docker-entrypoint.sh` (корень): проверяет `node_modules/sharp`, при отсутствии `npm ci` (continue-on-error), `exec opencode "$@"`. Dockerfile `ENTRYPOINT` → `/usr/local/bin/docker-entrypoint.sh`. Новый `.dockerignore` (`app_data/`, `.git`, `**/node_modules`).

Тесты:
- vitest: `render.optional.test.ts` (unit: empty badge → no rect, empty subtitle → no text, filled badge → rect, set subtitle → text), `render.optional.integration.test.ts` (PNG valid with/without subtitle+badge), `e2e.optional.test.ts` (CLI title-only → exit 0, clean SVG).
- pytest: `test_omits_subtitle_flag_when_not_provided` в `tests/test_draw_image_tool.py`.
- Все 55 vitest + 441 pytest зелёные, ruff чист.

Документация: project-map README обновлён (docker-entrypoint.sh, .dockerignore, новые тест-файлы).

## Почему
PR#134 выявил три проблемы: (1) cover без badge рисовал пустой `<rect>` с `fill=#121212` + `stroke=#ccff00` — визуальный мусор; (2) cover без subtitle оставлял пустой `<text>` элемент в DOM; (3) fresh clone + `docker compose up` → `Cannot find module 'sharp'` (node_modules gitignored, Dockerfile не устанавливал deps). Большинство cover'ов — title-only, нужен чистый вывод.

## Pending
—

## Watch out
- `docker-entrypoint.sh` использует `#!/bin/sh` (POSIX), не bash — для максимальной портативности в slim-образах. `set -euo pipefail` НЕ используется намеренно: `npm ci` failure не должен рвать entrypoint (continue-on-error pattern).
- `.dockerignore` исключает `**/node_modules` — это означает что `npm ci` в entrypoint ВСЕГДА будет выполняться при первом старте контейнера (node_modules не копируется в образ). Это by design — self-healing.
- `renderSlotBackground` теперь вызывается только для заполненных слотов — если в будущем понадобится bg для пустого слота-плейсхолдера, придется пересмотреть логику.
- Regex `/[^\n]*\{\{subtitle\}\}[^\n]*\n?/g` удаляет всю строку с `{{subtitle}}` — подразумевает что `{{subtitle}}` на отдельной строке в шаблоне (так и есть в cover.svg).