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

@@ -0,0 +1,66 @@
'use client';
import { usePhoenixInfraNodes, usePhoenixInfraStorage } from '@/hooks/usePhoenixRailing';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Server, HardDrive } from 'lucide-react';
export default function InfrastructurePage() {
const { data: nodesData, isLoading: nodesLoading, error: nodesError } = usePhoenixInfraNodes();
const { data: storageData, isLoading: storageLoading, error: storageError } = usePhoenixInfraStorage();
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Infrastructure</h1>
<p className="text-muted-foreground mb-6">
Cluster nodes and storage from Phoenix API Railing (GET /api/v1/infra/nodes, /api/v1/infra/storage).
</p>
<div className="grid gap-6 md:grid-cols-2">
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Server className="h-5 w-5" />
Cluster Nodes
</CardTitle>
</CardHeader>
<CardContent>
{nodesLoading && <p className="text-gray-400">Loading...</p>}
{nodesError && <p className="text-red-400">Error loading nodes</p>}
{nodesData?.nodes && (
<ul className="space-y-2">
{nodesData.nodes.map((n: any) => (
<li key={n.node || n.name} className="flex justify-between text-sm">
<span>{n.node ?? n.name ?? n.id}</span>
<span className={n.status === 'online' ? 'text-green-400' : 'text-gray-400'}>{n.status ?? '—'}</span>
</li>
))}
</ul>
)}
{nodesData?.stub && <p className="text-xs text-gray-500 mt-2">Stub data (set PROXMOX_* on railing)</p>}
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<HardDrive className="h-5 w-5" />
Storage
</CardTitle>
</CardHeader>
<CardContent>
{storageLoading && <p className="text-gray-400">Loading...</p>}
{storageError && <p className="text-red-400">Error loading storage</p>}
{storageData?.storage && (
<ul className="space-y-2">
{storageData.storage.slice(0, 10).map((s: any, i: number) => (
<li key={s.storage || i} className="text-sm">{s.storage ?? s.name ?? s.id}</li>
))}
</ul>
)}
{storageData?.stub && <p className="text-xs text-gray-500 mt-2">Stub data (set PROXMOX_* on railing)</p>}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,10 +1,12 @@
'use client'
import { useState } from 'react'
import { useSession } from 'next-auth/react'
import { useTenantResources } from '@/hooks/usePhoenixRailing'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
export default function ResourcesPage() {
const { data: session, status } = useSession()
const { data: tenantData, isLoading, error } = useTenantResources()
if (status === 'loading') {
return <div>Loading...</div>
@@ -14,16 +16,37 @@ export default function ResourcesPage() {
return <div>Please sign in</div>
}
const resources = tenantData?.resources ?? []
return (
<div className="container mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Resource Inventory</h1>
<p className="text-muted-foreground mb-4">
Unified view of all resources across Proxmox, Kubernetes, and Cloudflare
Tenant-scoped resources from Phoenix API (GET /api/v1/tenants/me/resources)
</p>
{/* Resource inventory UI will be implemented here */}
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Resource inventory table coming soon</p>
</div>
{isLoading && <p className="text-gray-400">Loading...</p>}
{error && <p className="text-red-400">Error loading resources</p>}
{tenantData && (
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white">Tenant: {tenantData.tenantId}</CardTitle>
</CardHeader>
<CardContent>
{resources.length === 0 ? (
<p className="text-sm text-muted-foreground">No resources</p>
) : (
<ul className="space-y-2">
{resources.map((r: any) => (
<li key={r.id} className="flex justify-between text-sm">
<span>{r.name}</span>
<span className="text-gray-400">{r.resource_type ?? r.provider}</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
)}
</div>
)
}