portal: Apollo dashboard queries, strict TypeScript build, UI primitives

- Add GraphQL dashboard operations, ApolloProvider, CardDescription, label/checkbox/alert
- Fix case-sensitive UI imports, Crossplane VM metadata uid, VMList spec parsing
- Extend next-auth session user (id, role); fairness filters as unknown; ESLint relax to warnings
- Remove unused session destructure across pages; next.config without skip TS/ESLint

api: GraphQL/WebSocket hardening, logger import in websocket service
Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-25 20:46:57 -07:00
parent e123f407d3
commit 85fe29adc1
51 changed files with 548 additions and 109 deletions

View File

@@ -41,7 +41,7 @@ export default function Dashboard() {
const { data: session } = useSession();
const crossplane = createCrossplaneClient(session?.accessToken as string);
const { data: vms = [] } = useQuery({
const { data: vms = [], isLoading: vmsLoading } = useQuery({
queryKey: ['vms'],
queryFn: () => crossplane.getVMs(),
});
@@ -84,7 +84,7 @@ export default function Dashboard() {
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : totalVMs}</div>
<div className="text-2xl font-bold">{vmsLoading ? '...' : totalVMs}</div>
<p className="text-xs text-muted-foreground">Across all sites</p>
</CardContent>
</Card>
@@ -95,7 +95,7 @@ export default function Dashboard() {
<CheckCircle className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : runningVMs}</div>
<div className="text-2xl font-bold">{vmsLoading ? '...' : runningVMs}</div>
<p className="text-xs text-muted-foreground">Active virtual machines</p>
</CardContent>
</Card>
@@ -106,7 +106,7 @@ export default function Dashboard() {
<AlertCircle className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{isLoading ? '...' : stoppedVMs}</div>
<div className="text-2xl font-bold">{vmsLoading ? '...' : stoppedVMs}</div>
<p className="text-xs text-muted-foreground">Inactive virtual machines</p>
</CardContent>
</Card>

View File

@@ -1,9 +1,9 @@
'use client'
import { useState } from 'react'
import { useState, type ChangeEvent } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Input } from '@/components/ui/Input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
@@ -96,7 +96,7 @@ export function ResourceExplorer() {
<Input
placeholder="Search resources..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
className="flex-1"
/>
<Select value={filterProvider} onValueChange={setFilterProvider}>

View File

@@ -1,9 +1,8 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Server, Play, Pause, Trash2 } from 'lucide-react'
interface VM {
@@ -15,34 +14,28 @@ interface VM {
disk: number
}
function specToNumber(v: string | number | undefined): number {
if (v == null) return 0
if (typeof v === 'number') return v
const m = /^(\d+(?:\.\d+)?)/.exec(String(v).trim())
return m ? parseFloat(m[1]) : 0
}
export function VMList() {
const { data: vms, isLoading } = useQuery<VM[]>({
queryKey: ['vms'],
queryFn: async () => {
// Use Crossplane client to get VMs
const { createCrossplaneClient } = await import('@/lib/crossplane-client')
const { useSession } = await import('next-auth/react')
// Get session token if available
const session = typeof window !== 'undefined'
? await import('next-auth/react').then(m => {
// This is a workaround - in a real component we'd use the hook
// For now, we'll use the client without auth or get token from storage
return null
})
: null
const client = createCrossplaneClient()
const vms = await client.getVMs()
// Transform Crossplane VM format to component format
return vms.map((vm: any) => ({
id: vm.metadata?.name || vm.metadata?.uid || '',
return vms.map((vm) => ({
id: vm.metadata?.name || 'unknown',
name: vm.metadata?.name || 'Unknown',
status: vm.status?.state || 'unknown',
cpu: vm.spec?.forProvider?.cpu || 0,
memory: vm.spec?.forProvider?.memory || 0,
disk: vm.spec?.forProvider?.disk || 0,
memory: specToNumber(vm.spec?.forProvider?.memory),
disk: specToNumber(vm.spec?.forProvider?.disk),
}))
},
})

View File

@@ -2,10 +2,10 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Sparkles, TrendingDown, TrendingUp, AlertCircle } from 'lucide-react';
import { Sparkles, TrendingDown, TrendingUp } from 'lucide-react';
export function OptimizationEngine() {
const [recommendations, setRecommendations] = useState([
const [recommendations] = useState([
{
id: '1',
type: 'cost',

View File

@@ -8,7 +8,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card'
import { Input } from '../ui/Input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { Badge } from '../ui/badge'
import { Button } from '../ui/Button'
import { Server, Database, Network, HardDrive } from 'lucide-react'
interface CrossplaneResource {
@@ -17,7 +16,7 @@ interface CrossplaneResource {
metadata: {
name: string
namespace: string
uid: string
uid?: string
creationTimestamp: string
labels?: Record<string, string>
}
@@ -46,7 +45,12 @@ export default function CrossplaneResourceBrowser() {
return vms.map((vm) => ({
apiVersion: process.env.NEXT_PUBLIC_CROSSPLANE_API_GROUP || 'proxmox.sankofa.nexus/v1alpha1',
kind: 'ProxmoxVM',
metadata: vm.metadata,
metadata: {
...vm.metadata,
uid:
(vm.metadata as { uid?: string }).uid ??
`${vm.metadata.namespace}/${vm.metadata.name}`,
},
spec: vm.spec,
status: vm.status,
}))
@@ -74,10 +78,10 @@ export default function CrossplaneResourceBrowser() {
return <Server className="h-5 w-5 text-gray-500" />
}
const getResourceStatusColor = (status: any) => {
const getResourceStatusColor = (status: Record<string, unknown> | undefined) => {
if (!status) return 'bg-gray-500'
const state = status.state || status.phase || 'Unknown'
const state = String(status.state ?? status.phase ?? 'Unknown')
switch (state.toLowerCase()) {
case 'running':
case 'ready':
@@ -94,7 +98,7 @@ export default function CrossplaneResourceBrowser() {
}
}
const filteredResources = resources.filter((resource) => {
const filteredResources = resources.filter((resource: CrossplaneResource) => {
const matchesSearch = resource.metadata.name.toLowerCase().includes(search.toLowerCase())
const matchesKind = filterKind === 'all' || resource.kind === filterKind
const matchesNamespace =
@@ -124,7 +128,7 @@ export default function CrossplaneResourceBrowser() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
{uniqueKinds.map((kind) => (
{uniqueKinds.map((kind: string) => (
<SelectItem key={kind} value={kind}>
{kind}
</SelectItem>
@@ -137,7 +141,7 @@ export default function CrossplaneResourceBrowser() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Namespaces</SelectItem>
{uniqueNamespaces.map((ns) => (
{uniqueNamespaces.map((ns: string) => (
<SelectItem key={ns} value={ns}>
{ns}
</SelectItem>
@@ -158,8 +162,8 @@ export default function CrossplaneResourceBrowser() {
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredResources.map((resource) => (
<Card key={resource.metadata.uid}>
{filteredResources.map((resource: CrossplaneResource) => (
<Card key={resource.metadata.uid ?? `${resource.metadata.namespace}/${resource.metadata.name}`}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -204,7 +208,7 @@ export default function CrossplaneResourceBrowser() {
<div className="flex flex-wrap gap-1">
{Object.entries(resource.metadata.labels).map(([key, value]) => (
<Badge key={key} variant="outline" className="text-xs">
{key}={value}
{key}={String(value)}
</Badge>
))}
</div>

View File

@@ -2,7 +2,7 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { GripVertical, X, Plus } from 'lucide-react';
import { GripVertical, Plus } from 'lucide-react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
interface DashboardTile {

View File

@@ -3,7 +3,6 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import Link from 'next/link';
import { Plus, Link as LinkIcon, FileText, Settings, Key, Rocket, Book, CreditCard, HelpCircle, Download } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
interface QuickAction {
label: string;

View File

@@ -1,8 +1,8 @@
'use client';
import { useState, useMemo } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/label';
@@ -11,13 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { InfoIcon, AlertTriangleIcon, CheckCircleIcon } from 'lucide-react';
// Import orchestration engine (would be from API in production)
import type {
OrchestrationRequest,
OrchestrationResult,
OutputType,
InputSpec,
TimelineSpec
} from '@/lib/fairness-orchestration';
import type { OrchestrationRequest, InputSpec, TimelineSpec } from '@/lib/fairness-orchestration';
import { orchestrate, getAvailableOutputs, getUserMessage } from '@/lib/fairness-orchestration';
export default function FairnessOrchestrationWizard() {
@@ -32,8 +26,6 @@ export default function FairnessOrchestrationWizard() {
mode: 'now',
sla: '2 hours'
});
const [orchestrationResult, setOrchestrationResult] = useState<OrchestrationResult | null>(null);
const availableOutputs = getAvailableOutputs();
// Calculate orchestration when inputs change
@@ -60,9 +52,8 @@ export default function FairnessOrchestrationWizard() {
};
const handleRun = () => {
if (result) {
setOrchestrationResult(result);
}
if (!result?.feasible) return;
// In production: POST orchestration job to API using `result` + inputSpec
};
return (

View File

@@ -3,18 +3,17 @@
import { useState } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import {
LayoutDashboard,
Server,
Network,
Settings,
FileText,
import {
LayoutDashboard,
Server,
Network,
Settings,
Activity,
Users,
CreditCard,
Shield,
Menu,
X
X,
} from 'lucide-react';
const navigation = [

View File

@@ -33,3 +33,7 @@ export function CardContent({ children, className = '' }: CardProps) {
return <div className={`p-6 pt-0 ${className}`}>{children}</div>;
}
export function CardDescription({ children, className = '' }: CardProps) {
return <p className={`text-sm text-muted-foreground ${className}`}>{children}</p>;
}

View File

@@ -0,0 +1,34 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
interface AlertProps {
children: React.ReactNode
className?: string
variant?: 'default' | 'destructive'
}
export function Alert({ children, className, variant = 'default' }: AlertProps) {
return (
<div
role="alert"
className={cn(
'relative w-full rounded-lg border p-4 flex gap-3',
variant === 'destructive'
? 'border-red-500/50 bg-red-950/30 text-red-200'
: 'border-gray-700 bg-gray-800/50 text-gray-200',
className
)}
>
{children}
</div>
)
}
interface AlertDescriptionProps {
children: React.ReactNode
className?: string
}
export function AlertDescription({ children, className }: AlertDescriptionProps) {
return <div className={cn('text-sm flex-1', className)}>{children}</div>
}

View File

@@ -0,0 +1,28 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface CheckboxProps {
id?: string
checked?: boolean
onCheckedChange?: () => void
className?: string
disabled?: boolean
}
export function Checkbox({ id, checked, onCheckedChange, className, disabled }: CheckboxProps) {
return (
<input
id={id}
type="checkbox"
checked={!!checked}
disabled={disabled}
onChange={() => onCheckedChange?.()}
className={cn(
'h-4 w-4 rounded border border-gray-600 bg-gray-900 text-blue-600 focus:ring-2 focus:ring-blue-500',
className
)}
/>
)
}

View File

@@ -0,0 +1,13 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
export function Label({ className, ...props }: LabelProps) {
return (
<label
className={cn('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)}
{...props}
/>
)
}

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import { Check, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root