Show OpenWeather API call usage in the app

OpenWeather reports no remaining quota (only 429 when over), so add an
in-app gauge that counts the calls PilotVault itself makes and shows them
against the plan's per-minute limit.

- Plugin: process-wide usage store bucketed per API key (hashed) into the
  current minute and UTC day; a do() wrapper counts each request that
  reaches OpenWeather (transport errors consume no quota, so uncounted).
  Health and Invoke both route through it.
- New callsPerMinute config field (default 60) that cascades global -> org
  -> user; plugin contract gains plugins.HealthUsage on Health.
- Web App: a "Calls per minute limit" field and an "API call usage" meter
  in the OpenWeather card, shown after Test connection and cached.
- Counts reset on restart and cover only PilotVault's own calls (noted in
  the UI); the account dashboard stays authoritative.
- Tests for per-key counting and minute/day rollover; rebuilt frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-14 11:49:19 +02:00
co-authored by Claude Opus 4.8
parent 285551fe8a
commit 6d73c89cc8
9 changed files with 289 additions and 41 deletions
@@ -29,7 +29,7 @@ const openWeatherPlugin = "openweather"
// owFields are the plugin's ConfigField keys, in display order. Kept in sync with
// the plugin descriptor so the cascade covers every setting.
var owFields = []string{"apiKey", "units", "lat", "lon", "lang"}
var owFields = []string{"apiKey", "units", "lat", "lon", "lang", "callsPerMinute"}
// owSecretKeys are masked in every view and preserved on save when left at the mask.
var owSecretKeys = map[string]bool{"apiKey": true}
@@ -37,11 +37,12 @@ var owSecretKeys = map[string]bool{"apiKey": true}
// owConfig is one layer's openweather settings. Values are strings to match the
// plugin's ConfigField keys 1:1 (they are handed straight to plugins.Init).
type owConfig struct {
APIKey string `json:"apiKey"`
Units string `json:"units"`
Lat string `json:"lat"`
Lon string `json:"lon"`
Lang string `json:"lang"`
APIKey string `json:"apiKey"`
Units string `json:"units"`
Lat string `json:"lat"`
Lon string `json:"lon"`
Lang string `json:"lang"`
CallsPerMinute string `json:"callsPerMinute"`
}
// owStored is what we persist per user/org under pluginSettings.openweather.
@@ -74,7 +75,7 @@ type owResolution struct {
// owConfigFromMap builds an owConfig from a flat string map (global plugin config).
func owConfigFromMap(m map[string]string) owConfig {
return owConfig{APIKey: m["apiKey"], Units: m["units"], Lat: m["lat"], Lon: m["lon"], Lang: m["lang"]}
return owConfig{APIKey: m["apiKey"], Units: m["units"], Lat: m["lat"], Lon: m["lon"], Lang: m["lang"], CallsPerMinute: m["callsPerMinute"]}
}
// owGet returns a config field by the plugin's key name.
@@ -90,6 +91,8 @@ func owGet(c owConfig, key string) string {
return c.Lon
case "lang":
return c.Lang
case "callsPerMinute":
return c.CallsPerMinute
}
return ""
}
@@ -107,6 +110,8 @@ func owSet(c *owConfig, key, v string) {
c.Lon = v
case "lang":
c.Lang = v
case "callsPerMinute":
c.CallsPerMinute = v
}
}