botui/ui/suite/monitoring/alerts.html

1573 lines
48 KiB
HTML

<div class="alerts-container">
<!-- Alerts Header -->
<div class="alerts-header">
<div class="header-info">
<h2></h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
Alert Configuration
</h2>
<span class="alert-summary"
hx-get="/api/monitoring/alerts/summary"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<span class="summary-item critical">0 Critical</span>
<span class="summary-item warning">0 Warning</span>
<span class="summary-item info">0 Info</span>
</span>
</div>
<div class="header-actions">
<button class="action-btn" onclick="openCreateAlertModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Alert Rule
</button>
<button class="action-btn secondary" onclick="acknowledgeAllAlerts()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 11 12 14 22 4"></polyline>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
Acknowledge All
</button>
</div>
</div>
<!-- Tabs -->
<div class="alerts-tabs">
<button class="tab-btn active" onclick="switchTab('active', this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
Active Alerts
<span class="tab-badge" id="active-count">0</span>
</button>
<button class="tab-btn" onclick="switchTab('rules', this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
</svg>
Alert Rules
<span class="tab-badge" id="rules-count">0</span>
</button>
<button class="tab-btn" onclick="switchTab('history', this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
History
</button>
</div>
<!-- Active Alerts Panel -->
<div class="tab-panel active" id="active-panel">
<div class="panel-toolbar">
<div class="filter-group">
<select id="severity-filter" onchange="filterAlerts()">
<option value="all">All Severities</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
<select id="status-filter" onchange="filterAlerts()">
<option value="all">All Status</option>
<option value="firing">Firing</option>
<option value="pending">Pending</option>
<option value="acknowledged">Acknowledged</option>
</select>
</div>
<div class="search-box">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="alert-search" placeholder="Search alerts..." onkeyup="searchAlerts(this.value)">
</div>
</div>
<div class="alerts-list" id="alerts-list"
hx-get="/api/monitoring/alerts/active"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<!-- Alert items loaded via HTMX -->
<div class="alert-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<p>No active alerts</p>
<span></span>All systems are operating normally</span>
</div>
</div>
</div>
<!-- Alert Rules Panel -->
<div class="tab-panel" id="rules-panel">
<div class="panel-toolbar">
<div class="filter-group">
<select id="rule-status-filter" onchange="filterRules()">
<option value="all">All Rules</option>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
<select id="rule-category-filter" onchange="filterRules()">
<option value="all">All Categories</option>
<option value="system">System</option>
<option value="application">Application</option>
<option value="database">Database</option>
<option value="network">Network</option>
<option value="security">Security</option>
</select>
</div>
<button class="action-btn secondary small" onclick="openCreateAlertModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New Rule
</button>
</div>
<div class="rules-grid" id="rules-grid"
hx-get="/api/monitoring/alerts/rules"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Rules loaded via HTMX -->
<div class="rule-card skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
<div class="rule-card skeleton">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
</div>
<!-- History Panel -->
<div class="tab-panel" id="history-panel">
<div class="panel-toolbar">
<div class="filter-group">
<select id="history-time-filter" onchange="filterHistory()">
<option value="24h">Last 24 hours</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
</select>
<select id="history-outcome-filter" onchange="filterHistory()">
<option value="all">All Outcomes</option>
<option value="resolved">Resolved</option>
<option value="acknowledged">Acknowledged</option>
<option value="expired">Expired</option>
</select>
</div>
<button class="action-btn secondary small" onclick="exportHistory()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export
</button>
</div>
<div class="history-list" id="history-list"
hx-get="/api/monitoring/alerts/history"
hx-trigger="load"
hx-swap="innerHTML">
<!-- History items loaded via HTMX -->
<div class="loading-state">
<div class="spinner"></div>
<p>Loading history...</p>
</div>
</div>
</div>
</div>
<!-- Create Alert Rule Modal -->
<div class="modal-overlay" id="create-alert-modal">
<div class="modal">
<div class="modal-header">
<h3>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
Create Alert Rule
</h3>
<button class="close-btn" onclick="closeCreateAlertModal()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form id="create-alert-form"
hx-post="/api/monitoring/alerts/rules"
hx-target="#rules-grid"
hx-swap="innerHTML"
hx-on::after-request="closeCreateAlertModal()">
<div class="modal-body">
<div class="form-section">
<h4>Basic Information</h4>
<div class="form-group">
<label for="alert-name">Alert Name *</label>
<input type="text" id="alert-name" name="name" required placeholder="e.g., High CPU Usage">
</div>
<div class="form-row">
<div class="form-group">
<label for="alert-severity">Severity *</label>
<select id="alert-severity" name="severity" required>
<option value="">Select severity</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</div>
<div class="form-group">
<label for="alert-category">Category *</label>
<select id="alert-category" name="category" required>
<option value="">Select category</option>
<option value="system">System</option>
<option value="application">Application</option>
<option value="database">Database</option>
<option value="network">Network</option>
<option value="security">Security</option>
</select>
</div>
</div>
<div class="form-group">
<label for="alert-description">Description</label>
<textarea id="alert-description" name="description" rows="2" placeholder="Brief description of this alert rule"></textarea>
</div>
</div>
<div class="form-section">
<h4>Condition</h4>
<div class="form-group">
<label for="alert-metric">Metric *</label>
<select id="alert-metric" name="metric" required>
<option value="">Select metric</option>
<optgroup label="System">
<option value="cpu_usage">CPU Usage (%)</option>
<option value="memory_usage">Memory Usage (%)</option>
<option value="disk_usage">Disk Usage (%)</option>
<option value="load_average">Load Average</option>
</optgroup>
<optgroup label="Application">
<option value="request_rate">Request Rate (req/s)</option>
<option value="error_rate">Error Rate (%)</option>
<option value="response_time">Response Time (ms)</option>
<option value="active_connections">Active Connections</option>
</optgroup>
<optgroup label="Database">
<option value="db_connections">DB Connections</option>
<option value="query_time">Query Time (ms)</option>
<option value="replication_lag">Replication Lag (s)</option>
</optgroup>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="alert-operator">Operator *</label>
<select id="alert-operator" name="operator" required>
<option value="gt">Greater than (>)</option>
<option value="gte">Greater than or equal (>=)</option>
<option value="lt">Less than (<)</option>
<option value="lte">Less than or equal (<=)</option>
<option value="eq">Equal to (=)</option>
<option value="neq">Not equal to (!=)</option>
</select>
</div>
<div class="form-group">
<label for="alert-threshold">Threshold *</label>
<input type="number" id="alert-threshold" name="threshold" required placeholder="e.g., 90" step="any">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="alert-duration">Duration *</label>
<select id="alert-duration" name="duration" required>
<option value="0">Instant</option>
<option value="60">1 minute</option>
<option value="300" selected>5 minutes</option>
<option value="600">10 minutes</option>
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
</select>
</div>
<div class="form-group">
<label for="alert-evaluation">Evaluation Interval</label>
<select id="alert-evaluation" name="evaluation_interval">
<option value="30">30 seconds</option>
<option value="60" selected>1 minute</option>
<option value="300">5 minutes</option>
</select>
</div>
</div>
</div>
<div class="form-section">
<h4>Notifications</h4>
<div class="notification-channels">
<label class="checkbox-label">
<input type="checkbox" name="notify_email" value="true" checked>
<span class="checkbox-custom"></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
Email
</label>
<label class="checkbox-label">
<input type="checkbox" name="notify_slack" value="true">
<span class="checkbox-custom"></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"></path>
<path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"></path>
<path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"></path>
<path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"></path>
<path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"></path>
<path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path>
<path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"></path>
<path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"></path>
</svg>
Slack
</label>
<label class="checkbox-label">
<input type="checkbox" name="notify_webhook" value="true">
<span class="checkbox-custom"></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
Webhook
</label>
<label class="checkbox-label">
<input type="checkbox" name="notify_sms" value="true">
<span class="checkbox-custom"></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
<line x1="12" y1="18" x2="12.01" y2="18"></line>
</svg>
SMS
</label>
</div>
</div>
<div class="form-section">
<div class="form-group inline">
<label class="toggle-label">
<input type="checkbox" name="enabled" value="true" checked>
<span class="toggle-switch"></span>
<span>Enable this alert rule</span>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn secondary" onclick="closeCreateAlertModal()">Cancel</button>
<button type="submit" class="btn primary">Create Alert Rule</button>
</div>
</form>
</div>
</div>
<!-- Alert Detail Modal -->
<div class="modal-overlay" id="alert-detail-modal">
<div class="modal">
<div class="modal-header">
<h3 id="alert-detail-title">Alert Details</h3>
<button class="close-btn" onclick="closeAlertDetailModal()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body" id="alert-detail-content">
<!-- Alert details loaded here -->
</div>
<div class="modal-footer" id="alert-detail-actions">
<!-- Actions loaded dynamically -->
</div>
</div>
</div>
<style>
.alerts-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Header */
.alerts-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.header-info h2 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.5rem;
}
.header-info h2 svg {
color: var(--primary);
}
.alert-summary {
display: flex;
gap: 1rem;
}
.summary-item {
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.summary-item.critical {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.summary-item.warning {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.summary-item.info {
background: var(--primary-light);
color: var(--primary);
}
.header-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
background: var(--primary);
color: white;
border: none;
border-radius: 6px;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
background: var(--primary-hover);
}
.action-btn.secondary {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
}
.action-btn.secondary:hover {
background: var(--surface-hover);
border-color: var(--primary);
}
.action-btn.small {
padding: 0.375rem 0.625rem;
font-size: 0.75rem;
}
/* Tabs */
.alerts-tabs {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0;
}
.tab-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s ease;
}
.tab-btn:hover {
color: var(--text);
background: var(--surface-hover);
}
.tab-btn.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-badge {
background: var(--border);
color: var(--text-secondary);
font-size: 0.6875rem;
font-weight: 600;
padding: 0.125rem 0.375rem;
border-radius: 10px;
min-width: 18px;
text-align: center;
}
.tab-btn.active .tab-badge {
background: var(--primary-light);
color: var(--primary);
}
/* Tab Panels */
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
.panel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 0;
gap: 1rem;
flex-wrap: wrap;
}
.filter-group {
display: flex;
gap: 0.5rem;
}
.filter-group select {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.8125rem;
cursor: pointer;
}
.filter-group select:focus {
outline: none;
border-color: var(--primary);
}
.search-box {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
.search-box svg {
color: var(--text-secondary);
flex-shrink: 0;
}
.search-box input {
background: transparent;
border: none;
color: var(--text);
font-size: 0.8125rem;
width: 200px;
outline: none;
}
.search-box input::placeholder {
color: var(--text-secondary);
}
/* Alerts List */
.alerts-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.alert-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
transition: all 0.2s ease;
cursor: pointer;
}
.alert-item:hover {
border-color: var(--primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.alert-item.critical {
border-left: 3px solid var(--error);
}
.alert-item.warning {
border-left: 3px solid var(--warning);
}
.alert-item.info {
border-left: 3px solid var(--primary);
}
.alert-severity {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
flex-shrink: 0;
}
.alert-item.critical .alert-severity {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.alert-item.warning .alert-severity {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.alert-item.info .alert-severity {
background: var(--primary-light);
color: var(--primary);
}
.alert-content {
flex: 1;
min-width: 0;
}
.alert-title {
font-size: 0.9375rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.25rem;
}
.alert-message {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
line-height: 1.4;
}
.alert-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.alert-meta span {
font-size: 0.75rem;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 0.25rem;
}
.alert-meta svg {
width: 12px;
height: 12px;
}
.alert-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.alert-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.alert-action-btn:hover {
background: var(--surface-hover);
color: var(--primary);
border-color: var(--primary);
}
.alert-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem;
text-align: center;
color: var(--text-secondary);
}
.alert-placeholder svg {
margin-bottom: 1rem;
color: var(--success);
opacity: 0.7;
}
.alert-placeholder p {
font-size: 1rem;
color: var(--text);
margin-bottom: 0.25rem;
}
.alert-placeholder span {
font-size: 0.8125rem;
}
/* Rules Grid */
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1rem;
}
.rule-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
transition: all 0.2s ease;
}
.rule-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.rule-card.disabled {
opacity: 0.6;
}
.rule-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.rule-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
}
.rule-card.disabled .rule-status {
background: var(--text-secondary);
}
.rule-toggle {
position: relative;
width: 36px;
height: 20px;
}
.rule-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.rule-toggle .toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--border);
border-radius: 20px;
transition: 0.3s;
}
.rule-toggle .toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.rule-toggle input:checked + .toggle-slider {
background: var(--primary);
}
.rule-toggle input:checked + .toggle-slider:before {
transform: translateX(16px);
}
.rule-body {
padding: 1rem;
}
.rule-name {
font-size: 0.9375rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.375rem;
}
.rule-condition {
font-size: 0.8125rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
background: var(--bg);
padding: 0.375rem 0.5rem;
border-radius: 4px;
}
.rule-info {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.rule-info span {
font-size: 0.75rem;
color: var(--text-secondary);
}
.rule-info .severity {
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-weight: 500;
}
.rule-info .severity.critical {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.rule-info .severity.warning {
background: rgba(245, 158, 11, 0.1);
color: var(--warning);
}
.rule-info .severity.info {
background: var(--primary-light);
color: var(--primary);
}
.rule-actions {
display: flex;
border-top: 1px solid var(--border);
}
.rule-action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.rule-action:hover {
background: var(--surface-hover);
color: var(--primary);
}
.rule-action:not(:last-child) {
border-right: 1px solid var(--border);
}
.rule-action svg {
width: 14px;
height: 14px;
margin-right: 0.375rem;
}
/* Skeleton */
.rule-card.skeleton {
min-height: 150px;
padding: 1rem;
}
.skeleton-line {
height: 16px;
background: linear-gradient(90deg, var(--border) 25%, var(--surface-hover) 50%, var(--border) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
margin-bottom: 0.75rem;
}
.skeleton-line.short {
width: 60%;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* History List */
.history-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.history-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
}
.history-time {
font-size: 0.75rem;
color: var(--text-secondary);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
white-space: nowrap;
}
.history-severity {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.history-severity.critical { background: var(--error); }
.history-severity.warning { background: var(--warning); }
.history-severity.info { background: var(--primary); }
.history-content {
flex: 1;
min-width: 0;
}
.history-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--text);
}
.history-outcome {
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-weight: 500;
}
.history-outcome.resolved {
background: rgba(34, 197, 94, 0.1);
color: var(--success);
}
.history-outcome.acknowledged {
background: var(--primary-light);
color: var(--primary);
}
.history-outcome.expired {
background: var(--bg);
color: var(--text-secondary);
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-secondary);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 0.75rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal-overlay.open {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
width: 100%;
max-width: 560px;
max-height: 90vh;
overflow: hidden;
transform: translateY(20px);
transition: transform 0.3s ease;
}
.modal-overlay.open .modal {
transform: translateY(0);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1rem;
font-weight: 600;
color: var(--text);
}
.modal-header h3 svg {
color: var(--primary);
}
.close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
}
.close-btn:hover {
background: var(--surface-hover);
color: var(--text);
}
.modal-body {
padding: 1.25rem;
overflow-y: auto;
max-height: calc(90vh - 140px);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-top: 1px solid var(--border);
}
.btn {
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn.primary {
background: var(--primary);
color: white;
border: none;
}
.btn.primary:hover {
background: var(--primary-hover);
}
.btn.secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn.secondary:hover {
background: var(--surface-hover);
border-color: var(--primary);
}
/* Form Styles */
.form-section {
margin-bottom: 1.5rem;
}
.form-section:last-child {
margin-bottom: 0;
}
.form-section h4 {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.form-group {
margin-bottom: 1rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
font-size: 0.8125rem;
font-weight: 500;
color: var(--text);
margin-bottom: 0.375rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.form-group input::placeholder,
.form-group textarea::placeholder {
color: var(--text-secondary);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.notification-channels {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: 0.8125rem;
color: var(--text);
transition: all 0.2s ease;
}
.checkbox-label:hover {
border-color: var(--primary);
}
.checkbox-label input {
display: none;
}
.checkbox-label input:checked + .checkbox-custom + svg {
color: var(--primary);
}
.checkbox-label input:checked ~ span:last-child {
color: var(--primary);
}
.checkbox-custom {
width: 16px;
height: 16px;
border: 2px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.checkbox-label input:checked + .checkbox-custom {
background: var(--primary);
border-color: var(--primary);
}
.checkbox-label input:checked + .checkbox-custom::after {
content: "";
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.toggle-label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
font-size: 0.875rem;
color: var(--text);
}
.toggle-label input {
display: none;
}
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
background: var(--border);
border-radius: 24px;
transition: background 0.3s ease;
}
.toggle-switch::before {
content: "";
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.3s ease;
}
.toggle-label input:checked + .toggle-switch {
background: var(--primary);
}
.toggle-label input:checked + .toggle-switch::before {
transform: translateX(20px);
}
/* Responsive */
@media (max-width: 768px) {
.alerts-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
flex-wrap: wrap;
}
.alerts-tabs {
overflow-x: auto;
}
.tab-btn {
white-space: nowrap;
}
.panel-toolbar {
flex-direction: column;
align-items: stretch;
}
.filter-group {
flex-wrap: wrap;
}
.search-box {
width: 100%;
}
.search-box input {
width: 100%;
}
.rules-grid {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.modal {
margin: 1rem;
max-height: calc(100vh - 2rem);
}
}
</style>
<script>
function switchTab(tab, btn) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update panels
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.getElementById(`${tab}-panel`).classList.add('active');
}
function filterAlerts() {
const severity = document.getElementById('severity-filter').value;
const status = document.getElementById('status-filter').value;
const items = document.querySelectorAll('.alert-item');
items.forEach(item => {
let show = true;
if (severity !== 'all' && !item.classList.contains(severity)) {
show = false;
}
if (status !== 'all' && item.dataset.status !== status) {
show = false;
}
item.style.display = show ? '' : 'none';
});
}
function searchAlerts(query) {
const items = document.querySelectorAll('.alert-item');
const lowerQuery = query.toLowerCase();
items.forEach(item => {
const text = item.textContent.toLowerCase();
item.style.display = text.includes(lowerQuery) ? '' : 'none';
});
}
function filterRules() {
const status = document.getElementById('rule-status-filter').value;
const category = document.getElementById('rule-category-filter').value;
const cards = document.querySelectorAll('.rule-card:not(.skeleton)');
cards.forEach(card => {
let show = true;
if (status === 'enabled' && card.classList.contains('disabled')) {
show = false;
}
if (status === 'disabled' && !card.classList.contains('disabled')) {
show = false;
}
if (category !== 'all' && card.dataset.category !== category) {
show = false;
}
card.style.display = show ? '' : 'none';
});
}
function filterHistory() {
htmx.trigger('#history-list', 'refresh');
}
function openCreateAlertModal() {
document.getElementById('create-alert-modal').classList.add('open');
}
function closeCreateAlertModal() {
document.getElementById('create-alert-modal').classList.remove('open');
document.getElementById('create-alert-form').reset();
}
function openAlertDetailModal(alertId) {
const modal = document.getElementById('alert-detail-modal');
const content = document.getElementById('alert-detail-content');
// Load alert details
htmx.ajax('GET', `/api/monitoring/alerts/${alertId}`, {
target: content,
swap: 'innerHTML'
});
modal.classList.add('open');
}
function closeAlertDetailModal() {
document.getElementById('alert-detail-modal').classList.remove('open');
}
function acknowledgeAlert(alertId) {
htmx.ajax('POST', `/api/monitoring/alerts/${alertId}/acknowledge`, {
swap: 'none'
}).then(() => {
htmx.trigger('#alerts-list', 'refresh');
});
}
function acknowledgeAllAlerts() {
if (confirm('Are you sure you want to acknowledge all active alerts?')) {
htmx.ajax('POST', '/api/monitoring/alerts/acknowledge-all', {
swap: 'none'
}).then(() => {
htmx.trigger('#alerts-list', 'refresh');
});
}
}
function silenceAlert(alertId, duration) {
htmx.ajax('POST', `/api/monitoring/alerts/${alertId}/silence?duration=${duration}`, {
swap: 'none'
}).then(() => {
htmx.trigger('#alerts-list', 'refresh');
});
}
function toggleRule(ruleId, enabled) {
htmx.ajax('PATCH', `/api/monitoring/alerts/rules/${ruleId}`, {
values: { enabled: enabled },
swap: 'none'
}).then(() => {
htmx.trigger('#rules-grid', 'refresh');
});
}
function editRule(ruleId) {
// Load rule into form and open modal
htmx.ajax('GET', `/api/monitoring/alerts/rules/${ruleId}`, {
target: '#create-alert-form',
swap: 'outerHTML'
}).then(() => {
openCreateAlertModal();
});
}
function deleteRule(ruleId) {
if (confirm('Are you sure you want to delete this alert rule?')) {
htmx.ajax('DELETE', `/api/monitoring/alerts/rules/${ruleId}`, {
swap: 'none'
}).then(() => {
htmx.trigger('#rules-grid', 'refresh');
});
}
}
function exportHistory() {
const timeFilter = document.getElementById('history-time-filter').value;
window.open(`/api/monitoring/alerts/history/export?range=${timeFilter}`, '_blank');
}
// Close modals on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCreateAlertModal();
closeAlertDetailModal();
}
});
// Close modals when clicking overlay
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', function(e) {
if (e.target === this) {
this.classList.remove('open');
}
});
});
// Update counts from HTMX responses
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.target.id === 'alerts-list') {
const count = evt.target.querySelectorAll('.alert-item').length;
document.getElementById('active-count').textContent = count;
}
if (evt.target.id === 'rules-grid') {
const count = evt.target.querySelectorAll('.rule-card:not(.skeleton)').length;
document.getElementById('rules-count').textContent = count;
}
});
</script>