Documentation
¶
Overview ¶
Package shots caches shot history from the Meticulous machine in a local SQLite database and serves it to the UI.
The machine exposes a single GET /api/v1/history endpoint that returns every shot, including the full per-sample time-series, in one blob. For the UI we want cheap list-many / read-one semantics. This package:
- Periodically fetches /api/v1/history and upserts each shot by id.
- Stores metadata in one row per shot plus a JSON blob for samples.
- Serves compact list + full-detail reads straight from SQLite.
Index ¶
- Variables
- type LiveShotInput
- type Shot
- type ShotListItem
- type ShotMetrics
- type ShotSibling
- type Status
- type Store
- func (s *Store) Close() error
- func (s *Store) DB() *sql.DB
- func (s *Store) GetAnalysis(ctx context.Context, shotID, model string) (json.RawMessage, error)
- func (s *Store) GetCoachSuggestion(ctx context.Context, shotID, model string) (json.RawMessage, error)
- func (s *Store) GetCompare(ctx context.Context, a, b, model string) (json.RawMessage, error)
- func (s *Store) GetLatestAnalysis(ctx context.Context, shotID string) (model string, analysis json.RawMessage, err error)
- func (s *Store) GetLatestCoachSuggestion(ctx context.Context, shotID string) (model string, suggestion json.RawMessage, err error)
- func (s *Store) GetLatestCompare(ctx context.Context, a, b string) (model string, compare json.RawMessage, err error)
- func (s *Store) GetShot(ctx context.Context, id string) (*Shot, error)
- func (s *Store) HideShot(ctx context.Context, id string) error
- func (s *Store) ListShotMetrics(ctx context.Context, limit, points int) (map[string]ShotMetrics, error)
- func (s *Store) ListShotSiblings(ctx context.Context, profileID, excludeID string, limit int) ([]ShotSibling, error)
- func (s *Store) ListShots(ctx context.Context, limit int) ([]ShotListItem, error)
- func (s *Store) ListSparklines(ctx context.Context, limit, points int) (map[string][]float64, error)
- func (s *Store) SaveAnalysis(ctx context.Context, shotID, model string, analysis json.RawMessage) error
- func (s *Store) SaveCoachSuggestion(ctx context.Context, shotID, model string, suggestion json.RawMessage) error
- func (s *Store) SaveCompare(ctx context.Context, a, b, model string, compare json.RawMessage) error
- func (s *Store) SaveLiveShot(ctx context.Context, in LiveShotInput) error
- func (s *Store) SetActiveBeanResolver(fn func(context.Context) (id, grind string, rpm *float64))
- func (s *Store) SetBean(ctx context.Context, id, beanID string) error
- func (s *Store) SetFeedback(ctx context.Context, id string, rating *int, note string) error
- func (s *Store) SetGrind(ctx context.Context, id, grind string, rpm *float64) error
- type Syncer
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("shot not found")
ErrNotFound is returned by GetShot when no row matches.
Functions ¶
This section is empty.
Types ¶
type LiveShotInput ¶
type LiveShotInput struct {
ID string
Time float64 // unix seconds (ms fraction ok)
Name string
ProfileID string
ProfileName string
Samples json.RawMessage
Profile json.RawMessage // may be nil; stored inside summary_json
}
LiveShotInput is a shot captured from the live WebSocket stream. The recorder in internal/live builds this from a sequence of status events. Samples must be a JSON array matching the /api/v1/history per-shot `data` shape so readers don't branch on source.
type Shot ¶
type Shot struct {
ShotListItem
DebugFile string `json:"debug_file,omitempty"`
Samples json.RawMessage `json:"samples"`
Profile json.RawMessage `json:"profile,omitempty"`
}
Shot is the full detail view including raw samples and the profile snapshot.
type ShotListItem ¶
type ShotListItem struct {
ID string `json:"id"`
Time float64 `json:"time"`
Name string `json:"name"`
File string `json:"file,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
ProfileName string `json:"profile_name,omitempty"`
SampleCount int `json:"sample_count"`
// User feedback. Rating is 1..5, or nil if unrated. Note is a
// free-form tasting note the user attached to the shot.
Rating *int `json:"rating,omitempty"`
Note string `json:"note,omitempty"`
// BeanID links this shot to a record in the beans table (empty when unset).
BeanID string `json:"bean_id,omitempty"`
// Grind is a free-form grinder setting label (e.g. "2.8" on a
// Niche, "12" clicks on a Kingrinder). RPM is only meaningful for
// variable-speed grinders (DF64, P100) and is nil otherwise.
Grind string `json:"grind,omitempty"`
GrindRPM *float64 `json:"grind_rpm,omitempty"`
}
ShotListItem is the compact list view (no sample data).
type ShotMetrics ¶
type ShotMetrics struct {
Spark []float64 `json:"spark,omitempty"`
PeakPressure float64 `json:"peak_pressure,omitempty"`
FinalWeight float64 `json:"final_weight,omitempty"`
}
ShotMetrics is the per-shot summary used to decorate list rows: a downsampled pressure trace (for the thumbnail) plus cheap-to-derive headline numbers. Everything here is computed from samples_json on demand, so we don't have to migrate the schema when we want a new metric.
type ShotSibling ¶
type ShotSibling struct {
ID string `json:"id"`
Name string `json:"name"`
TimeISO string `json:"time_iso"`
Duration float64 `json:"duration_s"`
PeakPressure float64 `json:"peak_pressure_bar"`
FinalWeight float64 `json:"final_weight_g"`
Rating *int `json:"rating,omitempty"`
Note string `json:"note,omitempty"`
}
ShotSibling is a compact per-shot row the coach / comparator use for historical context. Metrics come from samples_json on demand, so no schema migration is needed to add more fields later.
type Status ¶
type Status struct {
LastSync time.Time `json:"last_sync"`
LastError string `json:"last_error,omitempty"`
ShotsCached int `json:"shots_cached"`
MachineURL string `json:"machine_url"`
IntervalSecs float64 `json:"interval_secs"`
}
Status is the sync status surface used by the API.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps a SQLite database holding the cached shot history.
func (*Store) DB ¶
DB exposes the raw *sql.DB so sibling packages (ai usage recorder, beans store, …) can reuse the same SQLite file without opening a second connection and fighting over WAL locks.
func (*Store) GetAnalysis ¶
GetAnalysis returns the cached analysis JSON for (shotID, model), or ErrNotFound if none is cached.
func (*Store) GetCoachSuggestion ¶
func (s *Store) GetCoachSuggestion(ctx context.Context, shotID, model string) (json.RawMessage, error)
GetCoachSuggestion returns the cached suggestion for (shotID, model), or ErrNotFound.
func (*Store) GetCompare ¶
GetCompare returns the cached comparison for (a,b,model). a and b may be supplied in either order.
func (*Store) GetLatestAnalysis ¶
func (s *Store) GetLatestAnalysis(ctx context.Context, shotID string) (model string, analysis json.RawMessage, err error)
GetLatestAnalysis returns the most recently cached analysis for a shot regardless of model, plus the model it was generated with. Used by the read path so switching the configured model in Settings doesn't hide existing analyses from the user — they're still in the DB, we just weren't looking for them.
func (*Store) GetLatestCoachSuggestion ¶
func (s *Store) GetLatestCoachSuggestion(ctx context.Context, shotID string) (model string, suggestion json.RawMessage, err error)
GetLatestCoachSuggestion returns the most recently cached suggestion for a shot regardless of model.
func (*Store) GetLatestCompare ¶
func (s *Store) GetLatestCompare(ctx context.Context, a, b string) (model string, compare json.RawMessage, err error)
GetLatestCompare returns the most recent cached comparison for (a,b) regardless of model. a and b may be supplied in either order.
func (*Store) HideShot ¶
HideShot soft-deletes a shot: flips its hidden flag so it drops out of the list and sparkline views. We don't hard-delete because the next sync from the machine would just bring it back — hidden survives the upsert (the ON CONFLICT clause doesn't touch it).
func (*Store) ListShotMetrics ¶
func (s *Store) ListShotMetrics(ctx context.Context, limit, points int) (map[string]ShotMetrics, error)
ListShotMetrics returns a newest-first map of shot id -> ShotMetrics, one entry per shot up to limit. The sparkline trace is normalised to `points` values using nearest-neighbour bucketing. Shots with no samples are omitted entirely.
func (*Store) ListShotSiblings ¶
func (s *Store) ListShotSiblings(ctx context.Context, profileID, excludeID string, limit int) ([]ShotSibling, error)
ListShotSiblings returns up to `limit` recent shots with the given profile id (newest first), optionally excluding `excludeID`. Used by the profile-coach endpoint to pass historical context to the LLM.
func (*Store) ListSparklines ¶
func (s *Store) ListSparklines(ctx context.Context, limit, points int) (map[string][]float64, error)
ListSparklines returns a newest-first map of shot id -> downsampled pressure series, one entry per shot up to limit. The series is normalised to `points` values using nearest-neighbour bucketing; shots with no samples are omitted. Intended for the history list thumbnail.
func (*Store) SaveAnalysis ¶
func (s *Store) SaveAnalysis(ctx context.Context, shotID, model string, analysis json.RawMessage) error
SaveAnalysis upserts an analysis for (shotID, model).
func (*Store) SaveCoachSuggestion ¶
func (s *Store) SaveCoachSuggestion(ctx context.Context, shotID, model string, suggestion json.RawMessage) error
SaveCoachSuggestion upserts a suggestion for (shotID, model).
func (*Store) SaveCompare ¶
SaveCompare upserts a comparison for (a,b,model). a and b are canonicalised so save order doesn't matter.
func (*Store) SaveLiveShot ¶
func (s *Store) SaveLiveShot(ctx context.Context, in LiveShotInput) error
SaveLiveShot inserts a shot iff no row with the same id already exists. This keeps the canonical /history-synced row authoritative: if the history sync lands first (or later), it wins via ON CONFLICT in the full upsert path. We do NOT upsert here — the live stream lacks some metadata fields the machine provides in /history.
func (*Store) SetActiveBeanResolver ¶
SetActiveBeanResolver wires the "which bag is currently loaded" lookup. Called by main.go after the beans store is constructed. Safe to call more than once; last setter wins.
func (*Store) SetBean ¶
SetBean attaches (or clears) a bean id on a shot. Pass "" to clear. Like SetFeedback, this column is not overwritten by machine sync.
func (*Store) SetFeedback ¶
SetFeedback persists the user's rating (1..5, or nil to clear) and free-form note for a shot. Like `hidden`, these columns aren't touched by the sync upsert, so they survive future machine syncs.
type Syncer ¶
type Syncer struct {
// contains filtered or unexported fields
}
Syncer keeps the Store up to date by polling the machine.
func (*Syncer) Run ¶
Run performs an initial sync, then repeats every interval until ctx ends. Errors are logged but never terminate the loop.