This commit is contained in:
2026-03-21 15:41:30 -04:00
parent 197a1577c2
commit ba919bbde4
19 changed files with 824 additions and 198 deletions

View File

@@ -280,6 +280,13 @@ type ImportResultsResponse struct {
// ImportType defines model for ImportType.
type ImportType string
// InfoResponse defines model for InfoResponse.
type InfoResponse struct {
RegistrationEnabled bool `json:"registration_enabled"`
SearchEnabled bool `json:"search_enabled"`
Version string `json:"version"`
}
// LeaderboardData defines model for LeaderboardData.
type LeaderboardData struct {
All []LeaderboardEntry `json:"all"`
@@ -290,8 +297,8 @@ type LeaderboardData struct {
// LeaderboardEntry defines model for LeaderboardEntry.
type LeaderboardEntry struct {
UserId string `json:"user_id"`
Value int64 `json:"value"`
UserId string `json:"user_id"`
Value float64 `json:"value"`
}
// LogEntry defines model for LogEntry.
@@ -598,6 +605,9 @@ type ServerInterface interface {
// Get user streaks
// (GET /home/streaks)
GetStreaks(w http.ResponseWriter, r *http.Request)
// Get server information
// (GET /info)
GetInfo(w http.ResponseWriter, r *http.Request)
// List progress records
// (GET /progress)
GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams)
@@ -1174,6 +1184,20 @@ func (siw *ServerInterfaceWrapper) GetStreaks(w http.ResponseWriter, r *http.Req
handler.ServeHTTP(w, r)
}
// GetInfo operation middleware
func (siw *ServerInterfaceWrapper) GetInfo(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetInfo(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetProgressList operation middleware
func (siw *ServerInterfaceWrapper) GetProgressList(w http.ResponseWriter, r *http.Request) {
@@ -1510,6 +1534,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc("GET "+options.BaseURL+"/home/graph", wrapper.GetGraphData)
m.HandleFunc("GET "+options.BaseURL+"/home/statistics", wrapper.GetUserStatistics)
m.HandleFunc("GET "+options.BaseURL+"/home/streaks", wrapper.GetStreaks)
m.HandleFunc("GET "+options.BaseURL+"/info", wrapper.GetInfo)
m.HandleFunc("GET "+options.BaseURL+"/progress", wrapper.GetProgressList)
m.HandleFunc("GET "+options.BaseURL+"/progress/{id}", wrapper.GetProgress)
m.HandleFunc("GET "+options.BaseURL+"/search", wrapper.GetSearch)
@@ -2350,6 +2375,31 @@ func (response GetStreaks500JSONResponse) VisitGetStreaksResponse(w http.Respons
return json.NewEncoder(w).Encode(response)
}
type GetInfoRequestObject struct {
}
type GetInfoResponseObject interface {
VisitGetInfoResponse(w http.ResponseWriter) error
}
type GetInfo200JSONResponse InfoResponse
func (response GetInfo200JSONResponse) VisitGetInfoResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type GetInfo500JSONResponse ErrorResponse
func (response GetInfo500JSONResponse) VisitGetInfoResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
return json.NewEncoder(w).Encode(response)
}
type GetProgressListRequestObject struct {
Params GetProgressListParams
}
@@ -2650,6 +2700,9 @@ type StrictServerInterface interface {
// Get user streaks
// (GET /home/streaks)
GetStreaks(ctx context.Context, request GetStreaksRequestObject) (GetStreaksResponseObject, error)
// Get server information
// (GET /info)
GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error)
// List progress records
// (GET /progress)
GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error)
@@ -3260,6 +3313,30 @@ func (sh *strictHandler) GetStreaks(w http.ResponseWriter, r *http.Request) {
}
}
// GetInfo operation middleware
func (sh *strictHandler) GetInfo(w http.ResponseWriter, r *http.Request) {
var request GetInfoRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.GetInfo(ctx, request.(GetInfoRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "GetInfo")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(GetInfoResponseObject); ok {
if err := validResponse.VisitGetInfoResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// GetProgressList operation middleware
func (sh *strictHandler) GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) {
var request GetProgressListRequestObject

View File

@@ -174,66 +174,66 @@ func convertGraphData(graphData []database.GetDailyReadStatsRow) []GraphDataPoin
}
func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) UserStatisticsResponse {
// Sort helper - sort by WPM
sortByWPM := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry {
// Sort by WPM for each period
sortByWPM := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) float64) []LeaderboardEntry {
sorted := append([]database.GetUserStatisticsRow(nil), stats...)
sort.SliceStable(sorted, func(i, j int) bool {
return sorted[i].TotalWpm > sorted[j].TotalWpm
return getter(sorted[i]) > getter(sorted[j])
})
result := make([]LeaderboardEntry, len(sorted))
for i, item := range sorted {
result[i] = LeaderboardEntry{UserId: item.UserID, Value: int64(item.TotalWpm)}
result[i] = LeaderboardEntry{UserId: item.UserID, Value: getter(item)}
}
return result
}
// Sort by duration (seconds)
sortByDuration := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry {
// Sort by duration (seconds) for each period
sortByDuration := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) int64) []LeaderboardEntry {
sorted := append([]database.GetUserStatisticsRow(nil), stats...)
sort.SliceStable(sorted, func(i, j int) bool {
return sorted[i].TotalSeconds > sorted[j].TotalSeconds
return getter(sorted[i]) > getter(sorted[j])
})
result := make([]LeaderboardEntry, len(sorted))
for i, item := range sorted {
result[i] = LeaderboardEntry{UserId: item.UserID, Value: item.TotalSeconds}
result[i] = LeaderboardEntry{UserId: item.UserID, Value: float64(getter(item))}
}
return result
}
// Sort by words
sortByWords := func(stats []database.GetUserStatisticsRow) []LeaderboardEntry {
// Sort by words for each period
sortByWords := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) int64) []LeaderboardEntry {
sorted := append([]database.GetUserStatisticsRow(nil), stats...)
sort.SliceStable(sorted, func(i, j int) bool {
return sorted[i].TotalWordsRead > sorted[j].TotalWordsRead
return getter(sorted[i]) > getter(sorted[j])
})
result := make([]LeaderboardEntry, len(sorted))
for i, item := range sorted {
result[i] = LeaderboardEntry{UserId: item.UserID, Value: item.TotalWordsRead}
result[i] = LeaderboardEntry{UserId: item.UserID, Value: float64(getter(item))}
}
return result
}
return UserStatisticsResponse{
Wpm: LeaderboardData{
All: sortByWPM(userStatistics),
Year: sortByWPM(userStatistics),
Month: sortByWPM(userStatistics),
Week: sortByWPM(userStatistics),
All: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.TotalWpm }),
Year: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.YearlyWpm }),
Month: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.MonthlyWpm }),
Week: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.WeeklyWpm }),
},
Duration: LeaderboardData{
All: sortByDuration(userStatistics),
Year: sortByDuration(userStatistics),
Month: sortByDuration(userStatistics),
Week: sortByDuration(userStatistics),
All: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.TotalSeconds }),
Year: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.YearlySeconds }),
Month: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.MonthlySeconds }),
Week: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.WeeklySeconds }),
},
Words: LeaderboardData{
All: sortByWords(userStatistics),
Year: sortByWords(userStatistics),
Month: sortByWords(userStatistics),
Week: sortByWords(userStatistics),
All: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.TotalWordsRead }),
Year: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.YearlyWordsRead }),
Month: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.MonthlyWordsRead }),
Week: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.WeeklyWordsRead }),
},
}
}

View File

@@ -460,8 +460,8 @@ components:
user_id:
type: string
value:
type: integer
format: int64
type: number
format: double
required:
- user_id
- value
@@ -617,6 +617,20 @@ components:
filter:
type: string
InfoResponse:
type: object
properties:
version:
type: string
search_enabled:
type: boolean
registration_enabled:
type: boolean
required:
- version
- search_enabled
- registration_enabled
securitySchemes:
BearerAuth:
type: http
@@ -1111,6 +1125,26 @@ paths:
schema:
$ref: '#/components/schemas/ErrorResponse'
/info:
get:
summary: Get server information
operationId: getInfo
tags:
- Info
responses:
200:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/InfoResponse'
500:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/home:
get:
summary: Get home page data

View File

@@ -46,8 +46,8 @@ func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) S
ctx = context.WithValue(ctx, "request", r)
ctx = context.WithValue(ctx, "response", w)
// Skip auth for login endpoint only - cover and file require auth via cookies
if operationID == "Login" {
// Skip auth for login and info endpoints - cover and file require auth via cookies
if operationID == "Login" || operationID == "GetInfo" {
return handler(ctx, w, r, request)
}
@@ -67,3 +67,13 @@ func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) S
}
}
// GetInfo returns server information
func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) {
return GetInfo200JSONResponse{
Version: s.cfg.Version,
SearchEnabled: s.cfg.SearchEnabled,
RegistrationEnabled: s.cfg.RegistrationEnabled,
}, nil
}