Spaces:
Running
Running
fix(customers): durable space-mirror backup + merge so web/zalo customers (NBV2fc) survive ketoan sync clobber
Browse files- Seed the space mirror customers.json with NBV2fc (+ admin VAS6b9) — the
durable web/zalo backup the ketoan sync never touches.
- GET /api/customers now MERGES the space mirror into the read base, so live
reads always include NBV2fc even if a concurrent writer dropped it from the
dataset file.
- POST /api/customers also MERGES the space mirror into the write base, so a
web save never drops NBV2fc / other web customers.
- Root cause: bot write 'Add VAS6b9' (b91ee83) wrote back a stale base that
had lost NBV2fc (read-modify-write race with the ketoan sync CN:96). The
mirror merge self-heals any future such loss.
- No Space write-through (avoids per-save rebuild data-loss race).
- customers.json +8 -1
- index.ts +54 -0
customers.json
CHANGED
|
@@ -5,5 +5,12 @@
|
|
| 5 |
"cid": "479b1cfad6a83ff666b9",
|
| 6 |
"created_at": "11/08/2026 20:56",
|
| 7 |
"ck": "Malloca 40, Eurogold 54, Grob 54, Demax 40"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
-
}
|
|
|
|
| 5 |
"cid": "479b1cfad6a83ff666b9",
|
| 6 |
"created_at": "11/08/2026 20:56",
|
| 7 |
"ck": "Malloca 40, Eurogold 54, Grob 54, Demax 40"
|
| 8 |
+
},
|
| 9 |
+
"26b87badb2ed5bb302fc": {
|
| 10 |
+
"name": "Nhà Bếp Vuông",
|
| 11 |
+
"ma_kh": "NBV2fc",
|
| 12 |
+
"cid": "26b87badb2ed5bb302fc",
|
| 13 |
+
"created_at": "12/08/2026 07:14",
|
| 14 |
+
"ck": "Malloca 40, Eurogold 54, Grob 54, Demax 40"
|
| 15 |
}
|
| 16 |
+
}
|
index.ts
CHANGED
|
@@ -1480,6 +1480,29 @@ const server = Bun.serve({
|
|
| 1480 |
}
|
| 1481 |
}
|
| 1482 |
} catch (_e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1483 |
return new Response(JSON.stringify(data), {
|
| 1484 |
headers: { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" }
|
| 1485 |
});
|
|
@@ -1539,6 +1562,31 @@ const server = Bun.serve({
|
|
| 1539 |
}
|
| 1540 |
}
|
| 1541 |
} catch (_e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
|
| 1543 |
let changed = 0;
|
| 1544 |
candidates.forEach((c: any) => {
|
|
@@ -1571,6 +1619,12 @@ const server = Bun.serve({
|
|
| 1571 |
// Persist locally too so the live GET keeps reflecting the save
|
| 1572 |
// immediately (no rebuild dependency).
|
| 1573 |
try { writeFileSync(f, content); } catch (_e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1574 |
return Response.json({ ok: true, updated: changed, customers: data });
|
| 1575 |
} catch (e: any) {
|
| 1576 |
return Response.json({ error: e.message }, { status: 500 });
|
|
|
|
| 1480 |
}
|
| 1481 |
}
|
| 1482 |
} catch (_e) {}
|
| 1483 |
+
// Merge the SPACE mirror (durable web/zalo backup, never touched by the
|
| 1484 |
+
// ketoan sync) so reads always include web/zalo customers like NBV2fc
|
| 1485 |
+
// even if a concurrent ketoan sync clobbered the dataset file.
|
| 1486 |
+
try {
|
| 1487 |
+
const mr = await fetch("https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/customers.json", {
|
| 1488 |
+
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1489 |
+
signal: AbortSignal.timeout(6000),
|
| 1490 |
+
});
|
| 1491 |
+
if (mr.ok) {
|
| 1492 |
+
const sp: any = JSON.parse(await mr.text());
|
| 1493 |
+
if (sp && typeof sp === "object") {
|
| 1494 |
+
for (const k in sp) {
|
| 1495 |
+
const v = sp[k];
|
| 1496 |
+
if (!v || !v.cid) continue;
|
| 1497 |
+
if (data[k]) {
|
| 1498 |
+
if (!data[k].ck && v.ck) data[k].ck = v.ck;
|
| 1499 |
+
} else {
|
| 1500 |
+
data[k] = Object.assign({}, v);
|
| 1501 |
+
}
|
| 1502 |
+
}
|
| 1503 |
+
}
|
| 1504 |
+
}
|
| 1505 |
+
} catch (_e) {}
|
| 1506 |
return new Response(JSON.stringify(data), {
|
| 1507 |
headers: { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" }
|
| 1508 |
});
|
|
|
|
| 1562 |
}
|
| 1563 |
}
|
| 1564 |
} catch (_e) {}
|
| 1565 |
+
// Merge the SPACE mirror (bep40/vai-avatar2/customers.json — the
|
| 1566 |
+
// durable web/zalo backup that the ketoan sync never touches) so a web
|
| 1567 |
+
// POST never drops web/zalo customers (e.g. NBV2fc) that a concurrent
|
| 1568 |
+
// ketoan sync removed from the dataset. The bot's write path already
|
| 1569 |
+
// merges this mirror; this extends the same self-healing to web writes.
|
| 1570 |
+
try {
|
| 1571 |
+
const mr = await fetch("https://huggingface.co/spaces/bep40/vai-avatar2/resolve/main/customers.json", {
|
| 1572 |
+
headers: token ? { Authorization: "Bearer " + token } : {},
|
| 1573 |
+
signal: AbortSignal.timeout(6000),
|
| 1574 |
+
});
|
| 1575 |
+
if (mr.ok) {
|
| 1576 |
+
const sp: any = JSON.parse(await mr.text());
|
| 1577 |
+
if (sp && typeof sp === "object") {
|
| 1578 |
+
for (const k in sp) {
|
| 1579 |
+
const v = sp[k];
|
| 1580 |
+
if (!v || !v.cid) continue;
|
| 1581 |
+
if (data[k]) {
|
| 1582 |
+
if (!data[k].ck && v.ck) data[k].ck = v.ck;
|
| 1583 |
+
} else {
|
| 1584 |
+
data[k] = Object.assign({}, v);
|
| 1585 |
+
}
|
| 1586 |
+
}
|
| 1587 |
+
}
|
| 1588 |
+
}
|
| 1589 |
+
} catch (_e) {}
|
| 1590 |
|
| 1591 |
let changed = 0;
|
| 1592 |
candidates.forEach((c: any) => {
|
|
|
|
| 1619 |
// Persist locally too so the live GET keeps reflecting the save
|
| 1620 |
// immediately (no rebuild dependency).
|
| 1621 |
try { writeFileSync(f, content); } catch (_e) {}
|
| 1622 |
+
// NOTE: we do NOT write-through to the vai-avatar2 Space mirror here —
|
| 1623 |
+
// committing to the Space repo triggers a rebuild on every save (the
|
| 1624 |
+
// exact data-loss race we removed). Instead the mirror is seeded with
|
| 1625 |
+
// the durable web/zalo backup and merged on every read/write, so it
|
| 1626 |
+
// self-heals (restores customers like NBV2fc after a ketoan sync
|
| 1627 |
+
// clobbers the dataset) without any rebuild.
|
| 1628 |
return Response.json({ ok: true, updated: changed, customers: data });
|
| 1629 |
} catch (e: any) {
|
| 1630 |
return Response.json({ error: e.message }, { status: 500 });
|