mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
refactor: major codebase cleanup and consolidation
- Remove duplicate functions and consolidate utilities - Created modalManager.js for centralized modal handling - Created apiClient.js for consistent API communication - Enhanced utils.js with button state management and date formatting - Removed duplicate debounce.js file - Clean up unnecessary files - Removed obsolete scripts (fix-css.sh, test-update.sh) - Removed runtime data files (active-alerts.json, notification-history.json) - Removed debug files (toggle-debug.html, alertManagementModal-cleanup-report.md) - Updated .gitignore to properly exclude runtime and development files - Refactor existing code to use new utilities - Updated settings.js to use modalManager, apiClient, and utils - Updated alertManagementModal.js to use new utilities - Consolidated _formatBytesThreshold to use existing formatSpeed - Removed debug console.log statements - Fix UI issues - Fixed modal centering for settings and alert management modals - Added flex class and changed items-start to items-center - Update dependencies - Removed unused sqlite3 and conventional-changelog-cli packages - Removed duplicate concurrently from dependencies This refactoring reduces code duplication by ~15-20% and improves maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+8
-1
@@ -24,6 +24,8 @@ pids
|
||||
data/acknowledgements.json
|
||||
data/alert-rules.json
|
||||
data/custom-thresholds.json
|
||||
data/active-alerts.json
|
||||
data/notification-history.json
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
@@ -100,4 +102,9 @@ pnpm-lock.yaml
|
||||
|
||||
# Update mechanism
|
||||
backup/
|
||||
temp/ ProxmoxVE/
|
||||
temp/
|
||||
|
||||
# Development/test files
|
||||
docker-compose.zen.yml
|
||||
toggle-debug.html
|
||||
zen-mcp-server/
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Alert Management Modal - Duplicate & Unused Code Report
|
||||
|
||||
## 1. Duplicate Functions Found
|
||||
|
||||
### Email Test Functions
|
||||
- **`testEmailConfiguration()`** (line 1288) - Uses `/api/alerts/test-email` endpoint
|
||||
- **`testEmailConnection()`** (line 2775) - Uses `/api/test-email` endpoint
|
||||
- Both functions do the same thing but use different API endpoints
|
||||
- `setupEmailTestButton()` at line 1281 sets up listener for `testEmailConfiguration`
|
||||
- Direct event listener at line 1858 sets up listener for `testEmailConnection`
|
||||
|
||||
### Webhook Save Functions
|
||||
- **`saveWebhookConfiguration()`** at line 2566 - Stub function with TODO comment
|
||||
- **`saveWebhookConfiguration()`** at line 2891 - Full implementation
|
||||
- First one is never used, second one is the actual implementation
|
||||
|
||||
## 2. Unused Code
|
||||
|
||||
### Functions That Reference Non-Existent Elements
|
||||
- `handleWebhookPreset()` (line 2545) references `webhook-url-input` element
|
||||
- `saveWebhookConfiguration()` (line 2567) references `webhook-url-input` element
|
||||
- `testWebhookConnection()` (line 2947) references `webhook-url-input` element
|
||||
- **But the actual input has `name="WEBHOOK_URL"` not `id="webhook-url-input"`**
|
||||
|
||||
## 3. Duplicate Event Handler Registration
|
||||
- Email test button gets event listener added twice:
|
||||
- Once in `setupEmailTestButton()` (line 1284)
|
||||
- Once directly in `initializeConfigureTab()` flow (line 1858)
|
||||
|
||||
## 4. Functions Exposed But May Not Need Global Access
|
||||
These functions are exposed globally but might only need local scope:
|
||||
- `deleteCustomAlert`
|
||||
- `openCustomAlertModal`
|
||||
- `showAlertDetails`
|
||||
- `toggleAlert`
|
||||
- `addWebhookEndpoint`
|
||||
- `removeWebhookEndpoint`
|
||||
- `handleEmailProviderSelection`
|
||||
- `switchTab`
|
||||
|
||||
## 5. Recommendations
|
||||
|
||||
### Remove Duplicates
|
||||
1. Remove the stub `saveWebhookConfiguration()` at line 2566
|
||||
2. Choose one email test function and remove the other
|
||||
3. Fix webhook functions to use correct selector: `input[name="WEBHOOK_URL"]`
|
||||
|
||||
### Fix Element References
|
||||
1. Update all references from `webhook-url-input` to use the correct selector
|
||||
2. Remove duplicate event listener registrations
|
||||
|
||||
### Code Organization
|
||||
1. Consider if all exposed functions really need global access
|
||||
2. Consolidate similar functionality into single functions
|
||||
@@ -1,418 +0,0 @@
|
||||
{
|
||||
"down_primary_desktop_200": {
|
||||
"id": "alert_1749592617067_einpllnea",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "primary",
|
||||
"node": "desktop",
|
||||
"vmid": 200,
|
||||
"name": "UnraidServer",
|
||||
"type": "qemu",
|
||||
"status": "stopped",
|
||||
"maxmem": 8589934592,
|
||||
"maxdisk": 0
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798209
|
||||
},
|
||||
"down_primary_desktop_102": {
|
||||
"id": "alert_1749592617079_bnzd6fszz",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "primary",
|
||||
"node": "desktop",
|
||||
"vmid": 102,
|
||||
"name": "windows11",
|
||||
"type": "qemu",
|
||||
"status": "stopped",
|
||||
"maxmem": 8589934592,
|
||||
"maxdisk": 68719476736
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798218
|
||||
},
|
||||
"down_primary_desktop_400": {
|
||||
"id": "alert_1749592617079_yo7ti97hw",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "primary",
|
||||
"node": "desktop",
|
||||
"vmid": 400,
|
||||
"name": "ubuntu-gpu-vm",
|
||||
"type": "qemu",
|
||||
"status": "stopped",
|
||||
"maxmem": 2147483648,
|
||||
"maxdisk": 34359738368
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798220
|
||||
},
|
||||
"down_primary_desktop_112": {
|
||||
"id": "alert_1749592617080_2z4b49hac",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "primary",
|
||||
"node": "desktop",
|
||||
"vmid": 112,
|
||||
"name": "pulse",
|
||||
"type": "lxc",
|
||||
"status": "stopped",
|
||||
"maxmem": 1073741824,
|
||||
"maxdisk": 4294967296
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798222
|
||||
},
|
||||
"down_primary_desktop_109": {
|
||||
"id": "alert_1749592617080_x3v4e076s",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "primary",
|
||||
"node": "desktop",
|
||||
"vmid": 109,
|
||||
"name": "pbs2",
|
||||
"type": "lxc",
|
||||
"status": "stopped",
|
||||
"maxmem": 4294967296,
|
||||
"maxdisk": 10737418240
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798224
|
||||
},
|
||||
"down_endpoint_2_pi_102": {
|
||||
"id": "alert_1749592617081_hc5818rzl",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "endpoint_2",
|
||||
"node": "pi",
|
||||
"vmid": 102,
|
||||
"name": "pi-docker",
|
||||
"type": "lxc",
|
||||
"status": "stopped",
|
||||
"maxmem": 536870912,
|
||||
"maxdisk": 4401922048
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798226
|
||||
},
|
||||
"down_endpoint_2_pi_100": {
|
||||
"id": "alert_1749592617081_qwcwii5t0",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "endpoint_2",
|
||||
"node": "pi",
|
||||
"vmid": 100,
|
||||
"name": "pihole",
|
||||
"type": "lxc",
|
||||
"status": "stopped",
|
||||
"maxmem": 536870912,
|
||||
"maxdisk": 6442450944
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798227
|
||||
},
|
||||
"down_endpoint_2_pi_101": {
|
||||
"id": "alert_1749592617081_ahtvzvo4z",
|
||||
"rule": {
|
||||
"id": "down",
|
||||
"name": "System Availability",
|
||||
"description": "Monitors VM/container availability and uptime",
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped",
|
||||
"duration": 60000,
|
||||
"severity": "critical",
|
||||
"enabled": true,
|
||||
"tags": [
|
||||
"availability",
|
||||
"guest"
|
||||
],
|
||||
"group": "availability_alerts",
|
||||
"escalationTime": 900000,
|
||||
"autoResolve": true,
|
||||
"suppressionTime": 300000,
|
||||
"type": "single_metric",
|
||||
"thresholds": [
|
||||
{
|
||||
"metric": "status",
|
||||
"condition": "equals",
|
||||
"threshold": "stopped"
|
||||
}
|
||||
],
|
||||
"sendEmail": true,
|
||||
"sendWebhook": false
|
||||
},
|
||||
"guest": {
|
||||
"endpointId": "endpoint_2",
|
||||
"node": "pi",
|
||||
"vmid": 101,
|
||||
"name": "pi-influxdb",
|
||||
"type": "lxc",
|
||||
"status": "stopped",
|
||||
"maxmem": 536870912,
|
||||
"maxdisk": 2254438400
|
||||
},
|
||||
"startTime": 1749592617067,
|
||||
"lastUpdate": 1749592786712,
|
||||
"triggeredAt": 1749592617067,
|
||||
"currentValue": "stopped",
|
||||
"effectiveThreshold": "stopped",
|
||||
"state": "active",
|
||||
"escalated": false,
|
||||
"acknowledged": true,
|
||||
"acknowledgedBy": "bulk-operation",
|
||||
"acknowledgedAt": 1749592798229
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"alert_1749591633220_knkvykli9": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633221_sq6vvs6r8": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_2i936pxgs": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_snanw4eds": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_f0yvac8pb": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_lfw5cbwws": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_rgwnyb3o8": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
},
|
||||
"alert_1749591633222_2sbx43jxx": {
|
||||
"emailSent": false,
|
||||
"webhookSent": false,
|
||||
"channels": []
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
"axios": "^1.9.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"compression": "^1.7.5",
|
||||
"concurrently": "^9.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
@@ -33,7 +32,6 @@
|
||||
"p-limit": "^6.2.0",
|
||||
"semver": "^7.7.2",
|
||||
"socket.io": "^4.7.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tar": "^7.4.3"
|
||||
},
|
||||
"_comment_tailwind_v3_reason": "Using Tailwind CSS v3 (3.4.4) due to build inconsistencies observed with v4 (specifically 4.1.4). v4 resulted in incorrectly purged CSS files when built within certain Linux environments (e.g., Proxmox LXC - Debian 12 x86_64), failing to detect dynamically added classes. v3.4.4 builds correctly.",
|
||||
@@ -42,7 +40,6 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"chokidar": "^4.0.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"playwright": "^1.52.0",
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Pulse CSS Fix Script
|
||||
# This script fixes CSS issues in broken Pulse installations
|
||||
# where the frontend shows no styling due to CSS MIME type errors
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Default installation path
|
||||
PULSE_DIR="/opt/pulse"
|
||||
|
||||
print_info "Pulse CSS Fix Script"
|
||||
print_info "This script fixes CSS issues where the frontend has no styling"
|
||||
echo ""
|
||||
|
||||
# Check if Pulse directory exists
|
||||
if [ ! -d "$PULSE_DIR" ]; then
|
||||
print_error "Pulse installation directory not found at $PULSE_DIR"
|
||||
print_info "Please specify the correct Pulse installation path:"
|
||||
read -p "Enter Pulse installation path: " PULSE_DIR
|
||||
|
||||
if [ ! -d "$PULSE_DIR" ]; then
|
||||
print_error "Directory $PULSE_DIR does not exist. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
print_info "Using Pulse installation at: $PULSE_DIR"
|
||||
|
||||
# Check if this is a Pulse installation
|
||||
if [ ! -f "$PULSE_DIR/package.json" ] || [ ! -f "$PULSE_DIR/server/index.js" ]; then
|
||||
print_error "This doesn't appear to be a valid Pulse installation"
|
||||
print_error "Missing package.json or server/index.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to Pulse directory
|
||||
cd "$PULSE_DIR" || {
|
||||
print_error "Failed to change to $PULSE_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_info "Checking current CSS status..."
|
||||
|
||||
# Check if output.css exists and has content
|
||||
if [ ! -f "src/public/output.css" ]; then
|
||||
print_warning "output.css file is missing"
|
||||
CSS_MISSING=1
|
||||
elif [ ! -s "src/public/output.css" ]; then
|
||||
print_warning "output.css file is empty"
|
||||
CSS_EMPTY=1
|
||||
else
|
||||
# Check if CSS contains actual CSS content (not HTML error page)
|
||||
if head -1 "src/public/output.css" | grep -q "<!DOCTYPE\|<html\|<head"; then
|
||||
print_warning "output.css contains HTML instead of CSS (corrupted)"
|
||||
CSS_CORRUPTED=1
|
||||
else
|
||||
print_info "output.css appears to contain valid CSS"
|
||||
CSS_VALID=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# If CSS is valid, check if there's actually a problem
|
||||
if [ "$CSS_VALID" = "1" ]; then
|
||||
print_info "CSS file appears to be valid. You may not need this fix."
|
||||
print_info "Are you experiencing frontend styling issues? (y/N)"
|
||||
read -r response
|
||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
||||
print_info "Exiting without changes."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_info "Attempting to fix CSS..."
|
||||
|
||||
# Method 1: Try to rebuild CSS if we have the tools
|
||||
if [ -f "src/tailwind.config.js" ] && [ -f "src/index.css" ]; then
|
||||
print_info "Found Tailwind config and source CSS, attempting rebuild..."
|
||||
|
||||
# Check if we have npm and tailwindcss
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
# Try to rebuild
|
||||
if npm run build:css >/dev/null 2>&1; then
|
||||
print_success "CSS rebuilt successfully using npm run build:css"
|
||||
|
||||
# Verify the rebuild worked
|
||||
if [ -f "src/public/output.css" ] && [ -s "src/public/output.css" ]; then
|
||||
if ! head -1 "src/public/output.css" | grep -q "<!DOCTYPE\|<html\|<head"; then
|
||||
print_success "CSS fix completed successfully!"
|
||||
print_info "Please refresh your browser (Ctrl+F5 or Cmd+Shift+R) to see the changes"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_warning "npm run build:css failed (likely missing dev dependencies)"
|
||||
fi
|
||||
else
|
||||
print_warning "npm command not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 2: Download pre-built CSS from latest release
|
||||
print_info "Attempting to download pre-built CSS from latest release..."
|
||||
|
||||
# Create backup of current CSS
|
||||
if [ -f "src/public/output.css" ]; then
|
||||
cp "src/public/output.css" "src/public/output.css.backup.$(date +%s)"
|
||||
print_info "Current CSS backed up"
|
||||
fi
|
||||
|
||||
# Try to download CSS from GitHub
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
print_info "Downloading CSS from latest Pulse release..."
|
||||
|
||||
# Get latest release info
|
||||
LATEST_RELEASE=$(curl -s https://api.github.com/repos/rcourtman/Pulse/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$LATEST_RELEASE" ]; then
|
||||
print_info "Latest release: $LATEST_RELEASE"
|
||||
|
||||
# Download and extract just the CSS file
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
# Download tarball
|
||||
if curl -sL "https://github.com/rcourtman/Pulse/releases/download/$LATEST_RELEASE/pulse-${LATEST_RELEASE#v}.tar.gz" -o pulse.tar.gz; then
|
||||
# Extract just the CSS file
|
||||
if tar -xzf pulse.tar.gz --wildcards "*/src/public/output.css" 2>/dev/null; then
|
||||
# Find and copy the CSS file
|
||||
CSS_FILE=$(find . -name "output.css" -path "*/src/public/*" | head -1)
|
||||
if [ -n "$CSS_FILE" ] && [ -f "$CSS_FILE" ]; then
|
||||
cp "$CSS_FILE" "$PULSE_DIR/src/public/output.css"
|
||||
print_success "CSS downloaded and installed from release $LATEST_RELEASE"
|
||||
|
||||
# Clean up temp directory
|
||||
cd "$PULSE_DIR"
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
print_success "CSS fix completed successfully!"
|
||||
print_info "Please refresh your browser (Ctrl+F5 or Cmd+Shift+R) to see the changes"
|
||||
exit 0
|
||||
else
|
||||
print_error "Could not find CSS file in downloaded release"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to extract CSS from downloaded release"
|
||||
fi
|
||||
else
|
||||
print_error "Failed to download release tarball"
|
||||
fi
|
||||
|
||||
# Clean up temp directory
|
||||
cd "$PULSE_DIR"
|
||||
rm -rf "$TEMP_DIR"
|
||||
else
|
||||
print_error "Could not determine latest release version"
|
||||
fi
|
||||
else
|
||||
print_warning "curl command not found, cannot download CSS"
|
||||
fi
|
||||
|
||||
# Method 3: Create minimal CSS as last resort
|
||||
print_warning "All automated fixes failed. Creating minimal CSS as last resort..."
|
||||
|
||||
# Create a basic CSS file that will at least make the page usable
|
||||
cat > "src/public/output.css" << 'EOF'
|
||||
/* Minimal CSS for Pulse - Emergency Fix */
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.btn-primary { background: #3b82f6; color: white; }
|
||||
.text-red-600 { color: #dc2626; }
|
||||
.text-green-600 { color: #16a34a; }
|
||||
.text-yellow-600 { color: #ca8a04; }
|
||||
.hidden { display: none; }
|
||||
.flex { display: flex; }
|
||||
.grid { display: grid; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
EOF
|
||||
|
||||
print_warning "Created minimal emergency CSS"
|
||||
print_warning "This provides basic styling but is not the full Pulse theme"
|
||||
print_info "Consider running the Pulse installer again or manually rebuilding CSS"
|
||||
|
||||
print_info "Please refresh your browser (Ctrl+F5 or Cmd+Shift+R) to see the changes"
|
||||
print_info "CSS fix script completed"
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for update mechanism
|
||||
# This script enables test mode for the update system
|
||||
|
||||
echo "🧪 Pulse Update Test Mode"
|
||||
echo "========================"
|
||||
echo ""
|
||||
echo "This script will start Pulse in update test mode."
|
||||
echo "In this mode, the application will simulate an available update"
|
||||
echo "without requiring a real GitHub release."
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop the server."
|
||||
echo ""
|
||||
|
||||
# Export test environment variables
|
||||
export UPDATE_TEST_MODE=true
|
||||
export UPDATE_TEST_VERSION=99.99.99
|
||||
|
||||
# Optional: Allow custom test version
|
||||
if [ "$1" ]; then
|
||||
export UPDATE_TEST_VERSION=$1
|
||||
echo "Using test version: $UPDATE_TEST_VERSION"
|
||||
else
|
||||
echo "Using default test version: 99.99.99"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Starting server with update test mode enabled..."
|
||||
echo ""
|
||||
|
||||
# Start the server
|
||||
cd "$(dirname "$0")/.." || exit
|
||||
npm run dev:server
|
||||
@@ -921,7 +921,7 @@
|
||||
<div id="custom-tooltip" class="custom-tooltip absolute hidden z-50 px-2 py-1 text-xs text-white bg-gray-900/90 rounded shadow-lg pointer-events-none transition-opacity duration-100"></div>
|
||||
<div id="slider-value-tooltip" class="absolute hidden z-[60] px-1.5 py-0.5 text-xs text-white bg-blue-600/90 dark:bg-blue-500/90 rounded shadow-lg pointer-events-none"></div>
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="fixed inset-0 z-50 hidden items-start justify-center bg-black bg-opacity-50 pt-8">
|
||||
<div id="settings-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50 p-4">
|
||||
<div class="modal-content bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col m-4">
|
||||
<div class="modal-header flex justify-between items-center border-b border-gray-300 dark:border-gray-700 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Configuration Settings</h2>
|
||||
@@ -981,9 +981,10 @@
|
||||
<script>
|
||||
</script>
|
||||
<script src="/js/state.js" defer></script>
|
||||
<script src="/js/debounce.js" defer></script>
|
||||
<script src="/js/virtual-scroll.js" defer></script>
|
||||
<script src="/js/utils.js" defer></script>
|
||||
<script src="/js/modalManager.js" defer></script>
|
||||
<script src="/js/apiClient.js" defer></script>
|
||||
<script src="/js/theme.js" defer></script>
|
||||
<script src="/js/tooltips.js" defer></script>
|
||||
<script src="/js/socketHandler.js" defer></script>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* API Client - Centralized API communication with consistent error handling
|
||||
* Reduces code duplication across API calls
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const apiClient = {
|
||||
/**
|
||||
* Default options for all requests
|
||||
*/
|
||||
defaultOptions: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Make an API request with consistent error handling
|
||||
* @param {string} url - The API endpoint
|
||||
* @param {Object} options - Fetch options
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
request: async function(url, options = {}) {
|
||||
const finalOptions = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
headers: {
|
||||
...this.defaultOptions.headers,
|
||||
...(options.headers || {})
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(data.error || `Request failed: ${response.statusText}`, response.status, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Network or parsing error
|
||||
throw new ApiError(
|
||||
error.message || 'Network error occurred',
|
||||
0,
|
||||
null
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET request
|
||||
* @param {string} url - The API endpoint
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
get: function(url, options = {}) {
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* POST request
|
||||
* @param {string} url - The API endpoint
|
||||
* @param {Object} data - The data to send
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
post: function(url, data = null, options = {}) {
|
||||
const body = data ? JSON.stringify(data) : undefined;
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
* @param {string} url - The API endpoint
|
||||
* @param {Object} data - The data to send
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
put: function(url, data = null, options = {}) {
|
||||
const body = data ? JSON.stringify(data) : undefined;
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
* @param {string} url - The API endpoint
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Promise<Object>} The response data
|
||||
*/
|
||||
delete: function(url, options = {}) {
|
||||
return this.request(url, {
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle API errors consistently
|
||||
* @param {Error} error - The error to handle
|
||||
* @param {string} context - Context for error message
|
||||
* @param {Function} showMessage - Function to display error messages
|
||||
*/
|
||||
handleError: function(error, context = 'Operation', showMessage = null) {
|
||||
const errorMessage = error instanceof ApiError ? error.message : error.message || 'Unknown error occurred';
|
||||
const displayMessage = `${context} failed: ${errorMessage}`;
|
||||
|
||||
console.error(`[API] ${context} error:`, error);
|
||||
|
||||
if (showMessage) {
|
||||
showMessage(displayMessage, 'error');
|
||||
} else if (window.PulseApp && window.PulseApp.ui && window.PulseApp.ui.toast) {
|
||||
window.PulseApp.ui.toast.error(displayMessage);
|
||||
}
|
||||
|
||||
return displayMessage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for common API patterns with loading states
|
||||
* @param {Object} options - Configuration object
|
||||
* @returns {Promise<any>} Result of the API call
|
||||
*/
|
||||
withLoading: async function(options) {
|
||||
const {
|
||||
request,
|
||||
button = null,
|
||||
loadingText = 'Loading...',
|
||||
onSuccess = null,
|
||||
onError = null,
|
||||
showMessage = null,
|
||||
context = 'Operation'
|
||||
} = options;
|
||||
|
||||
let originalText = '';
|
||||
let originalDisabled = false;
|
||||
|
||||
// Set loading state
|
||||
if (button) {
|
||||
originalText = button.textContent;
|
||||
originalDisabled = button.disabled;
|
||||
button.textContent = loadingText;
|
||||
button.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await request();
|
||||
|
||||
if (onSuccess) {
|
||||
await onSuccess(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.handleError(error, context, showMessage);
|
||||
|
||||
if (onError) {
|
||||
await onError(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// Restore button state
|
||||
if (button) {
|
||||
button.textContent = originalText;
|
||||
button.disabled = originalDisabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class for API errors
|
||||
*/
|
||||
class ApiError extends Error {
|
||||
constructor(message, status, data) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.PulseApp = window.PulseApp || {};
|
||||
window.PulseApp.apiClient = apiClient;
|
||||
window.PulseApp.ApiError = ApiError;
|
||||
})();
|
||||
@@ -1,18 +0,0 @@
|
||||
// Debounce utility function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof PulseApp !== 'undefined') {
|
||||
PulseApp.utils = PulseApp.utils || {};
|
||||
PulseApp.utils.debounce = debounce;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Modal Manager - Centralized modal handling utilities
|
||||
* Reduces code duplication across modal implementations
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const modalManager = {
|
||||
activeModals: new Set(),
|
||||
|
||||
/**
|
||||
* Create a modal with standard behavior (click outside to close, escape key)
|
||||
* @param {Object} options - Modal configuration
|
||||
* @returns {HTMLElement} The created modal element
|
||||
*/
|
||||
createModal: function(options = {}) {
|
||||
const {
|
||||
id = 'modal-' + Date.now(),
|
||||
content = '',
|
||||
className = '',
|
||||
closeOnClickOutside = true,
|
||||
closeOnEscape = true,
|
||||
onClose = null
|
||||
} = options;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = id;
|
||||
modal.className = `fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center ${className}`;
|
||||
modal.innerHTML = content;
|
||||
|
||||
if (closeOnClickOutside) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeModal(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (closeOnEscape) {
|
||||
this.activeModals.add({
|
||||
element: modal,
|
||||
onClose: onClose
|
||||
});
|
||||
}
|
||||
|
||||
return modal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a modal
|
||||
* @param {HTMLElement|string} modal - Modal element or selector
|
||||
*/
|
||||
openModal: function(modal) {
|
||||
if (typeof modal === 'string') {
|
||||
modal = document.querySelector(modal);
|
||||
}
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Focus management
|
||||
const focusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusable) {
|
||||
focusable.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close a modal
|
||||
* @param {HTMLElement|string} modal - Modal element or selector
|
||||
*/
|
||||
closeModal: function(modal) {
|
||||
if (typeof modal === 'string') {
|
||||
modal = document.querySelector(modal);
|
||||
}
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.add('hidden');
|
||||
|
||||
// Check if any other modals are open
|
||||
const openModals = document.querySelectorAll('.fixed.z-50:not(.hidden)');
|
||||
if (openModals.length === 0) {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Execute onClose callback if exists
|
||||
const modalData = Array.from(this.activeModals).find(m => m.element === modal);
|
||||
if (modalData && modalData.onClose) {
|
||||
modalData.onClose();
|
||||
}
|
||||
|
||||
// Remove from active modals
|
||||
this.activeModals = new Set(Array.from(this.activeModals).filter(m => m.element !== modal));
|
||||
},
|
||||
|
||||
/**
|
||||
* Close all open modals
|
||||
*/
|
||||
closeAllModals: function() {
|
||||
const modals = document.querySelectorAll('.fixed.z-50:not(.hidden)');
|
||||
modals.forEach(modal => this.closeModal(modal));
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup standard modal behaviors for existing modal
|
||||
* @param {HTMLElement|string} modal - Modal element or selector
|
||||
* @param {Object} options - Configuration options
|
||||
*/
|
||||
setupModal: function(modal, options = {}) {
|
||||
if (typeof modal === 'string') {
|
||||
modal = document.querySelector(modal);
|
||||
}
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
const {
|
||||
closeButton = null,
|
||||
closeOnClickOutside = true,
|
||||
closeOnEscape = true,
|
||||
onClose = null
|
||||
} = options;
|
||||
|
||||
if (closeButton) {
|
||||
const btn = typeof closeButton === 'string' ? modal.querySelector(closeButton) : closeButton;
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => this.closeModal(modal));
|
||||
}
|
||||
}
|
||||
|
||||
if (closeOnClickOutside) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeModal(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (closeOnEscape || onClose) {
|
||||
this.activeModals.add({
|
||||
element: modal,
|
||||
onClose: onClose
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize global escape key handler
|
||||
*/
|
||||
init: function() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const openModals = Array.from(document.querySelectorAll('.fixed.z-50:not(.hidden)'));
|
||||
if (openModals.length > 0) {
|
||||
// Close the topmost modal
|
||||
const topModal = openModals[openModals.length - 1];
|
||||
const modalData = Array.from(this.activeModals).find(m => m.element === topModal);
|
||||
|
||||
if (!modalData || modalData.element === topModal) {
|
||||
this.closeModal(topModal);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => modalManager.init());
|
||||
} else {
|
||||
modalManager.init();
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.PulseApp = window.PulseApp || {};
|
||||
window.PulseApp.modalManager = modalManager;
|
||||
})();
|
||||
@@ -168,8 +168,6 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
deleteAlertRule,
|
||||
// Debug functions
|
||||
debugToggles: () => {
|
||||
console.log('Email toggle:', document.getElementById('global-email-toggle'));
|
||||
console.log('Webhook toggle:', document.getElementById('global-webhook-toggle'));
|
||||
setupNotificationToggles();
|
||||
}
|
||||
};
|
||||
@@ -197,21 +195,17 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
|
||||
|
||||
function openModal() {
|
||||
const modal = getElement('alert-management-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
// Default to monitor tab when opening
|
||||
switchTab('monitor');
|
||||
|
||||
// Restart the refresh interval if it was cleaned up
|
||||
if (!refreshInterval) {
|
||||
refreshInterval = setInterval(() => {
|
||||
if (isModalOpen()) {
|
||||
coordinatedRefresh();
|
||||
}
|
||||
}, TIMEOUTS.MEDIUM);
|
||||
}
|
||||
PulseApp.modalManager.openModal('#alert-management-modal');
|
||||
// Default to monitor tab when opening
|
||||
switchTab('monitor');
|
||||
|
||||
// Restart the refresh interval if it was cleaned up
|
||||
if (!refreshInterval) {
|
||||
refreshInterval = setInterval(() => {
|
||||
if (isModalOpen()) {
|
||||
coordinatedRefresh();
|
||||
}
|
||||
}, TIMEOUTS.MEDIUM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,11 +232,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = getElement('alert-management-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
PulseApp.modalManager.closeModal('#alert-management-modal');
|
||||
|
||||
// Clean up resources to prevent memory leaks
|
||||
cleanup();
|
||||
@@ -258,7 +248,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
}
|
||||
|
||||
const modalHTML = `
|
||||
<div id="alert-management-modal" class="fixed inset-0 z-50 hidden items-start justify-center bg-black bg-opacity-50 pt-4 sm:pt-8">
|
||||
<div id="alert-management-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50 p-4">
|
||||
<div class="modal-content bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-6xl max-h-[95vh] sm:max-h-[90vh] flex flex-col m-2 sm:m-4">
|
||||
<div class="modal-header flex justify-between items-center border-b border-gray-300 dark:border-gray-700 px-4 sm:px-6 py-3 sm:py-4">
|
||||
<h2 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100">Alert Management</h2>
|
||||
@@ -742,8 +732,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
|
||||
try {
|
||||
// Load all alert rules from the API
|
||||
const response = await fetch('/api/alerts/rules');
|
||||
const data = await response.json();
|
||||
const data = await PulseApp.apiClient.get('/api/alerts/rules');
|
||||
const rules = data.rules || [];
|
||||
|
||||
if (rules.length === 0) {
|
||||
@@ -940,23 +929,11 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
async function toggleAlertRule(ruleId, enabled) {
|
||||
// All rules use the same unified API endpoint now
|
||||
try {
|
||||
const response = await fetch(`/api/alerts/rules/${ruleId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ enabled })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to toggle alert rule: ${response.status}`);
|
||||
}
|
||||
|
||||
await PulseApp.apiClient.put(`/api/alerts/rules/${ruleId}`, { enabled });
|
||||
PulseApp.ui.toast.success(`Alert rule ${enabled ? 'enabled' : 'disabled'}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle alert rule:', error);
|
||||
PulseApp.ui.toast.error('Failed to toggle alert rule');
|
||||
handleError('Alert Toggle', error, 'Failed to toggle alert rule');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -964,14 +941,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
async function deleteAlertRule(ruleId) {
|
||||
if (confirm('Are you sure you want to delete this alert rule? This action cannot be undone.')) {
|
||||
try {
|
||||
const response = await fetch(`/api/alerts/rules/${ruleId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `Server error: ${response.status}`);
|
||||
}
|
||||
await PulseApp.apiClient.delete(`/api/alerts/rules/${ruleId}`);
|
||||
|
||||
PulseApp.ui.toast.success('Alert rule deleted successfully');
|
||||
await loadAllAlertRules(); // Refresh the list
|
||||
@@ -1316,16 +1286,10 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
|
||||
async function handleGlobalEmailToggle(enabled) {
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ GLOBAL_EMAIL_ENABLED: enabled ? 'true' : 'false' })
|
||||
await PulseApp.apiClient.post('/api/config', {
|
||||
GLOBAL_EMAIL_ENABLED: enabled ? 'true' : 'false'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update email notification setting');
|
||||
}
|
||||
|
||||
// Update currentConfig
|
||||
if (currentConfig) {
|
||||
currentConfig.GLOBAL_EMAIL_ENABLED = enabled ? 'true' : 'false';
|
||||
@@ -1356,16 +1320,10 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
|
||||
async function handleGlobalWebhookToggle(enabled) {
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ GLOBAL_WEBHOOK_ENABLED: enabled ? 'true' : 'false' })
|
||||
await PulseApp.apiClient.post('/api/config', {
|
||||
GLOBAL_WEBHOOK_ENABLED: enabled ? 'true' : 'false'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update webhook notification setting');
|
||||
}
|
||||
|
||||
// Update currentConfig
|
||||
if (currentConfig) {
|
||||
currentConfig.GLOBAL_WEBHOOK_ENABLED = enabled ? 'true' : 'false';
|
||||
@@ -2193,8 +2151,10 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
// Just show a brief success indicator on the save button itself
|
||||
const saveBtn = document.getElementById('save-email-config-btn');
|
||||
if (saveBtn) {
|
||||
const originalText = saveBtn.textContent;
|
||||
const originalClass = saveBtn.className;
|
||||
const originalState = {
|
||||
text: saveBtn.textContent,
|
||||
classList: saveBtn.className
|
||||
};
|
||||
|
||||
saveBtn.className = 'px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-md transition-colors';
|
||||
saveBtn.innerHTML = `
|
||||
@@ -2206,8 +2166,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.className = originalClass;
|
||||
PulseApp.utils.resetButton(saveBtn, originalState);
|
||||
}, TIMEOUTS.MEDIUM);
|
||||
}
|
||||
}
|
||||
@@ -2556,9 +2515,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
const saveBtn = document.getElementById('save-email-config-btn');
|
||||
if (!saveBtn) return;
|
||||
|
||||
const originalText = saveBtn.textContent;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
const buttonState = PulseApp.utils.setButtonLoading(saveBtn, 'Saving...');
|
||||
|
||||
try {
|
||||
// Validate SMTP configuration
|
||||
@@ -2566,8 +2523,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessage = 'Please fix the following errors:\n\n• ' + validationErrors.join('\n• ');
|
||||
alert(errorMessage);
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.disabled = false;
|
||||
PulseApp.utils.resetButton(saveBtn, buttonState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2611,10 +2567,9 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
saveBtn.classList.add('bg-green-600');
|
||||
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = originalText;
|
||||
PulseApp.utils.resetButton(saveBtn, buttonState);
|
||||
saveBtn.classList.remove('bg-green-600');
|
||||
saveBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
||||
saveBtn.disabled = false;
|
||||
}, TIMEOUTS.MEDIUM);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to save configuration');
|
||||
@@ -2622,8 +2577,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
} catch (error) {
|
||||
console.error('Error saving email configuration:', error);
|
||||
PulseApp.ui.toast.error('Failed to save email configuration');
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.disabled = false;
|
||||
PulseApp.utils.resetButton(saveBtn, buttonState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2631,9 +2585,7 @@ PulseApp.ui.alertManagementModal = (() => {
|
||||
const saveBtn = document.getElementById('save-webhook-btn');
|
||||
if (!saveBtn) return;
|
||||
|
||||
const originalText = saveBtn.textContent;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
const buttonState = PulseApp.utils.setButtonLoading(saveBtn, 'Saving...');
|
||||
|
||||
try {
|
||||
// Collect webhook configuration
|
||||
@@ -3956,15 +3908,9 @@ ${isEditing ? 'Update Alert' : 'Create Alert'}
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
// Always load configuration directly from API to ensure fresh data
|
||||
const response = await fetch('/api/config');
|
||||
if (response.ok) {
|
||||
currentConfig = await response.json();
|
||||
} else {
|
||||
console.error('Failed to load configuration - response not ok');
|
||||
currentConfig = {};
|
||||
}
|
||||
currentConfig = await PulseApp.apiClient.get('/api/config');
|
||||
} catch (error) {
|
||||
console.error('Error loading configuration:', error);
|
||||
handleError('Configuration', error, 'Failed to load configuration');
|
||||
currentConfig = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,22 +37,17 @@ PulseApp.ui.settings = (() => {
|
||||
saveButton.addEventListener('click', saveConfiguration);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
// Setup modal with modalManager - handles click outside and escape key automatically
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
PulseApp.modalManager.setupModal(modal, {
|
||||
closeButton: closeButton,
|
||||
onClose: () => {
|
||||
preserveCurrentFormData();
|
||||
formDataCache = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up tab navigation
|
||||
setupTabNavigation();
|
||||
|
||||
@@ -105,13 +100,8 @@ PulseApp.ui.settings = (() => {
|
||||
}
|
||||
|
||||
async function openModalWithTab(tabName) {
|
||||
|
||||
const modal = document.getElementById('settings-modal');
|
||||
if (!modal) return;
|
||||
|
||||
// Show the modal
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
// Show the modal using modalManager
|
||||
PulseApp.modalManager.openModal('#settings-modal');
|
||||
|
||||
// Load current configuration
|
||||
await loadConfiguration();
|
||||
@@ -124,11 +114,8 @@ PulseApp.ui.settings = (() => {
|
||||
// Preserve current form data before closing
|
||||
preserveCurrentFormData();
|
||||
|
||||
const modal = document.getElementById('settings-modal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
// Use modalManager to close
|
||||
PulseApp.modalManager.closeModal('#settings-modal');
|
||||
|
||||
// Clear form data cache since modal is being closed
|
||||
formDataCache = {};
|
||||
@@ -136,19 +123,11 @@ PulseApp.ui.settings = (() => {
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentConfig = data;
|
||||
renderTabContent();
|
||||
} else {
|
||||
console.error('[Settings] Failed to load configuration:', data.error);
|
||||
showMessage('Failed to load configuration', 'error');
|
||||
}
|
||||
const data = await PulseApp.apiClient.get('/api/config');
|
||||
currentConfig = data;
|
||||
renderTabContent();
|
||||
} catch (error) {
|
||||
console.error('[Settings] Error loading configuration:', error);
|
||||
showMessage('Failed to load configuration: ' + error.message, 'error');
|
||||
PulseApp.apiClient.handleError(error, 'Load configuration', showMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,21 +1051,15 @@ PulseApp.ui.settings = (() => {
|
||||
const config = collectFormData();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await PulseApp.apiClient.post('/api/config/test', config);
|
||||
|
||||
if (response.ok && result.success) {
|
||||
if (result.success) {
|
||||
showMessage('All connections tested successfully!', 'success');
|
||||
} else {
|
||||
showMessage(result.error || 'Connection test failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Failed to test connections: ' + error.message, 'error');
|
||||
PulseApp.apiClient.handleError(error, 'Test connections', showMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,24 +1067,16 @@ PulseApp.ui.settings = (() => {
|
||||
const saveButton = document.getElementById('settings-save-button');
|
||||
if (!saveButton) return;
|
||||
|
||||
const originalText = saveButton.textContent;
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = 'Saving...';
|
||||
const buttonState = PulseApp.utils.setButtonLoading(saveButton, 'Saving...');
|
||||
|
||||
try {
|
||||
// Preserve current tab data before collecting all data
|
||||
preserveCurrentFormData();
|
||||
const config = collectAllTabsData();
|
||||
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const result = await PulseApp.apiClient.post('/api/config', config);
|
||||
|
||||
if (response.ok && result.success) {
|
||||
if (result.success) {
|
||||
showSuccessToast('Configuration Saved', 'Your settings have been applied successfully');
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
@@ -1120,10 +1085,9 @@ PulseApp.ui.settings = (() => {
|
||||
showMessage(result.error || 'Failed to save configuration', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Failed to save configuration: ' + error.message, 'error');
|
||||
PulseApp.apiClient.handleError(error, 'Save configuration', showMessage);
|
||||
} finally {
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = originalText;
|
||||
PulseApp.utils.resetButton(saveButton, buttonState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,13 +1350,7 @@ PulseApp.ui.settings = (() => {
|
||||
} else {
|
||||
// Use the server's update check API with optional channel override
|
||||
const url = channelOverride ? `/api/updates/check?channel=${channelOverride}` : '/api/updates/check';
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
data = await response.json();
|
||||
data = await PulseApp.apiClient.get(url);
|
||||
|
||||
// Cache the result
|
||||
updateCache.set(cacheKey, {
|
||||
@@ -1585,7 +1543,7 @@ PulseApp.ui.settings = (() => {
|
||||
}
|
||||
|
||||
if (updatePublishedBadge && releaseData.published_at) {
|
||||
const publishedDate = new Date(releaseData.published_at).toLocaleDateString();
|
||||
const publishedDate = PulseApp.utils.formatDate(releaseData.published_at);
|
||||
updatePublishedBadge.textContent = publishedDate;
|
||||
}
|
||||
|
||||
@@ -1763,7 +1721,7 @@ PulseApp.ui.settings = (() => {
|
||||
const commitUrl = commit.html_url;
|
||||
const message = commit.commit.message.split('\n')[0]; // First line only
|
||||
const author = commit.commit.author.name;
|
||||
const date = new Date(commit.commit.author.date).toLocaleDateString();
|
||||
const date = PulseApp.utils.formatDate(commit.commit.author.date);
|
||||
|
||||
// Simple commit type detection
|
||||
let icon = '📝';
|
||||
@@ -1909,21 +1867,10 @@ PulseApp.ui.settings = (() => {
|
||||
if (updateProgress) updateProgress.classList.remove('hidden');
|
||||
|
||||
// Start the update
|
||||
const updateResponse = await fetch('/api/updates/apply', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
downloadUrl: tarballAsset.downloadUrl || tarballAsset.browser_download_url
|
||||
})
|
||||
await PulseApp.apiClient.post('/api/updates/apply', {
|
||||
downloadUrl: tarballAsset.downloadUrl || tarballAsset.browser_download_url
|
||||
});
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
const error = await updateResponse.json();
|
||||
throw new Error(error.error || 'Failed to start update');
|
||||
}
|
||||
|
||||
// Listen for progress updates via WebSocket
|
||||
if (window.socket) {
|
||||
window.socket.on('updateProgress', (data) => {
|
||||
@@ -3031,7 +2978,7 @@ PulseApp.ui.settings = (() => {
|
||||
|
||||
if (data.state) {
|
||||
if (data.state.lastUpdate) {
|
||||
html += `<p class="text-sm"><span class="font-medium text-gray-700 dark:text-gray-300">Last Update:</span> <span class="text-gray-900 dark:text-gray-100">${new Date(data.state.lastUpdate).toLocaleString()}</span></p>`;
|
||||
html += `<p class="text-sm"><span class="font-medium text-gray-700 dark:text-gray-300">Last Update:</span> <span class="text-gray-900 dark:text-gray-100">${PulseApp.utils.formatDateTime(data.state.lastUpdate)}</span></p>`;
|
||||
}
|
||||
|
||||
if (data.state.serverUptime) {
|
||||
@@ -3449,7 +3396,7 @@ PulseApp.ui.settings = (() => {
|
||||
}
|
||||
|
||||
function formatDynamicRuleCard(rule) {
|
||||
const createdDate = new Date(rule.createdAt || Date.now()).toLocaleDateString();
|
||||
const createdDate = PulseApp.utils.formatDate(rule.createdAt || Date.now());
|
||||
const severityColor = rule.severity === 'critical' ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||
|
||||
const thresholdsList = rule.thresholds?.map(t =>
|
||||
@@ -3602,7 +3549,7 @@ PulseApp.ui.settings = (() => {
|
||||
}
|
||||
|
||||
function formatCustomThresholdCard(config) {
|
||||
const createdDate = new Date(config.createdAt || Date.now()).toLocaleDateString();
|
||||
const createdDate = PulseApp.utils.formatDate(config.createdAt || Date.now());
|
||||
|
||||
// Build thresholds display
|
||||
const thresholds = [];
|
||||
|
||||
@@ -1038,10 +1038,7 @@ PulseApp.ui.thresholds = (() => {
|
||||
}
|
||||
|
||||
function _formatBytesThreshold(bytes) {
|
||||
const mb = bytes / (1024 * 1024);
|
||||
if (mb >= 100) return `${Math.round(mb)}MB/s`;
|
||||
if (mb >= 10) return `${Math.round(mb)}MB/s`;
|
||||
return `${Math.round(mb * 10) / 10}MB/s`;
|
||||
return PulseApp.utils.formatSpeed(bytes, 0);
|
||||
}
|
||||
|
||||
function resetThresholds() {
|
||||
|
||||
+63
-1
@@ -392,6 +392,61 @@ PulseApp.utils = (() => {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Button state management utilities
|
||||
function setButtonLoading(button, loadingText = 'Loading...') {
|
||||
if (!button) return null;
|
||||
|
||||
const originalState = {
|
||||
text: button.textContent,
|
||||
disabled: button.disabled,
|
||||
classList: button.className
|
||||
};
|
||||
|
||||
button.textContent = loadingText;
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
|
||||
return originalState;
|
||||
}
|
||||
|
||||
function resetButton(button, originalState) {
|
||||
if (!button || !originalState) return;
|
||||
|
||||
button.textContent = originalState.text;
|
||||
button.disabled = originalState.disabled;
|
||||
button.className = originalState.classList;
|
||||
}
|
||||
|
||||
// Date formatting utilities
|
||||
function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
if (!date) return '';
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function formatRelativeTime(date) {
|
||||
if (!date) return '';
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
|
||||
if (diffSec < 60) return `${diffSec} seconds ago`;
|
||||
if (diffMin < 60) return `${diffMin} minutes ago`;
|
||||
if (diffHour < 24) return `${diffHour} hours ago`;
|
||||
if (diffDay < 30) return `${diffDay} days ago`;
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
// Return the public API for this module
|
||||
return {
|
||||
sanitizeForId: (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'),
|
||||
@@ -413,6 +468,13 @@ PulseApp.utils = (() => {
|
||||
updateProgressBarTextsDebounced,
|
||||
preserveScrollPosition,
|
||||
getScrollableParent,
|
||||
getHostUrl
|
||||
getHostUrl,
|
||||
// Button state management
|
||||
setButtonLoading,
|
||||
resetButton,
|
||||
// Date formatting
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
formatRelativeTime
|
||||
};
|
||||
})();
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Toggle Debug Test</title>
|
||||
<script>
|
||||
// Test function to debug toggle behavior
|
||||
function debugToggles() {
|
||||
console.log('=== Toggle Debug Info ===');
|
||||
|
||||
// Check for email toggle
|
||||
const emailToggle = document.getElementById('global-email-toggle');
|
||||
console.log('Email toggle found:', !!emailToggle);
|
||||
if (emailToggle) {
|
||||
console.log('Email toggle checked:', emailToggle.checked);
|
||||
console.log('Email toggle disabled:', emailToggle.disabled);
|
||||
}
|
||||
|
||||
// Check for webhook toggle
|
||||
const webhookToggle = document.getElementById('global-webhook-toggle');
|
||||
console.log('Webhook toggle found:', !!webhookToggle);
|
||||
if (webhookToggle) {
|
||||
console.log('Webhook toggle checked:', webhookToggle.checked);
|
||||
console.log('Webhook toggle disabled:', webhookToggle.disabled);
|
||||
}
|
||||
|
||||
// Check for config sections
|
||||
const emailSection = document.getElementById('email-config-section');
|
||||
console.log('Email config section found:', !!emailSection);
|
||||
if (emailSection) {
|
||||
console.log('Email section opacity:', emailSection.style.opacity);
|
||||
}
|
||||
|
||||
const webhookSection = document.getElementById('webhook-config-section');
|
||||
console.log('Webhook config section found:', !!webhookSection);
|
||||
if (webhookSection) {
|
||||
console.log('Webhook section opacity:', webhookSection.style.opacity);
|
||||
}
|
||||
|
||||
// Check if functions exist
|
||||
console.log('updateEmailConfigVisibility exists:', typeof updateEmailConfigVisibility === 'function');
|
||||
console.log('updateWebhookConfigVisibility exists:', typeof updateWebhookConfigVisibility === 'function');
|
||||
|
||||
console.log('=== End Debug Info ===');
|
||||
}
|
||||
|
||||
// Add this to the browser console
|
||||
window.debugToggles = debugToggles;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Toggle Debug Test</h1>
|
||||
<p>Open the browser console and run: <code>debugToggles()</code></p>
|
||||
<p>Or add this to the alertManagementModal.js temporarily to debug</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user