Added a wallet indicator for Ostris Cloud
This commit is contained in:
1 parent
3e86d81fc6
commit
30886b8f92
3 files changed
+116
No files matched your search
@@ -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)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ThemeToggle from './ThemeToggle';
|
||||||
import ThemeLogo from './ThemeLogo';
|
import ThemeLogo from './ThemeLogo';
|
||||||
import ActiveJobWidget from './ActiveJobWidget';
|
import ActiveJobWidget from './ActiveJobWidget';
|
||||||
|
import OstrisCloudBalance from './OstrisCloudBalance';
|
||||||
|
|
||||||
export const mobileSidebarState = createGlobalState<boolean>(false);
|
export const mobileSidebarState = createGlobalState<boolean>(false);
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ const Sidebar = () => {
|
|||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<OstrisCloudBalance />
|
||||||
<nav className="flex-1">
|
<nav className="flex-1">
|
||||||
<ul className="px-2 py-4 space-y-2">
|
<ul className="px-2 py-4 space-y-2">
|
||||||
{navigation.map(item => (
|
{navigation.map(item => (
|
||||||
|
|||||||
Reference in new issue
Block a user