257 lines
6.8 KiB
Go
257 lines
6.8 KiB
Go
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Due string `json:"due,omitempty"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
Source string `json:"source,omitempty"`
|
|
Confidence float64 `json:"confidence,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type CalendarEvent struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Start string `json:"start"`
|
|
End string `json:"end,omitempty"`
|
|
AllDay bool `json:"allDay"`
|
|
Location string `json:"location,omitempty"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type Reminder struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
At string `json:"at"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Done bool `json:"done"`
|
|
LinkedType string `json:"linkedType,omitempty"`
|
|
LinkedID string `json:"linkedId,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type GroceryItem struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Quantity string `json:"quantity,omitempty"`
|
|
Unit string `json:"unit,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
Checked bool `json:"checked"`
|
|
Source string `json:"source,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type Ingredient struct {
|
|
Name string `json:"name"`
|
|
Quantity string `json:"quantity,omitempty"`
|
|
Unit string `json:"unit,omitempty"`
|
|
}
|
|
|
|
type Recipe struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
Servings int `json:"servings,omitempty"`
|
|
PrepMinutes int `json:"prepMinutes,omitempty"`
|
|
Ingredients []Ingredient `json:"ingredients"`
|
|
Steps []string `json:"steps"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type MealPlan struct {
|
|
ID string `json:"id"`
|
|
Date string `json:"date"`
|
|
Meal string `json:"meal"`
|
|
RecipeID string `json:"recipeId,omitempty"`
|
|
Title string `json:"title"`
|
|
Servings int `json:"servings,omitempty"`
|
|
Notes string `json:"notes,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
Source string `json:"source,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type KanbanCard struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Notes string `json:"notes,omitempty"`
|
|
Column string `json:"column"`
|
|
Priority string `json:"priority"`
|
|
Due string `json:"due,omitempty"`
|
|
TaskID string `json:"taskId,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
AIManaged bool `json:"aiManaged"`
|
|
Confidence float64 `json:"confidence,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
type Document struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Mime string `json:"mime,omitempty"`
|
|
Text string `json:"text"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
ChunkCount int `json:"chunkCount"`
|
|
IndexedAt string `json:"indexedAt,omitempty"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type ChatTurn struct {
|
|
ID string `json:"id"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type Activity struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Message string `json:"message"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type State struct {
|
|
Tasks []Task `json:"tasks"`
|
|
Events []CalendarEvent `json:"events"`
|
|
Reminders []Reminder `json:"reminders"`
|
|
Groceries []GroceryItem `json:"groceries"`
|
|
Recipes []Recipe `json:"recipes"`
|
|
MealPlans []MealPlan `json:"mealPlans"`
|
|
Kanban []KanbanCard `json:"kanban"`
|
|
Documents []Document `json:"documents"`
|
|
Chat []ChatTurn `json:"chat"`
|
|
Activity []Activity `json:"activity"`
|
|
}
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
data State
|
|
}
|
|
|
|
func New(path string) (*Store, error) {
|
|
s := &Store{path: path}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
s.data = emptyState()
|
|
if err := s.saveLocked(); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(b) > 0 {
|
|
if err := json.Unmarshal(b, &s.data); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
normalize(&s.data)
|
|
return s, nil
|
|
}
|
|
|
|
func emptyState() State {
|
|
return State{Tasks: []Task{}, Events: []CalendarEvent{}, Reminders: []Reminder{}, Groceries: []GroceryItem{}, Recipes: []Recipe{}, MealPlans: []MealPlan{}, Kanban: []KanbanCard{}, Documents: []Document{}, Chat: []ChatTurn{}, Activity: []Activity{}}
|
|
}
|
|
|
|
func normalize(s *State) {
|
|
if s.Tasks == nil {
|
|
s.Tasks = []Task{}
|
|
}
|
|
if s.Events == nil {
|
|
s.Events = []CalendarEvent{}
|
|
}
|
|
if s.Reminders == nil {
|
|
s.Reminders = []Reminder{}
|
|
}
|
|
if s.Groceries == nil {
|
|
s.Groceries = []GroceryItem{}
|
|
}
|
|
if s.Recipes == nil {
|
|
s.Recipes = []Recipe{}
|
|
}
|
|
if s.MealPlans == nil {
|
|
s.MealPlans = []MealPlan{}
|
|
}
|
|
if s.Kanban == nil {
|
|
s.Kanban = []KanbanCard{}
|
|
}
|
|
if s.Documents == nil {
|
|
s.Documents = []Document{}
|
|
}
|
|
if s.Chat == nil {
|
|
s.Chat = []ChatTurn{}
|
|
}
|
|
if s.Activity == nil {
|
|
s.Activity = []Activity{}
|
|
}
|
|
}
|
|
|
|
func (s *Store) Snapshot() State {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
b, _ := json.Marshal(s.data)
|
|
var out State
|
|
_ = json.Unmarshal(b, &out)
|
|
normalize(&out)
|
|
return out
|
|
}
|
|
|
|
func (s *Store) Update(fn func(*State) error) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if err := fn(&s.data); err != nil {
|
|
return err
|
|
}
|
|
normalize(&s.data)
|
|
return s.saveLocked()
|
|
}
|
|
|
|
func (s *Store) saveLocked() error {
|
|
b, err := json.MarshalIndent(s.data, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := s.path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, s.path)
|
|
}
|
|
|
|
func Now() string { return time.Now().Format(time.RFC3339) }
|