api: scope activities, sync, review-queue, and reclassify handlers to userID

Completes the internal/api scoping pass -- the whole package now compiles
against the per-user store/sync/garmin signatures from Tasks 4-13. Also
fixes the test helpers (newTestServer now returns the provisioned userID)
and a latent bug in TestResolveUser_LeavesContextEmptyWhenNotProvisioned,
which relied on doJSON's hardcoded "test-user" session sub being
unprovisioned -- never caught before since internal/api couldn't compile
since Task 12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 18:28:02 +02:00
parent eaca8e602b
commit e37173ea08
7 changed files with 137 additions and 94 deletions

View File

@@ -19,8 +19,14 @@ const detailFillBatchSize = 50
// FullSync run so "last sync" reports the combined activity count, not just
// whichever of Backfill/IncrementalSync happened to finish last.
func (s *Server) handleSyncRun(w http.ResponseWriter, r *http.Request) {
ok := s.backgroundSync(func(ctx context.Context) error {
return s.Sync.FullSync(ctx, detailFillBatchSize)
userID := userIDFromContext(r.Context())
svc, err := s.syncFor(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
ok := s.backgroundSync(userID, func(ctx context.Context) error {
return svc.FullSync(ctx, detailFillBatchSize)
})
if !ok {
writeError(w, http.StatusConflict, "a sync is already in progress")
@@ -34,8 +40,14 @@ func (s *Server) handleSyncRun(w http.ResponseWriter, r *http.Request) {
// performs a genuinely fresh pull from Garmin. Destructive -- the frontend
// gates this behind a confirmation.
func (s *Server) handleSyncReset(w http.ResponseWriter, r *http.Request) {
ok := s.backgroundSync(func(ctx context.Context) error {
return s.Sync.ResetAll(ctx)
userID := userIDFromContext(r.Context())
svc, err := s.syncFor(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
ok := s.backgroundSync(userID, func(ctx context.Context) error {
return svc.ResetAll(ctx)
})
if !ok {
writeError(w, http.StatusConflict, "a sync is already in progress")
@@ -45,7 +57,8 @@ func (s *Server) handleSyncReset(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleSyncRuns(w http.ResponseWriter, r *http.Request) {
runs, err := s.DB.ListSyncRuns(r.Context(), 20)
userID := userIDFromContext(r.Context())
runs, err := s.DB.ListSyncRuns(r.Context(), userID, 20)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -54,22 +67,28 @@ func (s *Server) handleSyncRuns(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) {
run, ok, err := s.DB.LatestSyncRun(r.Context())
userID := userIDFromContext(r.Context())
run, ok, err := s.DB.LatestSyncRun(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
remaining, err := s.DB.CountActivitiesMissingDetails(r.Context())
remaining, err := s.DB.CountActivitiesMissingDetails(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.mu.Lock()
inProgress := s.syncRunning
inProgress := s.userSyncRunning[userID]
s.mu.Unlock()
progress := s.Sync.Progress()
svc, err := s.syncFor(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
progress := svc.Progress()
resp := map[string]any{
"in_progress": inProgress,