Files
Archivum/backend/internal/graph/server.go
brasse b d93f3bb888 fix+feat: SPA routing, theme system, modern UI, setup wizard improvements
Fixes:
- Go server now serves index.html for all non-asset paths (SPA fallback)
- Null guard on data.documents prevents undefined.length TypeError
- App.vue waits for checkStatus() before rendering AppLayout (race condition)
- PWA manifest no longer references missing icon files
- index.html applies theme class before paint to prevent flash

Features:
- Theme store: dark/light toggle + 6 accent color presets + custom hex picker
- ThemeToggle component in header dropdown
- CSS custom properties for accent color (--accent-400/500/600/700)
- Tailwind accent-* color utilities driven by CSS vars
- SetupWizard: 3-step flow, password confirmation, LDAP test button,
  animated toggle, review step, proper error/success states
- Sidebar: loading skeleton, active page highlight, null-safe document list
- Global .input, .btn-primary, .btn-secondary, .card component classes
- Stub GraphQL responses for setup/login/testLdapConnection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:40:42 +02:00

87 lines
2.4 KiB
Go

package graph
import (
"io"
"net/http"
"os"
"strings"
"github.com/brasse-b/archivum/internal/config"
)
func NewServer(cfg *config.Config) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
if cfg == nil {
_, _ = w.Write([]byte(`{"data":{"systemStatus":"REQUIRE_SETUP"}}`))
return
}
// Stub: inspect operation name to return sensible responses.
body, _ := io.ReadAll(r.Body)
bs := string(body)
switch {
case strings.Contains(bs, "testLdapConnection"):
_, _ = w.Write([]byte(`{"data":{"testLdapConnection":{"success":false,"message":"LDAP resolver not yet implemented"}}}`))
case strings.Contains(bs, "setup"):
_, _ = w.Write([]byte(`{"data":{"setup":true}}`))
case strings.Contains(bs, "login"):
_, _ = w.Write([]byte(`{"data":{"login":"stub-token"}}`))
default:
_, _ = w.Write([]byte(`{"data":{"systemStatus":"OK","documents":[]}}`))
}
})
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
uiDir := os.Getenv("UI_DIR")
if uiDir == "" {
uiDir = "/srv/archivum/ui"
}
mux.Handle("/", spaHandler(uiDir))
return mux
}
// spaHandler serves static files and falls back to index.html for all paths
// that don't resolve to a real file — required for client-side routing.
func spaHandler(dir string) http.Handler {
fs := http.Dir(dir)
fileServer := http.FileServer(fs)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Assets (JS, CSS, images) must be served as-is.
if isAssetPath(r.URL.Path) {
fileServer.ServeHTTP(w, r)
return
}
// Try to open the path; if it exists serve it, otherwise serve SPA root.
f, err := fs.Open(r.URL.Path)
if err == nil {
f.Close()
fileServer.ServeHTTP(w, r)
return
}
http.ServeFile(w, r, dir+"/index.html")
})
}
func isAssetPath(path string) bool {
return strings.HasPrefix(path, "/assets/") ||
strings.HasPrefix(path, "/icons/") ||
path == "/favicon.ico" ||
path == "/favicon.svg" ||
path == "/manifest.webmanifest" ||
path == "/registerSW.js" ||
path == "/sw.js"
}