Added a "Published in the last X days" filter, applied by the backend

GET /api/opportunities takes released_from, an RFC 3339 time, and lists
only the opportunities whose latest release is at or after it. It works
with paging and with changed_after, which keeps the same batch either
way; an invalid value is 400.

The frontend's filter defaults to 7 days, from midnight UK time, and
sends it with every load and catch-up, so a year of opportunities isn't
downloaded to show a week's. Changing the days loads the list again;
Clear filters goes back to 7.
This commit is contained in:
bwilliams committed 2026-10-19 10:05:45 +01:00
1 parent b8244da2e5
commit 746fa0a9c1
19 files changed
+333 -64

No files matched your search

+26 -11
View File
@@ -7,6 +7,7 @@ import (
"reflect"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
@@ -71,7 +72,8 @@ func (h *opportunityRoutes) Routes(router chi.Router) {
// list serves GET /opportunities: a page of opportunities, newest release
// first, with their classifications and marks but not their releases. The
// page is set by the offset and limit (default 50, at most 100) query
// parameters. With changed_after, it serves the opportunities changed since
// parameters, and released_from leaves out opportunities whose latest release
// is before it. With changed_after, it serves the opportunities changed since
// that batch instead (see listChanged).
//
// @Summary List opportunities
@@ -85,27 +87,38 @@ func (h *opportunityRoutes) Routes(router chi.Router) {
// @Description releases or with changed marks), and batch is the newest
// @Description batch they include. offset and limit can't be given with
// @Description changed_after. An unknown batch is 410 Gone: reload the
// @Description whole list.
// @Description whole list. released_from (RFC 3339, inclusive) leaves out
// @Description opportunities whose latest release is before it, in a page
// @Description or with changed_after.
// @Tags opportunities
// @Produce json
// @Param offset query int false "Opportunities to skip" minimum(0) default(0)
// @Param limit query int false "Page size" minimum(1) maximum(100) default(50)
// @Param changed_after query string false "List the opportunities changed after this batch instead"
// @Param released_from query string false "Only opportunities whose latest release is at or after this time" format(date-time)
// @Success 200 {object} OpportunityPage
// @Failure 400 {object} httpx.Problem "Invalid offset or limit, or either with changed_after"
// @Failure 400 {object} httpx.Problem "Invalid offset, limit or released_from, or offset or limit with changed_after"
// @Failure 410 {object} httpx.Problem "Unknown batch"
// @Failure 500 {object} httpx.Problem
// @Router /opportunities [get]
func (h *opportunityRoutes) list(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
releasedFrom, err := timeParam(
"released_from", query.Get("released_from"),
)
if err != nil {
httpx.WriteProblem(w, r, http.StatusBadRequest, err.Error())
return
}
if query.Has("changed_after") {
h.listChanged(w, r)
h.listChanged(w, r, releasedFrom)
return
}
offset, offsetErr := intParam(query.Get("offset"), 0, 0, 1<<30)
limit, limitErr := intParam(query.Get("limit"), 50, 1, 100)
err := errors.Join(offsetErr, limitErr)
err = errors.Join(offsetErr, limitErr)
if err != nil {
httpx.WriteProblem(w, r, http.StatusBadRequest, err.Error())
return
@@ -123,8 +136,9 @@ func (h *opportunityRoutes) list(w http.ResponseWriter, r *http.Request) {
opportunities, err := h.store.ListOpportunities(
r.Context(),
store.OpportunityFilter{
Limit: limit + 1,
Offset: offset,
Limit: limit + 1,
Offset: offset,
ReleasedFrom: releasedFrom,
},
)
if err != nil {
@@ -148,10 +162,11 @@ func (h *opportunityRoutes) list(w http.ResponseWriter, r *http.Request) {
// listChanged serves GET /opportunities?changed_after=batch: every
// opportunity changed after the change batch, as one page, with the newest
// batch it includes. An unknown batch, such as one from another database, is
// 410 Gone, as the client can't catch up from it.
// batch it includes, leaving out those whose latest release is before
// releasedFrom unless it is zero. An unknown batch, such as one from another
// database, is 410 Gone, as the client can't catch up from it.
func (h *opportunityRoutes) listChanged(
w http.ResponseWriter, r *http.Request,
w http.ResponseWriter, r *http.Request, releasedFrom time.Time,
) {
query := r.URL.Query()
if query.Has("offset") || query.Has("limit") {
@@ -165,7 +180,7 @@ func (h *opportunityRoutes) listChanged(
}
opportunities, batch, err := h.store.ListChangedOpportunities(
r.Context(), query.Get("changed_after"),
r.Context(), query.Get("changed_after"), releasedFrom,
)
if errors.Is(err, store.ErrUnknownBatch) {
httpx.WriteProblem(