This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/docs.go
Jeffrey Smith ba4c9ca65c Feat v0.7.4 documentation + deferred surface work
Docs restructuring: category grouping (Getting Started, Platform,
Extension Development, Operations) with 14 ordered docs. Four new
guides: Permissions & Groups, Workflows, Starlark Reference, Frontend
JS Guide. Extension Guide updated with config_section docs.

Content refresh across 10 existing docs: rebrand volume names,
stale CSS vars, TLS_MODE env, self-hosted font notes, Architecture
frontend section.

Team Admin workflows.js (722 lines) split into 3 modules:
workflows.js (router+CRUD), workflow-editor.js (editor+stages),
workflow-monitor.js (assignments+monitor+signoff).

Fix: docs outline scroll no longer pushes topbar off-screen.
Fix: --bg-2 (undefined CSS var) replaced with --bg-secondary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:49:00 +00:00

148 lines
4.8 KiB
Go

package handlers
import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
// DocsHandler serves documentation files.
type DocsHandler struct {
docsDir string
}
// NewDocsHandler creates a new DocsHandler.
func NewDocsHandler(docsDir string) *DocsHandler {
return &DocsHandler{docsDir: docsDir}
}
// docEntry describes a documentation file.
type docEntry struct {
Slug string `json:"slug"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
}
// docsOrder defines the display order and metadata for documentation files.
var docsOrder = []docEntry{
// Getting Started
{Slug: "GETTING-STARTED", Title: "Getting Started", Description: "Quick start guide", Category: "Getting Started"},
{Slug: "TUTORIAL-FIRST-EXTENSION", Title: "Tutorial: First Extension", Description: "Step-by-step extension walkthrough", Category: "Getting Started"},
// Platform
{Slug: "ARCHITECTURE", Title: "Architecture", Description: "Kernel design and components", Category: "Platform"},
{Slug: "PERMISSIONS-AND-GROUPS", Title: "Permissions & Groups", Description: "RBAC model and settings cascade", Category: "Platform"},
{Slug: "WORKFLOWS", Title: "Workflows", Description: "Multi-stage processes with team validation", Category: "Platform"},
{Slug: "DEPLOYMENT", Title: "Deployment", Description: "Production deployment guide", Category: "Platform"},
// Extension Development
{Slug: "EXTENSION-GUIDE", Title: "Extension Guide", Description: "How to author extensions", Category: "Extension Development"},
{Slug: "STARLARK-REFERENCE", Title: "Starlark Reference", Description: "Sandbox scripting API", Category: "Extension Development"},
{Slug: "FRONTEND-JS-GUIDE", Title: "Frontend JS Guide", Description: "Preact SDK and shell contract", Category: "Extension Development"},
{Slug: "EXTENSION-CSS", Title: "Extension CSS", Description: "CSS isolation and primitives", Category: "Extension Development"},
{Slug: "PACKAGE-FORMAT", Title: "Package Format", Description: "The .pkg archive format", Category: "Extension Development"},
// Operations
{Slug: "API-REFERENCE", Title: "API Reference", Description: "REST API overview", Category: "Operations"},
{Slug: "DISTRIBUTION", Title: "Distribution", Description: "Docker image and bundled packages", Category: "Operations"},
{Slug: "PACKAGE-REGISTRY", Title: "Package Registry", Description: "Registry API and operations", Category: "Operations"},
}
// ListDocs returns the list of available documentation files.
// GET /api/v1/docs
func (h *DocsHandler) ListDocs(c *gin.Context) {
if h.docsDir == "" {
c.JSON(http.StatusOK, gin.H{"data": []docEntry{}})
return
}
var available []docEntry
for _, d := range docsOrder {
path := filepath.Join(h.docsDir, d.Slug+".md")
if _, err := os.Stat(path); err == nil {
available = append(available, d)
}
}
// Also add any .md files not in the ordered list
entries, err := os.ReadDir(h.docsDir)
if err == nil {
known := make(map[string]bool)
for _, d := range docsOrder {
known[d.Slug] = true
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
slug := strings.TrimSuffix(e.Name(), ".md")
if known[slug] {
continue
}
// Skip design docs and other non-user-facing files
if strings.HasPrefix(slug, "DESIGN-") || strings.HasPrefix(slug, "DEMO-") ||
strings.HasPrefix(slug, "AUDIT-") || strings.HasPrefix(slug, "USABILITY-") {
continue
}
available = append(available, docEntry{
Slug: slug,
Title: slugToTitle(slug),
})
}
}
if available == nil {
available = []docEntry{}
}
c.JSON(http.StatusOK, gin.H{"data": available})
}
// GetDoc returns the raw markdown content of a documentation file.
// GET /api/v1/docs/:name
func (h *DocsHandler) GetDoc(c *gin.Context) {
name := c.Param("name")
if !isValidDocName(name) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document name"})
return
}
if h.docsDir == "" {
c.JSON(http.StatusNotFound, gin.H{"error": "docs not configured"})
return
}
path := filepath.Join(h.docsDir, name+".md")
data, err := os.ReadFile(path)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "document not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"slug": name,
"title": slugToTitle(name),
"content": string(data),
},
})
}
var validDocNameRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
func isValidDocName(name string) bool {
return validDocNameRe.MatchString(name) && !strings.Contains(name, "..")
}
func slugToTitle(slug string) string {
// Convert "GETTING-STARTED" → "Getting Started"
parts := strings.Split(slug, "-")
for i, p := range parts {
if len(p) > 0 {
parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:])
}
}
return strings.Join(parts, " ")
}