• New blacklist store (dressup-store.js) — explicitly distinct from T32's
    automated face-blacklist model and T26's garment moderation, per M.11's
    own warning. isBlacklisted(targetType, targetId) is checked at every
    submission entry point (/api/dance/pick, both paid upload endpoints —
    rejected before any spend, matching M.11's "不进流水线、不扣积分") and
    filters GET /api/dance/wardrobe and pickNextDanceClip's random-fill
    pool (extending T36's expired-character filter to also drop
    blacklisted garments/characters/generated_videos) — records are never
    deleted, just excluded from future selection, same "记录保留" spirit as
    T36.
    
    Admin CRUD: GET/POST /api/admin/blacklist, DELETE
    /api/admin/blacklist/:id — creation attributed via adminActorUserId
    (T30), same null-over-the-legacy-ADMIN_PASSWORD-channel behavior as
    credit/adjust.
    
    Playback management (需求文档 6.5 节): POST
    /api/admin/dance/schedule/pin inserts an existing clip into the very
    next slot (factored T34's priorityInbox-insert logic into a shared
    insertClipNext() so both call sites move in lockstep); POST
    .../schedule/skip advances past whatever's currently playing; POST
    /api/admin/dance/pause freezes only the random-fill loop (paid_boost
    content still drains immediately even while paused — 需求文档 6.5 节
    frames "暂停自动填充" as filler-specific, not a full stop).
    
    dance-admin-ops.test.js covers blacklist CRUD/attribution, entry-point
    rejection for both a blacklisted garment (pick) and a blacklisted
    user (upload), pin/skip changing the schedule, and pause/resume
    against a deliberately drained-to-stable-size schedule (comparing exact
    lengths — comparing clip ids/count directly was flaky since a small
    mock pool can legitimately re-pick the same clip after a skip).
    
    This completes dev-plan-5.md (T31-T39) — 需求文档 9 节's fifth and final
    planned phase.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • 需求文档 7.4 节's stated floor: an expired user_paid character (T36)
    or its videos must stay individually retrievable by their owner —
    "下线=不再出现在公开直播间和其他人的衣柜/随机池里",不是"下线=消失".
    
    GET /api/dance/my-works (login required, no emailVerified gate — this
    reads your own past purchases, doesn't spend anything) returns the
    requesting user's own characters/garments (ownerUserId match) each
    joined with their generated videos from library.json. New standalone
    page my-works.html (matches live.css's visual language) renders two
    sections with inline video players (same preload="none" pattern as
    admin.html), a status badge (播放中/已下线 for characters, 衣柜中/已下架
    for garments), and a login gate for logged-out visitors — reusing
    GET /api/dance/my-works's 401 rather than duplicating dance.html's auth
    modal in a second page. Linked from dance.html's header once logged in.
    
    my-works.test.js: 401 without a session; a real user who uploaded one
    garment (T27) and one 1-segment character (T35, deliberately expired
    by test time) sees both with their videos still attached; a second,
    unrelated user sees neither.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Same shape as T28's 传衣服 panel: file input, live price pulled from
    GET /api/pricing (今 spend_character_per_segment × segmentsPerUnit,
    not hardcoded), a garment picker (随机搭配 default, or any wardrobe
    item, 需求文档 3.3 第 4 点), a segments-to-buy input (1-20, matches the
    server-side clamp), not-logged-in opens the existing login modal
    instead of failing silently, insufficient balance points at the topup
    flow. Success message is visually distinct ("boosted", yellow box,
    🎉 已插队播放") from mode 1/2's plain "生成中" — T37's explicit
    acceptance criterion that a paid_boost submission must be recognizable
    as skipping the vote queue, not just another queue entry.
    
    Also fixes a real bug this surfaced via a live smoke test (not just
    new to T37 — T28's submitGarmentUpload had the identical bug): both
    upload handlers set the success/status message and *then* called
    `await refreshAuthState()`, which re-renders the whole panel
    (creditBalance changed -> renderAuthButton -> renderWardrobe) and wipes
    whatever was just set. Both now set the message after refreshAuthState()
    resolves, re-querying the fresh DOM it produced.
    
    Seeded the live pricing.json with spend_character_per_segment (it only
    existed in pricing.js's DEFAULTS, which don't retroactively apply to
    an already-existing file — the same class of gap as the isDefault
    character fix two commits ago).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • consumeCharacterSegment(clip), called alongside markPlayed() at both
    dance-specific scheduling call sites (T34's priorityInbox insert and
    the normal fill loop) — deliberately not folded into the shared
    markPlayed() itself, since live.html's text-channel clips never have a
    user_paid character behind them and architecture.md 2.1 keeps the two
    channels' scheduling logic independent. Decrements
    characters.remainingSegments each time a user_paid character's clip
    gets scheduled (its first play via T34's priority insert, and every
    later replay through the normal random-fill pool); hits 0 ->
    status:"expired".
    
    pickNextDanceClip's random-fill pool now excludes clips belonging to
    an expired character (需求 3.3 第 7 点: excluded from future selection,
    but the character record and its generated videos are never deleted —
    GET /api/admin/characters and GET /api/library still return them).
    
    dance-mode3.test.js: buying exactly 1 segment expires the character
    right after its single (priority-boosted) play; totalSegments stays 1
    as history while remainingSegments goes to 0; the character and its
    clip remain queryable via the admin/library APIs afterward.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • T34: priorityInbox (M.8) — paid_boost clips get inserted immediately
    after whatever's currently playing (danceSchedule[0]), pushing every
    later entry's timing back, rather than waiting for the fill-loop's
    normal "top up to 30s ahead" trigger. That distinction mattered:
    draining priorityInbox only from inside the fill loop (first attempt)
    technically satisfied "never loses to a vote-ranked pick" but could
    leave a ready paid_boost clip waiting a long time if the schedule
    already had ~30s queued — dance-mode3.test.js's race test caught this
    (timed out) before the eager-insert fix. ensureDanceSchedule's existing
    "generating -> done" reconciliation loop diverts a job into
    priorityInbox instead of danceQueue's "ready" state when
    job.priority === "paid_boost", spliced out of danceQueue after the
    loop (not mid-iteration).
    
    T35: POST /api/dance/upload-character (需登录+emailVerified, same rule
    as T27). Face photo -> T2 upload+EXIF strip -> T32 moderateFaceBlacklist
    -> T25-style describeCharacterImage (new pluggable fn, shares
    VISION_API_KEY) -> characters.create(source:"user_paid", isDefault:
    false, totalSegments/remainingSegments computed from pricing_config's
    new spend_character_per_segment×segmentsPerUnit) -> creditLedger.spend
    (relatedAssetId=character.id, rolled back on failure) ->
    submitGeneration(..., priority:"paid_boost") -> lands in priorityInbox
    (cache hit) or danceQueue pending T34's reconciliation (fresh
    generation). Garment is optional (need求文档 3.3 第 4 点): given
    garmentId is validated active-or-400, otherwise a random active
    garment is picked — a fully empty wardrobe is a 400 before any spend.
    
    dance-mode3.test.js covers both: auth/balance/no-active-garments gates,
    the happy path (cost scales with requested segments, character record
    shape, clip carries priority/requestedByUserId), and T34's actual
    guarantee — a paid_boost character is scheduled at or ahead of an
    already-ready free pick for the same garment, distinguished by
    characterId since both target the same garment on purpose.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • T32: moderateFaceBlacklist(imagePath, assetId) — same pluggable seam
    shape as T26, for mode 3's uploaded face photos (需求文档 5 节: filtering
    politically-sensitive likenesses). Default-permissive (auto_pass) when
    unconfigured, per explicit user confirmation this session — deliberately
    not assumed, since a wrong-default here (a real face silently passed
    through) has a different risk profile than T26's garment photos.
    
    T33: forwardToFalAssets (uploads.js) + fal's queue API (falSubmit/
    falPollStatus/falFetchResult) wired into submitGeneration/
    generateDanceVideo, which now dispatches on job.backend between the
    existing H3 Console path (renamed generateDanceVideoViaH3, unchanged
    behavior) and the new generateDanceVideoViaFal. A paid request (userId
    set) routes to fal once FAL_API_KEY is configured; everything else —
    free mode-一, or paid but no key yet — stays on H3 Console exactly as
    before (verified: full suite green, no FAL_API_KEY set in any existing
    test's env).
    
    Endpoints/params (storage/upload/initiate, queue.fal.run/{model}/
    requests/{id}/status vs /requests/{id} as two separate calls,
    Authorization: Key <FAL_API_KEY>) are taken from fal's published docs
    and the fal-js client's own source, not guessed — fal-generation.test.js
    proves this codebase's side of the integration against a mock fal
    server. What remains genuinely unverified (no FAL_API_KEY exists) is
    whether the H3-Console-tuned prompt is interpreted the same way by
    fal's minimax/h3/reference-to-video model, and fal's exact
    terminal-failure status string(s) — flagged in code comments rather
    than guessed at.
    
    Also fixes a real gap T27 shipped with: submitGeneration never received
    userId, so every mode-2 clip's requestedByUserId (M.7) was silently
    null. T27's call site now passes userId (priority stays "normal" —
    only mode 3 skips voting), which also makes mode 2 fal-eligible.
    dance-mode2.test.js's happy-path test now asserts this.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • The schema field existed since T1 but nothing read or wrote it —
    admin-characters.html now shows a 片段配额 column (remainingSegments/
    totalSegments, "-" for admin_seed/unlimited characters) so an operator
    can see mode 3's (T35) purchased-segment quota once it exists. No
    server.js change needed: PATCH already allows arbitrary field updates
    (mountCrud's isPatch branch), this is purely making the existing
    plumbing visible plus a round-trip test proving it actually holds the
    values mode 3 will write.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Breaks down dressup-dance-room.md 9节第5条 (paid character upload,
    face-blacklist moderation, character lifecycle/quota, priority
    scheduling for paid_boost content, fal.ai real-time compute, "我的
    作品" page, playback management) into T31-T39 with acceptance
    criteria, estimates, and a dependency graph, matching dev-plan.md
    through dev-plan-4.md's format.
    
    Flags one thing worth being upfront about: T27 (mode 2, completed in
    the prior phase) never actually integrated fal.ai despite dev-plan-4.md's
    own T27 row calling for it — it still runs entirely through H3 Console.
    No FAL_API_KEY has ever been configured, so there was no credential to
    build or test a real fal.ai path against; T27 shipped a fully working
    H3 Console path instead, same call as T24 did with Stripe. T33 folds
    paying off that debt into this phase (mode 3 needs the same fal.ai
    integration anyway, so building it twice would be wasted work), with
    the same "build the plumbing, real key unlocks real verification"
    shape as T24.
    
    Also asks the user a real question rather than defaulting silently:
    T26's "no vendor configured -> auto-pass" precedent doesn't transfer
    to face-blacklist review of real uploaded faces the way it did for
    garment photos — the risk profile is different, so T32's default
    policy when unconfigured is left for the user to decide rather than
    assumed.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • T28 replaces the "开发中敬请期待" placeholder on dance.html's 传衣服
    tab with a real upload panel: file input, the live spend_garment price
    pulled from the new public GET /api/pricing (not hardcoded, per
    dev-plan-4.md's acceptance criterion), and a submit flow that POSTs to
    T27's /api/dance/upload-garment. Not logged in opens the existing T22
    login modal instead of failing silently; not email-verified shows an
    inline message; a 402 (insufficient balance) points at the new topup
    flow; success refreshes the balance and immediately polls the queue,
    same as the free wardrobe pick's own pollState() call.
    
    T29 adds a 充值 pill (visible once logged in) opening a tier picker
    built from GET /api/pricing's topup_tier entries, POSTing to T24's
    /api/credits/checkout and navigating to whatever URL comes back (the
    mock completion page today, Stripe's hosted Checkout once a real key
    lands tomorrow — both redirect back to dance.html?topup=success/
    cancelled). A one-shot banner reads that query param on load, then
    scrubs it via history.replaceState. The account button's own label
    now also shows creditBalance, riding on the field GET /api/auth/me
    already started returning in T24.
    
    Manually verified end to end against the live server (localhost:8934):
    register -> verify -> mock checkout -> balance updates in the header
    -> topup banner -> upload panel shows the live price and opens the
    login modal when logged out. The upload itself surfaced that
    production's one seed character had never been marked isDefault (see
    the preceding commit) — not a bug in this code, but blocked a live
    walk of the full generate path until fixed.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Live data gap, not a code bug: the one character in the production
    characters.json had never been marked isDefault, which both mode 1
    (POST /api/dance/pick) and the just-added mode 2 (POST
    /api/dance/upload-garment, T27) require ("no default character
    configured" otherwise) — found while smoke-testing T28's upload panel
    against the live server. User confirmed setting it.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Requires login + emailVerified (M.10). Order matters and each step
    undoes the previous one on failure: moderate (T26) before anything
    else exists, so a rejected image leaves nothing behind; create the
    garments row (source: user_paid) before spending, so the ledger entry
    can carry relatedAssetId — a failed spend deletes it back out;
    generation submission failure refunds and deletes it too. Reuses T8/
    T9's submitGeneration whole, same M.7 dedupe cache, then rides the
    same danceQueue/ensureDanceSchedule machinery /api/dance/pick (mode 1)
    already uses — a paid entry only differs by carrying userId/
    amountPaid, which mode 1's free entries don't set.
    
    Async generation failure (job fails after this handler already
    responded) is refunded from ensureDanceSchedule's existing "generating
    -> failed" tick, gated on a findByRelatedAssetAndReason lookup so a
    crash between that write and saveDanceState() persisting it can't
    double-refund — a duplicate here is real money, not a cosmetic bug.
    
    dance-mode2.test.js covers the full chain against a mock H3 Console:
    login/verification/balance gates, a non-image upload rejected before
    anything is spent, the happy path (spend -> auto-tag -> generate ->
    lands in dance schedule, garments row has user_paid/ownerUserId/
    appearanceDescriptor), and — via a second server pointed at a mock H3
    that always fails submission — the async refund path, redelivered
    twice to prove it's exactly-once.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • createCheckoutSession() is the seam (dev-plan-4.md §1.2 item 4): with
    no STRIPE_SECRET_KEY set — no real test-mode key exists yet, the user
    said "先做一个 mock,我明天再给你真实测试的 key" — POST
    /api/credits/checkout returns a same-origin /api/credits/mock-checkout
    URL that completes the topup itself, standing in for "redirected to
    Stripe, paid, redirected back" so the whole checkout -> credited loop
    (and T29's UI on top of it) can be built and tested today. With a key
    set, it creates a real Stripe Checkout Session via plain REST (fetch,
    no stripe SDK — this project has no dependencies at all) — untested
    against a live account until the key lands.
    
    POST /api/credits/webhook hand-rolls Stripe's documented HMAC-SHA256
    signature scheme and refuses anything unsigned or unverifiable,
    including every request while STRIPE_WEBHOOK_SECRET is unset (not just
    badly-signed ones). Both the mock and real paths route through one
    completeTopup() -> creditLedger.topup() (T23) so Stripe's guaranteed
    at-least-once webhook delivery can't double-credit — verified in
    credits.test.js by redelivering the same event id twice.
    
    Also added creditBalance to GET /api/auth/me's response (T29's need)
    instead of a dedicated balance endpoint — it's the same "logged-in
    user's own state" this route already serves, and live.html/dance.html
    already poll it after login.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • /api/admin/* accepted only the T6 ADMIN_PASSWORD cookie until now —
    MVCP M.1/M.10's stated rule is users.role='admin' + a real session, and
    M.2's credit_ledger needs a real admin identity to attribute manual
    balance corrections to. isAdminAuthed() now accepts either path (the
    ADMIN_PASSWORD channel stays as a fallback so nobody's locked out
    before any role='admin' account exists by hand — dev-plan-4.md T30's
    explicit requirement). New POST /api/admin/credit/adjust wraps the
    new creditLedger.adjust() (db.js), which — unlike spend/refund — takes
    either sign of delta and records actorUserId (null over the legacy
    ADMIN_PASSWORD channel, which has no per-admin identity to attribute
    to).
    
    credit_ledger's actorUserId column needed an idempotent ALTER TABLE
    guard (no migration framework exists anywhere in this project) since
    the table's CREATE TABLE IF NOT EXISTS already shipped in T23 without
    it. Also switched db.js to WAL journal mode — surfaced by two processes
    legitimately touching the same data.sqlite at once in this session
    (the long-running server + a test run) hitting "database is locked" on
    the default rollback-journal mode; worth having before this bites in
    production too, not just to unblock local test runs.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • 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
    yibo authored
     
    Browse Code »
  • credit_ledger (dev-plan-4.md T23; schema M.2) joins users/sessions in
    the same SQLite db.js — architecture.md 1.4 named this as the other
    half of why SQLite got picked in T18 ("扣积分 + 创建生成任务 + 扣减片段
    配额" needs real atomicity, T18 only used the credential-storage half).
    spend()/refund()/topup() each read-modify-write users.creditBalance
    inside a transaction; spend() refuses to take a balance negative
    (INSUFFICIENT_BALANCE) instead of allowing debt, and topup() dedupes
    by paymentRef so a Stripe webhook's guaranteed at-least-once delivery
    can't double-credit (T24 will rely on this).
    
    pricing_config (M.12) stays a JSON file via a new pricing.js — it's
    operator-edited config, not on the atomic spend path, so SQLite would
    be over-engineering here; keyed by `key` rather than an id, hence its
    own tiny module instead of store.js's createStore. Seeded with
    placeholder credit amounts (dev-plan-4.md §1.2 item 3 — real numbers
    are still TBD).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Replaces the single "← 返回直播频道" link (which only pointed at
    live.html) with two explicit links — 视频直播间 (live.html) and
    跳舞直播间 (dance.html) — each opening in a new tab so the admin
    doesn't lose their place in the library table.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • preload="none" left every row's player a plain black box until
    clicked, giving no way to eyeball which clip is which without playing
    it. Switch to preload="metadata" (browsers paint the first frame once
    metadata loads, without playing) so the row itself doubles as a
    thumbnail. Load it lazily via IntersectionObserver rather than
    src'ing all 361 rows on page load — /lib/video/:id has no local cache
    and re-proxies to H3 Console on every hit, so firing that many
    requests at once would hammer it; only rows actually scrolled into
    view pull their frame.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Breaks down dressup-dance-room.md 9节第4条 (credit ledger, Stripe
    topup, mode 2 paid garment upload — explicitly not mode 3, which is
    period 5) into T23-T30 with acceptance criteria, estimates, and a
    dependency graph, matching dev-plan.md/dev-plan-2.md/dev-plan-3.md's
    format.
    
    Key decisions:
    - credit_ledger joins users/sessions in the same SQLite db.js (T18) —
      architecture.md 1.4 flagged this atomicity requirement explicitly,
      this is the other half of why SQLite got picked in period 3.
      pricing_config stays a JSON file (store.js) since it's not on that
      hot atomic path.
    - Flags a real doc inconsistency: architecture.md's diagram labels
      fal.ai integration "period 5", but dressup-dance-room.md §8 item 10
      and §9 item 4 both say mode 2's fal.ai path belongs in period 4.
      Resolved in favor of the more specific/recent requirements doc.
    - Flags an uncovered gap: §5 references an image-moderation vendor
      "TBD, see §8" but §8 has no matching item for it — added as an open
      question here (§1.2).
    - Three genuinely external blockers (image moderation vendor, vision
      model vendor for auto-tagging, real Stripe API keys) are each
      designed around with a pluggable interface (same pattern as T20's
      sendVerificationEmail) so building isn't blocked on vendor picks —
      except the Stripe key itself, which only the user can supply.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • The modal (previous commit) meant an extra click and a popup just to
    see the clip. Swap it for a native <video controls preload="none">
    directly in the 预览 column — preload="none" keeps the 361-row table
    from firing off thumbnails for every row on load, and clicking the
    player's own play button starts it right there, no dialog involved.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • Reuses the existing nickname-modal/.nickname-box comic-panel look (a
    new .auth-error line and a plain-text auth-link-btn are the only new
    CSS) rather than inventing a second visual language. A "登录" topbar
    button opens the modal (email/password, toggle between login/
    register, cancel); once logged in it becomes "<nickname or email
    prefix> · 退出".
    
    Entirely optional, same as T19-T21 designed it: the modal's only
    trigger is that one button, nothing forces it open, and free browsing/
    voting keeps working on the plain viewer_uid cookie exactly as before.
    Manually verified end to end against the real running server (Chrome):
    register auto-logs in, an existing viewer_nickname cookie bound onto
    the new account and shown in the button, logout reverts the UI.
    
    auth-ui.test.js is a structural check (id/getElementById consistency,
    all four /api/auth/* endpoints wired, modal starts hidden) since no
    DOM/browser tooling is available to `node --test` here to render the
    pages for a full interaction test.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • Browsing a clip previously meant opening /lib/video/:id in a new tab
    via the raw address link. Add a 预览 button per row that opens a
    native <dialog> with an inline <video> player instead — closes on
    Esc, backdrop click, or its own close button, and clears the src on
    close so playback/buffering stops.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • live.html and dance.html already have independent scheduling pools
    (architecture.md 2.1), distinguished by clips' channel field, but
    admin.html rendered everything as one flat table with no way to tell
    the two apart or scope edits to one room. Add a 视频直播间/跳舞直播间
    tab switcher that filters the table by channel, lets manual adds pick
    which room they land in, and lets existing rows be reclassified via
    the edit form's new channel select (backed by server.js's existing
    channel support on POST/PATCH /api/admin/library, committed in e96d689
    as part of an unrelated concurrent session's work).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
    yibo authored
     
    Browse Code »
  • T20: register now also creates a kind='email_verify' session (24h TTL)
    and calls sendVerificationEmail() — the one pluggable seam for a real
    provider once one's chosen (dev-plan-3.md §1 flagged no vendor exists
    yet). Outside production the register response carries a devVerifyUrl
    so the whole loop is testable without a real inbox. GET
    /api/auth/verify?token= flips emailVerified and revokes the token so
    it can't be replayed.
    
    T21: register reads any existing viewer_uid/viewer_nickname cookie
    (live.html/dance.html's plain client-set identity, unrelated to this
    session system) and carries it onto the new account as legacyUid/
    nickname. Login pushes that legacyUid back onto the browser via
    Set-Cookie — even from a clean browser with no viewer_uid cookie at
    all — so logging into an account doesn't let someone dodge
    per-viewer_uid vote limits by starting a fresh anonymous identity each
    time. No history is rewritten, per dev-plan-3.md's explicit scope cut.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • 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
    yibo authored
     
    Browse Code »
  • New db.js opens data.sqlite (node:sqlite's DatabaseSync — a Node 24
    built-in, experimental but functional) and creates the users/sessions
    tables per dressup-dance-room-mvcp.md M.1/M.10. Zero new npm
    dependency: no bcrypt/argon2 (node:crypto's scryptSync covers password
    hashing in T19), no better-sqlite3, no package.json needed.
    
    Mirrors store.js's ergonomics (create/get/update, plain JS objects in
    and out — booleans and the oauthProviders array marshalled to/from
    SQLite's TEXT/INTEGER columns internally) rather than reaching for an
    ORM. Exports the openDb/createUsersStore/createSessionsStore factories
    alongside a default singleton so tests can point at an isolated tmp
    file instead of the real data.sqlite (same pattern
    dressup-generate.test.js uses for library.json).
    
    data.sqlite is gitignored under the secrets section, not the "runtime
    state" one — unlike library.json it holds real credential material
    (password hashes, session tokens), not just regenerable content.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • 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
    yibo authored
     
    Browse Code »
  • Breaks down dressup-dance-room.md 9节 phase 3 (dressup-dance-room.md
    9节第3条: email login, bind anonymous viewer_uid to an account, no
    credits/payment yet) into T18-T22 with acceptance criteria, work
    estimates, and a dependency graph, same format as dev-plan.md/
    dev-plan-2.md.
    
    Key decisions: users/sessions land in SQLite per architecture.md 1.4's
    own trigger condition (credential storage), using Node 24's built-in
    node:sqlite + node:crypto scryptSync — zero new npm dependency, no
    package.json needed. Flags one real gap the requirements doc's 待验证
    list missed: no email-delivery vendor has been chosen, so T20 ships a
    pluggable sendVerificationEmail() with a dev-mode bypass rather than
    blocking on that decision.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • Same hue-recolor approach as the first 10 (dev-plan.md's exit bar
    needs >=30 garments given M.7's per-character-garment dedupe), batch
    generation for the new 20 already submitted to H3.
    
    Also stops tracking uploads/ — binary image/video content, not a good
    git citizen, and this content is explicitly placeholder ("占位旗袍" /
    "待替换") the user intends to replace wholesale later. The recipe
    (characters/garments/dance-refs.json) stays tracked; the files they
    point at don't.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • Spawned server.js with cwd: __dirname (the real repo path, not an
    isolated copy), so its own test.after() cleanup — rm -rf on
    "uploads/" — wiped every real uploaded file in the repo on every
    `node --test` run, not just what the test itself created. Discovered
    after running the suite several times today silently deleted this
    session's real character/garment/dance-ref source images (the
    generated dance videos in library.json were unaffected — those proxy
    from H3, not served from disk — but admin/dance.html thumbnails broke
    until the images were re-uploaded).
    
    Now spawns from an fs.mkdtempSync isolated copy and cleans up only
    that tmpdir, matching the pattern already used correctly in
    dressup-generate.test.js and dance-channel.test.js.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • The rebuild path for non-edited entries only carried `likes` forward
    from the old record; playCount and lastPlayedAt were dropped, so any
    clip not yet touched in admin.html had its play stats silently reset
    to "never played" on every periodic sync (server.js runs this every
    5 min). Now carries both forward when present, and leaves them absent
    for a genuinely never-played clip rather than writing a false 0.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • Same category as live-state.json/dance-state.json: rewritten by
    ingest.js's periodic sync and by every clip play (playCount/
    lastPlayedAt), so every commit touching it was just churn from
    whoever last had the server running locally. File stays on disk,
    just untracked going forward.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • Records the 2026-09-02 real-H3 verification of T1-T10 (11 videos / 10
    garments, the barefoot reference-video leak finding, the two bugs
    found and fixed) and cross-references that T11-T17 (dev-plan-2.md)
    landed the same day from a separate session, so the docs don't read
    as stale the next time someone opens them.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • …ce refs, 11 generated videos
    
    Real (non-test) content for T1-T10's asset libraries, entered through
    the actual admin CRUD flow (admin-characters/garments/dance-refs.html
    endpoints) rather than test fixtures: 1 default character, 10 garment
    variants, 3 reference dance videos. Batch-generated through T9's
    /api/admin/batch-gen against the real H3 Console — 11 dance videos
    landed in library.json (one combo generated twice from a dedupe race
    before the edited:true fix), all with characterId/garmentId/
    refDanceVideoId traceability.
    
    Quality note found while spot-checking: 3 of the 11 clips show bare
    feet instead of the character reference's boots, and all three trace
    back to the same dance-ref ("轻柔手势舞", sourced from the S1-spike's
    control clip) — the reference video's own footwear leaks through
    despite the prompt's explicit "weak_reference" instruction. Worth
    avoiding barefoot reference footage when curating real dance-refs.
    
    This doesn't yet hit dev-plan.md's phase-1 exit bar of >=30 videos
    (that requires 30 garments given the M.7 per-character-garment dedupe
    cap; this seeds 10) but is enough real content for dance.html's
    wardrobe to have something to show.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • 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
    yibo authored
     
    Browse Code »
  • 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
    yibo authored
     
    Browse Code »
  • Backend (server.js):
    - T11: dance.html gets its own schedule/queue state machine, persisted to
      dance-state.json (gitignored, same as live-state.json). Per MVCP M.8,
      the schedule stores videoId references rather than inlined clip
      snapshots like live's does, joining back to library.json at read time.
      Generalized pickBestIndexByStatus/extracted pickLeastPlayedClip so both
      channels' schedulers share the same selection logic instead of forking
      it; live's random-fill pool now excludes channel:"dance" clips (and
      vice versa) so the two channels' filler picks stay independent, which
      wasn't true before this (T8's dance clips could have leaked into
      live.html's filler).
    - T12: GET /api/dance/wardrobe — public, unauthenticated list of active
      garments.
    - T15: POST /api/dance/pick {garmentId, nickname?} — free path, always
      uses the configured default character, reuses submitGeneration (T8/T9)
      whole including its M.7 dedupe cache, then hands the result to
      danceQueue. In-flight generations are reconciled against generationJobs
      on each schedule tick rather than via a promise chain, so a pick
      request survives a server restart the same way T8's own jobs do.
    - T17: separate danceViewers map and POST /api/dance/queue/vote/:id, so
      viewer counts and votes don't mix with live.html's.
    - SCHEDULE_TICK_MS env var makes both channels' tick interval
      configurable so tests don't wait out a real 2s.
    
    Frontend:
    - dance.html (T13/T14/T16): reuses live.html's visual language via
      live.css, adds dance.css for the 9:16 boxed player and wardrobe grid.
      Left column is a 衣柜/传衣服/传人 tab set (only 衣柜 is wired up —
      the other two need accounts/credits, dev-plan-2.md 第三/四期); right
      column shows the queue with vote buttons instead of live.html's chat.
      Age-confirmation gate (5.1节) blocks the page on first visit via a
      cookie, non-dismissable by clicking the backdrop.
    
    Tests: dance-channel.test.js spins up a mock H3 Console (same approach
    as dressup-generate.test.js) to exercise wardrobe listing, pick
    rejection/happy-path/dedupe, vote, and independent viewer counts
    end to end.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • Breaks down dev-plan.md's phase-2 scope (dressup-dance-room.md 9节
    第2条: dance.html + 免费衣柜模式 + 去重缓存, no paid modes/accounts/
    credits yet) into T11-T17 with acceptance criteria, work estimates,
    and a dependency graph, same format as dev-plan.md's T1-T10.
    
    Notes that dev-plan.md's own phase-1 exit criterion (batch-generate
    >=30 seed videos with real assets) is still outstanding and is an
    operational step, not blocking this plan but blocking the wardrobe
    actually having content once T11-T17 land.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    yibo authored
     
    Browse Code »
  • Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
    yibo authored
     
    Browse Code »
  • 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>
    yibo authored
     
    Browse Code »
  • 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>
    yibo authored
     
    Browse Code »