LS bekommt einen festen Platz unter Gaming
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Das Modul stand nur unter "Meine Funktionen" — also solange, wie du es
angeheftet laesst. Loest man die Nadel, ist es nur noch uebers Modul-Raster
erreichbar. Fuer die groesste Seite im Panel zu wenig.

Jetzt kann ein Modul `nav: true` setzen und bekommt damit einen Eintrag in der
Seitenleiste, unter seiner eigenen Gruppe. Kein Sonderfall fuer LS: jedes
Modul, das eine ganze Seite fuellt, kann sich so einsortieren.

Dabei aufgefallen und mitgenommen: eine halbe Antwort von /api/modules/:id hat
die ganze Config weiss gemacht. Der Ladepfad ist zwar sauber abgesichert —
`fehler` und `!detail` fangen ab —, aber ein Objekt OHNE die Listen kommt
durch beide Wachen und laesst dann `detail.werte.some(...)` werfen, was React
mit dem gesamten Baum quittiert. Vier Listen mit `?? []` kosten nichts und
machen aus dem Weissbild schlimmstenfalls eine leere Sektion.

Zwei Werkzeug-Fallen nebenbei: `npx vite build` zieht mal das lokale Vite 6,
mal ein gecachtes Vite 8 aus dem npx-Ordner — ab jetzt nur noch
`npm --prefix frontend run build`. Und der laufende Preview-Server haelt
dist/assets fest, was den Build mit EPERM abbricht; vorher stoppen.

Geprueft: Seitenleiste zeigt Gaming mit Game-Server und LS, Klick oeffnet die
Modulseite und markiert den Eintrag, kein Fehler in der Konsole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 08:38:52 +02:00
co-authored by Claude Opus 5
parent 9f929fe0e4
commit 5de172f862
3 changed files with 47 additions and 17 deletions
+22 -15
View File
@@ -80,6 +80,13 @@ export default function ModulePage(props) {
if (fehler) return <p className="notice">Diese Funktion konnte nicht geladen werden.</p>; if (fehler) return <p className="notice">Diese Funktion konnte nicht geladen werden.</p>;
if (!detail) return <p className="notice">Lade </p>; if (!detail) return <p className="notice">Lade </p>;
// Listen notfalls auffüllen. Eine halbe Antwort — abgeschnitten, veraltet,
// von einem Proxy verstümmelt — hat sonst die ganze Config weiß gemacht:
// `werte.some(...)` wirft, und React reisst den Baum mit.
const felder = detail.felder ?? [];
const texte = detail.texte ?? [];
const werte = detail.werte ?? [];
const missing = detail.missing ?? [];
const Icon = moduleIcon(detail.id); const Icon = moduleIcon(detail.id);
@@ -97,7 +104,7 @@ export default function ModulePage(props) {
async function werteSpeichern() { async function werteSpeichern() {
const geaendert = Object.fromEntries( const geaendert = Object.fromEntries(
Object.entries(werteEntwurf).filter(([wid, v]) => { Object.entries(werteEntwurf).filter(([wid, v]) => {
const jetzt = detail.werte.find((w) => w.id === wid); const jetzt = werte.find((w) => w.id === wid);
return jetzt && String(v) !== String(jetzt.value); return jetzt && String(v) !== String(jetzt.value);
}) })
); );
@@ -125,7 +132,7 @@ export default function ModulePage(props) {
} }
} }
const werteOffen = detail.werte.some( const werteOffen = werte.some(
(w) => werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value) (w) => werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value)
); );
@@ -159,22 +166,22 @@ export default function ModulePage(props) {
</button> </button>
</div> </div>
{detail.enabled && detail.missing.length > 0 && ( {detail.enabled && missing.length > 0 && (
<p className="mod-missing with-icon" style={{ marginBottom: '1.2rem' }}> <p className="mod-missing with-icon" style={{ marginBottom: '1.2rem' }}>
<IconWarning size={13} /> Fehlt noch: {detail.missing.map((m) => m.label).join(', ')} <IconWarning size={13} /> Fehlt noch: {missing.map((m) => m.label).join(', ')}
</p> </p>
)} )}
{detail.enabled && detail.missing.length === 0 && ( {detail.enabled && missing.length === 0 && (
<p className="mod-ready with-icon" style={{ marginBottom: '1.2rem' }}> <p className="mod-ready with-icon" style={{ marginBottom: '1.2rem' }}>
<IconCheck size={13} /> Einsatzbereit <IconCheck size={13} /> Einsatzbereit
</p> </p>
)} )}
{/* Kanäle, Rollen und alles Weitere */} {/* Kanäle, Rollen und alles Weitere */}
{detail.felder.length > 0 && ( {felder.length > 0 && (
<div className="modpage-block"> <div className="modpage-block">
<h3 className="settings-title">Einstellungen</h3> <h3 className="settings-title">Einstellungen</h3>
{detail.felder.filter((f) => f.art !== 'farbe').map((f) => ( {felder.filter((f) => f.art !== 'farbe').map((f) => (
<div className="field" key={f.setting}> <div className="field" key={f.setting}>
{f.art !== 'schalter' && ( {f.art !== 'schalter' && (
<label htmlFor={`feld-${f.setting}`}> <label htmlFor={`feld-${f.setting}`}>
@@ -192,8 +199,8 @@ export default function ModulePage(props) {
roleItems={roleItems} roleItems={roleItems}
/> />
{/* Fehlt eine Voraussetzung, kann der Bot sie anlegen */} {/* Fehlt eine Voraussetzung, kann der Bot sie anlegen */}
{f.pflicht && detail.missing.some((m) => m.setting === f.setting) {f.pflicht && missing.some((m) => m.setting === f.setting)
&& detail.missing.find((m) => m.setting === f.setting)?.create && ( && missing.find((m) => m.setting === f.setting)?.create && (
<button className="btn-mini btn-create" onClick={() => anlegen(f.setting)}> <button className="btn-mini btn-create" onClick={() => anlegen(f.setting)}>
{f.art === 'rolle' ? 'Rolle anlegen' : 'Kanal anlegen'} {f.art === 'rolle' ? 'Rolle anlegen' : 'Kanal anlegen'}
</button> </button>
@@ -202,9 +209,9 @@ export default function ModulePage(props) {
</div> </div>
))} ))}
{detail.felder.some((f) => f.art === 'farbe') && ( {felder.some((f) => f.art === 'farbe') && (
<div className="wc-colors"> <div className="wc-colors">
{detail.felder.filter((f) => f.art === 'farbe').map((f) => ( {felder.filter((f) => f.art === 'farbe').map((f) => (
<Feld <Feld
key={f.setting} key={f.setting}
feld={f} feld={f}
@@ -229,11 +236,11 @@ export default function ModulePage(props) {
)} )}
{/* Texte dieser Funktion */} {/* Texte dieser Funktion */}
{detail.texte.length > 0 && ( {texte.length > 0 && (
<div className="modpage-block"> <div className="modpage-block">
<h3 className="settings-title">Texte</h3> <h3 className="settings-title">Texte</h3>
<div className="tpl-list"> <div className="tpl-list">
{detail.texte.map((t) => { {texte.map((t) => {
const entwurf = textEntwurf[t.id]; const entwurf = textEntwurf[t.id];
const wert = entwurf ?? t.text; const wert = entwurf ?? t.text;
const geaendert = entwurf != null && entwurf !== t.text; const geaendert = entwurf != null && entwurf !== t.text;
@@ -294,11 +301,11 @@ export default function ModulePage(props) {
)} )}
{/* Werte dieser Funktion */} {/* Werte dieser Funktion */}
{detail.werte.length > 0 && ( {werte.length > 0 && (
<div className="modpage-block"> <div className="modpage-block">
<h3 className="settings-title">Werte</h3> <h3 className="settings-title">Werte</h3>
<div className="tune-list"> <div className="tune-list">
{detail.werte.map((w) => { {werte.map((w) => {
const wert = werteEntwurf[w.id] ?? w.value; const wert = werteEntwurf[w.id] ?? w.value;
const geaendert = werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value); const geaendert = werteEntwurf[w.id] !== undefined && String(werteEntwurf[w.id]) !== String(w.value);
return ( return (
+19 -1
View File
@@ -4103,7 +4103,12 @@ export default function Settings({ me }) {
{TAB_SECTIONS.map((section) => { {TAB_SECTIONS.map((section) => {
const items = foundTabs.filter((t) => t.section === section.id); const items = foundTabs.filter((t) => t.section === section.id);
if (items.length === 0) return null; // Module mit `nav` bekommen hier einen festen Platz —
// sonst sind sie nur übers Raster erreichbar oder
// solange man sie angeheftet lässt.
const eigene = modules.filter((m) => m.nav && m.group === section.id
&& (!query || m.name.toLowerCase().includes(query.toLowerCase())));
if (items.length === 0 && eigene.length === 0) return null;
return ( return (
<div className="settings-nav-group" key={section.id}> <div className="settings-nav-group" key={section.id}>
<span className="settings-nav-label">{section.label}</span> <span className="settings-nav-label">{section.label}</span>
@@ -4117,6 +4122,19 @@ export default function Settings({ me }) {
{t.label} {t.label}
</button> </button>
))} ))}
{eigene.map((m) => {
const Icon = moduleIcon(m.id);
return (
<button
key={m.id}
className={`settings-nav-item ${openModule === m.id ? 'active' : ''}`}
onClick={() => { setTabState('module'); setOpenModule(m.id); }}
>
<span className="settings-nav-icon"><Icon /></span>
{m.name}
</button>
);
})}
</div> </div>
); );
})} })}
+6 -1
View File
@@ -247,7 +247,7 @@ export const MODULES = [
tool: { tab: 'server', label: 'Game-Server verwalten' }, tool: { tab: 'server', label: 'Game-Server verwalten' },
}, },
{ {
id: 'ls_farm', group: 'gaming', name: 'LS — Höfe & Preise', id: 'ls_farm', group: 'gaming', name: 'LS — Höfe & Preise', nav: true,
desc: 'Landwirtschafts-Simulator: Besitzkarte mit Höfen, Fahrzeugen und Spielern, ' desc: 'Landwirtschafts-Simulator: Besitzkarte mit Höfen, Fahrzeugen und Spielern, '
+ 'dazu die Preiskurven übers Jahr. Braucht einen LS-Server mit Zugangs-Code unter „Game-Server".', + 'dazu die Preiskurven übers Jahr. Braucht einen LS-Server mit Zugangs-Code unter „Game-Server".',
tab: 'server', setting: 'ls_farm_enabled', tab: 'server', setting: 'ls_farm_enabled',
@@ -481,6 +481,11 @@ export function moduleStates() {
desc: mod.desc, desc: mod.desc,
group: mod.group, group: mod.group,
tab: mod.tab ?? null, tab: mod.tab ?? null,
// Bekommt einen festen Platz in der Seitenleiste, unter der eigenen
// Gruppe. Für Module gedacht, die eine ganze Seite füllen — sonst
// erreicht man sie nur über das Raster oder als angeheftete Funktion,
// und dann steht ausgerechnet das grösste Modul nirgends.
nav: mod.nav === true,
enabled: moduleEnabled(mod.id), enabled: moduleEnabled(mod.id),
missing: moduleMissing(mod), missing: moduleMissing(mod),
})); }));