3002935b76
continuous-integration/drone/push Build is passing
- Increased the cache duration for dashboard statistics from 60 seconds to 5 minutes, improving data freshness and reducing load on the backend. - Introduced a stale cache mechanism that allows retrieval of stale statistics while refreshing them in the background, enhancing user experience by providing quicker access to data. - Updated the statistics repository to handle the new caching logic, ensuring accurate and timely statistics reporting. - Added tests to validate the new caching behavior and ensure the integrity of statistics retrieval. This update optimizes the dashboard's performance and responsiveness by improving the caching strategy for statistics.
203 lines
6.5 KiB
Go
203 lines
6.5 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"strconv"
|
|
"tm/pkg/response"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// Handler handles dashboard HTTP requests.
|
|
type Handler struct {
|
|
service Service
|
|
}
|
|
|
|
// NewHandler creates a dashboard handler.
|
|
func NewHandler(service Service) *Handler {
|
|
return &Handler{service: service}
|
|
}
|
|
|
|
// Summary returns top-level dashboard counters.
|
|
// @Summary Dashboard summary
|
|
// @Description Top-level counters for hero pills and stat cards
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param closing_window query int false "Hours considered closing soon (default 168)"
|
|
// @Success 200 {object} response.APIResponse{data=SummaryResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/summary [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Summary(c echo.Context) error {
|
|
query := SummaryQuery{
|
|
ClosingWindow: parseIntQuery(c, "closing_window", 0),
|
|
}
|
|
|
|
out, err := h.service.Summary(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard summary")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
return response.Success(c, out, "Dashboard summary retrieved successfully")
|
|
}
|
|
|
|
// Trend returns per-day tender counts for the flow chart.
|
|
// @Summary Dashboard trend
|
|
// @Description Per-day counts for the tender flow chart
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param days query int false "Days back (default 14, max 90)"
|
|
// @Param metric query string false "created|published|awarded (default created)"
|
|
// @Success 200 {object} response.APIResponse{data=TrendResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/trend [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Trend(c echo.Context) error {
|
|
query := TrendQuery{
|
|
Days: parseIntQuery(c, "days", 0),
|
|
Metric: c.QueryParam("metric"),
|
|
}
|
|
|
|
out, err := h.service.Trend(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard trend")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
return response.Success(c, out, "Dashboard trend retrieved successfully")
|
|
}
|
|
|
|
// Countries returns country distribution buckets.
|
|
// @Summary Dashboard countries
|
|
// @Description Country distribution for donut and ranking list
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param limit query int false "Top N countries (default 6)"
|
|
// @Success 200 {object} response.APIResponse{data=CountriesResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/countries [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Countries(c echo.Context) error {
|
|
query := CountriesQuery{
|
|
Limit: parseIntQuery(c, "limit", 0),
|
|
}
|
|
|
|
out, err := h.service.Countries(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard countries")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
return response.Success(c, out, "Dashboard countries retrieved successfully")
|
|
}
|
|
|
|
// NoticeTypes returns notice type distribution.
|
|
// @Summary Dashboard notice types
|
|
// @Description Notice type mix for stacked bars
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Success 200 {object} response.APIResponse{data=NoticeTypesResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/notice-types [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) NoticeTypes(c echo.Context) error {
|
|
out, err := h.service.NoticeTypes(c.Request().Context())
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard notice types")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
return response.Success(c, out, "Dashboard notice types retrieved successfully")
|
|
}
|
|
|
|
// ClosingSoon returns tenders closing within the configured window.
|
|
// @Summary Dashboard closing soon
|
|
// @Description Actionable tenders sorted by urgency
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param limit query int false "Max rows (default 5, max 20)"
|
|
// @Param window query int false "Hours horizon (default 168)"
|
|
// @Success 200 {object} response.APIResponse{data=ClosingSoonResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/closing-soon [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) ClosingSoon(c echo.Context) error {
|
|
query := ClosingSoonQuery{
|
|
Limit: parseIntQuery(c, "limit", 0),
|
|
Window: parseIntQuery(c, "window", 0),
|
|
}
|
|
|
|
out, err := h.service.ClosingSoon(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load closing soon tenders")
|
|
}
|
|
|
|
return response.Success(c, out, "Closing soon tenders retrieved successfully")
|
|
}
|
|
|
|
// Recent returns the most recently created tenders.
|
|
// @Summary Dashboard recent tenders
|
|
// @Description Recent tenders feed sorted by created_at
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param limit query int false "Max rows (default 5, max 20)"
|
|
// @Param cursor query string false "Opaque pagination cursor"
|
|
// @Success 200 {object} response.APIResponse{data=RecentResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/recent [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Recent(c echo.Context) error {
|
|
query := RecentQuery{
|
|
Limit: parseIntQuery(c, "limit", 0),
|
|
Cursor: c.QueryParam("cursor"),
|
|
}
|
|
|
|
out, err := h.service.Recent(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load recent tenders")
|
|
}
|
|
|
|
setPrivateCache(c, 60)
|
|
return response.Success(c, out, "Recent tenders retrieved successfully")
|
|
}
|
|
|
|
// Statistics returns scraping and translation statistics for charts.
|
|
// @Summary Dashboard statistics report
|
|
// @Description Daily and lifetime statistics for TED scraping, document scraping, and AI translation
|
|
// @Tags Admin-Dashboard
|
|
// @Produce json
|
|
// @Param days query int false "Days back for daily series (default 14, max 90)"
|
|
// @Success 200 {object} response.APIResponse{data=StatisticsReportResponse}
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/v1/dashboard/statistics [get]
|
|
// @Security BearerAuth
|
|
func (h *Handler) Statistics(c echo.Context) error {
|
|
query := StatisticsQuery{
|
|
Days: parseIntQuery(c, "days", 0),
|
|
}
|
|
|
|
out, err := h.service.Statistics(c.Request().Context(), query)
|
|
if err != nil {
|
|
return response.InternalServerError(c, "Failed to load dashboard statistics")
|
|
}
|
|
|
|
setPrivateCache(c, 300)
|
|
return response.Success(c, out, "Dashboard statistics retrieved successfully")
|
|
}
|
|
|
|
func parseIntQuery(c echo.Context, name string, fallback int) int {
|
|
raw := c.QueryParam(name)
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
v, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return v
|
|
}
|
|
|
|
func setPrivateCache(c echo.Context, maxAge int) {
|
|
c.Response().Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(maxAge))
|
|
}
|