feat: initial implementation of Oriana uptime monitor
Security Scan and Upload / Security & DefectDojo Upload (push) Failing after 50s

Implements a full-stack uptime monitoring application with:
- HTTPS, HTTPS keyword, HTTPS status, port, and MongoDB Atlas monitor types
- Cron-based monitoring engine with webhook notifications on status changes
- Discord OAuth2 admin panel (single-owner)
- Public status page with category grouping and failure reason display
- Admin dashboard with sortable monitors table and detailed failure info
- SQLite persistence with migration support
This commit is contained in:
2026-03-05 17:25:50 -08:00
committed by Naomi Carrigan
parent f77d2ed273
commit 1e3b06036d
38 changed files with 9887 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Oriana</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "oriana-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -p tsconfig.json && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"react-router-dom": "7.2.0",
"recharts": "2.15.1"
},
"devDependencies": {
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@vitejs/plugin-react": "4.3.4",
"typescript": "5.8.2",
"vite": "6.2.1"
}
}
+1437
View File
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { useAuth } from "./hooks/useAuth.js";
import { StatusPage } from "./pages/StatusPage.js";
import { Login } from "./pages/Login.js";
import { Dashboard } from "./pages/Dashboard.js";
import { Monitors } from "./pages/Monitors.js";
import { Incidents } from "./pages/Incidents.js";
const ProtectedRoute = ({
children,
}: {
children: React.ReactNode;
}): React.ReactElement => {
const { user, loading } = useAuth();
if (loading) {
return <div className="loading">Loading...</div>;
}
if (!user) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
export const App = (): React.ReactElement => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<StatusPage />} />
<Route path="/login" element={<Login />} />
<Route
path="/admin"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin/monitors"
element={
<ProtectedRoute>
<Monitors />
</ProtectedRoute>
}
/>
<Route
path="/admin/incidents"
element={
<ProtectedRoute>
<Incidents />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
};
+87
View File
@@ -0,0 +1,87 @@
import { Link, useLocation } from "react-router-dom";
import type { CurrentUser } from "../types.js";
const navLinks = [
{ to: "/admin", label: "Dashboard" },
{ to: "/admin/monitors", label: "Monitors" },
{ to: "/admin/incidents", label: "Incidents & Maintenance" },
];
export const AdminNav = ({
user,
}: {
user: CurrentUser;
}): React.ReactElement => {
const { pathname } = useLocation();
const handleLogout = async (): Promise<void> => {
await fetch("/auth/logout", { method: "POST", credentials: "include" });
window.location.href = "/";
};
return (
<nav
style={{
background: "#0a0009",
color: "#f5f5f5",
padding: "0 24px",
display: "flex",
alignItems: "center",
gap: 24,
height: 56,
borderBottom: "1px solid #44275a",
}}
>
<span style={{ fontWeight: 700, fontSize: 18, marginRight: 8, fontFamily: "Griffy, cursive", color: "#d4a5c7" }}>
Oriana
</span>
{navLinks.map((link) => (
<Link
key={link.to}
to={link.to}
style={{
color: pathname === link.to ? "#e8d5e8" : "#d4a5c7",
textDecoration: "none",
fontSize: 14,
fontWeight: pathname === link.to ? 700 : 400,
transition: "opacity 0.3s ease",
}}
>
{link.label}
</Link>
))}
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 16 }}>
<Link
to="/"
style={{
color: "#d4a5c7",
textDecoration: "none",
fontSize: 13,
transition: "opacity 0.3s ease",
}}
>
Status Page
</Link>
<span style={{ fontSize: 13, color: "#d4a5c7" }}>
{user.globalName ?? user.username}
</span>
<button
onClick={() => void handleLogout()}
style={{
background: "transparent",
border: "1px solid #a8577e",
color: "#d4a5c7",
borderRadius: 6,
padding: "4px 12px",
cursor: "pointer",
fontSize: 13,
fontFamily: "Kalam, sans-serif",
transition: "all 0.3s ease",
}}
>
Log out
</button>
</div>
</nav>
);
};
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from "react";
import type { CurrentUser } from "../types.js";
export const useAuth = (): { user: CurrentUser | null; loading: boolean } => {
const [user, setUser] = useState<CurrentUser | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/auth/me", { credentials: "include" })
.then(async (res) => {
if (res.ok) {
setUser((await res.json()) as CurrentUser);
}
})
.finally(() => {
setLoading(false);
});
}, []);
return { user, loading };
};
+51
View File
@@ -0,0 +1,51 @@
@import url("https://fonts.googleapis.com/css2?family=Griffy&family=Kalam:wght@300;400;700&display=swap");
:root {
--witch-purple: #2b1b3d;
--witch-plum: #44275a;
--witch-rose: #a8577e;
--witch-mauve: #d4a5c7;
--witch-lavender: #e8d5e8;
--witch-black: #0a0009;
--witch-silver: #c0c0c0;
--witch-moon: #f5f5f5;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--witch-purple);
color: var(--witch-moon);
font-family: "Kalam", sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Griffy", cursive;
}
a {
color: var(--witch-rose);
transition: opacity 0.3s ease;
}
a:hover {
opacity: 0.8;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
color: var(--witch-mauve);
}
+15
View File
@@ -0,0 +1,15 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App.js";
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Root element not found.");
}
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
);
+127
View File
@@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import { useAuth } from "../hooks/useAuth.js";
import { AdminNav } from "../components/AdminNav.js";
import type { Incident, Monitor, StatusResponse } from "../types.js";
export const Dashboard = (): React.ReactElement => {
const { user } = useAuth();
const [status, setStatus] = useState<StatusResponse | null>(null);
const [monitors, setMonitors] = useState<Monitor[]>([]);
useEffect(() => {
fetch("/api/status")
.then(async (res) => res.json())
.then((d) => {
setStatus(d as StatusResponse);
});
fetch("/api/monitors", { credentials: "include" })
.then(async (res) => res.json())
.then((d) => {
setMonitors(d as Monitor[]);
});
}, []);
const upCount = status?.monitors.filter((m) => m.status === "up").length ?? 0;
const downCount = status?.monitors.filter((m) => m.status === "down").length ?? 0;
const activeIncidents = status?.incidents.filter(
(i: Incident) => i.status !== "resolved",
).length ?? 0;
return (
<div style={{ minHeight: "100vh" }}>
{user && <AdminNav user={user} />}
<div style={{ maxWidth: 900, margin: "0 auto", padding: "32px 16px" }}>
<h1 style={{ margin: "0 0 24px", fontSize: 24, color: "#d4a5c7" }}>Dashboard</h1>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16, marginBottom: 32 }}>
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, padding: 20 }}>
<div style={{ fontSize: 32, fontWeight: 700, color: "#4ade80", fontFamily: "Griffy, cursive" }}>{upCount}</div>
<div style={{ color: "#d4a5c7", fontSize: 14 }}>Monitors Up</div>
</div>
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, padding: 20 }}>
<div style={{ fontSize: 32, fontWeight: 700, color: "#f87171", fontFamily: "Griffy, cursive" }}>{downCount}</div>
<div style={{ color: "#d4a5c7", fontSize: 14 }}>Monitors Down</div>
</div>
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, padding: 20 }}>
<div style={{ fontSize: 32, fontWeight: 700, color: "#fb923c", fontFamily: "Griffy, cursive" }}>{activeIncidents}</div>
<div style={{ color: "#d4a5c7", fontSize: 14 }}>Active Incidents</div>
</div>
</div>
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, overflow: "hidden" }}>
<div style={{ padding: "16px 20px", borderBottom: "1px solid #2b1b3d", fontWeight: 600, color: "#e8d5e8" }}>
All Monitors
</div>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ background: "#0a0009", fontSize: 12, color: "#d4a5c7" }}>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Name</th>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Type</th>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Category</th>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Status / Details</th>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Interval</th>
<th style={{ padding: "8px 20px", textAlign: "left" }}>Enabled</th>
</tr>
</thead>
<tbody>
{monitors.map((m) => {
const summary = status?.monitors.find((s) => s.id === m.id);
const statusColour =
summary?.status === "up"
? "#4ade80"
: summary?.status === "down"
? "#f87171"
: "#c0c0c0";
const checkedAt = summary?.lastChecked
? new Date(summary.lastChecked).toLocaleString()
: null;
return (
<tr key={m.id} style={{ borderTop: "1px solid #2b1b3d", fontSize: 14 }}>
<td style={{ padding: "10px 20px", color: "#f5f5f5" }}>{m.name}</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.type}</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.category}</td>
<td style={{ padding: "10px 20px" }}>
<span style={{ fontWeight: 600, color: statusColour }}>
{(summary?.status ?? "unknown").toUpperCase()}
</span>
{summary?.status === "down" && (
<div style={{ marginTop: 4, fontSize: 12, color: "#f87171" }}>
{summary.message ?? "No details available."}
{summary.statusCode !== null && (
<span style={{ marginLeft: 6, color: "#c0c0c0" }}>
(HTTP {summary.statusCode})
</span>
)}
{summary.responseTimeMs !== null && (
<span style={{ marginLeft: 6, color: "#c0c0c0" }}>
· {summary.responseTimeMs}ms
</span>
)}
{checkedAt !== null && (
<div style={{ color: "#c0c0c0", marginTop: 2 }}>
Last checked: {checkedAt}
</div>
)}
</div>
)}
{summary?.status === "up" && (
<div style={{ marginTop: 2, fontSize: 12, color: "#c0c0c0" }}>
{summary.responseTimeMs !== null && `${summary.responseTimeMs}ms`}
{summary.responseTimeMs !== null && checkedAt !== null && " · "}
{checkedAt !== null && checkedAt}
</div>
)}
</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.intervalSeconds}s</td>
<td style={{ padding: "10px 20px", color: "#f5f5f5" }}>{m.enabled ? "✓" : "✗"}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
};
+285
View File
@@ -0,0 +1,285 @@
import { useEffect, useReducer, useState } from "react";
import { useAuth } from "../hooks/useAuth.js";
import { AdminNav } from "../components/AdminNav.js";
import type { Incident, IncidentStatus, MaintenanceWindow } from "../types.js";
type IncidentForm = { title: string; message: string; status: IncidentStatus };
type MaintenanceForm = {
title: string;
message: string;
startTime: string;
endTime: string;
};
const defaultIncidentForm: IncidentForm = {
title: "",
message: "",
status: "investigating",
};
const defaultMaintenanceForm: MaintenanceForm = {
title: "",
message: "",
startTime: "",
endTime: "",
};
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
border: "1px solid #44275a",
borderRadius: 8,
fontSize: 14,
boxSizing: "border-box",
background: "#2b1b3d",
color: "#f5f5f5",
fontFamily: "Kalam, sans-serif",
};
const statusColour: Record<IncidentStatus, string> = {
investigating: "#fb923c",
identified: "#facc15",
monitoring: "#60a5fa",
resolved: "#4ade80",
};
export const Incidents = (): React.ReactElement => {
const { user } = useAuth();
const [incidents, setIncidents] = useState<Incident[]>([]);
const [maintenance, setMaintenance] = useState<MaintenanceWindow[]>([]);
const [showIncidentForm, setShowIncidentForm] = useState(false);
const [showMaintenanceForm, setShowMaintenanceForm] = useState(false);
const [editingIncident, setEditingIncident] = useState<Incident | null>(null);
const [editingMaintenance, setEditingMaintenance] = useState<MaintenanceWindow | null>(null);
const [incidentForm, setIncidentForm] = useReducer(
(state: IncidentForm, updates: Partial<IncidentForm>) => ({ ...state, ...updates }),
defaultIncidentForm,
);
const [maintenanceForm, setMaintenanceForm] = useReducer(
(state: MaintenanceForm, updates: Partial<MaintenanceForm>) => ({ ...state, ...updates }),
defaultMaintenanceForm,
);
const load = (): void => {
fetch("/api/incidents")
.then(async (res) => res.json())
.then((d) => { setIncidents(d as Incident[]); });
fetch("/api/maintenance")
.then(async (res) => res.json())
.then((d) => { setMaintenance(d as MaintenanceWindow[]); });
};
useEffect(() => { load(); }, []);
const submitIncident = async (): Promise<void> => {
const url = editingIncident ? `/api/incidents/${editingIncident.id}` : "/api/incidents";
const method = editingIncident ? "PUT" : "POST";
await fetch(url, {
method,
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(incidentForm),
});
setShowIncidentForm(false);
load();
};
const deleteIncident = async (id: string): Promise<void> => {
if (!confirm("Delete this incident?")) return;
await fetch(`/api/incidents/${id}`, { method: "DELETE", credentials: "include" });
load();
};
const submitMaintenance = async (): Promise<void> => {
const url = editingMaintenance ? `/api/maintenance/${editingMaintenance.id}` : "/api/maintenance";
const method = editingMaintenance ? "PUT" : "POST";
await fetch(url, {
method,
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(maintenanceForm),
});
setShowMaintenanceForm(false);
load();
};
const deleteMaintenance = async (id: string): Promise<void> => {
if (!confirm("Delete this maintenance window?")) return;
await fetch(`/api/maintenance/${id}`, { method: "DELETE", credentials: "include" });
load();
};
const primaryButton: React.CSSProperties = {
background: "#a8577e",
color: "#f5f5f5",
border: "none",
borderRadius: 8,
padding: "10px 20px",
cursor: "pointer",
fontSize: 14,
fontWeight: 600,
fontFamily: "Kalam, sans-serif",
transition: "opacity 0.3s ease",
};
const secondaryButton: React.CSSProperties = {
background: "transparent",
color: "#d4a5c7",
border: "1px solid #44275a",
borderRadius: 8,
padding: "8px 20px",
cursor: "pointer",
fontSize: 14,
fontFamily: "Kalam, sans-serif",
};
return (
<div style={{ minHeight: "100vh" }}>
{user && <AdminNav user={user} />}
<div style={{ maxWidth: 900, margin: "0 auto", padding: "32px 16px" }}>
{/* Incidents */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<h1 style={{ margin: 0, fontSize: 24, color: "#d4a5c7" }}>Incidents</h1>
<button
onClick={() => {
setEditingIncident(null);
setIncidentForm(defaultIncidentForm);
setShowIncidentForm(true);
}}
style={primaryButton}
>
New Incident
</button>
</div>
{showIncidentForm && (
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h2 style={{ margin: "0 0 16px", fontSize: 18, color: "#e8d5e8" }}>{editingIncident ? "Edit Incident" : "New Incident"}</h2>
<div style={{ display: "grid", gap: 12 }}>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Title</label>
<input style={inputStyle} value={incidentForm.title} onChange={(e) => { setIncidentForm({ title: e.target.value }); }} />
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Message</label>
<textarea style={{ ...inputStyle, height: 80, resize: "vertical" }} value={incidentForm.message} onChange={(e) => { setIncidentForm({ message: e.target.value }); }} />
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Status</label>
<select style={inputStyle} value={incidentForm.status} onChange={(e) => { setIncidentForm({ status: e.target.value as IncidentStatus }); }}>
<option value="investigating">Investigating</option>
<option value="identified">Identified</option>
<option value="monitoring">Monitoring</option>
<option value="resolved">Resolved</option>
</select>
</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
<button onClick={() => void submitIncident()} style={primaryButton}>
{editingIncident ? "Save Changes" : "Create Incident"}
</button>
<button onClick={() => { setShowIncidentForm(false); }} style={secondaryButton}>
Cancel
</button>
</div>
</div>
)}
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, marginBottom: 40, overflow: "hidden" }}>
{incidents.length === 0 ? (
<div style={{ padding: 40, textAlign: "center", color: "#c0c0c0" }}>No incidents.</div>
) : incidents.map((incident) => (
<div key={incident.id} style={{ padding: 16, borderBottom: "1px solid #2b1b3d" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div>
<strong style={{ fontSize: 15, color: "#f5f5f5" }}>{incident.title}</strong>
<span style={{ marginLeft: 10, fontSize: 12, fontWeight: 600, color: statusColour[incident.status] }}>{incident.status}</span>
<div style={{ fontSize: 13, color: "#d4a5c7", marginTop: 4 }}>{incident.message}</div>
<div style={{ fontSize: 11, color: "#c0c0c0", marginTop: 4 }}>
Created {new Date(incident.createdAt).toLocaleString()} · Updated {new Date(incident.updatedAt).toLocaleString()}
</div>
</div>
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
<button onClick={() => { setEditingIncident(incident); setIncidentForm({ title: incident.title, message: incident.message, status: incident.status }); setShowIncidentForm(true); }} style={{ border: "1px solid #a8577e", background: "transparent", color: "#d4a5c7", borderRadius: 4, padding: "2px 10px", cursor: "pointer", fontSize: 12, fontFamily: "Kalam, sans-serif" }}>Edit</button>
<button onClick={() => void deleteIncident(incident.id)} style={{ border: "1px solid #f87171", color: "#f87171", background: "transparent", borderRadius: 4, padding: "2px 10px", cursor: "pointer", fontSize: 12, fontFamily: "Kalam, sans-serif" }}>Delete</button>
</div>
</div>
</div>
))}
</div>
{/* Maintenance */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<h2 style={{ margin: 0, fontSize: 22, color: "#d4a5c7" }}>Maintenance Windows</h2>
<button
onClick={() => {
setEditingMaintenance(null);
setMaintenanceForm(defaultMaintenanceForm);
setShowMaintenanceForm(true);
}}
style={primaryButton}
>
New Window
</button>
</div>
{showMaintenanceForm && (
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, padding: 24, marginBottom: 24 }}>
<h2 style={{ margin: "0 0 16px", fontSize: 18, color: "#e8d5e8" }}>{editingMaintenance ? "Edit Maintenance Window" : "New Maintenance Window"}</h2>
<div style={{ display: "grid", gap: 12 }}>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Title</label>
<input style={inputStyle} value={maintenanceForm.title} onChange={(e) => { setMaintenanceForm({ title: e.target.value }); }} />
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Message</label>
<textarea style={{ ...inputStyle, height: 60, resize: "vertical" }} value={maintenanceForm.message} onChange={(e) => { setMaintenanceForm({ message: e.target.value }); }} />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Start Time</label>
<input type="datetime-local" style={inputStyle} value={maintenanceForm.startTime} onChange={(e) => { setMaintenanceForm({ startTime: e.target.value }); }} />
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>End Time</label>
<input type="datetime-local" style={inputStyle} value={maintenanceForm.endTime} onChange={(e) => { setMaintenanceForm({ endTime: e.target.value }); }} />
</div>
</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
<button onClick={() => void submitMaintenance()} style={primaryButton}>
{editingMaintenance ? "Save Changes" : "Create Window"}
</button>
<button onClick={() => { setShowMaintenanceForm(false); }} style={secondaryButton}>
Cancel
</button>
</div>
</div>
)}
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, overflow: "hidden" }}>
{maintenance.length === 0 ? (
<div style={{ padding: 40, textAlign: "center", color: "#c0c0c0" }}>No maintenance windows.</div>
) : maintenance.map((w) => (
<div key={w.id} style={{ padding: 16, borderBottom: "1px solid #2b1b3d" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div>
<strong style={{ fontSize: 15, color: "#f5f5f5" }}>{w.title}</strong>
<div style={{ fontSize: 13, color: "#d4a5c7", marginTop: 4 }}>{w.message}</div>
<div style={{ fontSize: 11, color: "#c0c0c0", marginTop: 4 }}>
{new Date(w.startTime).toLocaleString()} {new Date(w.endTime).toLocaleString()}
</div>
</div>
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
<button onClick={() => { setEditingMaintenance(w); setMaintenanceForm({ title: w.title, message: w.message, startTime: w.startTime, endTime: w.endTime }); setShowMaintenanceForm(true); }} style={{ border: "1px solid #a8577e", background: "transparent", color: "#d4a5c7", borderRadius: 4, padding: "2px 10px", cursor: "pointer", fontSize: 12, fontFamily: "Kalam, sans-serif" }}>Edit</button>
<button onClick={() => void deleteMaintenance(w.id)} style={{ border: "1px solid #f87171", color: "#f87171", background: "transparent", borderRadius: 4, padding: "2px 10px", cursor: "pointer", fontSize: 12, fontFamily: "Kalam, sans-serif" }}>Delete</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
+81
View File
@@ -0,0 +1,81 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth.js";
export const Login = (): React.ReactElement => {
const { user, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && user) {
navigate("/admin");
}
}, [user, loading, navigate]);
const error = new URLSearchParams(window.location.search).get("error");
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
}}
>
<div
style={{
background: "#44275a",
border: "1px solid #a8577e",
borderRadius: 15,
padding: 40,
textAlign: "center",
maxWidth: 360,
width: "100%",
}}
>
<h1 style={{ margin: "0 0 8px", fontSize: 28, color: "#d4a5c7" }}>Oriana</h1>
<p style={{ color: "#c0c0c0", margin: "0 0 24px", fontSize: 14 }}>
Admin access via Discord
</p>
{error === "unauthorised" && (
<div
style={{
background: "#0a0009",
border: "1px solid #f87171",
borderRadius: 8,
padding: "10px 14px",
color: "#f87171",
fontSize: 13,
marginBottom: 16,
}}
>
Your Discord account is not authorised to access this dashboard.
</div>
)}
<a
href="/auth/discord"
style={{
display: "inline-block",
background: "#5865f2",
color: "#fff",
padding: "12px 24px",
borderRadius: 8,
textDecoration: "none",
fontWeight: 600,
fontSize: 15,
transition: "opacity 0.3s ease",
}}
>
Sign in with Discord
</a>
<div style={{ marginTop: 20 }}>
<a href="/" style={{ fontSize: 13, color: "#d4a5c7" }}>
Back to status page
</a>
</div>
</div>
</div>
);
};
+519
View File
@@ -0,0 +1,519 @@
import { useEffect, useReducer, useState } from "react";
import { useAuth } from "../hooks/useAuth.js";
import { AdminNav } from "../components/AdminNav.js";
import type { Monitor, MonitorType } from "../types.js";
type FormState = {
name: string;
type: MonitorType;
url: string;
host: string;
port: string;
keyword: string;
allowedStatusCodes: string;
deniedStatusCodes: string;
intervalSeconds: string;
category: string;
isPublic: boolean;
};
const defaultForm: FormState = {
name: "",
type: "https",
url: "",
host: "",
port: "",
keyword: "",
allowedStatusCodes: "",
deniedStatusCodes: "",
intervalSeconds: "60",
category: "General",
isPublic: true,
};
type SortKey = "category" | "enabled" | "intervalSeconds" | "isPublic" | "name" | "target" | "type";
type SortDir = "asc" | "desc";
const sortAccessors: Record<SortKey, (m: Monitor) => number | string> = {
category: (m) => m.category.toLowerCase(),
enabled: (m) => m.enabled,
intervalSeconds: (m) => m.intervalSeconds,
isPublic: (m) => m.isPublic,
name: (m) => m.name.toLowerCase(),
target: (m): string => {
const raw = m.url ?? (m.host !== null && m.port !== null
? `${m.host}:${m.port}`
: "");
return raw.toLowerCase();
},
type: (m) => m.type,
};
export const Monitors = (): React.ReactElement => {
const { user } = useAuth();
const [monitors, setMonitors] = useState<Monitor[]>([]);
const [showForm, setShowForm] = useState(false);
const [editing, setEditing] = useState<Monitor | null>(null);
const [form, setForm] = useReducer(
(state: FormState, updates: Partial<FormState>) => ({ ...state, ...updates }),
defaultForm,
);
const [sortKey, setSortKey] = useState<SortKey>("name");
const [sortDir, setSortDir] = useState<SortDir>("asc");
const handleSort = (key: SortKey): void => {
if (sortKey === key) {
setSortDir(sortDir === "asc" ? "desc" : "asc");
} else {
setSortKey(key);
setSortDir("asc");
}
};
const sortIcon = (col: SortKey): string => {
if (sortKey !== col) { return " ↕"; }
return sortDir === "asc" ? " ↑" : " ↓";
};
const sortedMonitors = [...monitors].sort((a, b) => {
const aVal = sortAccessors[sortKey](a);
const bVal = sortAccessors[sortKey](b);
if (aVal < bVal) {
return sortDir === "asc" ? -1 : 1;
}
if (aVal > bVal) {
return sortDir === "asc" ? 1 : -1;
}
return 0;
});
const loadMonitors = (): void => {
fetch("/api/monitors", { credentials: "include" })
.then(async (res) => res.json())
.then((d) => {
setMonitors(d as Monitor[]);
});
};
useEffect(() => {
loadMonitors();
}, []);
const openCreate = (): void => {
setEditing(null);
setForm(defaultForm);
setShowForm(true);
};
const openEdit = (monitor: Monitor): void => {
setEditing(monitor);
setForm({
name: monitor.name,
type: monitor.type,
url: monitor.url ?? "",
host: monitor.host ?? "",
port: monitor.port?.toString() ?? "",
keyword: monitor.keyword ?? "",
allowedStatusCodes: monitor.allowedStatusCodes
? (JSON.parse(monitor.allowedStatusCodes) as number[]).join(", ")
: "",
deniedStatusCodes: monitor.deniedStatusCodes
? (JSON.parse(monitor.deniedStatusCodes) as number[]).join(", ")
: "",
intervalSeconds: monitor.intervalSeconds.toString(),
category: monitor.category,
isPublic: monitor.isPublic === 1,
});
setShowForm(true);
};
const handleSubmit = async (): Promise<void> => {
const body = {
name: form.name,
type: form.type,
url: form.url || undefined,
host: form.host || undefined,
port: form.port ? Number(form.port) : undefined,
keyword: form.keyword || undefined,
allowedStatusCodes: form.allowedStatusCodes
? form.allowedStatusCodes.split(",").map((s) => Number(s.trim()))
: undefined,
deniedStatusCodes: form.deniedStatusCodes
? form.deniedStatusCodes.split(",").map((s) => Number(s.trim()))
: undefined,
intervalSeconds: Number(form.intervalSeconds),
category: form.category,
isPublic: form.isPublic,
};
const url = editing ? `/api/monitors/${editing.id}` : "/api/monitors";
const method = editing ? "PUT" : "POST";
await fetch(url, {
method,
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
setShowForm(false);
loadMonitors();
};
const handleDelete = async (id: string): Promise<void> => {
if (!confirm("Delete this monitor?")) return;
await fetch(`/api/monitors/${id}`, { method: "DELETE", credentials: "include" });
loadMonitors();
};
const handleToggleEnabled = async (monitor: Monitor): Promise<void> => {
await fetch(`/api/monitors/${monitor.id}`, {
method: "PUT",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: !monitor.enabled }),
});
loadMonitors();
};
const handleTogglePublic = async (monitor: Monitor): Promise<void> => {
await fetch(`/api/monitors/${monitor.id}`, {
method: "PUT",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isPublic: !monitor.isPublic }),
});
loadMonitors();
};
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
border: "1px solid #44275a",
borderRadius: 8,
fontSize: 14,
boxSizing: "border-box",
background: "#2b1b3d",
color: "#f5f5f5",
fontFamily: "Kalam, sans-serif",
};
return (
<div style={{ minHeight: "100vh" }}>
{user && <AdminNav user={user} />}
<div style={{ maxWidth: 900, margin: "0 auto", padding: "32px 16px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
<h1 style={{ margin: 0, fontSize: 24, color: "#d4a5c7" }}>Monitors</h1>
<button
onClick={openCreate}
style={{
background: "#a8577e",
color: "#f5f5f5",
border: "none",
borderRadius: 8,
padding: "10px 20px",
cursor: "pointer",
fontSize: 14,
fontWeight: 600,
fontFamily: "Kalam, sans-serif",
transition: "opacity 0.3s ease",
}}
>
Add Monitor
</button>
</div>
{showForm && (
<div
style={{
background: "#44275a",
border: "1px solid #a8577e",
borderRadius: 12,
padding: 24,
marginBottom: 24,
}}
>
<h2 style={{ margin: "0 0 16px", fontSize: 18, color: "#e8d5e8" }}>
{editing ? "Edit Monitor" : "New Monitor"}
</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Name</label>
<input
style={inputStyle}
value={form.name}
onChange={(e) => { setForm({ name: e.target.value }); }}
/>
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Type</label>
<select
style={inputStyle}
value={form.type}
onChange={(e) => { setForm({ type: e.target.value as MonitorType }); }}
>
<option value="https">HTTPS</option>
<option value="https_keyword">HTTPS + Keyword</option>
<option value="https_status">HTTPS + Status Codes</option>
<option value="mongodb">MongoDB Atlas</option>
<option value="port">Port</option>
</select>
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Category</label>
<input
style={inputStyle}
value={form.category}
onChange={(e) => { setForm({ category: e.target.value }); }}
placeholder="General"
/>
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>
Interval (seconds)
</label>
<input
style={inputStyle}
type="number"
value={form.intervalSeconds}
onChange={(e) => { setForm({ intervalSeconds: e.target.value }); }}
/>
</div>
{(form.type === "https" || form.type === "https_keyword" || form.type === "https_status") && (
<div style={{ gridColumn: "span 2" }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>URL</label>
<input
style={inputStyle}
value={form.url}
onChange={(e) => { setForm({ url: e.target.value }); }}
placeholder="https://example.com"
/>
</div>
)}
{form.type === "mongodb" && (
<div style={{ gridColumn: "span 2" }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Connection String</label>
<input
style={inputStyle}
value={form.url}
onChange={(e) => { setForm({ url: e.target.value }); }}
placeholder="mongodb+srv://user:password@cluster.mongodb.net/"
/>
</div>
)}
{form.type === "https_keyword" && (
<div style={{ gridColumn: "span 2" }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Keyword</label>
<input
style={inputStyle}
value={form.keyword}
onChange={(e) => { setForm({ keyword: e.target.value }); }}
placeholder="Expected text in response"
/>
</div>
)}
{form.type === "https_status" && (
<>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>
Allowed Status Codes (comma-separated)
</label>
<input
style={inputStyle}
value={form.allowedStatusCodes}
onChange={(e) => { setForm({ allowedStatusCodes: e.target.value }); }}
placeholder="200, 201"
/>
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>
Denied Status Codes (comma-separated)
</label>
<input
style={inputStyle}
value={form.deniedStatusCodes}
onChange={(e) => { setForm({ deniedStatusCodes: e.target.value }); }}
placeholder="500, 503"
/>
</div>
</>
)}
{form.type === "port" && (
<>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Host</label>
<input
style={inputStyle}
value={form.host}
onChange={(e) => { setForm({ host: e.target.value }); }}
placeholder="my-server.example.com"
/>
</div>
<div>
<label style={{ display: "block", fontSize: 13, marginBottom: 4, color: "#d4a5c7" }}>Port</label>
<input
style={inputStyle}
type="number"
value={form.port}
onChange={(e) => { setForm({ port: e.target.value }); }}
placeholder="22"
/>
</div>
</>
)}
<div style={{ gridColumn: "span 2" }}>
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer", color: "#d4a5c7", fontSize: 13 }}>
<input
type="checkbox"
checked={form.isPublic}
onChange={(e) => { setForm({ isPublic: e.target.checked }); }}
style={{ accentColor: "#a8577e", width: 16, height: 16 }}
/>
Show on public status page
</label>
</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
<button
onClick={() => void handleSubmit()}
style={{
background: "#a8577e",
color: "#f5f5f5",
border: "none",
borderRadius: 8,
padding: "8px 20px",
cursor: "pointer",
fontSize: 14,
fontFamily: "Kalam, sans-serif",
transition: "opacity 0.3s ease",
}}
>
{editing ? "Save Changes" : "Create Monitor"}
</button>
<button
onClick={() => { setShowForm(false); }}
style={{
background: "transparent",
color: "#d4a5c7",
border: "1px solid #44275a",
borderRadius: 8,
padding: "8px 20px",
cursor: "pointer",
fontSize: 14,
fontFamily: "Kalam, sans-serif",
}}
>
Cancel
</button>
</div>
</div>
)}
<div style={{ background: "#44275a", border: "1px solid #a8577e", borderRadius: 12, overflow: "hidden" }}>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ background: "#0a0009", fontSize: 12, color: "#d4a5c7" }}>
{(["name", "type", "category", "target", "intervalSeconds", "enabled", "isPublic"] as SortKey[]).map((col) => (
<th
key={col}
onClick={() => { handleSort(col); }}
style={{ padding: "8px 20px", textAlign: "left", cursor: "pointer", userSelect: "none" }}
>
{col === "intervalSeconds" ? "Interval" : col === "isPublic" ? "Public" : col.charAt(0).toUpperCase() + col.slice(1)}
{sortIcon(col)}
</th>
))}
<th style={{ padding: "8px 20px", textAlign: "left" }}>Actions</th>
</tr>
</thead>
<tbody>
{sortedMonitors.map((m) => (
<tr key={m.id} style={{ borderTop: "1px solid #2b1b3d", fontSize: 14 }}>
<td style={{ padding: "10px 20px", color: "#f5f5f5" }}>{m.name}</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.type}</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.category}</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7", maxWidth: 180, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{m.url ?? (m.host && m.port ? `${m.host}:${m.port}` : "—")}
</td>
<td style={{ padding: "10px 20px", color: "#d4a5c7" }}>{m.intervalSeconds}s</td>
<td style={{ padding: "10px 20px" }}>
<button
onClick={() => void handleToggleEnabled(m)}
style={{
background: m.enabled ? "rgba(74, 222, 128, 0.15)" : "rgba(248, 113, 113, 0.15)",
color: m.enabled ? "#4ade80" : "#f87171",
border: `1px solid ${m.enabled ? "#4ade80" : "#f87171"}`,
borderRadius: 4,
padding: "2px 8px",
cursor: "pointer",
fontSize: 12,
fontFamily: "Kalam, sans-serif",
}}
>
{m.enabled ? "On" : "Off"}
</button>
</td>
<td style={{ padding: "10px 20px" }}>
<button
onClick={() => void handleTogglePublic(m)}
style={{
background: m.isPublic ? "rgba(74, 222, 128, 0.15)" : "rgba(192, 192, 192, 0.15)",
color: m.isPublic ? "#4ade80" : "#c0c0c0",
border: `1px solid ${m.isPublic ? "#4ade80" : "#c0c0c0"}`,
borderRadius: 4,
padding: "2px 8px",
cursor: "pointer",
fontSize: 12,
fontFamily: "Kalam, sans-serif",
}}
>
{m.isPublic ? "Visible" : "Hidden"}
</button>
</td>
<td style={{ padding: "10px 20px" }}>
<button
onClick={() => { openEdit(m); }}
style={{
background: "transparent",
border: "1px solid #a8577e",
color: "#d4a5c7",
borderRadius: 4,
padding: "2px 10px",
cursor: "pointer",
fontSize: 12,
marginRight: 6,
fontFamily: "Kalam, sans-serif",
}}
>
Edit
</button>
<button
onClick={() => void handleDelete(m.id)}
style={{
background: "transparent",
border: "1px solid #f87171",
color: "#f87171",
borderRadius: 4,
padding: "2px 10px",
cursor: "pointer",
fontSize: 12,
fontFamily: "Kalam, sans-serif",
}}
>
Delete
</button>
</td>
</tr>
))}
{monitors.length === 0 && (
<tr>
<td colSpan={8} style={{ padding: 40, textAlign: "center", color: "#c0c0c0" }}>
No monitors yet. Add one above!
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
+277
View File
@@ -0,0 +1,277 @@
import { useEffect, useState } from "react";
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { MonitorHistory, StatusResponse } from "../types.js";
const formatTime = (iso: string): string =>
new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
const MonitorCard = ({
monitor,
}: {
monitor: StatusResponse["monitors"][number];
}): React.ReactElement => {
const [history, setHistory] = useState<MonitorHistory[]>([]);
useEffect(() => {
fetch(`/api/monitors/${monitor.id}/history?limit=20`)
.then(async (res) => res.json())
.then((data) => {
setHistory((data as MonitorHistory[]).toReversed());
});
}, [monitor.id]);
const chartData = history.map((h) => ({
time: formatTime(h.checkedAt),
ms: h.responseTimeMs,
}));
return (
<div
style={{
border: "1px solid #44275a",
borderRadius: 12,
padding: 16,
marginBottom: 16,
background: "#44275a",
}}
>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
<div>
<strong style={{ fontSize: 16, color: "#f5f5f5" }}>{monitor.name}</strong>
<span
style={{
marginLeft: 8,
fontSize: 12,
color: "#d4a5c7",
textTransform: "uppercase",
}}
>
{monitor.type}
</span>
</div>
<span
style={{
fontWeight: 700,
color:
monitor.status === "up"
? "#4ade80"
: monitor.status === "down"
? "#f87171"
: "#c0c0c0",
}}
>
{monitor.status.toUpperCase()}
</span>
</div>
{monitor.status === "down" && monitor.message !== null && (
<div style={{ fontSize: 12, color: "#f87171", marginTop: 4 }}>
{monitor.message}
</div>
)}
{monitor.responseTimeMs !== null && (
<div style={{ fontSize: 12, color: "#d4a5c7", marginTop: 4 }}>
{monitor.responseTimeMs}ms
{monitor.lastChecked &&
` · Last checked ${formatTime(monitor.lastChecked)}`}
</div>
)}
{chartData.length > 0 && (
<ResponsiveContainer width="100%" height={80}>
<LineChart data={chartData} margin={{ top: 8, right: 0, left: -32, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#2b1b3d" />
<XAxis dataKey="time" tick={{ fontSize: 9, fill: "#d4a5c7" }} />
<YAxis tick={{ fontSize: 9, fill: "#d4a5c7" }} />
<Tooltip
formatter={(value: number) => [`${value}ms`, "Response time"]}
contentStyle={{ background: "#0a0009", border: "1px solid #44275a", color: "#f5f5f5" }}
/>
<Line
type="monotone"
dataKey="ms"
stroke="#a8577e"
dot={false}
strokeWidth={1.5}
/>
</LineChart>
</ResponsiveContainer>
)}
</div>
);
};
export const StatusPage = (): React.ReactElement => {
const [data, setData] = useState<StatusResponse | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const load = (): void => {
fetch("/api/status")
.then(async (res) => res.json())
.then((d) => {
setData(d as StatusResponse);
})
.catch(() => {
setError(true);
});
};
load();
const interval = setInterval(load, 30_000);
return () => {
clearInterval(interval);
};
}, []);
const allUp = data?.monitors.every((m) => m.status === "up");
const grouped = data?.monitors.reduce<Record<string, StatusResponse["monitors"]>>(
(acc, monitor) => {
const { category } = monitor;
if (acc[category] === undefined) {
acc[category] = [];
}
acc[category].push(monitor);
return acc;
},
{},
) ?? {};
const categories = Object.keys(grouped).sort();
return (
<div
style={{
maxWidth: 800,
margin: "0 auto",
padding: "32px 16px",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 24,
}}
>
<h1 style={{ margin: 0, fontSize: 28, color: "#d4a5c7" }}>Oriana Status</h1>
<a href="/login" style={{ fontSize: 14, color: "#a8577e" }}>
Admin
</a>
</div>
{error && (
<div style={{ color: "#f87171", marginBottom: 16 }}>
Failed to load status data.
</div>
)}
{data && (
<>
<div
style={{
borderRadius: 12,
padding: 16,
marginBottom: 24,
background: "#0a0009",
border: `1px solid ${allUp ? "#4ade80" : "#f87171"}`,
color: allUp ? "#4ade80" : "#f87171",
fontWeight: 600,
}}
>
{allUp
? "All systems operational"
: "One or more systems are experiencing issues"}
</div>
{data.maintenance.length > 0 && (
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18, marginBottom: 12, color: "#d4a5c7" }}>
Scheduled Maintenance
</h2>
{data.maintenance.map((w) => (
<div
key={w.id}
style={{
border: "1px solid #a8577e",
borderRadius: 12,
padding: 12,
marginBottom: 8,
background: "#44275a",
}}
>
<strong style={{ color: "#f5f5f5" }}>{w.title}</strong>
<div style={{ fontSize: 13, marginTop: 4, color: "#d4a5c7" }}>{w.message}</div>
<div style={{ fontSize: 12, color: "#c0c0c0", marginTop: 4 }}>
{new Date(w.startTime).toLocaleString()} {" "}
{new Date(w.endTime).toLocaleString()}
</div>
</div>
))}
</section>
)}
{data.incidents.length > 0 && (
<section style={{ marginBottom: 24 }}>
<h2 style={{ fontSize: 18, marginBottom: 12, color: "#d4a5c7" }}>
Active Incidents
</h2>
{data.incidents.map((incident) => (
<div
key={incident.id}
style={{
border: "1px solid #f87171",
borderRadius: 12,
padding: 12,
marginBottom: 8,
background: "#44275a",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
}}
>
<strong style={{ color: "#f5f5f5" }}>{incident.title}</strong>
<span
style={{
fontSize: 12,
textTransform: "uppercase",
color: "#f87171",
}}
>
{incident.status}
</span>
</div>
<div style={{ fontSize: 13, marginTop: 4, color: "#d4a5c7" }}>
{incident.message}
</div>
</div>
))}
</section>
)}
{categories.map((category) => (
<section key={category} style={{ marginBottom: 32 }}>
<h2 style={{ fontSize: 18, marginBottom: 12, color: "#d4a5c7" }}>
{category}
</h2>
{grouped[category].map((monitor) => (
<MonitorCard key={monitor.id} monitor={monitor} />
))}
</section>
))}
</>
)}
</div>
);
};
+77
View File
@@ -0,0 +1,77 @@
export type MonitorType = "https" | "https_keyword" | "https_status" | "mongodb" | "port";
export type MonitorStatus = "up" | "down" | "unknown";
export type IncidentStatus =
| "investigating"
| "identified"
| "monitoring"
| "resolved";
export interface MonitorSummary {
category: string;
id: string;
lastChecked: string | null;
message: string | null;
name: string;
responseTimeMs: number | null;
status: MonitorStatus;
statusCode: number | null;
type: MonitorType;
}
export interface Monitor {
allowedStatusCodes: string | null;
category: string;
createdAt: string;
deniedStatusCodes: string | null;
enabled: number;
host: string | null;
id: string;
intervalSeconds: number;
isPublic: number;
keyword: string | null;
name: string;
port: number | null;
type: MonitorType;
url: string | null;
}
export interface MonitorHistory {
checkedAt: string;
id: string;
message: string | null;
monitorId: string;
responseTimeMs: number | null;
status: "up" | "down";
statusCode: number | null;
}
export interface Incident {
createdAt: string;
id: string;
message: string;
status: IncidentStatus;
title: string;
updatedAt: string;
}
export interface MaintenanceWindow {
createdAt: string;
endTime: string;
id: string;
message: string;
startTime: string;
title: string;
}
export interface StatusResponse {
monitors: MonitorSummary[];
incidents: Incident[];
maintenance: MaintenanceWindow[];
}
export interface CurrentUser {
userId: string;
username: string;
globalName: string | null;
avatar: string | null;
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2023",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:3000",
"/auth": "http://localhost:3000",
},
},
});