Галочка «Проверить по шаблону» в окне отчёта отправляет текст модели: совпал с шаблоном — остаётся как есть (skipped), не совпал — переписывается в деловом виде (done). Обработка идёт в фоне, HTTP-запрос не ждёт модель, оригинал тьютора сохраняется в text_original. - схема: text_original/text_ai/ai_status/ai_checked_at/ai_error в lesson_reports, таблица lesson_report_versions, ensureLessonReportsTable() - настройки lesson_ai_enabled и lesson_ai_prompt (раздел sec-lesson-ai), значения только 'true'/'false' - worker.js: createLessonReportChecker (FOR UPDATE OF lr SKIP LOCKED, до 3 попыток), хук назовён notifyEvent — notify в createPhotoEnhanceWorker уже занят будильником - server.js: wakeLessonAiWorker, onLessonAiDone (версия, аудит с diff, уведомление lesson.ai.formatted, SSE lesson_report_status), маршруты /versions, /versions/:id/restore и /ai/revert - aiComplete вместо aiCorrectText: общий вызов модели с таймаутом - бэкап/восстановление: lesson_reports и lesson_report_versions в payload - фронтенд: openLessonVersions/restoreLessonVersion в admin.js, бейджи статусов в lessons.js, лейблы аудита, renderAuditPager - docs: раздел 3d в AGENTS.md и Agent Workflow, пункт в README - тесты: контракт lesson-report и настройки уведомления в api.smoketest.js
32 KiB
AGENT.md — Developer Agent Guidelines for WhatIDo
This document defines how AI agents should work with the WhatIDo codebase. Follow these rules strictly.
Обязательное правило: разведку, чтение и анализ репозитория делают субагенты, а не основной агент — контекст основного агента не должен раздуваться простынями кода. См. Agent Workflow.
Project Overview
WhatIDo — Accounting system for an educational center: attendance journal, student project works, group gallery, detached files, and public showcase pages (share links).
- Stack: Node.js 20 + Express, PostgreSQL 16, Redis 7, Docker Compose, S3-совместимое хранилище файлов, Tailscale (Serve/Funnel)
- Architecture: Single Express server (
server.js) + storage abstraction (storage.js) + cache/pub-sub abstraction (redis.js) + static frontend inpublic/ - Deployment: Docker Compose (app + db + s3 + tailscale), bind-mounted uploads, named volumes for Postgres and S3 data
- Auth: сессии в БД.
POST /api/auth/login(bcrypt) → токен в заголовкеX-Auth-Token. Роли:adminи не-admin, ограниченные филиалами (user_branches).ADMIN_PASSWORDиспользуется только для автосоздания первого админа в пустой БД — это не механизм авторизации API
Agent Workflow (обязательные правила работы)
Главное правило: исследование репозитория выполняют субагенты, а не основной агент. Контекст основного агента — самый дорогой ресурс проекта: он живёт дольше одной задачи и должен содержать план, решения и точные точки правки, а не выгрузку файлов. Любой поиск, чтение и анализ файлов, которые пользователь явно не назвал, отдаются субагенту (spawn_agent).
Что делегировать суагенту (обязательно)
- Разведку: «где реализовано X», «какие эндпоинты трогают Y», «кто вызывает Z» —
search_codebase+ точечное чтение. - Чтение крупных файлов:
server.js,worker.js,storage.js,redis.js,public/js/*.js,README.md,AGENTS.mdцеликом. Субагент возвращает релевантные куски, основной агент файл целиком не читает. - Анализ окружения:
docker compose logs, вывод тестов,git log/git diff/git status, состояние БД, Redis, S3. - Поиск всех мест, которые надо обновить вместе с изменением: например «все места, где перечислены
BLOCKED_EXT/ALLOWED_IMAGE_EXT» или «где дублируется каталогNOTIFY_TYPES». - Ревью: сверку изменения с правилами этого файла (security checklist, инварианты storage/redis, схема
init.sql+migration.sql). - Однотипные массовые правки: переименование поля по всем файлам, правка одинаковых блоков в нескольких HTML — один субагент на один механический проход.
Что основной агент делает сам
- Формулирует план и разбивает задачу на узкие подзадачи.
- Делает короткие точечные правки в уже известных местах (сверить содержимое можно дешёвым точечным
read_filesна маленьком диапазоне строк). - Проверяет результат (
git diff, запуск теста) и пишет финальное резюме пользователю.
Как ставить задачу субагенту
- Один субагент — одна узкая подзадача. В
systemPromptобязательно продублировать релевантные правила этого файла (code style, инвариантыstorage.*/redis.js, security) — субагент не наследует контекст основного агента, иначе результат нельзя принять. - Требовать формат ответа:
путь:строка+ короткая выдержка + вывод. Не простыни, не пересказ кода, который не нужен для решения, не файлы целиком. - Независимые разведки запускать параллельно, а не последовательно.
- Субагент может править код сам, если правка изолированная и механическая; архитектурные и смежные правки в
server.js/db/*.sqlделает основной агент. - Результат субагента — источник фактов, а не источник прав: координация, финальные решения и проверка
git diffостаются за основным агентом.
Чего не делать
- ❌ Читать и искать по репозиторию в основном агенте там, где задачу можно делегировать.
- ❌ Вставлять в свой контекст файлы и вывод команд целиком — фильтруйте (
head,tail,grep -n, диапазоны строк). - ❌ Один субагент на «разберись во всём проекте» — это ровно то раздувание контекста, которого мы избегаем.
Development Rules
1. Code Style
- No comments unless explicitly requested
- ES modules not used — CommonJS (
require) throughout - Error handling: try/catch with explicit status codes, no global error handler
- Validation: Inline helper functions (
reqInt,reqStr,optInt, etc.) — use them - Security first: All uploads validated, path traversal blocked, rate limits on public routes
2. Database
- Schema: Defined in
db/init.sql(runs on first container start) - Migrations:
db/migration.sqlfor existing DBs — update both when changing schema - Connection: Single
Poolfrompg,DATABASE_URLfrom env - Queries: Parameterized only (
$1,$2...), never string interpolation - Transactions: Use
client.query('BEGIN')/COMMIT/ROLLBACKfor multi-statement ops
3. File Uploads
- Multer configs:
upload(images only),adminUpload(wider allowed ext),uploadBackup(restore) - Видео в интерфейсе:
mp4/m4v/webm/ogvиграются в модалке#videoModalвjournal.html(data-videoвfilesHTML); остальные видео (mov,mkv,avi, …) остаются обычными ссылками на скачивание. Отдача —GET /api/files/:token?play=1inline сAccept-Ranges; без?play=1файл по-прежнему уходит какattachment, чтобы старые ссылки не поменяли поведение - Limits:
UPLOAD_FILE_LIMIT_MBper file (default 50),UPLOAD_TOTAL_LIMIT_MBper entry (default 200) — both env-driven;UPLOAD_REQUEST_TIMEOUT_MSoverrides the auto-computed request timeout. The frontend reads the two MB values fromGET /api/public-settings(upload_file_limit_mb,upload_total_limit_mb) — do not hardcode them again inpublic/js/index.js - Staging: Multer always writes to
uploads/(timestamp-random.ext); a globalres.on('finish')hook persists each uploaded file throughstorage.persiston successful responses (only whenSTORAGE_DRIVER=s3) - HEIC: Auto-converted to JPEG via
heic-convert - Cleanup:
safeUnlink/sweepOrphanedUploads— never delete outsideuploads/or the configured bucket
3a. Storage (storage.js)
- Drivers:
local(default, files inuploads/) ands3(S3-compatible: SeaweedFS by default, MinIO viadocker-compose.minio.yml) - Keys are stable: DB stores
/uploads/<name>; S3 object keys are the same<name>(plus.originals/<name>). Never change key format — it would break existing DB rows and URLs - API:
put,putFile,head,exists,sizeOf,getStream,getBuffer,getRange,del,copyObject,listAll,localize,persist,streamTo,streamRangeTo,downloadAll,uploadTree,ensureBucket,usage,pruneCache - Диапазоны:
getRange(key, start, end)иstreamRangeTo(res, key, start, end, opts)отдают206сContent-Range/Accept-Ranges— только для медиа, разборRangeна стороне сервера (parseByteRange) - Rules: never call
fs.*onuploads/directly in request/worker code — usestorage.*.safeUnlinkis the only deletion helper (local + remote, idempotent) - Read path:
STORAGE_LOCAL_FALLBACK=1prefers a local file when it still exists (covers in-flight uploads and partial migration); otherwise the app streams the object from S3 - Cache:
.thumbs(WebP miniatures) and.cache(originals localized for sharp/zip) live insideuploads/and are pruned hourly (STORAGE_CACHE_MAX_AGE_HOURS) - Never publish the S3 API port: only
127.0.0.1on the host, file access stays behind app auth/rate limits
3b. Redis (redis.js)
- Единственная точка доступа:
createRedis({ url, prefix })— все операции кэша/счётчиков/pub-sub идут через неё - API:
get,set,del,dropPrefix,dropMatch,clear,wrap,incr,publish,on,rateLimitStore,info,connect,close - Graceful fallback — обязательное требование: при недоступном Redis все операции уходят в in-memory backend с той же семантикой. Приложение обязано стартовать и работать без Redis
- Первое подключение ограничено по времени (
REDIS_CONNECT_TIMEOUT_MS, 5 с): node-redis не отклоняетconnect()при недоступном сервере, а повторяет попытки бесконечно — без таймаута старт приложения зависнет навсегда - Переподключение: node-redis переподключается сам; по событию
readyподписки и subscriber-клиент восстанавливаются (ensureSubscriber). Не пересоздавать subscriber черезdestroy()— это гонка с внутренним teardown node-redis - Ключи:
get/setсами добавляют namespace (REDIS_PREFIX, по умолчаниюwhatido),dropPrefix/dropMatchтоже. ВrateLimitStoreпрефикс добавляется один раз вbase— не применяйтеfullKeyповторно scanDelete: курсорSCANв node-redis v5 обязан быть строкой, числовой0вызоветTypeError. Возвращаемое значение курсора — тоже строка, сравнивайте с'0'resetTimeвrateLimitStore.incrementобязан бытьDate— express-rate-limit v8 вызываетresetTime.getTime()- Пабликация всегда отдаёт подписчикам строку (JSON), независимо от бэкенда — иначе fallback и Redis расходятся по формату
- Инвалидация — по префиксу (
SCAN+DEL), точечного удаления по ключу избегайте - Ключевые пространства:
setting:,groups:,students:,entries:,lessons:,stats:,dashboard:,share:payload:,public-settings,system-info,session:,ban:,fail:,rl: - Сессии:
loadUserByTokenкэширует пользователя на 30 с. Любая мутацияusers/sessions/user_branchesобязана вызыватьinvalidateSessions()или удалятьsession:<token>, иначе деактивированный пользователь сохранит доступ - Секреты: пароль только в
REDIS_URL/REDIS_PASSWORD, порт 6379 публикуется лишь на127.0.0.1
3c. Уведомления (server.js, worker.js, public/)
- Каталог событий — только
NOTIFY_TYPESвserver.js(тип →label,hint,icon,level,enabledпо умолчанию,admin); фронтенд берёт список изGET /api/notifications/meta, дублировать каталог в HTML нельзя - Таблицы:
notifications(событие,admin_only,branch_id) +notification_reads(прочтение на пользователя). Изменения схемы — вdb/init.sqlиdb/migration.sqlи вensureNotificationsTable() - Настройки:
notify_enabled(общий),notify_retention_days(1–365),notify_<тип>(точки типа заменяются на_, см.notifySettingKey). Значения только'true'/'false'—PUT /api/settingsэто валидирует - Создание события — только через
pushNotification()/notifyEntry(); они сами проверяют переключатели и при выключенном типе возвращаютnull.notifyEntryподставляет{student}и{group}и определяет филиал по группе записи - Хук в фото-воркере называется
notifyEvent— имяnotifyвнутриcreatePhotoEnhanceWorkerуже занято будильником воркера (photoWorker.notify()изPOST /api/photo-jobs/wake), объявление функции перекрыло бы параметр - Видимость: админ видит всё; остальные —
admin_only = falseиbranch_id IS NULLили филиал изuser_branches - Доставка: запись в БД →
cache.publish('whatido:notifications', row)→ SSEGET /api/notifications/stream(клиенты фильтруются поnotificationVisible). Redis недоступен — работает in-memory pub/sub - Очистка:
purgeOldNotifications()при старте и раз в час поnotify_retention_days; чтения удаляются каскадом - Новое событие добавляется вместе с: записью в
NOTIFY_TYPES, строкамиINSERT INTO settingsвdb/init.sql+db/migration.sql, вызовомpushNotification/notifyEntryв точке события и паройiconиз Lucide - Аудит: удаление/очистка уведомлений логируется (
notifications.delete,notifications.clear)
3d. Отчёты о занятии и проверка по шаблону (server.js, worker.js, public/)
- Таблицы:
lesson_reports(+text_original,text_ai,ai_status,ai_checked_at,ai_error) иlesson_report_versions(история версий:text,source=manual|ai|restore). Схема — вdb/init.sql,db/migration.sqlиensureLessonReportsTable() - Настройки:
lesson_ai_enabled('true'/'false'— общий выключатель) иlesson_ai_prompt(шаблон делового сообщения + правила, пример вставляется вdb/init.sql,db/migration.sqlи вLESSON_AI_DEFAULT_PROMPTвserver.js). Раздел в UI —sec-lesson-aiнаpublic/settings.html - Флаг из UI: чекбокс
#lessonAiCheckв модалке#lessonModal— включён при создании, выключен при редактировании (resetLessonModalFields/fillLessonModalFromReport). Уходит в теле какai_check - Роут не ждёт модель:
POST/PUT /api/lesson-reportsприai_check: trueсохраняют отчёт как есть и ставятai_status = 'pending', затемwakeLessonAiWorker(). Ответ возвращается сразу — не блокируйте HTTP-запрос вызовом модели - Воркер:
createLessonReportCheckerвworker.jsзабираетpendingчерезFOR UPDATE OF lr SKIP LOCKED, шлёт в модель текст + контекст (группа, дата, время), результат: без изменений →skipped, переписан →done(новый текст вtextиtext_ai), сбой → до 3 попыток, затемerror - Доставка результата:
onDoneвserver.jsпишет версию (saveLessonReportVersion), аудит с diff (lesson_report.ai.format), уведомлениеlesson.ai.formattedи SSElesson_report_statusнаEVENTS_CHANNEL - История версий:
GET /api/lesson-reports/:id/versions, восстановление —POST /api/lesson-reports/:id/versions/:versionId/restore, откат к тексту тьютора —POST /api/lesson-reports/:id/ai/revert. Хранится последниеLESSON_AI_VERSION_LIMITверсий на отчёт - Хуки фронтенда:
openLessonVersions(id)иrestoreLessonVersion(...)живут вpublic/admin.js(модалка доступна с журнала, отчётов и дашборда), список и бейджи статусов — вpublic/js/lessons.js
4. API Patterns
- Middleware:
requireAuth— читаетX-Auth-Token, 401 без валидной активной сессии.requireAdmin— самодостаточный (внутри вызываетrequireAuth, еслиreq.userещё нет), 403 приrole !== 'admin'.optionalAuth— для публичных страниц с персонализацией - Филиалы:
branchScope(user)/branchWhere(user, alias)— для не-adminuser.branch_ids(изuser_branches) ограничивают выборку; уadminids = nullи фильтр не добавляется - Public routes:
apiLimiter(300/15min),entryLimiter(10/15min),fileLimiter(300/15min) — все наcache.rateLimitStore(...), не наMemoryStore - Responses: JSON,
{ error: 'message' }on failure, data directly on success - Pagination:
limit/offsetquery params, return{ items, total }or{ entries, total } - Filters:
group_id,date_from,date_to,student_name,search,deleted
5. Frontend (public/)
- Vanilla HTML/CSS/JS, no build step
- Each page = single HTML file + shared
admin.js/admin.css - API calls via
fetchwithX-Auth-Token(токен изlocalStorage);X-Admin-Tokenбольше не используется и не работает - Share pages (
share.html,links.html) work without auth
6. Docker / Compose
- Dockerfile: Node 22 Alpine, installs deps, generates self-signed TLS cert
- docker-compose.yml: сервисы
db,app,redis,s3(+ опциональноtailscale,cloudflared,text-corrector,photo-ai)db: postgres:16-alpine, healthcheck, init.sql mountedredis: redis:7-alpine,--requirepass, AOF,maxmemory+allkeys-lru, healthcheck, томredis-data, порт только на127.0.0.1app: builds from Dockerfile, exposes 3003/3443, mounts uploadstailscale: host network, NET_ADMIN, runsstart-tailscale.sh(funnel to 127.0.0.1:3443)
- Env vars (required):
ADMIN_PASSWORD,DB_PASSWORD,REDIS_PASSWORD - Env vars (optional):
REDIS_PREFIX(defaultwhatido),REDIS_MAXMEMORY(default256mb),REDIS_CONNECT_TIMEOUT_MS(default5000) - Port 443 on host must be free (tailscale listens directly)
7. Tailscale Publication
- No external IP / port forwarding needed
- Access:
https://whatido.<tailnet>.ts.net(inside tailnet + internet via Funnel) - First run:
docker exec -it whatido-tailscale-1 tailscale up --hostname=whatido→ authorize in browser - Enable Serve/Funnel in Tailscale admin console for the node
- Cert: app generates self-signed cert at build (
certs/cert.pem), mounted into tailscale container
8. Backup / Restore
- Admin UI:
/api/backup(download tar.gz),/api/restore(upload tar.gz) - Scripts:
scripts/backup.sh,scripts/restore.sh(host-level) - Backup format:
data.json(all tables) +uploads/directory - Restore validates all data, resets sequences, sweeps orphans
Common Tasks
Каждый рецепт ниже начинается с субагента-разведки (см. Agent Workflow): пусть он найдёт нужные места и вернёт путь:строка, а правки вносит основной агент.
Add a new API endpoint
- Add route in
server.js(group with related routes) - Use
requireAdminfor admin,apiLimiter/fileLimiterfor public - Validate input with helper functions
- Use parameterized queries, transactions if multi-table
- Call
logAudit(req, 'action.name', { ... })for mutations - Return JSON, handle errors with appropriate status codes
Add a database column/table
- Update
db/init.sql(CREATE TABLE / ALTER TABLE) - Update
db/migration.sql(idempotent ALTERs) - Update
server.jsqueries that SELECT/INSERT the table - Test:
docker compose down && docker compose up -d --build
Add a frontend page
- Create
public/newpage.html(copy structure from existing) - Link in
public/admin.htmlnavigation if admin page - Use
admin.jsutilities:api(),requireAuth(),formatDate(), etc. - No build step — just refresh browser
Modify file upload rules
- Edit
BLOCKED_EXT,ALLOWED_IMAGE_EXT,ADMIN_ALLOWED_EXTconstants - Update Multer
fileFilterfunctions - Keep
MAX_TOTAL_UPLOAD_BYTESand per-file limit in sync
Migrate files to S3 / switch storage driver
docker compose up -d s3docker compose exec -T app node scripts/migrate-to-s3.js --dry-runthen without the flag (idempotent, size-checked, keeps local files)docker compose exec -T app node scripts/migrate-to-s3.js --verify-only- Set
STORAGE_DRIVER=s3in.env,docker compose up -d app - After verification:
docker compose exec -T app node scripts/migrate-to-s3.js --delete-local
- Rollback:
STORAGE_DRIVER=local+docker compose up -d app - Do not run
--delete-localbefore the app serves reads from S3 and the verification passes
Testing & Verification
No automated test suite exists. Verify manually:
# Start stack
docker compose up -d --build
# Check logs
docker compose logs -f app
# Test API (replace LOGIN/PASS; X-Admin-Token больше не работает)
TOKEN=$(curl -s -X POST http://localhost:3003/api/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$LOGIN\",\"password\":\"$PASS\"}" | sed -E 's/.*"token":"([a-f0-9]+)".*/\1/')
curl -H "X-Auth-Token: $TOKEN" http://localhost:3003/api/auth/me
# /api/groups — публичный (optionalAuth), 200 даже без токена:
# для проверки авторизации берите /api/auth/me или /api/users
# Run backup/restore scripts
./scripts/backup.sh
./scripts/restore.sh backups/whatido-backup-<date>.tar.gz
# Storage checks
docker compose up -d s3
docker compose exec -T app node scripts/migrate-to-s3.js --dry-run
docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
# Redis checks
node redis.selftest.js # unit + degradation, needs redis on 127.0.0.1:6379
node api.smoketest.js # e2e, needs running stack
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning INFO
docker compose stop redis && node api.smoketest.js # app must keep working in-memory
docker compose start redis # app reconnects on its own
# Audit diff checks
node diff.selftest.js # unit, no stack needed
diff.js builds the audit payload for text changes: word-level segments
(eq/del/add), per-step stats (added_words, removed_words, chars_before/after) and a
light summarizeChanges/stripDiffs pair for the audit list. Two rules to keep:
the diff payload stored in audit_log.target must stay capped (it is rendered raw in the audit
UI), and GET /api/audit must keep stripping diff while GET /api/audit/:id returns it —
otherwise the list endpoint ships kilobytes of text per row.
Verify Redis state through GET /api/system-info → cache (driver, ready, hits, misses,
fallbackOps, used_memory_human, keys).
GET /api/system-info also returns a stack block (built by getStackInfo(), outside the
response cache so versions and load stay fresh): app (Node, PID, RSS/heap, uptime, version from
package.json + public/version.json), deps (installed versions of the main packages), runtime
(OS from /etc/os-release, kernel, arch, CPU count/model, loadavg, memory, container detection),
database (PostgreSQL version, host, pool counters), cache (driver, version, ready, keys, memory,
hits/misses, fallback ops) and storage (driver, endpoint, bucket). Hosts come from URL.hostname
only — credentials from DATABASE_URL/REDIS_URL must never reach the payload. The «Статус стека»
block on public/settings.html renders exactly this payload.
api.smoketest.js also locks the auth contract: only X-Auth-Token with a session token
authenticates, while X-Admin-Token, Authorization: Bearer and ADMIN_PASSWORD used as a
token must all be rejected with 401. If you change the auth scheme, update this test and the
Auth notes in this file together — a doc that drifts from the code is the failure mode this
guards against.
Security Checklist (before any change)
- No SQL interpolation — only
$1,$2... - Upload path validation via
isSafeUploadPath/safeUnlink - File I/O через
storage.*, ключи объектов не выходят за пределы бакета/uploads/ - Rate limiter on new public routes
- Admin routes behind
requireAdmin - New auth paths checked against the contract in
api.smoketest.js, docs updated in the same change - No secrets in code — only via env vars
- Helmet headers present (already global)
- CORS disabled (no
corsmiddleware)
File Map (key files)
| File | Purpose |
|---|---|
server.js |
Entire backend (Express, routes, DB, uploads, backup) |
storage.js |
Storage abstraction: local and s3 drivers, key normalization, cache/thumb helpers |
redis.js |
Redis abstraction: cache, counters, rate-limit store, pub/sub, in-memory fallback |
redis.selftest.js |
Self-tests for redis.js, including behaviour with Redis unavailable |
diff.js / diff.selftest.js |
Word-level text diff and audit change payload; self-tests |
api.smoketest.js |
End-to-end API smoke test against a running stack |
worker.js |
Background AI auto-check workers: entry messages, lesson-report template check, photo enhance |
db/init.sql |
Initial schema (runs on fresh DB) |
db/migration.sql |
Idempotent migrations for existing DBs |
docker-compose.yml |
Service definitions (app, db, s3, tailscale) |
docker-compose.minio.yml |
Override: S3 service backed by MinIO instead of SeaweedFS |
Dockerfile |
App image build |
public/*.html |
Frontend pages |
public/admin.js |
Shared frontend logic, модалка отчёта о занятии (openLessonModal) |
public/lessons.html |
Отчёты о занятиях: список, фильтры, редактирование |
scripts/backup.sh |
Host-level backup script (DB dump + storage export) |
scripts/restore.sh |
Host-level restore script (DB dump + storage import) |
scripts/storage-sync.js |
Export/import all storage objects (used by backup/restore) |
scripts/migrate-to-s3.js |
One-off/idempotent migration uploads/ -> S3 bucket |
scripts/deploy.sh |
Deploy script (pull master, build image with commit version, restart app) |
start-tailscale.sh |
Tailscale container entrypoint |
.env.example |
Env var template |
Do Not
- ❌ Add dependencies without updating
package.jsonand rebuilding - ❌ Write files outside
uploads/orcerts/ - ❌ Touch
uploads/withfs.*in request/worker code — usestorage.*(files may live only in S3) - ❌ Run
migrate-to-s3.js --delete-localbefore verification and cutover - ❌ Expose the S3 API port publicly (only
127.0.0.1in compose) - ❌ Expose the Redis port publicly (only
127.0.0.1in compose) - ❌ Call
fs.*/pgdirectly for cache, counters or pub/sub — useredis.js - ❌ Make Redis a hard dependency: any new Redis-backed path must keep the in-memory fallback
- ❌
await client.connect()without a timeout — it never rejects while Redis is unreachable - ❌ Cache authorization-relevant data without an invalidation path on the mutation
- ❌ Commit
.env,certs/,uploads/,backups/,node_modules/ - ❌ Expose DB port (5432) outside docker network
- ❌ Use
eval,Functionconstructor, or dynamic code execution - ❌ Add comments to code (this file excepted)
- ❌ Read/search the repo in the main agent when the work can be delegated to a subagent
- ❌ Load whole files (
server.js,worker.js,storage.js,redis.js,public/js/*.js,README.md) or raw command output into the main context — delegate and filter (grep -n,head, line ranges) - ❌ Give a subagent an open-ended "explore the whole project" task, or omit the project rules from its
systemPrompt— it does not inherit the main context - ❌ Treat a subagent report as the final word: coordinate, edit and verify the result yourself (
git diff)
Quick Commands
# Full rebuild
docker compose down && docker compose up -d --build
# Обновление на сервере (pull master + сборка образа с версией коммита + перезапуск app)
./scripts/deploy.sh
# App logs
docker compose logs -f app
# DB shell
docker compose exec db psql -U app -d whereldo
# Redis status and cache keys
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning DBSIZE
docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning KEYS 'whatido:*'
# S3 storage status and migration verification
docker compose up -d s3
docker compose exec -T app node scripts/migrate-to-s3.js --verify-only
docker compose exec -T app node scripts/migrate-to-s3.js --delete-local
# Tailscale status
docker exec -it whatido-tailscale-1 tailscale status
# Manual funnel restart
docker exec whatido-tailscale-1 tailscale funnel --bg --yes https://127.0.0.1:3443