initial wcag-ada
This commit is contained in:
parent
042b8cb83a
commit
d52cfe7de2
112 changed files with 9069 additions and 0 deletions
90
apps/wcag-ada/worker/src/services/health.ts
Normal file
90
apps/wcag-ada/worker/src/services/health.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { createServer, Server } from 'http';
|
||||
import { Queue } from 'bullmq';
|
||||
import { getServiceConfig, getWorkerConfig } from '@wcag-ada/config';
|
||||
import { logger } from '../utils/logger';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { createRedisConnection } from '../utils/redis';
|
||||
|
||||
export class HealthService {
|
||||
private server: Server | null = null;
|
||||
private config = getServiceConfig('worker');
|
||||
private workerConfig = getWorkerConfig();
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.server = createServer(async (req, res) => {
|
||||
if (req.url === '/health' && req.method === 'GET') {
|
||||
const health = await this.getHealthStatus();
|
||||
res.writeHead(health.healthy ? 200 : 503, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(health));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(this.config.port, () => {
|
||||
logger.info(`Health server listening on port ${this.config.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async getHealthStatus() {
|
||||
const checks = {
|
||||
server: 'ok',
|
||||
database: 'unknown',
|
||||
redis: 'unknown',
|
||||
queue: 'unknown',
|
||||
};
|
||||
|
||||
// Check database
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
checks.database = 'ok';
|
||||
} catch (error) {
|
||||
checks.database = 'error';
|
||||
logger.error('Database health check failed', error);
|
||||
}
|
||||
|
||||
// Check Redis
|
||||
try {
|
||||
const redis = createRedisConnection();
|
||||
await redis.ping();
|
||||
await redis.quit();
|
||||
checks.redis = 'ok';
|
||||
} catch (error) {
|
||||
checks.redis = 'error';
|
||||
logger.error('Redis health check failed', error);
|
||||
}
|
||||
|
||||
// Check queue
|
||||
try {
|
||||
const queue = new Queue(this.workerConfig.queueName, {
|
||||
connection: createRedisConnection(),
|
||||
});
|
||||
await queue.getJobCounts();
|
||||
await queue.close();
|
||||
checks.queue = 'ok';
|
||||
} catch (error) {
|
||||
checks.queue = 'error';
|
||||
logger.error('Queue health check failed', error);
|
||||
}
|
||||
|
||||
const healthy = Object.values(checks).every(status => status === 'ok');
|
||||
|
||||
return {
|
||||
healthy,
|
||||
checks,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.server) {
|
||||
return new Promise((resolve) => {
|
||||
this.server!.close(() => {
|
||||
logger.info('Health server stopped');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
55
apps/wcag-ada/worker/src/services/prisma.ts
Normal file
55
apps/wcag-ada/worker/src/services/prisma.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('prisma');
|
||||
|
||||
// Create a singleton instance of PrismaClient
|
||||
export const prisma = new PrismaClient({
|
||||
log: [
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'query',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'error',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'info',
|
||||
},
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Log database events in development
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
prisma.$on('query', (e: any) => {
|
||||
logger.debug('Query', {
|
||||
query: e.query,
|
||||
params: e.params,
|
||||
duration: e.duration,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prisma.$on('error', (e: any) => {
|
||||
logger.error('Database error', { error: e });
|
||||
});
|
||||
|
||||
prisma.$on('info', (e: any) => {
|
||||
logger.info('Database info', { message: e.message });
|
||||
});
|
||||
|
||||
prisma.$on('warn', (e: any) => {
|
||||
logger.warn('Database warning', { message: e.message });
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('beforeExit', async () => {
|
||||
await prisma.$disconnect();
|
||||
logger.info('Prisma disconnected');
|
||||
});
|
||||
139
apps/wcag-ada/worker/src/services/scheduler.ts
Normal file
139
apps/wcag-ada/worker/src/services/scheduler.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import cron from 'node-cron';
|
||||
import { Queue } from 'bullmq';
|
||||
import { getWorkerConfig } from '@wcag-ada/config';
|
||||
import { logger } from '../utils/logger';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { createRedisConnection } from '../utils/redis';
|
||||
import type { AccessibilityScanOptions } from '@wcag-ada/shared';
|
||||
|
||||
export class SchedulerService {
|
||||
private tasks: cron.ScheduledTask[] = [];
|
||||
private scanQueue: Queue | null = null;
|
||||
private config = getWorkerConfig();
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Create queue instance
|
||||
this.scanQueue = new Queue(this.config.queueName, {
|
||||
connection: createRedisConnection(),
|
||||
});
|
||||
|
||||
// Schedule periodic check for websites that need scanning
|
||||
const task = cron.schedule('*/1 * * * *', async () => {
|
||||
await this.checkAndScheduleScans();
|
||||
}, {
|
||||
timezone: this.config.scheduler.timezone,
|
||||
});
|
||||
|
||||
this.tasks.push(task);
|
||||
task.start();
|
||||
|
||||
logger.info('Scheduler service started');
|
||||
}
|
||||
|
||||
private async checkAndScheduleScans(): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
// Find websites that need scanning based on their schedule
|
||||
const websites = await prisma.website.findMany({
|
||||
where: {
|
||||
active: true,
|
||||
scanSchedule: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const website of websites) {
|
||||
const schedule = website.scanSchedule as any;
|
||||
if (!schedule || schedule.frequency === 'manual') continue;
|
||||
|
||||
const shouldScan = this.shouldScanWebsite(website, schedule, now);
|
||||
|
||||
if (shouldScan) {
|
||||
await this.scheduleWebsiteScan(website);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking scheduled scans', error);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldScanWebsite(website: any, schedule: any, now: Date): boolean {
|
||||
if (!website.lastScanAt) return true;
|
||||
|
||||
const lastScan = new Date(website.lastScanAt);
|
||||
const timeSinceLastScan = now.getTime() - lastScan.getTime();
|
||||
|
||||
switch (schedule.frequency) {
|
||||
case 'hourly':
|
||||
return timeSinceLastScan >= 60 * 60 * 1000; // 1 hour
|
||||
case 'daily':
|
||||
return timeSinceLastScan >= 24 * 60 * 60 * 1000; // 24 hours
|
||||
case 'weekly':
|
||||
return timeSinceLastScan >= 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
case 'monthly':
|
||||
return timeSinceLastScan >= 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async scheduleWebsiteScan(website: any): Promise<void> {
|
||||
try {
|
||||
// Create scan job in database
|
||||
const job = await prisma.scanJob.create({
|
||||
data: {
|
||||
websiteId: website.id,
|
||||
userId: website.userId,
|
||||
url: website.url,
|
||||
options: {
|
||||
url: website.url,
|
||||
...(website.scanOptions as any),
|
||||
authenticate: website.authConfig as any,
|
||||
},
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
|
||||
// Queue the scan
|
||||
if (this.scanQueue) {
|
||||
await this.scanQueue.add('scan', {
|
||||
jobId: job.id,
|
||||
websiteId: website.id,
|
||||
userId: website.userId,
|
||||
options: job.options as AccessibilityScanOptions,
|
||||
}, {
|
||||
jobId: job.id,
|
||||
});
|
||||
|
||||
logger.info('Scheduled scan for website', {
|
||||
websiteId: website.id,
|
||||
jobId: job.id,
|
||||
url: website.url,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error scheduling website scan', {
|
||||
websiteId: website.id,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Stopping scheduler service...');
|
||||
|
||||
for (const task of this.tasks) {
|
||||
task.stop();
|
||||
}
|
||||
this.tasks = [];
|
||||
|
||||
if (this.scanQueue) {
|
||||
await this.scanQueue.close();
|
||||
this.scanQueue = null;
|
||||
}
|
||||
|
||||
logger.info('Scheduler service stopped');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue