From f0bb454ca2deb206a81eb9b0e2d5dddcea62b1c5 Mon Sep 17 00:00:00 2001 From: Kelly Date: Sat, 13 Dec 2025 21:58:47 -0700 Subject: [PATCH] fix(workers): Require geo for worker qualification status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Workers without geo now show orange "NO GEO" badge instead of gold qualified - Orange ring + X badge on avatar when preflight OK but no geo - Gold ring + checkmark only when fully qualified (preflight + geo) - Add VenetianMask icon for antidetect status indicator - Lock K8s replica count at exactly 8 pods in CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 12 +++-- backend/src/services/task-scheduler.ts | 2 +- cannaiq/src/pages/WorkersDashboard.tsx | 69 +++++++++++++++++++++----- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3d7396e8..cbff82e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,12 +21,18 @@ Never deploy unless user explicitly says: "CLAUDE — DEPLOYMENT IS NOW AUTHORIZ Never import `src/db/migrate.ts` at runtime. Use `src/db/pool.ts` for DB access. ### 6. K8S POD LIMITS — CRITICAL -**MAX 8 PODS** for `scraper-worker` deployment. NEVER EXCEED THIS. +**EXACTLY 8 PODS** for `scraper-worker` deployment. NEVER CHANGE THIS. + +**Replica Count is LOCKED:** +- Always 8 replicas — no more, no less +- NEVER scale down (even temporarily) +- NEVER scale up beyond 8 +- If pods are not 8, restore to 8 immediately **Pods vs Workers:** -- **Pod** = Kubernetes container instance (MAX 8) +- **Pod** = Kubernetes container instance (ALWAYS 8) - **Worker** = Concurrent task runner INSIDE a pod (controlled by `MAX_CONCURRENT_TASKS` env var) -- Formula: `8 pods × MAX_CONCURRENT_TASKS = total concurrent workers` +- Formula: `8 pods × MAX_CONCURRENT_TASKS = 24 total concurrent workers` **Browser Task Memory Limits:** - Each Puppeteer/Chrome browser uses ~400 MB RAM diff --git a/backend/src/services/task-scheduler.ts b/backend/src/services/task-scheduler.ts index 579e1064..2c59e411 100644 --- a/backend/src/services/task-scheduler.ts +++ b/backend/src/services/task-scheduler.ts @@ -103,7 +103,7 @@ class TaskScheduler { role: 'store_discovery' as TaskRole, interval_hours: 168, // Weekly priority: 5, - description: 'Discover new Dutchie stores weekly', + description: 'Discover new Dutchie stores weekly (HTTP transport)', method: 'http', is_immutable: true, platform: 'dutchie', diff --git a/cannaiq/src/pages/WorkersDashboard.tsx b/cannaiq/src/pages/WorkersDashboard.tsx index 5b9c10aa..96938179 100644 --- a/cannaiq/src/pages/WorkersDashboard.tsx +++ b/cannaiq/src/pages/WorkersDashboard.tsx @@ -28,6 +28,7 @@ import { ShieldX, Globe, Fingerprint, + VenetianMask, } from 'lucide-react'; // Worker from registry @@ -358,7 +359,6 @@ function ResourceBadge({ worker }: { worker: Worker }) { // Preflight Summary - shows IP, fingerprint, antidetect status, and qualification function PreflightSummary({ worker }: { worker: Worker }) { const httpStatus = worker.preflight_http_status || 'pending'; - const isQualified = worker.is_qualified || httpStatus === 'passed'; const httpIp = worker.http_ip; const fingerprint = worker.fingerprint_data; const httpError = worker.preflight_http_error; @@ -366,6 +366,9 @@ function PreflightSummary({ worker }: { worker: Worker }) { // Geo from current_city/state columns, or fallback to fingerprint detected location const geoState = worker.current_state || fingerprint?.detectedLocation?.region; const geoCity = worker.current_city || fingerprint?.detectedLocation?.city; + // Worker is ONLY qualified if http preflight passed AND has geo assigned + const hasGeo = Boolean(geoState); + const isQualified = (worker.is_qualified || httpStatus === 'passed') && hasGeo; // Build detailed tooltip const tooltipLines: string[] = []; @@ -385,7 +388,7 @@ function PreflightSummary({ worker }: { worker: Worker }) { } if (httpError) tooltipLines.push(`Error: ${httpError}`); - // Qualification styling - gold shield + geo for qualified + // Qualification styling - gold shield + geo for qualified (requires geo) if (isQualified) { return (
@@ -393,9 +396,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
- {geoCity && geoState ? `${geoCity}, ${geoState}` : - geoState ? geoState : - 'No geo assigned'} + {geoCity && geoState ? `${geoCity}, ${geoState}` : geoState}
{/* IP address */} @@ -414,7 +415,7 @@ function PreflightSummary({ worker }: { worker: Worker }) { )} {/* Antidetect status */}
- + Antidetect OK {httpMs && ({httpMs}ms)}
@@ -422,7 +423,7 @@ function PreflightSummary({ worker }: { worker: Worker }) { ); } - // Not qualified - show failure state + // Preflight failed if (httpStatus === 'failed') { return (
@@ -437,6 +438,27 @@ function PreflightSummary({ worker }: { worker: Worker }) { ); } + // Preflight passed but NO GEO - not qualified + if (httpStatus === 'passed' && !hasGeo) { + return ( +
+
+ + NO GEO +
+ {httpIp && ( +
+ + {httpIp} +
+ )} +
+ Preflight OK, awaiting geo +
+
+ ); + } + // Pending state return (
@@ -1405,18 +1427,39 @@ export function WorkersDashboard() { worker.health_status === 'stale' ? 'bg-yellow-500' : worker.health_status === 'busy' ? 'bg-blue-500' : 'bg-emerald-500' - } ${worker.is_qualified ? 'ring-2 ring-amber-400 ring-offset-2' : ''}`}> + } ${(() => { + const hasGeo = worker.current_state || worker.fingerprint_data?.detectedLocation?.region; + const preflightPassed = worker.is_qualified || worker.preflight_http_status === 'passed'; + if (preflightPassed && hasGeo) return 'ring-2 ring-amber-400 ring-offset-2'; + if (preflightPassed && !hasGeo) return 'ring-2 ring-orange-400 ring-offset-2'; + return ''; + })()}`}> {worker.friendly_name?.charAt(0) || '?'} {worker.decommission_requested && (
)} - {worker.is_qualified && !worker.decommission_requested && ( -
- -
- )} + {!worker.decommission_requested && (() => { + const hasGeo = worker.current_state || worker.fingerprint_data?.detectedLocation?.region; + const preflightPassed = worker.is_qualified || worker.preflight_http_status === 'passed'; + if (preflightPassed && hasGeo) { + // Qualified - gold shield with check + return ( +
+ +
+ ); + } else if (preflightPassed && !hasGeo) { + // Preflight OK but no geo - orange X + return ( +
+ +
+ ); + } + return null; + })()}