Portal: Phoenix API Railing wiring, env example, per-tenant rate limit

- Portal: phoenix-api-client, usePhoenixRailing hooks, /infrastructure page
- Portal: PhoenixHealthTile on dashboard, resources page uses tenant me/resources
- Sidebar: Infrastructure link; Keycloak token used for API calls (BFF)
- api/.env.example: PHOENIX_RAILING_URL, PHOENIX_RAILING_API_KEY
- rate-limit: key by tenant when tenantContext present

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-11 13:00:46 -07:00
parent 8436e22f4c
commit e123f407d3
9 changed files with 369 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
import { PhoenixHealthTile } from './dashboard/PhoenixHealthTile';
import { Badge } from './ui/badge';
import { gql } from '@apollo/client';
import { useQuery as useApolloQuery } from '@apollo/client';
@@ -132,6 +133,10 @@ export default function Dashboard() {
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<PhoenixHealthTile />
</div>
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>

View File

@@ -0,0 +1,81 @@
'use client';
import { usePhoenixHealthSummary } from '@/hooks/usePhoenixRailing';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Activity, CheckCircle, AlertCircle } from 'lucide-react';
export function PhoenixHealthTile() {
const { data, isLoading, error } = usePhoenixHealthSummary();
if (isLoading) {
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
Phoenix Health
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-gray-400 py-4">Loading...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
Phoenix Health
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-red-400 py-4">Error loading health (check PHOENIX_RAILING_URL)</div>
</CardContent>
</Card>
);
}
const status = data?.status ?? 'unknown';
const hosts = data?.hosts ?? [];
const alerts = data?.alerts ?? [];
return (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-white flex items-center gap-2">
<Activity className="h-5 w-5 text-orange-500" />
Phoenix Health (Railing)
</CardTitle>
<span
className={`px-3 py-1 rounded-full text-xs font-semibold ${
status === 'healthy' ? 'bg-green-500/20 text-green-400' :
status === 'degraded' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-red-500/20 text-red-400'
}`}
>
{status}
</span>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-400" />
<span>Hosts: {hosts.length}</span>
</div>
{alerts.length > 0 && (
<div className="flex items-center gap-2 text-yellow-400">
<AlertCircle className="h-4 w-4" />
<span>Alerts: {alerts.length}</span>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -18,6 +18,7 @@ import { cn } from '@/lib/utils'
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Infrastructure', href: '/infrastructure', icon: Server },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },