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:
@@ -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!
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user