feat: massive overhaul to manage costs (#103)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

### Explanation

_No response_

### Issue

Closes #102

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #103
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #103.
This commit is contained in:
2026-02-04 19:58:43 -08:00
committed by Naomi Carrigan
parent daedbfd865
commit 1c45507cdf
30 changed files with 4024 additions and 103 deletions
+182
View File
@@ -0,0 +1,182 @@
import { writable, derived } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import { notificationManager } from "$lib/notifications/notificationManager";
// Types matching Rust backend
export interface DailyCost {
date: string;
input_tokens: number;
output_tokens: number;
cost_usd: number;
messages_sent: number;
sessions_count: number;
}
export interface CostSummary {
period_days: number;
total_input_tokens: number;
total_output_tokens: number;
total_cost: number;
total_messages: number;
total_sessions: number;
average_daily_cost: number;
daily_breakdown: DailyCost[];
}
export type AlertType = "Daily" | "Weekly" | "Monthly";
export interface CostAlert {
alert_type: AlertType;
threshold: number;
current_cost: number;
}
export interface CostAlertThresholds {
daily: number | null;
weekly: number | null;
monthly: number | null;
}
// Store state
interface CostTrackingState {
todayCost: number;
weekCost: number;
monthCost: number;
summary: CostSummary | null;
alerts: CostAlert[];
thresholds: CostAlertThresholds;
isLoading: boolean;
lastUpdated: Date | null;
}
const defaultState: CostTrackingState = {
todayCost: 0,
weekCost: 0,
monthCost: 0,
summary: null,
alerts: [],
thresholds: { daily: null, weekly: null, monthly: null },
isLoading: false,
lastUpdated: null,
};
function createCostTrackingStore() {
const { subscribe, set, update } = writable<CostTrackingState>(defaultState);
return {
subscribe,
async refresh() {
update((s) => ({ ...s, isLoading: true }));
try {
const [todayCost, weekCost, monthCost, alerts] = await Promise.all([
invoke<number>("get_today_cost"),
invoke<number>("get_week_cost"),
invoke<number>("get_month_cost"),
invoke<CostAlert[]>("get_cost_alerts"),
]);
update((s) => ({
...s,
todayCost,
weekCost,
monthCost,
alerts,
isLoading: false,
lastUpdated: new Date(),
}));
// Trigger notifications for any new alerts
if (alerts.length > 0) {
for (const alert of alerts) {
const message = getAlertMessage(alert);
notificationManager.notifyCostAlert(message);
}
}
return alerts;
} catch (error) {
console.error("Failed to refresh cost tracking:", error);
update((s) => ({ ...s, isLoading: false }));
return [];
}
},
async getSummary(days: number): Promise<CostSummary | null> {
try {
const summary = await invoke<CostSummary>("get_cost_summary", { days });
update((s) => ({ ...s, summary }));
return summary;
} catch (error) {
console.error("Failed to get cost summary:", error);
return null;
}
},
async setAlertThresholds(thresholds: CostAlertThresholds) {
try {
await invoke("set_cost_alert_thresholds", {
daily: thresholds.daily,
weekly: thresholds.weekly,
monthly: thresholds.monthly,
});
update((s) => ({ ...s, thresholds }));
} catch (error) {
console.error("Failed to set alert thresholds:", error);
}
},
async exportCsv(days: number): Promise<string | null> {
try {
return await invoke<string>("export_cost_csv", { days });
} catch (error) {
console.error("Failed to export CSV:", error);
return null;
}
},
reset() {
set(defaultState);
},
};
}
export const costTrackingStore = createCostTrackingStore();
// Derived stores for formatted values
export const formattedCosts = derived(costTrackingStore, ($store) => ({
today: formatCost($store.todayCost),
week: formatCost($store.weekCost),
month: formatCost($store.monthCost),
todayRaw: $store.todayCost,
weekRaw: $store.weekCost,
monthRaw: $store.monthCost,
}));
// Helper functions
export function formatCost(cost: number): string {
if (cost < 0.01) {
return `$${cost.toFixed(4)}`;
}
if (cost < 1) {
return `$${cost.toFixed(3)}`;
}
return `$${cost.toFixed(2)}`;
}
export function formatAlertType(type: AlertType): string {
switch (type) {
case "Daily":
return "Today";
case "Weekly":
return "This Week";
case "Monthly":
return "This Month";
}
}
export function getAlertMessage(alert: CostAlert): string {
const period = formatAlertType(alert.alert_type);
return `${period}'s spending (${formatCost(alert.current_cost)}) has exceeded your ${formatCost(alert.threshold)} threshold`;
}