From 30886b8f927dc76dfd9a4f79ad21711474d5d150 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Thu, 28 May 2026 11:09:51 -0600 Subject: [PATCH] Added a wallet indicator for Ostris Cloud --- ui/src/app/api/ostris_cloud/route.ts | 38 ++++++++++++ ui/src/components/OstrisCloudBalance.tsx | 76 ++++++++++++++++++++++++ ui/src/components/Sidebar.tsx | 2 + 3 files changed, 116 insertions(+) create mode 100644 ui/src/app/api/ostris_cloud/route.ts create mode 100644 ui/src/components/OstrisCloudBalance.tsx diff --git a/ui/src/app/api/ostris_cloud/route.ts b/ui/src/app/api/ostris_cloud/route.ts new file mode 100644 index 0000000..b883928 --- /dev/null +++ b/ui/src/app/api/ostris_cloud/route.ts @@ -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)}`, + }); + } +} diff --git a/ui/src/components/OstrisCloudBalance.tsx b/ui/src/components/OstrisCloudBalance.tsx new file mode 100644 index 0000000..90d2f52 --- /dev/null +++ b/ui/src/components/OstrisCloudBalance.tsx @@ -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(null); + + useEffect(() => { + let cancelled = false; + + const fetchBalance = async () => { + try { + const res = await apiClient.get('/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 ( + +
+
+ + + Ostris Cloud + +
+ {display ? ( + {display} + ) : ( + + )} +
+
+ ); +} diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 7f9e176..6882dfc 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -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(false); @@ -61,6 +62,7 @@ const Sidebar = () => { +