refactor: Move worker scaling to Workers page with password confirmation

- Worker scaling controls now on /workers page only (removed from /tasks)
- Password confirmation required before scaling
- Show only git SHA in header (removed version number)

🤖 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-11 09:41:58 -07:00
parent 1eaf95c06b
commit 529c447413
2 changed files with 36 additions and 96 deletions

View File

@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import { api } from '../lib/api';
import { Layout } from '../components/Layout';
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
import {
ListChecks,
Clock,
@@ -17,9 +16,6 @@ import {
Users,
Play,
Square,
Plus,
Minus,
Server,
} from 'lucide-react';
interface Task {
@@ -144,16 +140,6 @@ export default function TasksDashboard() {
const [poolPaused, setPoolPaused] = useState(false);
const [poolLoading, setPoolLoading] = useState(false);
// K8s worker state
const [k8sAvailable, setK8sAvailable] = useState(false);
const [workerReplicas, setWorkerReplicas] = useState(0);
const [workerReady, setWorkerReady] = useState(0);
const [scalingWorkers, setScalingWorkers] = useState(false);
// Password confirmation for scaling
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingScaleDelta, setPendingScaleDelta] = useState(0);
// Filters
const [roleFilter, setRoleFilter] = useState<string>('');
const [statusFilter, setStatusFilter] = useState<string>('');
@@ -163,7 +149,7 @@ export default function TasksDashboard() {
const fetchData = async () => {
try {
const [tasksRes, countsRes, capacityRes, poolStatus, k8sRes] = await Promise.all([
const [tasksRes, countsRes, capacityRes, poolStatus] = await Promise.all([
api.getTasks({
role: roleFilter || undefined,
status: statusFilter || undefined,
@@ -172,16 +158,12 @@ export default function TasksDashboard() {
api.getTaskCounts(),
api.getTaskCapacity(),
api.getTaskPoolStatus(),
api.getK8sWorkers(),
]);
setTasks(tasksRes.tasks || []);
setCounts(countsRes);
setCapacity(capacityRes.metrics || []);
setPoolPaused(poolStatus.paused);
setK8sAvailable(k8sRes.available);
setWorkerReplicas(k8sRes.replicas);
setWorkerReady(k8sRes.readyReplicas);
setError(null);
} catch (err: any) {
setError(err.message || 'Failed to load tasks');
@@ -190,35 +172,6 @@ export default function TasksDashboard() {
}
};
// Request to scale workers - shows confirmation modal first
const requestScaleWorkers = (delta: number) => {
const newReplicas = Math.max(0, Math.min(50, workerReplicas + delta));
if (newReplicas === workerReplicas) return;
setPendingScaleDelta(delta);
setShowConfirmModal(true);
};
// Actually scale workers after password confirmation
const executeScaleWorkers = async () => {
const newReplicas = Math.max(0, Math.min(50, workerReplicas + pendingScaleDelta));
setScalingWorkers(true);
try {
const res = await api.scaleK8sWorkers(newReplicas);
if (res.success) {
setWorkerReplicas(res.replicas);
} else {
setError(res.error || 'Failed to scale workers');
}
} catch (err: any) {
setError(err.message || 'Failed to scale workers');
} finally {
setScalingWorkers(false);
setPendingScaleDelta(0);
}
};
const togglePool = async () => {
setPoolLoading(true);
try {
@@ -285,34 +238,8 @@ export default function TasksDashboard() {
</div>
<div className="flex items-center gap-4">
{/* Worker Scaling */}
{k8sAvailable && (
<div className="flex items-center gap-2 px-3 py-2 bg-gray-100 rounded-lg">
<Server className="w-4 h-4 text-gray-500" />
<button
onClick={() => requestScaleWorkers(-5)}
disabled={scalingWorkers || workerReplicas === 0}
className="p-1 hover:bg-gray-200 rounded disabled:opacity-50"
title="Remove 5 workers"
>
<Minus className="w-4 h-4" />
</button>
<span className={`min-w-[60px] text-center font-mono ${scalingWorkers ? 'animate-pulse' : ''}`}>
{workerReady}/{workerReplicas}
</span>
<button
onClick={() => requestScaleWorkers(5)}
disabled={scalingWorkers || workerReplicas >= 50}
className="p-1 hover:bg-gray-200 rounded disabled:opacity-50"
title="Add 5 workers"
>
<Plus className="w-4 h-4" />
</button>
</div>
)}
{/* Pool Toggle */}
<button
<button
onClick={togglePool}
disabled={poolLoading}
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
@@ -593,18 +520,6 @@ export default function TasksDashboard() {
</div>
</div>
</div>
{/* Password Confirmation Modal for Worker Scaling */}
<PasswordConfirmModal
isOpen={showConfirmModal}
onClose={() => {
setShowConfirmModal(false);
setPendingScaleDelta(0);
}}
onConfirm={executeScaleWorkers}
title="Confirm Worker Scaling"
description={`You are about to ${pendingScaleDelta > 0 ? 'add' : 'remove'} ${Math.abs(pendingScaleDelta)} workers (${workerReplicas}${Math.max(0, Math.min(50, workerReplicas + pendingScaleDelta))}). This action affects production infrastructure.`}
/>
</Layout>
);
}

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { Layout } from '../components/Layout';
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
import { api } from '../lib/api';
import {
Users,
@@ -232,6 +233,10 @@ export function WorkersDashboard() {
const [scaling, setScaling] = useState(false);
const [targetReplicas, setTargetReplicas] = useState<number | null>(null);
// Password confirmation for scaling
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingReplicas, setPendingReplicas] = useState<number | null>(null);
// Pagination
const [page, setPage] = useState(0);
const workersPerPage = 15;
@@ -254,14 +259,21 @@ export function WorkersDashboard() {
}
}, [targetReplicas]);
// Scale workers (added 2024-12-10)
const handleScale = useCallback(async (replicas: number) => {
// Request scale - shows confirmation modal first
const requestScale = useCallback((replicas: number) => {
if (replicas < 0 || replicas > 20) return;
setPendingReplicas(replicas);
setShowConfirmModal(true);
}, []);
// Execute scale after password confirmation
const executeScale = useCallback(async () => {
if (pendingReplicas === null) return;
setScaling(true);
try {
const res = await api.post('/api/workers/k8s/scale', { replicas });
const res = await api.post('/api/workers/k8s/scale', { replicas: pendingReplicas });
if (res.data.success) {
setTargetReplicas(replicas);
setTargetReplicas(pendingReplicas);
// Refresh after a short delay to see the change
setTimeout(fetchK8sReplicas, 1000);
}
@@ -270,8 +282,9 @@ export function WorkersDashboard() {
setK8sError(err.response?.data?.error || 'Failed to scale');
} finally {
setScaling(false);
setPendingReplicas(null);
}
}, [fetchK8sReplicas]);
}, [fetchK8sReplicas, pendingReplicas]);
const fetchData = useCallback(async () => {
try {
@@ -389,7 +402,7 @@ export function WorkersDashboard() {
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) - 1)}
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) - 1)}
disabled={scaling || (targetReplicas || k8sReplicas.desired) <= 0}
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Scale down"
@@ -404,18 +417,18 @@ export function WorkersDashboard() {
onChange={(e) => setTargetReplicas(Math.max(0, Math.min(20, parseInt(e.target.value) || 0)))}
onBlur={() => {
if (targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
handleScale(targetReplicas);
requestScale(targetReplicas);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
handleScale(targetReplicas);
requestScale(targetReplicas);
}
}}
className="w-16 text-center border border-gray-300 rounded-lg px-2 py-1 text-lg font-semibold"
/>
<button
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) + 1)}
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) + 1)}
disabled={scaling || (targetReplicas || k8sReplicas.desired) >= 20}
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Scale up"
@@ -685,6 +698,18 @@ export function WorkersDashboard() {
)}
</div>
</div>
{/* Password Confirmation Modal for Scaling */}
<PasswordConfirmModal
isOpen={showConfirmModal}
onClose={() => {
setShowConfirmModal(false);
setPendingReplicas(null);
}}
onConfirm={executeScale}
title="Confirm Worker Scaling"
description={`You are about to scale workers to ${pendingReplicas} replicas${k8sReplicas ? ` (currently ${k8sReplicas.desired})` : ''}. This action affects production infrastructure.`}
/>
</Layout>
);
}