feat: fix mods tab, add client/server split, and scaffold server dirs
Mods tab bug fixes:
- mod_manager: fix wrong kwargs in set_enabled_mods, fix scan dir to use
mods/ subdir instead of server root, migrate old string-list format to
dict format on read
- service: replace dead server_mods SQL JOIN with get_enabled_mods()
call through the mod_manager capability; pass is_server_mod to
build_mod_args
- mods_router: accept list[EnabledModEntry] objects (name + is_server_mod)
instead of bare strings
Client/server mod split:
- Mods now stored as list[{"name": str, "is_server_mod": bool}]; old
string-list format auto-migrated on read
- is_server_mod=true routes to -serverMod= arg; false to -mod= arg
- ModList UI: amber Client/Server badge in selected pane; toggle button
in split-pane selector
Directory scaffold:
- process_config: adds "mods" to dir layout; provides get_dir_readme()
with per-directory README.txt content
- file_utils: ensure_server_dirs() gains readme_provider kwarg; writes
README.txt idempotently if absent
- service.create_server: passes readme_provider via hasattr probe
- main.py startup: backfills all existing servers with correct subdirs
and README files (idempotent)
Docs: API.md and FRONTEND.md updated for new mod schema and types
Test __init__.py files added for pytest discovery
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Save } from "lucide-react";
|
||||
import { Save, Server } from "lucide-react";
|
||||
|
||||
import { useServerMods, useSetEnabledMods } from "@/hooks/useServerDetail";
|
||||
import type { Mod } from "@/hooks/useServerDetail";
|
||||
@@ -38,14 +38,25 @@ export function ModList({ serverId }: ModListProps) {
|
||||
setAvailable((prev) => [...prev, { ...mod, enabled: false }].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
};
|
||||
|
||||
const toggleServerMod = (modName: string) => {
|
||||
setSelected((prev) =>
|
||||
prev.map((m) => m.name === modName ? { ...m, is_server_mod: !m.is_server_mod } : m),
|
||||
);
|
||||
};
|
||||
|
||||
const _selectedKey = (mods: Mod[]) =>
|
||||
mods.map((m) => `${m.name}:${m.is_server_mod}`).sort().join(",");
|
||||
|
||||
const hasChanges = modsData !== undefined && (
|
||||
selected.map((m) => m.name).sort().join(",") !==
|
||||
(modsData.mods.filter((m) => m.enabled).map((m) => m.name).sort().join(","))
|
||||
_selectedKey(selected) !==
|
||||
_selectedKey(modsData.mods.filter((m) => m.enabled))
|
||||
);
|
||||
|
||||
const handleApply = async () => {
|
||||
try {
|
||||
await setEnabledMods.mutateAsync(selected.map((m) => m.name));
|
||||
await setEnabledMods.mutateAsync(
|
||||
selected.map((m) => ({ name: m.name, is_server_mod: m.is_server_mod })),
|
||||
);
|
||||
addNotification({ type: "success", message: `${selected.length} mod(s) enabled. Server restart required.` });
|
||||
} catch (err) {
|
||||
logger.error("ModList", "Failed to apply mods: %s", err);
|
||||
@@ -144,6 +155,7 @@ export function ModList({ serverId }: ModListProps) {
|
||||
mod={mod}
|
||||
actionLabel="←"
|
||||
onAction={isAdmin ? () => moveToAvailable(mod) : undefined}
|
||||
onToggleServerMod={isAdmin ? () => toggleServerMod(mod.name) : undefined}
|
||||
selected
|
||||
/>
|
||||
))
|
||||
@@ -159,17 +171,17 @@ function ModRow({
|
||||
mod,
|
||||
actionLabel,
|
||||
onAction,
|
||||
onToggleServerMod,
|
||||
selected = false,
|
||||
}: {
|
||||
mod: Mod;
|
||||
actionLabel: string;
|
||||
onAction?: () => void;
|
||||
onToggleServerMod?: () => void;
|
||||
selected?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-surface-recessed shadow-neu-recessed"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-surface-recessed shadow-neu-recessed">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-text-primary text-sm font-medium truncate">
|
||||
{mod.display_name ?? mod.name}
|
||||
@@ -186,6 +198,21 @@ function ModRow({
|
||||
<span className="text-text-muted text-xs">{formatSize(mod.size_bytes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{selected && onToggleServerMod && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleServerMod}
|
||||
title={mod.is_server_mod ? "Server-only mod (-serverMod). Click to switch to client mod (-mod)" : "Client mod (-mod). Click to switch to server-only (-serverMod)"}
|
||||
className={`flex items-center gap-1 text-xs px-1.5 py-0.5 rounded shrink-0 transition-colors ${
|
||||
mod.is_server_mod
|
||||
? "bg-amber-500/20 text-amber-400"
|
||||
: "bg-surface-raised text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
<Server size={10} />
|
||||
{mod.is_server_mod ? "Server" : "Client"}
|
||||
</button>
|
||||
)}
|
||||
{onAction && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user