diff --git a/admin-characters.html b/admin-characters.html
index 3489951..95df689 100644
--- a/admin-characters.html
+++ b/admin-characters.html
@@ -69,6 +69,7 @@
默认 |
来源 |
状态 |
+ 片段配额 |
创建时间 |
操作 |
@@ -97,6 +98,14 @@ function escapeHtml(s) {
return div.innerHTML;
}
+// T31: totalSegments/remainingSegments are only ever non-null for
+// source:"user_paid" characters (mode 3, T35) — admin_seed characters like
+// the default one stay null forever (unlimited plays, not a purchased quota).
+function segmentsLabel(c) {
+ if (c.totalSegments == null) return "-";
+ return `${c.remainingSegments ?? 0}/${c.totalSegments}`;
+}
+
async function uploadImage(file) {
const fd = new FormData();
fd.append("file", file);
@@ -134,6 +143,7 @@ function render() {
|
${c.source} |
${c.status} |
+ ${segmentsLabel(c)} |
${new Date(c.createdAt).toLocaleDateString()} |
@@ -150,6 +160,7 @@ function render() {
| ${c.isDefault ? '默认' : ""} |
${c.source} |
${c.status} |
+ ${segmentsLabel(c)} |
${new Date(c.createdAt).toLocaleDateString()} |
diff --git a/dressup-crud.test.js b/dressup-crud.test.js
index 9648a94..f8361d6 100644
--- a/dressup-crud.test.js
+++ b/dressup-crud.test.js
@@ -122,6 +122,35 @@ test("characters: setting isDefault on a second character clears it on the first
);
});
+// T31 (dev-plan-5.md): totalSegments/remainingSegments/status were already
+// part of the schema (T1) but never actually read/written until mode 3
+// (T35) exists. This just proves the plumbing (PATCH, admin.html's
+// segmentsLabel()) has something real to render — admin_seed characters
+// stay null/active by default (unaffected).
+test("characters: totalSegments/remainingSegments/status round-trip via PATCH (mode 3's shape, before T35 exists)", async () => {
+ const created = await fetch(
+ `${BASE}/api/admin/characters`,
+ authed({ method: "POST", body: JSON.stringify({ name: "付费人物占位", imageUrl: "/x.png", appearanceDescriptor: "d" }) })
+ ).then((r) => r.json());
+ assert.equal(created.totalSegments, null);
+ assert.equal(created.remainingSegments, null);
+ assert.equal(created.status, "active");
+
+ const patched = await fetch(
+ `${BASE}/api/admin/characters/${created.id}`,
+ authed({ method: "PATCH", body: JSON.stringify({ totalSegments: 3, remainingSegments: 3, status: "active" }) })
+ ).then((r) => r.json());
+ assert.equal(patched.totalSegments, 3);
+ assert.equal(patched.remainingSegments, 3);
+
+ const expired = await fetch(
+ `${BASE}/api/admin/characters/${created.id}`,
+ authed({ method: "PATCH", body: JSON.stringify({ remainingSegments: 0, status: "expired" }) })
+ ).then((r) => r.json());
+ assert.equal(expired.status, "expired");
+ assert.equal(expired.totalSegments, 3, "totalSegments is history, not reset by going expired");
+});
+
test("garments: create/list round-trip with defaults", async () => {
const res = await fetch(
`${BASE}/api/admin/garments`,
|