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

105 lines
3.1 KiB
Go

package api
import (
"context"
"encoding/json"
"net/http"
"time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/ai"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/events"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/services"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/worker"
)
type Handler struct {
db *db.DB
auth *auth.AuthService
bus *events.EventBus
entryService *services.EntryService
feedService *services.FeedService
jobService *services.JobService
settingsService *services.SettingsService
jobEngine *worker.JobEngine
aiClient *ai.Client
broadcaster *SSEBroadcaster
loginLimiter *RateLimiter
}
func NewHandler(
database *db.DB,
authService *auth.AuthService,
bus *events.EventBus,
entrySvc *services.EntryService,
feedSvc *services.FeedService,
jobSvc *services.JobService,
settingsSvc *services.SettingsService,
jobEngine *worker.JobEngine,
) *Handler {
broadcaster := NewSSEBroadcaster()
limiter := NewRateLimiter(5, 1*time.Minute)
h := &Handler{
db: database,
auth: authService,
bus: bus,
entryService: entrySvc,
feedService: feedSvc,
jobService: jobSvc,
settingsService: settingsSvc,
jobEngine: jobEngine,
aiClient: ai.NewClient(45 * time.Second),
broadcaster: broadcaster,
loginLimiter: limiter,
}
// Wire EventBus to SSE Broadcaster
if bus != nil {
bus.Subscribe(events.EventJobProgress, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("job_progress", evt.Job)
return nil
})
bus.Subscribe(events.EventJobCompleted, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("job_completed", evt.Job)
return nil
})
bus.Subscribe(events.EventJobFailed, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("job_failed", evt.Job)
return nil
})
bus.Subscribe(events.EventPreferencesSync, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("preferences_updated", evt.DirectAttrs)
return nil
})
bus.Subscribe(events.EventPostCreated, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("post_created", evt.Entry)
return nil
})
bus.Subscribe(events.EventPostUpdated, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("post_updated", evt.Entry)
return nil
})
bus.Subscribe(events.EventVariantUpdated, func(ctx context.Context, evt events.Event) error {
broadcaster.Broadcast("variant_updated", map[string]interface{}{
"entry": evt.Entry,
"variant_key": evt.VariantKey,
})
return nil
})
}
return h
}
func (h *Handler) writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func (h *Handler) writeError(w http.ResponseWriter, status int, msg string) {
h.writeJSON(w, status, map[string]string{"error": msg})
}