0.2.77
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// The console's window on the outbound notification log.
|
||||
//
|
||||
// One route rather than three, unlike the sign-in history: an operator arrives here with a
|
||||
// *question* — "did the weekly summary go out", "why did nobody get told about that
|
||||
// import" — and every part of the answer is the same filtered window. Splitting the table
|
||||
// from its totals would mean two requests that could disagree with each other while a
|
||||
// filter was being typed.
|
||||
const (
|
||||
// notificationPageLimit caps one page. Large enough that the ordinary answer needs no
|
||||
// paging, small enough that a household with a busy week does not send a megabyte.
|
||||
notificationPageLimit = 100
|
||||
// notificationWindowDays is the widest window the page offers, derived from the
|
||||
// retention period rather than written down: PruneNotificationLog removes anything
|
||||
// older, so a page offering 180 days would draw a flat line for half of it.
|
||||
notificationWindowDays = int(store.NotificationRetention / (24 * time.Hour))
|
||||
)
|
||||
|
||||
type adminNotificationLogResponse struct {
|
||||
Entries []store.NotificationLogEntry `json:"entries"`
|
||||
Total int `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Totals store.NotificationLogTotals `json:"totals"`
|
||||
Days []store.NotificationLogDay `json:"days"`
|
||||
Facets store.NotificationFacets `json:"facets"`
|
||||
Users []store.KnownUser `json:"users"`
|
||||
Retention int `json:"retentionDays"`
|
||||
}
|
||||
|
||||
// notificationLogFilter reads the console's question off the query string.
|
||||
//
|
||||
// Every list filter is comma-separated and multi-valued, because the useful questions are
|
||||
// plural: "everything that failed or was skipped", "both digest kinds". A single-valued
|
||||
// filter would make the common troubleshooting question take two passes.
|
||||
func notificationLogFilter(r *http.Request) store.NotificationLogFilter {
|
||||
query := r.URL.Query()
|
||||
filter := store.NotificationLogFilter{
|
||||
UserID: strings.TrimSpace(query.Get("user")),
|
||||
Kinds: splitCSV(query.Get("kind")),
|
||||
Channels: splitCSV(query.Get("channel")),
|
||||
Statuses: splitCSV(query.Get("status")),
|
||||
Sources: splitCSV(query.Get("source")),
|
||||
Query: strings.TrimSpace(query.Get("q")),
|
||||
Limit: queryInt(r, "limit", notificationPageLimit, 500),
|
||||
Offset: queryInt(r, "offset", 0, 100000),
|
||||
}
|
||||
filter.From, filter.To = notificationWindow(r)
|
||||
return filter
|
||||
}
|
||||
|
||||
// notificationWindow resolves the date range.
|
||||
//
|
||||
// An explicit `from` wins over the day window, the rule the sign-in history follows: an
|
||||
// operator who typed a date meant it, and silently narrowing it to the last week would
|
||||
// answer a question they did not ask. `to` is read as the *end* of the day named, because
|
||||
// somebody filtering "to the 12th" means through the 12th, not up to midnight at its start.
|
||||
func notificationWindow(r *http.Request) (time.Time, time.Time) {
|
||||
query := r.URL.Query()
|
||||
from := parseDay(query.Get("from"))
|
||||
to := parseDay(query.Get("to"))
|
||||
if !to.IsZero() {
|
||||
to = to.AddDate(0, 0, 1)
|
||||
}
|
||||
if from.IsZero() {
|
||||
days := queryInt(r, "days", 7, notificationWindowDays)
|
||||
if days > 0 {
|
||||
from = time.Now().UTC().AddDate(0, 0, -days)
|
||||
}
|
||||
}
|
||||
return from, to
|
||||
}
|
||||
|
||||
func parseDay(raw string) time.Time {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
day, err := time.Parse("2006-01-02", raw)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return day
|
||||
}
|
||||
|
||||
// handleAdminNotificationLog answers the Notifications page.
|
||||
//
|
||||
// The log is the page and everything else is decoration, which is why only its failure is
|
||||
// a 500: a facet list or a name lookup that will not answer costs a dropdown, and an
|
||||
// operator reading this page after something went wrong must still get the rows.
|
||||
func (s *Server) handleAdminNotificationLog(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
filter := notificationLogFilter(r)
|
||||
|
||||
page, err := s.store.NotificationLog(ctx, filter)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Error("notification log read failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not read the notification history")
|
||||
return
|
||||
}
|
||||
|
||||
response := adminNotificationLogResponse{
|
||||
Entries: page.Entries, Total: page.Total, Limit: page.Limit, Offset: page.Offset,
|
||||
Days: []store.NotificationLogDay{}, Users: []store.KnownUser{},
|
||||
Retention: notificationWindowDays,
|
||||
}
|
||||
if totals, err := s.store.NotificationLogTotals(ctx, filter); err == nil {
|
||||
response.Totals = totals
|
||||
} else {
|
||||
s.loggerFor(ctx).Warn("notification totals unavailable", "error", err)
|
||||
}
|
||||
if days, err := s.store.NotificationLogDays(ctx, filter); err == nil {
|
||||
response.Days = days
|
||||
}
|
||||
// Facets are computed over the whole retention window rather than the current filter,
|
||||
// so narrowing the table never removes the option that would widen it again.
|
||||
since := time.Now().UTC().Add(-store.NotificationRetention)
|
||||
if facets, err := s.store.NotificationLogFacets(ctx, since); err == nil {
|
||||
response.Facets = facets
|
||||
} else {
|
||||
s.loggerFor(ctx).Warn("notification facets unavailable", "error", err)
|
||||
}
|
||||
if users, err := s.store.KnownUsers(ctx); err == nil {
|
||||
response.Users = users
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
Reference in New Issue
Block a user