Skip to content

Commit ecaebd3

Browse files
committed
Fixed server sorting using a best defaults guess. Restyled the server list view page, made ux a bit more intuitive and rm dropdown bs.
1 parent 64a29b6 commit ecaebd3

File tree

5 files changed

+260
-151
lines changed

5 files changed

+260
-151
lines changed

web/discopanel/src/lib/stores/auth.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ChangePasswordRequestSchema,
1111
UseRecoveryKeyRequestSchema
1212
} from '$lib/proto/discopanel/v1/auth_pb';
13+
import * as _ from 'lodash-es';
1314

1415
interface AuthState {
1516
user: User | null;
@@ -152,7 +153,7 @@ function createAuthStore() {
152153
},
153154

154155
async logout() {
155-
let currentState: AuthState = get({ subscribe });
156+
const currentState: AuthState = get({ subscribe });
156157

157158
try {
158159
if (currentState.token) {
@@ -214,8 +215,8 @@ function createAuthStore() {
214215
});
215216
const response = await rpcClient.auth.changePassword(request);
216217
return response;
217-
} catch (error: any) {
218-
throw new Error(error.message || 'Failed to change password');
218+
} catch (error) {
219+
throw new Error(_.get(error, 'message') || 'Failed to change password');
219220
}
220221
},
221222

web/discopanel/src/lib/stores/servers.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,61 @@ export const runningServers = derived(
5151
export const stoppedServers = derived(
5252
serversStore,
5353
$servers => $servers.filter(server => server.status === ServerStatus.STOPPED)
54+
);
55+
56+
function getTimestampMs(ts: { seconds: bigint } | undefined): number {
57+
if (!ts) return 0;
58+
return Number(ts.seconds) * 1000;
59+
}
60+
61+
/**
62+
* AUTO SORT PRIORITY:
63+
* 1. Pin most recently created/updated server as #1
64+
* 2. Running servers w/ players first (by player count desc)
65+
* 3. Running servers wo/ players (by lastStarted desc)
66+
* 4. Non-running servers (by updatedAt desc)
67+
*/
68+
export function sortServersByActivity(servers: Server[]): Server[] {
69+
if (servers.length <= 1) return servers;
70+
71+
// Find the most recently created or updated server to pin
72+
let pinnedIdx = 0;
73+
let pinnedTime = 0;
74+
for (let i = 0; i < servers.length; i++) {
75+
const created = getTimestampMs(servers[i].createdAt);
76+
const updated = getTimestampMs(servers[i].updatedAt);
77+
const latest = Math.max(created, updated);
78+
if (latest > pinnedTime) {
79+
pinnedTime = latest;
80+
pinnedIdx = i;
81+
}
82+
}
83+
84+
const pinned = servers[pinnedIdx];
85+
const rest = servers.filter((_, i) => i !== pinnedIdx);
86+
87+
rest.sort((a, b) => {
88+
const aRunning = a.status === ServerStatus.RUNNING ? 1 : 0;
89+
const bRunning = b.status === ServerStatus.RUNNING ? 1 : 0;
90+
// Running servers first
91+
if (aRunning !== bRunning) return bRunning - aRunning;
92+
93+
// Both running: sort by players online desc
94+
if (aRunning && bRunning) {
95+
const playerDiff = (b.playersOnline || 0) - (a.playersOnline || 0);
96+
if (playerDiff !== 0) return playerDiff;
97+
// Tiebreak by lastStarted (most recent first)
98+
return getTimestampMs(b.lastStarted) - getTimestampMs(a.lastStarted);
99+
}
100+
101+
// Both not running: sort by updatedAt desc
102+
return getTimestampMs(b.updatedAt) - getTimestampMs(a.updatedAt);
103+
});
104+
105+
return [pinned, ...rest];
106+
}
107+
108+
export const activitySortedServers = derived(
109+
serversStore,
110+
$servers => sortServersByActivity([...$servers])
54111
);

web/discopanel/src/routes/+layout.svelte

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,31 @@
3131
DropdownMenuTrigger
3232
} from '$lib/components/ui/dropdown-menu';
3333
import { get } from 'svelte/store';
34-
import { serversStore, runningServers } from '$lib/stores/servers';
34+
import { serversStore, runningServers, activitySortedServers } from '$lib/stores/servers';
3535
import { authStore, currentUser, canAccessSettings, authEnabled } from '$lib/stores/auth';
3636
import { onMount } from 'svelte';
3737
import { Toaster } from '$lib/components/ui/sonner';
3838
import GlobalLoading from '$lib/components/global-loading.svelte';
3939
40-
import { Server, Home, Settings, Package, User, LogOut, LogIn, FileText, Sun, Moon } from '@lucide/svelte';
40+
import { Server, Home, Settings, Package, User as UserIcon, LogOut, LogIn, FileText, Sun, Moon } from '@lucide/svelte';
4141
import { toggleMode, mode } from 'mode-watcher';
42-
import { ServerStatus } from '$lib/proto/discopanel/v1/common_pb';
42+
import { ServerStatus, type User } from '$lib/proto/discopanel/v1/common_pb';
4343
4444
let { children } = $props();
4545
46-
let servers = $derived($serversStore);
46+
let servers = $derived($activitySortedServers);
4747
let runningCount = $derived($runningServers.length);
4848
let user = $derived($currentUser);
4949
let showSettingsNav = $derived($canAccessSettings);
5050
let loading = $state(true);
5151
let isAuthEnabled = $derived($authEnabled);
5252
53-
function getUserInitials(user: any) {
53+
function getUserInitials(user: User) {
5454
if (!user) return '';
5555
return user.username.slice(0, 2).toUpperCase();
5656
}
5757
58-
function getDisplayRole(user: any): string {
58+
function getDisplayRole(user: User): string {
5959
if (!user?.roles?.length) return 'No roles';
6060
return user.roles[0];
6161
}
@@ -215,7 +215,7 @@
215215
<SidebarGroupLabel>Quick Access</SidebarGroupLabel>
216216
<SidebarGroupContent>
217217
<SidebarMenu>
218-
{#each servers.slice(0, 5) as server}
218+
{#each servers as server (server.id)}
219219
<SidebarMenuItem>
220220
<SidebarMenuButton isActive={page.url.pathname === `/servers/${server.id}`}>
221221
{#snippet child({ props })}
@@ -278,7 +278,7 @@
278278
</DropdownMenuLabel>
279279
<DropdownMenuSeparator />
280280
<DropdownMenuItem onclick={() => goto('/profile')}>
281-
<User class="mr-2 h-4 w-4" />
281+
<UserIcon class="mr-2 h-4 w-4" />
282282
<span>Profile</span>
283283
</DropdownMenuItem>
284284
<DropdownMenuSeparator />

web/discopanel/src/routes/+page.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
} from '@lucide/svelte';
1717
import { ServerStatus, type Server as ServerType } from '$lib/proto/discopanel/v1/common_pb';
1818
import { rpcClient } from '$lib/api/rpc-client';
19-
import { serversStore } from '$lib/stores/servers';
19+
import { serversStore, sortServersByActivity } from '$lib/stores/servers';
20+
import type { Timestamp } from '@bufbuild/protobuf/wkt';
2021
2122
// Dashboard data
2223
let dashboardServers: ServerType[] = $state([]);
@@ -147,7 +148,7 @@
147148
148149
149150
150-
const formatUptime = (lastStarted: any) => {
151+
const formatUptime = (lastStarted?: Timestamp) => {
151152
if (!lastStarted) return 'Never';
152153
const start = new Date(Number(lastStarted.seconds) * 1000);
153154
const diff = currentTime.getTime() - start.getTime();
@@ -376,7 +377,7 @@
376377
</div>
377378
{:else}
378379
<div class="space-y-3">
379-
{#each dashboardServers.slice(0, 5) as server}
380+
{#each sortServersByActivity([...dashboardServers]).slice(0, 5) as server (server.id)}
380381
{@const StatusIcon = getStatusIcon(server.status)}
381382
<div class="group flex items-center justify-between p-3 rounded-lg hover:bg-muted/50 transition-colors">
382383
<div class="flex items-center gap-3 flex-1">
@@ -444,7 +445,7 @@
444445
</div>
445446
{:else}
446447
<div class="space-y-3">
447-
{#each recentActivity as activity, i}
448+
{#each recentActivity as activity, i (activity)}
448449
<div class="flex items-start gap-3 animate-in fade-in-50 slide-in-from-right-2" style="animation-delay: {300 + i * 50}ms">
449450
<div class="mt-1">
450451
{#if activity.status === ServerStatus.RUNNING}

0 commit comments

Comments
 (0)