feat: multi-cluster management with pool-based load balancing

- Add ClusterPool entity for grouping clusters into named pools
- Support 3 deployment modes: manual cluster, pool load-balanced, default fallback
- Pool strategies: least-apps (fewest deployed apps) and round-robin
- Add pool CRUD API endpoints (admin) and public pool listing
- Frontend deploy page: 3-mode cluster selector (Default/Manual/Pool)
- Frontend app detail: shows assigned cluster and pool info
- Admin pools management page with cluster selection and strategy picker
- Application entity extended with poolId field
This commit is contained in:
keyhan
2026-04-05 17:37:05 +03:30
parent 1b1ccfc18f
commit 2621dc0cc6
14 changed files with 976 additions and 14 deletions
+27 -1
View File
@@ -4,7 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useRouter } from 'next/navigation';
import api from '@/lib/api';
import toast from 'react-hot-toast';
import type { Application, Deployment, ResourceUsage } from '@/types';
import type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic } from '@/types';
import { useState, useRef, useCallback, useEffect } from 'react';
const statusColors: Record<string, string> = {
@@ -85,6 +85,16 @@ export default function AppDetailPage() {
refetchInterval: showResources ? 5000 : false,
});
const { data: clusters = [] } = useQuery<ClusterPublic[]>({
queryKey: ['clusters-public'],
queryFn: () => api.get('/clusters/public').then((r) => r.data),
});
const { data: pools = [] } = useQuery<ClusterPoolPublic[]>({
queryKey: ['pools-public'],
queryFn: () => api.get('/clusters/pools/public').then((r) => r.data),
});
// Sync form when resource data loads
useEffect(() => {
if (resourceUsage?.configured) {
@@ -378,6 +388,22 @@ export default function AppDetailPage() {
<dt className="text-sm text-gray-500">Port</dt>
<dd className="text-sm font-medium text-gray-900">{app.port}</dd>
</div>
{app.clusterId && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Cluster</dt>
<dd className="text-sm font-medium text-gray-900">
🖥 {clusters.find((c) => c.id === app.clusterId)?.name || 'Unknown'}
</dd>
</div>
)}
{app.poolId && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Pool</dt>
<dd className="text-sm font-medium text-gray-900">
{pools.find((p) => p.id === app.poolId)?.name || 'Unknown'}
</dd>
</div>
)}
{app.latestImageTag && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Image</dt>