-
需求文档 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
-
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 -
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
-
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
-
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>