diff --git a/README.md b/README.md
index e2bf816..0eb1487 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,11 @@ statt alles über vier Bereiche verteilt. Für „Willkommen" also Kanal, Karte,
Farben, Vorschau und Begrüßungstext an einem Ort. Die Adresse merkt sich die
Seite (`/settings#module/welcome`).
+Die Funktionen, die man dauernd anfasst, stehen zusätzlich **oben in der
+Seitenleiste** unter *Meine Funktionen* — ein Klick statt Umweg übers Raster.
+Welche das sind, entscheidet der Stern auf der Modul-Karte (höchstens acht,
+damit die Leiste kurz bleibt).
+
Gebaut wird sie aus den Registern: [`src/modules.js`](src/modules.js) kennt die
Felder, [`src/templates.js`](src/templates.js) die Texte, [`src/tuning.js`](src/tuning.js)
die Werte — jeweils über `module` verknüpft. Eine neue Funktion bekommt ihre
diff --git a/frontend/src/components/ModulePage.jsx b/frontend/src/components/ModulePage.jsx
index f164e92..8ee0e55 100644
--- a/frontend/src/components/ModulePage.jsx
+++ b/frontend/src/components/ModulePage.jsx
@@ -7,7 +7,7 @@
import { useEffect, useState } from 'react';
import { apiGet, apiPut, apiPost } from '../api.js';
import { moduleIcon } from '../module-icons.jsx';
-import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown } from '../icons.jsx';
+import { IconArrowLeft, IconWarning, IconCheck, IconRefresh, IconChevronDown, IconStar } from '../icons.jsx';
/** Ein Feld aus dem Register — Art entscheidet über die Darstellung */
function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) {
@@ -64,7 +64,7 @@ function Feld({ feld, value, onChange, PickSelect, channelItems, roleItems }) {
*/
export default function ModulePage(props) {
const { id, onBack, onToggle, form, setForm, PickSelect, channelItems, roleItems,
- welcomePreviewUrl, flash, goTab, reloadSettings } = props;
+ welcomePreviewUrl, flash, goTab, reloadSettings, favorites, onToggleFavorite } = props;
const [detail, setDetail] = useState(null);
const [fehler, setFehler] = useState(false);
@@ -141,6 +141,15 @@ export default function ModulePage(props) {
{detail.name}
{detail.desc}
+ onToggleFavorite(detail.id)}
+ >
+
+
{
setModules(d.modules);
setModuleGroups(d.groups);
+ setFavorites(d.favorites ?? []);
+ setMaxFavorites(d.maxFavorites ?? 8);
}).catch(() => {});
apiGet('/api/texts').then((d) => {
setTemplates(d.templates);
@@ -526,6 +531,21 @@ export default function Settings({ me }) {
}
}
+ /** Funktion in der Seitenleiste an- oder abheften */
+ async function toggleFavorite(id) {
+ const dabei = favorites.includes(id);
+ const neu = dabei ? favorites.filter((f) => f !== id) : [...favorites, id];
+ const vorher = favorites;
+ setFavorites(neu); // sofort umschalten
+ try {
+ const res = await apiPut('/api/module-favorites', { ids: neu });
+ setFavorites(res.favorites);
+ } catch (error) {
+ setFavorites(vorher); // bei Ablehnung zurück
+ flash(`✗ ${error.body?.error ?? 'Fehlgeschlagen'}`);
+ }
+ }
+
/** Vorschau holen: was würde der Assistent anlegen? */
async function loadPlan() {
try {
@@ -2387,6 +2407,8 @@ export default function Settings({ me }) {
flash={flash}
goTab={setTab}
reloadSettings={reloadSettings}
+ favorites={favorites}
+ onToggleFavorite={toggleFavorite}
/>
) : (
@@ -2431,6 +2453,15 @@ export default function Settings({ me }) {
{groupLabel(m.group)}
+
toggleFavorite(m.id)}
+ >
+
+
Nichts gefunden.
)}
+ {/* Angeheftete Funktionen — direkt erreichbar, ohne Umweg
+ übers Raster. Bei aktiver Suche ausgeblendet, dort
+ zählen Treffer. */}
+ {!query && canTab('module') && favorites.length > 0 && (
+
+ Meine Funktionen
+ {favorites.map((id) => {
+ const m = modules.find((x) => x.id === id);
+ if (!m) return null;
+ const Icon = moduleIcon(id);
+ return (
+ { setTabState('module'); setOpenModule(id); }}
+ >
+
+ {m.name}
+
+ );
+ })}
+
+ )}
+
{TAB_SECTIONS.map((section) => {
const items = foundTabs.filter((t) => t.section === section.id);
if (items.length === 0) return null;
@@ -2916,7 +2971,7 @@ export default function Settings({ me }) {
{items.map((t) => (
setTab(t.id)}
>
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 00f7593..d7dddd6 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -726,6 +726,19 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
.btn-create:hover:not(:disabled) { background: rgba(245, 197, 24, .1); }
.btn-create:disabled { opacity: .4; cursor: default; }
+/* Stern: heftet eine Funktion in die Seitenleiste. Ungeheftet nur angedeutet,
+ damit die Karten nicht wie ein Sternenhimmel aussehen. */
+.mod-fav {
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
+ background: none; border: 0; padding: .15rem;
+ color: var(--muted2); opacity: .35; cursor: pointer;
+ transition: color .15s, opacity .15s;
+}
+.mod-card:hover .mod-fav, .modpage-head .mod-fav { opacity: .75; }
+.mod-fav:hover { color: var(--neon); opacity: 1; }
+.mod-fav.on { color: var(--neon); opacity: 1; }
+.mod-fav.on svg { fill: currentColor; }
+
/* ── MODUL-SEITE ───────────────────────────────────── */
/* Name und Symbol auf der Karte öffnen die Seite der Funktion */
.mod-open {
diff --git a/src/modules.js b/src/modules.js
index 1085524..0f33052 100644
--- a/src/modules.js
+++ b/src/modules.js
@@ -282,6 +282,44 @@ export const MODULES = [
},
];
+/**
+ * Funktionen, die in der Seitenleiste stehen — die paar, die man dauernd
+ * anfasst. Der Rest bleibt im Raster.
+ *
+ * Der Standard ist eine Vermutung, kein Gesetz: geändert wird er über den
+ * Stern auf der Modul-Karte. Deckel bei acht, damit die Leiste kurz bleibt —
+ * sonst ist der Gewinn gegenüber dem Raster wieder weg.
+ */
+export const DEFAULT_FAVORITES = ['devlogs', 'welcome', 'tickets', 'starboard', 'gallery'];
+export const MAX_FAVORITES = 8;
+
+/** Aktuelle Auswahl — nur existierende Module, in gespeicherter Reihenfolge */
+export function moduleFavorites() {
+ const gespeichert = String(getSetting('module_favorites') ?? '').trim();
+ if (!gespeichert) return DEFAULT_FAVORITES.filter((id) => MODULES.some((m) => m.id === id));
+ // Ein einzelner Bindestrich heißt „bewusst keine" — sonst käme der Standard zurück
+ if (gespeichert === '-') return [];
+ return gespeichert
+ .split(',')
+ .map((s) => s.trim())
+ .filter((id) => MODULES.some((m) => m.id === id))
+ .slice(0, MAX_FAVORITES);
+}
+
+/**
+ * Auswahl setzen.
+ * @returns {{ ok: true, favorites: string[] } | { ok: false, error: string }}
+ */
+export function setModuleFavorites(ids) {
+ if (!Array.isArray(ids)) return { ok: false, error: 'Liste erwartet' };
+ const gefiltert = [...new Set(ids.map(String))].filter((id) => MODULES.some((m) => m.id === id));
+ if (gefiltert.length > MAX_FAVORITES) {
+ return { ok: false, error: `Höchstens ${MAX_FAVORITES} Funktionen in der Seitenleiste` };
+ }
+ setSetting('module_favorites', gefiltert.length ? gefiltert.join(',') : '-');
+ return { ok: true, favorites: gefiltert };
+}
+
/** Einstellungs-Name des Schalters für ein Modul */
function toggleKey(mod) {
return mod.setting ?? `module_${mod.id}`;
diff --git a/src/web/api.js b/src/web/api.js
index 0bebec6..2784b0b 100644
--- a/src/web/api.js
+++ b/src/web/api.js
@@ -21,7 +21,7 @@ import {
savePage, getPage, listPages, listPublishedPages, deletePage,
} from '../db.js';
import crypto from 'node:crypto';
-import { moduleStates, moduleDetail, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js';
+import { moduleStates, moduleDetail, setModuleEnabled, moduleFavorites, setModuleFavorites, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
import { tuningStates, setTuning, TUNING_GROUPS } from '../tuning.js';
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
@@ -190,7 +190,21 @@ export function registerApiRoutes(app, client) {
app.get('/api/modules', async (request, reply) => {
if (requireAnyScope(request, reply)) return;
- return { modules: moduleStates(), groups: MODULE_GROUPS };
+ return {
+ modules: moduleStates(),
+ groups: MODULE_GROUPS,
+ favorites: moduleFavorites(),
+ maxFavorites: MAX_FAVORITES,
+ };
+ });
+
+ // Welche Funktionen in der Seitenleiste stehen
+ app.put('/api/module-favorites', async (request, reply) => {
+ if (requireScope(request, reply, 'settings')) return;
+ const res = setModuleFavorites(request.body?.ids ?? []);
+ if (!res.ok) return reply.code(400).send({ error: res.error });
+ logAudit(getSessionUser(request), 'seitenleiste geändert', res.favorites.join(', ') || 'leer');
+ return { ok: true, favorites: res.favorites };
});
// Alles zu einer Funktion an einem Ort: ihre Kanäle und Felder, ihre Texte,