Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
+104
View File
@@ -1,5 +1,6 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const oddsRoutes = require('./routes/odds');
const analyzeRoutes = require('./routes/analyze');
const scanRoutes = require('./routes/scan');
@@ -7,13 +8,104 @@ const movementsRoutes = require('./routes/movements');
const alertsRoutes = require('./routes/alerts');
const betsRoutes = require('./routes/bets');
const stripeRoutes = require('./routes/stripe');
const statsRoutes = require('./routes/stats');
const propsRoutes = require('./routes/props');
const waitlistRoutes = require('./routes/waitlist');
const pipelineRoutes = require('./routes/pipeline');
const shareCardRoutes = require('./routes/shareCard');
const pushRoutes = require('./routes/push');
const gradingRoutes = require('./routes/grading');
const correctionRoutes = require('./routes/corrections');
const { missionHeader } = require('./middleware/mission');
const app = express();
// CORS — accept the Next.js frontend on Vercel/production and localhost dev.
// FRONTEND_ORIGINS overrides at deploy time (comma-separated).
const defaultOrigins = [
'http://localhost:3000',
'http://localhost:3001',
'https://vyndr.app',
'https://www.vyndr.app',
];
const envOrigins = (process.env.FRONTEND_ORIGINS || '').split(',').map((s) => s.trim()).filter(Boolean);
const allowedOrigins = [...new Set([...defaultOrigins, ...envOrigins])];
app.use(
cors({
origin(origin, cb) {
// Allow same-origin (no Origin header) and the configured allowlist.
// Also allow any *.vercel.app preview for staging.
if (!origin) return cb(null, true);
if (allowedOrigins.includes(origin)) return cb(null, true);
if (/\.vercel\.app$/.test(new URL(origin).hostname)) return cb(null, true);
return cb(new Error(`Origin ${origin} not allowed`));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
})
);
// Mission header on all responses
app.use(missionHeader);
// Stripe webhook needs raw body — must be before express.json()
app.use('/api/stripe/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
// Health check — public minimal status (Coolify, uptime monitors). Detailed
// adapter + Python service status only with X-VYNDR-Internal-Key.
app.get('/api/health', async (req, res) => {
const checks = {};
try {
const { getRedisClient, isDegraded } = require('./utils/redis');
if (isDegraded()) throw new Error('degraded');
await getRedisClient().ping();
checks.redis = 'ok';
} catch { checks.redis = 'down'; }
try {
const { getSupabaseServiceClient } = require('./utils/supabase');
const { error } = await getSupabaseServiceClient().from('users').select('id').limit(1);
checks.supabase = error ? 'error' : 'ok';
} catch { checks.supabase = 'down'; }
const healthy = checks.redis === 'ok' && checks.supabase === 'ok';
const expectedKey = process.env.VYNDR_INTERNAL_KEY;
const providedKey = req.headers['x-vyndr-internal-key'];
if (expectedKey && providedKey === expectedKey) {
try {
const axios = require('axios');
const pyUrl = process.env.PYTHON_SERVICE_URL || 'http://localhost:8000';
await axios.get(`${pyUrl}/health`, { timeout: 3_000 });
checks.python = 'ok';
} catch { checks.python = 'down'; }
checks.adapters = {
sharpapi: require('./services/adapters/sharpApiAdapter').configured(),
propodds: require('./services/adapters/propOddsAdapter').configured(),
parlayapi: require('./services/adapters/parlayApiAdapter').configured(),
oddspapi: require('./services/adapters/oddsPapiAdapter').configured(),
cfbd: require('./services/adapters/cfbdAdapter').configured(),
openrouter: require('./services/adapters/openRouterAdapter').configured(),
};
checks.engine2_enabled = process.env.ENGINE2_ENABLED === 'true';
return res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'degraded',
checks,
version: require('../package.json').version || '1.0.0',
uptime: Math.floor(process.uptime()),
});
}
return res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'degraded',
});
});
app.use('/api/odds', oddsRoutes);
app.use('/api/analyze', analyzeRoutes);
app.use('/api/scan', scanRoutes);
@@ -21,5 +113,17 @@ app.use('/api/movements', movementsRoutes);
app.use('/api/alerts', alertsRoutes);
app.use('/api/bets', betsRoutes);
app.use('/api/stripe', stripeRoutes);
app.use('/api/stats', statsRoutes);
app.use('/api/props', propsRoutes);
app.use('/api/waitlist', waitlistRoutes);
app.use('/api/pipeline', pipelineRoutes);
app.use('/api/share-card', shareCardRoutes);
app.use('/api/push', pushRoutes);
// Resolution payloads carry full ESPN box scores (50-100KB). Scope a larger
// limit to /api/grading only so the other routes keep the safer 100KB default.
app.use('/api/grading', express.json({ limit: '2mb' }), gradingRoutes);
app.use('/api/grading', express.json({ limit: '256kb' }), correctionRoutes);
const widgetRoutes = require('./routes/widget');
app.use('/api/widget', widgetRoutes);
module.exports = app;