iteration 1 complete

This commit is contained in:
2026-01-27 14:42:26 -05:00
parent db35d8bd07
commit a6d445f36a
3 changed files with 172 additions and 13 deletions

29
env-sample Normal file
View File

@@ -0,0 +1,29 @@
# Database
DATABASE_URL=postgresql://postgres:changeme@postgres:5432/privacy_analyzer
# Redis
REDIS_URL=redis://redis:6379
# Meilisearch
MEILISEARCH_URL=http://meilisearch:7700
MEILISEARCH_API_KEY=your_secure_master_key_here
# AI Provider Configuration
# Option 1: Ollama (Local LLM - DEFAULT, no API costs!)
USE_OLLAMA=true
OLLAMA_URL=http://ollama:11434
OLLAMA_MODEL=gpt-oss:latest
# Option 2: OpenAI (Cloud - optional fallback)
# Set these if you want OpenAI as fallback when Ollama is unavailable
OPENAI_API_KEY=sk-proj-
OPENAI_MODEL=gpt-4o-mini
# Admin Credentials (change these!)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=secure_password_here
SESSION_SECRET=your_random_session_secret_here
# App
PORT=3000
NODE_ENV=local

View File

@@ -151,18 +151,60 @@ async function handleRequest(req) {
// Search - GET /search // Search - GET /search
if (method === 'GET' && pathname === '/search') { if (method === 'GET' && pathname === '/search') {
const query = url.searchParams.get('q'); const query = url.searchParams.get('q');
const services = await Service.findAllWithLatestAnalysis();
try {
let services;
if (query && query.trim().length > 0) {
// Use Meilisearch for search
console.log(`Searching for: ${query}`);
const searchResults = await SearchIndexer.search(query, { limit: 50 });
services = searchResults.hits.map(hit => ({
id: hit.id,
name: hit.name,
url: hit.url,
logo_url: hit.logo_url,
grade: hit.grade,
overall_score: hit.overall_score,
findings: hit.findings,
last_analyzed: hit.last_analyzed,
created_at: hit.created_at
}));
} else {
// Show all services if no query
services = await Service.findAllWithLatestAnalysis();
}
const html = await renderTemplate('public/index', { const html = await renderTemplate('public/index', {
title: query ? `Search Results for "${query}" | Privacy Policy Analyzer` : 'Search Services', title: query ? `Search Results for "${query}" | Privacy Policy Analyzer` : 'Search Services',
description: 'Search for services and see their privacy grades.', description: 'Search for services and see their privacy grades.',
canonical: `${req.protocol}://${url.host}/search`, canonical: `${req.protocol}://${url.host}/search`,
services services,
query,
pagination: null // Search results don't use pagination
}); });
return new Response(html, { return new Response(html, {
headers: { 'Content-Type': 'text/html' } headers: { 'Content-Type': 'text/html' }
}); });
} catch (error) {
console.error('Search error:', error);
// Fallback to showing all services
const services = await Service.findAllWithLatestAnalysis();
const html = await renderTemplate('public/index', {
title: 'Search Services',
description: 'Search for services and see their privacy grades.',
canonical: `${req.protocol}://${url.host}/search`,
services,
error: 'Search temporarily unavailable',
pagination: null
});
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
}
} }
// Admin login page - GET /admin/login // Admin login page - GET /admin/login
@@ -201,7 +243,7 @@ async function handleRequest(req) {
const headers = new Headers(); const headers = new Headers();
headers.set('Content-Type', 'application/json'); headers.set('Content-Type', 'application/json');
headers.set('Set-Cookie', `session_token=${session.session_token}; HttpOnly; Path=/; Max-Age=${24 * 60 * 60}`); headers.set('Set-Cookie', `session_token=${session.session_token}; HttpOnly; Path=/; Max-Age=${24 * 60 * 60}; SameSite=Lax`);
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: true, success: true,
@@ -249,6 +291,9 @@ async function handleRequest(req) {
return new Response(null, { status: 302, headers }); return new Response(null, { status: 302, headers });
} }
// Extend session on each request
await AdminSession.extendSession(sessionToken);
// Clean up expired sessions // Clean up expired sessions
await AdminSession.deleteExpired(); await AdminSession.deleteExpired();
@@ -354,9 +399,17 @@ async function handleRequest(req) {
return new Response('Name, URL, and policy URL are required', { status: 400 }); return new Response('Name, URL, and policy URL are required', { status: 400 });
} }
await Service.create(data); const service = await Service.create(data);
console.log('Service created successfully'); console.log('Service created successfully');
// Index in Meilisearch
try {
await SearchIndexer.indexService(service);
console.log('Service indexed in Meilisearch');
} catch (indexError) {
console.error('Failed to index service:', indexError);
}
// Invalidate homepage cache // Invalidate homepage cache
await PageCache.invalidateHomepage(); await PageCache.invalidateHomepage();
@@ -436,6 +489,15 @@ async function handleRequest(req) {
await Service.update(id, data); await Service.update(id, data);
console.log('Service updated successfully'); console.log('Service updated successfully');
// Re-index in Meilisearch
try {
const updatedService = await Service.findById(id);
await SearchIndexer.indexService(updatedService);
console.log('Service re-indexed in Meilisearch');
} catch (indexError) {
console.error('Failed to re-index service:', indexError);
}
// Invalidate caches // Invalidate caches
await PageCache.invalidateHomepage(); await PageCache.invalidateHomepage();
await PageCache.invalidateService(id); await PageCache.invalidateService(id);
@@ -467,6 +529,14 @@ async function handleRequest(req) {
const id = parseInt(match[1]); const id = parseInt(match[1]);
await Service.delete(id); await Service.delete(id);
// Delete from Meilisearch
try {
await SearchIndexer.deleteService(id);
console.log('Service deleted from Meilisearch');
} catch (indexError) {
console.error('Failed to delete service from index:', indexError);
}
// Invalidate caches // Invalidate caches
await PageCache.invalidateHomepage(); await PageCache.invalidateHomepage();
await PageCache.invalidateService(id); await PageCache.invalidateService(id);
@@ -580,6 +650,36 @@ async function handleRequest(req) {
} }
} }
// Admin reindex all services - POST /api/admin/reindex
if (method === 'POST' && pathname === '/api/admin/reindex') {
const sessionToken = req.cookies?.session_token;
if (!sessionToken || !(await AdminSession.findByToken(sessionToken))) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
try {
console.log('Starting bulk reindex...');
const result = await SearchIndexer.indexAll();
return new Response(JSON.stringify({
success: true,
message: `Indexed ${result.indexed} services`,
indexed: result.indexed
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Reindex error:', error);
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
// Sitemap.xml - GET /sitemap.xml // Sitemap.xml - GET /sitemap.xml
if (method === 'GET' && pathname === '/sitemap.xml') { if (method === 'GET' && pathname === '/sitemap.xml') {
try { try {
@@ -684,6 +784,15 @@ console.log(`Server running at http://localhost:${server.port}`);
await SearchIndexer.init(); await SearchIndexer.init();
console.log('Search indexer initialized'); console.log('Search indexer initialized');
// Index all existing services on startup
try {
console.log('Indexing all services on startup...');
const result = await SearchIndexer.indexAll();
console.log(`Indexed ${result.indexed} services on startup`);
} catch (indexError) {
console.error('Startup indexing error:', indexError.message);
}
// Start analysis worker // Start analysis worker
AnalysisWorker.start(); AnalysisWorker.start();
console.log('Analysis worker started'); console.log('Analysis worker started');

View File

@@ -123,6 +123,27 @@ export class AnalysisWorker {
console.log(`[${jobId}] Analysis complete: Grade ${analysis.overall_score}`); console.log(`[${jobId}] Analysis complete: Grade ${analysis.overall_score}`);
// Index in Meilisearch
try {
const serviceWithAnalysis = {
id: serviceId,
name: service.name,
url: service.url,
logo_url: service.logo_url,
grade: analysis.overall_score,
overall_score: analysis.overall_score,
findings: analysisResult.findings,
data_types_collected: analysisResult.data_types_collected || [],
third_parties: analysisResult.third_parties || [],
last_analyzed: analysis.created_at,
created_at: service.created_at
};
await SearchIndexer.indexService(serviceWithAnalysis);
console.log(`[${jobId}] Service indexed in Meilisearch`);
} catch (indexError) {
console.error(`[${jobId}] Meilisearch indexing error:`, indexError.message);
}
// Invalidate caches // Invalidate caches
try { try {
await PageCache.invalidateHomepage(); await PageCache.invalidateHomepage();