Added a wallet indicator for Ostris Cloud
This commit is contained in:
38
ui/src/app/api/ostris_cloud/route.ts
Normal file
38
ui/src/app/api/ostris_cloud/route.ts
Normal 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)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
76
ui/src/components/OstrisCloudBalance.tsx
Normal file
76
ui/src/components/OstrisCloudBalance.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 => (
|
||||
|
||||
Reference in New Issue
Block a user