45 lines
6.2 KiB
TypeScript
45 lines
6.2 KiB
TypeScript
import type {State} from './types';
|
|
import {activityReason,type Evidence} from './realm-insights';
|
|
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded','lootedCopper','nativeDeaths'] as const;
|
|
export type NativeOutcome={guid:number;name:string}&Record<Exclude<typeof counterKeys[number],'nativeDeaths'>,number>&{nativeDeaths?:number};
|
|
export type OutcomeBaseline={at:number;startedAt:number;rows:NativeOutcome[]};
|
|
export function outcomeChanges(rows:NativeOutcome[],previous:OutcomeBaseline|undefined,now:number){
|
|
const events:Evidence[]=[];
|
|
for(const r of rows){const old=previous?.rows.find(b=>b.guid===r.guid);if(!old)continue;
|
|
for(const key of counterKeys){const value=r[key],prior=old[key];if(typeof value!=='number'||typeof prior!=='number')continue;const amount=value-prior;if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({nativeDeaths:'Native death records, including normal resurrection',lootedCopper:'Currency looted',quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
|
|
}
|
|
return {baseline:{at:now,startedAt:previous?.startedAt??now,rows},events};
|
|
}
|
|
export type Recovery={id:number;guid:number;name:string;at:number;detail:string;status?:'pending'|'resumed'|'no_progress'|'unverified';checkedAt?:number;result?:string};
|
|
export type Perf={at:number;enabled:boolean;ticks:number;averageTickMs:number;rows:{category:string;name:string;calls:number;totalMs:number;averageMs:number;maxMs:number;perTickMs:number}[]};
|
|
export type Health={host:string;at:number;memoryAvailableMB:number;memoryTotalMB:number;diskFreeGB:number;cpuPercent:number;modelAvailable?:boolean;runtimeService?:string};
|
|
export type Attention={key:string;severity:'warning'|'critical';title:string;detail:string;target:string;name?:string};
|
|
export function attentionItems(s:State,now:number,collectedAt:number|null,outcomeAt:number|null,perf:Perf|null,recoveries:Recovery[],health:Health[]){
|
|
const items:Attention[]=[];
|
|
const add=(key:string,severity:Attention['severity'],title:string,detail:string,target:string,name?:string)=>items.push({key,severity,title,detail,target,name});
|
|
if(!collectedAt||now-collectedAt>180)add('collector','warning','Observations are delayed','The minute collector has not delivered a fresh sample.','System Health');
|
|
if(!outcomeAt||now-outcomeAt>180)add('outcomes','warning','Gameplay evidence is delayed','Quest and loot counters are unavailable or stale.','Gameplay Results');
|
|
if(!perf||!perf.enabled||now-perf.at>720)add('profiler','warning','Bot profiler needs attention','A current enabled profiler sample is not available.','Bot Performance');
|
|
for(const host of ['ubuntu','windows']){const h=health.find(v=>v.host===host);if(!h||now-h.at>180){add('host:'+host,'warning',`${host} telemetry delayed`,'Host resource readings are missing or stale.','System Health');continue;}
|
|
if(h.memoryAvailableMB<Math.max(256,h.memoryTotalMB*.05))add('memory:'+host,'critical',`${host} memory pressure`,`${Math.round(h.memoryAvailableMB)} MB available. Check host workload before raising bot population.`,'System Health');
|
|
if(h.diskFreeGB<4)add('disk:'+host,'critical',`${host} disk space low`,`${h.diskFreeGB.toFixed(1)} GB free.`,'System Health');
|
|
if(h.cpuPercent>90)add('cpu:'+host,'warning',`${host} CPU is busy`,'Latest sample exceeds 90%; this alone does not establish sustained overload.','System Health');
|
|
if(host==='ubuntu'&&h.modelAvailable===false)add('model','warning','Language model unavailable','Bot conversations may be delayed.','System Health');
|
|
}
|
|
const online=s.bots.filter(b=>b.autonomy?.managed&&b.status==='online');
|
|
if(now-(s.aleRosterAt??0)<120){if(online.length!==50)add('population',online.length>50?'critical':'warning',`${online.length} / 50 bots online`,online.length>50?'The hard cap needs investigation.':'A vacancy may be waiting for its replacement.','Bots');
|
|
const alliance=online.filter(b=>b.world?.team===0).length,horde=online.filter(b=>b.world?.team===1).length;
|
|
if(alliance!==25||horde!==25)add('factions','warning','Faction balance differs from target',`${alliance} Alliance · ${horde} Horde · target 25 each.`,'World Monitor');}
|
|
for(const b of online){const w=b.world;if(w&&!w.isDead&&now-w.at<120&&w.readinessAt&&now-w.readinessAt<180){
|
|
const needs=[(w.minDurabilityPct??100)<30?`lowest equipped durability ${Math.round(w.minDurabilityPct!)}%`:null,(w.brokenItems??0)>0?`${w.brokenItems} broken equipped items`:null,(w.freeTalents??0)>0?`${w.freeTalents} unspent talent points`:null,w.foodCount===0?'no usable food':null,w.manaPct!==undefined&&w.drinkCount===0?'no usable drinks':null].filter(Boolean);
|
|
if(needs.length)add('readiness:'+b.id,'warning',b.name+' · supplies and readiness',needs.join(' · ')+'. Native maintenance will attempt normal remedies; completion is checked on the next audit.','Bot Activity',b.name);
|
|
}
|
|
if(w?.trainingAt&&now-w.trainingAt<120&&(w.trainingPending??0)>0&&w.trainingStatus!=='traveling')add('training:'+b.id,'warning',b.name+' · class training',`${w.trainingPending} eligible spells pending · ${w.trainingStatus} · ${w.trainingTrainer||'trainer unavailable'}.`,'Bot Activity',b.name);
|
|
const reason=activityReason(b,now);if(['Possible stall','Telemetry delayed'].includes(reason.label))add('bot:'+b.id,'warning',b.name+' · '+reason.label,reason.detail,'Bot Activity',b.name);}
|
|
const latest=new Map<number,Recovery>();for(const r of [...recoveries].sort((a,b)=>b.at-a.at))if(!latest.has(r.guid))latest.set(r.guid,r);
|
|
for(const r of latest.values())if(r.status==='no_progress'&&now-r.at<3600&&online.some(b=>b.autonomy?.characterGuid===r.guid&&activityReason(b,now).label==='Possible stall'))add('recovery:'+r.id,'warning',r.name+' · recovery needs follow-up','No native movement or level progress was observed within three minutes.','Recovery Results',r.name);
|
|
return items.sort((a,b)=>Number(b.severity==='critical')-Number(a.severity==='critical'));
|
|
}
|
|
|
|
export function formatCopper(amount:number){const n=Math.max(0,Math.floor(Number.isFinite(amount)?amount:0));return `${Math.floor(n/10000)}g ${Math.floor(n%10000/100)}s ${n%100}c`;}
|