1574 lines
48 KiB
HTML
1574 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>
|