feat: add region diversity, HTML report, and transfer pricing
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>
This commit is contained in:
@@ -4,85 +4,10 @@
|
||||
|
||||
import type { Env } from '../types';
|
||||
import { jsonResponse, isValidServer } from '../utils';
|
||||
|
||||
/**
|
||||
* Default region filter SQL for anvil_regions (when no region is specified)
|
||||
*/
|
||||
const DEFAULT_ANVIL_REGION_FILTER_SQL = `(
|
||||
-- Korea (Seoul)
|
||||
ar.name IN ('icn', 'ap-northeast-2') OR
|
||||
LOWER(ar.display_name) LIKE '%seoul%' OR
|
||||
-- Japan (Tokyo, Osaka)
|
||||
ar.name IN ('nrt', 'itm', 'ap-northeast-1', 'ap-northeast-3') OR
|
||||
LOWER(ar.name) LIKE '%tyo%' OR
|
||||
LOWER(ar.name) LIKE '%osa%' OR
|
||||
LOWER(ar.display_name) LIKE '%tokyo%' OR
|
||||
LOWER(ar.display_name) LIKE '%osaka%' OR
|
||||
-- Singapore
|
||||
ar.name IN ('sgp', 'ap-southeast-1') OR
|
||||
LOWER(ar.name) LIKE '%sin%' OR
|
||||
LOWER(ar.name) LIKE '%sgp%' OR
|
||||
LOWER(ar.display_name) LIKE '%singapore%'
|
||||
)`;
|
||||
|
||||
/**
|
||||
* Country name to region code/city mapping for flexible region matching
|
||||
*/
|
||||
const COUNTRY_NAME_TO_REGIONS: Record<string, string[]> = {
|
||||
'korea': ['seoul', 'koreacentral', 'kr', 'ap-northeast-2'],
|
||||
'south korea': ['seoul', 'koreacentral', 'kr', 'ap-northeast-2'],
|
||||
'japan': ['tokyo', 'osaka', 'ap-northeast-1', 'ap-northeast-3'],
|
||||
'singapore': ['singapore', 'ap-southeast-1'],
|
||||
'indonesia': ['jakarta', 'ap-southeast-3'],
|
||||
'australia': ['sydney', 'melbourne', 'au-east', 'ap-southeast-2'],
|
||||
'india': ['mumbai', 'delhi', 'chennai', 'ap-south'],
|
||||
'usa': ['us-east', 'us-west', 'us-central', 'america'],
|
||||
'us': ['us-east', 'us-west', 'us-central', 'america'],
|
||||
'united states': ['us-east', 'us-west', 'us-central', 'america'],
|
||||
'uk': ['london', 'uk-south', 'eu-west-2'],
|
||||
'united kingdom': ['london', 'uk-south', 'eu-west-2'],
|
||||
'germany': ['frankfurt', 'eu-central', 'eu-west-3'],
|
||||
'france': ['paris', 'eu-west-3'],
|
||||
'netherlands': ['amsterdam', 'eu-west-1'],
|
||||
'brazil': ['sao paulo', 'sa-east'],
|
||||
'canada': ['toronto', 'montreal', 'ca-central'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape LIKE pattern special characters
|
||||
*/
|
||||
function escapeLikePattern(pattern: string): string {
|
||||
return pattern.replace(/[%_\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build flexible region matching SQL conditions for anvil_regions
|
||||
*/
|
||||
function buildFlexibleRegionConditionsAnvil(
|
||||
regions: string[]
|
||||
): { conditions: string[]; params: (string | number)[] } {
|
||||
const conditions: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
for (const region of regions) {
|
||||
const lowerRegion = region.toLowerCase();
|
||||
const expandedRegions = COUNTRY_NAME_TO_REGIONS[lowerRegion] || [lowerRegion];
|
||||
const allRegions = [lowerRegion, ...expandedRegions];
|
||||
|
||||
for (const r of allRegions) {
|
||||
const escapedRegion = escapeLikePattern(r);
|
||||
conditions.push(`(
|
||||
LOWER(ar.name) = ? OR
|
||||
LOWER(ar.name) LIKE ? ESCAPE '\\' OR
|
||||
LOWER(ar.display_name) LIKE ? ESCAPE '\\' OR
|
||||
LOWER(ar.country_code) = ?
|
||||
)`);
|
||||
params.push(r, `%${escapedRegion}%`, `%${escapedRegion}%`, r);
|
||||
}
|
||||
}
|
||||
|
||||
return { conditions, params };
|
||||
}
|
||||
import {
|
||||
DEFAULT_ANVIL_REGION_FILTER_SQL,
|
||||
buildFlexibleRegionConditionsAnvil
|
||||
} from '../region-utils';
|
||||
|
||||
/**
|
||||
* GET /api/servers - Server list with filtering
|
||||
@@ -105,6 +30,18 @@ export async function handleGetServers(
|
||||
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
|
||||
@@ -182,15 +119,20 @@ export async function handleGetServers(
|
||||
|
||||
console.log('[GetServers] Found servers:', servers.length);
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
servers,
|
||||
count: servers.length,
|
||||
filters: { minCpu, minMemory, region },
|
||||
},
|
||||
200,
|
||||
corsHeaders
|
||||
);
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user