Files
jarvis-home/internal/store/store.go
T
jbergner 112c13f9f0
release-tag / release-image (push) Successful in 3m44s
init
2026-08-28 11:29:58 +02:00

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