{title}
+{mod.modId}
++ configured v{configuredVersion || '-'} + {latestVersion && latestVersion !== configuredVersion + ? ` · latest v${latestVersion}` + : ''} +
++ {description ?? 'No description available.'} +
+From 8748807d1fbbdf8ac857db1155bfe22ab50769e1 Mon Sep 17 00:00:00 2001
From: SowinskiBraeden
- Dependencies (add these too) -
-Dependencies
+{currentName}
+{shortScenario(current)}
- {shortScenario(current)} + {current}
- No scenario listing found in the current log — make sure the server runs with - -listScenarios and has booted recently. -
- ))} + placeholder="{FDE33AFE2ED7875B}Missions/23_Campaign_Montignac.conf" + className="input w-full font-mono text-xs" + /> +{message}
} diff --git a/apps/web/src/components/ui.tsx b/apps/web/src/components/ui.tsx index a262cc5..4009fcf 100644 --- a/apps/web/src/components/ui.tsx +++ b/apps/web/src/components/ui.tsx @@ -56,7 +56,11 @@ export function ModImage({ src, className = '' }: { src: string | null; classNam } const STATUS_STYLES: Record
+ {streamLines.join('\n')}
+
+ )
+ ) : pollLoading ? (
{error.message}
+ ) : pollError ? ( +{pollError.message}
) : (
- {data?.lines.join('\n')}
+ {pollData?.lines.join('\n')}
)}
- Read-only tail of the current Reforger console log, downloaded through the Pterodactyl - API. Visible to owner and server admins only. + {mode === 'stream' + ? 'Live log tail streamed via SSE (polls every 2 s). Switch to polling for manual refresh.' + : 'Read-only tail of the current Reforger console log, downloaded through the Pterodactyl API.'} + {' '}Visible to owner and server admins only.
diff --git a/apps/web/src/pages/mods.tsx b/apps/web/src/pages/mods.tsx index 8c67c93..819a31c 100644 --- a/apps/web/src/pages/mods.tsx +++ b/apps/web/src/pages/mods.tsx @@ -1,9 +1,20 @@ -import { useState } from 'react'; -import type { CurrentUser, ReforgerConfigMod, WorkshopModDetail } from '@reforger-panel/shared'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import type { + CurrentUser, + ModDependencyIssue, + ModsCheckResponse, + ReforgerConfigMod, + WorkshopModDetail, + WorkshopModPreview, +} from '@reforger-panel/shared'; import { api } from '../api/client.js'; import { + useConfiguration, useServerMods, + useServerModsCheck, useServers, + useSetPerformanceSettings, useSetServerMods, useWorkshopMod, useWorkshopSearch, @@ -12,6 +23,9 @@ import { formatRelativeTime } from '../lib/format.js'; import { Button, Card, EmptyState, ModImage, Spinner } from '../components/ui.js'; const COMMON_WORKSHOP_TAGS = [ + 'SCENARIO', + 'SCENARIOS_MP', + 'SCENARIOS_SP', 'WEAPONS', 'VEHICLES', 'MISSIONS', @@ -28,6 +42,19 @@ const WORKSHOP_SORTS = [ { value: 'version_size', label: 'Size' }, ] as const; +const WORKSHOP_MOD_DETAIL_STALE_MS = 5 * 60_000; +const WORKSHOP_DETAIL_BATCH_INTERVAL_MS = 1_000; +const WORKSHOP_DETAIL_BATCH_SIZE = 4; +const WORKSHOP_DETAIL_BURST_CAPACITY = 20; +const WORKSHOP_DETAIL_FAILURE_COOLDOWN_MS = 60_000; +const WORKSHOP_DETAIL_PREFETCH_MARGIN = '500px'; + +const workshopDetailLimiter = { + inFlight: false, + burstTokens: WORKSHOP_DETAIL_BURST_CAPACITY, + lastTokenRefillAt: Date.now(), +}; + export function ModsPage({ user }: { user: CurrentUser }) { const { data: serversData } = useServers(); const slug = serversData?.servers[0]?.slug; @@ -39,145 +66,717 @@ function sameMods(a: ReforgerConfigMod[], b: ReforgerConfigMod[]): boolean { return JSON.stringify(a) === JSON.stringify(b); } +type ModsTab = 'installed' | 'browse'; + +const DEFAULT_SCENARIO_ID = '{FDE33AFE2ED7875B}Missions/23_Campaign_Montignac.conf'; + function ModsBody({ slug, user }: { slug: string; user: CurrentUser }) { const canManage = user.capabilities.includes('mods.manage'); + const canEditConfig = user.capabilities.includes('config.edit'); const { data, isLoading, error, refetch } = useServerMods(slug); + const { data: configData } = useConfiguration(slug); + const currentScenarioId = configData?.config.scenarioId ?? null; const save = useSetServerMods(slug); + const savePerf = useSetPerformanceSettings(slug); const [draft, setDraft] = useState- Review the live server mod list, stage changes, and pull metadata from the Reforger - Workshop before saving config.json. + Manage the server mod list and browse the Reforger Workshop. Changes apply on the next + server restart.
{error.message}
- ) : mods.length === 0 ? ( -- {mod.name ?? mod.modId} -
-- {mod.modId} - {mod.version ? ` · v${mod.version}` : ' · latest version'} -
-{message}
} -- Changes are written directly to the server's config.json (a config.json.bak backup is - kept) and take effect on the next server restart. -
- +{loadError}
+ ) : mods.length === 0 ? ( +Mission will be unavailable
++ The configured mission{' '} + + {checkResult.orphanedMission.name ?? + checkResult.orphanedMission.scenarioId.split('/').pop()} + {' '} + is not provided by any installed mod or official content. The server will fail to start. +
+ {canEditConfig ? ( +Switch the mission on the Configuration page.
+ )} + {resetMissionMessage && ( +{resetMissionMessage}
+ )} ++ {checkResult.modsWithMissingDeps.length} mod + {checkResult.modsWithMissingDeps.length !== 1 ? 's' : ''} have missing dependencies ( + {uniqueMissingDeps.length} unique) +
+ {canManage && ( +All dependencies are installed.
+ )} + + {message &&{message}
} ++ Changes are written to config.json (backup kept) and take effect on restart. +
+{mod.modId}
++ configured v{configuredVersion || '-'} + {latestVersion && latestVersion !== configuredVersion + ? ` · latest v${latestVersion}` + : ''} +
++ {description ?? 'No description available.'} +
+