refactor(core): overhaul state transition architecture, persistent job queue, and API layer
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

- 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.
This commit is contained in:
2026-09-01 00:41:05 -07:00
parent 271a542196
commit ebcaa5d72a
38 changed files with 3748 additions and 1202 deletions
+94 -1
View File
@@ -67,7 +67,7 @@ type FeedEntry struct {
PublishedAt time.Time `json:"published_at"`
IsRead bool `json:"is_read"`
IsStarred bool `json:"is_starred"`
ShareUUID *string `json:"share_uuid,omitempty"`
ShareUUID *string `json:"share_uuid"`
Variants map[string]EntryVariant `json:"variants,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
@@ -175,3 +175,96 @@ type ChangePasswordRequest struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
// Job represents a background task in the persistent job queue
type Job struct {
ID string `json:"id"`
Type string `json:"type"`
Payload map[string]interface{} `json:"payload"`
Status string `json:"status"` // 'pending', 'running', 'completed', 'failed', 'cancelled'
Progress int `json:"progress"`
Result map[string]interface{} `json:"result,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
RunAt time.Time `json:"run_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// JobFilter is used to filter job listings
type JobFilter struct {
Type string
Status string
Limit int
Offset int
}
// PatchEntryRequest is the body for PATCH /api/entries/{id}
type PatchEntryRequest struct {
IsRead *bool `json:"is_read,omitempty"`
IsStarred *bool `json:"is_starred,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Variants map[string]*EntryVariant `json:"variants,omitempty"`
}
// EntryDiff represents the delta between OldState and NewState of an Entry
type EntryDiff struct {
FieldsChanged []string `json:"fields_changed"` // e.g. ["is_read", "is_starred", "metadata.tags"]
VariantsAdded []string `json:"variants_added"` // e.g. ["readability_full_text"]
VariantsChanged []string `json:"variants_changed"` // e.g. ["ai_summary"]
VariantsRemoved []string `json:"variants_removed"`
}
// HasVariantAddition checks if a specific variant key was newly added in this transition
func (d EntryDiff) HasVariantAddition(variantKey string) bool {
for _, k := range d.VariantsAdded {
if k == variantKey {
return true
}
}
return false
}
// HasVariantChange checks if a specific variant key was modified in this transition
func (d EntryDiff) HasVariantChange(variantKey string) bool {
for _, k := range d.VariantsChanged {
if k == variantKey {
return true
}
}
return false
}
// HasFieldChange checks if a specific top-level or metadata field changed
func (d EntryDiff) HasFieldChange(fieldName string) bool {
for _, f := range d.FieldsChanged {
if f == fieldName {
return true
}
}
return false
}
// PromptTemplate represents an AI prompt template
type PromptTemplate struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Prompt string `json:"prompt"`
IsDefault bool `json:"is_default,omitempty"`
}
// AISettings represents AI provider configuration
type AISettings struct {
Enabled bool `json:"enabled"`
Provider string `json:"provider"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Model string `json:"model"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Prompts []PromptTemplate `json:"prompts"`
}