diff --git a/src/games/hordeseer/assets/leaderboard-modal.module.css b/src/games/hordeseer/assets/leaderboard-modal.module.css
index a35c831..b5100e7 100644
--- a/src/games/hordeseer/assets/leaderboard-modal.module.css
+++ b/src/games/hordeseer/assets/leaderboard-modal.module.css
@@ -1,5 +1,4 @@
.modal {
- max-width: 480px;
height: fit-content;
}
@@ -61,17 +60,16 @@
color: var(--accent-alt) !important;
}
-.nextPlace {
+.timeCol {
+ color: var(--yellow) !important;
+ white-space: nowrap;
+}
+
+.footer {
font-size: 13px;
color: var(--text-muted);
strong {
color: var(--yellow);
}
-}
-
-.nextPlaceMeta {
- font-size: 12px;
- color: var(--text-muted);
- opacity: 0.7;
}
\ No newline at end of file
diff --git a/src/games/hordeseer/assets/stats-panel.module.css b/src/games/hordeseer/assets/stats-panel.module.css
index 1a2bae1..b255e6e 100644
--- a/src/games/hordeseer/assets/stats-panel.module.css
+++ b/src/games/hordeseer/assets/stats-panel.module.css
@@ -1,6 +1,7 @@
.panel {
- width: 420px;
+ width: 520px;
min-width: 320px;
+ max-width: 50dvw;
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -15,6 +16,7 @@
@media (max-width: 768px) {
.panel {
width: 100%;
+ max-width: unset;
border-right: none;
border-bottom: 1px solid var(--border);
}
diff --git a/src/games/hordeseer/assets/workers-panel.module.css b/src/games/hordeseer/assets/workers-panel.module.css
index 78e78d6..7e096ee 100644
--- a/src/games/hordeseer/assets/workers-panel.module.css
+++ b/src/games/hordeseer/assets/workers-panel.module.css
@@ -5,6 +5,15 @@
overflow: hidden;
}
+@media (max-width: 768px) {
+ .panel {
+ width: 100%;
+ max-width: unset;
+ height: fit-content;
+ overflow: unset;
+ }
+}
+
.header {
display: flex;
flex-direction: row;
diff --git a/src/games/hordeseer/components/modals/leaderboard-modal.tsx b/src/games/hordeseer/components/modals/leaderboard-modal.tsx
index 3cf3c61..06f3a53 100644
--- a/src/games/hordeseer/components/modals/leaderboard-modal.tsx
+++ b/src/games/hordeseer/components/modals/leaderboard-modal.tsx
@@ -3,6 +3,7 @@ import { useBool } from "@common/hooks/useBool";
import { useHordeState } from "../../contexts/state";
import { fetchLeaderboard, type LeaderboardEntry } from "../../utils/api";
import { formatNumber, formatTime } from "@common/utils";
+import { calculateTotalKudosPerHour } from "../../utils/calculations";
import { Modal } from "@common/components/Modal";
import styles from "../../assets/leaderboard-modal.module.css";
@@ -19,9 +20,7 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
const ownWorkerIds = new Set(user?.worker_ids ?? []);
const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id));
- const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0);
- const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0);
- const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0;
+ const kudosPerHour = calculateTotalKudosPerHour(ownWorkers);
const [rows, setRows] = useState<(LeaderboardEntry & { rank: number; diff?: number })[]>([]);
const loading = useBool(false);
@@ -93,17 +92,8 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
return () => { cancelled = true; };
}, [open, user?.username]);
- const nextRow = rows.length >= 2 ? rows[rows.length - 2] : null;
- const footer = user && nextRow?.diff && nextRow.diff > 0 && kudosPerHour > 0
- ? (() => {
- const secs = Math.ceil((nextRow.diff / kudosPerHour) * 3600);
- return (
-
- Time to #{nextRow.rank}: {formatTime(secs)}
- ({formatNumber(Math.round(kudosPerHour))} kudos/h)
-
- );
- })()
+ const footer = user && kudosPerHour > 0
+ ?
: undefined;
return (
@@ -118,21 +108,36 @@ export const LeaderboardModal = ({ open, onClose }: Props) => {
Username |
Kudos |
{user && Diff | }
+ {user && kudosPerHour > 0 && Time | }
- {rows.map(r => (
-
- | {r.rank} |
- {r.username} |
- {formatNumber(r.kudos)} |
- {user && (
- 0 ? styles.above : styles.below) : undefined}>
- {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
- |
- )}
-
- ))}
+ {rows.map(r => {
+ const remainingSecs = r.diff !== undefined && r.diff > 0 && kudosPerHour > 0
+ ? Math.ceil((r.diff / kudosPerHour) * 3600)
+ : undefined;
+ return (
+
+ | {r.rank} |
+ {r.username} |
+ {formatNumber(r.kudos)} |
+ {user && (
+ 0 ? styles.above : styles.below) : undefined}>
+ {r.diff !== undefined ? `+${formatNumber(r.diff)}` : '—'}
+ |
+ )}
+ {user && kudosPerHour > 0 && (
+
+ {r.username === user?.username
+ ? '—'
+ : remainingSecs !== undefined
+ ? formatTime(remainingSecs)
+ : '—'}
+ |
+ )}
+
+ );
+ })}
)}
diff --git a/src/games/hordeseer/components/stats-panel.tsx b/src/games/hordeseer/components/stats-panel.tsx
index a2c2697..817ba59 100644
--- a/src/games/hordeseer/components/stats-panel.tsx
+++ b/src/games/hordeseer/components/stats-panel.tsx
@@ -1,6 +1,7 @@
import { useState } from "preact/hooks";
import { useHordeState } from "../contexts/state";
import { formatNumber, formatTime } from "@common/utils";
+import { calculateTotalKudosPerHour } from "../utils/calculations";
import styles from "../assets/stats-panel.module.css";
type ModelSortKey = 'name' | 'count' | 'queued' | 'jobs' | 'performance';
@@ -16,10 +17,9 @@ export const StatsPanel = () => {
const ownWorkerIds = new Set(user?.worker_ids ?? []);
const ownWorkers = workers.filter(w => ownWorkerIds.has(w.id));
- // Kudos/hour: sum generated kudos across own workers / total uptime hours
+ // Kudos/hour: sum of per-worker kudos/hour rates
+ const kudosPerHour = calculateTotalKudosPerHour(ownWorkers);
const totalUptime = ownWorkers.reduce((s, w) => s + w.uptime, 0);
- const totalGenerated = ownWorkers.reduce((s, w) => s + (w.kudos_details?.generated ?? 0), 0);
- const kudosPerHour = totalUptime > 0 ? (totalGenerated / totalUptime) * 3600 : 0;
// Own model names set
const ownModelNames = new Set(ownWorkers.flatMap(w => w.models));
@@ -27,11 +27,12 @@ export const StatsPanel = () => {
// Sort models
const sortedModels = [...models].sort((a, b) => {
let cmp = 0;
- if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
- else if (sortKey === 'count') cmp = a.count - b.count;
+ if (sortKey === 'name') cmp = a.name.localeCompare(b.name);
+ else if (sortKey === 'count') cmp = a.count - b.count;
else if (sortKey === 'queued') cmp = a.queued - b.queued;
- else if (sortKey === 'jobs') cmp = a.jobs - b.jobs;
- else cmp = a.performance - b.performance;
+ else if (sortKey === 'jobs') cmp = a.jobs - b.jobs;
+ else cmp = a.performance - b.performance;
+
return sortDir === 'asc' ? cmp : -cmp;
});
@@ -45,14 +46,13 @@ export const StatsPanel = () => {
return (