bot commited on
Commit
af3d386
·
1 Parent(s): 1f334b7

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).

Files changed (2) hide show
  1. customers.json +8 -1
  2. 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 });