munin/internal/help/help.go
2026-10-19 10:13:56 +02:00

141 lines
5 KiB
Go

// 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/<Slug>
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/<Slug>.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)
}