Files
bvandeusen 7fac264c73 feat(playlists): wire Refresh into PUT /api/me/timezone + registration
Completes the server side of #392 Half B.

PUT /api/me/timezone now calls scheduler.Refresh(ctx, userID) after
the DB write so the rescheduled daily-at-03:00-local job takes
effect synchronously. Failure to refresh is logged but doesn't
undo the DB write — the hourly reconciliation pass would pick it
up within an hour regardless.

POST /api/auth/register calls Refresh after successful user
insert so brand-new users get scheduled immediately rather than
waiting for the hourly pass to discover them.

system_cron.go deleted: the new scheduler subsumes its
responsibilities. The StartSystemPlaylistCron call in main.go is
also removed. Server restart now runs the new scheduler's startup
recovery + catch-up instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:58:24 -04:00

71 lines
2.2 KiB
Go

// PUT /api/me/timezone — accept the client's IANA timezone and store
// it on the user row. The system-playlist scheduler (#392 Half B)
// reads this column to fire each user's daily 03:00 build in their
// local time.
//
// Scheduler.Refresh(ctx, userID) is invoked in a later commit (Task 3)
// so a timezone change takes effect synchronously rather than waiting
// for the hourly reconciliation pass.
package api
import (
"encoding/json"
"net/http"
"time"
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// putTimezoneBody is the JSON body for PUT /api/me/timezone.
type putTimezoneBody struct {
Timezone string `json:"timezone"`
}
func (h *handlers) handlePutTimezone(w http.ResponseWriter, r *http.Request) {
user, ok := auth.UserFromContext(r.Context())
if !ok {
writeErr(w, apierror.Unauthorized("unauthorized", "authentication required"))
return
}
var body putTimezoneBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, apierror.BadRequest("bad_body", "invalid JSON body"))
return
}
if body.Timezone == "" {
writeErr(w, apierror.BadRequest("bad_body", "timezone is required"))
return
}
if _, err := time.LoadLocation(body.Timezone); err != nil {
writeErr(w, apierror.BadRequest("invalid_timezone",
"timezone must be a valid IANA name (e.g. America/New_York)"))
return
}
q := dbq.New(h.pool)
if err := q.UpdateUserTimezone(r.Context(), dbq.UpdateUserTimezoneParams{
ID: user.ID,
Timezone: body.Timezone,
}); err != nil {
h.logger.Error("api: update timezone", "err", err)
writeErr(w, apierror.InternalMsg("update failed", err))
return
}
// Reschedule the user's daily build at their new timezone. Failure
// here doesn't undo the DB write — the hourly reconciliation pass
// would pick up the new tz within an hour at worst.
if h.playlistScheduler != nil {
if err := h.playlistScheduler.Refresh(r.Context(), user.ID); err != nil {
h.logger.Warn("api: scheduler refresh after timezone update",
"user_id", uuidToString(user.ID), "err", err)
}
}
w.WriteHeader(http.StatusNoContent)
}