@tokenbuddy/tb-admin 1.0.33 → 1.0.35
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.
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +28 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/seller.d.ts +40 -1
- package/dist/src/seller.d.ts.map +1 -1
- package/dist/src/seller.js +132 -2
- package/dist/src/seller.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +8 -6
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.d.ts +1 -0
- package/dist/src/ui-command.d.ts.map +1 -1
- package/dist/src/ui-command.js +7 -2
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.js +17 -0
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +31 -0
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +461 -115
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +267 -144
- package/dist/src/ui-static.js.map +1 -1
- package/dist/src/upstream-balance-probe.d.ts +2 -40
- package/dist/src/upstream-balance-probe.d.ts.map +1 -1
- package/dist/src/upstream-balance-probe.js +1 -378
- package/dist/src/upstream-balance-probe.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +31 -0
- package/src/seller.ts +179 -3
- package/src/ui-actions.ts +10 -6
- package/src/ui-command.ts +7 -2
- package/src/ui-server.ts +18 -1
- package/src/ui-state.ts +541 -115
- package/src/ui-static.ts +267 -144
- package/src/upstream-balance-probe.ts +13 -505
- package/tests/admin.test.ts +418 -39
- package/tests/seller.test.ts +51 -0
- package/tests/ui-state-fleet.test.ts +272 -3
- package/tests/ui-static-row.test.ts +273 -8
package/src/ui-static.ts
CHANGED
|
@@ -52,18 +52,12 @@ export function adminUiHtml(): string {
|
|
|
52
52
|
.top-link:hover{background:var(--panel);color:var(--ink)}
|
|
53
53
|
.top-link.active{color:var(--ink);font-weight:760}
|
|
54
54
|
.top-link.active:after{opacity:1;transform:scaleX(1)}
|
|
55
|
-
.content{width:min(
|
|
55
|
+
.content{width:min(1560px,calc(100vw - 64px));margin:0 auto;padding:20px 0 36px;display:grid;gap:16px}
|
|
56
56
|
.page{display:none}.page.active{display:grid;gap:16px}
|
|
57
57
|
/* ---- Panels ------------------------------------------------------- */
|
|
58
58
|
.panel,.bootstrap-card{border:1px solid var(--hairline);background:var(--panel);border-radius:12px;box-shadow:var(--shadow);overflow:hidden}
|
|
59
59
|
.panel-head{min-height:56px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--hairline);background:var(--panel)}
|
|
60
60
|
.title{margin:0;color:var(--ink);font-size:20px;line-height:28px;font-weight:750}
|
|
61
|
-
/* ---- Refresh pill ------------------------------------------------- */
|
|
62
|
-
.list-status{display:flex;align-items:center;gap:12px;min-width:0;color:var(--muted);font-size:12px;font-weight:650}
|
|
63
|
-
.refresh-pill{border:1px solid var(--hairline-strong);border-radius:8px;background:#fff;padding:6px 10px;color:var(--primary);font-size:12px;font-weight:750}
|
|
64
|
-
.refresh-pill.refreshing{display:inline-flex;align-items:center;gap:7px;color:var(--primary-deep);background:var(--primary-soft)}
|
|
65
|
-
.refresh-pill.error{color:var(--danger);background:var(--danger-soft);border-color:#f4bdc9}
|
|
66
|
-
.refresh-pill .spinner{width:12px;height:12px;border-width:2px}
|
|
67
61
|
/* ---- Buttons ------------------------------------------------------ */
|
|
68
62
|
.btn{border:1px solid var(--hairline-strong);border-radius:8px;min-height:38px;padding:0 12px;background:#fff;color:var(--ink);font-size:13px;font-weight:750}
|
|
69
63
|
.btn:hover{border-color:var(--primary);color:var(--primary)}
|
|
@@ -73,12 +67,14 @@ export function adminUiHtml(): string {
|
|
|
73
67
|
.btn.icon{width:36px;padding:0;border-radius:8px;display:inline-grid;place-items:center}
|
|
74
68
|
.btn.icon svg{width:17px;height:17px;stroke:currentColor;stroke-width:2.2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
75
69
|
/* ---- Seller list -------------------------------------------------- */
|
|
76
|
-
.app-list{--seller-grid:
|
|
77
|
-
#sellerRows{display:grid;gap:
|
|
78
|
-
.app-table-head,.app-row{display:grid;grid-template-columns:var(--seller-grid);align-items:center;gap:
|
|
70
|
+
.app-list{--seller-grid:18px minmax(142px,1.1fr) 64px 48px 58px 60px minmax(128px,.82fr) minmax(190px,1.25fr) 84px 62px minmax(132px,.9fr) 76px 112px 28px;display:grid;gap:8px;padding:14px;background:var(--panel-subtle);overflow-x:auto}
|
|
71
|
+
#sellerRows{display:grid;gap:8px;width:100%;min-width:0}
|
|
72
|
+
.app-table-head,.app-row{display:grid;grid-template-columns:var(--seller-grid);align-items:center;gap:10px;width:100%;min-width:1392px;padding:0 16px}
|
|
79
73
|
.app-table-head{min-height:34px;color:var(--muted);font-size:var(--label-fs);font-weight:var(--label-weight);text-transform:uppercase;letter-spacing:var(--label-spacing)}
|
|
80
|
-
.app-row{border:1px solid var(--hairline);border-radius:8px;background:#fff;min-height:
|
|
81
|
-
.app-row:hover{border-color:var(--hairline-strong);background:#fff}
|
|
74
|
+
.app-row{position:relative;border:1px solid var(--hairline);border-left-width:4px;border-radius:8px;background:#fff;min-height:66px;text-align:left}
|
|
75
|
+
.app-row:hover{border-color:var(--hairline-strong);background:#fff;box-shadow:0 8px 22px rgba(60,41,112,.06)}
|
|
76
|
+
.app-row.row-warn{border-left-color:var(--warning)}
|
|
77
|
+
.app-row.row-alert{border-left-color:var(--danger);background:#fffafb}
|
|
82
78
|
/* Step 13 v1.1: 双源 (fly + registry) 4 类行视觉. dataSource 决定
|
|
83
79
|
dataSource="fly" → 灰/中性边, "未发布" 提示
|
|
84
80
|
dataSource="registry" → 整行红边 + 软红底, "立即下线 (registry-only)" 按钮
|
|
@@ -88,15 +84,22 @@ export function adminUiHtml(): string {
|
|
|
88
84
|
.app-row.app-row-fly-only:hover{background:#f1f3f8}
|
|
89
85
|
.app-row.app-row-registry-only{border:2px solid var(--danger);background:var(--danger-soft);box-shadow:0 0 0 3px rgba(239,91,120,.08)}
|
|
90
86
|
.app-row.app-row-registry-only:hover{background:#ffe3ea}
|
|
91
|
-
.datasource-chip{display:inline-block;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;line-height:16px;
|
|
87
|
+
.datasource-chip{display:inline-block;padding:1px 8px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;line-height:16px;vertical-align:middle}
|
|
92
88
|
.datasource-chip.both{background:#e7f6ee;color:#0a8754}
|
|
93
89
|
.datasource-chip.fly{background:#e3e6ee;color:#4a5170}
|
|
94
90
|
.datasource-chip.registry{background:var(--danger-soft);color:var(--danger);border:1px solid var(--danger)}
|
|
91
|
+
.datasource-chip.published{background:#e7f6ee;color:#0a8754}
|
|
92
|
+
.datasource-chip.unpublished{background:#e3e6ee;color:#4a5170}
|
|
93
|
+
.datasource-chip.registry_only{background:var(--danger-soft);color:var(--danger);border:1px solid var(--danger)}
|
|
94
|
+
.datasource-chip.draining{background:var(--warning-soft);color:#98630a;border:1px solid #f6d99b}
|
|
95
|
+
.datasource-chip.offline{background:#f4f1fb;color:#6b6384;border:1px solid var(--hairline-strong)}
|
|
96
|
+
.datasource-chip.pending{background:var(--tokens-soft);color:#1d4ed8;border:1px solid #bfd7ff}
|
|
97
|
+
.datasource-chip.checking,.datasource-chip.unknown{background:#f4f1fb;color:#6b6384;border:1px solid var(--hairline-strong)}
|
|
95
98
|
.alert-reason{color:var(--danger);font-size:11px;line-height:1.4;font-weight:700;margin-top:4px;display:block}
|
|
96
|
-
.remove-hint-btn{margin-top:6px;background:var(--danger);color:#fff;border:0;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;
|
|
99
|
+
.remove-hint-btn{margin-top:6px;background:var(--danger);color:#fff;border:0;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;display:inline-block}
|
|
97
100
|
.remove-hint-btn:hover{background:#d63d5a}
|
|
98
|
-
.remove-hint-btn::before{content:"
|
|
99
|
-
.publish-hint-btn{margin-top:6px;background:#fff;color:var(--primary);border:1px solid var(--hairline-strong);border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;
|
|
101
|
+
.remove-hint-btn::before{content:"! ";margin-right:2px}
|
|
102
|
+
.publish-hint-btn{margin-top:6px;background:#fff;color:var(--primary);border:1px solid var(--hairline-strong);border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;display:inline-block}
|
|
100
103
|
.publish-hint-btn:hover{background:#f5f3ff}
|
|
101
104
|
/* Status dot — five spec tones (green/amber/red/blue/gray) */
|
|
102
105
|
.app-dot{width:10px;height:10px;border-radius:999px;background:#c8ced8;box-shadow:0 0 0 4px #edf1f8}
|
|
@@ -105,19 +108,40 @@ export function adminUiHtml(): string {
|
|
|
105
108
|
.app-dot.tone-red{background:var(--danger);box-shadow:0 0 0 4px rgba(239,91,120,.18)}
|
|
106
109
|
.app-dot.tone-blue{background:var(--tokens);box-shadow:0 0 0 4px rgba(37,99,235,.18)}
|
|
107
110
|
.app-dot.tone-gray{background:#8d86a3;box-shadow:0 0 0 4px rgba(141,134,163,.17)}
|
|
108
|
-
.app-name{display:
|
|
109
|
-
.seller-title{display:
|
|
111
|
+
.app-name{display:block;min-width:0}
|
|
112
|
+
.seller-title{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:6px;align-items:center;min-width:0;width:100%}
|
|
110
113
|
.app-name strong,.field-cell strong,.field-cell span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
111
|
-
.app-name strong{color:var(--ink);font-size:
|
|
112
|
-
.field-cell{display:grid;gap:
|
|
113
|
-
.field-cell strong{color:var(--ink);font-family:var(--font-mono);font-size:var(--numeric-fs);font-weight:var(--numeric-weight)}
|
|
114
|
-
.
|
|
114
|
+
.app-name strong{color:var(--ink);font-size:14px;font-weight:750}
|
|
115
|
+
.field-cell,.speed-cell{display:grid;gap:2px;min-width:0;color:var(--muted);font-size:var(--body-fs);line-height:var(--body-lh)}
|
|
116
|
+
.field-cell strong,.speed-cell strong{color:var(--ink);font-family:var(--font-mono);font-size:var(--numeric-fs);font-weight:var(--numeric-weight)}
|
|
117
|
+
.metric-label{color:var(--muted);font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
|
|
118
|
+
.metric-sub{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;line-height:15px;font-family:var(--font-mono)}
|
|
119
|
+
.metric-sub.danger{color:var(--danger);font-family:var(--font-sans);font-weight:700}
|
|
120
|
+
.op-value{color:var(--ink);font-family:var(--font-mono);font-size:var(--numeric-fs);font-weight:var(--numeric-weight)}
|
|
121
|
+
.op-chip{width:max-content;max-width:100%;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--hairline-strong);border-radius:7px;background:var(--panel-subtle);padding:3px 7px;color:var(--muted);font-family:var(--font-mono);font-size:11px;font-weight:850;line-height:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
122
|
+
.op-chip:before{content:"";width:6px;height:6px;border-radius:999px;background:currentColor;flex:0 0 auto}
|
|
123
|
+
.op-chip.tone-green{border-color:#bde9dc;background:var(--success-soft);color:#0f766e}
|
|
124
|
+
.op-chip.tone-amber{border-color:#f6d99b;background:var(--warning-soft);color:#98630a}
|
|
125
|
+
.op-chip.tone-red{border-color:#f4bdc9;background:var(--danger-soft);color:#c0264e}
|
|
126
|
+
.op-chip.tone-blue{border-color:#bfd7ff;background:var(--tokens-soft);color:#1d4ed8}
|
|
127
|
+
.op-chip.tone-gray{border-color:var(--hairline-strong);background:#f4f1fb;color:#6b6384}
|
|
128
|
+
.status-pill{width:max-content;max-width:100%;border:1px solid var(--hairline-strong);border-radius:999px;background:var(--panel-subtle);padding:2px 8px;color:var(--muted);font-family:var(--font-mono);font-size:11px;font-weight:800;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
129
|
+
.status-pill.tone-green{border-color:#bde9dc;background:var(--success-soft);color:#0f766e}
|
|
130
|
+
.status-pill.tone-amber{border-color:#f6d99b;background:var(--warning-soft);color:#98630a}
|
|
131
|
+
.status-pill.tone-red{border-color:#f4bdc9;background:var(--danger-soft);color:#c0264e}
|
|
132
|
+
.status-pill.tone-blue{border-color:#bfd7ff;background:var(--tokens-soft);color:#1d4ed8}
|
|
133
|
+
.mobile-metrics{display:none}
|
|
134
|
+
.balance-line{display:flex;align-items:center;gap:6px;min-width:0}
|
|
135
|
+
.balance-line .op-chip{max-width:100%}
|
|
115
136
|
.muted-value{color:var(--faint)}
|
|
116
137
|
.recharge-btn,.detail-btn{width:30px;height:30px;border:1px solid var(--hairline-strong);border-radius:8px;background:#fff;color:var(--primary);display:inline-grid;place-items:center;text-decoration:none;font-weight:900}
|
|
117
138
|
.recharge-btn:hover,.detail-btn:hover{border-color:var(--primary);background:var(--primary-soft)}
|
|
118
139
|
.row-actions{display:flex;gap:8px;justify-content:flex-end}
|
|
119
140
|
.spec-tip{border:1px solid var(--hairline-strong);border-radius:7px;background:#fff;color:var(--primary);width:24px;height:24px;padding:0;display:inline-grid;place-items:center}
|
|
120
141
|
.spec-tip svg{width:14px;height:14px;stroke:currentColor;stroke-width:2.2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
142
|
+
@media(min-width:901px){
|
|
143
|
+
.app-row>.field-cell>.metric-label,.app-row>.speed-cell>.metric-label{display:none}
|
|
144
|
+
}
|
|
121
145
|
/* ---- Entry cards (Bootstrap summary) ----------------------------- */
|
|
122
146
|
.bootstrap-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;padding:16px}
|
|
123
147
|
.entry-card{border:1px solid var(--hairline-strong);border-radius:12px;background:var(--panel);padding:16px;box-shadow:var(--shadow);display:grid;gap:7px}
|
|
@@ -132,9 +156,12 @@ export function adminUiHtml(): string {
|
|
|
132
156
|
.modal{position:fixed;inset:0;background:rgba(32,26,56,.28);z-index:40;display:none}
|
|
133
157
|
.modal.open{display:grid}
|
|
134
158
|
.modal-shell{background:var(--canvas);min-height:100dvh;display:grid;grid-template-rows:auto 1fr}
|
|
135
|
-
.modal-head{height:64px;display:flex;align-items:center;
|
|
159
|
+
.modal-head{height:64px;display:flex;align-items:center;gap:16px;padding:0 24px;border-bottom:1px solid var(--hairline);background:var(--panel)}
|
|
136
160
|
.modal-title{display:flex;align-items:center;gap:10px;color:var(--ink);font-size:20px;line-height:28px;font-weight:800}
|
|
137
|
-
.modal-actions{display:flex;gap:8px;align-items:center}
|
|
161
|
+
.modal-actions{display:flex;gap:8px;align-items:center;margin-left:auto}
|
|
162
|
+
.modal-close{width:44px;height:44px;border:1px solid var(--hairline-strong);border-radius:10px;background:#fff;color:var(--muted);display:inline-grid;place-items:center;padding:0}
|
|
163
|
+
.modal-close:hover{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-deep)}
|
|
164
|
+
.modal-close svg{width:18px;height:18px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
138
165
|
.modal-body{padding:20px 24px 36px;display:grid;gap:16px;overflow:auto}
|
|
139
166
|
.detail-grid{display:grid;grid-template-columns:minmax(260px,.72fr) minmax(0,1fr);gap:16px}
|
|
140
167
|
.card{border:1px solid var(--hairline);border-radius:12px;background:#fff;padding:16px;box-shadow:var(--shadow)}
|
|
@@ -164,6 +191,8 @@ export function adminUiHtml(): string {
|
|
|
164
191
|
.field input,.field select,.readonly-value{min-height:40px;border:1px solid var(--hairline-strong);border-radius:8px;padding:0 10px;background:#fff;color:var(--ink)}
|
|
165
192
|
.field input,.field select{font:inherit}
|
|
166
193
|
.field input:disabled,.field select:disabled{background:#f3f0fb;color:#958daa}
|
|
194
|
+
.model-toggle{width:18px;height:18px;accent-color:var(--primary)}
|
|
195
|
+
.model-toggle:disabled{cursor:default}
|
|
167
196
|
.readonly-value{display:flex;align-items:center;background:var(--panel-subtle);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
168
197
|
.field input[readonly]{background:var(--panel-subtle);color:var(--muted)}
|
|
169
198
|
/* ---- Tables (audit) ---------------------------------------------- */
|
|
@@ -197,15 +226,21 @@ export function adminUiHtml(): string {
|
|
|
197
226
|
.progress-toggle{color:var(--primary);font-size:var(--label-fs);font-weight:var(--label-weight);text-transform:uppercase;letter-spacing:var(--label-spacing)}
|
|
198
227
|
.progress-log{margin:4px 0 0;max-height:150px;overflow:auto;border:1px solid var(--hairline);border-radius:8px;background:var(--ink);color:#f3f0ff;padding:9px;font-size:11px;white-space:pre-wrap;font-family:var(--font-mono)}
|
|
199
228
|
.spinner{width:19px;height:19px;border:2px solid rgba(124,61,240,.18);border-top-color:var(--primary);border-radius:999px;animation:spin .75s linear infinite}
|
|
229
|
+
.inline-spinner{width:13px;height:13px;border:2px solid rgba(124,61,240,.18);border-top-color:var(--primary);border-radius:999px;animation:spin .75s linear infinite;display:inline-block;vertical-align:middle}
|
|
200
230
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
201
231
|
.hidden{display:none!important}
|
|
202
232
|
/* ---- Mobile ------------------------------------------------------- */
|
|
203
233
|
@media(max-width:900px){
|
|
204
234
|
.topnav{padding:0 20px}
|
|
205
|
-
.content{width:min(100% - 28px,
|
|
235
|
+
.content{width:min(100% - 28px,1560px)}
|
|
206
236
|
.app-table-head{display:none}
|
|
207
|
-
.app-row{grid-template-columns:18px minmax(0,1fr) auto;gap:12px;padding:14px;min-height:44px}
|
|
237
|
+
.app-row{grid-template-columns:18px minmax(0,1fr) auto;gap:12px;padding:14px;min-height:44px;align-items:flex-start}
|
|
208
238
|
.app-row .field-cell,.app-row .speed-cell{display:none}
|
|
239
|
+
.mobile-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:4px}
|
|
240
|
+
.mini-metric{border:1px solid var(--hairline);border-radius:8px;background:#fff;padding:8px;display:grid;gap:3px;min-width:0}
|
|
241
|
+
.mini-metric label{color:var(--muted);font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
|
|
242
|
+
.mini-metric strong{color:var(--ink);font-family:var(--font-mono);font-size:12px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
243
|
+
.mini-metric span{color:var(--faint);font-size:11px;line-height:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
209
244
|
.detail-grid,.form-grid,.section-grid,.bootstrap-grid{grid-template-columns:1fr}
|
|
210
245
|
.field.full{grid-column:auto}
|
|
211
246
|
.modal-head{padding:0 16px}
|
|
@@ -214,22 +249,22 @@ export function adminUiHtml(): string {
|
|
|
214
249
|
</style>
|
|
215
250
|
</head>
|
|
216
251
|
<body>
|
|
217
|
-
<nav class="topnav"><div class="logo">TOKENBUDDY ADMIN</div
|
|
252
|
+
<nav class="topnav"><div class="logo">TOKENBUDDY ADMIN</div></nav>
|
|
218
253
|
<main class="content">
|
|
219
254
|
<section id="page-sellers" class="page active">
|
|
220
255
|
<div class="panel">
|
|
221
256
|
<div class="panel-head">
|
|
222
257
|
<div>
|
|
223
258
|
<h1 class="title">Seller fleet</h1>
|
|
224
|
-
<div class="list-status"><span id="sellerRefreshState" class="refresh-pill">Starting</span></div>
|
|
225
259
|
</div>
|
|
226
260
|
<button id="createSeller" class="btn primary">Create Seller</button>
|
|
227
261
|
</div>
|
|
228
262
|
<div class="app-list">
|
|
229
263
|
<div class="app-table-head" role="row">
|
|
230
|
-
<span></span><span>Seller</span><span>
|
|
231
|
-
<span>
|
|
232
|
-
<span>
|
|
264
|
+
<span></span><span>Seller</span><span>Pub</span><span>Region</span>
|
|
265
|
+
<span>Models</span><span>Conn</span><span>CPU/RAM</span>
|
|
266
|
+
<span>Upstream</span><span>Status</span><span>Disc</span><span>Balance</span>
|
|
267
|
+
<span>TTFT</span><span>Next</span><span></span>
|
|
233
268
|
</div>
|
|
234
269
|
<div id="sellerRows">
|
|
235
270
|
<div class="loading-row" role="status" aria-label="Loading sellers">
|
|
@@ -239,22 +274,6 @@ export function adminUiHtml(): string {
|
|
|
239
274
|
</div>
|
|
240
275
|
</div>
|
|
241
276
|
</section>
|
|
242
|
-
<section id="page-releases" class="page">
|
|
243
|
-
<div class="bootstrap-card">
|
|
244
|
-
<div class="panel-head">
|
|
245
|
-
<h1 class="title">Release Requests</h1>
|
|
246
|
-
<div class="modal-actions">
|
|
247
|
-
<button id="refreshReleases" class="btn">Refresh</button>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
<p class="hint" style="color:var(--muted);font-size:12px;margin:0 0 12px;">
|
|
251
|
-
Pending and historical release requests you have submitted to the wallet-bootstrap
|
|
252
|
-
registry. Force-publish is available to the platform super-admin via the registry
|
|
253
|
-
admin web; vendors do not publish their own releases.
|
|
254
|
-
</p>
|
|
255
|
-
<div id="releasesGrid" class="bootstrap-grid"></div>
|
|
256
|
-
</div>
|
|
257
|
-
</section>
|
|
258
277
|
</main>
|
|
259
278
|
<section id="detailModal" class="modal">
|
|
260
279
|
<div class="modal-shell">
|
|
@@ -273,12 +292,16 @@ export function adminUiHtml(): string {
|
|
|
273
292
|
<button class="btn" data-status-action="offline">Offline</button>
|
|
274
293
|
<button class="btn" data-status-action="activate">Activate</button>
|
|
275
294
|
<button id="editDetail" class="btn primary">Edit config</button>
|
|
276
|
-
<button id="closeDetail" class="btn">Close</button>
|
|
277
295
|
</div>
|
|
296
|
+
<button id="closeDetail" class="modal-close" title="Close detail" aria-label="Close detail">
|
|
297
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
298
|
+
<path d="M6 6l12 12"></path><path d="M18 6L6 18"></path>
|
|
299
|
+
</svg>
|
|
300
|
+
</button>
|
|
278
301
|
</header>
|
|
279
302
|
<div class="modal-body">
|
|
280
303
|
<div id="detailStatus" class="status-line hidden"></div>
|
|
281
|
-
<div class="detail-grid">
|
|
304
|
+
<div id="detailGrid" class="detail-grid hidden">
|
|
282
305
|
<div class="card"><h2>Seller configuration</h2><div id="configFields" class="form-grid"></div></div>
|
|
283
306
|
<div class="card"><h2>Models</h2><div id="modelsTable"></div></div>
|
|
284
307
|
</div>
|
|
@@ -348,108 +371,210 @@ async function api(path, options={}){
|
|
|
348
371
|
}
|
|
349
372
|
}
|
|
350
373
|
function uiErrorMessage(err){ const message = err instanceof Error ? err.message : String(err || ""); if (/failed to fetch|networkerror|load failed|fetch failed/i.test(message)) return "Admin UI connection lost. Restart tb-admin ui and reload this page."; if (/operator_auth_required/i.test(message)) return "Admin profile authentication failed. Check the configured operator token."; if (/HTTP Error 401/i.test(message)) return "Authentication failed while loading admin data. Check the local admin profile."; return message || "Request failed."; }
|
|
351
|
-
const
|
|
352
|
-
|
|
374
|
+
const sellerStatusRefreshIntervalMs = 30000;
|
|
375
|
+
const sellerDetailConcurrency = 2;
|
|
376
|
+
let currentDetail = null; let sellerRowsCache = []; let editing = false; let deleteReady = false; let sellerInventoryInFlight = false; let sellerDetailQueue = []; let sellerDetailQueueKeys = new Set(); let sellerDetailInFlight = new Set(); let sellerDetailTimers = new Map(); let sellerClockTimer = null; let sellerRefreshLoaded = false; let sellerRefreshStage = "Starting"; let sellerRefreshError = ""; let createJobTimer = null; let currentCreateJob = null; let expandedProgressSteps = new Set(); let createAppSuffix = "";
|
|
353
377
|
const esc = value => String(value ?? "").replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c]));
|
|
354
378
|
const missing = value => '<span class="muted-value">'+esc(value)+'</span>';
|
|
355
379
|
const dash = () => '<span class="muted-value">'+window.__tbFmt.UNKNOWN_VALUE+'</span>';
|
|
356
|
-
const infoIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h7"></path><path d="M15 7h5"></path><path d="M13 5v4"></path><path d="M4 12h3"></path><path d="M11 12h9"></path><path d="M9 10v4"></path><path d="M4 17h10"></path><path d="M18 17h2"></path><path d="M16 15v4"></path></svg>';
|
|
357
380
|
const balanceProbeTemplates = ["openrouter","deepseek","stepfun","siliconflow","novita","newapi_generic","usage_generic","none","auto"];
|
|
358
381
|
const paymentMethods = ["clawtip","mock"];
|
|
359
382
|
const loadingSpinner = label => '<div class="loading-row" role="status" aria-label="'+esc(label)+'"><span class="spinner" aria-hidden="true"></span></div>';
|
|
360
|
-
|
|
361
|
-
|
|
383
|
+
async function loadSellers(options={}){
|
|
384
|
+
if (sellerInventoryInFlight) return;
|
|
385
|
+
const initial = Boolean(options.initial);
|
|
386
|
+
sellerInventoryInFlight = true;
|
|
387
|
+
sellerRefreshLoaded = false;
|
|
388
|
+
sellerRefreshError = "";
|
|
389
|
+
resetSellerDetailQueue();
|
|
390
|
+
if (initial) document.getElementById("sellerRows").innerHTML = loadingSpinner("Loading registry");
|
|
391
|
+
try {
|
|
392
|
+
sellerRefreshStage = "Loading registry";
|
|
393
|
+
updateSellerRefreshMeta();
|
|
394
|
+
const registryRows = await api("/api/sellers/registry");
|
|
395
|
+
renderSellerRows(registryRows);
|
|
396
|
+
sellerRefreshStage = "Loading Fly inventory";
|
|
397
|
+
updateSellerRefreshMeta();
|
|
398
|
+
const inventoryRows = await api("/api/sellers/inventory");
|
|
399
|
+
renderSellerRows(inventoryRows);
|
|
400
|
+
sellerRefreshLoaded = true;
|
|
401
|
+
sellerRefreshStage = "Refreshing details";
|
|
402
|
+
enqueueSellerDetails(inventoryRows);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
sellerRefreshError = err.message || "Refresh failed";
|
|
405
|
+
sellerRefreshStage = "Load failed";
|
|
406
|
+
if (initial) document.getElementById("sellerRows").innerHTML = '<div class="status-line">'+esc(sellerRefreshError)+'</div>';
|
|
407
|
+
} finally {
|
|
408
|
+
sellerInventoryInFlight = false;
|
|
409
|
+
updateSellerRefreshMeta();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function statusRefreshRow(row){ return { id:row.id, name:row.name, app:row.app, url:row.url, registryStatus:row.registryStatus, nodeStatus:row.nodeStatus, region:row.region, upstreamDomain:row.upstreamDomain, upstreamStatus:row.upstreamStatus, upstreamBalanceUsdMicros:row.upstreamBalanceUsdMicros, upstreamBalanceCurrency:row.upstreamBalanceCurrency, upstreamBalanceSource:row.upstreamBalanceSource, upstreamBalanceFetchedAt:row.upstreamBalanceFetchedAt, upstreamBalanceError:row.upstreamBalanceError, upstreamRechargeUrl:row.upstreamRechargeUrl, discountRatio:row.discountRatio, capacityUsed:row.capacityUsed, capacityLimit:row.capacityLimit, resourceCpuPercent:row.resourceCpuPercent, resourceMemoryPercent:row.resourceMemoryPercent, resourceMemoryRssMb:row.resourceMemoryRssMb, resourceMemoryLimitMb:row.resourceMemoryLimitMb, ttftMs:row.ttftMs, avgInferenceMs:row.avgInferenceMs, lastInferenceMs:row.lastInferenceMs, avgTokensPerSecond:row.avgTokensPerSecond, lastTokensPerSecond:row.lastTokensPerSecond, latencySamples:row.latencySamples, lastSwitchAt:row.lastSwitchAt, modelsCount:row.modelsCount, specs:row.specs, error:row.error, dataSource:row.dataSource, registryAlert:row.registryAlert, alertReason:row.alertReason, publishHint:row.publishHint, removeHint:row.removeHint, flyApp:row.flyApp, publishStatus:row.publishStatus, detailStatus:row.detailStatus, detailUpdatedAt:row.detailUpdatedAt, detailNextRefreshAt:row.detailNextRefreshAt }; }
|
|
362
413
|
function renderSellerRows(rows){ sellerRowsCache = rows; document.getElementById("sellerRows").innerHTML = rows.map(row => sellerRow(row)).join(""); document.querySelectorAll("[data-detail]").forEach(btn => btn.onclick = () => openDetail(btn.dataset.detail)); }
|
|
363
|
-
function startSellerRefresh(){ loadSellers({ initial:true }); sellerClockTimer = setInterval(() => updateSellerRefreshMeta(
|
|
364
|
-
function
|
|
365
|
-
function
|
|
414
|
+
function startSellerRefresh(){ loadSellers({ initial:true }); sellerClockTimer = setInterval(() => { updateSellerRefreshMeta(); updateSellerRowRefreshCountdowns(); }, 1000); window.addEventListener("beforeunload", () => { resetSellerDetailQueue(); clearInterval(sellerClockTimer); }); }
|
|
415
|
+
function sellerRowKey(row){ return String(row.id || row.app || row.url || ""); }
|
|
416
|
+
function resetSellerDetailQueue(){ sellerDetailQueue = []; sellerDetailQueueKeys.clear(); sellerDetailInFlight.clear(); sellerDetailTimers.forEach(timer => clearTimeout(timer)); sellerDetailTimers.clear(); }
|
|
417
|
+
function enqueueSellerDetails(rows){
|
|
418
|
+
rows.forEach(row => {
|
|
419
|
+
if (!shouldRefreshSellerDetail(row)) return;
|
|
420
|
+
const key = sellerRowKey(row);
|
|
421
|
+
if (!key || sellerDetailQueueKeys.has(key) || sellerDetailInFlight.has(key)) return;
|
|
422
|
+
sellerDetailQueue.push(key);
|
|
423
|
+
sellerDetailQueueKeys.add(key);
|
|
424
|
+
patchSellerRow(key, { detailStatus:"queued", detailNextRefreshAt:undefined });
|
|
425
|
+
});
|
|
426
|
+
pumpSellerDetailQueue();
|
|
427
|
+
updateSellerRefreshMeta();
|
|
428
|
+
}
|
|
429
|
+
function shouldRefreshSellerDetail(row){ return row && row.detailStatus !== "skipped" && row.publishStatus !== "registry_only" && row.dataSource !== "registry"; }
|
|
430
|
+
function patchSellerRow(key, patch){ let changed = false; sellerRowsCache = sellerRowsCache.map(row => { if (sellerRowKey(row) !== key) return row; changed = true; return { ...row, ...patch }; }); if (changed) renderSellerRows(sellerRowsCache); }
|
|
431
|
+
function patchSellerRegistryStatus(id, status){ let changed = false; sellerRowsCache = sellerRowsCache.map(row => { if (sellerRowKey(row) !== id && row.id !== id && row.name !== id && row.app !== id) return row; changed = true; return { ...row, registryStatus:status }; }); if (changed) renderSellerRows(sellerRowsCache); }
|
|
432
|
+
function mergeSellerRowUpdate(updated){ const key = sellerRowKey(updated); if (!key) return; let changed = false; sellerRowsCache = sellerRowsCache.map(row => { if (sellerRowKey(row) !== key) return row; changed = true; return { ...row, ...updated }; }); if (changed) renderSellerRows(sellerRowsCache); }
|
|
433
|
+
function latestSellerRow(key){ return sellerRowsCache.find(row => sellerRowKey(row) === key); }
|
|
434
|
+
function scheduleSellerDetailRefresh(row){
|
|
435
|
+
if (!shouldRefreshSellerDetail(row)) return;
|
|
436
|
+
const key = sellerRowKey(row);
|
|
437
|
+
if (!key) return;
|
|
438
|
+
const existing = sellerDetailTimers.get(key);
|
|
439
|
+
if (existing) clearTimeout(existing);
|
|
440
|
+
const nextRefreshAt = new Date(Date.now() + sellerStatusRefreshIntervalMs).toISOString();
|
|
441
|
+
const timer = setTimeout(() => {
|
|
442
|
+
sellerDetailTimers.delete(key);
|
|
443
|
+
const latest = latestSellerRow(key);
|
|
444
|
+
if (!latest) return;
|
|
445
|
+
patchSellerRow(key, { detailStatus:"stale", detailNextRefreshAt:undefined });
|
|
446
|
+
enqueueSellerDetails([latest]);
|
|
447
|
+
}, sellerStatusRefreshIntervalMs);
|
|
448
|
+
sellerDetailTimers.set(key, timer);
|
|
449
|
+
patchSellerRow(key, { detailNextRefreshAt:nextRefreshAt });
|
|
450
|
+
}
|
|
451
|
+
async function pumpSellerDetailQueue(){
|
|
452
|
+
while (sellerDetailInFlight.size < sellerDetailConcurrency && sellerDetailQueue.length > 0) {
|
|
453
|
+
const key = sellerDetailQueue.shift();
|
|
454
|
+
sellerDetailQueueKeys.delete(key);
|
|
455
|
+
const row = latestSellerRow(key);
|
|
456
|
+
if (!row || !shouldRefreshSellerDetail(row)) continue;
|
|
457
|
+
sellerDetailInFlight.add(key);
|
|
458
|
+
patchSellerRow(key, { detailStatus:"loading", detailNextRefreshAt:undefined });
|
|
459
|
+
updateSellerRefreshMeta();
|
|
460
|
+
api("/api/sellers/status", { method:"POST", body: JSON.stringify({ rows:[statusRefreshRow(row)] }) })
|
|
461
|
+
.then(rows => {
|
|
462
|
+
const updated = Array.isArray(rows) ? rows[0] : null;
|
|
463
|
+
if (updated) {
|
|
464
|
+
mergeSellerRowUpdate(updated);
|
|
465
|
+
scheduleSellerDetailRefresh(updated);
|
|
466
|
+
sellerRefreshError = "";
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
.catch(err => {
|
|
470
|
+
sellerRefreshError = err.message || "Status refresh failed";
|
|
471
|
+
patchSellerRow(key, { detailStatus:"error", detailUpdatedAt:new Date().toISOString(), error:sellerRefreshError });
|
|
472
|
+
const latest = latestSellerRow(key);
|
|
473
|
+
if (latest) scheduleSellerDetailRefresh(latest);
|
|
474
|
+
})
|
|
475
|
+
.finally(() => {
|
|
476
|
+
sellerDetailInFlight.delete(key);
|
|
477
|
+
updateSellerRefreshMeta();
|
|
478
|
+
pumpSellerDetailQueue();
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function updateSellerRefreshMeta(){ const state = document.getElementById("sellerRefreshState"); if (!state) return; const busy = sellerInventoryInFlight || sellerDetailInFlight.size > 0 || sellerDetailQueue.length > 0; state.classList.toggle("refreshing", Boolean(busy)); state.classList.toggle("error", Boolean(sellerRefreshError)); if (sellerRefreshError) { state.textContent = sellerRefreshError; return; } if (busy) { state.innerHTML = '<span class="spinner" aria-hidden="true"></span><span>'+esc(sellerRefreshStage || "Refreshing details")+'</span>'; return; } state.textContent = sellerRefreshLoaded ? "Inventory loaded · row timers active" : (sellerRefreshStage || "Loading inventory"); }
|
|
483
|
+
function updateSellerRowRefreshCountdowns(){ if (!sellerRefreshLoaded || sellerRowsCache.length === 0) return; renderSellerRows(sellerRowsCache); }
|
|
484
|
+
function publishStatusLabel(status){ if (status === "published") return "已发布"; if (status === "unpublished") return "未发布"; if (status === "registry_only") return "Registry-only"; if (status === "checking") return "发布待确认"; if (status === "draining") return "下线中"; if (status === "offline") return "已下线"; if (status === "pending") return "待发布"; return "发布未知"; }
|
|
485
|
+
function visiblePublishStatus(row, relation){ const registry = row.registryStatus; if ((relation === "published" || relation === "checking") && ["draining","offline","pending"].includes(registry)) return registry; return relation; }
|
|
486
|
+
function registryStatusDisplay(status){ const value = String(status || "unknown").toLowerCase(); if (["active","draining","offline","pending"].includes(value)) return value; return window.__tbFmt.UNKNOWN_VALUE; }
|
|
487
|
+
function detailStatusLabel(status){ if (status === "queued") return "detail queued"; if (status === "loading") return "detail loading"; if (status === "fresh") return "detail fresh"; if (status === "stale") return "detail stale"; if (status === "error") return "detail error"; if (status === "skipped") return "detail skipped"; return "detail pending"; }
|
|
488
|
+
function detailCountdown(row){ if (!row.detailNextRefreshAt) return ""; const ms = new Date(row.detailNextRefreshAt).getTime() - Date.now(); if (!Number.isFinite(ms)) return ""; if (ms <= 0) return "due now"; const seconds = Math.ceil(ms / 1000); return seconds >= 60 ? Math.ceil(seconds / 60) + "m" : seconds + "s"; }
|
|
489
|
+
function sellerHostMatches(row, domain){ const normalized = String(domain || "").toLowerCase(); if (!normalized) return false; const rowHost = hostName(row.url); const appHost = row.app ? String(row.app).toLowerCase() + ".fly.dev" : ""; const flyHost = row.flyApp?.name ? String(row.flyApp.name).toLowerCase() + ".fly.dev" : ""; return normalized === rowHost || normalized === appHost || normalized === flyHost; }
|
|
490
|
+
function usagePercent(value){ const n = Number(value); if (!Number.isFinite(n)) return "—"; return (Math.round(Math.max(0, Math.min(100, n)) * 10) / 10).toString().replace(/\.0$/, "") + "%"; }
|
|
491
|
+
function percentTone(value){ const n = Number(value); if (!Number.isFinite(n)) return "gray"; if (n >= 90) return "red"; if (n >= 75) return "amber"; return "green"; }
|
|
492
|
+
function worstTone(...tones){ if (tones.includes("red")) return "red"; if (tones.includes("amber")) return "amber"; if (tones.includes("blue")) return "blue"; if (tones.includes("green")) return "green"; return "gray"; }
|
|
493
|
+
function capacityTone(used, limit){ const u = Number(used); const l = Number(limit); if (!Number.isFinite(u) || !Number.isFinite(l) || l <= 0) return "gray"; const ratio = u / l; if (ratio >= 1) return "red"; if (ratio >= .75) return "amber"; return "green"; }
|
|
494
|
+
function latencyTone(ms){ const n = Number(ms); if (!Number.isFinite(n)) return "gray"; if (n >= 5000) return "red"; if (n >= 2000) return "amber"; return "green"; }
|
|
495
|
+
function upstreamStatusTone(status){ const value = String(status || "unknown").toLowerCase(); if (value === "healthy") return "green"; if (value === "degraded") return "amber"; if (value === "unhealthy") return "red"; return "gray"; }
|
|
496
|
+
function renderOpMetric(text, tone, title){ const value = text || "—"; if (tone === "green") return '<strong class="op-value"'+(title ? ' title="'+esc(title)+'"' : '')+'>'+esc(value)+'</strong>'; return '<span class="op-chip tone-'+esc(tone || "gray")+'"'+(title ? ' title="'+esc(title)+'"' : '')+'>'+esc(value)+'</span>'; }
|
|
497
|
+
function renderStatusMetric(status, title){ const tone = upstreamStatusTone(status); const value = tone === "gray" ? "-" : window.__tbFmt.normalizeStatusLabel(status); return '<span class="op-chip tone-'+esc(tone)+'"'+(title ? ' title="'+esc(title)+'"' : '')+'>'+esc(value)+'</span>'; }
|
|
366
498
|
function sellerRow(row){
|
|
367
499
|
const fmt = window.__tbFmt;
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
// 兜底当 "both" (老 UI 看到的所有行, 默认都按已发布处理).
|
|
500
|
+
// publishStatus 决定用户可见发布态; dataSource 只作为排障线索.
|
|
501
|
+
// checking/unknown 必须保持中性, 不能提前显示成未发布或事故.
|
|
371
502
|
const ds = row.dataSource || "both";
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
503
|
+
const publishRelation = row.publishStatus || (ds === "registry" ? "unknown" : ds === "fly" ? "unpublished" : "published");
|
|
504
|
+
const publish = visiblePublishStatus(row, publishRelation);
|
|
505
|
+
const publishLabel = publishStatusLabel(publish);
|
|
506
|
+
const dsChip = '<span class="datasource-chip '+esc(publish)+'" title="'+esc('Publish: ' + publish + ' · registry: ' + row.registryStatus + ' · source: ' + ds)+'">'+esc(publishLabel)+'</span>';
|
|
507
|
+
const cpuValue = row.specs?.cpuCores ?? row.specs?.cpus ?? row.specs?.cpu;
|
|
508
|
+
const cpuText = cpuValue === undefined || cpuValue === null || cpuValue === "" ? "—" : String(cpuValue) + " vCPU";
|
|
509
|
+
const memoryText = row.specs?.memoryMb ? (row.specs.memoryMb >= 1024 ? (Math.round(row.specs.memoryMb / 102.4) / 10) + " GB" : row.specs.memoryMb + " MB") : (row.specs?.memoryGb ? row.specs.memoryGb + " GB" : "—");
|
|
510
|
+
const cpuUsageText = usagePercent(row.resourceCpuPercent);
|
|
511
|
+
const memoryUsageText = usagePercent(row.resourceMemoryPercent);
|
|
512
|
+
const hasUsage = cpuUsageText !== "—" || memoryUsageText !== "—";
|
|
513
|
+
const resourcePrimary = (cpuUsageText === "—" ? "-" : cpuUsageText) + "/" + (memoryUsageText === "—" ? "-" : memoryUsageText);
|
|
514
|
+
const resourceTone = hasUsage ? worstTone(percentTone(row.resourceCpuPercent), percentTone(row.resourceMemoryPercent)) : "gray";
|
|
515
|
+
const machineText = row.specs?.machines ? ((row.specs.runningMachines ?? row.specs.machines) + "/" + row.specs.machines + " running") : (row.flyApp?.status ? "Fly " + row.flyApp.status : "Fly —");
|
|
516
|
+
const volumeText = row.specs?.volumeGb ? " · vol " + row.specs.volumeGb + " GB" : "";
|
|
517
|
+
const usageDetail = (hasUsage ? "usage CPU " + cpuUsageText + " · memory " + memoryUsageText : "usage unavailable") + (row.resourceMemoryRssMb ? " (" + row.resourceMemoryRssMb + " MB RSS" + (row.resourceMemoryLimitMb ? " / " + row.resourceMemoryLimitMb + " MB" : "") + ")" : "") + " · spec " + cpuText + " / " + memoryText + " · " + machineText + volumeText;
|
|
380
518
|
const ttftText = fmt.formatDuration(row.ttftMs);
|
|
381
|
-
const ttft = "TTFT: " + (ttftText === fmt.UNKNOWN_VALUE ? "—" : ttftText);
|
|
382
519
|
const avgText = fmt.formatSpeed(row.avgTokensPerSecond);
|
|
383
|
-
const avgSpeed = avgText === fmt.UNKNOWN_VALUE ?
|
|
520
|
+
const avgSpeed = avgText === fmt.UNKNOWN_VALUE ? "Tok/s —" : "Tok/s " + esc(avgText.replace(/ tok\\/s$/i, ""));
|
|
384
521
|
const capacity = fmt.formatSellerCapacity(row.capacityUsed, row.capacityLimit);
|
|
522
|
+
const capacityText = capacity === fmt.UNKNOWN_VALUE ? "—" : capacity;
|
|
523
|
+
const connTone = capacityTone(row.capacityUsed, row.capacityLimit);
|
|
385
524
|
const disc = fmt.formatDiscountRatio(row.discountRatio);
|
|
386
525
|
const balanceRaw = row.upstreamBalanceUsdMicros;
|
|
387
|
-
const
|
|
388
|
-
const
|
|
526
|
+
const hasBalance = balanceRaw !== undefined && balanceRaw !== null;
|
|
527
|
+
const lowBalance = hasBalance && Number(balanceRaw) < 10000000;
|
|
528
|
+
const balanceTone = hasBalance ? (lowBalance ? "red" : "green") : "gray";
|
|
529
|
+
const balanceDisplay = hasBalance ? fmt.formatBalanceAmount(balanceRaw, row.upstreamBalanceCurrency || "USD") : "—";
|
|
530
|
+
const balanceMissingTitle = row.upstreamBalanceError || "Balance probe has not returned data";
|
|
531
|
+
const balanceText = hasBalance ? renderOpMetric(balanceDisplay, balanceTone, lowBalance ? "Upstream balance below 10" : "Upstream balance ok") : '<span class="muted-value" title="'+esc(balanceMissingTitle)+'">-</span>';
|
|
532
|
+
const balancePlain = hasBalance ? fmt.formatBalanceAmount(balanceRaw, row.upstreamBalanceCurrency || "USD") : "-";
|
|
533
|
+
const balanceLabel = hasBalance ? (lowBalance ? "low balance" : "ok") : "-";
|
|
389
534
|
// Step 13 v1.1: 绿点 (status + tone) 仍按 nodeStatus 决定 (probeManifest
|
|
390
535
|
// 200 → active 绿点; 失败 → unknown 灰). registryStatus 单独 tooltip.
|
|
391
536
|
const status = fmt.formatSellerStatus(row.nodeStatus);
|
|
392
537
|
const tone = fmt.sellerStatusTone(row.nodeStatus);
|
|
393
|
-
const statusTip = "registry: " + esc(
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
538
|
+
const statusTip = "registry: " + esc(registryStatusDisplay(row.registryStatus)) + " · upstream: " + esc(fmt.normalizeStatusLabel(row.upstreamStatus)) + " · publish: " + esc(publish) + " · detail: " + esc(row.detailStatus || "pending");
|
|
539
|
+
const upstreamDisplay = sellerHostMatches(row, row.upstreamDomain) ? "—" : (row.upstreamDomain || "—");
|
|
540
|
+
const upstreamTone = upstreamStatusTone(row.upstreamStatus);
|
|
541
|
+
const discountText = disc === fmt.UNKNOWN_VALUE ? "—" : disc;
|
|
542
|
+
const regionText = row.region || row.specs?.region || "—";
|
|
543
|
+
const modelsText = row.modelsCount === undefined || row.modelsCount === null ? "—" : fmt.formatCount(row.modelsCount);
|
|
544
|
+
const ttftTone = latencyTone(row.ttftMs);
|
|
545
|
+
const ttftDisplay = ttftText === fmt.UNKNOWN_VALUE ? "—" : ttftText;
|
|
546
|
+
const refreshCountdown = detailCountdown(row);
|
|
547
|
+
const refreshLoading = row.detailStatus === "queued" || row.detailStatus === "loading";
|
|
548
|
+
const refreshText = refreshLoading ? ""
|
|
549
|
+
: row.detailStatus === "skipped" ? "-"
|
|
550
|
+
: row.detailUpdatedAt ? fmt.formatTimeCompact(row.detailUpdatedAt) + " / " + (refreshCountdown || "—")
|
|
551
|
+
: "-";
|
|
552
|
+
const rowRiskTone = publishRelation === "registry_only" || lowBalance || upstreamTone === "red" || connTone === "red" || resourceTone === "red" || ttftTone === "red" || tone === "red" ? "alert"
|
|
553
|
+
: publishRelation === "unpublished" || publish === "draining" || publish === "offline" || upstreamTone === "amber" || connTone === "amber" || resourceTone === "amber" || ttftTone === "amber" || tone === "amber" ? "warn"
|
|
554
|
+
: "";
|
|
555
|
+
const rowClass = publishRelation === "registry_only" ? "app-row app-row-registry-only"
|
|
556
|
+
: publishRelation === "unpublished" ? "app-row app-row-fly-only"
|
|
557
|
+
: "app-row" + (rowRiskTone ? " row-" + rowRiskTone : "");
|
|
558
|
+
const publishExtras = publishRelation === "registry_only" ? '<strong class="registry-incident" title="'+esc(statusTip)+'">严重事故</strong>' +
|
|
408
559
|
(row.alertReason ? '<span class="alert-reason">'+esc(row.alertReason)+'</span>' : '') +
|
|
409
|
-
(row.removeHint ? '<
|
|
410
|
-
'</span>';
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
const data = await api("/api/vendor/release-requests");
|
|
429
|
-
const rows = (data.releaseRequests || []);
|
|
430
|
-
if (rows.length === 0) {
|
|
431
|
-
document.getElementById("releasesGrid").innerHTML = '<div class="status-line">No release requests yet. Submit one with <code>tb-admin vendor-bootstrap stage</code> + <code>release submit</code>.</div>';
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
const table = '<table style="width:100%;border-collapse:collapse;font-size:13px;">' +
|
|
435
|
-
'<thead><tr><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">ID</th><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">Status</th><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">Sellers</th><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">Submitted</th><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">Version</th><th style="text-align:left;padding:6px 8px;color:var(--muted);text-transform:uppercase;font-size:11px;letter-spacing:0.04em;">Error</th></tr></thead>' +
|
|
436
|
-
'<tbody>' + rows.map((r) => {
|
|
437
|
-
const summary = r.payloadSummary || { count: 0, sellerIds: [] };
|
|
438
|
-
const sellerList = (summary.sellerIds || []).join(", ") || "—";
|
|
439
|
-
const version = r.publishedVersion !== null && r.publishedVersion !== undefined ? "v" + esc(String(r.publishedVersion)) : "—";
|
|
440
|
-
return '<tr>' +
|
|
441
|
-
'<td style="padding:6px 8px;font-family:ui-monospace,monospace;">#' + esc(String(r.id)) + '</td>' +
|
|
442
|
-
'<td style="padding:6px 8px;">' + esc(r.status) + '</td>' +
|
|
443
|
-
'<td style="padding:6px 8px;">' + esc(String(summary.count)) + ' <span style="color:var(--muted);font-size:11px;">(' + esc(sellerList) + ')</span></td>' +
|
|
444
|
-
'<td style="padding:6px 8px;font-family:ui-monospace,monospace;">' + esc(fmt.formatTimeCompact(r.submittedAt)) + '</td>' +
|
|
445
|
-
'<td style="padding:6px 8px;">' + version + '</td>' +
|
|
446
|
-
'<td style="padding:6px 8px;color:var(--danger);">' + (r.errorMessage ? esc(r.errorMessage) : "—") + '</td>' +
|
|
447
|
-
'</tr>';
|
|
448
|
-
}).join("") + '</tbody></table>';
|
|
449
|
-
document.getElementById("releasesGrid").innerHTML = table;
|
|
450
|
-
} catch (err) {
|
|
451
|
-
document.getElementById("releasesGrid").innerHTML = '<div class="status-line">'+esc(uiErrorMessage(err))+'</div>';
|
|
452
|
-
}
|
|
560
|
+
(row.removeHint ? '<span class="remove-hint-btn" data-action="remove" data-seller-id="'+esc(row.id)+'" title="'+esc(row.removeHint)+'">'+esc(row.removeHint)+'</span>' : '')
|
|
561
|
+
: publishRelation === "unpublished" && row.publishHint ? '<span class="publish-hint-btn" data-action="publish" data-seller-id="'+esc(row.id)+'" title="'+esc(row.publishHint)+'">'+esc(row.publishHint)+'</span>' : '';
|
|
562
|
+
const publishCell = '<span class="field-cell"><span class="metric-label">Pub</span>'+dsChip+publishExtras+'</span>';
|
|
563
|
+
const regionCell = '<span class="field-cell"><span class="metric-label">Region</span><strong>'+esc(regionText)+'</strong></span>';
|
|
564
|
+
const modelsCell = '<span class="field-cell"><span class="metric-label">Models</span><strong>'+esc(modelsText)+'</strong></span>';
|
|
565
|
+
const connectionCell = '<span class="field-cell"><span class="metric-label">Connection</span>'+renderOpMetric(capacityText, connTone, "Active / max connections")+'</span>';
|
|
566
|
+
const resourcesCell = '<span class="field-cell"><span class="metric-label">Resources</span>'+renderOpMetric(resourcePrimary, resourceTone, usageDetail)+'</span>';
|
|
567
|
+
const upstreamCell = '<span class="field-cell"><span class="metric-label">Upstream</span><strong title="'+esc(row.upstreamDomain || "")+'">'+esc(upstreamDisplay)+'</strong></span>';
|
|
568
|
+
const upstreamStatusCell = '<span class="field-cell"><span class="metric-label">Status</span>'+renderStatusMetric(row.upstreamStatus, "Upstream status from seller /operator/status")+'</span>';
|
|
569
|
+
const discountCell = '<span class="field-cell"><span class="metric-label">Disc</span><strong>'+esc(discountText)+'</strong></span>';
|
|
570
|
+
const balanceCell = '<span class="field-cell"><span class="metric-label">Balance</span><span class="balance-line'+(lowBalance ? ' tone-red' : '')+'">'+balanceText+(row.upstreamRechargeUrl ? '<span class="recharge-btn" title="'+esc(row.upstreamRechargeUrl)+'">↗</span>' : '')+'</span></span>';
|
|
571
|
+
const speedCell = '<span class="speed-cell"><span class="metric-label">TTFT</span>'+renderOpMetric(ttftDisplay, ttftTone, avgSpeed+' · samples '+(row.latencySamples === undefined ? "—" : fmt.formatCount(row.latencySamples)))+'</span>';
|
|
572
|
+
const refreshContent = refreshLoading ? '<span class="inline-spinner" role="status" aria-label="'+esc(detailStatusLabel(row.detailStatus))+'" title="'+esc(detailStatusLabel(row.detailStatus))+'"></span>' : '<strong title="'+esc(row.detailUpdatedAt ? "updated " + fmt.formatTimeCompact(row.detailUpdatedAt) : "")+'">'+esc(refreshText)+'</strong>';
|
|
573
|
+
const refreshCell = '<span class="field-cell"><span class="metric-label">Next</span>'+refreshContent+'</span>';
|
|
574
|
+
const mobileRefreshValue = refreshLoading ? '<span class="inline-spinner" role="status" aria-label="'+esc(detailStatusLabel(row.detailStatus))+'" title="'+esc(detailStatusLabel(row.detailStatus))+'"></span>' : esc(refreshText);
|
|
575
|
+
const mobileUpstreamStatus = upstreamTone === "gray" ? "-" : fmt.normalizeStatusLabel(row.upstreamStatus);
|
|
576
|
+
const mobileMetrics = '<span class="mobile-metrics"><span class="mini-metric"><label>Connection</label><strong>'+esc(capacityText)+'</strong><span>'+esc(status)+'</span></span><span class="mini-metric"><label>Resources</label><strong>'+esc(resourcePrimary)+'</strong><span>'+esc(hasUsage ? cpuText + " / " + memoryText : machineText)+'</span></span><span class="mini-metric"><label>Upstream</label><strong>'+esc(mobileUpstreamStatus)+'</strong><span>'+esc(upstreamDisplay)+'</span></span><span class="mini-metric"><label>Balance</label><strong>'+esc(balancePlain)+'</strong><span>'+esc(hasBalance ? balanceLabel : "-")+'</span></span><span class="mini-metric"><label>Next</label><strong>'+mobileRefreshValue+'</strong><span>'+esc(refreshLoading ? "refreshing" : row.detailStatus || "pending")+'</span></span></span>';
|
|
577
|
+
return '<button class="'+esc(rowClass)+'" type="button" data-detail="'+esc(row.id)+'"><span class="app-dot tone-'+esc(tone)+'" aria-label="'+esc(status)+'" title="'+esc(statusTip)+'"></span><span class="app-name"><strong title="'+esc(row.name)+'">'+esc(row.name)+'</strong>'+mobileMetrics+'</span>'+publishCell+regionCell+modelsCell+connectionCell+resourcesCell+upstreamCell+upstreamStatusCell+discountCell+balanceCell+speedCell+refreshCell+'<span class="row-actions"><span class="detail-btn">›</span></span></button>';
|
|
453
578
|
}
|
|
454
579
|
function entryCardHtml(item){
|
|
455
580
|
const secondary = item.secondary ? '<span class="secondary">'+item.secondary+'</span>' : "";
|
|
@@ -478,7 +603,7 @@ async function openBootstrapConfig(){
|
|
|
478
603
|
document.getElementById("bootstrapModal").classList.add("open");
|
|
479
604
|
}
|
|
480
605
|
}
|
|
481
|
-
async function openDetail(id){ editing = false; deleteReady = false; currentDetail = null; document.getElementById("detailTitle").textContent = id + " detail";
|
|
606
|
+
async function openDetail(id){ editing = false; deleteReady = false; currentDetail = null; document.getElementById("detailTitle").textContent = id + " detail"; document.getElementById("detailGrid").classList.add("hidden"); document.getElementById("configFields").innerHTML = ""; document.getElementById("modelsTable").innerHTML = ""; showDetailStatus("Loading seller data", true); document.getElementById("detailModal").classList.add("open"); try { currentDetail = await api("/api/sellers/"+encodeURIComponent(id)); renderDetail(); } catch (err) { document.getElementById("detailGrid").classList.add("hidden"); showDetailStatus(err.message || "Failed to load seller detail", false); } }
|
|
482
607
|
function renderDetail(){
|
|
483
608
|
const fmt = window.__tbFmt;
|
|
484
609
|
const d = currentDetail;
|
|
@@ -486,11 +611,12 @@ function renderDetail(){
|
|
|
486
611
|
document.getElementById("editDetail").textContent = editing ? "Save changes" : "Edit config";
|
|
487
612
|
document.getElementById("deleteSeller").title = deleteReady ? "Confirm destroy deployment" : "Delete deployment";
|
|
488
613
|
showDetailStatus(d.row.error || "", false);
|
|
614
|
+
document.getElementById("detailGrid").classList.remove("hidden");
|
|
489
615
|
const c = d.configuration;
|
|
490
|
-
const fields = [["registryStatus",
|
|
616
|
+
const fields = [["registryStatus", registryStatusDisplay(c.registryStatus || d.row.registryStatus)],["region", c.region],["upstreamUrl", c.upstreamUrl],["upstreamApiKey", c.upstreamApiKeyMasked],["upstreamStatus", fmt.normalizeStatusLabel(c.upstreamStatus)],["ttftMs", fmt.formatDuration(d.row.ttftMs)],["avgTokensPerSecond", fmt.formatSpeed(d.row.avgTokensPerSecond)],["lastTokensPerSecond", fmt.formatSpeed(d.row.lastTokensPerSecond)],["lastInferenceMs", fmt.formatDuration(d.row.lastInferenceMs)],["latencySamples", d.row.latencySamples === undefined ? "—" : esc(fmt.formatCount(d.row.latencySamples))],["upstreamBalance", c.upstreamBalance],["upstreamBalanceSource", c.upstreamBalanceSource],["upstreamBalanceFetchedAt", c.upstreamBalanceFetchedAt ? fmt.formatTimeFull(c.upstreamBalanceFetchedAt) : "—"],["upstreamBalanceError", c.upstreamBalanceError],["upstreamBalanceProbeTemplate", c.upstreamBalanceProbeTemplate],["upstreamBalanceProbeUrl", c.upstreamBalanceProbeUrl],["upstreamBalanceProbeUserId", c.upstreamBalanceProbeUserId],["upstreamBalanceProbeRechargeUrl", c.upstreamBalanceProbeRechargeUrl],["markupRatio", c.markupRatio],["discountRatio", c.discountRatio],["maxConnections", c.maxConnections],["maxQueueDepth", c.maxQueueDepth]];
|
|
491
617
|
document.getElementById("configFields").innerHTML = detailFieldsHtml(fields);
|
|
492
618
|
const billingOptions = Array.from(new Set(d.models.map(m => m.billingModel).filter(Boolean)));
|
|
493
|
-
document.getElementById("modelsTable").innerHTML = '<table><thead><tr><th>Upstream model</th><th>Billing model</th><th>Input price</th><th>Output price</th><th>TTFT</th><th>AVG speed</th><th>Samples</th></tr></thead><tbody>'+d.models.map(m => '<tr><td>'+esc(m.upstreamModel)+'</td><td>'+(editing ? selectHtml(m.upstreamModel, m.billingModel, billingOptions) : esc(m.billingModel))+'</td><td>'+esc(m.inputPrice || "—")+'</td><td>'+esc(m.outputPrice || "—")+'</td><td>'+esc(m.ttftMs === undefined ? "—" : fmt.formatDuration(m.ttftMs))+'</td><td>'+esc(m.avgTokensPerSecond === undefined ? "—" : fmt.formatSpeed(m.avgTokensPerSecond))+'</td><td>'+esc(m.latencySamples === undefined ? "—" : fmt.formatCount(m.latencySamples))+'</td></tr>').join("")+'</tbody></table>';
|
|
619
|
+
document.getElementById("modelsTable").innerHTML = '<table><thead><tr><th>Enable</th><th>Upstream model</th><th>Billing model</th><th>Input price</th><th>Output price</th><th>TTFT</th><th>AVG speed</th><th>Samples</th></tr></thead><tbody>'+d.models.map(m => '<tr><td>'+modelEnableHtml(m)+'</td><td>'+esc(m.upstreamModel)+'</td><td>'+(editing ? selectHtml(m.upstreamModel, m.billingModel, billingOptions) : esc(m.billingModel))+'</td><td>'+esc(m.inputPrice || "—")+'</td><td>'+esc(m.outputPrice || "—")+'</td><td>'+esc(m.ttftMs === undefined ? "—" : fmt.formatDuration(m.ttftMs))+'</td><td>'+esc(m.avgTokensPerSecond === undefined ? "—" : fmt.formatSpeed(m.avgTokensPerSecond))+'</td><td>'+esc(m.latencySamples === undefined ? "—" : fmt.formatCount(m.latencySamples))+'</td></tr>').join("")+'</tbody></table>';
|
|
494
620
|
}
|
|
495
621
|
function showDetailStatus(message, loading){ const el = document.getElementById("detailStatus"); if (loading){ el.innerHTML = '<span class="spinner" aria-hidden="true"></span>'; el.setAttribute("role", "status"); el.setAttribute("aria-label", message || "Loading"); } else { el.textContent = message || ""; el.removeAttribute("role"); el.removeAttribute("aria-label"); } el.classList.toggle("hidden", !message && !loading); el.classList.toggle("loading", Boolean(loading)); }
|
|
496
622
|
function metricCell(value){ return value === undefined || value === null || value === "" ? missing("—") : esc(value); }
|
|
@@ -498,10 +624,15 @@ function detailFieldsHtml(fields){ return fields.filter(([key,value]) => !isMiss
|
|
|
498
624
|
function isMissingDetailField(key,value){ const fmt = window.__tbFmt; return value === undefined || value === null || value === "" || value === fmt.UNKNOWN_VALUE || (key === "registryStatus" && value === "unknown"); }
|
|
499
625
|
function fieldHtml(key,value, editable, options={}){ const fieldId = "field-" + String(key).replace(/[^a-z0-9_-]/gi, "-") + "-" + Math.random().toString(36).slice(2); const label = options.label || key; const labelText = label + (options.required ? ' <span class="required-star">*</span>' : ''); const display = value === undefined || value === null ? "" : value; const className = "field" + (options.full ? " full" : ""); const help = options.help ? '<p class="field-help">'+esc(options.help)+'</p>' : ""; const hiddenInput = '<input type="hidden" data-field="'+esc(key)+'" value="'+esc(display)+'">'; if (options.hidden) return hiddenInput; if (!editable || options.readonly) return '<div class="'+className+'"><label>'+labelText+'</label><div class="readonly-value" title="'+esc(display)+'" data-readonly-field="'+esc(key)+'">'+esc(display || window.__tbFmt.UNKNOWN_VALUE)+'</div>'+(options.submit ? hiddenInput : "")+help+'</div>'; if (key === "upstreamBalanceProbeTemplate") return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><select id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'">'+balanceProbeTemplates.map(t => '<option value="'+esc(t)+'" '+(t === display ? "selected" : "")+'>'+esc(t)+'</option>').join("")+'</select>'+help+'</div>'; const type = options.type || (String(key).toLowerCase().includes("key") ? "password" : "text"); const attrs = options.type === "number" ? ' type="number" step="any"' : ' type="'+esc(type)+'"'; const placeholder = options.placeholder ? ' placeholder="'+esc(options.placeholder)+'"' : ""; return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><input'+attrs+placeholder+' id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'" value="'+esc(display)+'">'+help+'</div>'; }
|
|
500
626
|
function selectHtml(upstreamModel, billingModel, options){ const values = options.includes(billingModel) ? options : [billingModel, ...options].filter(Boolean); return '<select name="billingModel" aria-label="billingModel" autocomplete="off" data-model="'+esc(upstreamModel)+'">'+values.map(value => '<option '+(value === billingModel ? "selected" : "")+'>'+esc(value)+'</option>').join("")+'</select>'; }
|
|
501
|
-
|
|
627
|
+
function modelEnableHtml(model){ return '<input class="model-toggle" type="checkbox" aria-label="Enable '+esc(model.upstreamModel)+'" data-model-enabled="'+esc(model.upstreamModel)+'" '+(model.enabled === false ? "" : "checked")+' '+(editing ? "" : "disabled")+'>'; }
|
|
628
|
+
function registryStatusForAction(action){ if (action === "drain") return "draining"; if (action === "activate") return "active"; return "offline"; }
|
|
629
|
+
function setStatusActionBusy(busy){ document.querySelectorAll("[data-status-action]").forEach(btn => { btn.disabled = Boolean(busy); }); }
|
|
630
|
+
function setDetailSavingBusy(busy){ const edit = document.getElementById("editDetail"); edit.disabled = Boolean(busy); edit.textContent = busy ? "Saving" : (editing ? "Save changes" : "Edit config"); document.querySelectorAll("#detailGrid [data-field], #detailGrid [data-model], #detailGrid [data-model-enabled], [data-status-action], #deleteSeller").forEach(input => { input.disabled = Boolean(busy); }); }
|
|
631
|
+
document.getElementById("editDetail").onclick = async () => { if (!editing){ editing = true; renderDetail(); return; } const patch = {}; document.querySelectorAll("[data-field]").forEach(input => { const value = input.value; if (value === "" || value === input.dataset.original) return; patch[input.dataset.field] = numeric(value); }); const aliases = {}; document.querySelectorAll("[data-model]").forEach(input => aliases[input.dataset.model] = input.value); patch.modelAliases = aliases; patch.models = modelConfigPatchFromDetail(); try { setDetailSavingBusy(true); showDetailStatus("Saving seller config", true); const result = await api("/api/sellers/"+encodeURIComponent(currentDetail.row.id)+"/config", { method:"PUT", body: JSON.stringify(patch) }); if (!result.ok) throw new Error(result.stderr || "Save failed"); currentDetail = await api("/api/sellers/"+encodeURIComponent(currentDetail.row.id)); editing = false; renderDetail(); loadSellers(); } catch (err) { showDetailStatus(err.message || "Save failed", false); } finally { setDetailSavingBusy(false); } };
|
|
632
|
+
function modelConfigPatchFromDetail(){ const enabledByModel = {}; document.querySelectorAll("[data-model-enabled]").forEach(input => enabledByModel[input.dataset.modelEnabled] = Boolean(input.checked)); return (currentDetail?.models || []).map(model => ({ ...(model.configModel || { id:model.upstreamModel }), id:model.upstreamModel, enabled: enabledByModel[model.upstreamModel] !== false })); }
|
|
502
633
|
document.getElementById("deleteSeller").onclick = async () => { if (!currentDetail) return; if (deleteReady && !confirm("Destroy deployment for "+currentDetail.row.name+"?")) return; const result = await api("/api/sellers/"+encodeURIComponent(currentDetail.row.id)+"/deployment", { method:"DELETE", body: JSON.stringify({ confirm: deleteReady }) }); showDetailStatus(result.stdout || (deleteReady ? "Deployment destroy requested." : "Dry-run ready. Click delete again to confirm destroy."), false); deleteReady = !deleteReady; document.getElementById("deleteSeller").title = deleteReady ? "Confirm destroy deployment" : "Delete deployment"; };
|
|
503
634
|
document.getElementById("closeDetail").onclick = () => { deleteReady = false; document.getElementById("detailModal").classList.remove("open"); };
|
|
504
|
-
document.querySelectorAll("[data-status-action]").forEach(btn => btn.onclick = async () => { if (!currentDetail) return; const action = btn.dataset.statusAction;
|
|
635
|
+
document.querySelectorAll("[data-status-action]").forEach(btn => btn.onclick = async () => { if (!currentDetail) return; const action = btn.dataset.statusAction; const id = currentDetail.row.id; const status = registryStatusForAction(action); try { setStatusActionBusy(true); showDetailStatus("Updating registry status", true); const result = await api("/api/sellers/"+encodeURIComponent(id)+"/"+action, { method:"POST" }); if (!result.ok) throw new Error(result.stderr || "Status update failed."); patchSellerRegistryStatus(id, status); deleteReady = false; currentDetail = null; document.getElementById("detailModal").classList.remove("open"); } catch (err) { showDetailStatus(err.message || "Status update failed.", false); } finally { setStatusActionBusy(false); } });
|
|
505
636
|
document.getElementById("createSeller").onclick = () => { buildCreateForm(); document.getElementById("createModal").classList.add("open"); };
|
|
506
637
|
document.getElementById("closeCreate").onclick = () => document.getElementById("createModal").classList.remove("open");
|
|
507
638
|
function buildCreateForm(){ clearInterval(createJobTimer); createJobTimer = null; currentCreateJob = null; expandedProgressSteps = new Set(); createAppSuffix = randomAppSuffix(); document.getElementById("submitCreate").disabled = false; document.getElementById("submitCreate").textContent = "Create seller"; document.getElementById("createStatus").textContent = ""; document.getElementById("createStatus").classList.add("hidden"); document.getElementById("createStatus").classList.remove("loading"); document.getElementById("createProgress").classList.add("hidden"); document.getElementById("createProgress").innerHTML = ""; setCreateFormDisabled(false); const defaults = createDefaults(); document.getElementById("createFields").innerHTML = createFormHtml(defaults); setupCreateFormBehavior(); setupPaymentTabs(); }
|
|
@@ -534,14 +665,6 @@ function renderCreateJob(job){ if (!job) return; currentCreateJob = job; const d
|
|
|
534
665
|
function progressStep(event){ const result = event.result || {}; const log = [result.command ? "$ " + result.command.join(" ") : "", result.stdout || "", result.stderr || ""].filter(Boolean).join("\\n").slice(0, 1600); const expanded = expandedProgressSteps.has(event.stepId); const spinner = event.status === "running" ? '<span class="spinner" aria-hidden="true"></span>' : ""; return '<button type="button" class="progress-step '+esc(event.status)+'" data-progress-step="'+esc(event.stepId)+'" aria-expanded="'+String(expanded)+'"><div class="progress-title">'+spinner+'<strong>'+esc(event.title)+'</strong></div><div class="progress-meta"><span>'+esc(event.message || event.status)+'</span>'+(log ? '<span class="progress-toggle">'+(expanded ? "Hide details" : "Show details")+'</span>' : '')+'</div>'+(log && expanded ? '<pre class="progress-log">'+esc(log)+'</pre>' : '')+'</button>'; }
|
|
535
666
|
function setCreateFormDisabled(disabled){ document.querySelectorAll("#createFields [data-field], #createFields [data-payment-tab], #createFields [data-payment-toggle]").forEach(input => { input.disabled = Boolean(disabled); }); if (!disabled) updatePaymentPanels(); }
|
|
536
667
|
document.getElementById("createProgress").onclick = event => { const step = event.target.closest("[data-progress-step]"); if (!step) return; const id = step.dataset.progressStep; if (expandedProgressSteps.has(id)) expandedProgressSteps.delete(id); else expandedProgressSteps.add(id); if (currentCreateJob) renderCreateJob(currentCreateJob); };
|
|
537
|
-
const refreshBootstrapEl = document.getElementById("refreshBootstrap");
|
|
538
|
-
if (refreshBootstrapEl) {
|
|
539
|
-
refreshBootstrapEl.onclick = loadBootstrap;
|
|
540
|
-
}
|
|
541
|
-
const refreshReleasesEl = document.getElementById("refreshReleases");
|
|
542
|
-
if (refreshReleasesEl) {
|
|
543
|
-
refreshReleasesEl.onclick = loadBootstrap;
|
|
544
|
-
}
|
|
545
668
|
document.getElementById("closeBootstrapConfig").onclick = () => document.getElementById("bootstrapModal").classList.remove("open");
|
|
546
669
|
function fieldValue(input){ return numeric(input.value); }
|
|
547
670
|
function numeric(value){ const n = Number(value); return value !== "" && Number.isFinite(n) ? n : value; }
|