## SEO Template Library - Add complete template library with 7 page types (state, city, category, brand, product, search, regeneration) - Add Template Library tab in SEO Orchestrator with accordion-based editors - Add template preview, validation, and variable injection engine - Add API endpoints: /api/seo/templates, preview, validate, generate, regenerate ## Discovery Pipeline - Add promotion.ts for discovery location validation and promotion - Add discover-all-states.ts script for multi-state discovery - Add promotion log migration (067) - Enhance discovery routes and types ## Orchestrator & Admin - Add crawl_enabled filter to stores page - Add API permissions page - Add job queue management - Add price analytics routes - Add markets and intelligence routes - Enhance dashboard and worker monitoring ## Infrastructure - Add migrations for worker definitions, SEO settings, field alignment - Add canonical pipeline for scraper v2 - Update hydration and sync orchestrator - Enhance multi-state query service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
190 lines
5.5 KiB
TypeScript
Executable File
190 lines
5.5 KiB
TypeScript
Executable File
import { Router } from 'express';
|
|
import { authMiddleware, requireRole } from '../auth/middleware';
|
|
import { pool } from '../db/pool';
|
|
import { restartScheduler } from '../services/scheduler';
|
|
|
|
const router = Router();
|
|
router.use(authMiddleware);
|
|
|
|
// Get all settings
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT key, value, description, updated_at
|
|
FROM settings
|
|
ORDER BY key
|
|
`);
|
|
|
|
res.json({ settings: result.rows });
|
|
} catch (error) {
|
|
console.error('Error fetching settings:', error);
|
|
res.status(500).json({ error: 'Failed to fetch settings' });
|
|
}
|
|
});
|
|
|
|
// Get single setting
|
|
router.get('/:key', async (req, res) => {
|
|
try {
|
|
const { key } = req.params;
|
|
|
|
const result = await pool.query(`
|
|
SELECT key, value, description, updated_at
|
|
FROM settings
|
|
WHERE key = $1
|
|
`, [key]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Setting not found' });
|
|
}
|
|
|
|
res.json({ setting: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Error fetching setting:', error);
|
|
res.status(500).json({ error: 'Failed to fetch setting' });
|
|
}
|
|
});
|
|
|
|
// Update setting
|
|
router.put('/:key', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { key } = req.params;
|
|
const { value } = req.body;
|
|
|
|
if (value === undefined) {
|
|
return res.status(400).json({ error: 'Value required' });
|
|
}
|
|
|
|
const result = await pool.query(`
|
|
UPDATE settings
|
|
SET value = $1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE key = $2
|
|
RETURNING *
|
|
`, [value, key]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Setting not found' });
|
|
}
|
|
|
|
// Restart scheduler if scrape settings changed
|
|
if (key === 'scrape_interval_hours' || key === 'scrape_specials_time') {
|
|
console.log('Restarting scheduler due to setting change...');
|
|
await restartScheduler();
|
|
}
|
|
|
|
res.json({ setting: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('Error updating setting:', error);
|
|
res.status(500).json({ error: 'Failed to update setting' });
|
|
}
|
|
});
|
|
|
|
// Test AI provider connection
|
|
router.post('/test-ai', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { provider, apiKey } = req.body;
|
|
|
|
if (!provider || !apiKey) {
|
|
return res.status(400).json({ success: false, error: 'Provider and API key required' });
|
|
}
|
|
|
|
if (provider === 'anthropic') {
|
|
// Test Anthropic API
|
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': apiKey,
|
|
'anthropic-version': '2023-06-01'
|
|
},
|
|
body: JSON.stringify({
|
|
model: 'claude-3-haiku-20240307',
|
|
max_tokens: 10,
|
|
messages: [{ role: 'user', content: 'Hi' }]
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
res.json({ success: true, model: 'claude-3-haiku-20240307' });
|
|
} else {
|
|
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
|
res.json({ success: false, error: error.error?.message || 'Invalid API key' });
|
|
}
|
|
} else if (provider === 'openai') {
|
|
// Test OpenAI API
|
|
const response = await fetch('https://api.openai.com/v1/models', {
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
res.json({ success: true, model: 'gpt-4' });
|
|
} else {
|
|
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
|
|
res.json({ success: false, error: error.error?.message || 'Invalid API key' });
|
|
}
|
|
} else {
|
|
res.status(400).json({ success: false, error: 'Unknown provider' });
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error testing AI connection:', error);
|
|
res.json({ success: false, error: error.message || 'Connection failed' });
|
|
}
|
|
});
|
|
|
|
// Update multiple settings at once
|
|
router.put('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
|
try {
|
|
const { settings } = req.body;
|
|
|
|
if (!settings || !Array.isArray(settings)) {
|
|
return res.status(400).json({ error: 'Settings array required' });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
const updated = [];
|
|
let needsSchedulerRestart = false;
|
|
|
|
for (const setting of settings) {
|
|
const result = await client.query(`
|
|
UPDATE settings
|
|
SET value = $1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE key = $2
|
|
RETURNING *
|
|
`, [setting.value, setting.key]);
|
|
|
|
if (result.rows.length > 0) {
|
|
updated.push(result.rows[0]);
|
|
|
|
if (setting.key === 'scrape_interval_hours' || setting.key === 'scrape_specials_time') {
|
|
needsSchedulerRestart = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
|
|
if (needsSchedulerRestart) {
|
|
console.log('Restarting scheduler due to setting changes...');
|
|
await restartScheduler();
|
|
}
|
|
|
|
res.json({ settings: updated });
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating settings:', error);
|
|
res.status(500).json({ error: 'Failed to update settings' });
|
|
}
|
|
});
|
|
|
|
export default router;
|