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:
1 parent
285551fe8a
commit
6d73c89cc8
9 files changed
+289
-41
No files matched your search
@@ -51,7 +51,7 @@ const SECTIONS = computed(() => {
|
||||
const list = [
|
||||
{ id: 'account', label: 'Account', icon: 'user', kw: 'name username email password verification login credentials role' },
|
||||
{ id: 'appearance', label: 'Appearance', icon: 'sliders', kw: 'theme light dark system language region font size accessibility date time format motion' },
|
||||
{ id: 'integrations', label: 'Integrations', icon: 'radio', kw: 'opensky flights adsb aircraft plugin oauth credentials bounding box plan connection ftp sftp ftps file transfer server host upload download local storage folder drive isolated private read only webdav nextcloud owncloud dav url https openweather weather forecast temperature api key units' },
|
||||
{ id: 'integrations', label: 'Integrations', icon: 'radio', kw: 'opensky flights adsb aircraft plugin oauth credentials bounding box plan connection ftp sftp ftps file transfer server host upload download local storage folder drive isolated private read only webdav nextcloud owncloud dav url https openweather weather forecast temperature api key units calls per minute usage limit quota' },
|
||||
{ id: 'profile', label: 'Profile', icon: 'image', kw: 'avatar photo display name bio public' },
|
||||
{ id: 'security', label: 'Privacy & Security', icon: 'shield', kw: 'two factor authentication 2fa sessions devices logout security privacy' },
|
||||
]
|
||||
@@ -80,7 +80,7 @@ function sectionMatches(s) {
|
||||
const ROW_KW = {
|
||||
account: ['full name', 'username', 'email address verification verify', 'password change current new'],
|
||||
appearance: ['theme light dark system', 'language', 'region', 'font size accessibility', 'reduce motion', 'date format', 'time format clock'],
|
||||
integrations: ['opensky live flights', 'enable plugin', 'oauth client id secret', 'plan credits', 'bounding box', 'test connection', 'file transfer ftp sftp ftps', 'server host port username password', 'private key passphrase', 'base path directory', 'local storage folder drive', 'private isolated folder', 'read only access mode', 'webdav nextcloud owncloud dav', 'server url username password tls', 'base path directory folder', 'openweather weather forecast', 'api key units metric imperial', 'default latitude longitude language'],
|
||||
integrations: ['opensky live flights', 'enable plugin', 'oauth client id secret', 'plan credits', 'bounding box', 'test connection', 'file transfer ftp sftp ftps', 'server host port username password', 'private key passphrase', 'base path directory', 'local storage folder drive', 'private isolated folder', 'read only access mode', 'webdav nextcloud owncloud dav', 'server url username password tls', 'base path directory folder', 'openweather weather forecast', 'api key units metric imperial', 'default latitude longitude language', 'calls per minute limit usage quota', 'api call usage today rate limit'],
|
||||
profile: ['profile photo avatar', 'display name', 'bio about', 'show email public'],
|
||||
security: ['two factor authentication', 'active sessions devices', 'sign out'],
|
||||
team: ['add user create account', 'members list role admin remove delete', 'organization org assign'],
|
||||
@@ -731,7 +731,7 @@ const ow = reactive({
|
||||
})
|
||||
const owScope = ref('user')
|
||||
// All editable fields; the API key is prefilled with the mask and preserved on save.
|
||||
const OW_KEYS = ['apiKey', 'units', 'lat', 'lon', 'lang']
|
||||
const OW_KEYS = ['apiKey', 'units', 'lat', 'lon', 'lang', 'callsPerMinute']
|
||||
const owForm = reactive(Object.fromEntries(OW_KEYS.map((k) => [k, ''])))
|
||||
const owMsg = ref('')
|
||||
const owSaving = ref(false)
|
||||
@@ -861,6 +861,24 @@ function owHealthClass(status) {
|
||||
return status === 'ok' ? badgeClass.success : status === 'degraded' ? badgeClass.warning : badgeClass.danger
|
||||
}
|
||||
|
||||
// App-side call usage from the last probe (null until one runs). OpenWeather does
|
||||
// not report remaining quota, so these are the calls PilotVault itself has made.
|
||||
const owUsage = computed(() => (owHealth.value && owHealth.value.usage) || null)
|
||||
// Fraction of the per-minute limit consumed (null when no limit is known).
|
||||
const owUsagePct = computed(() => {
|
||||
const u = owUsage.value
|
||||
if (!u || !u.minuteLimit) return null
|
||||
return Math.max(0, Math.min(100, Math.round((u.minuteUsed / u.minuteLimit) * 100)))
|
||||
})
|
||||
// Bar colour tracks how close to the per-minute limit we are (fills as usage grows).
|
||||
const owUsageBarClass = computed(() => {
|
||||
const p = owUsagePct.value
|
||||
if (p == null) return 'bg-accent'
|
||||
if (p >= 90) return 'bg-danger'
|
||||
if (p >= 70) return 'bg-amber'
|
||||
return 'bg-success'
|
||||
})
|
||||
|
||||
/* ---------- integrations: Local storage (host filesystem) ---------- */
|
||||
// Same three-layer cascade as OpenSky (global → org → user), but the folder is not
|
||||
// editable: it is derived from identity so each org-less user gets a private folder
|
||||
@@ -1872,6 +1890,43 @@ onBeforeUnmount(() => {
|
||||
<input v-else v-model="owForm.lang" class="field w-24 font-mono" placeholder="en" />
|
||||
</Row>
|
||||
|
||||
<!-- calls-per-minute limit (used only to gauge usage) -->
|
||||
<Row title="Calls per minute limit" desc="Your plan's per-minute limit (free tier is 60). Only used to gauge app usage below." keywords="calls per minute limit rate quota plan usage">
|
||||
<template v-if="owLocked('callsPerMinute')">
|
||||
<span class="inline-flex items-center gap-2 font-mono text-sm text-ink">
|
||||
{{ owField('callsPerMinute').effective || '60' }}
|
||||
<span v-if="owSourceLabel('callsPerMinute')" class="inline-flex items-center gap-1 rounded-full bg-surface-2 px-2 py-0.5 text-[10px] font-semibold text-ink-secondary"><Icon name="lock" :size="10" />{{ owSourceLabel('callsPerMinute') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<input v-else v-model="owForm.callsPerMinute" inputmode="numeric" class="field w-24 font-mono" placeholder="60" />
|
||||
</Row>
|
||||
|
||||
<!-- app-side call usage gauge (OpenWeather does not report remaining quota) -->
|
||||
<div v-if="ow.available && !owEditingOrg" class="my-4 rounded-lg border border-line bg-surface-2 p-4" data-keywords="usage quota rate limit calls per minute remaining left api">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-ink">
|
||||
<Icon name="signal" :size="15" />API call usage
|
||||
</div>
|
||||
<span v-if="owHealthTs" class="text-[11px] text-ink-muted">Checked {{ owHealthAgo() }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="owUsage">
|
||||
<div class="mt-2 flex items-baseline gap-1.5">
|
||||
<span class="font-mono text-2xl font-semibold text-ink">{{ owUsage.minuteUsed }}</span>
|
||||
<span class="text-sm text-ink-muted">/ {{ owUsage.minuteLimit || '—' }} calls this minute</span>
|
||||
</div>
|
||||
<div v-if="owUsagePct != null" class="mt-2 h-2 w-full overflow-hidden rounded-full bg-line">
|
||||
<div class="h-full rounded-full transition-all" :class="owUsageBarClass" :style="{ width: owUsagePct + '%' }"></div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-ink-muted">
|
||||
{{ owUsage.dayUsed }} calls today · counts only requests PilotVault makes with this key, since server start. OpenWeather does not report remaining quota — check your account dashboard for the authoritative total.
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="mt-2 text-xs text-ink-muted">
|
||||
Run <span class="font-semibold text-ink-secondary">Test connection</span> below to record and show call usage.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
<button v-if="!owReadOnly" class="btn-accent" :disabled="owSaving || !ow.available" @click="saveOpenWeatherSettings">
|
||||
|
||||
Reference in new issue
Block a user