2026-05-01 19:36:24 +00:00
// Boot-time background job runner. Started once from server.ts after Next.js prepare().
import { getDb , getSetting } from "./db" ;
import { checkDomainDns } from "./dns" ;
import { reloadCaddy } from "./caddy" ;
import { invalidateRedirectCache } from "./redirect-resolver" ;
2026-05-21 12:51:18 +00:00
import { applyUpdate , checkForUpdate } from "./updater" ;
2026-05-01 19:36:24 +00:00
let started = false ;
const timers : NodeJS.Timeout [ ] = [ ] ;
function schedule ( fn : ( ) = > void | Promise < void > , intervalMs : number , immediate = false ) {
if ( immediate ) Promise . resolve ( fn ( ) ) . catch ( ( e ) = > console . error ( "[job] error" , e ) ) ;
const t = setInterval ( ( ) = > {
Promise . resolve ( fn ( ) ) . catch ( ( e ) = > console . error ( "[job] error" , e ) ) ;
} , intervalMs ) ;
t . unref ? . ( ) ;
timers . push ( t ) ;
}
async function pruneHits() {
const days = Number ( getSetting ( "hits_retention_days" ) || 365 ) ;
if ( ! Number . isFinite ( days ) || days <= 0 ) return ;
const cutoff = Date . now ( ) - days * 24 * 60 * 60 * 1000 ;
const result = getDb ( ) . prepare ( "DELETE FROM hits WHERE ts < ?" ) . run ( cutoff ) ;
if ( result . changes > 0 ) {
console . log ( ` [job:hits-retention] pruned ${ result . changes } hits older than ${ days } d ` ) ;
}
}
async function pruneAuditLog() {
// keep last 5000 entries
getDb ( ) . exec ( ` DELETE FROM audit_log WHERE id NOT IN (SELECT id FROM audit_log ORDER BY id DESC LIMIT 5000) ` ) ;
}
async function dnsHealthCheck() {
const db = getDb ( ) ;
const rows = db . prepare ( "SELECT id, domain, include_www, status FROM domains WHERE status IN ('active','error')" ) . all ( ) as { id : number ; domain : string ; include_www : number ; status : string } [ ] ;
let changed = 0 ;
for ( const d of rows ) {
try {
const r = await checkDomainDns ( d . domain , ! ! d . include_www ) ;
if ( r . ok && d . status !== "active" ) {
db . prepare ( "UPDATE domains SET status='active', verified_at=? WHERE id=?" ) . run ( Date . now ( ) , d . id ) ;
changed ++ ;
} else if ( ! r . ok && d . status === "active" ) {
db . prepare ( "UPDATE domains SET status='error' WHERE id=?" ) . run ( d . id ) ;
changed ++ ;
}
} catch {
// ignore individual lookup failures
}
}
if ( changed > 0 ) {
invalidateRedirectCache ( ) ;
reloadCaddy ( ) . catch ( ( ) = > { } ) ;
console . log ( ` [job:dns-health] status changed for ${ changed } domains ` ) ;
}
}
async function pruneIpBlocklist() {
// Auto-expire entries older than 24h
getDb ( ) . prepare ( "DELETE FROM ip_blocklist WHERE expires_at < ?" ) . run ( Date . now ( ) ) ;
}
2026-05-21 12:51:18 +00:00
async function runAutoUpdate() {
if ( getSetting ( "update_auto" ) !== "true" ) return ;
const status = await checkForUpdate ( ) ;
if ( ! status . update_available ) return ;
console . log ( ` [job:auto-update] update available ( ${ status . current } → ${ status . latest } ), applying ` ) ;
const result = await applyUpdate ( ) ;
if ( result . ok ) {
console . log ( ` [job:auto-update] applied ${ result . from } → ${ result . to } ` ) ;
} else {
console . error ( ` [job:auto-update] failed: ${ result . error } ` ) ;
}
}
2026-05-01 19:36:24 +00:00
export function startJobs() {
if ( started ) return ;
started = true ;
const HOUR = 60 * 60 * 1000 ;
const DAY = 24 * HOUR ;
// Hits retention: daily
schedule ( pruneHits , DAY ) ;
// Audit-log retention: daily
schedule ( pruneAuditLog , DAY ) ;
// DNS health check: every 6h, first run after 5min boot to give DNS time
setTimeout ( ( ) = > {
dnsHealthCheck ( ) . catch ( ( ) = > { } ) ;
schedule ( dnsHealthCheck , 6 * HOUR ) ;
} , 5 * 60 * 1000 ) ;
// IP blocklist cleanup: hourly
schedule ( pruneIpBlocklist , HOUR ) ;
2026-05-21 12:51:18 +00:00
// Auto-update check: every 6h, first run 10min after boot
setTimeout ( ( ) = > {
runAutoUpdate ( ) . catch ( ( ) = > { } ) ;
schedule ( runAutoUpdate , 6 * HOUR ) ;
} , 10 * 60 * 1000 ) ;
2026-05-01 19:36:24 +00:00
console . log ( "[jobs] background jobs started" ) ;
}
export function stopJobs() {
for ( const t of timers ) clearInterval ( t ) ;
timers . length = 0 ;
started = false ;
}