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>
This commit is contained in:
2026-04-10 13:40:42 +02:00
parent 87821e8988
commit d93f3bb888
12 changed files with 799 additions and 146 deletions

View File

@@ -22,6 +22,9 @@ type Mutation {
# First-run setup.
setup(input: SetupInput!): Boolean!
# Test an LDAP configuration before saving.
testLdapConnection(input: LDAPInput!): LDAPTestResult!
# Authenticate and receive a bearer token.
login(username: String!, password: String!): String!
@@ -54,6 +57,11 @@ type DocumentMeta {
updatedAt: String!
}
type LDAPTestResult {
success: Boolean!
message: String!
}
type CommitEntry {
hash: String!
author: String!

View File

@@ -1,15 +1,14 @@
package graph
import (
"io"
"net/http"
"os"
"strings"
"github.com/brasse-b/archivum/internal/config"
)
// NewServer wires up the HTTP handler for the GraphQL endpoint.
// cfg may be nil when the system has not been configured yet; in that case
// every GraphQL request returns {"data":{"systemStatus":"REQUIRE_SETUP"}} so
// the frontend can redirect to the Setup Wizard.
func NewServer(cfg *config.Config) http.Handler {
mux := http.NewServeMux()
@@ -21,24 +20,67 @@ func NewServer(cfg *config.Config) http.Handler {
w.WriteHeader(http.StatusNoContent)
return
}
if cfg == nil {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"data":{"systemStatus":"REQUIRE_SETUP"}}`))
return
}
// TODO: replace with gqlgen handler once generated.
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"data":{"systemStatus":"OK"}}`))
// 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)
})
// Serve the embedded Vue SPA for every other path.
mux.Handle("/", http.FileServer(http.Dir("/srv/archivum/ui")))
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"
}