-
Both are pluggable seams (dev-plan-4.md §1.2 items 1-2), same shape as T20's sendVerificationEmail: a fixed env var gates a real vendor call that isn't implemented yet (none has been picked), and the no-vendor path is what actually runs today. describeGarmentImage() — mode 2 has no admin around to hand-write garments.appearanceDescriptor the way admin_seed garments do, so it needs *something* non-empty to fill that field; today that's a fixed placeholder string, swapped for a real vision-model call once a vendor's picked. moderateGarmentImage() — the requirements doc flags image-content moderation as vendor-TBD but never actually lists it as its own task; this fills that gap. Every call writes a moderation_log row (new dressup-store.js export, M.9 schema) regardless of which branch runs, so the audit trail exists before a real vendor does. Default-permissive when unconfigured — T2's upload endpoint already enforces the real trust-boundary checks (magic-byte type check, 8MB cap, EXIF-stripped re-encode); this only covers the *content* check, which nothing here can perform without a vendor. Neither is unit-tested standalone (server.js doesn't export internal functions, matching sendVerificationEmail's existing precedent) — T27 integration-tests both through the actual upload endpoint. Also adds pricing.js to the tmpdir file-copy list in every test that spawns server.js in isolation — server.js now requires it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, and a debug GET /api/auth/me (T19's own acceptance criteria calls for it explicitly). Passwords hashed with node:crypto's scryptSync (salt stored alongside the hash, verified with timingSafeEqual) — still zero new dependencies. Sessions live in a separate user_session cookie from admin_session (T6) — the two are unrelated systems, confirmed by a test that a user session can't unlock /api/admin/*. Login and "email not registered" return the identical 401 body so the endpoint can't be used to enumerate registered emails. Every test file that spawns server.js in an isolated tmpdir now also copies db.js alongside it (dance-channel/dressup-generate/dressup-crud/ admin-pages-flow/uploads-route .test.js) — server.js requires db.js unconditionally now, so those child processes were crashing silently without it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
h3ConsoleFetch blindly JSON.parses the response body regardless of status code; a momentary non-JSON 500 from H3 Console during polling (GET /v1/videos/:id) rejected the promise, which the poll loop let propagate straight to the outer catch — permanently marking the job "failed" even though the job was still fine on H3's side. Hit this for real today: a 20-job batch's concurrent polling briefly 500'd H3 Console's HTTP layer, and all 20 jobs got marked failed locally while every one of them was still queued/running on H3 (confirmed via direct GET /v1/videos/:id against the real console). Now a failed poll attempt is swallowed and retried on the next tick, bounded by the same wall-clock deadline as everything else (12aff60). Only a genuine terminal status (done/error/failed/cancelled) or deleted_at ends the job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
H3's 4 GPUs run one sharded model (H3-CONSOLE.md /v1/status), not 4 independent workers, so a batch of N jobs is served ~serially. The poll loop's old fixed 200-iteration budget (~1000s at the 5s default interval) didn't account for queue depth: submitting a 9-job batch hit this live and 9/11 jobs got marked "generation timed out" locally while still sitting healthy in H3's queue (queue_position confirmed via GET /v1/videos/:id). Also treats H3's "cancelled" status as terminal, matching "error"/"failed". Recovered the wrongly-failed jobs by resetting them to "generating" in generation-jobs.json (their h3JobId was already set, so resumeDressupGenerations() picked them back up without resubmitting to H3) rather than resubmitting and burning the queue further. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
ingest.js's syncLibrary() only carries a library entry forward as-is when edited is true; otherwise its periodic auto-sync (every 5 min) rebuilds the record from the raw H3 job and silently drops channel/characterId/garmentId/refDanceVideoId. That broke the M.7 dedupe cache and would have let dance clips leak into the regular live-channel pool once channel got wiped. Caught while running a real batch generation: the second sync cycle stripped a clip's traceability fields within 5 minutes of it landing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
Fetches characters/garments/dance-refs alongside the library on load and joins each clip's characterId/garmentId/refDanceVideoId back to their names, so an operator can see "谁的脸 + 谁的衣服 + 哪条参考视频" per row (M.7). No composedImageId column — the one-step pipeline never produces an intermediate composed image. Extends the dressup-generate.test.js happy-path test to confirm the join resolves real names, not just matching ids. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
POST /api/admin/batch-gen takes {characterId, garmentIds[], refDanceVideoIds?[]}, expands to one job per garment (cross-producted with refDanceVideoIds when given, else one random active dance-ref per garment like the single-generate default), and submits each through the same submitGeneration() helper the single POST /api/admin/generate route now uses — so the M.7 dedupe cache (existing characterId+ garmentId combo) is honored per combo, not just per batch. GET /api/admin/batch-gen/status?batchId= returns that batch's generationJobs records for progress polling. admin-batch-gen.html: pick one character, multiple garments, and optionally pin specific reference dances; submits the batch, polls status every 3s into a progress table, and lets failed rows be retried individually via POST /api/admin/generate. Linked from the other admin pages' nav. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
generateDanceVideo() takes a queued generationJobs record through prompt build -> forward character/garment/dance-ref uploads to H3 Console's /v1/assets -> POST /v1/videos -> poll -> push a traced clip into library.json (characterId/garmentId/refDanceVideoId). Mirrors generateForPrompt's submit/poll/persist shape but keyed off the new generationJobs store so a restart resumes any in-flight job by its h3JobId, same as resumeGenerations() does for the live queue. POST /api/admin/generate accepts characterId/garmentId (+ optional refDanceVideoId, else a random active dance-ref is picked) and skips straight to the cached clip if that character x garment combo was already generated (M.7 dedupe). GET /api/admin/generate/:id polls job status. Tests spin up a mock H3 Console over HTTP so the full submit/poll/land path runs offline. Also fixes two existing test fixtures (dressup-crud, admin-pages-flow) that spawn server.js without copying prompt-template.js — worked until T8 added the first require() of it into server.js, at which point the spawned server silently failed to boot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>