diff --git a/pkg/hold/admin/admin.go b/pkg/hold/admin/admin.go index 46e30ec..4d11295 100644 --- a/pkg/hold/admin/admin.go +++ b/pkg/hold/admin/admin.go @@ -423,6 +423,7 @@ func (ui *AdminUI) RegisterRoutes(r chi.Router) { r.Get("/admin/api/stats", ui.handleStatsAPI) r.Get("/admin/api/top-users", ui.handleTopUsersAPI) r.Get("/admin/api/relay/status", ui.handleRelayStatus) + r.Get("/admin/api/crew/member", ui.handleCrewMemberInfo) // Logout r.Post("/admin/auth/logout", ui.handleLogout) diff --git a/pkg/hold/admin/handlers_crew.go b/pkg/hold/admin/handlers_crew.go index 9a73eef..9598637 100644 --- a/pkg/hold/admin/handlers_crew.go +++ b/pkg/hold/admin/handlers_crew.go @@ -9,11 +9,10 @@ import ( "time" "atcr.io/pkg/atproto" - "atcr.io/pkg/hold/pds" "github.com/go-chi/chi/v5" ) -// CrewMemberView represents a crew member for display +// CrewMemberView represents a crew member for display (populated row) type CrewMemberView struct { RKey string DID string @@ -28,6 +27,17 @@ type CrewMemberView struct { AddedAt time.Time } +// CrewSkeletonView is the minimal crew member data for skeleton rendering. +// Contains only data available from the MST walk (no network calls). +type CrewSkeletonView struct { + RKey string + DID string + Role string + Permissions []string + Tier string + AddedAt time.Time +} + // resolveHandle attempts to resolve a DID to a handle // Returns empty string if resolution fails func resolveHandle(ctx context.Context, did string) string { @@ -50,18 +60,14 @@ type TierOption struct { Limit string } -// getCrewViews builds the crew member view list -func (ui *AdminUI) getCrewViews(ctx context.Context) ([]CrewMemberView, error) { - crew, err := ui.pds.ListCrewMembers(ctx) +// handleCrewTab returns the crew tab content (HTMX partial). +// Only does the MST walk — no handle resolution or usage queries. +// Each row lazy-loads its details via handleCrewMemberInfo. +func (ui *AdminUI) handleCrewTab(w http.ResponseWriter, r *http.Request) { + crew, err := ui.pds.ListCrewMembers(r.Context()) if err != nil { - return nil, err - } - - // Single bulk query for all user quotas - allQuotas, err := ui.pds.GetAllUserQuotas(ctx) - if err != nil { - slog.Warn("Failed to get all user quotas for crew views", "error", err) - allQuotas = make(map[string]*pds.QuotaStats) + http.Error(w, "Failed to list crew: "+err.Error(), http.StatusInternalServerError) + return } defaultTier := "default" @@ -69,68 +75,94 @@ func (ui *AdminUI) getCrewViews(ctx context.Context) ([]CrewMemberView, error) { defaultTier = ui.quotaMgr.GetDefaultTier() } - var crewViews []CrewMemberView + var skeletons []CrewSkeletonView for _, member := range crew { tier := member.Record.Tier if tier == "" { tier = defaultTier } - - view := CrewMemberView{ + skeletons = append(skeletons, CrewSkeletonView{ RKey: member.Rkey, DID: member.Record.Member, - Handle: resolveHandle(ctx, member.Record.Member), Role: member.Record.Role, Permissions: member.Record.Permissions, Tier: tier, AddedAt: parseTime(member.Record.AddedAt), - } + }) + } - usage := int64(0) - if q, ok := allQuotas[member.Record.Member]; ok { - usage = q.TotalSize - } + sort.Slice(skeletons, func(i, j int) bool { + return skeletons[i].AddedAt.After(skeletons[j].AddedAt) + }) - if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { - if limit := ui.quotaMgr.GetTierLimit(tier); limit != nil { - view.TierLimit = formatHumanBytes(*limit) - if *limit > 0 { - view.UsagePercent = int(float64(usage) / float64(*limit) * 100) - } - } else { - view.TierLimit = "Unlimited" + data := struct { + Crew []CrewSkeletonView + }{ + Crew: skeletons, + } + ui.renderTemplate(w, "partials/tab_crew.html", data) +} + +// handleCrewMemberInfo returns a fully populated crew member row (HTMX partial). +// Called per-row via hx-trigger="load" — resolves handle and fetches usage. +func (ui *AdminUI) handleCrewMemberInfo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + rkey := r.URL.Query().Get("rkey") + if rkey == "" { + http.Error(w, "Missing rkey parameter", http.StatusBadRequest) + return + } + + _, member, err := ui.pds.GetCrewMember(ctx, rkey) + if err != nil { + slog.Warn("Failed to get crew member for lazy load", "rkey", rkey, "error", err) + http.Error(w, "Crew member not found", http.StatusNotFound) + return + } + + handle := resolveHandle(ctx, member.Member) + + usage := int64(0) + if q, err := ui.pds.GetQuotaForUser(ctx, member.Member); err == nil { + usage = q.TotalSize + } + + defaultTier := "default" + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { + defaultTier = ui.quotaMgr.GetDefaultTier() + } + + tier := member.Tier + if tier == "" { + tier = defaultTier + } + + view := CrewMemberView{ + RKey: rkey, + DID: member.Member, + Handle: handle, + Role: member.Role, + Permissions: member.Permissions, + Tier: tier, + CurrentUsage: usage, + UsageHuman: formatHumanBytes(usage), + AddedAt: parseTime(member.AddedAt), + } + + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { + if limit := ui.quotaMgr.GetTierLimit(tier); limit != nil { + view.TierLimit = formatHumanBytes(*limit) + if *limit > 0 { + view.UsagePercent = int(float64(usage) / float64(*limit) * 100) } } else { view.TierLimit = "Unlimited" } - - view.CurrentUsage = usage - view.UsageHuman = formatHumanBytes(view.CurrentUsage) - - crewViews = append(crewViews, view) + } else { + view.TierLimit = "Unlimited" } - sort.Slice(crewViews, func(i, j int) bool { - return crewViews[i].CurrentUsage > crewViews[j].CurrentUsage - }) - - return crewViews, nil -} - -// handleCrewTab returns the crew tab content (HTMX partial) -func (ui *AdminUI) handleCrewTab(w http.ResponseWriter, r *http.Request) { - crewViews, err := ui.getCrewViews(r.Context()) - if err != nil { - http.Error(w, "Failed to list crew: "+err.Error(), http.StatusInternalServerError) - return - } - - data := struct { - Crew []CrewMemberView - }{ - Crew: crewViews, - } - ui.renderTemplate(w, "partials/tab_crew.html", data) + ui.renderTemplate(w, "partials/crew_member_row.html", view) } // handleCrewAddForm displays the add crew form diff --git a/pkg/hold/admin/templates/partials/crew_member_row.html b/pkg/hold/admin/templates/partials/crew_member_row.html new file mode 100644 index 0000000..935e43b --- /dev/null +++ b/pkg/hold/admin/templates/partials/crew_member_row.html @@ -0,0 +1,38 @@ +{{define "partials/crew_member_row.html"}} +
{{.DID}}
+ {{.DID}}
+
+ {{truncate .DID 32}}