Files
cannaiq/backend/src/routes/settings.ts
Kelly 2f483b3084 feat: SEO template library, discovery pipeline, and orchestrator enhancements
## 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>
2025-12-09 00:05:34 -07:00

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;