feat: Add lease "first seen" timestamp and "duration" sort option to the UI.
lemon-mint committed
Dec 10, 2025 at 12:21 UTC
3419621519ee5bf6cf4939f2e843c891efc3bf6b
8 files changed
+70
-39
cmd/relay-server/frontend/src/components/SearchBar.tsx
+1
@@ -69,6 +69,7 @@ export function SearchBar({
69
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
70
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
71
<SelectItem value="updated">Recently Updated</SelectItem>
72
+ <SelectItem value="duration">Duration (Maintained)</SelectItem>
73
<SelectItem value="description">Description</SelectItem>
74
<SelectItem value="tags">Tags</SelectItem>
75
<SelectItem value="owner">Owner</SelectItem>
cmd/relay-server/frontend/src/hooks/useAdmin.ts
+1
@@ -49,6 +49,7 @@ function convertServerDataToAdminServer(
49
dns: row.DNS || "",
50
link: row.Link,
51
lastUpdated: row.LastSeenISO || row.LastSeen || undefined,
52
+ firstSeen: row.FirstSeenISO || undefined,
53
// Admin-specific fields
54
peerId: row.Peer,
55
isBanned: bannedLeases.includes(row.Peer),
cmd/relay-server/frontend/src/hooks/useList.ts
+11
@@ -13,6 +13,7 @@ export interface BaseServer {
13
dns: string;
14
link: string;
15
lastUpdated?: string;
16
+ firstSeen?: string;
17
}
18
19
export interface UseListOptions<T extends BaseServer> {
@@ -114,6 +115,16 @@ export function useList<T extends BaseServer>({
115
return bTime - aTime;
116
});
117
break;
118
+ case "duration":
119
+ sorted.sort((a, b) => {
120
+ // Duration = Now - FirstSeen.
121
+ // Longer duration = Older FirstSeen.
122
+ // Sort Descending (Longest/Oldest first) -> Ascending FirstSeen timestamp.
123
+ const aTime = a.firstSeen ? Date.parse(a.firstSeen) : Date.now();
124
+ const bTime = b.firstSeen ? Date.parse(b.firstSeen) : Date.now();
125
+ return aTime - bTime;
126
+ });
127
+ break;
128
case "description":
129
sorted.sort((a, b) => a.description.localeCompare(b.description));
130
break;
cmd/relay-server/frontend/src/hooks/useSSRData.ts
+1
@@ -16,6 +16,7 @@ export interface ServerData {
16
DNS: string;
17
LastSeen: string;
18
LastSeenISO: string;
19
+ FirstSeenISO: string;
20
TTL: string;
21
Link: string;
22
StaleRed: boolean;
cmd/relay-server/frontend/src/hooks/useServerList.ts
+1
@@ -43,6 +43,7 @@ function convertSSRDataToServers(ssrData: ServerData[]): ClientServer[] {
43
dns: row.DNS || "",
44
link: row.Link,
45
lastUpdated: row.LastSeenISO || row.LastSeen || undefined,
46
+ firstSeen: row.FirstSeenISO || undefined,
47
};
48
});
49
}
cmd/relay-server/frontend/src/types/filters.ts
+2
-1
@@ -7,6 +7,7 @@ export type SortOption =
7
| "owner"
8
| "name-asc"
9
| "name-desc"
10
- | "updated";
10
+ | "updated"
11
+ | "duration";
12
13
export type TagMode = "AND" | "OR";
cmd/relay-server/view.go
+43
-38
@@ -191,19 +191,20 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
191
}
192
193
type leaseRow struct {
194
- Peer string
195
- Name string
196
- Kind string
197
- Connected bool
198
- DNS string
199
- LastSeen string
200
- LastSeenISO string
201
- TTL string
202
- Link string
203
- StaleRed bool
204
- Hide bool
205
- Metadata string
206
- BPS int64 // bytes-per-second limit (0 = unlimited)
194
+ Peer string
195
+ Name string
196
+ Kind string
197
+ Connected bool
198
+ DNS string
199
+ LastSeen string
200
+ LastSeenISO string
201
+ FirstSeenISO string
202
+ TTL string
203
+ Link string
204
+ StaleRed bool
205
+ Hide bool
206
+ Metadata string
207
+ BPS int64 // bytes-per-second limit (0 = unlimited)
208
}
209
210
// convertLeaseEntriesToRows converts LeaseEntry data from LeaseManager to leaseRow format for the app page
@@ -382,6 +383,7 @@ func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
383
return fmt.Sprintf("%ds", s)
384
}(since)
385
lastSeenISO := leaseEntry.LastSeen.UTC().Format(time.RFC3339)
386
+ firstSeenISO := leaseEntry.FirstSeen.UTC().Format(time.RFC3339)
387
388
// Check if connection is still active
389
connected := serv.IsConnectionActive(leaseEntry.ConnectionID)
@@ -415,19 +417,20 @@ func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
417
bps := globalBPSManager.GetBPSLimit(identityID)
418
419
row := leaseRow{
418
- Peer: identityID,
419
- Name: name,
420
- Kind: kind,
421
- Connected: connected,
422
- DNS: dnsLabel,
423
- LastSeen: lastSeenStr,
424
- LastSeenISO: lastSeenISO,
425
- TTL: ttlStr,
426
- Link: link,
427
- StaleRed: !connected && since >= 15*time.Second,
428
- Hide: leaseEntry.ParsedMetadata != nil && leaseEntry.ParsedMetadata.Hide,
429
- Metadata: lease.Metadata,
430
- BPS: bps,
420
+ Peer: identityID,
421
+ Name: name,
422
+ Kind: kind,
423
+ Connected: connected,
424
+ DNS: dnsLabel,
425
+ LastSeen: lastSeenStr,
426
+ LastSeenISO: lastSeenISO,
427
+ FirstSeenISO: firstSeenISO,
428
+ TTL: ttlStr,
429
+ Link: link,
430
+ StaleRed: !connected && since >= 15*time.Second,
431
+ Hide: leaseEntry.ParsedMetadata != nil && leaseEntry.ParsedMetadata.Hide,
432
+ Metadata: lease.Metadata,
433
+ BPS: bps,
434
}
435
436
rows = append(rows, row)
@@ -505,6 +508,7 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
508
return fmt.Sprintf("%ds", s)
509
}(since)
510
lastSeenISO := leaseEntry.LastSeen.UTC().Format(time.RFC3339)
511
+ firstSeenISO := leaseEntry.FirstSeen.UTC().Format(time.RFC3339)
512
513
// Check if connection is still active
514
connected := serv.IsConnectionActive(leaseEntry.ConnectionID)
@@ -540,18 +544,19 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
544
link := fmt.Sprintf("//%s.%s/", lease.Name, utils.StripWildCard(utils.StripScheme(base)))
545
546
row := leaseRow{
543
- Peer: identityID,
544
- Name: name,
545
- Kind: kind,
546
- Connected: connected,
547
- DNS: dnsLabel,
548
- LastSeen: lastSeenStr,
549
- LastSeenISO: lastSeenISO,
550
- TTL: ttlStr,
551
- Link: link,
552
- StaleRed: !connected && since >= 15*time.Second,
553
- Hide: leaseEntry.ParsedMetadata != nil && leaseEntry.ParsedMetadata.Hide,
554
- Metadata: lease.Metadata,
547
+ Peer: identityID,
548
+ Name: name,
549
+ Kind: kind,
550
+ Connected: connected,
551
+ DNS: dnsLabel,
552
+ LastSeen: lastSeenStr,
553
+ LastSeenISO: lastSeenISO,
554
+ FirstSeenISO: firstSeenISO,
555
+ TTL: ttlStr,
556
+ Link: link,
557
+ StaleRed: !connected && since >= 15*time.Second,
558
+ Hide: leaseEntry.ParsedMetadata != nil && leaseEntry.ParsedMetadata.Hide,
559
+ Metadata: lease.Metadata,
560
}
561
562
// Hidden entries are already filtered above, but keep check for safety
portal/lease.go
+10
@@ -25,6 +25,7 @@ type LeaseEntry struct {
25
Lease *rdverb.Lease
26
Expires time.Time
27
LastSeen time.Time
28
+ FirstSeen time.Time
29
ConnectionID int64
30
ParsedMetadata *ParsedMetadata // Cached parsed metadata
31
}
@@ -151,10 +152,19 @@ func (lm *LeaseManager) UpdateLease(lease *rdverb.Lease, connectionID int64) boo
152
}
153
}
154
155
+ var firstSeen time.Time
156
+ if existing, exists := lm.leases[identityID]; exists {
157
+ firstSeen = existing.FirstSeen
158
+ }
159
+ if firstSeen.IsZero() {
160
+ firstSeen = time.Now()
161
+ }
162
+
163
lm.leases[identityID] = &LeaseEntry{
164
Lease: lease,
165
Expires: expires,
166
LastSeen: time.Now(),
167
+ FirstSeen: firstSeen,
168
ConnectionID: connectionID,
169
ParsedMetadata: parsedMeta,
170
}