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);