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.
|
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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user