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:
@@ -21,5 +21,14 @@ KEYCLOAK_CLIENT_SECRET=your_keycloak_client_secret
|
|||||||
# For production: minimum 64 characters
|
# For production: minimum 64 characters
|
||||||
JWT_SECRET=your_jwt_secret_here_minimum_64_chars_for_production
|
JWT_SECRET=your_jwt_secret_here_minimum_64_chars_for_production
|
||||||
|
|
||||||
|
# Phoenix API Railing (optional — for /api/v1/infra, /api/v1/ve, /api/v1/health proxy)
|
||||||
|
# Base URL of Phoenix Deploy API or Phoenix API (e.g. http://phoenix-deploy-api:4001)
|
||||||
|
PHOENIX_RAILING_URL=
|
||||||
|
# Optional: API key for server-to-server calls when railing requires PHOENIX_PARTNER_KEYS
|
||||||
|
PHOENIX_RAILING_API_KEY=
|
||||||
|
|
||||||
|
# Public URL for GraphQL Playground link (default http://localhost:4000)
|
||||||
|
# PUBLIC_URL=https://api.sankofa.nexus
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
|
|||||||
const RATE_LIMIT_MAX_REQUESTS = 100 // 100 requests per minute
|
const RATE_LIMIT_MAX_REQUESTS = 100 // 100 requests per minute
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get client identifier from request
|
* Get client identifier from request (per-tenant when available)
|
||||||
*/
|
*/
|
||||||
function getClientId(request: FastifyRequest): string {
|
function getClientId(request: FastifyRequest): string {
|
||||||
// Use IP address or user ID
|
const tenantId = (request as any).tenantContext?.tenantId
|
||||||
const ip = request.ip || request.socket.remoteAddress || 'unknown'
|
if (tenantId) return `tenant:${tenantId}`
|
||||||
const userId = (request as any).user?.id
|
const userId = (request as any).user?.id
|
||||||
|
if (userId) return `user:${userId}`
|
||||||
return userId ? `user:${userId}` : `ip:${ip}`
|
const ip = request.ip || request.socket.remoteAddress || 'unknown'
|
||||||
|
return `ip:${ip}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
66
portal/src/app/infrastructure/page.tsx
Normal file
66
portal/src/app/infrastructure/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useSession } from 'next-auth/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() {
|
export default function ResourcesPage() {
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
|
const { data: tenantData, isLoading, error } = useTenantResources()
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
@@ -14,16 +16,37 @@ export default function ResourcesPage() {
|
|||||||
return <div>Please sign in</div>
|
return <div>Please sign in</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resources = tenantData?.resources ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<h1 className="text-3xl font-bold mb-6">Resource Inventory</h1>
|
<h1 className="text-3xl font-bold mb-6">Resource Inventory</h1>
|
||||||
<p className="text-muted-foreground mb-4">
|
<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>
|
</p>
|
||||||
{/* Resource inventory UI will be implemented here */}
|
{isLoading && <p className="text-gray-400">Loading...</p>}
|
||||||
<div className="border rounded-lg p-4">
|
{error && <p className="text-red-400">Error loading resources</p>}
|
||||||
<p className="text-sm text-muted-foreground">Resource inventory table coming soon</p>
|
{tenantData && (
|
||||||
</div>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
|
import { createCrossplaneClient, VM } from '@/lib/crossplane-client';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
|
||||||
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
import { Server, Activity, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { PhoenixHealthTile } from './dashboard/PhoenixHealthTile';
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { useQuery as useApolloQuery } from '@apollo/client';
|
import { useQuery as useApolloQuery } from '@apollo/client';
|
||||||
@@ -132,6 +133,10 @@ export default function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
<PhoenixHealthTile />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Activity</CardTitle>
|
<CardTitle>Recent Activity</CardTitle>
|
||||||
|
|||||||
81
portal/src/components/dashboard/PhoenixHealthTile.tsx
Normal file
81
portal/src/components/dashboard/PhoenixHealthTile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { cn } from '@/lib/utils'
|
|||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||||
|
{ name: 'Infrastructure', href: '/infrastructure', icon: Server },
|
||||||
{ name: 'Resources', href: '/resources', icon: Server },
|
{ name: 'Resources', href: '/resources', icon: Server },
|
||||||
{ name: 'Virtual Machines', href: '/vms', icon: Server },
|
{ name: 'Virtual Machines', href: '/vms', icon: Server },
|
||||||
{ name: 'Networking', href: '/network', icon: Network },
|
{ name: 'Networking', href: '/network', icon: Network },
|
||||||
|
|||||||
99
portal/src/hooks/usePhoenixRailing.ts
Normal file
99
portal/src/hooks/usePhoenixRailing.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import {
|
||||||
|
getInfraNodes,
|
||||||
|
getInfraStorage,
|
||||||
|
getVMs,
|
||||||
|
getHealthSummary,
|
||||||
|
getHealthAlerts,
|
||||||
|
getTenantResources,
|
||||||
|
getTenantHealth,
|
||||||
|
vmAction,
|
||||||
|
} from '@/lib/phoenix-api-client';
|
||||||
|
|
||||||
|
export function usePhoenixInfraNodes() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'infra', 'nodes'],
|
||||||
|
queryFn: () => getInfraNodes(token),
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePhoenixInfraStorage() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'infra', 'storage'],
|
||||||
|
queryFn: () => getInfraStorage(token),
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePhoenixVMs(node?: string) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 've', 'vms', node],
|
||||||
|
queryFn: () => getVMs(token, node),
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePhoenixHealthSummary() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'health', 'summary'],
|
||||||
|
queryFn: () => getHealthSummary(token),
|
||||||
|
enabled: !!token,
|
||||||
|
refetchInterval: 60000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePhoenixHealthAlerts() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'health', 'alerts'],
|
||||||
|
queryFn: () => getHealthAlerts(token),
|
||||||
|
enabled: !!token,
|
||||||
|
refetchInterval: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTenantResources() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'tenants', 'me', 'resources'],
|
||||||
|
queryFn: () => getTenantResources(token),
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTenantHealth() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['phoenix', 'tenants', 'me', 'health'],
|
||||||
|
queryFn: () => getTenantHealth(token),
|
||||||
|
enabled: !!token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVMAction() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const token = (session as any)?.accessToken as string | undefined;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ node, vmid, action, type }: { node: string; vmid: string; action: 'start' | 'stop' | 'reboot'; type?: 'qemu' | 'lxc' }) =>
|
||||||
|
vmAction(node, vmid, action, token, type || 'qemu'),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['phoenix', 've', 'vms'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
73
portal/src/lib/phoenix-api-client.ts
Normal file
73
portal/src/lib/phoenix-api-client.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Phoenix API Railing client — Infra, VE, Health, tenant-scoped.
|
||||||
|
* Calls Sankofa API /api/v1/* (proxies to Phoenix Deploy API when PHOENIX_RAILING_URL is set).
|
||||||
|
* Auth: Bearer token from NextAuth session (Keycloak/JWT) or X-API-Key.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
const g = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || '';
|
||||||
|
if (g) return g.replace(/\/graphql\/?$/, '');
|
||||||
|
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function phoenixFetch<T>(
|
||||||
|
path: string,
|
||||||
|
token: string | undefined,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const base = getBaseUrl();
|
||||||
|
const url = path.startsWith('http') ? path : `${base}${path}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||||
|
throw new Error((err as any).message || (err as any).error || res.statusText);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInfraNodes(token?: string) {
|
||||||
|
return phoenixFetch<{ nodes: any[]; stub?: boolean }>('/api/v1/infra/nodes', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInfraStorage(token?: string) {
|
||||||
|
return phoenixFetch<{ storage: any[]; stub?: boolean }>('/api/v1/infra/storage', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVMs(token?: string, node?: string) {
|
||||||
|
const qs = node ? `?node=${encodeURIComponent(node)}` : '';
|
||||||
|
return phoenixFetch<{ vms: any[]; stub?: boolean }>(`/api/v1/ve/vms${qs}`, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVMStatus(node: string, vmid: string, token?: string, type: 'qemu' | 'lxc' = 'qemu') {
|
||||||
|
return phoenixFetch<{ node: string; vmid: string; status?: string }>(
|
||||||
|
`/api/v1/ve/vms/${node}/${vmid}/status?type=${type}`,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function vmAction(node: string, vmid: string, action: 'start' | 'stop' | 'reboot', token?: string, type: 'qemu' | 'lxc' = 'qemu') {
|
||||||
|
return phoenixFetch<{ ok: boolean }>(`/api/v1/ve/vms/${node}/${vmid}/${action}?type=${type}`, token, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHealthSummary(token?: string) {
|
||||||
|
return phoenixFetch<{ status: string; updated_at: string; hosts: any[]; alerts: any[] }>('/api/v1/health/summary', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHealthAlerts(token?: string) {
|
||||||
|
return phoenixFetch<{ alerts: any[] }>('/api/v1/health/alerts', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTenantResources(token?: string) {
|
||||||
|
return phoenixFetch<{ resources: any[]; tenantId: string }>('/api/v1/tenants/me/resources', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTenantHealth(token?: string) {
|
||||||
|
return phoenixFetch<{ tenantId: string; status: string }>('/api/v1/tenants/me/health', token);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user