// Package hilfe enthält die eingebaute Onboarding-Dokumentation der // Weboberfläche: wie man Munin einrichtet, benutzt und was die Begriffe // bedeuten. // // # Warum eingebettet und nicht als Einträge in einem Projekt? // // Die Hilfe beschreibt das Programm selbst. Sie muss also zu genau der Fassung // passen, die gerade läuft — ändert sich eine Funktion, ändert sich im selben // Commit auch ihre Beschreibung. Als Einträge in der Datenbank würde sie // unbemerkt veralten, und in jedem neuen Workspace müsste sie erst jemand // anlegen. // // Die Seiten sind Markdown-Dateien im Ordner seiten/. //go:embed packt sie // beim Kompilieren ins Binary, genau wie templates/ und static/. package hilfe import ( "embed" "fmt" "munin/internal/markdown" ) //go:embed seiten/*.md var seitenFS embed.FS // Seite beschreibt eine Hilfeseite. type Seite struct { Slug string // Teil der Adresse: /hilfe/ Titel string // Überschrift und Name in der Navigation Kurz string // ein Satz für die Übersicht // Fuer legt fest, wer die Seite sieht. Wer nicht dazugehört, findet sie // weder in der Navigation, noch lässt sie sich direkt aufrufen (404). // Geheim ist darin nichts — aber sie beschreibt Knöpfe, die andere gar // nicht sehen. Fuer Zielgruppe } // Zielgruppe einer Hilfeseite. type Zielgruppe int const ( // FuerAlle: jeder, der angemeldet ist. FuerAlle Zielgruppe = iota // FuerAdmins: Workspace-Admins und der Plattform-Admin. FuerAdmins // FuerPlattformAdmin: nur der Plattform-Admin. FuerPlattformAdmin ) // Leser beschreibt, wer die Hilfe gerade liest — so viel, wie für die // Auswahl der Seiten nötig ist. type Leser struct { PlattformAdmin bool WorkspaceAdmin bool } // DarfLesen sagt, ob l die Seite sehen darf. func (s Seite) DarfLesen(l Leser) bool { switch s.Fuer { case FuerPlattformAdmin: return l.PlattformAdmin case FuerAdmins: return l.PlattformAdmin || l.WorkspaceAdmin default: return true } } // seiten legt Reihenfolge und Inhalt der Navigation fest. Die Datei zu einer // Seite heißt immer seiten/.md — so gibt es nur eine Stelle, an der ein // Name steht, und der Test in hilfe_test.go prüft, dass beides zusammenpasst. var seiten = []Seite{ {Slug: "start", Titel: "Willkommen", Kurz: "Was Munin ist und wo man anfängt."}, {Slug: "begriffe", Titel: "Begriffe", Kurz: "Workspace, Projekt, Eintrag, Bezeichner, Rollen — was sie bedeuten."}, {Slug: "mcp-einrichten", Titel: "Agent anbinden", Kurz: "Claude Code, Cline oder OpenCode mit Munin verbinden."}, {Slug: "mit-agenten-arbeiten", Titel: "Mit Agenten arbeiten", Kurz: "Wie man Agenten anspricht, damit sie das Gedächtnis wirklich nutzen."}, {Slug: "weboberflaeche", Titel: "Weboberfläche", Kurz: "Einträge im Browser suchen, schreiben, vergleichen und verknüpfen."}, {Slug: "zugaenge", Titel: "Zugänge und Sicherheit", Kurz: "Keys, Rollen, Protokoll und was man beim Teilen von Wissen bedenken sollte."}, {Slug: "workspace-admin", Titel: "Für Workspace-Admins", Kurz: "Projekte und Zugänge einer Abteilung verwalten, Protokoll, Suche über alle Projekte.", Fuer: FuerAdmins}, {Slug: "plattform-admin", Titel: "Für Plattform-Admins", Kurz: "Workspaces, Workspace-Admins, Schreibbudget, System, Sicherung.", Fuer: FuerPlattformAdmin}, } // Alle gibt alle Seiten in der Reihenfolge der Navigation zurück, auch die // nur für Admins. Für die Anzeige ist Sichtbar die richtige Funktion. // // Zurückgegeben wird eine Kopie: Wer die Liste verändert, soll damit nicht aus // Versehen die Navigation aller anderen Anfragen verändern. func Alle() []Seite { kopie := make([]Seite, len(seiten)) copy(kopie, seiten) return kopie } // Sichtbar gibt die Seiten zurück, die l sehen darf, in der Reihenfolge der // Navigation. func Sichtbar(l Leser) []Seite { var ergebnis []Seite for _, s := range seiten { if s.DarfLesen(l) { ergebnis = append(ergebnis, s) } } return ergebnis } // Finde sucht eine Seite über ihren Slug. func Finde(slug string) (Seite, bool) { for _, s := range seiten { if s.Slug == slug { return s, true } } return Seite{}, false } // Markdown liefert den Rohtext einer Seite. func (s Seite) Markdown() (string, error) { inhalt, err := seitenFS.ReadFile("seiten/" + s.Slug + ".md") if err != nil { return "", fmt.Errorf("Hilfeseite %q fehlt: %w", s.Slug, err) } return string(inhalt), nil } // Render wandelt eine Hilfeseite in HTML. // // Es ist derselbe Renderer wie für Einträge (internal/markdown), damit die // Hilfe nichts zeigt, was im eigenen Eintrag dann anders aussieht. Einziger // Unterschied: Überschriften bekommen hier eine id, damit die Oberfläche mit // "?"-Links direkt auf einen Abschnitt zeigen kann, etwa // /hilfe/weboberflaeche#markdown-im-inhalt. // // Roh-HTML bleibt auch hier gesperrt. Die Seiten stammen zwar aus dem // Repository und nicht von Nutzern, aber die Platzhalter (siehe // web/handlers_hilfe.go) setzen Werte aus der Anfrage ein — und dann soll // daraus auf keinen Fall HTML werden können. func Render(quelle string) (string, error) { return markdown.RenderMitAnkern(quelle) }