refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns

Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-24 23:43:50 +01:00
co-authored by Sisyphus
parent 7b24f8e484
commit e4b9003439
36 changed files with 1923 additions and 590 deletions
+49 -26
View File
@@ -9,11 +9,20 @@ import (
"time"
"crussell/db"
"crussell/clock"
"crussell/internal/validators"
"crussell/mw"
"log"
)
var londonLocation = func() *time.Location {
loc, err := time.LoadLocation("Europe/London")
if err != nil {
panic("failed to load Europe/London timezone: " + err.Error())
}
return loc
}()
// --- Types ---
type DefaultHours struct {
Weekday int `json:"weekday" validate:"gte=0,lte=6"`
@@ -156,9 +165,11 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
}
useOutOfHours := outOfHours && isAdmin
// Set to local start/end of day
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, time.Local)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, time.Local)
// Set to local start/end of day in Europe/London so that bookings
// at BST midnight (23:00 UTC the previous day) are included in the
// correct date range.
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, londonLocation)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, londonLocation)
// Load default hours
defaultMap := map[int]DefaultHours{}
@@ -197,7 +208,7 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
for appRows.Next() {
var a appEntry
if err := appRows.Scan(&a.GroupID, &a.WeekStart); err == nil {
a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, time.Local)
a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, time.UTC)
apps = append(apps, a)
groupIDs = append(groupIDs, a.GroupID)
}
@@ -236,7 +247,7 @@ func GetWorkingHours(w http.ResponseWriter, r *http.Request) {
daysSinceMonday = 6 // Sunday
}
weekStart := d.AddDate(0, 0, -daysSinceMonday)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, time.Local)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, time.UTC)
var applied *ExceptionalHours
weekStartStr := weekStart.Format("2006-01-02")
@@ -346,9 +357,9 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
// Parse out_of_hours toggle (admin-only extended hours)
outOfHours := r.URL.Query().Get("out_of_hours") == "true"
// set start/end of day
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, time.Local)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, time.Local)
// set start/end of day in Europe/London (see comment above)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, londonLocation)
end = time.Date(end.Year(), end.Month(), end.Day(), 23, 59, 59, 999999999, londonLocation)
// Clean up old reservations (older than 1 hour)
if err := CleanupOldReservations(r.Context()); err != nil {
@@ -429,7 +440,7 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
for appRows.Next() {
var a appEntry
if err := appRows.Scan(&a.GroupID, &a.WeekStart); err == nil {
a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, time.Local)
a.WeekStart = time.Date(a.WeekStart.Year(), a.WeekStart.Month(), a.WeekStart.Day(), 0, 0, 0, 0, time.UTC)
apps = append(apps, a)
groupIDs = append(groupIDs, a.GroupID)
}
@@ -472,11 +483,12 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
var t time.Time
var dur int
if err := bookingRows.Scan(&t, &dur); err == nil {
dateStr := t.Format("2006-01-02")
tLondon := t.In(londonLocation)
dateStr := tLondon.Format("2006-01-02")
endTime := t.Add(time.Duration(dur) * time.Minute)
bookings[dateStr] = append(bookings[dateStr], TimeSlot{
StartTime: t.Format("15:04"),
EndTime: endTime.Format("15:04"),
StartTime: tLondon.Format("15:04"),
EndTime: endTime.In(londonLocation).Format("15:04"),
})
}
}
@@ -501,19 +513,23 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
// "00:00" as an end time would incorrectly appear before all slot times.
cur := blockStart
for cur.Before(blockEnd) {
dayEnd := time.Date(cur.Year(), cur.Month(), cur.Day(), 0, 0, 0, 0, cur.Location()).AddDate(0, 0, 1)
dayEnd := time.Date(cur.Year(), cur.Month(), cur.Day(), 0, 0, 0, 0, londonLocation).AddDate(0, 0, 1)
segEnd := blockEnd
if segEnd.After(dayEnd) {
segEnd = dayEnd
}
dateStr := cur.Format("2006-01-02")
endStr := segEnd.Format("15:04")
// Format times in Europe/London so that blocker time strings use
// wall-clock hours matching working_hours and booking slots.
londonStart := cur.In(londonLocation)
londonEnd := segEnd.In(londonLocation)
endStr := londonEnd.Format("15:04")
if segEnd.Equal(dayEnd) {
endStr = "24:00"
}
blockerMap[dateStr] = append(blockerMap[dateStr], TimeSlot{
StartTime: cur.Format("15:04"),
StartTime: londonStart.Format("15:04"),
EndTime: endStr,
})
@@ -543,7 +559,7 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
daysSinceMonday = 6 // Sunday
}
weekStart := d.AddDate(0, 0, -daysSinceMonday)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, time.Local)
weekStart = time.Date(weekStart.Year(), weekStart.Month(), weekStart.Day(), 0, 0, 0, 0, time.UTC)
var applied *ExceptionalHours
weekStartStr := weekStart.Format("2006-01-02")
@@ -610,8 +626,9 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
// Late night lock: after 22:00, block next morning 00:00-11:00 for non-admin users
if !isAdmin {
now := time.Now()
if now.Hour() >= 22 {
now := clock.Now()
londonNow := now.In(londonLocation)
if londonNow.Hour() >= 22 {
// Check if this is tomorrow's date
tomorrow := now.AddDate(0, 0, 1)
tomorrowStr := tomorrow.Format("2006-01-02")
@@ -639,15 +656,21 @@ func GetAvailableHours(w http.ResponseWriter, r *http.Request) {
// normalizeTime strips seconds from HH:MM:SS to HH:MM for consistent string
// comparison with blocker and booking time formats in subtractTimeSlots.
func normalizeTime(t string) string {
// Strip trailing :SS (seconds) from HH:MM:SS format while leaving
// bare HH:MM untouched and handling single-digit hours (e.g. 9:00:00).
if len(t) > 5 && t[len(t)-3] == ':' {
prefix := t[:len(t)-3]
if strings.Contains(prefix, ":") {
return prefix
}
parts := strings.Split(t, ":")
if len(parts) < 2 {
return t
}
return t
hour := parts[0]
minute := parts[1]
// Only pad numeric single-digit segments. Non-numeric single-char
// values (e.g. from garbage input) pass through without padding.
if len(hour) == 1 && hour[0] >= '0' && hour[0] <= '9' {
hour = "0" + hour
}
if len(minute) == 1 && minute[0] >= '0' && minute[0] <= '9' {
minute = "0" + minute
}
return hour + ":" + minute
}
// subtractTimeSlots removes gaps from available slots