@tokenbuddy/tb-admin 1.0.32 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/src/cli.d.ts.map +1 -1
  2. package/dist/src/cli.js +29 -1
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/client.js +3 -3
  5. package/dist/src/client.js.map +1 -1
  6. package/dist/src/seller.d.ts +40 -1
  7. package/dist/src/seller.d.ts.map +1 -1
  8. package/dist/src/seller.js +132 -2
  9. package/dist/src/seller.js.map +1 -1
  10. package/dist/src/ui-actions.d.ts +2 -0
  11. package/dist/src/ui-actions.d.ts.map +1 -1
  12. package/dist/src/ui-actions.js +8 -6
  13. package/dist/src/ui-actions.js.map +1 -1
  14. package/dist/src/ui-command.d.ts +1 -0
  15. package/dist/src/ui-command.d.ts.map +1 -1
  16. package/dist/src/ui-command.js +7 -2
  17. package/dist/src/ui-command.js.map +1 -1
  18. package/dist/src/ui-server.d.ts.map +1 -1
  19. package/dist/src/ui-server.js +29 -8
  20. package/dist/src/ui-server.js.map +1 -1
  21. package/dist/src/ui-state.d.ts +29 -0
  22. package/dist/src/ui-state.d.ts.map +1 -1
  23. package/dist/src/ui-state.js +455 -111
  24. package/dist/src/ui-state.js.map +1 -1
  25. package/dist/src/ui-static.d.ts.map +1 -1
  26. package/dist/src/ui-static.js +262 -143
  27. package/dist/src/ui-static.js.map +1 -1
  28. package/dist/src/upstream-balance-probe.d.ts +2 -40
  29. package/dist/src/upstream-balance-probe.d.ts.map +1 -1
  30. package/dist/src/upstream-balance-probe.js +1 -378
  31. package/dist/src/upstream-balance-probe.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.ts +32 -1
  34. package/src/client.ts +3 -4
  35. package/src/seller.ts +179 -3
  36. package/src/ui-actions.ts +10 -6
  37. package/src/ui-command.ts +7 -2
  38. package/src/ui-server.ts +30 -8
  39. package/src/ui-state.ts +533 -111
  40. package/src/ui-static.ts +262 -143
  41. package/src/upstream-balance-probe.ts +13 -505
  42. package/tests/admin.test.ts +472 -36
  43. package/tests/seller.test.ts +84 -3
  44. package/tests/ui-state-fleet.test.ts +272 -3
  45. package/tests/ui-static-row.test.ts +273 -8
  46. package/tests/vendor-cli.test.ts +45 -1
@@ -2,6 +2,7 @@ import { ConfigManager } from "../src/config.js";
2
2
  import { buildAdminCli } from "../src/cli.js";
3
3
  import { FlyProvider, parseFlyMachineIds, requirePublishedDockerImage } from "../src/server-cmd.js";
4
4
  import { startAdminUiServer } from "../src/ui-server.js";
5
+ import { defaultUiProfile } from "../src/ui-command.js";
5
6
  import { AdminUiState } from "../src/ui-state.js";
6
7
  import { UiActions, type CreateSellerRequest, type UiActionResult } from "../src/ui-actions.js";
7
8
  import { adminUiHtml } from "../src/ui-static.js";
@@ -38,6 +39,7 @@ import * as vm from "vm";
38
39
 
39
40
  const TEMP_CONF_PATH = path.resolve(__dirname, "../../data-test/admin-config.json");
40
41
  const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
42
+ const ADMIN_UI_STATE_TEST_TIMEOUT_MS = 15_000;
41
43
 
42
44
  describe("Admin CLI Config Profile Management Tests", () => {
43
45
  beforeEach(() => {
@@ -90,6 +92,20 @@ describe("Admin CLI Config Profile Management Tests", () => {
90
92
  expect(() => new vm.Script(scripts[0])).not.toThrow();
91
93
  });
92
94
 
95
+ test("admin UI keeps release state in the seller table instead of a separate releases page", () => {
96
+ const html = adminUiHtml();
97
+
98
+ expect(html).toContain("<span>Pub</span>");
99
+ expect(html).not.toContain("Release Requests");
100
+ expect(html).not.toContain('data-page="releases"');
101
+ expect(html).not.toContain('id="page-releases"');
102
+ expect(html).not.toContain('id="releasesGrid"');
103
+ expect(html).not.toContain('btn.dataset.page === "releases"');
104
+ expect(html).not.toContain("/api/vendor/release-requests");
105
+ expect(html).not.toContain('id="page-bootstrap"');
106
+ expect(html).not.toContain('getElementById("bootstrapGrid")');
107
+ });
108
+
93
109
  test("Switch default profiles", () => {
94
110
  const mgr = new ConfigManager(TEMP_CONF_PATH);
95
111
  mgr.setProfile("prod", { url: "http://127.0.0.1:8000", token: "secret-op" });
@@ -358,6 +374,15 @@ describe("Admin CLI Config Profile Management Tests", () => {
358
374
  expect(() => validateRegistryDocument(badDefault)).toThrow("defaultSeller `missing`");
359
375
  });
360
376
 
377
+ test("tb-admin ui prefers vendor bootstrap profile when available", () => {
378
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
379
+ mgr.setProfile("bootstrap", { url: "https://tb-registry.fly.dev", token: "operator-token" });
380
+ expect(defaultUiProfile(mgr)).toBe("bootstrap");
381
+
382
+ mgr.setProfile("bootstrap-vendor", { url: "https://tb-registry.fly.dev", token: "vendor-token" });
383
+ expect(defaultUiProfile(mgr)).toBe("bootstrap-vendor");
384
+ });
385
+
361
386
  test("tb-admin ui server binds loopback, rejects cross-origin APIs, and serves bootstrap data", async () => {
362
387
  const mgr = new ConfigManager(TEMP_CONF_PATH);
363
388
  const started = await startAdminUiServer({
@@ -406,6 +431,216 @@ describe("Admin CLI Config Profile Management Tests", () => {
406
431
  })).rejects.toThrow("loopback");
407
432
  });
408
433
 
434
+ test("tb-admin ui status refresh updates existing rows without inventory reload", async () => {
435
+ const seller = http.createServer((req, res) => {
436
+ const url = new URL(req.url || "/", "http://127.0.0.1");
437
+ if (req.method === "GET" && url.pathname === "/manifest") {
438
+ sendJson(res, { sellerId: "tbs-status-only" });
439
+ return;
440
+ }
441
+ if (req.method === "GET" && url.pathname === "/operator/status") {
442
+ expect(req.headers.authorization).toBe("Bearer operator-secret");
443
+ sendJson(res, {
444
+ status: "healthy",
445
+ upstream: { status: "healthy" },
446
+ capacity: { activeConnections: 2, maxConnections: 9 },
447
+ runtime: { cpuPercent: 8.4, memoryPercent: 41.2, memoryRssMb: 211, memoryLimitMb: 512 },
448
+ latency: { ttftMs: 123, avgTokensPerSecond: 42.5, sampleCount: 7 }
449
+ });
450
+ return;
451
+ }
452
+ if (req.method === "GET" && url.pathname === "/operator/admin/upstream-balance") {
453
+ expect(req.headers.authorization).toBe("Bearer operator-secret");
454
+ sendJson(res, {
455
+ status: "ok",
456
+ balance: {
457
+ rawAmount: 12.75,
458
+ amountUsdMicros: 12750000,
459
+ currency: "USD",
460
+ source: "openrouter",
461
+ fetchedAt: 1800000000000
462
+ }
463
+ });
464
+ return;
465
+ }
466
+ res.statusCode = 404;
467
+ sendJson(res, { error: "not found" });
468
+ });
469
+ await new Promise<void>((resolve) => seller.listen(0, "127.0.0.1", () => resolve()));
470
+ const address = seller.address();
471
+ if (!address || typeof address !== "object") {
472
+ throw new Error("seller fixture did not bind a TCP port");
473
+ }
474
+ const sellerUrl = `http://127.0.0.1:${address.port}`;
475
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
476
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
477
+ const started = await startAdminUiServer({
478
+ host: "127.0.0.1",
479
+ port: 0,
480
+ openBrowser: false,
481
+ configManager: mgr
482
+ });
483
+ try {
484
+ const response = await fetch(`${started.url}api/sellers/status`, {
485
+ method: "POST",
486
+ headers: { "Content-Type": "application/json" },
487
+ body: JSON.stringify({
488
+ rows: [{
489
+ id: "tbs-status-only",
490
+ name: "tbs-status-only",
491
+ app: "tbs-status-only",
492
+ url: sellerUrl,
493
+ registryStatus: "unknown",
494
+ nodeStatus: "unknown",
495
+ upstreamDomain: "seller.example",
496
+ upstreamStatus: "unknown",
497
+ upstreamBalanceError: "unauthorized: check upstreamApiKey",
498
+ dataSource: "fly",
499
+ flyApp: { name: "tbs-status-only", status: "deployed" }
500
+ }]
501
+ })
502
+ });
503
+ expect(response.status).toBe(200);
504
+ const wrongMethodResponse = await fetch(`${started.url}api/sellers/status`);
505
+ expect(wrongMethodResponse.status).toBe(405);
506
+ await expect(wrongMethodResponse.json()).resolves.toMatchObject({ error: "method not allowed" });
507
+ const rows = await response.json() as any[];
508
+ expect(rows).toHaveLength(1);
509
+ expect(rows[0]).toMatchObject({
510
+ id: "tbs-status-only",
511
+ nodeStatus: "active",
512
+ upstreamStatus: "healthy",
513
+ capacityUsed: 2,
514
+ capacityLimit: 9,
515
+ resourceCpuPercent: 8.4,
516
+ resourceMemoryPercent: 41.2,
517
+ ttftMs: 123,
518
+ avgTokensPerSecond: 42.5,
519
+ latencySamples: 7,
520
+ upstreamBalanceUsdMicros: 12750000,
521
+ upstreamBalanceCurrency: "USD",
522
+ upstreamBalanceSource: "openrouter"
523
+ });
524
+ expect(rows[0].upstreamBalanceError).toBeUndefined();
525
+ } finally {
526
+ await new Promise<void>((resolve) => started.server.close(() => resolve()));
527
+ await new Promise<void>((resolve) => seller.close(() => resolve()));
528
+ }
529
+ });
530
+
531
+ test("tb-admin ui release requests endpoint proxies the active vendor profile", async () => {
532
+ const registry = http.createServer((req, res) => {
533
+ const url = new URL(req.url || "/", "http://127.0.0.1");
534
+ if (req.method === "GET" && url.pathname === "/platform/release-requests") {
535
+ expect(req.headers.authorization).toBe("Bearer vendor-token-value");
536
+ expect(url.searchParams.get("limit")).toBe("20");
537
+ sendJson(res, {
538
+ releaseRequests: [{
539
+ id: 42,
540
+ status: "pending",
541
+ submittedAt: "2026-06-11T00:00:00.000Z",
542
+ sellerCount: 1,
543
+ note: "fixture release"
544
+ }]
545
+ });
546
+ return;
547
+ }
548
+ res.statusCode = 404;
549
+ sendJson(res, { error: "not found" });
550
+ });
551
+ await new Promise<void>((resolve) => registry.listen(0, "127.0.0.1", () => resolve()));
552
+ const address = registry.address();
553
+ if (!address || typeof address !== "object") {
554
+ throw new Error("registry fixture did not bind a TCP port");
555
+ }
556
+
557
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
558
+ mgr.setProfile("bootstrap", { url: `http://127.0.0.1:${address.port}`, token: "vendor-token-value" });
559
+ const started = await startAdminUiServer({
560
+ host: "127.0.0.1",
561
+ port: 0,
562
+ openBrowser: false,
563
+ configManager: mgr,
564
+ profile: "bootstrap"
565
+ });
566
+ try {
567
+ const response = await fetch(`${started.url}api/vendor/release-requests`);
568
+ expect(response.status).toBe(200);
569
+ await expect(response.json()).resolves.toMatchObject({
570
+ releaseRequests: [{ id: 42, status: "pending", note: "fixture release" }]
571
+ });
572
+ } finally {
573
+ await new Promise<void>((resolve) => started.server.close(() => resolve()));
574
+ await new Promise<void>((resolve) => registry.close(() => resolve()));
575
+ }
576
+ });
577
+
578
+ test("UiActions setRegistryStatus uses vendor-scoped seller list and mutation auth", async () => {
579
+ const calls: Array<{ method?: string; path: string; authorization?: string; body?: any }> = [];
580
+ const registry = http.createServer((req, res) => {
581
+ const url = new URL(req.url || "/", "http://127.0.0.1");
582
+ let body = "";
583
+ req.on("data", (chunk) => {
584
+ body += chunk;
585
+ });
586
+ req.on("end", () => {
587
+ const parsed = body ? JSON.parse(body) : undefined;
588
+ calls.push({ method: req.method, path: url.pathname, authorization: req.headers.authorization, body: parsed });
589
+ if (req.method === "GET" && url.pathname === "/platform/sellers") {
590
+ expect(req.headers.authorization).toBe("Bearer vendor-token-value");
591
+ sendJson(res, {
592
+ sellers: [{
593
+ id: "vendor-live-1",
594
+ name: "Vendor Live 1",
595
+ app: "tbs-vendor-live-1",
596
+ url: "https://tbs-vendor-live-1.fly.dev",
597
+ status: "active",
598
+ supportedProtocols: ["chat_completions"],
599
+ paymentMethods: ["clawtip"]
600
+ }]
601
+ });
602
+ return;
603
+ }
604
+ if (req.method === "GET" && url.pathname === "/registry/sellers") {
605
+ expect(req.headers.authorization).toBeUndefined();
606
+ sendJson(res, { version: 12, sellers: [] });
607
+ return;
608
+ }
609
+ if (req.method === "PUT" && url.pathname === "/platform/sellers/vendor-live-1/status") {
610
+ expect(req.headers.authorization).toBe("Bearer vendor-token-value");
611
+ expect(parsed).toMatchObject({ status: "draining", expectedVersion: 12 });
612
+ expect(req.headers["idempotency-key"]).toMatch(/^tb-admin-ui-vendor-live-1-draining-12-/);
613
+ sendJson(res, { registry: { version: 13, sellers: [] } });
614
+ return;
615
+ }
616
+ res.statusCode = 404;
617
+ sendJson(res, { error: "not found" });
618
+ });
619
+ });
620
+ await new Promise<void>((resolve) => registry.listen(0, "127.0.0.1", () => resolve()));
621
+ const address = registry.address();
622
+ if (!address || typeof address !== "object") {
623
+ throw new Error("registry fixture did not bind a TCP port");
624
+ }
625
+
626
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
627
+ mgr.setProfile("bootstrap", { url: `http://127.0.0.1:${address.port}`, token: "vendor-token-value" });
628
+ const actions = new UiActions({ configManager: mgr, profile: "bootstrap" });
629
+ try {
630
+ const result = await actions.setRegistryStatus("vendor-live-1", "draining");
631
+
632
+ expect(result.ok).toBe(true);
633
+ expect(result.stdout).toContain("version=13");
634
+ expect(calls.map((call) => `${call.method} ${call.path}`)).toEqual([
635
+ "GET /platform/sellers",
636
+ "GET /registry/sellers",
637
+ "PUT /platform/sellers/vendor-live-1/status"
638
+ ]);
639
+ } finally {
640
+ await new Promise<void>((resolve) => registry.close(() => resolve()));
641
+ }
642
+ });
643
+
409
644
  test("tb-admin ui create seller returns a progress job", async () => {
410
645
  const mgr = new ConfigManager(TEMP_CONF_PATH);
411
646
  mgr.setSellerProvider("fly", { operator_secret: "operator-token-value" });
@@ -520,10 +755,20 @@ describe("Admin CLI Config Profile Management Tests", () => {
520
755
  owner: "vendor-a",
521
756
  raw: {}
522
757
  }],
523
- balanceFetch: async (url, init) => {
524
- expect(String(url)).toBe("https://openrouter.ai/api/v1/credits");
525
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer fixture-live-key-27f9");
526
- return jsonResponse({ data: { total_credits: 50, total_usage: 2.05 } });
758
+ flyMachineSpecs: async (appName) => {
759
+ expect(appName).toBe("tbs-sin-06");
760
+ return {
761
+ machines: 1,
762
+ runningMachines: 1,
763
+ cpuKind: "shared",
764
+ cpuCores: 1,
765
+ memoryMb: 512,
766
+ volumeGb: 1,
767
+ regions: ["sin"]
768
+ };
769
+ },
770
+ balanceFetch: async () => {
771
+ throw new Error("admin UI must use seller operator balance endpoint before local probing");
527
772
  }
528
773
  });
529
774
  const sellers = await state.sellers();
@@ -542,9 +787,16 @@ describe("Admin CLI Config Profile Management Tests", () => {
542
787
  upstreamBalanceCurrency: "USD",
543
788
  upstreamBalanceSource: "openrouter"
544
789
  });
790
+ expect(sellers[0].specs).toMatchObject({
791
+ machines: 1,
792
+ runningMachines: 1,
793
+ cpuCores: 1,
794
+ memoryMb: 512,
795
+ volumeGb: 1
796
+ });
545
797
 
546
798
  const detail = await state.sellerDetail("tbs-sin-06");
547
- expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** 27f9");
799
+ expect(detail.configuration.upstreamApiKeyMasked).toBe("configured");
548
800
  expect(detail.configuration.upstreamBalance).toBe("USD 47.95");
549
801
  expect(detail.configuration.upstreamBalanceSource).toBe("openrouter");
550
802
  expect(detail.configuration.upstreamBalanceProbeTemplate).toBe("openrouter");
@@ -558,7 +810,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
558
810
  } finally {
559
811
  await fixture.close();
560
812
  }
561
- });
813
+ }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
562
814
 
563
815
  test("AdminUiState uses Fly provider operator secret for registry sellers without local profiles", async () => {
564
816
  const mgr = new ConfigManager(TEMP_CONF_PATH);
@@ -642,7 +894,68 @@ describe("Admin CLI Config Profile Management Tests", () => {
642
894
  expect(detail.configuration.upstreamUrl).toBe("https://openrouter.ai/api");
643
895
  expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** abcd");
644
896
  expect(detail.models.map((model) => model.upstreamModel)).toEqual(["openai/gpt-5.4", "anthropic/claude-opus-4.7"]);
645
- });
897
+ }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
898
+
899
+ test("AdminUiState loads Fly seller detail when vendor registry auth fails", async () => {
900
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
901
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
902
+ const state = new AdminUiState({
903
+ configManager: mgr,
904
+ url: "https://bootstrap.example.test",
905
+ flyApps: async () => [{
906
+ name: "tbs-flyonly-node",
907
+ status: "running",
908
+ owner: "vendor-a",
909
+ raw: {}
910
+ }],
911
+ fetchJson: async (url, init) => {
912
+ const pathName = new URL(url).pathname;
913
+ if (pathName === "/platform/sellers") {
914
+ throw new Error('HTTP Error 401: {"error":"vendor_auth_failed"}');
915
+ }
916
+ expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
917
+ if (pathName === "/operator/status") {
918
+ return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 2, maxConnections: 6 } };
919
+ }
920
+ if (pathName === "/operator/admin/service") {
921
+ return { sellerId: "tbs-flyonly-node", modelsCount: 1, capacity: { maxConnections: 6, maxQueueDepth: 3 } };
922
+ }
923
+ if (pathName === "/operator/admin/upstreams") {
924
+ return {
925
+ upstreams: [{
926
+ upstreamUrl: "https://api.moonshot.cn/v1",
927
+ models: [{ id: "moonshot-v1-8k" }]
928
+ }]
929
+ };
930
+ }
931
+ if (pathName === "/operator/admin/config") {
932
+ return {
933
+ config: {
934
+ upstreamUrl: "https://api.moonshot.cn/v1",
935
+ upstreamApiKey: "moonshot-live-secret",
936
+ upstreamBalanceProbe: { template: "none" },
937
+ maxConnections: 6,
938
+ maxQueueDepth: 3
939
+ }
940
+ };
941
+ }
942
+ throw new Error(`unexpected fetch url ${url}`);
943
+ }
944
+ });
945
+
946
+ const detail = await state.sellerDetail("tbs-flyonly-node");
947
+
948
+ expect(detail.row).toMatchObject({
949
+ id: "tbs-flyonly-node",
950
+ dataSource: "fly",
951
+ nodeStatus: "active",
952
+ upstreamDomain: "api.moonshot.cn",
953
+ capacityUsed: 2,
954
+ capacityLimit: 6
955
+ });
956
+ expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** cret");
957
+ expect(detail.models.map((model) => model.upstreamModel)).toEqual(["moonshot-v1-8k"]);
958
+ }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
646
959
 
647
960
  test("UiActions updates new registry seller config without a local profile", async () => {
648
961
  const mgr = new ConfigManager(TEMP_CONF_PATH);
@@ -695,6 +1008,61 @@ describe("Admin CLI Config Profile Management Tests", () => {
695
1008
  ]);
696
1009
  });
697
1010
 
1011
+ test("UiActions updates Fly seller config when vendor registry auth fails", async () => {
1012
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
1013
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1014
+ const calls: string[][] = [];
1015
+ const fetchPaths: string[] = [];
1016
+ const actions = new UiActions({
1017
+ configManager: mgr,
1018
+ url: "https://bootstrap.example.test",
1019
+ token: "vendor-token",
1020
+ flyApps: async () => [{
1021
+ name: "tbs-flyonly-node",
1022
+ status: "running",
1023
+ owner: "vendor-a",
1024
+ raw: {}
1025
+ }],
1026
+ fetchJson: async (url, init) => {
1027
+ const pathName = new URL(url).pathname;
1028
+ fetchPaths.push(pathName);
1029
+ if (pathName === "/platform/sellers") {
1030
+ expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer vendor-token");
1031
+ throw new Error('HTTP Error 401: {"error":"vendor_auth_failed"}');
1032
+ }
1033
+ expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
1034
+ if (pathName === "/operator/admin/config") {
1035
+ return {
1036
+ config: {
1037
+ upstreamUrl: "https://api.moonshot.cn/v1",
1038
+ upstreamApiKey: "live-secret-abcd",
1039
+ maxConnections: 6,
1040
+ maxQueueDepth: 3
1041
+ }
1042
+ };
1043
+ }
1044
+ throw new Error(`unexpected fetch url ${url}`);
1045
+ },
1046
+ commandRunner: async (args): Promise<UiActionResult> => {
1047
+ calls.push(args);
1048
+ return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1049
+ }
1050
+ });
1051
+
1052
+ const result = await actions.updateSellerConfig("tbs-flyonly-node", { maxConnections: 9 });
1053
+
1054
+ expect(result.ok).toBe(true);
1055
+ expect(fetchPaths).toEqual([
1056
+ "/platform/sellers",
1057
+ "/registry/sellers",
1058
+ "/operator/admin/config"
1059
+ ]);
1060
+ expect(calls.map((args) => args.join(" "))).toEqual([
1061
+ expect.stringContaining("--url https://tbs-flyonly-node.fly.dev --token operator-secret seller-config validate --file"),
1062
+ expect.stringContaining("--url https://tbs-flyonly-node.fly.dev --token operator-secret seller-config put --file")
1063
+ ]);
1064
+ });
1065
+
698
1066
  test("admin UI seller rows render missing telemetry without unknown data labels", () => {
699
1067
  const html = adminUiHtml();
700
1068
  expect(html).toContain("class=\"spinner\"");
@@ -722,31 +1090,62 @@ describe("Admin CLI Config Profile Management Tests", () => {
722
1090
  expect(html).toContain("pollCreateJob");
723
1091
  expect(html).toContain("Created and added to bootstrap registry.");
724
1092
  expect(html).toContain("upstreamUrl:\"https://openrouter.ai/api/v1\"");
725
- expect(html).toContain("loadingSpinner(\"Loading sellers\")");
726
- expect(html).toContain("loadingSpinner(\"Loading configuration\")");
727
- expect(html).toContain("loadingSpinner(\"Loading models\")");
728
- expect(html).toContain("sellerRefreshIntervalMs = 30000");
729
- expect(html).toContain("function scheduleSellerRefresh()");
730
- expect(html).toContain("sellerNextRefreshAt = new Date(Date.now() + sellerRefreshIntervalMs)");
731
- expect(html).toContain("sellerRefreshTimer = setTimeout(() => loadSellers(), sellerRefreshIntervalMs)");
732
- expect(html).toContain("Next refresh: ");
1093
+ expect(html).toContain("loadingSpinner(\"Loading registry\")");
1094
+ expect(html).toContain('id="detailGrid" class="detail-grid hidden"');
1095
+ expect(html).toContain('showDetailStatus("Loading seller data", true)');
1096
+ expect(html).toContain('document.getElementById("detailGrid").classList.add("hidden")');
1097
+ expect(html).toContain('document.getElementById("detailGrid").classList.remove("hidden")');
1098
+ expect(html).not.toContain("loadingSpinner(\"Loading configuration\")");
1099
+ expect(html).not.toContain("loadingSpinner(\"Loading models\")");
1100
+ expect(html).toContain("sellerStatusRefreshIntervalMs = 30000");
1101
+ expect(html).toContain("/api/sellers/status");
1102
+ expect(html).toContain("function pumpSellerDetailQueue()");
1103
+ expect(html).toContain("function statusRefreshRow(row)");
1104
+ expect(html).toContain("function scheduleSellerDetailRefresh(row)");
1105
+ expect(html).toContain("const nextRefreshAt = new Date(Date.now() + sellerStatusRefreshIntervalMs).toISOString()");
1106
+ expect(html).toContain("sellerDetailTimers.set(key, timer)");
1107
+ expect(html).toContain("Refreshing details");
733
1108
  expect(html).not.toContain("sellerLastUpdated");
734
1109
  expect(html).not.toContain("Last updated:");
735
1110
  expect(html).not.toContain("s ago");
736
- expect(html).toContain("sellerClockTimer = setInterval(() => updateSellerRefreshMeta(sellerRefreshInFlight), 1000)");
1111
+ expect(html).toContain("sellerClockTimer = setInterval(() => { updateSellerRefreshMeta(); updateSellerRowRefreshCountdowns(); }, 1000)");
737
1112
  expect(html).toContain("function renderSellerRows(rows)");
738
1113
  expect(html).toContain("sellerRefreshLoaded = true");
739
1114
  expect(html).toContain("readonly-value");
740
1115
  expect(html).toContain("--seller-grid");
741
1116
  expect(html).toContain("grid-template-columns:var(--seller-grid)");
742
- expect(html).toContain("#sellerRows{display:grid;gap:10px;width:100%;min-width:0}");
743
- expect(html).toContain("gap:12px;width:100%;min-width:0");
1117
+ expect(html).toContain("#sellerRows{display:grid;gap:8px;width:100%;min-width:0}");
1118
+ expect(html).toContain("gap:8px;width:100%;min-width:0");
744
1119
  expect(html).toContain("detailFieldsHtml");
745
1120
  expect(html).toContain("data-original");
746
- // Design-spec header labels (TTFT not Latency, Disc not Discount)
1121
+ expect(html).toContain('id="closeDetail" class="modal-close"');
1122
+ expect(html).toContain('aria-label="Close detail"');
1123
+ expect(html).not.toContain('id="closeDetail" class="btn">Close</button>');
1124
+ expect(html).toContain("function registryStatusForAction(action)");
1125
+ expect(html).toContain("function patchSellerRegistryStatus(id, status)");
1126
+ expect(html).toContain('showDetailStatus("Updating registry status", true)');
1127
+ expect(html).toContain("patchSellerRegistryStatus(id, status)");
1128
+ expect(html).toContain('document.getElementById("detailModal").classList.remove("open")');
1129
+ expect(html).not.toContain("Set \"+currentDetail.row.name+\" registry status via");
1130
+ expect(html).toContain("function registryStatusDisplay(status)");
1131
+ expect(html).toContain('["registryStatus", registryStatusDisplay(c.registryStatus || d.row.registryStatus)]');
1132
+ expect(html).toContain("function setDetailSavingBusy(busy)");
1133
+ expect(html).toContain('edit.textContent = busy ? "Saving" : (editing ? "Save changes" : "Edit config")');
1134
+ expect(html).toContain('showDetailStatus("Saving seller config", true)');
1135
+ expect(html).toContain('if (!result.ok) throw new Error(result.stderr || "Save failed")');
1136
+ expect(html).toContain("editing = false; renderDetail(); loadSellers();");
1137
+ expect(html).toContain('catch (err) { showDetailStatus(err.message || "Save failed", false); }');
1138
+ expect(html).not.toContain('showDetailStatus(result.ok ? "Saved"');
1139
+ // Design-spec header labels keep monitoring fields visible.
1140
+ expect(html).toContain(">Connection</span>");
1141
+ expect(html).toContain(">Resources</span>");
1142
+ expect(html).toContain(">Balance</span>");
747
1143
  expect(html).toContain(">TTFT</span>");
748
- expect(html).toContain(">Disc</span>");
1144
+ expect(html).toContain(">Status</span>");
1145
+ expect(html).toContain("function upstreamStatusTone(status)");
1146
+ expect(html).toContain("Upstream status from seller /operator/status");
749
1147
  expect(html).not.toContain(">Latency</span>");
1148
+ expect(html).not.toContain(">Discount</span>");
750
1149
  // Spec-compliant token formats
751
1150
  expect(html).toContain("tok/s");
752
1151
  expect(html).toContain("AVG speed");
@@ -757,6 +1156,13 @@ describe("Admin CLI Config Profile Management Tests", () => {
757
1156
  expect(html).toContain("formatSellerStatus");
758
1157
  expect(html).toContain("formatDiscountRatio");
759
1158
  expect(html).toContain("formatBalanceAmount");
1159
+ expect(html).not.toContain("status-badge");
1160
+ expect(html).not.toContain("balance-badge");
1161
+ expect(html).not.toContain("probe blocked");
1162
+ expect(html).toContain("runningMachines");
1163
+ expect(html).toContain("memoryMb");
1164
+ expect(html).toContain("resourceCpuPercent");
1165
+ expect(html).toContain("resourceMemoryPercent");
760
1166
  // No glassmorphism per spec
761
1167
  expect(html).not.toContain("backdrop-filter");
762
1168
  // Detail field references
@@ -811,8 +1217,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
811
1217
  // Spec uses — (em dash) for unknown, not "not reported" or "n/a"
812
1218
  expect(html).not.toContain("not reported");
813
1219
  expect(html).not.toContain("\"n/a\"");
814
- expect(html).toContain("aria-label=\"Seller specs\"");
815
- expect(html).toContain("<svg viewBox=\"0 0 24 24\"");
1220
+ expect(html).toContain("function usagePercent(value)");
1221
+ expect(html).toContain("usage CPU ");
1222
+ expect(html).toContain('const resourcePrimary = (cpuUsageText === "—" ? "-" : cpuUsageText) + "/" + (memoryUsageText === "—" ? "-" : memoryUsageText)');
816
1223
  expect(html).not.toContain(">i</span>");
817
1224
  expect(html).not.toContain(">×</button>");
818
1225
  expect(html).not.toContain("Loading sellers...");
@@ -834,6 +1241,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
834
1241
  const actions = new UiActions({
835
1242
  configManager: mgr,
836
1243
  profile: "bootstrap",
1244
+ flyApps: async () => [],
837
1245
  commandRunner: async (args): Promise<UiActionResult> => {
838
1246
  calls.push(args);
839
1247
  return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
@@ -1529,24 +1937,39 @@ async function startMutableSellerConfigServer(initialConfig: Record<string, any>
1529
1937
  async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
1530
1938
  const server = http.createServer((req, res) => {
1531
1939
  const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
1940
+ const baseUrl = `http://${req.headers.host}`;
1941
+ const registrySellers = [{
1942
+ id: "tbs-sin-06",
1943
+ name: "tbs-sin-06",
1944
+ profile: "seller-sin",
1945
+ app: "tbs-sin-06",
1946
+ url: baseUrl,
1947
+ status: "active",
1948
+ region: "sin",
1949
+ modelsCount: 1,
1950
+ sampleModels: ["openai/gpt-5.4"],
1951
+ supportedProtocols: ["responses"],
1952
+ paymentMethods: ["clawtip"]
1953
+ }];
1954
+ if (pathName === "/platform/sellers") {
1955
+ expect(req.headers.authorization).toBe("Bearer bootstrap-token");
1956
+ sendJson(res, { sellers: registrySellers });
1957
+ return;
1958
+ }
1532
1959
  if (pathName === "/registry/sellers") {
1533
1960
  sendJson(res, {
1534
1961
  version: 7,
1535
1962
  updatedAt: "2026-06-05T00:00:00.000Z",
1536
1963
  defaultSeller: "tbs-sin-06",
1537
- sellers: [{
1538
- id: "tbs-sin-06",
1539
- name: "tbs-sin-06",
1540
- profile: "seller-sin",
1541
- app: "tbs-sin-06",
1542
- url: "https://seller.example.test",
1543
- status: "active",
1544
- region: "sin",
1545
- modelsCount: 1,
1546
- sampleModels: ["openai/gpt-5.4"],
1547
- supportedProtocols: ["responses"],
1548
- paymentMethods: ["clawtip"]
1549
- }]
1964
+ sellers: registrySellers
1965
+ });
1966
+ return;
1967
+ }
1968
+ if (pathName === "/manifest") {
1969
+ sendJson(res, {
1970
+ sellerId: "tbs-sin-06",
1971
+ supportedProtocols: ["responses"],
1972
+ models: [{ id: "openai/gpt-5.4" }]
1550
1973
  });
1551
1974
  return;
1552
1975
  }
@@ -1587,7 +2010,7 @@ async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () =
1587
2010
  sendJson(res, {
1588
2011
  config: {
1589
2012
  upstreamUrl: "https://openrouter.ai/api/v1",
1590
- upstreamApiKey: "fixture-live-key-27f9",
2013
+ upstreamApiKey: "[redacted]",
1591
2014
  upstreamBalanceUrl: "https://openrouter.ai/api/v1/credits",
1592
2015
  upstreamRechargeUrl: "https://openrouter.ai/settings/credits",
1593
2016
  upstreamBalanceProbe: {
@@ -1605,6 +2028,19 @@ async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () =
1605
2028
  });
1606
2029
  return;
1607
2030
  }
2031
+ if (pathName === "/operator/admin/upstream-balance") {
2032
+ sendJson(res, {
2033
+ status: "ok",
2034
+ balance: {
2035
+ source: "openrouter",
2036
+ rawAmount: 47.95,
2037
+ amountUsdMicros: 47_950_000,
2038
+ currency: "USD",
2039
+ fetchedAt: 1781220000000
2040
+ }
2041
+ });
2042
+ return;
2043
+ }
1608
2044
  res.statusCode = 404;
1609
2045
  res.end();
1610
2046
  });