fix(workers): Require geo for worker qualification status

- 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 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-13 21:58:47 -07:00
parent b8bdc48c1e
commit f0bb454ca2
3 changed files with 66 additions and 17 deletions

View File

@@ -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. Never import `src/db/migrate.ts` at runtime. Use `src/db/pool.ts` for DB access.
### 6. K8S POD LIMITS — CRITICAL ### 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:** **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) - **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:** **Browser Task Memory Limits:**
- Each Puppeteer/Chrome browser uses ~400 MB RAM - Each Puppeteer/Chrome browser uses ~400 MB RAM

View File

@@ -103,7 +103,7 @@ class TaskScheduler {
role: 'store_discovery' as TaskRole, role: 'store_discovery' as TaskRole,
interval_hours: 168, // Weekly interval_hours: 168, // Weekly
priority: 5, priority: 5,
description: 'Discover new Dutchie stores weekly', description: 'Discover new Dutchie stores weekly (HTTP transport)',
method: 'http', method: 'http',
is_immutable: true, is_immutable: true,
platform: 'dutchie', platform: 'dutchie',

View File

@@ -28,6 +28,7 @@ import {
ShieldX, ShieldX,
Globe, Globe,
Fingerprint, Fingerprint,
VenetianMask,
} from 'lucide-react'; } from 'lucide-react';
// Worker from registry // Worker from registry
@@ -358,7 +359,6 @@ function ResourceBadge({ worker }: { worker: Worker }) {
// Preflight Summary - shows IP, fingerprint, antidetect status, and qualification // Preflight Summary - shows IP, fingerprint, antidetect status, and qualification
function PreflightSummary({ worker }: { worker: Worker }) { function PreflightSummary({ worker }: { worker: Worker }) {
const httpStatus = worker.preflight_http_status || 'pending'; const httpStatus = worker.preflight_http_status || 'pending';
const isQualified = worker.is_qualified || httpStatus === 'passed';
const httpIp = worker.http_ip; const httpIp = worker.http_ip;
const fingerprint = worker.fingerprint_data; const fingerprint = worker.fingerprint_data;
const httpError = worker.preflight_http_error; 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 // Geo from current_city/state columns, or fallback to fingerprint detected location
const geoState = worker.current_state || fingerprint?.detectedLocation?.region; const geoState = worker.current_state || fingerprint?.detectedLocation?.region;
const geoCity = worker.current_city || fingerprint?.detectedLocation?.city; 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 // Build detailed tooltip
const tooltipLines: string[] = []; const tooltipLines: string[] = [];
@@ -385,7 +388,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
} }
if (httpError) tooltipLines.push(`Error: ${httpError}`); if (httpError) tooltipLines.push(`Error: ${httpError}`);
// Qualification styling - gold shield + geo for qualified // Qualification styling - gold shield + geo for qualified (requires geo)
if (isQualified) { if (isQualified) {
return ( return (
<div className="flex flex-col gap-1" title={tooltipLines.join('\n')}> <div className="flex flex-col gap-1" title={tooltipLines.join('\n')}>
@@ -393,9 +396,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-amber-500" /> <ShieldCheck className="w-5 h-5 text-amber-500" />
<span className="font-semibold text-gray-800"> <span className="font-semibold text-gray-800">
{geoCity && geoState ? `${geoCity}, ${geoState}` : {geoCity && geoState ? `${geoCity}, ${geoState}` : geoState}
geoState ? geoState :
'No geo assigned'}
</span> </span>
</div> </div>
{/* IP address */} {/* IP address */}
@@ -414,7 +415,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
)} )}
{/* Antidetect status */} {/* Antidetect status */}
<div className="flex items-center gap-1 text-xs"> <div className="flex items-center gap-1 text-xs">
<Shield className="w-3 h-3 text-emerald-500" /> <VenetianMask className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-600">Antidetect OK</span> <span className="text-emerald-600">Antidetect OK</span>
{httpMs && <span className="text-gray-400">({httpMs}ms)</span>} {httpMs && <span className="text-gray-400">({httpMs}ms)</span>}
</div> </div>
@@ -422,7 +423,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
); );
} }
// Not qualified - show failure state // Preflight failed
if (httpStatus === 'failed') { if (httpStatus === 'failed') {
return ( return (
<div className="flex flex-col gap-1" title={tooltipLines.join('\n')}> <div className="flex flex-col gap-1" title={tooltipLines.join('\n')}>
@@ -437,6 +438,27 @@ function PreflightSummary({ worker }: { worker: Worker }) {
); );
} }
// Preflight passed but NO GEO - not qualified
if (httpStatus === 'passed' && !hasGeo) {
return (
<div className="flex flex-col gap-1" title={tooltipLines.join('\n')}>
<div className="inline-flex items-center gap-1.5 px-2 py-1 rounded-lg bg-orange-100 border border-orange-300">
<MapPin className="w-4 h-4 text-orange-600" />
<span className="text-xs font-bold text-orange-700">NO GEO</span>
</div>
{httpIp && (
<div className="flex items-center gap-1 text-xs text-gray-600">
<Globe className="w-3 h-3 text-blue-500" />
<span className="font-mono">{httpIp}</span>
</div>
)}
<div className="text-xs text-orange-600">
Preflight OK, awaiting geo
</div>
</div>
);
}
// Pending state // Pending state
return ( return (
<div className="flex flex-col gap-1" title={tooltipLines.join('\n')}> <div className="flex flex-col gap-1" title={tooltipLines.join('\n')}>
@@ -1405,18 +1427,39 @@ export function WorkersDashboard() {
worker.health_status === 'stale' ? 'bg-yellow-500' : worker.health_status === 'stale' ? 'bg-yellow-500' :
worker.health_status === 'busy' ? 'bg-blue-500' : worker.health_status === 'busy' ? 'bg-blue-500' :
'bg-emerald-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.friendly_name?.charAt(0) || '?'}
{worker.decommission_requested && ( {worker.decommission_requested && (
<div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center"> <div className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
<PowerOff className="w-2.5 h-2.5 text-white" /> <PowerOff className="w-2.5 h-2.5 text-white" />
</div> </div>
)} )}
{worker.is_qualified && !worker.decommission_requested && ( {!worker.decommission_requested && (() => {
<div className="absolute -top-1 -right-1 w-4 h-4 bg-amber-400 rounded-full flex items-center justify-center"> const hasGeo = worker.current_state || worker.fingerprint_data?.detectedLocation?.region;
<ShieldCheck className="w-2.5 h-2.5 text-amber-800" /> const preflightPassed = worker.is_qualified || worker.preflight_http_status === 'passed';
</div> if (preflightPassed && hasGeo) {
)} // Qualified - gold shield with check
return (
<div className="absolute -top-1 -right-1 w-4 h-4 bg-amber-400 rounded-full flex items-center justify-center">
<ShieldCheck className="w-2.5 h-2.5 text-amber-800" />
</div>
);
} else if (preflightPassed && !hasGeo) {
// Preflight OK but no geo - orange X
return (
<div className="absolute -top-1 -right-1 w-4 h-4 bg-orange-400 rounded-full flex items-center justify-center">
<ShieldX className="w-2.5 h-2.5 text-orange-800" />
</div>
);
}
return null;
})()}
</div> </div>
<div> <div>
<p className="font-medium text-gray-900 flex items-center gap-1.5"> <p className="font-medium text-gray-900 flex items-center gap-1.5">