diff --git a/src/index.ts b/src/index.ts index feb1493..047974a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -720,8 +720,26 @@ async function handleRecommend( ? Math.max(...memoryIntensiveSpecs.map(s => s.min_memory_mb)) : undefined; - // Phase 2: Query candidate servers (depends on minMemoryMb) - const candidates = await queryCandidateServers(env.DB, body, minMemoryMb, lang); + // Calculate minimum vCPU based on expected users and tech specs + // Formula: expected_users / vcpu_per_users (use minimum ratio from all tech specs) + let minVcpu: number | undefined; + if (techSpecs.length > 0) { + const vcpuRequirements = techSpecs.map(spec => { + // Use vcpu_per_users: 1 vCPU can handle N users + // So for expected_users, we need: expected_users / vcpu_per_users vCPUs + const vcpuNeeded = Math.ceil(body.expected_users / spec.vcpu_per_users); + return vcpuNeeded; + }); + minVcpu = Math.max(...vcpuRequirements, 1); // At least 1 vCPU + console.log(`[Recommend] Minimum vCPU required: ${minVcpu} (for ${body.expected_users} users)`); + } + + // Calculate bandwidth estimate for provider filtering + const bandwidthEstimate = estimateBandwidth(body.expected_users, body.use_case, body.traffic_pattern); + console.log(`[Recommend] Bandwidth estimate: ${bandwidthEstimate.monthly_tb >= 1 ? bandwidthEstimate.monthly_tb + ' TB' : bandwidthEstimate.monthly_gb + ' GB'}/month (${bandwidthEstimate.category})`); + + // Phase 2: Query candidate servers (depends on minMemoryMb, minVcpu, bandwidth) + const candidates = await queryCandidateServers(env.DB, body, minMemoryMb, minVcpu, bandwidthEstimate, lang); console.log('[Recommend] Candidate servers:', candidates.length); if (candidates.length === 0) { @@ -904,12 +922,16 @@ function escapeLikePattern(pattern: string): string { /** * Query candidate servers from database * @param minMemoryMb - Minimum memory requirement from tech specs (optional) + * @param minVcpu - Minimum vCPU requirement based on expected users (optional) + * @param bandwidthEstimate - Bandwidth estimate for provider prioritization (optional) * @param lang - Language for currency selection: 'ko' → KRW, others → retail USD */ async function queryCandidateServers( db: D1Database, req: RecommendRequest, minMemoryMb?: number, + minVcpu?: number, + bandwidthEstimate?: BandwidthEstimate, lang: string = 'en' ): Promise { // Select price column based on language @@ -961,6 +983,28 @@ async function queryCandidateServers( console.log(`[Candidates] Filtering by minimum memory: ${minMemoryMb}MB (${(minMemoryMb/1024).toFixed(1)}GB)`); } + // Filter by minimum vCPU requirement (from expected users + tech specs) + if (minVcpu && minVcpu > 0) { + query += ` AND it.vcpu >= ?`; + params.push(minVcpu); + console.log(`[Candidates] Filtering by minimum vCPU: ${minVcpu}`); + } + + // Provider filtering based on bandwidth requirements + // Heavy bandwidth (>2TB/month) → Linode only (better bandwidth allowance) + // Very heavy bandwidth (>6TB/month) → Linode only with warning + if (bandwidthEstimate) { + if (bandwidthEstimate.category === 'very_heavy') { + // >6TB/month: Linode only (includes up to 20TB depending on plan) + query += ` AND p.id = 1`; // Linode only + console.log(`[Candidates] Very heavy bandwidth (${bandwidthEstimate.monthly_tb}TB/month): Linode only`); + } else if (bandwidthEstimate.category === 'heavy') { + // 2-6TB/month: Prefer Linode, but allow Vultr + // Order by Linode first (handled in ORDER BY) + console.log(`[Candidates] Heavy bandwidth (${bandwidthEstimate.monthly_tb}TB/month): Linode preferred`); + } + } + // Country name to code mapping for common names // Note: Use specific city names to avoid LIKE pattern collisions (e.g., 'de' matches 'Delhi') const countryNameToCode: Record = { @@ -1035,7 +1079,11 @@ async function queryCandidateServers( } // Group by instance + region to show each server per region - query += ` GROUP BY it.id, r.id ORDER BY monthly_price ASC LIMIT 50`; + // For heavy bandwidth, prioritize Linode (p.id=1) over Vultr (p.id=2) + const orderByClause = bandwidthEstimate?.category === 'heavy' + ? `ORDER BY CASE WHEN p.id = 1 THEN 0 ELSE 1 END, monthly_price ASC` + : `ORDER BY monthly_price ASC`; + query += ` GROUP BY it.id, r.id ${orderByClause} LIMIT 50`; const result = await db.prepare(query).bind(...params).all();