Skip to content

Dashboard Integration DE

Domekologe edited this page Jun 30, 2026 · 1 revision

Dashboard-Integration

🌐 English · Deutsch

Dies ist die Entwickler-Anleitung, um einen eigenen Red-DiscordBot-Cog an das PDC Web Dashboard anzubinden. Du deklarierst deine Beiträge — Widgets, Panels, Listen, Seiten — mit einer Handvoll Decorators und lieferst deklarative Schemas zurück, niemals rohes HTML. Das Frontend rendert sie in eine gemeinsame, themebare Oberfläche.

Zwei Prinzipien machen das sicher und reibungslos: Es gibt keine harte Abhängigkeit (dein Cog lädt und läuft auch ohne das Dashboard, weil die Decorators dann zu No-ops werden), und es läuft parallel zu AAA3As Red-Web-Dashboard (unterschiedliche Marker-Attribute, keine Kollision). Du kannst einen einzigen Cog ausliefern, der beide Dashboards gleichzeitig bedient.

Screenshot: Widget und Einstellungs-Panel eines Cogs im PDC-Dashboard

Die kanonische Referenz ist der dashboardtemplate-Cog (ein voll kommentiertes Template, das nichts Sinnvolles tut, aber jeden Beitragstyp zeigt) sowie der kleinere dashboardexample-Cog. Beide liegen in diesem Repository.


Das Drop-in-Prinzip

Kopiere die Datei pdc_dashboard.py 1:1 in deinen Cog-Ordner (sie liegt in jedem angebundenen Cog, z. B. dashboardtemplate/pdc_dashboard.py). Sie ist die gesamte Integrationsfläche — ein einzelnes Modul ohne externe Anforderungen.

Was diese Datei leistet:

Verhalten Detail
Importiert die echte Maschinerie Ist pdc_webdashboard installiert, re-exportiert sie die Decorators und Schema-Klassen aus pdc_webdashboard.integration.
Fällt auf No-ops zurück Ist pdc_webdashboard nicht installiert, wird jeder Decorator zum Durchreicher und jede Schema-Klasse zu einem harmlosen Stub. Dein Cog lädt unverändert.
Stellt DASHBOARD_AVAILABLE bereit Ein Boolean, das du zur Laufzeit prüfen kannst, um optional zu verzweigen.
Liefert Register-/Unregister-Helfer register_dashboard(cog) und unregister_dashboard(cog) — beide sicher aufrufbar, egal ob das Dashboard geladen ist.

Importiere dann aus deiner lokalen Kopie, nicht aus pdc_webdashboard:

from .pdc_dashboard import (
    dashboard_widget, dashboard_panel, dashboard_list, dashboard_page,
    WidgetData, PanelSchema, PageSchema, Field, Component, SubmitResult,
    register_dashboard, unregister_dashboard, DASHBOARD_AVAILABLE,
)

Wenn dein Cog ohnehin hart von pdc_webdashboard abhängt, kannst du auch direkt aus pdc_webdashboard.integration importieren — empfohlen ist aber die Drop-in-Kopie, damit der Cog eigenständig nutzbar bleibt.

Der Fallback ist voll robust: Selbst das No-op-@dashboard_panel hängt ein .on_submit an (und die No-op-Liste ihre Edit-/Delete-Handler), sodass dein Cog nicht abstürzt, wenn WebDashboard fehlt oder mitten im Reload ist.


Lebenszyklus: bedingt registrieren

Registriere in cog_load, deregistriere in cog_unload. Beide Aufrufe sind No-ops, wenn das Dashboard nicht geladen ist.

async def cog_load(self) -> None:
    register_dashboard(self)     # integriert NUR, wenn WebDashboard geladen ist
    # ... deine eigene Lade-Logik ...

def cog_unload(self) -> None:
    unregister_dashboard(self)   # immer sicher

register_dashboard deckt den Fall ab, dass dein Cog nach dem Dashboard geladen wird. Das Dashboard scannt beim Start zudem alle bereits geladenen Cogs automatisch, womit auch der Fall „Cog vor dem Dashboard geladen" abgedeckt ist — der explizite register_dashboard-Aufruf ist aber der robuste Weg für beide Reihenfolgen.


Das „Ein Modul mit Tabs"-Modell

Ein Cog steuert genau ein Modul zur Detailseite eines Servers bei. Alle Panels und Listen dieses Cogs werden zu Tabs innerhalb dieses Moduls, sortiert über das Schlüsselwort order= (kleiner = weiter links). So bleiben zusammengehörige Einstellungen beisammen, statt pro Cog eine isolierte Extra-Seite zu erzeugen. Nutze order= für eine bewusste Tab-Anordnung — z. B. ein „Anlegen"-Panel bei order=25 direkt links neben seiner Liste bei order=30.


Beitragstypen

@dashboard_widget — eine Kachel auf dem Board

Ein Widget ist eine kleine, schreibgeschützte Kachel auf dem zentralen Board / der Server-Übersicht. Die Methode erhält einen DashboardContext und liefert ein WidgetData.

@dashboard_widget("member_count", "Mitglieder", size="sm", refresh=60, permission="guild_member")
async def member_count(self, ctx):
    return WidgetData.kpi(value=ctx.guild.member_count, label="Mitglieder", icon="users")
Argument Bedeutung
identifier, name Stabile ID und Anzeigetitel.
size sm · md · lg.
refresh Auto-Refresh-Intervall in Sekunden (optional).
permission Mindest-Berechtigungsstufe.
scope guild (Standard) oder global.

WidgetData bietet Convenience-Konstruktoren je Widget-Art:

Konstruktor Rendert
WidgetData.kpi(value, label, …) Eine einzelne Kennzahl (mit optionalem trend, icon, intent).
WidgetData.list(items, …) Eine Liste von Einträgen.
WidgetData.chart(series, chart_type=…) Ein Mini-Chart (line · bar · area · doughnut).
WidgetData.status(state, label, …) Ein Status-Indikator (ok / warn / error).
WidgetData.markdown(text) Sicher gerendertes Markdown.

@dashboard_panel — ein kontextuelles Formular

Ein Panel ist ein eingebettetes Formular (ein Tab in deinem Modul), keine eigene Seite. Die dekorierte Methode liefert ein PanelSchema; der Speicher-Handler wird mit @<panel>.on_submit angehängt und erhält ein flaches {field_key: value}-Dict.

@dashboard_panel("welcome", "Willkommensnachricht", mount="guild_settings",
                 permission="guild_admin", order=10)
async def welcome_panel(self, ctx):
    cfg = await self.config.guild(ctx.guild).welcome()
    return PanelSchema(fields=[
        Field.switch("enabled", "Aktiviert", value=cfg["enabled"]),
        Field.textarea("message", "Nachricht", value=cfg["message"], max_length=2000),
        Field.select("channel", "Kanal", channel_options, value=str(cfg["channel"] or "")),
    ])

@welcome_panel.on_submit
async def save_welcome(self, ctx, data):
    await self.config.guild(ctx.guild).welcome.set(data)
    return SubmitResult.ok("Gespeichert.")
Argument Bedeutung
mount Einbettungsort: guild_settings (Server-Einstellungen) oder bot_settings (global, mit scope="global").
permission Mindeststufe (Standard guild_admin).
order Tab-Reihenfolge innerhalb des Moduls.
scope guild oder global. Für ein globales/Owner-Panel: scope="global", mount="bot_settings", permission="bot_owner" — und ctx.guild nicht anfassen (es ist None).

@dashboard_list — eine verwaltete Tabelle

Eine Liste rendert eine Tabelle mit Anlegen-/Bearbeiten-/Löschen-Aktionen. Die dekorierte Methode liefert Zeilen [{"id": ..., "cells": {column_key: value, …}}]. Drei optionale Handler vervollständigen den CRUD-Zyklus:

Handler Signatur Zweck
@<list>.edit_form (ctx, id) -> PanelSchema Das Formular zum Bearbeiten einer Zeile.
@<list>.on_edit (ctx, id, data) -> SubmitResult Speichert die bearbeitete Zeile.
@<list>.on_delete (ctx, id) -> SubmitResult Löscht eine Zeile.

Spalten werden am Decorator deklariert: columns=[{"key": "name", "label": "Name"}, …]. Kombiniere die Liste mit einem kleinen @dashboard_panel („Anlegen"-Tab) bei niedrigerem order=, um neue Einträge hinzuzufügen.

@dashboard_page — eine vollständige eigene Seite

Für den seltenen Fall, dass ein Panel/Tab nicht reicht, registriert @dashboard_page eine ganze Seite aus einem PageSchema von Component-Knoten (heading · text · table · chart · panel_ref · divider · grid) — ebenfalls ohne rohes HTML. Mit nav=False bleibt sie aus der Seitennavigation.


Deklarative Felder

Field-Builder bilden je eine Einstellung auf je ein Eingabefeld ab. Dass deklarative Felder (statt HTML) zurückkommen, eliminiert die XSS-Fläche vollständig.

Builder Eingabe
Field.text(key, label, …) Einzeiliger Text.
Field.textarea(key, label, max_length=…, variables=…) Mehrzeiliger Text; variables=[{"token": "{member}", "desc": "Mitglied"}] ergänzt Einfüge-Buttons.
Field.number(key, label, min=…, max=…) Begrenzte Zahl.
Field.switch(key, label) Boolescher Schalter.
Field.select(key, label, options, reload_on_change=…) Einfachauswahl aus options.
Field.multiselect(key, label, options) Mehrfachauswahl.
Field.channel(key, label) / Field.role(key, label) Kanal-/Rollen-Auswahl.

Zwei Feld-Optionen verdienen besondere Erwähnung:

  • reload_on_change=True an einem select — ändert der Nutzer den Wert, wird das Panel sofort gespeichert und neu geladen. Nutze es für eine Einstellung, von der andere Felder abhängen (z. B. Umschalten eines „Profils" oder einer Modul-Sprache, sodass sich der Rest des Formulars neu rendert).
  • variables=[…] an einer textarea — rendert klickbare Buttons, die ein Token ({member}, {server}, …) an der Cursorposition einfügen, damit Admins deine Platzhalter-Syntax nicht auswendig kennen müssen.

Ein Submit-Handler liefert bei Erfolg SubmitResult.ok("Nachricht") oder bei Validierungsfehlern SubmitResult.fail("Nachricht", errors={"field_key": "Grund"}), um feldspezifische Fehler anzuzeigen.


Lokalisierung (i18n)

Dein Cog kann sowohl Deutsch als auch Englisch ausliefern — für seine Dashboard-Texte und seine Discord-Ausgabe. Der Drop-in stellt dafür drei kleine Helfer bereit — importiere sie neben den Decorators; wie alles andere werden sie zu harmlosen No-ops, wenn pdc_webdashboard nicht installiert ist:

from .pdc_dashboard import L, tr, tr_lang

Jeder Helfer deckt eine andere Fläche ab und ermittelt seine Sprache unterschiedlich:

Helfer Verwenden für Folgt
L("Deutsch", "English") Einen Decorator-Namen oder eine Beschreibung — den name von @dashboard_panel / @dashboard_widget / @dashboard_list sowie das description= einer @dashboard_list. Der Website-UI-Sprache (dem DE/EN-Umschalter oben rechts). Tab-Titel folgen der Seite.
tr(ctx, "Deutsch", "English") Text, der aus einem Handler zurückkommtPanelSchema(description=…), eine SubmitResult.ok/fail(…)-Nachricht, ein eigenes submit_label oder WidgetData-Text. Der Website-UI-Sprache, über ctx.locale.
tr_lang(lang, "Deutsch", "English") Die Discord-Ausgabe des Cogs — Befehlsantworten, Embeds, DMs. Einer Pro-Gilde-Einstellung language, die der Cog selbst speichert (NICHT der Website-Sprache).

Übergibst du L nur ein Argument (z. B. L("Gleicher Text")), wird dieser Text für alle Sprachen verwendet.

Kernregel — Feld-LABELS bleiben Englisch. Halte eine einzige Quelle der Wahrheit: Nur Namen/Beschreibungen (Web-Sprache) und Discord-Ausgabe (pro Gilde) werden lokalisiert. Umhülle keine Field.*-Labels.

Web-Texte: L und tr

L lokalisiert, was das Gateway als Tab-Titel zeigt; tr lokalisiert, was ein Handler zurückgibt, über ctx.locale. Der Website-Umschalter lädt die Modul-Texte live neu, sodass ein DE/EN-Wechsel beide sofort aktualisiert.

@dashboard_panel("welcome", L("Willkommen", "Welcome"), mount="guild_settings",
                 permission="guild_admin", order=10)
async def welcome_panel(self, ctx):
    cfg = await self.config.guild(ctx.guild).welcome()
    return PanelSchema(
        description=tr(ctx, "Begrüße neue Mitglieder.", "Greet new members."),
        fields=[
            # LABELS bleiben Englisch — eine Quelle der Wahrheit:
            Field.switch("enabled", "Enabled", value=cfg["enabled"]),
            Field.textarea("message", "Message", value=cfg["message"], max_length=2000),
        ],
    )

@welcome_panel.on_submit
async def save_welcome(self, ctx, data):
    await self.config.guild(ctx.guild).welcome.set(data)
    return SubmitResult.ok(tr(ctx, "Gespeichert.", "Saved."))

Discord-Ausgabe: eine Pro-Gilde-Einstellung language

Discord-Antworten folgen einer Sprache, die du pro Gilde speicherst — unabhängig davon, wer gerade das Dashboard betrachtet. In drei Schritten verdrahtet:

  1. Standard registrieren in register_guild (oder deinen Defaults):

    self.config.register_guild(language="de-DE")
  2. Im Einstellungs-Panel anbieten als select (beachte: der name nutzt L, die Option-Labels sind aber schlicht). Nutze reload_on_change=True, damit der Rest des Formulars reagieren kann:

    Field.select("language", L("Sprache", "Language"),
                 [{"value": "de-DE", "label": "Deutsch"},
                  {"value": "en-US", "label": "English"}],
                 value=cfg["language"], reload_on_change=True),

    …und persistiere sie in deinem on_submit.

  3. Jeden Befehl lokalisieren, indem du diese Einstellung liest und die Ausgabe mit tr_lang umhüllst:

    @commands.command()
    async def hello(self, ctx):
        lang = await self.config.guild(ctx.guild).language()
        await ctx.send(tr_lang(lang, "Hallo!", "Hello!"))

Referenz-Cogs

Die Cogs dashboardtemplate und dashboardexample sind voll funktionsfähige Referenzen für alle drei Helfer — übernimm ihre Muster wortwörtlich.


Befehle im Dashboard

Das Dashboard listet auch die Befehle deines Cogs auf. Ein paar Muster lassen sie nativ wirken — zweisprachige Beschreibungen, Slash-Formen und die Stolperfallen, die sie still zerlegen.

Zweisprachige Befehlsbeschreibungen

Ein Befehl kann seine Beschreibung in der im Dashboard gewählten Sprache anzeigen. Ergänze eine i18n_desc-Map im extras= des Befehls:

@commands.hybrid_command(
    name="greet",
    description="Greet a member.",
    extras={"i18n_desc": {"de-DE": "Ein Mitglied begrüßen.", "en-US": "Greet a member."}},
)
async def greet(self, ctx, member: discord.Member):
    ...

Die Befehlsliste des Dashboards löst i18n_desc über den Website-Sprachumschalter (DE/EN, oben rechts) auf. Das schlichte description= bleibt Englisch — das ist der Text, den Discord selbst anzeigt — und auch das Parameter-@app_commands.describe(...) bleibt Englisch. extras={"i18n_desc": …} funktioniert bei @commands.hybrid_command, @commands.command, @commands.group und @app_commands.command.

Hybrid-Befehle (eine Slash-Form)

Um einem Befehl eine Slash-Form zu geben, nutze @commands.hybrid_command / @commands.hybrid_group statt der schlichten Varianten.

Keyword-only-Vorbehalt. Ein Hybrid-Callback darf höchstens einen keyword-only-Parameter haben (das abschließende *, value als Consume-Rest). Zwei oder mehr keyword-only-Parameter lassen die Slash-Hälfte still wegfallen — der Befehl bleibt ohne Fehler nur als Prefix-Befehl. Lösung: Verschiebe die zusätzlichen optionalen Parameter zu positional-mit-Default vor das *:

async def ban(self, ctx, member: discord.Member,
              delete_message_days: app_commands.Range[int, 0, 7] = 0,
              *, reason: str | None = None):
    ...

Slash-Befehle in Red aktivieren

Red lässt neu hinzugefügte App-/Hybrid-Slash-Befehle standardmäßig deaktiviert. Aktiviere sie mit [p]slash enablecog <CogClassName> (oder pro Befehl [p]slash enable <name>), danach [p]slash sync. Sie lassen sich auch im Web-Dashboard unter Cogs → Slash umschalten.

Häufige Stolperfallen

  • Importiere app_commands aus discord, nicht aus redbot.core. Nutze from discord import app_commands. from redbot.core import app_commands wirft beim Laden einen ImportError/AttributeError und der gesamte Cog lädt nicht — seine Prefix- und Slash-Befehle verschwinden.
  • Zwei keyword-only-Parameter an einem Hybrid lassen die Slash-Hälfte still wegfallen (siehe oben).
  • [p]slash enablecog + [p]slash sync nach dem Hinzufügen von Slash-Befehlen vergessen — sie existieren, bleiben aber verborgen, bis sie aktiviert sind.

Das Context-Objekt

Jede Beitragsmethode erhält einen DashboardContext (ctx) mit bot, user, guild, member und locale. Der Aufruf ist bereits serverseitig autorisiert — gemäß der deklarierten permission, bevor deine Methode läuft. Du prüfst die Berechtigung also nicht erneut, sondern liest/schreibst nur Daten. In einem globalen Panel (scope="global") ist ctx.guild gleich None; entsprechend absichern.


Berechtigungsstufen

Setze über permission= an jedem Beitrag die Mindeststufe, die ein Betrachter haben muss. Die Stufen werden serverseitig aus Reds eigenem Rechtesystem abgeleitet:

Stufe Wer
authenticated Jeder eingeloggte Discord-Nutzer.
guild_member Mitglied des Servers.
guild_mod Server-Moderator (Reds Mod-Rolle).
guild_admin Server-Admin (Reds Admin-Rolle / manage_guild).
guild_owner Server-Eigentümer.
bot_owner Bot-Owner.

Wähle die niedrigste sinnvolle Stufe: Widgets oft guild_member, Einstellungs-Panels guild_admin, globale/API-Panels bot_owner.


Parallelbetrieb mit AAA3A

Du kannst beide Dashboards aus demselben Cog bedienen. Die PDC-Marker (__dashboard_widget__, __dashboard_panel__, __dashboard_page__, __dashboard_list__) unterscheiden sich von denen von AAA3A, sodass sich die beiden Scanner nie stören und keines der Systeme das andere abschaltet. PDC integriert über register_dashboard(self) / den Auto-Scan und ist unabhängig von AAA3As Mixin-Vererbung — du kannst AAA3As Mixin also normal weiter erben. Importierst du beide dashboard_page-Symbole, aliasiere eines, um einen Namenskonflikt zu vermeiden.


Checkliste

  1. Kopiere pdc_dashboard.py in deinen Cog-Ordner und importiere die Decorators von dort.
  2. Dekoriere deine Methoden (@dashboard_widget / @dashboard_panel / @dashboard_list / @dashboard_page) und liefere Schemas zurück, niemals HTML.
  3. Ergänze @<panel>.on_submit (und Listen-Edit-/Delete-Handler), um Änderungen über dein Config zu speichern.
  4. Rufe register_dashboard(self) in cog_load und unregister_dashboard(self) in cog_unload auf.
  5. Lade den Cog — er funktioniert eigenständig und leuchtet automatisch auf, sobald der WebDashboard-Cog vorhanden ist.

Siehe auch

Clone this wiki locally