diff --git a/backend/internal/graph/schema.graphql b/backend/internal/graph/schema.graphql index d2a77cd..7f61acb 100644 --- a/backend/internal/graph/schema.graphql +++ b/backend/internal/graph/schema.graphql @@ -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! diff --git a/backend/internal/graph/server.go b/backend/internal/graph/server.go index 39e7a80..754a44d 100644 --- a/backend/internal/graph/server.go +++ b/backend/internal/graph/server.go @@ -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" +} diff --git a/frontend/index.html b/frontend/index.html index 824ebe2..bbb26ec 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,14 +1,18 @@ - + - - - Archivum + + - +
diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 88cd0e5..ae1fcac 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,23 +1,42 @@ diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 8c0ee8a..a71a579 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -2,22 +2,113 @@ @tailwind components; @tailwind utilities; +/* ── Accent color variables (updated at runtime by ThemeStore) ─────────────── */ +:root { + --accent-400: 96 165 250; /* blue-400 */ + --accent-500: 59 130 246; /* blue-500 */ + --accent-600: 37 99 235; /* blue-600 */ + --accent-700: 29 78 216; /* blue-700 */ +} + +/* ── Base element styles ────────────────────────────────────────────────────── */ @layer base { html { font-family: 'Inter', ui-sans-serif, system-ui; + -webkit-font-smoothing: antialiased; } - /* AsciiDoc rendered output */ - .adoc-content h1 { @apply text-2xl font-bold mb-4 mt-6; } - .adoc-content h2 { @apply text-xl font-semibold mb-3 mt-5; } + body { + @apply bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100; + transition: background-color 0.2s, color 0.2s; + } + + * { + scrollbar-width: thin; + scrollbar-color: theme('colors.slate.600') transparent; + } + *::-webkit-scrollbar { width: 6px; height: 6px; } + *::-webkit-scrollbar-track { background: transparent; } + *::-webkit-scrollbar-thumb { + @apply bg-slate-300 dark:bg-slate-600 rounded-full; + } +} + +/* ── Reusable component classes ────────────────────────────────────────────── */ +@layer components { + .card { + @apply bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl; + } + + .input { + @apply w-full bg-slate-50 dark:bg-slate-900 + border border-slate-300 dark:border-slate-600 + rounded-lg px-3 py-2 text-sm + text-slate-900 dark:text-slate-100 + placeholder-slate-400 dark:placeholder-slate-500 + focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent + transition; + } + + .btn-primary { + @apply inline-flex items-center justify-center gap-2 + px-4 py-2 rounded-lg text-sm font-medium text-white + bg-accent-600 hover:bg-accent-500 active:bg-accent-700 + disabled:opacity-50 disabled:cursor-not-allowed + transition; + } + + .btn-secondary { + @apply inline-flex items-center justify-center gap-2 + px-4 py-2 rounded-lg text-sm font-medium + bg-slate-100 dark:bg-slate-800 + text-slate-700 dark:text-slate-300 + hover:bg-slate-200 dark:hover:bg-slate-700 + border border-slate-200 dark:border-slate-700 + disabled:opacity-50 transition; + } + + .btn-ghost { + @apply inline-flex items-center justify-center gap-2 + px-3 py-1.5 rounded-lg text-sm + text-slate-600 dark:text-slate-400 + hover:bg-slate-100 dark:hover:bg-slate-800 + transition; + } + + .label { + @apply block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5; + } +} + +/* ── AsciiDoc rendered output ──────────────────────────────────────────────── */ +@layer utilities { + .adoc-content { @apply text-slate-800 dark:text-slate-200 leading-relaxed; } + .adoc-content h1 { @apply text-2xl font-bold mb-4 mt-6 text-slate-900 dark:text-white; } + .adoc-content h2 { @apply text-xl font-semibold mb-3 mt-5 text-slate-900 dark:text-white; } .adoc-content h3 { @apply text-lg font-medium mb-2 mt-4; } - .adoc-content p { @apply mb-3 leading-relaxed; } - .adoc-content pre { @apply bg-surface-800 rounded-lg p-4 overflow-x-auto font-mono text-sm mb-4; } - .adoc-content code { @apply font-mono text-sm bg-surface-800 px-1 rounded; } + .adoc-content p { @apply mb-3; } + .adoc-content pre { + @apply bg-slate-100 dark:bg-slate-800/80 + border border-slate-200 dark:border-slate-700 + rounded-lg p-4 overflow-x-auto font-mono text-sm mb-4; + } + .adoc-content code { + @apply font-mono text-sm + bg-slate-100 dark:bg-slate-800 + text-accent-600 dark:text-accent-400 + px-1.5 py-0.5 rounded; + } .adoc-content ul { @apply list-disc list-inside mb-3 space-y-1; } .adoc-content ol { @apply list-decimal list-inside mb-3 space-y-1; } - .adoc-content blockquote { @apply border-l-4 border-slate-500 pl-4 italic text-slate-400 mb-3; } + .adoc-content blockquote { + @apply border-l-4 border-accent-500 pl-4 italic + text-slate-600 dark:text-slate-400 mb-3; + } .adoc-content table { @apply w-full text-sm border-collapse mb-4; } - .adoc-content th { @apply bg-surface-800 px-3 py-2 text-left border border-slate-700; } - .adoc-content td { @apply px-3 py-2 border border-slate-700; } + .adoc-content th { + @apply bg-slate-100 dark:bg-slate-800 + px-3 py-2 text-left + border border-slate-200 dark:border-slate-700 font-medium; + } + .adoc-content td { @apply px-3 py-2 border border-slate-200 dark:border-slate-700; } } diff --git a/frontend/src/components/layout/AppLayout.vue b/frontend/src/components/layout/AppLayout.vue index b5b18a5..6251689 100644 --- a/frontend/src/components/layout/AppLayout.vue +++ b/frontend/src/components/layout/AppLayout.vue @@ -1,25 +1,33 @@