Release v0.2.34
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// The console's window on search history.
|
||||
//
|
||||
// Two shapes of the same table, because they answer different questions. The summary says
|
||||
// what the household looks for, which is what a library is bought and organised against;
|
||||
// the log says what happened just now, which is what an operator needs the moment somebody
|
||||
// reports that search is not finding something — it shows the query exactly as it was
|
||||
// typed, by whom, and when.
|
||||
const (
|
||||
// searchTermLimit caps the summary. Long enough to show a tail, short enough that the
|
||||
// table is read rather than scrolled.
|
||||
searchTermLimit = 25
|
||||
// searchEventLimit caps the log. It is a window on recent activity, not an export.
|
||||
searchEventLimit = 100
|
||||
// searchWindowDays is the widest window the page offers, and it is the retention
|
||||
// period rather than a round number: RecordSearch prunes to it, so a page offering
|
||||
// more would draw a flat line for the difference.
|
||||
searchWindowDays = int(store.SearchRetention / (24 * time.Hour))
|
||||
)
|
||||
|
||||
type adminSearchesResponse struct {
|
||||
Days int `json:"days"`
|
||||
Retention int `json:"retentionDays"`
|
||||
Totals store.SearchTotals `json:"totals"`
|
||||
Terms []store.SearchTerm `json:"terms"`
|
||||
Recent []store.SearchEvent `json:"recent"`
|
||||
TermLimit int `json:"termLimit"`
|
||||
EventLimit int `json:"eventLimit"`
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSearches(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
days := queryInt(r, "days", 7, searchWindowDays)
|
||||
since := time.Now().UTC().AddDate(0, 0, -days)
|
||||
|
||||
totals, err := s.store.SearchTotals(ctx, since)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Error("search totals failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not read search history")
|
||||
return
|
||||
}
|
||||
terms, err := s.store.SearchTerms(ctx, since, searchTermLimit)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Error("search terms failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not read search history")
|
||||
return
|
||||
}
|
||||
recent, err := s.store.SearchEvents(ctx, since, searchEventLimit)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Error("search events failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not read search history")
|
||||
return
|
||||
}
|
||||
|
||||
// A name a search cannot be attributed to costs the column and nothing else: the
|
||||
// queries are the page, and an operator reading it after a household member's last
|
||||
// session expired must still see what was searched for.
|
||||
if users, err := s.store.KnownUsers(ctx); err == nil {
|
||||
recent = nameSearchEvents(recent, users)
|
||||
} else {
|
||||
s.loggerFor(ctx).Warn("search history names unresolved", "error", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, adminSearchesResponse{
|
||||
Days: days,
|
||||
Retention: searchWindowDays,
|
||||
Totals: totals,
|
||||
Terms: terms,
|
||||
Recent: recent,
|
||||
TermLimit: searchTermLimit,
|
||||
EventLimit: searchEventLimit,
|
||||
})
|
||||
}
|
||||
|
||||
// nameSearchEvents fills in who made each search.
|
||||
//
|
||||
// Resolved here rather than joined in SQL because the log is capped and the household is
|
||||
// small: one read of sessions serves a whole page, where a join would repeat the lookup
|
||||
// per row. An id with no session left is returned unnamed rather than dropped — the
|
||||
// console prints the id, which still distinguishes one searcher from another.
|
||||
func nameSearchEvents(events []store.SearchEvent, users []store.KnownUser) []store.SearchEvent {
|
||||
names := make(map[string]string, len(users))
|
||||
for _, user := range users {
|
||||
if user.Username != "" {
|
||||
names[user.ID] = user.Username
|
||||
}
|
||||
}
|
||||
for i := range events {
|
||||
events[i].Username = names[events[i].UserID]
|
||||
}
|
||||
return events
|
||||
}
|
||||
Reference in New Issue
Block a user