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:
kappa
2026-01-26 02:49:24 +09:00
parent 580cc1bbe2
commit 411cde4801
11 changed files with 1132 additions and 356 deletions

View File

@@ -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();