Added a wallet indicator for Ostris Cloud

This commit is contained in:
Jaret Burkett
2026-05-28 11:09:51 -06:00
parent 3e86d81fc6
commit 30886b8f92
3 changed files with 116 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
export async function GET() {
const appUrl = process.env.OSTRIS_CLOUD_APP_URL;
const apiKey = process.env.OSTRIS_CLOUD_API_KEY;
if (!appUrl || !apiKey) {
return NextResponse.json({ enabled: false });
}
try {
const res = await fetch(`${appUrl}/api/machine/me`, {
headers: { Authorization: `Bearer ${apiKey}` },
cache: 'no-store',
});
if (!res.ok) {
return NextResponse.json({
enabled: true,
appUrl,
error: `Ostris Cloud responded with ${res.status}`,
});
}
const data = await res.json();
return NextResponse.json({
enabled: true,
appUrl,
balance: data.balance ?? null,
});
} catch (error) {
return NextResponse.json({
enabled: true,
appUrl,
error: `Failed to fetch Ostris Cloud balance: ${error instanceof Error ? error.message : String(error)}`,
});
}
}

View File

@@ -0,0 +1,76 @@
'use client';
import { useEffect, useState } from 'react';
import { Wallet } from 'lucide-react';
import { apiClient } from '@/utils/api';
interface BalanceResponse {
enabled: boolean;
appUrl?: string;
balance?: {
units: string;
usd: number;
} | null;
error?: string;
}
export default function OstrisCloudBalance() {
const [data, setData] = useState<BalanceResponse | null>(null);
useEffect(() => {
let cancelled = false;
const fetchBalance = async () => {
try {
const res = await apiClient.get<BalanceResponse>('/api/ostris_cloud');
if (!cancelled) setData(res.data);
} catch {
// ignore — keep last known value
}
};
fetchBalance();
const interval = setInterval(fetchBalance, 60_000);
return () => {
cancelled = true;
clearInterval(interval);
};
}, []);
if (!data || !data.enabled || !data.appUrl) return null;
const usd = data.balance?.usd;
const hasAmount = typeof usd === 'number';
const display = hasAmount
? usd.toLocaleString(undefined, {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: null;
return (
<a
href={`${data.appUrl}/billing`}
target="_blank"
rel="noreferrer"
className="block px-4 py-2 hover:bg-gray-800 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Wallet className="w-4 h-4 text-blue-400" />
<span className="text-[11px] uppercase tracking-wide text-gray-400">
<span className="font-extrabold">Ostris</span> <span className="font-thin">Cloud</span>
</span>
</div>
{display ? (
<span className="text-xs font-medium text-gray-100 tabular-nums">{display}</span>
) : (
<span className="inline-block h-3 w-10 rounded bg-gray-700 animate-pulse" aria-label="Loading balance" />
)}
</div>
</a>
);
}

View File

@@ -9,6 +9,7 @@ import { createGlobalState } from 'react-global-hooks';
import ThemeToggle from './ThemeToggle';
import ThemeLogo from './ThemeLogo';
import ActiveJobWidget from './ActiveJobWidget';
import OstrisCloudBalance from './OstrisCloudBalance';
export const mobileSidebarState = createGlobalState<boolean>(false);
@@ -61,6 +62,7 @@ const Sidebar = () => {
<X className="w-5 h-5" />
</button>
</div>
<OstrisCloudBalance />
<nav className="flex-1">
<ul className="px-2 py-4 space-y-2">
{navigation.map(item => (