muchas cosas
This commit is contained in:
parent
e3936fd4cf
commit
eff656adea
20 changed files with 697 additions and 51 deletions
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
|
@ -386,6 +387,51 @@ func GetWorkerStatus(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetTranslationStats returns real translation throughput and live worker count.
|
||||
func GetTranslationStats(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var last1min, last5min, activeWorkers int64
|
||||
var elapsed1min, elapsed5min float64
|
||||
|
||||
db.GetPool().QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(items_translated),0), COALESCE(SUM(elapsed_ms),0)
|
||||
FROM translation_stats WHERE created_at >= NOW() - INTERVAL '1 minute'
|
||||
`).Scan(&last1min, &elapsed1min)
|
||||
|
||||
db.GetPool().QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(items_translated),0), COALESCE(SUM(elapsed_ms),0)
|
||||
FROM translation_stats WHERE created_at >= NOW() - INTERVAL '5 minutes'
|
||||
`).Scan(&last5min, &elapsed5min)
|
||||
|
||||
// Live workers = distinct hosts that recorded stats in the last 2 minutes
|
||||
db.GetPool().QueryRow(ctx, `
|
||||
SELECT COUNT(DISTINCT hostname) FROM translation_stats
|
||||
WHERE created_at >= NOW() - INTERVAL '2 minutes' AND hostname IS NOT NULL
|
||||
`).Scan(&activeWorkers)
|
||||
|
||||
// Also count WS remote workers currently online
|
||||
remoteOnline := 0
|
||||
if err := db.GetPool().QueryRow(ctx, `
|
||||
SELECT COUNT(*) FROM remote_workers WHERE status = 'online'
|
||||
`).Scan(&remoteOnline); err != nil {
|
||||
remoteOnline = 0
|
||||
}
|
||||
|
||||
rateSec := 0.0
|
||||
if elapsed1min > 0 {
|
||||
rateSec = float64(last1min) / (elapsed1min / 1000.0)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"translations_last_1min": last1min,
|
||||
"translations_last_5min": last5min,
|
||||
"rate_per_second": math.Round(rateSec*100) / 100,
|
||||
"rate_per_minute": math.Round(rateSec*60*100) / 100,
|
||||
"active_workers": activeWorkers + int64(remoteOnline),
|
||||
"remote_workers_online": remoteOnline,
|
||||
})
|
||||
}
|
||||
|
||||
func SetWorkerConfig(c *gin.Context) {
|
||||
var req WorkerConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,13 @@ func GetNews(c *gin.Context) {
|
|||
argNum := 1
|
||||
|
||||
if query != "" {
|
||||
where += fmt.Sprintf(" AND (n.titulo ILIKE $%d OR n.resumen ILIKE $%d)", argNum, argNum)
|
||||
if translatedOnly {
|
||||
// With translated_only, search the translated fields too so users can
|
||||
// find translated news by the Spanish (translated) wording.
|
||||
where += fmt.Sprintf(" AND (n.titulo ILIKE $%d OR n.resumen ILIKE $%d OR t.titulo_trad ILIKE $%d OR t.resumen_trad ILIKE $%d)", argNum, argNum, argNum, argNum)
|
||||
} else {
|
||||
where += fmt.Sprintf(" AND (n.titulo ILIKE $%d OR n.resumen ILIKE $%d)", argNum, argNum)
|
||||
}
|
||||
args = append(args, "%"+query+"%")
|
||||
argNum++
|
||||
}
|
||||
|
|
@ -149,6 +155,117 @@ func GetNews(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetEntityNews returns the (translated) news that mention a given entity/alias value.
|
||||
func GetEntityNews(c *gin.Context) {
|
||||
valor := c.Query("valor")
|
||||
tipo := c.DefaultQuery("tipo", "persona")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
perPageStr := c.DefaultQuery("per_page", "20")
|
||||
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
perPage, _ := strconv.Atoi(perPageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 || perPage > 50 {
|
||||
perPage = 20
|
||||
}
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
daysStr := c.Query("days")
|
||||
days := 0
|
||||
if daysStr != "" {
|
||||
days, _ = strconv.Atoi(daysStr)
|
||||
if days < 1 {
|
||||
days = 0
|
||||
}
|
||||
}
|
||||
today := c.Query("today") == "true"
|
||||
offsetStr := c.Query("day_offset")
|
||||
dayOffset := 0
|
||||
useDayOffset := false
|
||||
if offsetStr != "" {
|
||||
dayOffset, _ = strconv.Atoi(offsetStr)
|
||||
if dayOffset < 0 {
|
||||
dayOffset = 0
|
||||
}
|
||||
useDayOffset = true
|
||||
}
|
||||
|
||||
params := []interface{}{valor, tipo}
|
||||
next := 3
|
||||
timeCond := ""
|
||||
switch {
|
||||
case useDayOffset:
|
||||
// Día exacto: desde el inicio del día (hoy - offset) hasta el inicio del día siguiente
|
||||
timeCond = fmt.Sprintf(
|
||||
" AND n.fecha >= (CURRENT_DATE - %d)::timestamp AND n.fecha < (CURRENT_DATE - %d + 1)::timestamp",
|
||||
dayOffset, dayOffset)
|
||||
case today:
|
||||
timeCond = " AND n.fecha >= date_trunc('day', NOW())"
|
||||
case days > 0:
|
||||
timeCond = fmt.Sprintf(" AND n.fecha >= NOW() - make_interval(days => $%d)", next)
|
||||
params = append(params, days)
|
||||
next++
|
||||
}
|
||||
|
||||
base := fmt.Sprintf(`
|
||||
FROM tags_noticia tn
|
||||
JOIN tags t ON tn.tag_id = t.id
|
||||
JOIN traducciones tr ON tn.traduccion_id = tr.id
|
||||
JOIN noticias n ON tr.noticia_id = n.id
|
||||
LEFT JOIN entity_aliases ea ON LOWER(ea.alias) = LOWER(t.valor) AND ea.tipo = t.tipo
|
||||
WHERE LOWER(COALESCE(ea.canonical_name, t.valor)) = LOWER($1) AND t.tipo = $2%s`, timeCond)
|
||||
|
||||
var total int
|
||||
if err := db.GetPool().QueryRow(c.Request.Context(),
|
||||
fmt.Sprintf("SELECT COUNT(DISTINCT tn.noticia_id) %s", base), params...).Scan(&total); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to count entity news", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT DISTINCT n.id, n.titulo, COALESCE(n.resumen,''), n.url, n.fecha, n.imagen_url,
|
||||
n.fuente_nombre, tr.titulo_trad, tr.resumen_trad
|
||||
%s
|
||||
ORDER BY n.fecha DESC
|
||||
LIMIT $%d OFFSET $%d`, base, next, next+1)
|
||||
|
||||
rows, err := db.GetPool().Query(c.Request.Context(), query, append(params, perPage, offset)...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to get entity news", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var news []NewsResponse
|
||||
for rows.Next() {
|
||||
var n NewsResponse
|
||||
var img *string
|
||||
if err := rows.Scan(&n.ID, &n.Titulo, &n.Resumen, &n.URL, &n.Fecha, &img,
|
||||
&n.FuenteNombre, &n.TitleTranslated, &n.SummaryTranslated); err != nil {
|
||||
continue
|
||||
}
|
||||
if img != nil {
|
||||
n.ImagenURL = img
|
||||
}
|
||||
news = append(news, n)
|
||||
}
|
||||
if news == nil {
|
||||
news = []NewsResponse{}
|
||||
}
|
||||
|
||||
totalPages := (total + perPage - 1) / perPage
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"news": news,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
"total_pages": totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
func GetNewsByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,82 @@ package handlers
|
|||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rss2/backend/internal/auth"
|
||||
"github.com/rss2/backend/internal/db"
|
||||
"github.com/rss2/backend/internal/models"
|
||||
"github.com/rss2/backend/internal/services"
|
||||
)
|
||||
|
||||
// LogSearch records a logged-in user's search term so frequent searches gain priority.
|
||||
func LogSearch(c *gin.Context) {
|
||||
user := c.MustGet("user").(*auth.Claims)
|
||||
var req struct {
|
||||
Query string `json:"q"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "Invalid request"})
|
||||
return
|
||||
}
|
||||
term := strings.ToLower(strings.TrimSpace(req.Query))
|
||||
if term == "" {
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{Error: "q requerido"})
|
||||
return
|
||||
}
|
||||
if len(term) > 100 {
|
||||
term = term[:100]
|
||||
}
|
||||
|
||||
_, err := db.GetPool().Exec(c.Request.Context(), `
|
||||
INSERT INTO user_search_tags (user_id, term, count, last_used)
|
||||
VALUES ($1, $2, 1, NOW())
|
||||
ON CONFLICT (user_id, term)
|
||||
DO UPDATE SET count = user_search_tags.count + 1, last_used = NOW()
|
||||
`, user.UserID, term)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to save search", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// SearchSuggestions returns the user's most used search terms (dynamic tags).
|
||||
func SearchSuggestions(c *gin.Context) {
|
||||
user := c.MustGet("user").(*auth.Claims)
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
|
||||
query := `
|
||||
SELECT term FROM user_search_tags
|
||||
WHERE user_id = $1`
|
||||
args := []interface{}{user.UserID}
|
||||
if q != "" {
|
||||
query += ` AND term ILIKE $2`
|
||||
args = append(args, "%"+q+"%")
|
||||
}
|
||||
query += ` ORDER BY count DESC, last_used DESC LIMIT 10`
|
||||
|
||||
rows, err := db.GetPool().Query(c.Request.Context(), query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponse{Error: "Failed to get suggestions", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
terms := []string{}
|
||||
for rows.Next() {
|
||||
var t string
|
||||
if err := rows.Scan(&t); err != nil {
|
||||
continue
|
||||
}
|
||||
terms = append(terms, t)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"terms": terms})
|
||||
}
|
||||
|
||||
func SearchNews(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue