Rene Fichtmueller 141911537d feat: wire up the 13 orphaned Fastify routes via internal API server proxy
The 2026-07-14 backend refactor (PR #1) moved 13 features' route logic
out of server.js into src/features/*/routes.ts as Fastify plugins, and
left "// Migrated to src/features/X/" comments behind -- but never
actually built or started anything that served those plugins. Found a
complete, unused Fastify app bootstrap already sitting at
src/api/server.ts (added in an earlier commit, 5554c1a, alongside the
BGP hijack/PDF-export/ASPA-adoption work) that imports and registers
all 16 route modules, including 3 (pdf-export, aspa-adoption,
hijack-alerts) with proper Postgres-backed implementations, tests, and
PDF templates -- more capable than the puppeteer-based inline version
kept from the production snapshot. Also confirmed there is no code
anywhere that calls initializeDatabase() or startApiServer(): this
Fastify server had never actually been run, not even once.

What this commit does:

- Fixes 6 real TypeScript compile errors blocking `tsc` from building
  the affected modules cleanly (all pre-existing, none introduced by
  this branch):
  - src/routes/hijack-alerts.ts: route generic was missing `Body` in
    its type param, so it didn't match the handler's own declared type
  - src/features/aspa-adoption/scheduler.ts: node-cron v4's
    ScheduledTask dropped `.destroy()` (relevant now that this branch
    already bumped node-cron 3->4 for the uuid CVE fix) -- `.stop()`
    alone is sufficient
  - src/features/{bgp-communities,rpki-history}/routes.ts:
    `response.json()` typed as `{}` under this @types/node version,
    cast to `any` to match this file's existing loose-typing style
  - src/features/pdf-export/cache-manager.ts: `NodeJS.Timer` ->
    `NodeJS.Timeout` (what setInterval/clearInterval actually use)
  - src/features/pdf-export/renderer.ts: this uses Playwright, not
    Puppeteer -- `timeout` isn't a page.pdf() option in Playwright,
    moved to page.setDefaultTimeout() before the call

- Adds src/api/index.ts as the actual entry point (there wasn't one).
  Calls initializeDatabase() before dynamically importing ./server.js,
  because src/routes/hijack-alerts.ts calls getDatabase() at module
  load time -- a static top-level import of ./server would have
  crashed on startup before initializeDatabase() ever ran. Verified
  this boots cleanly and serves real responses (changelog-data,
  ix-matrix, rib/prefix graceful-503, bgp-communities with a live
  RIPE Stat call) in local testing.

- Wires server.js to reverse-proxy the 11 genuinely-orphaned paths
  (submarine-cables, global-infra, communities, irr-audit,
  asset-expand, rpki-history, aspath, looking-glass, ix-matrix,
  changelog-data, rib/*, prefix-changes) to this new internal server
  via a plain http.request() proxy -- chosen over exposing a second
  Cloudflare Tunnel ingress rule so this stays a same-origin,
  internal-only change with no DNS/tunnel config to touch. Verified
  the proxy mechanism itself against a live instance of the new server.
  Deliberately did NOT proxy /api/webhooks, /api/hijacks, or
  /api/hijack-subscribe -- server.js already serves those paths
  inline (file-backed, live, working), and src/routes/hijack-alerts.ts
  registers the same path names against an empty Postgres table.
  Proxying would have silently shadowed working data with nothing.

- Default port 3102, not 3100: checked Erik and found 3100 already
  bound by an unrelated docker-proxy container. 3102 is free and
  reads as "the second peercortex port" next to the existing 3101.

- Adds a `peercortex-api` PM2 app to ecosystem.config.js on Erik
  (backed up first via rollback-standard.sh) -- config only, NOT
  started. Nothing is deployed to Erik as part of this commit: no
  dist/ upload, no `npm install` for fastify/pg on Erik, no `pm2
  start`. server.js and public/index.html on Erik are untouched.
2026-07-16 11:54:33 +02:00

154 lines
4.7 KiB
TypeScript

import cron from 'node-cron'
import { ASNSampler } from './sampler'
import { ASPACollector } from './collector'
import { AdoptionAggregator } from './aggregator'
import { AdoptionForecaster } from './forecaster'
import { ASPADatabaseClient } from './db-client'
export class ASPAAdoptionScheduler {
private task: cron.ScheduledTask | null = null
private sampler = new ASNSampler()
private collector = new ASPACollector()
private aggregator = new AdoptionAggregator()
private forecaster = new AdoptionForecaster()
private dbClient = new ASPADatabaseClient()
private isRunning = false
/**
* Start the daily ASPA adoption sampling job
* Runs at 2 AM UTC every day (0 2 * * *)
*/
start(): void {
if (this.task) {
console.log('[ASPA Scheduler] Job already scheduled')
return
}
this.task = cron.schedule('0 2 * * *', () => {
if (!this.isRunning) {
this.runDailyJob().catch((error) => {
console.error('[ASPA Scheduler] Daily job failed:', error)
})
}
})
console.log('[ASPA Scheduler] Scheduled daily job at 2:00 AM UTC')
}
/**
* Stop the scheduled job
*/
stop(): void {
if (this.task) {
this.task.stop()
this.task = null
console.log('[ASPA Scheduler] Job stopped')
}
}
/**
* Manually trigger the daily job (useful for testing)
*/
async runManual(): Promise<void> {
if (this.isRunning) {
throw new Error('Job is already running')
}
await this.runDailyJob()
}
private async runDailyJob(): Promise<void> {
this.isRunning = true
const startTime = Date.now()
try {
console.log('[ASPA Scheduler] Starting daily adoption snapshot')
const sampleDate = new Date()
sampleDate.setUTCHours(0, 0, 0, 0)
// Step 1: Stratified sample of ASNs
console.log('[ASPA Scheduler] Sampling ASNs...')
const samplingResult = this.sampler.sample(750, {
stratifiedByRegion: true,
minASNsPerRegion: 100,
})
console.log(`[ASPA Scheduler] Sampled ${samplingResult.totalSampled} ASNs`)
// Step 2: Collect ASPA data for sampled ASNs
console.log('[ASPA Scheduler] Collecting ASPA data...')
const snapshots = await this.collector.collect(samplingResult.asns, {
maxRetries: 3,
timeoutMs: 5000,
rateLimit: 100,
})
console.log(`[ASPA Scheduler] Collected ASPA data for ${snapshots.length} ASNs`)
// Step 3: Aggregate by region and IXP
console.log('[ASPA Scheduler] Aggregating data by region and IXP...')
const { overall, regions, ixps } = this.aggregator.aggregate(snapshots, {
includeRegions: true,
includeIXPs: true,
})
console.log(
`[ASPA Scheduler] Aggregation complete: ${overall.withASPA}/${overall.total} ASNs with ASPA (${overall.coverage}%)`
)
// Step 4: Fetch historical data and forecast
console.log('[ASPA Scheduler] Calculating forecast...')
const history = await this.dbClient.getAdoptionHistory(90)
const timeSeriesData = history
.reverse()
.map((h) => ({
date: h.sampleDate,
coverage: h.coveragePercentage,
}))
const forecast = this.forecaster.forecast(timeSeriesData, {
historyDays: 90,
forecastMonths: 6,
regressionAlpha: 0.05,
})
console.log(`[ASPA Scheduler] Forecast: ${forecast.predictedCoverage6m}% coverage in 6 months (confidence: ${forecast.confidence})`)
// Step 5: Store results in database
console.log('[ASPA Scheduler] Storing results...')
const topAdopters = snapshots
.filter((s) => s.hasASPA)
.sort((a, b) => b.providers.length - a.providers.length)
.slice(0, 10)
.map((s) => ({
asn: s.asn,
name: `AS${s.asn}`,
providers: s.providers.length,
}))
const metadata = {
samplingMethod: 'stratified-regional',
forecastData: forecast,
dataCollectionTimeMs: Date.now() - startTime,
}
await this.dbClient.recordAdoptionSnapshot(sampleDate, overall.total, overall.withASPA, regions, topAdopters, metadata)
await this.dbClient.recordRegionalBreakdown(sampleDate, regions)
await this.dbClient.recordIXPBreakdown(sampleDate, ixps)
console.log(`[ASPA Scheduler] Daily job completed in ${Date.now() - startTime}ms`)
} catch (error) {
console.error('[ASPA Scheduler] Job failed:', error instanceof Error ? error.message : String(error))
throw error
} finally {
this.isRunning = false
}
}
isJobRunning(): boolean {
return this.isRunning
}
isScheduled(): boolean {
return this.task !== null
}
}
export const aspaScheduler = new ASPAAdoptionScheduler()