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:
12
CLAUDE.md
12
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<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">
|
||||
<ShieldCheck className="w-5 h-5 text-amber-500" />
|
||||
<span className="font-semibold text-gray-800">
|
||||
{geoCity && geoState ? `${geoCity}, ${geoState}` :
|
||||
geoState ? geoState :
|
||||
'No geo assigned'}
|
||||
{geoCity && geoState ? `${geoCity}, ${geoState}` : geoState}
|
||||
</span>
|
||||
</div>
|
||||
{/* IP address */}
|
||||
@@ -414,7 +415,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
|
||||
)}
|
||||
{/* Antidetect status */}
|
||||
<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>
|
||||
{httpMs && <span className="text-gray-400">({httpMs}ms)</span>}
|
||||
</div>
|
||||
@@ -422,7 +423,7 @@ function PreflightSummary({ worker }: { worker: Worker }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Not qualified - show failure state
|
||||
// Preflight failed
|
||||
if (httpStatus === 'failed') {
|
||||
return (
|
||||
<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
|
||||
return (
|
||||
<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 === '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 && (
|
||||
<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" />
|
||||
</div>
|
||||
)}
|
||||
{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 (
|
||||
<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>
|
||||
<p className="font-medium text-gray-900 flex items-center gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user