// Package help 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 pages/. //go:embed packt sie // beim Kompilieren ins Binary, genau wie templates/ und static/. package help import ( "embed" "fmt" "munin/internal/markdown" ) //go:embed pages/*.md var pagesFS embed.FS // Page beschreibt eine Hilfeseite. type Page struct { Slug string // Teil der Adresse: /hilfe/ Title string // Überschrift und Name in der Navigation Summary string // ein Satz für die Übersicht // For 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. For Audience } // Audience einer Hilfeseite. type Audience int const ( // ForEveryone: jeder, der angemeldet ist. ForEveryone Audience = iota // ForAdmins: Workspace-Admins und der Plattform-Admin. ForAdmins // ForPlatformAdmin: nur der Plattform-Admin. ForPlatformAdmin ) // Reader beschreibt, wer die Hilfe gerade liest — so viel, wie für die // Auswahl der Seiten nötig ist. type Reader struct { PlatformAdmin bool WorkspaceAdmin bool } // CanRead sagt, ob l die Seite sehen darf. func (s Page) CanRead(l Reader) bool { switch s.For { case ForPlatformAdmin: return l.PlatformAdmin case ForAdmins: return l.PlatformAdmin || l.WorkspaceAdmin default: return true } } // pages legt Reihenfolge und Inhalt der Navigation fest. Die Datei zu einer // Page heißt immer pages/.md — so gibt es nur eine Stelle, an der ein // Name steht, und der Test in help_test.go prüft, dass beides zusammenpasst. var pages = []Page{ {Slug: "start", Title: "Willkommen", Summary: "Was Munin ist und wo man anfängt."}, {Slug: "begriffe", Title: "Begriffe", Summary: "Workspace, Projekt, Eintrag, Bezeichner, Rollen — was sie bedeuten."}, {Slug: "mcp-einrichten", Title: "Agent anbinden", Summary: "Claude Code, Cline oder OpenCode mit Munin verbinden."}, {Slug: "mit-agenten-arbeiten", Title: "Mit Agenten arbeiten", Summary: "Wie man Agenten anspricht, damit sie das Gedächtnis wirklich nutzen."}, {Slug: "weboberflaeche", Title: "Weboberfläche", Summary: "Einträge im Browser suchen, schreiben, vergleichen und verknüpfen."}, {Slug: "zugaenge", Title: "Zugänge und Sicherheit", Summary: "Keys, Rollen, Protokoll und was man beim Teilen von Wissen bedenken sollte."}, {Slug: "workspace-admin", Title: "Für Workspace-Admins", Summary: "Projekte und Zugänge einer Abteilung verwalten, Protokoll, Suche über alle Projekte.", For: ForAdmins}, {Slug: "plattform-admin", Title: "Für Plattform-Admins", Summary: "Workspaces, Workspace-Admins, Schreibbudget, System, Sicherung.", For: ForPlatformAdmin}, } // All 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 All() []Page { clone := make([]Page, len(pages)) copy(clone, pages) return clone } // Visible gibt die Seiten zurück, die l sehen darf, in der Reihenfolge der // Navigation. func Visible(l Reader) []Page { var result []Page for _, s := range pages { if s.CanRead(l) { result = append(result, s) } } return result } // Find sucht eine Seite über ihren Slug. func Find(slug string) (Page, bool) { for _, s := range pages { if s.Slug == slug { return s, true } } return Page{}, false } // Markdown liefert den Rohtext einer Seite. func (s Page) Markdown() (string, error) { content, err := pagesFS.ReadFile("pages/" + s.Slug + ".md") if err != nil { return "", fmt.Errorf("Hilfeseite %q fehlt: %w", s.Slug, err) } return string(content), 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_help.go) setzen Werte aus der Anfrage ein — und dann soll // daraus auf keinen Fall HTML werden können. func Render(source string) (string, error) { return markdown.RenderWithAnchors(source) }