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.
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user