Implement all vendor integrations, WAN graphs, and FortiGate health panel
- Wire up 26 vendor providers: Atlassian Statuspage API, Status.io, Instatus, AWS RSS feeds, Apple/Google JSON feeds, M365 Graph API, and synthetic checks - Add 11 new providers: AWS, Cloudflare, SmartPass, School Dismissal Manager, SherpaDesk, Classkick, ClassDojo, Savvas, Study Island, Promethean, RAZ-Kids - Rename Local Infrastructure → Internet (TCP check to 8.8.8.8:53) - Add WAN throughput graph section: dual-link canvas graphs (Crown Castle + Comcast) polling FortiGate REST API every 30s with 30-min rolling history - Add FortiGate health card: uptime, CPU %, memory % from FortiOS API - Add /api/throughput and /api/fortigate-health endpoints - Add README with setup instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,128 @@
|
||||
export const name = "Microsoft 365";
|
||||
export const url = "https://admin.microsoft.com/Adminportal/Home#/servicehealth";
|
||||
|
||||
const GRAPH_HEALTH_URL =
|
||||
"https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews";
|
||||
|
||||
// Token cache
|
||||
let cachedToken = null;
|
||||
|
||||
// Graph serviceStatus → dashboard status
|
||||
const STATUS_MAP = {
|
||||
serviceOperational: "operational",
|
||||
serviceRestored: "operational",
|
||||
postIncidentReviewPublished: "operational",
|
||||
verifyingService: "operational",
|
||||
falsePositive: "operational",
|
||||
serviceDegradation: "degraded",
|
||||
investigating: "degraded",
|
||||
restoringService: "degraded",
|
||||
extendedRecovery: "degraded",
|
||||
investigationSuspended: "degraded",
|
||||
serviceInterruption: "outage",
|
||||
};
|
||||
|
||||
// Graph serviceStatus → human-readable label for the message
|
||||
const STATUS_LABEL = {
|
||||
serviceDegradation: "service degradation",
|
||||
investigating: "investigating",
|
||||
restoringService: "restoring service",
|
||||
extendedRecovery: "extended recovery",
|
||||
investigationSuspended: "investigation suspended",
|
||||
serviceInterruption: "service interruption",
|
||||
};
|
||||
|
||||
const SEVERITY_ORDER = ["operational", "degraded", "outage", "unknown"];
|
||||
|
||||
function mapStatus(graphStatus) {
|
||||
return STATUS_MAP[graphStatus] || "unknown";
|
||||
}
|
||||
|
||||
function worstStatus(a, b) {
|
||||
return SEVERITY_ORDER.indexOf(a) >= SEVERITY_ORDER.indexOf(b) ? a : b;
|
||||
}
|
||||
|
||||
async function getToken() {
|
||||
const { AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET } =
|
||||
process.env;
|
||||
|
||||
if (!AZURE_TENANT_ID || !AZURE_CLIENT_ID || !AZURE_CLIENT_SECRET) {
|
||||
throw new Error(
|
||||
"Missing Azure credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)"
|
||||
);
|
||||
}
|
||||
|
||||
// Return cached token if still valid (with 5-minute buffer)
|
||||
if (cachedToken && Date.now() < cachedToken.expiresAt - 5 * 60 * 1000) {
|
||||
return cachedToken.accessToken;
|
||||
}
|
||||
|
||||
const tokenUrl = `https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`;
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: AZURE_CLIENT_ID,
|
||||
client_secret: AZURE_CLIENT_SECRET,
|
||||
scope: "https://graph.microsoft.com/.default",
|
||||
});
|
||||
|
||||
const res = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Token request failed (${res.status}): ${text}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
cachedToken = {
|
||||
accessToken: data.access_token,
|
||||
expiresAt: Date.now() + data.expires_in * 1000,
|
||||
};
|
||||
|
||||
return cachedToken.accessToken;
|
||||
}
|
||||
|
||||
export async function checkStatus() {
|
||||
const token = await getToken();
|
||||
|
||||
const res = await fetch(GRAPH_HEALTH_URL, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Graph API request failed (${res.status}): ${text}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const services = data.value || [];
|
||||
|
||||
let overall = "operational";
|
||||
const issues = [];
|
||||
|
||||
for (const svc of services) {
|
||||
const mapped = mapStatus(svc.status);
|
||||
overall = worstStatus(overall, mapped);
|
||||
|
||||
if (mapped !== "operational") {
|
||||
const label = STATUS_LABEL[svc.status] ?? svc.status;
|
||||
issues.push(`${svc.service}: ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
const message =
|
||||
issues.length > 0
|
||||
? issues.join(", ")
|
||||
: "All services running normally.";
|
||||
|
||||
return {
|
||||
name,
|
||||
status: "operational",
|
||||
message: "All services running normally.",
|
||||
status: overall,
|
||||
message,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user