Region Diversity: - No region specified → same spec from 3 different regions - Cache key now includes region_preference - Fixed server_id to use ap.id (pricing) instead of ai.id (instance) HTML Report: - New /api/recommend/report endpoint for printable reports - Supports multi-language (en, ko, ja, zh) - Displays bandwidth_info with proper KRW formatting Transfer Pricing: - bandwidth_info includes overage costs from anvil_transfer_pricing - available_regions shows alternative regions with prices Code Quality: - Extracted region-utils.ts for flexible region matching - Cleaned up AI prompt (removed obsolete provider references) - Renamed project to cloud-orchestrator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
/**
|
|
* GET /api/servers - Server list with filtering handler
|
|
*/
|
|
|
|
import type { Env } from '../types';
|
|
import { jsonResponse, isValidServer } from '../utils';
|
|
import {
|
|
DEFAULT_ANVIL_REGION_FILTER_SQL,
|
|
buildFlexibleRegionConditionsAnvil
|
|
} from '../region-utils';
|
|
|
|
/**
|
|
* GET /api/servers - Server list with filtering
|
|
* Uses anvil_* tables for pricing data
|
|
*/
|
|
export async function handleGetServers(
|
|
request: Request,
|
|
env: Env,
|
|
corsHeaders: Record<string, string>
|
|
): Promise<Response> {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const minCpu = url.searchParams.get('minCpu');
|
|
const minMemory = url.searchParams.get('minMemory');
|
|
const region = url.searchParams.get('region');
|
|
|
|
console.log('[GetServers] Query params:', {
|
|
minCpu,
|
|
minMemory,
|
|
region,
|
|
});
|
|
|
|
// Generate cache key from query parameters
|
|
const cacheKey = `servers:${url.search || 'all'}`;
|
|
|
|
// Check cache first
|
|
if (env.CACHE) {
|
|
const cached = await env.CACHE.get(cacheKey);
|
|
if (cached) {
|
|
console.log('[GetServers] Cache hit for:', cacheKey);
|
|
return jsonResponse({ ...JSON.parse(cached), cached: true }, 200, corsHeaders);
|
|
}
|
|
}
|
|
|
|
// Build SQL query using anvil_* tables
|
|
let query = `
|
|
SELECT
|
|
ai.id,
|
|
'Anvil' as provider_name,
|
|
ai.name as instance_id,
|
|
ai.display_name as instance_name,
|
|
ai.vcpus as vcpu,
|
|
CAST(ai.memory_gb * 1024 AS INTEGER) as memory_mb,
|
|
ai.memory_gb,
|
|
ai.disk_gb as storage_gb,
|
|
ai.network_gbps as network_speed_gbps,
|
|
ai.category as instance_family,
|
|
CASE WHEN ai.gpu_model IS NOT NULL THEN 1 ELSE 0 END as gpu_count,
|
|
ai.gpu_model as gpu_type,
|
|
ap.monthly_price,
|
|
ar.display_name as region_name,
|
|
ar.name as region_code
|
|
FROM anvil_instances ai
|
|
JOIN anvil_pricing ap ON ap.anvil_instance_id = ai.id
|
|
JOIN anvil_regions ar ON ap.anvil_region_id = ar.id
|
|
WHERE ai.active = 1 AND ar.active = 1
|
|
AND ${DEFAULT_ANVIL_REGION_FILTER_SQL}
|
|
`;
|
|
|
|
const params: (string | number)[] = [];
|
|
|
|
if (minCpu) {
|
|
const parsedCpu = parseInt(minCpu, 10);
|
|
if (isNaN(parsedCpu)) {
|
|
return jsonResponse({ error: 'Invalid minCpu parameter' }, 400, corsHeaders);
|
|
}
|
|
query += ` AND ai.vcpus >= ?`;
|
|
params.push(parsedCpu);
|
|
}
|
|
|
|
if (minMemory) {
|
|
const parsedMemory = parseInt(minMemory, 10);
|
|
if (isNaN(parsedMemory)) {
|
|
return jsonResponse({ error: 'Invalid minMemory parameter' }, 400, corsHeaders);
|
|
}
|
|
// minMemory is in GB, anvil_instances stores memory_gb
|
|
query += ` AND ai.memory_gb >= ?`;
|
|
params.push(parsedMemory);
|
|
}
|
|
|
|
if (region) {
|
|
// Flexible region matching: supports country names, codes, city names
|
|
const { conditions, params: regionParams } = buildFlexibleRegionConditionsAnvil([region]);
|
|
query += ` AND (${conditions.join(' OR ')})`;
|
|
params.push(...regionParams);
|
|
}
|
|
|
|
query += ` ORDER BY ap.monthly_price ASC LIMIT 100`;
|
|
|
|
const result = await env.DB.prepare(query).bind(...params).all();
|
|
|
|
if (!result.success) {
|
|
throw new Error('Database query failed');
|
|
}
|
|
|
|
// Add USD currency to each result and validate with type guard
|
|
const serversWithCurrency = (result.results as unknown[]).map(server => {
|
|
if (typeof server === 'object' && server !== null) {
|
|
return { ...server, currency: 'USD' };
|
|
}
|
|
return server;
|
|
});
|
|
|
|
const servers = serversWithCurrency.filter(isValidServer);
|
|
const invalidCount = result.results.length - servers.length;
|
|
if (invalidCount > 0) {
|
|
console.warn(`[GetServers] Filtered out ${invalidCount} invalid server records`);
|
|
}
|
|
|
|
console.log('[GetServers] Found servers:', servers.length);
|
|
|
|
const responseData = {
|
|
servers,
|
|
count: servers.length,
|
|
filters: { minCpu, minMemory, region },
|
|
};
|
|
|
|
// Cache successful results (only if we have servers)
|
|
if (env.CACHE && servers.length > 0) {
|
|
await env.CACHE.put(cacheKey, JSON.stringify(responseData), {
|
|
expirationTtl: 300, // 5 minutes
|
|
});
|
|
}
|
|
|
|
return jsonResponse(responseData, 200, corsHeaders);
|
|
} catch (error) {
|
|
console.error('[GetServers] Error:', error);
|
|
const requestId = crypto.randomUUID();
|
|
return jsonResponse(
|
|
{
|
|
error: 'Failed to retrieve servers',
|
|
request_id: requestId,
|
|
},
|
|
500,
|
|
corsHeaders
|
|
);
|
|
}
|
|
}
|