initial wcag-ada

This commit is contained in:
Boki 2025-06-28 11:11:34 -04:00
parent 042b8cb83a
commit d52cfe7de2
112 changed files with 9069 additions and 0 deletions

View 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();
});
});
}
}
}

View 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');
});

View 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');
}
}