Documentation
¶
Overview ¶
Package settings persists app-level configuration (AI provider keys, charging cost, Rivian session) in the shared Postgres database.
Storage shape: one row per (user_id, namespace) in the user_settings table, with the value column holding a JSONB object of string keys. The public Store API keeps the flat key/value illusion the Manager, charging and rivian helpers were built against — the namespace is derived from the key's first segment ("ai.*" → "ai", "charging.*" → "charging", "rivian.*" → "rivian").
Index ¶
- Constants
- func SetChargingConfig(ctx context.Context, s *Store, cfg ChargingConfig) error
- func SetChargingNetworks(ctx context.Context, s *Store, networks []ChargingNetwork) error
- func SetHomeLocation(ctx context.Context, s *Store, h HomeLocation) error
- func SetPlannerPrefs(ctx context.Context, s *Store, p PlannerPrefs) error
- type AIConfig
- type AIImagePublic
- type AIPublic
- type AISpeechPublic
- type AIUpdate
- type ChargingConfig
- type ChargingNetwork
- type Factory
- type GPSPublic
- type GPSUpdate
- type HomeLocation
- type KV
- type Manager
- func (m *Manager) Analyzer() *ai.Analyzer
- func (m *Manager) GPSPublic() GPSPublic
- func (m *Manager) GeminiCreds() (apiKey, model string)
- func (m *Manager) ImageCreds() (provider, apiKey, model string)
- func (m *Manager) ListModels(ctx context.Context, provider string) ([]string, error)
- func (m *Manager) OpenAICreds() (apiKey, model string)
- func (m *Manager) Provider() ai.Provider
- func (m *Manager) Public() AIPublic
- func (m *Manager) RecapPublic() RecapPublic
- func (m *Manager) RecapWeatherEnabled() bool
- func (m *Manager) SpeechCreds() (provider, apiKey, model string)
- func (m *Manager) Update(ctx context.Context, patch AIUpdate) (AIPublic, error)
- func (m *Manager) UpdateGPS(ctx context.Context, patch GPSUpdate) (GPSPublic, error)
- func (m *Manager) UpdateRecap(ctx context.Context, patch RecapUpdate) (RecapPublic, error)
- func (m *Manager) VisionCreds() (provider, apiKey, model string)
- type PlannerPrefs
- type ProInfo
- type RecapPublic
- type RecapUpdate
- type Store
Constants ¶
const ( ProviderOpenAI = "openai" ProviderAnthropic = "anthropic" ProviderGemini = "gemini" )
Supported AI providers.
const ( DefaultGPSMissingPct float64 = 0.40 DefaultGPSStaleSec int = 300 DefaultGPSJumpCount int = 2 )
Defaults for the GPS accuracy heuristic. Tuned to surface drives with genuinely bad GPS (long tunnels, modem-side dropouts) while ignoring single anomalies and brief parking-garage stalls.
const ( DefaultOpenAIModel = "gpt-4o-mini" DefaultAnthropicModel = "claude-haiku-4-5-20251001" DefaultGeminiModel = "gemini-2.5-flash" DefaultOpenAIImageModel = "gpt-image-1" DefaultGeminiImageModel = "gemini-2.5-flash-image" // Speech-to-text defaults. Whisper still has a single public model; // Gemini uses whatever text model you point at /generateContent with // audio inline. gemini-2.5-flash is the cheapest multimodal tier. DefaultOpenAISpeechModel = "whisper-1" DefaultGeminiSpeechModel = "gemini-2.5-flash" )
Defaults used when nothing is stored and no env override is present.
const ( ImageProviderOpenAI = "openai" ImageProviderGemini = "gemini" )
Supported image generation providers. Anthropic isn't in this set because the Messages API has no image-generation surface.
const ( SpeechProviderOpenAI = "openai" SpeechProviderGemini = "gemini" )
Supported speech-to-text providers. Same caveat as image: Anthropic has no audio-input API yet.
const ( DriveModeEveryday = "EVERYDAY" DriveModeDistance = "DISTANCE" DriveModeSport = "SPORT" DriveModeWinter = "WINTER" DriveModeOffRoadAuto = "OFF_ROAD_AUTO" )
Valid driveMode values for Rivian's planTrip2 enum. Names cribbed from the home-assistant-rivian integration's DRIVE_MODE_MAP and confirmed against the live gateway. The mobile app's user-facing labels don't match the enum; mapping below.
EVERYDAY → "All-Purpose" (default) DISTANCE → "Conserve" (range-extension) SPORT → "Sport" WINTER → "Snow" OFF_ROAD_AUTO → "All-Terrain"
DISTANCE produces materially different plans (one fewer stop + shorter route on long trips) so the knob is load-bearing.
const DefaultHomeCurrency = "USD"
DefaultHomeCurrency is the ISO-4217 code used when the operator hasn't set anything. Picked to match the app's target audience; the UI exposes the field so it can be changed.
Variables ¶
This section is empty.
Functions ¶
func SetChargingConfig ¶
func SetChargingConfig(ctx context.Context, s *Store, cfg ChargingConfig) error
SetChargingConfig persists the provided cost settings. Negative prices are rejected by coercing to zero so the UI can't persist a nonsensical value.
func SetChargingNetworks ¶
func SetChargingNetworks(ctx context.Context, s *Store, networks []ChargingNetwork) error
SetChargingNetworks persists the provided price book, overwriting any prior value. Empty names and non-positive prices are dropped silently so a partially filled UI form can't corrupt the list.
func SetHomeLocation ¶
func SetHomeLocation(ctx context.Context, s *Store, h HomeLocation) error
SetHomeLocation persists a home location. Pass an empty HomeLocation (Set=false) to clear it.
func SetPlannerPrefs ¶
func SetPlannerPrefs(ctx context.Context, s *Store, p PlannerPrefs) error
SetPlannerPrefs persists the defaults. Empty DriveMode clears the stored value; nil HasAdapter clears the stored value.
Types ¶
type AIConfig ¶
type AIConfig struct {
Provider string // "", "openai", "anthropic", "gemini"
OpenAIModel string
OpenAIKey string
AnthropicModel string
AnthropicKey string
GeminiModel string
GeminiKey string
// Image generation is a separate pipeline from text analysis and so
// has its own provider + per-provider model overrides. The API keys
// are shared with the text providers above.
ImageProvider string // "", "openai", "gemini"
ImageOpenAIModel string
ImageGeminiModel string
// Speech-to-text pipeline (voice notes). Same shape as Image.
SpeechProvider string // "", "openai", "gemini"
SpeechOpenAIModel string
SpeechGeminiModel string
// RecapWeatherEnabled toggles the optional weather lookup that
// runs during trip recap generation. Off by default; the feature
// hits an external HTTP API (Open-Meteo) so it must be opt-in.
RecapWeatherEnabled bool
// GPS accuracy heuristic thresholds applied client-side on the
// drive detail page. See DefaultGPS* for the meaning of each.
GPSMissingPct float64
GPSStaleSec int
GPSJumpCount int
}
AIConfig is the raw persisted AI configuration. Keys are never returned from handlers; this struct is the internal representation.
type AIImagePublic ¶
type AIImagePublic struct {
Provider string `json:"provider"` // "", openai|gemini
Effective string `json:"effective,omitempty"` // what auto-selection picked
// Per-provider model overrides. Empty string means "use the default".
OpenAIModel string `json:"openai_model,omitempty"`
GeminiModel string `json:"gemini_model,omitempty"`
Ready bool `json:"ready"`
}
AIImagePublic is the subset surfaced to the Settings UI. Per-provider key presence is derived from the top-level Providers map, so we only repeat the model choices and the selected provider here.
type AIPublic ¶
type AIPublic struct {
Provider string `json:"provider"` // "", openai|anthropic|gemini
Effective string `json:"effective_provider,omitempty"`
Model string `json:"effective_model,omitempty"`
Providers map[string]ProInfo `json:"providers"`
Ready bool `json:"ready"`
// Image is the public view of the image-generation configuration.
Image AIImagePublic `json:"image"`
// Speech is the public view of the voice-transcription configuration.
Speech AISpeechPublic `json:"speech"`
}
AIPublic is the redacted view returned to the UI. Keys are reported only as "set" / "not set"; the actual secret never leaves the server.
type AISpeechPublic ¶
type AISpeechPublic struct {
Provider string `json:"provider"`
Effective string `json:"effective,omitempty"`
OpenAIModel string `json:"openai_model,omitempty"`
GeminiModel string `json:"gemini_model,omitempty"`
Ready bool `json:"ready"`
}
AISpeechPublic mirrors AIImagePublic for the speech-to-text pipeline.
type AIUpdate ¶
type AIUpdate struct {
Provider *string `json:"provider,omitempty"`
OpenAIModel *string `json:"openai_model,omitempty"`
OpenAIKey *string `json:"openai_api_key,omitempty"`
AnthropicModel *string `json:"anthropic_model,omitempty"`
AnthropicKey *string `json:"anthropic_api_key,omitempty"`
GeminiModel *string `json:"gemini_model,omitempty"`
GeminiKey *string `json:"gemini_api_key,omitempty"`
// Image generation settings.
ImageProvider *string `json:"image_provider,omitempty"`
ImageOpenAIModel *string `json:"image_openai_model,omitempty"`
ImageGeminiModel *string `json:"image_gemini_model,omitempty"`
// Speech-to-text settings.
SpeechProvider *string `json:"speech_provider,omitempty"`
SpeechOpenAIModel *string `json:"speech_openai_model,omitempty"`
SpeechGeminiModel *string `json:"speech_gemini_model,omitempty"`
}
AIUpdate is the JSON body accepted by PUT /api/settings/ai. A nil pointer means "leave this field alone"; an explicit empty string clears a key.
type ChargingConfig ¶
type ChargingConfig struct {
// HomePricePerKWh is the retail cost in HomeCurrency per kilowatt
// hour. Zero means "not configured"; in that case estimated cost
// is not computed.
HomePricePerKWh float64 `json:"home_price_per_kwh"`
// HomeCurrency is the ISO-4217 code displayed next to estimated
// cost. Defaults to USD.
HomeCurrency string `json:"home_currency"`
}
ChargingConfig is the user-configurable cost-of-energy settings applied locally to estimate the price of sessions Rivian reports as free (home AC, L2 on non-RAN chargers, etc.).
func GetChargingConfig ¶
func GetChargingConfig(ctx context.Context, s *Store) (ChargingConfig, error)
GetChargingConfig returns the stored home-charging cost settings, filling defaults where unset.
type ChargingNetwork ¶
type ChargingNetwork struct {
Name string `json:"name"`
PricePerKWh float64 `json:"price_per_kwh"`
Currency string `json:"currency"`
}
ChargingNetwork is one entry in the price book — a friendly name (e.g. "EVgo", "Electrify America") plus a default $/kWh rate the UI can apply with one click when pricing a fast-charge session.
We intentionally keep this flat: no per-tier pricing, no time-of- use rules. The goal is faster manual entry, not full automation.
func GetChargingNetworks ¶
func GetChargingNetworks(ctx context.Context, s *Store) ([]ChargingNetwork, error)
GetChargingNetworks returns the configured price book, or an empty list when nothing is set. A malformed blob is treated as empty rather than an error so a bad value can be overwritten from the UI.
type Factory ¶
type Factory struct {
// contains filtered or unexported fields
}
Factory hands out per-user *Store instances. The settings store is just (db, userID), so a per-user instance is essentially free — caching is purely so callers reuse the same pointer across requests and don't churn the heap on every handler hit.
type GPSPublic ¶
type GPSPublic struct {
MissingPct float64 `json:"missing_pct"`
StaleSec int `json:"stale_sec"`
JumpCount int `json:"jump_count"`
}
GPSPublic is the redacted view of the GPS accuracy thresholds. Surfaced through /api/config so every page that needs to apply them (drive detail, live panel) reads one source.
type GPSUpdate ¶
type GPSUpdate struct {
MissingPct *float64 `json:"missing_pct,omitempty"`
StaleSec *int `json:"stale_sec,omitempty"`
JumpCount *int `json:"jump_count,omitempty"`
}
GPSUpdate is the JSON body accepted by PUT /api/admin/settings/gps.
type HomeLocation ¶
type HomeLocation struct {
Set bool `json:"set"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Label string `json:"label,omitempty"`
}
HomeLocation is the user's "home" base — used by the trip planner to offer a one-click Origin / Destination preset, and (eventually) by the recap path to recognise round trips that end at home.
Set is the boolean the UI uses to decide whether to render the Home preset button at all. Latitude/Longitude are the resolved coords; Label is the human-readable name (city + state) so the UI doesn't need to reverse-geocode every time.
func GetHomeLocation ¶
func GetHomeLocation(ctx context.Context, s *Store) (HomeLocation, error)
GetHomeLocation reads the saved home location. Returns Set=false when no location has been configured (zero-value HomeLocation).
type KV ¶
type KV interface {
GetAll(ctx context.Context) (map[string]string, error)
Set(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
}
KV is the minimal key/value backing store the Manager needs. Both the per-user *Store in this package and the install-wide *appsettings.Store satisfy it. The Manager itself doesn't care which it talks to — but in practice Rivolt wires it to appsettings (AI keys are an install concern: the operator pays the LLM bill).
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the current AI configuration and the corresponding Analyzer. It is concurrency-safe; handlers call Analyzer() on every request.
func NewManager ¶
NewManager loads any persisted config and builds the initial Analyzer. envSeed is merged in for values that are not yet stored, so first-boot behaviour matches the previous env-only configuration.
func (*Manager) Analyzer ¶
Analyzer returns the currently configured analyzer, or nil if AI is disabled (no key for the selected provider).
func (*Manager) GeminiCreds ¶
GeminiCreds returns the currently stored Gemini API key and model. The caller must treat the key as a secret and never log it. Used by the profile-image generation endpoint which doesn't go through Analyzer.
func (*Manager) ImageCreds ¶
ImageCreds returns the provider, API key, and model to use for image generation. Provider is one of "openai" or "gemini", or the empty string when no image-capable provider has a key configured. Respects the user's explicit ImageProvider setting and falls back to auto-selection otherwise.
func (*Manager) ListModels ¶
ListModels fetches the catalogue of usable models for a provider, using the stored API key. The caller is expected to have already saved a key; we deliberately don't accept an ad-hoc key here so the UI has one place (PUT /api/settings/ai) that persists secrets.
func (*Manager) OpenAICreds ¶
OpenAICreds returns the stored OpenAI API key and text model. Used by non-Analyzer endpoints (e.g. Whisper transcription) that need OpenAI auth directly.
func (*Manager) Provider ¶
Provider returns the active ai.Provider, or nil when AI is disabled. Features outside of shot analysis (coach, comparator, ask, digest, profile-name) call this directly so they can wrap the provider in their own task-specific helper.
func (*Manager) RecapPublic ¶
func (m *Manager) RecapPublic() RecapPublic
RecapPublic returns the redacted recap settings DTO.
func (*Manager) RecapWeatherEnabled ¶
RecapWeatherEnabled returns the current value of the recap weather enrichment toggle. Cheap; safe to call per request.
func (*Manager) SpeechCreds ¶
SpeechCreds returns the provider, API key, and model to use for voice transcription. Same contract as ImageCreds: empty provider means "no speech-capable provider has a key configured".
func (*Manager) Update ¶
Update applies the patch, persists it, and rebuilds the analyzer. Returns the new public view (or an error if the patch is invalid).
func (*Manager) UpdateGPS ¶
UpdateGPS applies the patch and persists. Values are clamped to non-negative ranges so a malformed payload can't silently disable the heuristic by way of a negative threshold.
func (*Manager) UpdateRecap ¶
func (m *Manager) UpdateRecap(ctx context.Context, patch RecapUpdate) (RecapPublic, error)
UpdateRecap applies the patch and persists. Doesn't touch the AI analyzer because recap settings are orthogonal to provider config.
func (*Manager) VisionCreds ¶
VisionCreds returns the provider, API key and model to use for image UNDERSTANDING (vision input, e.g. scanning a coffee bag). This is distinct from ImageCreds, which generates images. Vision follows the main chat provider so the bag gets read by whichever LLM you've chosen to analyse your shots. Supported providers: "openai" and "gemini". Anthropic Claude supports vision but isn't wired here yet; if the user's chat provider is Anthropic we fall back to whichever of OpenAI/Gemini has a key. Returns empty provider when no vision- capable key is set.
type PlannerPrefs ¶
type PlannerPrefs struct {
// DriveMode is one of EVERYDAY / DISTANCE / SPORT / WINTER /
// OFF_ROAD_AUTO. Empty string means "no default — let Rivian
// pick" (the gateway's default is EVERYDAY, so empty and
// EVERYDAY produce the same plan).
DriveMode string `json:"drive_mode"`
// HasAdapter — does the vehicle have the Tesla NACS adapter?
// Pointer so absent/false/true are all distinguishable on the
// wire; default is unset.
HasAdapter *bool `json:"has_adapter,omitempty"`
}
PlannerPrefs are the user's saved trip-planner defaults. The SPA pre-fills the per-trip form from these so the user doesn't have to pick the same drive mode every time.
func GetPlannerPrefs ¶
func GetPlannerPrefs(ctx context.Context, s *Store) (PlannerPrefs, error)
GetPlannerPrefs returns the saved planner defaults. Returns the zero-value PlannerPrefs when nothing has been configured.
type RecapPublic ¶
type RecapPublic struct {
WeatherEnabled bool `json:"weather_enabled"`
}
RecapPublic is the redacted view of the install-wide recap config returned to the UI. Lives in its own DTO (and on its own endpoint) because the recap toggle isn't an AI configuration knob — it's a data-egress switch (Open-Meteo, see internal/weather) that happens to be consumed by an AI feature. Surfacing it under "AI" sent the wrong signal about what disabling AI does/doesn't hide.
type RecapUpdate ¶
type RecapUpdate struct {
WeatherEnabled *bool `json:"weather_enabled,omitempty"`
}
RecapUpdate is the JSON body accepted by PUT /api/settings/recap. Same nil-pointer-means-untouched contract as AIUpdate.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a thin key/value wrapper around a Postgres connection pool scoped to a single user.
func OpenStore ¶
OpenStore binds a pooled connection to a user_id. The pool is owned by the caller; Store does not close it.