0.2.45 - Server side git commits

This commit is contained in:
ponzischeme89
2026-08-10 20:39:26 +12:00
parent 56c1167382
commit 4c47a5f8a0
12 changed files with 321 additions and 132 deletions
+42 -3
View File
@@ -26,8 +26,8 @@ const adminCookieName = "memby_admin"
// a valid session, so the only thing it can extend is its own sign-in.
const adminActivityHeader = "X-Memby-Admin-Active"
// adminRoutes is the operator interface: library imports, the maintenance switch, and
// row engagement. Disabled entirely when MEMBY_ADMIN_TOKEN is unset, so it cannot be
// adminRoutes is the operator interface: library imports, maintenance and analytics.
// Disabled entirely when MEMBY_ADMIN_TOKEN is unset, so it cannot be
// left exposed by accident.
func (s *Server) adminRoutes() http.Handler {
mux := http.NewServeMux()
@@ -57,6 +57,7 @@ func (s *Server) adminRoutes() http.Handler {
mux.Handle("PUT /admin/api/accounts/{userID}/themes", s.adminAuth(s.handleAdminUserThemes))
mux.Handle("GET /admin/api/recommendations", s.adminAuth(s.handleAdminRecommendations))
mux.Handle("GET /admin/api/analytics", s.adminAuth(s.handleAdminAnalytics))
mux.Handle("GET /admin/api/journeys", s.adminAuth(s.handleAdminJourneys))
mux.Handle("GET /admin/api/searches", s.adminAuth(s.handleAdminSearches))
mux.Handle("GET /admin/api/events", s.adminAuth(s.handleAdminEvents))
mux.Handle("GET /admin/api/runtime", s.adminAuth(s.handleAdminRuntime))
@@ -720,6 +721,8 @@ func (s *Server) handleAdminAnalytics(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, "could not read analytics")
return
}
// Keep the journey fields on this older endpoint for scripts written before journeys
// gained their own page and endpoint. The console itself only reads rows from here.
users, err := s.store.AnalyticsUsers(r.Context(), since)
if err != nil {
s.loggerFor(r.Context()).Error("user analytics failed", "error", err)
@@ -733,7 +736,7 @@ func (s *Server) handleAdminAnalytics(w http.ResponseWriter, r *http.Request) {
paths, pathErr := s.store.UserPaths(r.Context(), userID, since)
events, eventErr := s.store.UserJourneyEvents(r.Context(), userID, since, 1000)
if featureErr != nil || pathErr != nil || eventErr != nil {
s.loggerFor(r.Context()).Error("user journey read failed", "user_id", userID)
s.loggerFor(r.Context()).Error("legacy user journey read failed", "user_id", userID)
writeError(w, http.StatusInternalServerError, "could not read user journey")
return
}
@@ -745,6 +748,42 @@ func (s *Server) handleAdminAnalytics(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, payload)
}
func (s *Server) handleAdminJourneys(w http.ResponseWriter, r *http.Request) {
days := queryInt(r, "days", 30, 90)
since := time.Now().UTC().AddDate(0, 0, -days)
userID := strings.TrimSpace(r.URL.Query().Get("userId"))
users, err := s.store.AnalyticsUsers(r.Context(), since)
if err != nil {
s.loggerFor(r.Context()).Error("journey users failed", "error", err)
writeError(w, http.StatusInternalServerError, "could not read journeys")
return
}
stats, statsErr := s.store.JourneyStats(r.Context(), userID, since)
features, featureErr := s.store.UserFeatureStats(r.Context(), userID, since)
paths, pathErr := s.store.UserPaths(r.Context(), userID, since)
actions, actionErr := s.store.JourneyActionStats(r.Context(), userID, since)
if statsErr != nil || featureErr != nil || pathErr != nil || actionErr != nil {
s.loggerFor(r.Context()).Error("journey analytics read failed", "user_id", userID)
writeError(w, http.StatusInternalServerError, "could not read journeys")
return
}
payload := map[string]any{
"days": days, "retentionDays": int(s.cfg.AnalyticsRetention / (24 * time.Hour)),
"users": users, "stats": stats, "features": features, "paths": paths, "actions": actions,
}
if userID != "" {
events, eventErr := s.store.UserJourneyEvents(r.Context(), userID, since, 1000)
if eventErr != nil {
s.loggerFor(r.Context()).Error("user journey read failed", "user_id", userID)
writeError(w, http.StatusInternalServerError, "could not read user journey")
return
}
payload["userId"] = userID
payload["events"] = events
}
writeJSON(w, http.StatusOK, payload)
}
// syncerHandle is the slice of the syncer the API needs, so api does not depend on the
// concrete type for testing.
type syncerHandle interface {