feat(admin): add full alerts dashboard functionality
This commit is contained in:
@@ -21,6 +21,7 @@ type Handlers struct {
|
|||||||
AdminLoginPost gin.HandlerFunc
|
AdminLoginPost gin.HandlerFunc
|
||||||
AdminLogout gin.HandlerFunc
|
AdminLogout gin.HandlerFunc
|
||||||
AdminDashboard gin.HandlerFunc
|
AdminDashboard gin.HandlerFunc
|
||||||
|
AdminAlerts gin.HandlerFunc
|
||||||
AdminAuth gin.HandlerFunc
|
AdminAuth gin.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +51,5 @@ func Register(router *gin.Engine, handlers Handlers) {
|
|||||||
|
|
||||||
protected := router.Group("/admin", handlers.AdminAuth)
|
protected := router.Group("/admin", handlers.AdminAuth)
|
||||||
protected.GET("/dashboard", handlers.AdminDashboard)
|
protected.GET("/dashboard", handlers.AdminDashboard)
|
||||||
|
protected.GET("/alerts", handlers.AdminAlerts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,20 @@ func (app *App) handleAdminDashboard(ctx *gin.Context) {
|
|||||||
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
||||||
"AdminUsername": app.config.AdminUsername,
|
"AdminUsername": app.config.AdminUsername,
|
||||||
"AdminEmail": app.config.AdminEmail,
|
"AdminEmail": app.config.AdminEmail,
|
||||||
|
"ActivePage": "dashboard",
|
||||||
"DashboardEnabled": string(dashboardEnabled),
|
"DashboardEnabled": string(dashboardEnabled),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
||||||
|
if !app.adminLoginEnabled() {
|
||||||
|
ctx.Redirect(http.StatusSeeOther, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
||||||
|
"AdminUsername": app.config.AdminUsername,
|
||||||
|
"AdminEmail": app.config.AdminEmail,
|
||||||
|
"ActivePage": "alerts",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func Run(addr string) error {
|
|||||||
AdminLoginPost: app.handleAdminLoginPost,
|
AdminLoginPost: app.handleAdminLoginPost,
|
||||||
AdminLogout: app.handleAdminLogout,
|
AdminLogout: app.handleAdminLogout,
|
||||||
AdminDashboard: app.handleAdminDashboard,
|
AdminDashboard: app.handleAdminDashboard,
|
||||||
|
AdminAlerts: app.handleAdminAlerts,
|
||||||
AdminAuth: app.adminAuthMiddleware,
|
AdminAuth: app.adminAuthMiddleware,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,8 @@
|
|||||||
/* ===========================
|
/* ===========================
|
||||||
Dashboard Window
|
Dashboard Window
|
||||||
=========================== */
|
=========================== */
|
||||||
.admin-dashboard-window {
|
.admin-dashboard-window,
|
||||||
|
.admin-workspace-window {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -215,11 +216,13 @@
|
|||||||
background-image: linear-gradient(180deg, rgba(255,255,255,.24), rgba(0,0,0,.06));
|
background-image: linear-gradient(180deg, rgba(255,255,255,.24), rgba(0,0,0,.06));
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-window > .win98-titlebar {
|
.admin-dashboard-window > .win98-titlebar,
|
||||||
|
.admin-workspace-window > .win98-titlebar {
|
||||||
margin: 2px 2px 0;
|
margin: 2px 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-window > .menu-bar {
|
.admin-dashboard-window > .menu-bar,
|
||||||
|
.admin-workspace-window > .menu-bar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
@@ -234,11 +237,13 @@
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-window > .menu-bar .menu-button {
|
.admin-dashboard-window > .menu-bar .menu-button,
|
||||||
|
.admin-workspace-window > .menu-bar .menu-button {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-window > .dashboard-body {
|
.admin-dashboard-window > .dashboard-body,
|
||||||
|
.admin-workspace-window > .admin-workspace-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding: 0 10px 10px;
|
padding: 0 10px 10px;
|
||||||
@@ -673,7 +678,8 @@ body.is-compact .admin-section-body {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dashboard-window {
|
.admin-dashboard-window,
|
||||||
|
.admin-workspace-window {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
|
|||||||
394
static/css/alerts.css
Normal file
394
static/css/alerts.css
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
.alerts-page-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-stat-card {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #808080;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
|
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-stat-card.is-danger { background: linear-gradient(180deg, #ffd8d8, #f1b3b3); }
|
||||||
|
.alerts-stat-card.is-warning { background: linear-gradient(180deg, #fff1c9, #ffe39f); }
|
||||||
|
.alerts-stat-card.is-info { background: linear-gradient(180deg, #d7e6fb, #bfd7f8); }
|
||||||
|
|
||||||
|
.alerts-stat-label {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-stat-value {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-stat-note {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0 6px;
|
||||||
|
color: #222222;
|
||||||
|
background: rgba(255,255,255,.65);
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #a0a0a0;
|
||||||
|
border-bottom: 1px solid #a0a0a0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.3fr) minmax(320px, .7fr);
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-list-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-actions-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
box-shadow: inset 1px 1px 0 rgba(255,255,255,.7), inset -1px -1px 0 rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-bottom: 1px solid #b0b0b0;
|
||||||
|
box-shadow: inset 1px 1px 0 #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-sub {
|
||||||
|
color: #444444;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image: linear-gradient(180deg, rgba(255,255,255,.9), rgba(238,238,238,.58));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-tool-button,
|
||||||
|
.alerts-row-button,
|
||||||
|
.alerts-footer-button {
|
||||||
|
min-width: 64px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-action-button {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-toolbar-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 1.2fr) repeat(4, minmax(110px, .6fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-input,
|
||||||
|
.alerts-select,
|
||||||
|
.alerts-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-input,
|
||||||
|
.alerts-select {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-table-wrap {
|
||||||
|
height: 430px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 2px solid #606060;
|
||||||
|
border-left: 2px solid #606060;
|
||||||
|
border-right: 2px solid #ffffff;
|
||||||
|
border-bottom: 2px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 14px;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 6px;
|
||||||
|
text-align: left;
|
||||||
|
background: #dfdfdf;
|
||||||
|
border-bottom: 1px solid #b0b0b0;
|
||||||
|
box-shadow: inset 0 1px 0 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-table tbody tr:nth-child(odd) { background: rgba(255,255,255,.96); }
|
||||||
|
.alerts-table tbody tr:nth-child(even) { background: rgba(240,244,255,.9); }
|
||||||
|
.alerts-table tbody tr:hover { background: #d8e5f8; }
|
||||||
|
.alerts-table tbody tr.is-selected { background: #c5dcff; }
|
||||||
|
|
||||||
|
.alerts-table td {
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid #e1e1e1;
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-col-check { width: 34px; }
|
||||||
|
.alerts-col-severity { width: 76px; }
|
||||||
|
.alerts-col-status { width: 82px; }
|
||||||
|
.alerts-col-code { width: 70px; }
|
||||||
|
.alerts-col-time { width: 110px; }
|
||||||
|
.alerts-col-actions { width: 88px; }
|
||||||
|
|
||||||
|
.alerts-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0 6px;
|
||||||
|
color: #222222;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #b0b0b0;
|
||||||
|
border-bottom: 1px solid #b0b0b0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-pill.low { background: #deebff; }
|
||||||
|
.alerts-pill.medium { background: #fff2c8; }
|
||||||
|
.alerts-pill.high { background: #ffdcdc; }
|
||||||
|
.alerts-pill.open { background: #f2e1ff; }
|
||||||
|
.alerts-pill.acked { background: #e2f0e2; }
|
||||||
|
.alerts-pill.closed { background: #ececec; }
|
||||||
|
|
||||||
|
.alerts-info-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-info-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 110px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #c0c0c0;
|
||||||
|
border-bottom: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-info-item strong {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-info-item span {
|
||||||
|
min-width: 0;
|
||||||
|
color: #222222;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-json-box {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
color: #b7ffc8;
|
||||||
|
background: #050505;
|
||||||
|
border-top: 2px solid #808080;
|
||||||
|
border-left: 2px solid #808080;
|
||||||
|
border-right: 2px solid #ffffff;
|
||||||
|
border-bottom: 2px solid #ffffff;
|
||||||
|
font-family: "MonoCraft", "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-mini-note {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffcc;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
border-left: 1px solid #ffffff;
|
||||||
|
border-right: 1px solid #a08000;
|
||||||
|
border-bottom: 1px solid #a08000;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-action-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-footerbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 42px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-top: 1px solid #ffffff;
|
||||||
|
background: #dfdfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-footer-left,
|
||||||
|
.alerts-footer-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-status-pill {
|
||||||
|
min-height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #808080;
|
||||||
|
border-left: 1px solid #808080;
|
||||||
|
border-right: 1px solid #ffffff;
|
||||||
|
border-bottom: 1px solid #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1120px) {
|
||||||
|
.alerts-summary-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-toolbar-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.alerts-summary-grid,
|
||||||
|
.alerts-toolbar-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-table-wrap {
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-panel-header,
|
||||||
|
.alerts-footerbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-info-item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
216
static/js/admin/alerts.js
Normal file
216
static/js/admin/alerts.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
(() => {
|
||||||
|
const menuController = window.WarpBoxUI?.bindMenuBar?.() || {
|
||||||
|
close() {
|
||||||
|
document.querySelectorAll(".menu-item.is-open").forEach((item) => {
|
||||||
|
item.classList.remove("is-open");
|
||||||
|
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const toast = document.getElementById("toast");
|
||||||
|
const searchInput = document.getElementById("search-input");
|
||||||
|
const severityFilter = document.getElementById("severity-filter");
|
||||||
|
const statusFilter = document.getElementById("status-filter");
|
||||||
|
const sourceFilter = document.getElementById("source-filter");
|
||||||
|
const sortFilter = document.getElementById("sort-filter");
|
||||||
|
const alertsBody = document.getElementById("alerts-body");
|
||||||
|
const selectedCountEl = document.getElementById("selected-count");
|
||||||
|
const openCountEl = document.querySelector("[data-open-count]");
|
||||||
|
const highCountEl = document.querySelector("[data-high-count]");
|
||||||
|
const ackCountEl = document.querySelector("[data-ack-count]");
|
||||||
|
const closedCountEl = document.querySelector("[data-closed-count]");
|
||||||
|
const selectAll = document.getElementById("select-all");
|
||||||
|
|
||||||
|
const detailEls = {
|
||||||
|
title: document.getElementById("detail-title"),
|
||||||
|
severity: document.getElementById("detail-severity"),
|
||||||
|
status: document.getElementById("detail-status"),
|
||||||
|
code: document.getElementById("detail-code"),
|
||||||
|
trace: document.getElementById("detail-trace"),
|
||||||
|
time: document.getElementById("detail-time"),
|
||||||
|
description: document.getElementById("detail-description"),
|
||||||
|
metadata: document.getElementById("detail-metadata")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!alertsBody || !searchInput || !statusFilter || !selectedCountEl) return;
|
||||||
|
|
||||||
|
function showToast(message, type = "info", duration = 1800) {
|
||||||
|
if (window.WarpBoxUI) {
|
||||||
|
window.WarpBoxUI.toast(message, type, { target: toast, duration });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!toast) return;
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add("is-visible");
|
||||||
|
window.setTimeout(() => toast.classList.remove("is-visible"), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allRows() {
|
||||||
|
return Array.from(alertsBody.querySelectorAll("tr"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleRows() {
|
||||||
|
return allRows().filter((row) => row.style.display !== "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedRows() {
|
||||||
|
return allRows().filter((row) => row.querySelector(".row-check")?.checked && row.style.display !== "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedCount() {
|
||||||
|
selectedCountEl.textContent = `Selected: ${selectedRows().length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummaryCounts() {
|
||||||
|
const rows = visibleRows();
|
||||||
|
openCountEl.textContent = String(rows.filter((row) => row.dataset.status === "open").length);
|
||||||
|
highCountEl.textContent = String(rows.filter((row) => row.dataset.severity === "high" && row.dataset.status !== "closed").length);
|
||||||
|
ackCountEl.textContent = String(rows.filter((row) => row.dataset.status === "acked").length);
|
||||||
|
closedCountEl.textContent = String(rows.filter((row) => row.dataset.status === "closed").length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDetails(row) {
|
||||||
|
if (!row) return;
|
||||||
|
allRows().forEach((item) => item.classList.remove("is-selected"));
|
||||||
|
row.classList.add("is-selected");
|
||||||
|
detailEls.title.textContent = row.dataset.title || "";
|
||||||
|
detailEls.severity.textContent = row.dataset.severity || "";
|
||||||
|
detailEls.status.textContent = row.dataset.status || "";
|
||||||
|
detailEls.code.textContent = row.dataset.code || "";
|
||||||
|
detailEls.trace.textContent = row.dataset.trace || "";
|
||||||
|
detailEls.time.textContent = row.dataset.time || "";
|
||||||
|
detailEls.description.textContent = row.dataset.description || "";
|
||||||
|
try {
|
||||||
|
detailEls.metadata.textContent = JSON.stringify(JSON.parse(row.dataset.metadata || "{}"), null, 2);
|
||||||
|
} catch (_) {
|
||||||
|
detailEls.metadata.textContent = row.dataset.metadata || "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const search = searchInput.value.trim().toLowerCase();
|
||||||
|
const severity = severityFilter.value;
|
||||||
|
const status = statusFilter.value;
|
||||||
|
const group = sourceFilter.value;
|
||||||
|
|
||||||
|
allRows().forEach((row) => {
|
||||||
|
const haystack = [
|
||||||
|
row.dataset.title,
|
||||||
|
row.dataset.description,
|
||||||
|
row.dataset.code,
|
||||||
|
row.dataset.trace,
|
||||||
|
row.dataset.group
|
||||||
|
].join(" ").toLowerCase();
|
||||||
|
const matchesSearch = !search || haystack.includes(search);
|
||||||
|
const matchesSeverity = severity === "all" || row.dataset.severity === severity;
|
||||||
|
const matchesStatus = status === "all" || row.dataset.status === status;
|
||||||
|
const matchesGroup = group === "all" || row.dataset.group === group;
|
||||||
|
row.style.display = matchesSearch && matchesSeverity && matchesStatus && matchesGroup ? "" : "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = { high: 3, medium: 2, low: 1 };
|
||||||
|
visibleRows().sort((a, b) => {
|
||||||
|
if (sortFilter.value === "severity") return order[b.dataset.severity] - order[a.dataset.severity];
|
||||||
|
if (sortFilter.value === "oldest") return Number(a.dataset.id) - Number(b.dataset.id);
|
||||||
|
return Number(b.dataset.id) - Number(a.dataset.id);
|
||||||
|
}).forEach((row) => alertsBody.appendChild(row));
|
||||||
|
|
||||||
|
const selectedVisible = visibleRows().find((row) => row.classList.contains("is-selected"));
|
||||||
|
if (!selectedVisible && visibleRows()[0]) updateDetails(visibleRows()[0]);
|
||||||
|
updateSelectedCount();
|
||||||
|
updateSummaryCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRowStatus(row, nextStatus) {
|
||||||
|
row.dataset.status = nextStatus;
|
||||||
|
const statusCell = row.children[3]?.querySelector(".alerts-pill");
|
||||||
|
if (!statusCell) return;
|
||||||
|
statusCell.className = `alerts-pill ${nextStatus}`;
|
||||||
|
statusCell.textContent = nextStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSelectedStatus(nextStatus) {
|
||||||
|
const rows = selectedRows();
|
||||||
|
if (!rows.length) {
|
||||||
|
showToast("Select one or more alerts first", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
setRowStatus(row, nextStatus);
|
||||||
|
row.querySelector(".row-check").checked = false;
|
||||||
|
});
|
||||||
|
if (selectAll) selectAll.checked = false;
|
||||||
|
updateSelectedCount();
|
||||||
|
updateSummaryCounts();
|
||||||
|
|
||||||
|
const currentRow = visibleRows().find((row) => row.classList.contains("is-selected")) || visibleRows()[0];
|
||||||
|
if (currentRow) updateDetails(currentRow);
|
||||||
|
showToast(nextStatus === "acked" ? "Selected alerts acknowledged" : "Selected alerts closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandMessages = {
|
||||||
|
refresh: "Alerts refreshed in mock view",
|
||||||
|
export: "Visible alerts exported in mock view",
|
||||||
|
"copy-meta": "Metadata copied in mock view",
|
||||||
|
"help-codes": "Each alert code maps to a unique trigger point and trace identifier.",
|
||||||
|
"help-meta": "Metadata explains why the alert happened and includes extra context."
|
||||||
|
};
|
||||||
|
|
||||||
|
function runCommand(command) {
|
||||||
|
switch (command) {
|
||||||
|
case "ack":
|
||||||
|
changeSelectedStatus("acked");
|
||||||
|
return;
|
||||||
|
case "close":
|
||||||
|
changeSelectedStatus("closed");
|
||||||
|
return;
|
||||||
|
case "open-only":
|
||||||
|
statusFilter.value = "open";
|
||||||
|
applyFilters();
|
||||||
|
showToast("Showing open alerts only");
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
showToast(commandMessages[command] || `Mock action: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[searchInput, severityFilter, statusFilter, sourceFilter, sortFilter].forEach((control) => {
|
||||||
|
control.addEventListener(control.tagName === "INPUT" ? "input" : "change", applyFilters);
|
||||||
|
});
|
||||||
|
|
||||||
|
allRows().forEach((row) => {
|
||||||
|
row.addEventListener("click", (event) => {
|
||||||
|
if (event.target.closest("button") || event.target.closest("input")) return;
|
||||||
|
updateDetails(row);
|
||||||
|
});
|
||||||
|
row.querySelector(".row-open")?.addEventListener("click", () => updateDetails(row));
|
||||||
|
row.querySelector(".row-check")?.addEventListener("change", updateSelectedCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
selectAll?.addEventListener("change", () => {
|
||||||
|
visibleRows().forEach((row) => {
|
||||||
|
const checkbox = row.querySelector(".row-check");
|
||||||
|
if (checkbox) checkbox.checked = selectAll.checked;
|
||||||
|
});
|
||||||
|
updateSelectedCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-command]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
menuController.close();
|
||||||
|
runCommand(button.dataset.command);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") menuController.close();
|
||||||
|
if (event.key === "F5") {
|
||||||
|
event.preventDefault();
|
||||||
|
runCommand("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
updateDetails(allRows()[0]);
|
||||||
|
})();
|
||||||
@@ -96,8 +96,8 @@
|
|||||||
"dashboard-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard snapshot export would start here.",
|
"dashboard-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard snapshot export would start here.",
|
||||||
logout: "CURRENTLY_MOCKED_LEAVE_AS_IS: logout would submit to the account logout route.",
|
logout: "CURRENTLY_MOCKED_LEAVE_AS_IS: logout would submit to the account logout route.",
|
||||||
"compact-mode": "Toggled compact density.",
|
"compact-mode": "Toggled compact density.",
|
||||||
"show-all-boxes": "TO-DO: navigate to /account/boxes.",
|
"show-all-boxes": "TO-DO: navigate to the admin boxes view when that page exists.",
|
||||||
"show-all-alerts": "TO-DO: navigate to /account/alerts.",
|
"show-all-alerts": "TO-DO: navigate to /admin/alerts.",
|
||||||
"export-boxes": "CURRENTLY_MOCKED_LEAVE_AS_IS: boxes CSV export would be requested.",
|
"export-boxes": "CURRENTLY_MOCKED_LEAVE_AS_IS: boxes CSV export would be requested.",
|
||||||
"export-alerts": "CURRENTLY_MOCKED_LEAVE_AS_IS: alerts JSON export would be requested.",
|
"export-alerts": "CURRENTLY_MOCKED_LEAVE_AS_IS: alerts JSON export would be requested.",
|
||||||
"cleanup-dry-run": "CURRENTLY_MOCKED_LEAVE_AS_IS: cleanup dry run would calculate affected boxes without deleting.",
|
"cleanup-dry-run": "CURRENTLY_MOCKED_LEAVE_AS_IS: cleanup dry run would calculate affected boxes without deleting.",
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
"config-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: config snapshot would summarize runtime settings and sources.",
|
"config-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: config snapshot would summarize runtime settings and sources.",
|
||||||
"support-summary": "CURRENTLY_MOCKED_LEAVE_AS_IS: support summary would collect safe diagnostic information.",
|
"support-summary": "CURRENTLY_MOCKED_LEAVE_AS_IS: support summary would collect safe diagnostic information.",
|
||||||
"thumbnail-rebuild": "CURRENTLY_MOCKED_LEAVE_AS_IS: thumbnail rebuild would enqueue preview regeneration.",
|
"thumbnail-rebuild": "CURRENTLY_MOCKED_LEAVE_AS_IS: thumbnail rebuild would enqueue preview regeneration.",
|
||||||
"open-users": "TO-DO: navigate to /account/users for admins.",
|
"open-users": "TO-DO: navigate to the admin users view when that page exists.",
|
||||||
"open-settings": "TO-DO: navigate to /account/settings for admins.",
|
"open-settings": "TO-DO: navigate to the admin settings view when that page exists.",
|
||||||
"alerts-help": "Alerts use title, description, severity, metadata JSON, trace identifier, and unique numeric code.",
|
"alerts-help": "Alerts use title, description, severity, metadata JSON, trace identifier, and unique numeric code.",
|
||||||
shortcuts: "Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.",
|
shortcuts: "Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.",
|
||||||
about: "WarpBox dashboard mock v5, single-window Win98 account dashboard."
|
about: "WarpBox dashboard mock v5, single-window Win98 account dashboard."
|
||||||
|
|||||||
321
templates/admin/alerts.html
Normal file
321
templates/admin/alerts.html
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
{{ define "admin/alerts.html" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>WarpBox Admin Alerts</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/WarpBoxLogo.png">
|
||||||
|
<link rel="stylesheet" href="/static/css/app.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/window.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/components/buttons.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/components/toast.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/admin.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/alerts.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="admin-shell">
|
||||||
|
<div class="admin-frame">
|
||||||
|
{{ template "admin/header.html" . }}
|
||||||
|
|
||||||
|
<div class="win98-window admin-workspace-window" role="main">
|
||||||
|
<div class="win98-titlebar">
|
||||||
|
<div class="win98-titlebar-label">
|
||||||
|
<img class="win98-titlebar-icon" src="/static/WarpBoxLogo.png" alt="" aria-hidden="true">
|
||||||
|
<h1>WarpBox Alerts</h1>
|
||||||
|
</div>
|
||||||
|
<div class="win98-window-controls" aria-hidden="true">
|
||||||
|
<button class="win98-control" type="button">_</button>
|
||||||
|
<button class="win98-control" type="button">□</button>
|
||||||
|
<button class="win98-control" type="button">x</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="menu-bar" aria-label="Alerts toolbar">
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
||||||
|
<div class="menu-popup">
|
||||||
|
<button class="menu-action" type="button" data-command="refresh"><span>R</span><span>Refresh alerts</span><span class="shortcut">F5</span></button>
|
||||||
|
<button class="menu-action" type="button" data-command="export"><span>E</span><span>Export visible alerts</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false">Alerts</button>
|
||||||
|
<div class="menu-popup">
|
||||||
|
<button class="menu-action" type="button" data-command="ack"><span>A</span><span>Acknowledge selected</span><span></span></button>
|
||||||
|
<button class="menu-action" type="button" data-command="close"><span>C</span><span>Close selected</span><span></span></button>
|
||||||
|
<div class="menu-separator"></div>
|
||||||
|
<button class="menu-action" type="button" data-command="open-only"><span>O</span><span>Show open only</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item">
|
||||||
|
<button class="menu-button" type="button" aria-expanded="false">Help</button>
|
||||||
|
<div class="menu-popup">
|
||||||
|
<button class="menu-action" type="button" data-command="help-codes"><span>?</span><span>Tracing and codes</span><span></span></button>
|
||||||
|
<button class="menu-action" type="button" data-command="help-meta"><span>I</span><span>Metadata preview</span><span></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="admin-workspace-body alerts-page-body">
|
||||||
|
<section class="alerts-summary-grid" aria-label="Alerts summary">
|
||||||
|
<article class="alerts-stat-card is-danger">
|
||||||
|
<p class="alerts-stat-label">Open alerts</p>
|
||||||
|
<p class="alerts-stat-value" data-open-count>5</p>
|
||||||
|
<p class="alerts-stat-note">Requires attention</p>
|
||||||
|
</article>
|
||||||
|
<article class="alerts-stat-card is-warning">
|
||||||
|
<p class="alerts-stat-label">High severity</p>
|
||||||
|
<p class="alerts-stat-value" data-high-count>2</p>
|
||||||
|
<p class="alerts-stat-note">Escalate first</p>
|
||||||
|
</article>
|
||||||
|
<article class="alerts-stat-card is-info">
|
||||||
|
<p class="alerts-stat-label">Acknowledged</p>
|
||||||
|
<p class="alerts-stat-value" data-ack-count>3</p>
|
||||||
|
<p class="alerts-stat-note">Seen but not closed</p>
|
||||||
|
</article>
|
||||||
|
<article class="alerts-stat-card is-info">
|
||||||
|
<p class="alerts-stat-label">Closed today</p>
|
||||||
|
<p class="alerts-stat-value" data-closed-count>2</p>
|
||||||
|
<p class="alerts-stat-note">History stays lightweight</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="alerts-content-grid">
|
||||||
|
<div class="alerts-column">
|
||||||
|
<section class="alerts-panel alerts-list-panel">
|
||||||
|
<div class="alerts-panel-header">
|
||||||
|
<div class="alerts-panel-title">Alert list <span class="alerts-panel-sub">search, filter, review</span></div>
|
||||||
|
<div class="alerts-panel-tools">
|
||||||
|
<button class="win98-button alerts-tool-button" type="button" data-command="ack">Acknowledge</button>
|
||||||
|
<button class="win98-button alerts-tool-button" type="button" data-command="close">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-panel-body">
|
||||||
|
<div class="alerts-toolbar-grid">
|
||||||
|
<input class="alerts-input" id="search-input" type="search" placeholder="Search title, code, trace or text">
|
||||||
|
<select class="alerts-select" id="severity-filter">
|
||||||
|
<option value="all" selected>All severities</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
</select>
|
||||||
|
<select class="alerts-select" id="status-filter">
|
||||||
|
<option value="all" selected>All statuses</option>
|
||||||
|
<option value="open">Open</option>
|
||||||
|
<option value="acked">Acknowledged</option>
|
||||||
|
<option value="closed">Closed</option>
|
||||||
|
</select>
|
||||||
|
<select class="alerts-select" id="source-filter">
|
||||||
|
<option value="all" selected>All groups</option>
|
||||||
|
<option value="thumbnails">Thumbnails</option>
|
||||||
|
<option value="storage">Storage</option>
|
||||||
|
<option value="uploads">Uploads</option>
|
||||||
|
<option value="auth">Auth</option>
|
||||||
|
</select>
|
||||||
|
<select class="alerts-select" id="sort-filter">
|
||||||
|
<option value="newest" selected>Newest first</option>
|
||||||
|
<option value="severity">Severity first</option>
|
||||||
|
<option value="oldest">Oldest first</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alerts-table-wrap">
|
||||||
|
<table class="alerts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="alerts-col-check"><input type="checkbox" id="select-all"></th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th class="alerts-col-severity">Severity</th>
|
||||||
|
<th class="alerts-col-status">Status</th>
|
||||||
|
<th class="alerts-col-code">Code</th>
|
||||||
|
<th>Trace</th>
|
||||||
|
<th class="alerts-col-time">Created</th>
|
||||||
|
<th class="alerts-col-actions">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="alerts-body">
|
||||||
|
<tr data-id="10" data-severity="high" data-status="open" data-group="storage" data-title="Storage connector unavailable" data-description="Primary local storage connector failed health check and new writes are paused." data-code="301" data-trace="storage.connector.health_failed" data-time="today 14:08" data-metadata='{"connector":"local-main","mode":"read_only","retry_in":"30s"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Storage connector unavailable</td>
|
||||||
|
<td><span class="alerts-pill high">high</span></td>
|
||||||
|
<td><span class="alerts-pill open">open</span></td>
|
||||||
|
<td>301</td>
|
||||||
|
<td>storage.connector.health_failed</td>
|
||||||
|
<td>today 14:08</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="9" data-severity="medium" data-status="open" data-group="thumbnails" data-title="Thumbnail generation failed" data-description="Thumbnail generation failed for one uploaded image. Original file remains available." data-code="601" data-trace="thumbnail.generate.failed" data-time="today 13:40" data-metadata='{"box":"bx_49aa","file":"poster.png","worker":"thumb-2"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Thumbnail generation failed</td>
|
||||||
|
<td><span class="alerts-pill medium">medium</span></td>
|
||||||
|
<td><span class="alerts-pill open">open</span></td>
|
||||||
|
<td>601</td>
|
||||||
|
<td>thumbnail.generate.failed</td>
|
||||||
|
<td>today 13:40</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="8" data-severity="low" data-status="acked" data-group="uploads" data-title="Large upload nearing account cap" data-description="A user is close to their daily upload budget." data-code="124" data-trace="upload.quota.nearing_cap" data-time="today 12:58" data-metadata='{"user":"geo","used":"44 GB","limit":"50 GB"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Large upload nearing account cap</td>
|
||||||
|
<td><span class="alerts-pill low">low</span></td>
|
||||||
|
<td><span class="alerts-pill acked">acked</span></td>
|
||||||
|
<td>124</td>
|
||||||
|
<td>upload.quota.nearing_cap</td>
|
||||||
|
<td>today 12:58</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="7" data-severity="high" data-status="open" data-group="auth" data-title="Repeated admin login failures" data-description="Multiple failed admin login attempts were detected from the same source." data-code="211" data-trace="auth.admin.failed_login_burst" data-time="today 12:10" data-metadata='{"ip":"198.51.100.4","attempts":7,"window":"10m"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Repeated admin login failures</td>
|
||||||
|
<td><span class="alerts-pill high">high</span></td>
|
||||||
|
<td><span class="alerts-pill open">open</span></td>
|
||||||
|
<td>211</td>
|
||||||
|
<td>auth.admin.failed_login_burst</td>
|
||||||
|
<td>today 12:10</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="6" data-severity="medium" data-status="acked" data-group="storage" data-title="Cleanup skipped locked files" data-description="Cleanup job encountered locked files and skipped them." data-code="342" data-trace="cleanup.skip.locked_files" data-time="today 10:22" data-metadata='{"count":3,"connector":"local-main"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Cleanup skipped locked files</td>
|
||||||
|
<td><span class="alerts-pill medium">medium</span></td>
|
||||||
|
<td><span class="alerts-pill acked">acked</span></td>
|
||||||
|
<td>342</td>
|
||||||
|
<td>cleanup.skip.locked_files</td>
|
||||||
|
<td>today 10:22</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="5" data-severity="low" data-status="closed" data-group="uploads" data-title="Archive completed with warnings" data-description="ZIP archive completed but excluded one unreadable temporary file." data-code="145" data-trace="archive.complete.with_warning" data-time="today 09:02" data-metadata='{"box":"bx_3901","skipped":1}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Archive completed with warnings</td>
|
||||||
|
<td><span class="alerts-pill low">low</span></td>
|
||||||
|
<td><span class="alerts-pill closed">closed</span></td>
|
||||||
|
<td>145</td>
|
||||||
|
<td>archive.complete.with_warning</td>
|
||||||
|
<td>today 09:02</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="4" data-severity="medium" data-status="open" data-group="uploads" data-title="Upload session expired mid-transfer" data-description="A long-running upload lost session validity before final commit." data-code="156" data-trace="upload.session.expired_mid_transfer" data-time="yesterday" data-metadata='{"user":"teo","partial_bytes":"1.2 GB"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Upload session expired mid-transfer</td>
|
||||||
|
<td><span class="alerts-pill medium">medium</span></td>
|
||||||
|
<td><span class="alerts-pill open">open</span></td>
|
||||||
|
<td>156</td>
|
||||||
|
<td>upload.session.expired_mid_transfer</td>
|
||||||
|
<td>yesterday</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="3" data-severity="low" data-status="closed" data-group="thumbnails" data-title="Thumbnail worker restarted" data-description="Thumbnail worker restarted after a normal watchdog recycle." data-code="602" data-trace="thumbnail.worker.restarted" data-time="yesterday" data-metadata='{"worker":"thumb-1","reason":"watchdog"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Thumbnail worker restarted</td>
|
||||||
|
<td><span class="alerts-pill low">low</span></td>
|
||||||
|
<td><span class="alerts-pill closed">closed</span></td>
|
||||||
|
<td>602</td>
|
||||||
|
<td>thumbnail.worker.restarted</td>
|
||||||
|
<td>yesterday</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="2" data-severity="medium" data-status="acked" data-group="auth" data-title="User invited without email delivery confirmation" data-description="Invite creation succeeded but email delivery confirmation was not returned." data-code="224" data-trace="auth.invite.delivery_unknown" data-time="2 days ago" data-metadata='{"user":"reo","provider":"smtp-primary"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>User invited without email delivery confirmation</td>
|
||||||
|
<td><span class="alerts-pill medium">medium</span></td>
|
||||||
|
<td><span class="alerts-pill acked">acked</span></td>
|
||||||
|
<td>224</td>
|
||||||
|
<td>auth.invite.delivery_unknown</td>
|
||||||
|
<td>2 days ago</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr data-id="1" data-severity="low" data-status="closed" data-group="storage" data-title="Secondary connector caught up" data-description="Delayed sync on a secondary storage connector completed successfully." data-code="329" data-trace="storage.secondary.sync_recovered" data-time="2 days ago" data-metadata='{"connector":"bucket-archive","lag":"0"}'>
|
||||||
|
<td><input type="checkbox" class="row-check"></td>
|
||||||
|
<td>Secondary connector caught up</td>
|
||||||
|
<td><span class="alerts-pill low">low</span></td>
|
||||||
|
<td><span class="alerts-pill closed">closed</span></td>
|
||||||
|
<td>329</td>
|
||||||
|
<td>storage.secondary.sync_recovered</td>
|
||||||
|
<td>2 days ago</td>
|
||||||
|
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alerts-column alerts-column-side">
|
||||||
|
<section class="alerts-panel">
|
||||||
|
<div class="alerts-panel-header">
|
||||||
|
<div class="alerts-panel-title">Alert details <span class="alerts-panel-sub">selected alert preview</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-panel-body">
|
||||||
|
<ul class="alerts-info-list">
|
||||||
|
<li class="alerts-info-item"><strong>Title</strong><span id="detail-title">Storage connector unavailable</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Severity</strong><span id="detail-severity">high</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Status</strong><span id="detail-status">open</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Code</strong><span id="detail-code">301</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Trace</strong><span id="detail-trace">storage.connector.health_failed</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Created</strong><span id="detail-time">today 14:08</span></li>
|
||||||
|
<li class="alerts-info-item"><strong>Description</strong><span id="detail-description">Primary local storage connector failed health check and new writes are paused.</span></li>
|
||||||
|
</ul>
|
||||||
|
<div class="alerts-mini-note">
|
||||||
|
TO-DO: later, limited alert access should only show alerts scoped to the user’s permissions, tags, or groups.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="alerts-panel">
|
||||||
|
<div class="alerts-panel-header">
|
||||||
|
<div class="alerts-panel-title">Metadata <span class="alerts-panel-sub">simple JSON preview</span></div>
|
||||||
|
<div class="alerts-panel-tools">
|
||||||
|
<button class="win98-button alerts-tool-button" type="button" data-command="copy-meta">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-panel-body">
|
||||||
|
<pre class="alerts-json-box" id="detail-metadata">{
|
||||||
|
"connector": "local-main",
|
||||||
|
"mode": "read_only",
|
||||||
|
"retry_in": "30s"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="alerts-panel alerts-actions-panel">
|
||||||
|
<div class="alerts-panel-header">
|
||||||
|
<div class="alerts-panel-title">Actions <span class="alerts-panel-sub">simple first version</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-panel-body">
|
||||||
|
<div class="alerts-action-stack">
|
||||||
|
<button class="win98-button alerts-action-button" type="button" data-command="ack">Acknowledge selected</button>
|
||||||
|
<button class="win98-button alerts-action-button" type="button" data-command="close">Close selected</button>
|
||||||
|
<button class="win98-button alerts-action-button" type="button" data-command="refresh">Refresh alerts</button>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-mini-note">
|
||||||
|
CURRENTLY_MOCKED_LEAVE_AS_IS: alerts use a lightweight lifecycle for now: open, acknowledged, closed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alerts-footerbar">
|
||||||
|
<div class="alerts-footer-left">
|
||||||
|
<span class="alerts-status-pill" id="selected-count">Selected: 0</span>
|
||||||
|
<span class="alerts-status-pill">10 mocked alerts</span>
|
||||||
|
</div>
|
||||||
|
<div class="alerts-footer-right">
|
||||||
|
<button class="win98-button alerts-footer-button" type="button" data-command="ack">Acknowledge</button>
|
||||||
|
<button class="win98-button alerts-footer-button" type="button" data-command="close">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script src="/static/js/warpbox-ui.js"></script>
|
||||||
|
<script src="/static/js/admin/alerts.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
<h2>Alerts Inbox</h2>
|
<h2>Alerts Inbox</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
<a class="titlebar-link-button" href="/account/alerts">Show all</a>
|
<a class="titlebar-link-button" href="/admin/alerts">Show all</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
<h2>Recent Activity</h2>
|
<h2>Recent Activity</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
<a class="titlebar-link-button" href="/account/activity">Show all</a>
|
<a class="titlebar-link-button" href="/admin/dashboard#recent-activity">Show all</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
<h2>Recent Boxes</h2>
|
<h2>Recent Boxes</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-actions">
|
<div class="titlebar-actions">
|
||||||
<a class="titlebar-link-button" href="/account/boxes">Show all</a>
|
<a class="titlebar-link-button" href="/admin/dashboard#recent-boxes">Show all</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body sunken-panel">
|
<div class="section-body sunken-panel">
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
<span>WarpBox</span>
|
<span>WarpBox</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="admin-taskbar-nav" aria-label="Primary">
|
<nav class="admin-taskbar-nav" aria-label="Primary">
|
||||||
<a class="admin-taskbar-button is-active" href="#dashboard">Dashboard</a>
|
<a class="admin-taskbar-button{{ if eq .ActivePage "dashboard" }} is-active{{ end }}" href="/admin/dashboard">Dashboard</a>
|
||||||
<a class="admin-taskbar-button" href="#alerts">Alerts</a>
|
<a class="admin-taskbar-button{{ if eq .ActivePage "alerts" }} is-active{{ end }}" href="/admin/alerts">Alerts</a>
|
||||||
<a class="admin-taskbar-button" href="#recent-boxes">Boxes</a>
|
<a class="admin-taskbar-button" href="/admin/dashboard#recent-boxes">Boxes</a>
|
||||||
<a class="admin-taskbar-button" href="#recent-activity">Activity</a>
|
<a class="admin-taskbar-button" href="/admin/dashboard#recent-activity">Activity</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="admin-taskbar-session" aria-label="Admin session summary">
|
<div class="admin-taskbar-session" aria-label="Admin session summary">
|
||||||
<a class="admin-alert-chip is-warning" href="#alerts" id="topAlertChip">! 15 alerts</a>
|
<a class="admin-alert-chip is-warning" href="/admin/alerts" id="topAlertChip">! 15 alerts</a>
|
||||||
<span class="admin-session-chip">signed in: {{ .AdminUsername }}</span>
|
<span class="admin-session-chip">signed in: {{ .AdminUsername }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user