feat: display server uptime on cards and default sort by duration
lemon-mint committed
Dec 10, 2025 at 12:38 UTC
83b4fc5110a88fdd6639ad51efc8376af805ea45
3 files changed
+32
-5
cmd/relay-server/frontend/src/components/ServerCard.tsx
+30
-4
@@ -1,7 +1,7 @@
1
import { Link } from "react-router-dom";
2
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
3
import clsx from "clsx";
4
-import { ReactNode, useState } from "react";
4
+import { ReactNode, useState, useMemo } from "react";
5
6
interface ServerCardProps {
7
serverId: number;
@@ -11,6 +11,7 @@ interface ServerCardProps {
11
thumbnail: string;
12
owner: string;
13
online: boolean;
14
+ firstSeen?: string;
15
dns: string;
16
serverUrl: string;
17
navigationPath: string;
@@ -34,6 +35,7 @@ export function ServerCard({
35
thumbnail,
36
owner,
37
online,
38
+ firstSeen,
39
navigationPath,
40
navigationState,
41
isFavorite = false,
@@ -130,6 +132,23 @@ export function ServerCard({
132
return `${value} B/s`;
133
};
134
135
+ const formattedDuration = useMemo(() => {
136
+ if (!firstSeen) return "";
137
+ const start = new Date(firstSeen).getTime();
138
+ const now = Date.now();
139
+ const diff = Math.max(0, now - start);
140
+
141
+ const seconds = Math.floor(diff / 1000);
142
+ const minutes = Math.floor(seconds / 60);
143
+ const hours = Math.floor(minutes / 60);
144
+ const days = Math.floor(hours / 24);
145
+
146
+ if (days > 0) return `${days}d ${hours % 24}h`;
147
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
148
+ if (minutes > 0) return `${minutes}m`;
149
+ return `${seconds}s`;
150
+ }, [firstSeen]);
151
+
152
const Wrapper = ({ children }: { children: ReactNode }) =>
153
showAdminControls ? (
154
<div className="relative">{children}</div>
@@ -194,9 +213,16 @@ export function ServerCard({
213
{online ? "Online" : "Offline"}
214
</p>
215
</div>
197
- <p className="text-foreground text-lg font-bold leading-tight truncate max-w-full">
198
- {name}
199
- </p>
216
+ <div className="flex items-center justify-between gap-2 max-w-full">
217
+ <p className="text-foreground text-lg font-bold leading-tight truncate flex-1">
218
+ {name}
219
+ </p>
220
+ {formattedDuration && online && (
221
+ <span className="text-xs text-text-muted font-medium whitespace-nowrap">
222
+ ({formattedDuration})
223
+ </span>
224
+ )}
225
+ </div>
226
{description && (
227
<p className="text-text-muted text-sm font-normal leading-normal truncate max-w-full">
228
{description}
cmd/relay-server/frontend/src/components/ServerListView.tsx
+1
@@ -141,6 +141,7 @@ export function ServerListView({
141
online: server.online,
142
serverUrl: server.link,
143
}}
144
+ firstSeen={server.firstSeen}
145
isFavorite={favorites.includes(server.id)}
146
onToggleFavorite={onToggleFavorite}
147
// Admin controls
cmd/relay-server/frontend/src/hooks/useList.ts
+1
-1
@@ -48,7 +48,7 @@ export function useList<T extends BaseServer>({
48
}: UseListOptions<T>): UseListReturn<T> {
49
const [searchQuery, setSearchQuery] = useState("");
50
const [status, setStatus] = useState<StatusFilter>("all");
51
- const [sortBy, setSortBy] = useState<SortOption>("default");
51
+ const [sortBy, setSortBy] = useState<SortOption>("duration");
52
const [selectedTags, setSelectedTags] = useState<string[]>([]);
53
const [favorites, setFavorites] = useState<number[]>(() => {
54
const stored = localStorage.getItem(storageKey);