package app import ( "bytes" "encoding/json" "io/fs" "net/http" "path" "strings" "time" "git.warky.dev/wdevs/amcs/internal/auth" "git.warky.dev/wdevs/amcs/internal/buildinfo" ) const connectedWindow = 10 * time.Minute type statusAPIResponse struct { Title string `json:"title"` Description string `json:"description"` Version string `json:"version"` BuildDate string `json:"build_date"` Commit string `json:"commit"` ConnectedCount int `json:"connected_count"` TotalKnown int `json:"total_known"` ConnectedWindow string `json:"connected_window"` Entries []auth.AccessSnapshot `json:"entries"` OAuthEnabled bool `json:"oauth_enabled"` } func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool, now time.Time) statusAPIResponse { entries := tracker.Snapshot() return statusAPIResponse{ Title: "Avelon Memory Crystal Server (AMCS)", Description: "AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools.", Version: fallback(info.Version, "dev"), BuildDate: fallback(info.BuildDate, "unknown"), Commit: fallback(info.Commit, "unknown"), ConnectedCount: tracker.ConnectedCount(now, connectedWindow), TotalKnown: len(entries), ConnectedWindow: "last 10 minutes", Entries: entries, OAuthEnabled: oauthEnabled, } } func fallback(value, defaultValue string) string { if strings.TrimSpace(value) == "" { return defaultValue } return value } func statusAPIHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/status" { http.NotFound(w, r) return } if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) if r.Method == http.MethodHead { return } _ = json.NewEncoder(w).Encode(statusSnapshot(info, tracker, oauthEnabled, time.Now())) } } func homeHandler(_ buildinfo.Info, _ *auth.AccessTracker, _ bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } requestPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if requestPath == "." { requestPath = "" } if requestPath != "" { if serveUIAsset(w, r, requestPath) { return } http.NotFound(w, r) return } serveUIIndex(w, r) } } func serveUIAsset(w http.ResponseWriter, r *http.Request, name string) bool { if uiDistFS == nil { return false } if strings.Contains(name, "..") { return false } file, err := uiDistFS.Open(name) if err != nil { return false } defer file.Close() info, err := file.Stat() if err != nil || info.IsDir() { return false } data, err := fs.ReadFile(uiDistFS, name) if err != nil { return false } http.ServeContent(w, r, info.Name(), info.ModTime(), bytes.NewReader(data)) return true } func serveUIIndex(w http.ResponseWriter, r *http.Request) { if indexHTML == nil { http.Error(w, "ui assets not built", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) if r.Method == http.MethodHead { return } _, _ = w.Write(indexHTML) }