restore bps manager
Kim committed
Mar 17, 2026 at 18:21 UTC
37dbf33b9883ac3097adb236a8fa300eafbb9526
12 files changed
+248
-30
cmd/relay-server/admin.go
+27
-6
@@ -248,7 +248,25 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
248
}
249
writeOK()
250
case "bps":
251
- utils.WriteAPIError(w, http.StatusNotImplemented, types.APIErrorCodeFeatureUnavailable, "bps control is not enabled in this build")
251
+ switch r.Method {
252
+ case http.MethodPost:
253
+ var req types.AdminBPSRequest
254
+ if err := utils.DecodeJSONBody(w, r, &req, 1<<16); err != nil {
255
+ utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "invalid request body")
256
+ return
257
+ }
258
+ if req.BPS <= 0 {
259
+ utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "bps must be greater than zero")
260
+ return
261
+ }
262
+ runtime.BPSManager().SetLeaseBPS(leaseID, req.BPS)
263
+ case http.MethodDelete:
264
+ runtime.BPSManager().DeleteLeaseBPS(leaseID)
265
+ default:
266
+ methodNotAllowed()
267
+ return
268
+ }
269
+ writeOK()
270
case "approve":
271
approver := runtime.Approver()
272
switch r.Method {
@@ -371,11 +389,12 @@ func saveAdminState(runtime *policy.Runtime) {
389
}
390
391
type persistedAdminState struct {
374
- ApprovalMode string `json:"approval_mode"`
375
- ApprovedLeases []string `json:"approved_leases,omitempty"`
376
- DeniedLeases []string `json:"denied_leases,omitempty"`
377
- BannedLeases []string `json:"banned_leases,omitempty"`
378
- BannedIPs []string `json:"banned_ips,omitempty"`
392
+ ApprovalMode string `json:"approval_mode"`
393
+ ApprovedLeases []string `json:"approved_leases,omitempty"`
394
+ DeniedLeases []string `json:"denied_leases,omitempty"`
395
+ BannedLeases []string `json:"banned_leases,omitempty"`
396
+ BannedIPs []string `json:"banned_ips,omitempty"`
397
+ LeaseBPS map[string]int64 `json:"lease_bps,omitempty"`
398
}
399
400
func persistedStateFromRuntime(runtime *policy.Runtime) persistedAdminState {
@@ -386,6 +405,7 @@ func persistedStateFromRuntime(runtime *policy.Runtime) persistedAdminState {
405
DeniedLeases: approver.DeniedLeases(),
406
BannedLeases: runtime.BannedLeases(),
407
BannedIPs: runtime.IPFilter().BannedIPs(),
408
+ LeaseBPS: runtime.BPSManager().LeaseBPSLimits(),
409
}
410
}
411
@@ -401,6 +421,7 @@ func (s persistedAdminState) apply(runtime *policy.Runtime) error {
421
runtime.Approver().SetDecisions(s.ApprovedLeases, s.DeniedLeases)
422
runtime.SetBannedLeases(s.BannedLeases)
423
runtime.IPFilter().SetBannedIPs(s.BannedIPs)
424
+ runtime.BPSManager().SetLeaseBPSLimits(s.LeaseBPS)
425
return nil
426
}
427
cmd/relay-server/frontend.go
+1
@@ -261,6 +261,7 @@ func (f *Frontend) publicLeaseSnapshots() []types.Lease {
261
}
262
263
snapshot.ClientIP = ""
264
+ snapshot.BPS = 0
265
snapshot.IsApproved = false
266
snapshot.IsBanned = false
267
snapshot.IsDenied = false
frontend/src/components/ServerCard.tsx
+1
-1
@@ -188,7 +188,7 @@ export function ServerCard({
188
};
189
190
const formatStepLabel = (value: number): string => {
191
- if (value === 0) return "∞";
191
+ if (value === 0) return "unl";
192
if (value >= 1000000) return `${value / 1000000}M`;
193
if (value >= 1000) return `${value / 1000}K`;
194
return value.toString();
frontend/src/hooks/useAdmin.test.ts
+61
@@ -6,6 +6,11 @@ import { useAdmin } from "@/hooks/useAdmin";
6
import { API_PATHS, adminLeasePath, encodeLeaseID } from "@/lib/apiPaths";
7
import { APIClientError, apiClient } from "@/lib/apiClient";
8
9
+type DeferredAdminSnapshot = {
10
+ leases: ServerData[];
11
+ approval_mode: "auto" | "manual";
12
+};
13
+
14
vi.mock("@/hooks/useList", () => ({
15
useList: vi.fn(() => ({
16
searchQuery: "",
@@ -45,6 +50,7 @@ function buildLease(peer: string): ServerData {
50
LastSeenAt: "2026-03-03T00:00:00Z",
51
ID: peer,
52
Name: "relay-1",
53
+ BPS: 1024,
54
ClientIP: "203.0.113.10",
55
Hostname: "relay.example.com",
56
Metadata: {
@@ -99,6 +105,7 @@ describe("useAdmin", () => {
105
expect(result.current.approvalMode).toBe("auto");
106
expect(result.current.servers[0]?.peerId).toBe("peer-a");
107
expect(result.current.servers[0]?.isBanned).toBe(true);
108
+ expect(result.current.servers[0]?.bps).toBe(1024);
109
});
110
111
it("surfaces fetchData API errors", async () => {
@@ -166,6 +173,60 @@ describe("useAdmin", () => {
173
);
174
});
175
176
+ it("posts bps updates to the lease action route", async () => {
177
+ const { result } = renderHook(() => useAdmin());
178
+ await waitForLoaded(result);
179
+
180
+ await act(async () => {
181
+ await result.current.handleBPSChange("peer-a", 4096);
182
+ });
183
+
184
+ expect(mockPost).toHaveBeenCalledWith(
185
+ adminLeasePath(encodeLeaseID("peer-a"), "bps"),
186
+ { bps: 4096 },
187
+ );
188
+ });
189
+
190
+ it("keeps loading false while refreshing bps in the background", async () => {
191
+ let getCalls = 0;
192
+ let resolveRefresh:
193
+ | ((value: DeferredAdminSnapshot | PromiseLike<DeferredAdminSnapshot>) => void)
194
+ | undefined;
195
+
196
+ mockGet.mockImplementation((path: string) => {
197
+ if (path !== API_PATHS.admin.snapshot) {
198
+ throw new Error(`Unexpected GET path: ${path}`);
199
+ }
200
+ getCalls++;
201
+ if (getCalls === 1) {
202
+ return Promise.resolve({
203
+ leases: [buildLease("peer-a")],
204
+ approval_mode: "auto",
205
+ } as never);
206
+ }
207
+ return new Promise<DeferredAdminSnapshot>((resolve) => {
208
+ resolveRefresh = resolve;
209
+ }) as never;
210
+ });
211
+
212
+ const { result } = renderHook(() => useAdmin());
213
+ await waitForLoaded(result);
214
+
215
+ let pending: Promise<void> | undefined;
216
+ await act(async () => {
217
+ pending = result.current.handleBPSChange("peer-a", 2048);
218
+ await Promise.resolve();
219
+ expect(result.current.loading).toBe(false);
220
+ resolveRefresh?.({
221
+ leases: [{ ...buildLease("peer-a"), BPS: 2048 }],
222
+ approval_mode: "auto",
223
+ });
224
+ await pending;
225
+ });
226
+
227
+ expect(result.current.servers[0]?.bps).toBe(2048);
228
+ });
229
+
230
it("bulk deny posts deduped lease IDs to action routes", async () => {
231
const { result } = renderHook(() => useAdmin());
232
await waitForLoaded(result);
frontend/src/hooks/useAdmin.ts
+36
-19
@@ -92,7 +92,7 @@ function toAdminServer(
92
firstSeen: row.FirstSeenAt || undefined,
93
peerId: row.ID,
94
isBanned: row.IsBanned || false,
95
- bps: 0,
95
+ bps: row.BPS || 0,
96
isApproved: row.IsApproved || false,
97
isDenied: row.IsDenied || false,
98
ip: row.ClientIP || "",
@@ -149,14 +149,11 @@ export function useAdmin() {
149
150
const fetchData = async () => {
151
setError("");
152
- setLoading(true);
152
153
try {
154
applySnapshot(await loadAdminSnapshot());
155
} catch (err: unknown) {
156
setError(toAdminErrorMessage(err, "Failed to load admin data"));
158
- } finally {
159
- setLoading(false);
157
}
158
};
159
@@ -243,23 +240,43 @@ export function useAdmin() {
240
const handleBanStatus = (peerId: string, isBan: boolean) =>
241
runAdminAction(() => updateLeaseAction(peerId, "ban", isBan));
242
246
- const handleBPSChange = (peerId: string, bps: number) =>
247
- runAdminAction(async () => {
248
- if (!peerId) {
249
- throw new Error("Missing lease ID");
250
- }
251
- const encodedLeaseID = encodeLeaseID(peerId);
252
- const normalizedBPS = Math.trunc(bps);
253
- if (!Number.isFinite(normalizedBPS) || normalizedBPS <= 0) {
254
- await apiClient.delete<LeaseActionResult>(
255
- adminLeasePath(encodedLeaseID, "bps")
243
+ const handleBPSChange = async (peerId: string, bps: number) => {
244
+ if (!peerId) {
245
+ throw new Error("Missing lease ID");
246
+ }
247
+
248
+ const encodedLeaseID = encodeLeaseID(peerId);
249
+ const normalizedBPS = Math.max(0, Math.trunc(bps));
250
+ const previousBPS = serverData.find((row) => row.ID === peerId)?.BPS ?? 0;
251
+
252
+ setServerData((prev) =>
253
+ prev.map((row) =>
254
+ row.ID === peerId ? { ...row, BPS: normalizedBPS } : row
255
+ )
256
+ );
257
+
258
+ try {
259
+ await runAdminAction(async () => {
260
+ if (!Number.isFinite(normalizedBPS) || normalizedBPS <= 0) {
261
+ await apiClient.delete<LeaseActionResult>(
262
+ adminLeasePath(encodedLeaseID, "bps")
263
+ );
264
+ return;
265
+ }
266
+ await apiClient.post<LeaseActionResult>(
267
+ adminLeasePath(encodedLeaseID, "bps"),
268
+ { bps: normalizedBPS }
269
);
257
- return;
258
- }
259
- await apiClient.post<LeaseActionResult>(adminLeasePath(encodedLeaseID, "bps"), {
260
- bps: normalizedBPS,
270
});
262
- });
271
+ } catch (err) {
272
+ setServerData((prev) =>
273
+ prev.map((row) =>
274
+ row.ID === peerId ? { ...row, BPS: previousBPS } : row
275
+ )
276
+ );
277
+ throw err;
278
+ }
279
+ };
280
281
const handleApprovalModeChange = async (mode: ApprovalMode) => {
282
await runAdminAction(async () => {
frontend/src/hooks/useSSRData.ts
+1
@@ -14,6 +14,7 @@ export interface ServerData {
14
LastSeenAt: string;
15
ID: string;
16
Name: string;
17
+ BPS?: number;
18
ClientIP: string;
19
Hostname: string;
20
Metadata: unknown;
frontend/src/pages/Admin.tsx
+9
-2
@@ -10,6 +10,7 @@ export function Admin() {
10
const { isAuthenticated, isLoading: authLoading, logout } = useAuth();
11
12
const {
13
+ servers,
14
filteredServers,
15
availableTags,
16
searchQuery,
@@ -28,6 +29,7 @@ export function Admin() {
29
handleBanFilterChange,
30
handleToggleFavorite,
31
handleBanStatus,
32
+ handleBPSChange,
33
handleApprovalModeChange,
34
handleApproveStatus,
35
handleDenyStatus,
@@ -57,8 +59,12 @@ export function Admin() {
59
return null; // Will redirect
60
}
61
60
- if (loading) return <div className="p-8 text-foreground">Loading...</div>;
61
- if (error) return <div className="p-8 text-red-500">Error: {error}</div>;
62
+ if (loading && servers.length === 0) {
63
+ return <div className="p-8 text-foreground">Loading...</div>;
64
+ }
65
+ if (error && servers.length === 0) {
66
+ return <div className="p-8 text-red-500">Error: {error}</div>;
67
+ }
68
69
return (
70
<SsgoiTransition id="admin">
@@ -82,6 +88,7 @@ export function Admin() {
88
approvalMode={approvalMode}
89
onBanFilterChange={handleBanFilterChange}
90
onBanStatusChange={handleBanStatus}
91
+ onBPSChange={handleBPSChange}
92
onApprovalModeChange={handleApprovalModeChange}
93
onApproveStatusChange={handleApproveStatus}
94
onDenyStatusChange={handleDenyStatus}
portal/lease.go
+1
@@ -212,6 +212,7 @@ func (r *leaseRegistry) Snapshot(record *leaseRecord) types.Lease {
212
snapshot := record.Lease
213
snapshot.Metadata = snapshot.Metadata.Copy()
214
clientIP := record.ClientIP
215
+ snapshot.BPS = r.policy.BPSManager().LeaseBPS(record.ID)
216
snapshot.Ready = record.Broker.ReadyCount()
217
snapshot.IsApproved = r.policy.EffectiveApproval(record.ID)
218
snapshot.IsBanned = r.policy.IsLeaseBanned(record.ID)
portal/policy/bps_manager.go
new
+90
@@ -0,0 +1,90 @@
1
+package policy
2
+
3
+import (
4
+ "strings"
5
+ "sync"
6
+)
7
+
8
+type BPSManager struct {
9
+ leaseBPS map[string]int64
10
+ mu sync.RWMutex
11
+}
12
+
13
+func NewBPSManager() *BPSManager {
14
+ return &BPSManager{
15
+ leaseBPS: make(map[string]int64),
16
+ }
17
+}
18
+
19
+func (m *BPSManager) LeaseBPS(leaseID string) int64 {
20
+ if m == nil {
21
+ return 0
22
+ }
23
+
24
+ m.mu.RLock()
25
+ defer m.mu.RUnlock()
26
+ return m.leaseBPS[strings.TrimSpace(leaseID)]
27
+}
28
+
29
+func (m *BPSManager) SetLeaseBPS(leaseID string, bps int64) {
30
+ if m == nil {
31
+ return
32
+ }
33
+
34
+ leaseID = strings.TrimSpace(leaseID)
35
+ if leaseID == "" {
36
+ return
37
+ }
38
+
39
+ m.mu.Lock()
40
+ defer m.mu.Unlock()
41
+ if bps <= 0 {
42
+ delete(m.leaseBPS, leaseID)
43
+ return
44
+ }
45
+ m.leaseBPS[leaseID] = bps
46
+}
47
+
48
+func (m *BPSManager) DeleteLeaseBPS(leaseID string) {
49
+ if m == nil {
50
+ return
51
+ }
52
+
53
+ m.mu.Lock()
54
+ defer m.mu.Unlock()
55
+ delete(m.leaseBPS, strings.TrimSpace(leaseID))
56
+}
57
+
58
+func (m *BPSManager) LeaseBPSLimits() map[string]int64 {
59
+ if m == nil {
60
+ return nil
61
+ }
62
+
63
+ m.mu.RLock()
64
+ defer m.mu.RUnlock()
65
+
66
+ out := make(map[string]int64, len(m.leaseBPS))
67
+ for leaseID, bps := range m.leaseBPS {
68
+ out[leaseID] = bps
69
+ }
70
+ return out
71
+}
72
+
73
+func (m *BPSManager) SetLeaseBPSLimits(limits map[string]int64) {
74
+ if m == nil {
75
+ return
76
+ }
77
+
78
+ next := make(map[string]int64, len(limits))
79
+ for leaseID, bps := range limits {
80
+ leaseID = strings.TrimSpace(leaseID)
81
+ if leaseID == "" || bps <= 0 {
82
+ continue
83
+ }
84
+ next[leaseID] = bps
85
+ }
86
+
87
+ m.mu.Lock()
88
+ m.leaseBPS = next
89
+ m.mu.Unlock()
90
+}
portal/policy/runtime.go
+16
-2
@@ -7,6 +7,7 @@ import (
7
8
type Runtime struct {
9
approver *Approver
10
+ bpsManager *BPSManager
11
ipFilter *IPFilter
12
bannedLeases map[string]struct{}
13
mu sync.RWMutex
@@ -15,6 +16,7 @@ type Runtime struct {
16
func NewRuntime() *Runtime {
17
return &Runtime{
18
approver: NewApprover(),
19
+ bpsManager: NewBPSManager(),
20
ipFilter: NewIPFilter(),
21
bannedLeases: make(map[string]struct{}),
22
}
@@ -34,6 +36,13 @@ func (r *Runtime) IPFilter() *IPFilter {
36
return r.ipFilter
37
}
38
39
+func (r *Runtime) BPSManager() *BPSManager {
40
+ if r == nil {
41
+ return nil
42
+ }
43
+ return r.bpsManager
44
+}
45
+
46
func (r *Runtime) BanLease(leaseID string) {
47
if r == nil {
48
return
@@ -127,8 +136,13 @@ func (r *Runtime) IsLeaseRoutable(leaseID string) bool {
136
}
137
138
func (r *Runtime) ForgetLease(leaseID string) {
130
- if r == nil || r.ipFilter == nil {
139
+ if r == nil {
140
return
141
}
133
- r.ipFilter.RemoveLeaseIP(leaseID)
142
+ if r.ipFilter != nil {
143
+ r.ipFilter.RemoveLeaseIP(leaseID)
144
+ }
145
+ if r.bpsManager != nil {
146
+ r.bpsManager.DeleteLeaseBPS(leaseID)
147
+ }
148
}
types/api.go
+4
@@ -113,3 +113,7 @@ type AdminApprovalModeRequest struct {
113
type AdminApprovalModeResponse struct {
114
ApprovalMode string `json:"approval_mode"`
115
}
116
+
117
+type AdminBPSRequest struct {
118
+ BPS int64 `json:"bps"`
119
+}
types/lease.go
+1
@@ -26,6 +26,7 @@ type Lease struct {
26
LastSeenAt time.Time
27
ID string
28
Name string
29
+ BPS int64
30
ClientIP string
31
Hostname string
32
Metadata LeaseMetadata