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-11
1 'use strict';e59b7f7cc6archadmin2026-10-11
2 const {now,rows,one,run,transaction}=require('./database');e59b7f7cc6archadmin2026-10-11
3 const {runProbe}=require('./probes');const {notifyQueue,processQueue}=require('./notifications');e59b7f7cc6archadmin2026-10-11
4 const MAX_SAMPLE_AGE=400*86400;e59b7f7cc6archadmin2026-10-11
5 function upsertState(db,cfg,id,result,time=now()){e59b7f7cc6archadmin2026-10-11
6 return transaction(db,()=>{e59b7f7cc6archadmin2026-10-11
7 const s=one(db,'SELECT * FROM services WHERE id=?',id);if(!s||!s.configured)return null;e59b7f7cc6archadmin2026-10-11
8 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-11
9 const warning=!!result.warning&&!!result.success;e59b7f7cc6archadmin2026-10-11
10 const fails=(!result.success||warning)?s.failures+1:0,successes=(result.success&&!warning)?s.successes+1:0;e59b7f7cc6archadmin2026-10-11
11 let state=s.current_state;e59b7f7cc6archadmin2026-10-11
12 if(!result.success&&fails>=cfg.failures)state='offline';e59b7f7cc6archadmin2026-10-11
13 if(warning&&fails>=cfg.failures)state='degraded';e59b7f7cc6archadmin2026-10-11
14 if(result.success&&!warning&&successes>=cfg.recoveries)state='operational';e59b7f7cc6archadmin2026-10-11
15 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-11
16 if(state!==s.current_state){e59b7f7cc6archadmin2026-10-11
17 if(state==='offline'||state==='degraded'){e59b7f7cc6archadmin2026-10-11
18 const title=state==='offline'?`${s.label} is unavailable`:`${s.label} requires attention`;e59b7f7cc6archadmin2026-10-11
19 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-11
20 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-11
21 notifyQueue(db,cfg,'ArchForge: '+title,`External checks detected a problem with ${s.label}. Open the status page for updates.`);e59b7f7cc6archadmin2026-10-11
22 } else if(state==='operational'){e59b7f7cc6archadmin2026-10-11
23 const active=rows(db,'SELECT id FROM incidents WHERE service_id=? AND automatic=1 AND resolved_at IS NULL',id);e59b7f7cc6archadmin2026-10-11
24 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-11
25 if(['offline','degraded'].includes(s.current_state))notifyQueue(db,cfg,'ArchForge: '+s.label+' recovered',`${s.label} is responding successfully again.`);e59b7f7cc6archadmin2026-10-11
26 }e59b7f7cc6archadmin2026-10-11
27 }e59b7f7cc6archadmin2026-10-11
28 return {state,changed:state!==s.current_state};e59b7f7cc6archadmin2026-10-11
29 });e59b7f7cc6archadmin2026-10-11
30 }e59b7f7cc6archadmin2026-10-11
31 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-11
32 function uptimeStats(db,days=30,serviceId=null){if(![7,30,90].includes(Number(days)))days=30;e59b7f7cc6archadmin2026-10-11
33 const end=now(),since=end-days*86400;e59b7f7cc6archadmin2026-10-11
34 const svcs=rows(db,`SELECT * FROM services WHERE configured=1 ${serviceId?'AND id=?':''} ORDER BY category,label`,...(serviceId?[serviceId]:[]));e59b7f7cc6archadmin2026-10-11
35 return svcs.map(s=>{e59b7f7cc6archadmin2026-10-11
36 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-11
37 const map=new Map(r.map(v=>[v.day,v]));const buckets=[];let total=0,good=0,coverage=0;e59b7f7cc6archadmin2026-10-11
38 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-11
39 return {...s,stats:{total,good,coverage,days,uptime:total?Math.round(good/total*10000)/100:null,buckets}};e59b7f7cc6archadmin2026-10-11
40 });e59b7f7cc6archadmin2026-10-11
41 }e59b7f7cc6archadmin2026-10-11
42 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-11
43 function startMonitor(db,cfg,{probe=runProbe,immediate=true}={}){e59b7f7cc6archadmin2026-10-11
44 let busy=false,stopped=false;e59b7f7cc6archadmin2026-10-11
45 const tick=async()=>{if(stopped||busy)return;busy=true;e59b7f7cc6archadmin2026-10-11
46 try{e59b7f7cc6archadmin2026-10-11
47 for(const s of rows(db,'SELECT * FROM services WHERE configured=1')){e59b7f7cc6archadmin2026-10-11
48 if(stopped)break;e59b7f7cc6archadmin2026-10-11
49 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-11
50 upsertState(db,cfg,s.id,result);e59b7f7cc6archadmin2026-10-11
51 }e59b7f7cc6archadmin2026-10-11
52 await processQueue(db,cfg);prune(db);e59b7f7cc6archadmin2026-10-11
53 }catch(e){console.error('Monitor cycle failure:',e.message);}finally{busy=false;}e59b7f7cc6archadmin2026-10-11
54 };e59b7f7cc6archadmin2026-10-11
55 const timer=setInterval(tick,cfg.interval);timer.unref?.();if(immediate)setImmediate(tick);e59b7f7cc6archadmin2026-10-11
56 return {stop(){stopped=true;clearInterval(timer);},tick};e59b7f7cc6archadmin2026-10-11
57 }e59b7f7cc6archadmin2026-10-11
58 module.exports={upsertState,incidentMaintenanceState,uptimeStats,startMonitor,prune};