feat: Auto-migrations on startup, worker exit location, proxy improvements

- Add auto-migration system that runs SQL files from migrations/ on server startup
- Track applied migrations in schema_migrations table
- Show proxy exit location in Workers dashboard
- Add "Cleanup Stale" button to remove old workers
- Add remove button for individual workers
- Include proxy location (city, state, country) in worker heartbeats
- Update Proxy interface with location fields
- Re-enable bulk proxy import without ON CONFLICT

🤖 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-10 01:42:00 -07:00
parent 9a9c2f76a2
commit 6eb1babc86
9 changed files with 598 additions and 85 deletions

View File

@@ -2,7 +2,7 @@ import { Router } from 'express';
import { authMiddleware, requireRole } from '../auth/middleware';
import { pool } from '../db/pool';
import { testProxy, addProxy, addProxiesFromList } from '../services/proxy';
import { createProxyTestJob, getProxyTestJob, getActiveProxyTestJob, cancelProxyTestJob } from '../services/proxyTestQueue';
import { createProxyTestJob, getProxyTestJob, getActiveProxyTestJob, cancelProxyTestJob, ProxyTestMode } from '../services/proxyTestQueue';
const router = Router();
router.use(authMiddleware);
@@ -11,9 +11,10 @@ router.use(authMiddleware);
router.get('/', async (req, res) => {
try {
const result = await pool.query(`
SELECT id, host, port, protocol, active, is_anonymous,
SELECT id, host, port, protocol, username, password, active, is_anonymous,
last_tested_at, test_result, response_time_ms, created_at,
city, state, country, country_code, location_updated_at
city, state, country, country_code, location_updated_at,
COALESCE(max_connections, 1) as max_connections
FROM proxies
ORDER BY created_at DESC
`);
@@ -166,13 +167,39 @@ router.post('/:id/test', requireRole('superadmin', 'admin'), async (req, res) =>
});
// Start proxy test job
// Query params: mode=all|failed|inactive, concurrency=10
router.post('/test-all', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const jobId = await createProxyTestJob();
res.json({ jobId, message: 'Proxy test job started' });
} catch (error) {
const mode = (req.query.mode as ProxyTestMode) || 'all';
const concurrency = parseInt(req.query.concurrency as string) || 10;
// Validate mode
if (!['all', 'failed', 'inactive'].includes(mode)) {
return res.status(400).json({ error: 'Invalid mode. Use: all, failed, or inactive' });
}
// Validate concurrency (1-50)
if (concurrency < 1 || concurrency > 50) {
return res.status(400).json({ error: 'Concurrency must be between 1 and 50' });
}
const jobId = await createProxyTestJob(mode, concurrency);
res.json({ jobId, mode, concurrency, message: `Proxy test job started (mode: ${mode}, concurrency: ${concurrency})` });
} catch (error: any) {
console.error('Error starting proxy test job:', error);
res.status(500).json({ error: 'Failed to start proxy test job' });
res.status(500).json({ error: error.message || 'Failed to start proxy test job' });
}
});
// Convenience endpoint: Test only failed proxies
router.post('/test-failed', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const concurrency = parseInt(req.query.concurrency as string) || 10;
const jobId = await createProxyTestJob('failed', concurrency);
res.json({ jobId, mode: 'failed', concurrency, message: 'Retesting failed proxies...' });
} catch (error: any) {
console.error('Error starting failed proxy test:', error);
res.status(500).json({ error: error.message || 'Failed to start proxy test job' });
}
});
@@ -197,8 +224,8 @@ router.post('/test-job/:jobId/cancel', requireRole('superadmin', 'admin'), async
router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { id } = req.params;
const { host, port, protocol, username, password, active } = req.body;
const { host, port, protocol, username, password, active, max_connections } = req.body;
const result = await pool.query(`
UPDATE proxies
SET host = COALESCE($1, host),
@@ -207,10 +234,11 @@ router.put('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
username = COALESCE($4, username),
password = COALESCE($5, password),
active = COALESCE($6, active),
max_connections = COALESCE($7, max_connections),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
WHERE id = $8
RETURNING *
`, [host, port, protocol, username, password, active, id]);
`, [host, port, protocol, username, password, active, max_connections, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Proxy not found' });