Files
haopengzhan ebcaa5d72a
CI Quality Checks & Tests / Gitleaks Secret Detection (push) Failing after 1m59s
CI Quality Checks & Tests / Go Lint & Unit Tests (push) Failing after 2m26s
CI Quality Checks & Tests / Frontend Typecheck & Build (push) Successful in 2m37s
refactor(core): overhaul state transition architecture, persistent job queue, and API layer
- 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.
2026-09-01 00:41:05 -07:00

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),
})
}