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 }