- Implement atomic state transitions: add EntryService.PatchEntry() with EntryDiff computation and EventEntryTransition event emission. - Add persistent SQLite task queue (JobEngine): add jobs table with crash recovery, exponential backoff retries, concurrency control, and idempotency key deduplication. - Decompose monolithic HTTP handlers: split handlers.go into domain handlers (auth, feeds, entries, jobs, settings, rules, sse). - Implement real-time Server-Sent Events (/api/events/stream) for multi-tab/device state synchronization. - Fix SQLite concurrency: apply busy_timeout(10000) and journal_mode(WAL) via DSN query parameters to resolve SQLITE_BUSY (5). - Fix AI settings & prompt loading: unmarshal JSON from ai_settings, apply configured prompt templates, and enforce strict enabled/API key validations. - Fix undo share state: remove omitempty from ShareUUID to serialize null for state resets. - Fix middle list counter: compute true unread/total stats from DB and clean switcher button labels. - Adapt mobile UI for iOS: add viewport-fit=cover in index.html and calibrate safe-area insets for the bottom navigation and article reader toolbar. - Add architectural documentation and worklog in docs/009_worklog.md.
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/events"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/rules"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Rules API Handlers
|
|
func (h *Handler) ListRules(w http.ResponseWriter, r *http.Request) {
|
|
rulesList, err := h.db.ListRules()
|
|
if err != nil {
|
|
h.writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if rulesList == nil {
|
|
rulesList = []models.Rule{}
|
|
}
|
|
h.writeJSON(w, http.StatusOK, rulesList)
|
|
}
|
|
|
|
func (h *Handler) CreateRule(w http.ResponseWriter, r *http.Request) {
|
|
var body models.Rule
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, "invalid rule payload")
|
|
return
|
|
}
|
|
|
|
body.Name = strings.TrimSpace(body.Name)
|
|
if body.Name == "" {
|
|
h.writeError(w, http.StatusBadRequest, "rule name is required")
|
|
return
|
|
}
|
|
if body.Trigger == "" {
|
|
body.Trigger = "on_update"
|
|
}
|
|
if body.MatchMode == "" {
|
|
body.MatchMode = "all"
|
|
}
|
|
|
|
if err := rules.ValidateRuleSafety(body); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
rule, err := h.db.CreateRule(body.Name, body.Trigger, body.MatchMode, body.Conditions, body.Actions)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
h.writeJSON(w, http.StatusCreated, rule)
|
|
}
|
|
|
|
func (h *Handler) UpdateRule(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
var body models.Rule
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, "invalid rule payload")
|
|
return
|
|
}
|
|
|
|
body.Name = strings.TrimSpace(body.Name)
|
|
if body.Name == "" {
|
|
h.writeError(w, http.StatusBadRequest, "rule name is required")
|
|
return
|
|
}
|
|
|
|
if err := rules.ValidateRuleSafety(body); err != nil {
|
|
h.writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
rule, err := h.db.UpdateRule(id, body.Name, body.Trigger, body.MatchMode, body.Conditions, body.Actions, body.IsActive)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
h.writeJSON(w, http.StatusOK, rule)
|
|
}
|
|
|
|
func (h *Handler) DeleteRule(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if err := h.db.DeleteRule(id); err != nil {
|
|
h.writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
h.writeJSON(w, http.StatusOK, map[string]string{"message": "rule deleted"})
|
|
}
|
|
|
|
func (h *Handler) ApplyRule(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
rule, err := h.db.GetRule(id)
|
|
if err != nil {
|
|
h.writeError(w, http.StatusNotFound, "rule not found")
|
|
return
|
|
}
|
|
|
|
entries, _, err := h.db.ListEntries(db.EntryFilter{Limit: 200})
|
|
if err != nil {
|
|
h.writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
eval := rules.NewEvaluator()
|
|
matchedCount := 0
|
|
|
|
for _, entry := range entries {
|
|
if eval.Evaluate(*rule, entry) {
|
|
matchedCount++
|
|
if h.bus != nil {
|
|
h.bus.Publish(r.Context(), events.Event{
|
|
Type: events.EventManualTrigger,
|
|
Entry: entry,
|
|
Depth: 0,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
h.writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"message": "rule applied to matching articles",
|
|
"matched_count": matchedCount,
|
|
"queued_jobs": matchedCount * len(rule.Actions),
|
|
})
|
|
}
|