Skip to content

Repository files, collaboration, releases and project activity.

ARCHFORGE HISTORY

Blame · src/monitor.js

Branch main · the Commit author is not necessarily a verified ArchForge account.

e59b7f7cc6archadmin2026-10-111 'use strict';
e59b7f7cc6archadmin2026-10-112 const {now,rows,one,run,transaction}=require('./database');
e59b7f7cc6archadmin2026-10-113 const {runProbe}=require('./probes');const {notifyQueue,processQueue}=require('./notifications');
e59b7f7cc6archadmin2026-10-114 const MAX_SAMPLE_AGE=400*86400;
e59b7f7cc6archadmin2026-10-115 function upsertState(db,cfg,id,result,time=now()){
e59b7f7cc6archadmin2026-10-116  return transaction(db,()=>{
e59b7f7cc6archadmin2026-10-117  const s=one(db,'SELECT * FROM services WHERE id=?',id);if(!s||!s.configured)return null;
e59b7f7cc6archadmin2026-10-118  run(db,'INSERT INTO samples(service_id,checked_at,success,latency_ms,warning,detail) VALUES(?,?,?,?,?,?)',id,time,result.success?1:0,result.latency===undefined?null:result.latency,result.warning?1:0,String(result.detail||'').slice(0,120));
e59b7f7cc6archadmin2026-10-119  const warning=!!result.warning&&!!result.success;
e59b7f7cc6archadmin2026-10-1110  const fails=(!result.success||warning)?s.failures+1:0,successes=(result.success&&!warning)?s.successes+1:0;
e59b7f7cc6archadmin2026-10-1111  let state=s.current_state;
e59b7f7cc6archadmin2026-10-1112  if(!result.success&&fails>=cfg.failures)state='offline';
e59b7f7cc6archadmin2026-10-1113  if(warning&&fails>=cfg.failures)state='degraded';
e59b7f7cc6archadmin2026-10-1114  if(result.success&&!warning&&successes>=cfg.recoveries)state='operational';
e59b7f7cc6archadmin2026-10-1115  run(db,'UPDATE services SET current_state=?,failures=?,successes=?,last_checked=?,last_latency=? WHERE id=?',state,fails,successes,time,result.latency??null,id);
e59b7f7cc6archadmin2026-10-1116  if(state!==s.current_state){
e59b7f7cc6archadmin2026-10-1117  if(state==='offline'||state==='degraded'){
e59b7f7cc6archadmin2026-10-1118  const title=state==='offline'?`${s.label} is unavailable`:`${s.label} requires attention`;
e59b7f7cc6archadmin2026-10-1119  const ins=run(db,'INSERT INTO incidents(service_id,title,description,state,severity,automatic,started_at) VALUES(?,?,?,?,?,?,?)',id,title,state==='offline'?'Repeated independent monitoring checks failed.':'Independent monitoring detected a warning.','investigating',state==='offline'?'outage':'degraded',1,time);
e59b7f7cc6archadmin2026-10-1120  run(db,'INSERT INTO incident_updates(incident_id,state,body,created_at) VALUES(?,?,?,?)',Number(ins.lastInsertRowid),'investigating',state==='offline'?'External monitoring confirmed an outage.':'External monitoring confirmed a warning.',time);
e59b7f7cc6archadmin2026-10-1121  notifyQueue(db,cfg,'ArchForge: '+title,`External checks detected a problem with ${s.label}. Open the status page for updates.`);
e59b7f7cc6archadmin2026-10-1122  } else if(state==='operational'){
e59b7f7cc6archadmin2026-10-1123  const active=rows(db,'SELECT id FROM incidents WHERE service_id=? AND automatic=1 AND resolved_at IS NULL',id);
e59b7f7cc6archadmin2026-10-1124  for(const inc of active){run(db,"UPDATE incidents SET state='resolved',resolved_at=? WHERE id=?",time,inc.id);run(db,'INSERT INTO incident_updates(incident_id,state,body,created_at) VALUES(?,?,?,?)',inc.id,'resolved','Service recovered after multiple successful checks.',time);}
e59b7f7cc6archadmin2026-10-1125  if(['offline','degraded'].includes(s.current_state))notifyQueue(db,cfg,'ArchForge: '+s.label+' recovered',`${s.label} is responding successfully again.`);
e59b7f7cc6archadmin2026-10-1126  }
e59b7f7cc6archadmin2026-10-1127  }
e59b7f7cc6archadmin2026-10-1128  return {state,changed:state!==s.current_state};
e59b7f7cc6archadmin2026-10-1129  });
e59b7f7cc6archadmin2026-10-1130 }
e59b7f7cc6archadmin2026-10-1131 function incidentMaintenanceState(db,time=now()){return rows(db,'SELECT * FROM maintenance WHERE starts_at<=? AND ends_at>? ORDER BY starts_at DESC',time,time);}
e59b7f7cc6archadmin2026-10-1132 function uptimeStats(db,days=30,serviceId=null){if(![7,30,90].includes(Number(days)))days=30;
e59b7f7cc6archadmin2026-10-1133  const end=now(),since=end-days*86400;
e59b7f7cc6archadmin2026-10-1134  const svcs=rows(db,`SELECT * FROM services WHERE configured=1 ${serviceId?'AND id=?':''} ORDER BY category,label`,...(serviceId?[serviceId]:[]));
e59b7f7cc6archadmin2026-10-1135  return svcs.map(s=>{
e59b7f7cc6archadmin2026-10-1136  const r=rows(db,`SELECT date(checked_at,'unixepoch') AS day, COUNT(*) count, SUM(success) successful, MAX(CASE WHEN success=0 THEN 1 ELSE 0 END) failed, MAX(warning) warned, ROUND(AVG(latency_ms)) latency FROM samples WHERE service_id=? AND checked_at>=? GROUP BY date(checked_at,'unixepoch') ORDER BY day`,s.id,since);
e59b7f7cc6archadmin2026-10-1137  const map=new Map(r.map(v=>[v.day,v]));const buckets=[];let total=0,good=0,coverage=0;
e59b7f7cc6archadmin2026-10-1138  for(let i=days-1;i>=0;i--){const date=new Date((Math.floor(end/86400)-i)*86400000).toISOString().slice(0,10),r=map.get(date);if(r){total+=r.count;good+=r.successful;coverage++;}buckets.push({day:date,state:!r?'unknown':r.failed?'outage':r.warned?'degraded':'operational',count:r?.count||0,successful:r?.successful||0,latency:r?.latency??null});}
e59b7f7cc6archadmin2026-10-1139  return {...s,stats:{total,good,coverage,days,uptime:total?Math.round(good/total*10000)/100:null,buckets}};
e59b7f7cc6archadmin2026-10-1140  });
e59b7f7cc6archadmin2026-10-1141 }
e59b7f7cc6archadmin2026-10-1142 function prune(db){run(db,'DELETE FROM samples WHERE checked_at<?',now()-MAX_SAMPLE_AGE);run(db,'DELETE FROM sessions WHERE expires_at<?',now());run(db,'DELETE FROM messages WHERE delivered_at<?',now()-30*86400);run(db,'DELETE FROM subscriptions WHERE confirmed=0 AND confirm_until<?',now()-86400);}
e59b7f7cc6archadmin2026-10-1143 function startMonitor(db,cfg,{probe=runProbe,immediate=true}={}){
e59b7f7cc6archadmin2026-10-1144  let busy=false,stopped=false;
e59b7f7cc6archadmin2026-10-1145  const tick=async()=>{if(stopped||busy)return;busy=true;
e59b7f7cc6archadmin2026-10-1146  try{
e59b7f7cc6archadmin2026-10-1147  for(const s of rows(db,'SELECT * FROM services WHERE configured=1')){
e59b7f7cc6archadmin2026-10-1148  if(stopped)break;
e59b7f7cc6archadmin2026-10-1149  let result;try{result=await probe(s.id,cfg);}catch(e){result={success:false,latency:null,detail:String(e.code||e.message||'Probe exception').slice(0,120)};}
e59b7f7cc6archadmin2026-10-1150  upsertState(db,cfg,s.id,result);
e59b7f7cc6archadmin2026-10-1151  }
e59b7f7cc6archadmin2026-10-1152  await processQueue(db,cfg);prune(db);
e59b7f7cc6archadmin2026-10-1153  }catch(e){console.error('Monitor cycle failure:',e.message);}finally{busy=false;}
e59b7f7cc6archadmin2026-10-1154  };
e59b7f7cc6archadmin2026-10-1155  const timer=setInterval(tick,cfg.interval);timer.unref?.();if(immediate)setImmediate(tick);
e59b7f7cc6archadmin2026-10-1156  return {stop(){stopped=true;clearInterval(timer);},tick};
e59b7f7cc6archadmin2026-10-1157 }
e59b7f7cc6archadmin2026-10-1158 module.exports={upsertState,incidentMaintenanceState,uptimeStats,startMonitor,prune};