All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m51s
CI/CD / test-sqlite (push) Successful in 2m51s
CI/CD / build-and-deploy (push) Successful in 39s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
148 lines
4.8 KiB
Go
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, " ")
|
|
}
|