From 7a3d98862f77e6243395aaf7704679c1063cd7c0 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Sat, 1 Nov 2025 02:55:38 +0000 Subject: [PATCH 1/7] Fix emoji parsing error in print functions - Changed from echo -e to printf for safer special character handling - Store emoji characters in variables using bash octal escape sequences - Prevents 'command not found' error when bash interprets emoji as commands - Fixes issue where line 41 error occurred during setup.sh --update --- setup.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/setup.sh b/setup.sh index bb2501e..1858926 100755 --- a/setup.sh +++ b/setup.sh @@ -33,6 +33,15 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +# Emoji characters (using octal escape sequences to avoid parsing issues) +EMOJI_CHECK=$'\342\234\225' +EMOJI_INFO=$'\342\204\271\357\270\217' +EMOJI_CROSS=$'\342\234\215' +EMOJI_WARN=$'\342\234\252' +EMOJI_QUEST=$'\342\234\223' +EMOJI_PARTY=$'\360\237\216\211' +EMOJI_REFRESH=$'\342\235\224' + # Global variables SCRIPT_VERSION="self-hosting-install.sh v1.3.2-selfhost-2025-10-31-1" DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git" @@ -66,27 +75,27 @@ SELECTED_SERVICE_NAME="" # Functions print_status() { - echo -e "${GREEN}✅ $1${NC}" + printf "${GREEN}%s %s${NC}\n" "$EMOJI_CHECK" "$1" } print_info() { - echo -e "${BLUE}ℹ️ $1${NC}" + printf "${BLUE}%s %s${NC}\n" "$EMOJI_INFO" "$1" } print_error() { - echo -e "${RED}❌ $1${NC}" + printf "${RED}%s %s${NC}\n" "$EMOJI_CROSS" "$1" } print_warning() { - echo -e "${YELLOW}⚠️ $1${NC}" + printf "${YELLOW}%s %s${NC}\n" "$EMOJI_WARN" "$1" } print_question() { - echo -e "${BLUE}❓ $1${NC}" + printf "${BLUE}%s %s${NC}\n" "$EMOJI_QUEST" "$1" } print_success() { - echo -e "${GREEN}🎉 $1${NC}" + printf "${GREEN}%s %s${NC}\n" "$EMOJI_PARTY" "$1" } # Interactive input functions From e57ff7612e0b1fbf78f3f5333c4bab0aa3d9671d Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Sat, 1 Nov 2025 03:25:31 +0000 Subject: [PATCH 2/7] removed emoji --- setup.sh | 53 ++++++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/setup.sh b/setup.sh index 1858926..4343e8a 100755 --- a/setup.sh +++ b/setup.sh @@ -33,15 +33,6 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color -# Emoji characters (using octal escape sequences to avoid parsing issues) -EMOJI_CHECK=$'\342\234\225' -EMOJI_INFO=$'\342\204\271\357\270\217' -EMOJI_CROSS=$'\342\234\215' -EMOJI_WARN=$'\342\234\252' -EMOJI_QUEST=$'\342\234\223' -EMOJI_PARTY=$'\360\237\216\211' -EMOJI_REFRESH=$'\342\235\224' - # Global variables SCRIPT_VERSION="self-hosting-install.sh v1.3.2-selfhost-2025-10-31-1" DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git" @@ -75,27 +66,27 @@ SELECTED_SERVICE_NAME="" # Functions print_status() { - printf "${GREEN}%s %s${NC}\n" "$EMOJI_CHECK" "$1" + printf "${GREEN}%s${NC}\n" "$1" } print_info() { - printf "${BLUE}%s %s${NC}\n" "$EMOJI_INFO" "$1" + printf "${BLUE}%s${NC}\n" "$1" } print_error() { - printf "${RED}%s %s${NC}\n" "$EMOJI_CROSS" "$1" + printf "${RED}%s${NC}\n" "$1" } print_warning() { - printf "${YELLOW}%s %s${NC}\n" "$EMOJI_WARN" "$1" + printf "${YELLOW}%s${NC}\n" "$1" } print_question() { - printf "${BLUE}%s %s${NC}\n" "$EMOJI_QUEST" "$1" + printf "${BLUE}%s${NC}\n" "$1" } print_success() { - printf "${GREEN}%s %s${NC}\n" "$EMOJI_PARTY" "$1" + printf "${GREEN}%s${NC}\n" "$1" } # Interactive input functions @@ -1666,7 +1657,7 @@ start_services() { local logs=$(journalctl -u "$SERVICE_NAME" -n 50 --no-pager 2>/dev/null || echo "") if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then - print_error "❌ Detected Redis authentication error!" + print_error "Detected Redis authentication error!" print_info "The service cannot authenticate with Redis." echo "" print_info "Current Redis configuration in .env:" @@ -1690,18 +1681,18 @@ start_services() { print_info " cat /etc/redis/users.acl" echo "" elif echo "$logs" | grep -q "ECONNREFUSED.*postgresql\|Connection refused.*5432"; then - print_error "❌ Detected PostgreSQL connection error!" + print_error "Detected PostgreSQL connection error!" print_info "Check if PostgreSQL is running:" print_info " systemctl status postgresql" elif echo "$logs" | grep -q "ECONNREFUSED.*redis\|Connection refused.*6379"; then - print_error "❌ Detected Redis connection error!" + print_error "Detected Redis connection error!" print_info "Check if Redis is running:" print_info " systemctl status redis-server" elif echo "$logs" | grep -q "database.*does not exist"; then - print_error "❌ Database does not exist!" + print_error "Database does not exist!" print_info "Database: $DB_NAME" elif echo "$logs" | grep -q "Error:"; then - print_error "❌ Application error detected in logs" + print_error "Application error detected in logs" fi echo "" @@ -1750,9 +1741,9 @@ async function updateSettings() { }); } - console.log('✅ Database settings updated successfully'); + console.log('Database settings updated successfully'); } catch (error) { - console.error('❌ Error updating settings:', error.message); + console.error('Error updating settings:', error.message); process.exit(1); } finally { await prisma.\$disconnect(); @@ -1876,7 +1867,7 @@ EOF if [ -f "$SUMMARY_FILE" ]; then print_status "Deployment summary appended to: $SUMMARY_FILE" else - print_error "⚠️ Failed to append to deployment-info.txt file" + print_error "Failed to append to deployment-info.txt file" return 1 fi } @@ -1958,7 +1949,7 @@ EOF print_status "Deployment information saved to: $INFO_FILE" print_info "File details: $(ls -lh "$INFO_FILE" | awk '{print $5, $9}')" else - print_error "⚠️ Failed to create deployment-info.txt file" + print_error "Failed to create deployment-info.txt file" return 1 fi } @@ -2151,7 +2142,7 @@ deploy_instance() { log_message "Backend port: $BACKEND_PORT" log_message "SSL enabled: $USE_LETSENCRYPT" - print_status "🎉 PatchMon instance deployed successfully!" + print_status "PatchMon instance deployed successfully!" echo "" print_info "Next steps:" echo " • Visit your URL: $SERVER_PROTOCOL_SEL://$FQDN (ensure DNS is configured)" @@ -3245,7 +3236,7 @@ update_installation() { sleep 5 if systemctl is-active --quiet "$service_name"; then - print_success "✅ Update completed successfully!" + print_success "Update completed successfully!" print_status "Service $service_name is running" # Get new version @@ -3273,7 +3264,7 @@ update_installation() { local logs=$(journalctl -u "$service_name" -n 50 --no-pager 2>/dev/null || echo "") if echo "$logs" | grep -q "WRONGPASS\|NOAUTH"; then - print_error "❌ Detected Redis authentication error!" + print_error "Detected Redis authentication error!" print_info "The service cannot authenticate with Redis." echo "" print_info "Current Redis configuration in .env:" @@ -3290,12 +3281,12 @@ update_installation() { print_info " redis-cli --user $test_user --pass $test_pass -n ${test_db:-0} ping" echo "" elif echo "$logs" | grep -q "ECONNREFUSED"; then - print_error "❌ Detected connection refused error!" + print_error "Detected connection refused error!" print_info "Check if required services are running:" print_info " systemctl status postgresql" print_info " systemctl status redis-server" elif echo "$logs" | grep -q "Error:"; then - print_error "❌ Application error detected in logs" + print_error "Application error detected in logs" fi echo "" @@ -3328,7 +3319,7 @@ main() { # Handle update mode if [ "$UPDATE_MODE" = "true" ]; then print_banner - print_info "🔄 PatchMon Update Mode" + print_info "PatchMon Update Mode" echo "" # Select installation to update @@ -3344,7 +3335,7 @@ main() { # Check if existing installations are present local existing_installs=($(detect_installations)) if [ ${#existing_installs[@]} -gt 0 ]; then - print_warning "⚠️ Found ${#existing_installs[@]} existing PatchMon installation(s):" + print_warning "Found ${#existing_installs[@]} existing PatchMon installation(s):" for install in "${existing_installs[@]}"; do print_info " - $install" done From 63831caba348b2a2d45afeb84f6c130d8a8c3569 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Fri, 7 Nov 2025 08:20:42 +0000 Subject: [PATCH 3/7] fixed tfa route for handling insertion of tfa number Better handling of existing systems already enrolled, done via checking if the config.yml file exists and ping through its credentials as opposed to checking for machine_ID UI justification improvements on repositories pages --- agents/patchmon_install.sh | 58 +++++++++++++++-------------- agents/proxmox_auto_enroll.sh | 44 ++++++++++++++++++++-- backend/src/routes/tfaRoutes.js | 15 ++++++-- frontend/src/pages/Repositories.jsx | 22 +++++++---- package-lock.json | 8 ++-- 5 files changed, 101 insertions(+), 46 deletions(-) diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh index 315f117..c41ba0b 100644 --- a/agents/patchmon_install.sh +++ b/agents/patchmon_install.sh @@ -311,6 +311,37 @@ else mkdir -p /etc/patchmon fi +# Check if agent is already configured and working (before we overwrite anything) +info "🔍 Checking if agent is already configured..." + +if [[ -f /etc/patchmon/config.yml ]] && [[ -f /etc/patchmon/credentials.yml ]]; then + if [[ -f /usr/local/bin/patchmon-agent ]]; then + info "📋 Found existing agent configuration" + info "🧪 Testing existing configuration with ping..." + + if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then + success "✅ Agent is already configured and ping successful" + info "📋 Existing configuration is working - skipping installation" + info "" + info "If you want to reinstall, remove the configuration files first:" + info " sudo rm -f /etc/patchmon/config.yml /etc/patchmon/credentials.yml" + echo "" + exit 0 + else + warning "⚠️ Agent configuration exists but ping failed" + warning "⚠️ Will move existing configuration and reinstall" + echo "" + fi + else + warning "⚠️ Configuration files exist but agent binary is missing" + warning "⚠️ Will move existing configuration and reinstall" + echo "" + fi +else + success "✅ Agent not yet configured - proceeding with installation" + echo "" +fi + # Step 2: Create configuration files info "🔐 Creating configuration files..." @@ -426,33 +457,6 @@ if [[ -f "/etc/patchmon/logs/patchmon-agent.log" ]]; then fi # Step 4: Test the configuration -# Check if this machine is already enrolled -info "🔍 Checking if machine is already enrolled..." -existing_check=$(curl $CURL_FLAGS -s -X POST \ - -H "X-API-ID: $API_ID" \ - -H "X-API-KEY: $API_KEY" \ - -H "Content-Type: application/json" \ - -d "{\"machine_id\": \"$MACHINE_ID\"}" \ - "$PATCHMON_URL/api/v1/hosts/check-machine-id" \ - -w "\n%{http_code}" 2>&1) - -http_code=$(echo "$existing_check" | tail -n 1) -response_body=$(echo "$existing_check" | sed '$d') - -if [[ "$http_code" == "200" ]]; then - already_enrolled=$(echo "$response_body" | jq -r '.exists' 2>/dev/null || echo "false") - if [[ "$already_enrolled" == "true" ]]; then - warning "⚠️ This machine is already enrolled in PatchMon" - info "Machine ID: $MACHINE_ID" - info "Existing host: $(echo "$response_body" | jq -r '.host.friendly_name' 2>/dev/null)" - info "" - info "The agent will be reinstalled/updated with existing credentials." - echo "" - else - success "✅ Machine not yet enrolled - proceeding with installation" - fi -fi - info "🧪 Testing API credentials and connectivity..." if /usr/local/bin/patchmon-agent ping; then success "✅ TEST: API credentials are valid and server is reachable" diff --git a/agents/proxmox_auto_enroll.sh b/agents/proxmox_auto_enroll.sh index 01c4158..04516f4 100755 --- a/agents/proxmox_auto_enroll.sh +++ b/agents/proxmox_auto_enroll.sh @@ -230,6 +230,40 @@ while IFS= read -r line; do info " ✓ Host enrolled successfully: $api_id" + # Check if agent is already installed and working + info " Checking if agent is already configured..." + config_check=$(timeout 10 pct exec "$vmid" -- bash -c " + if [[ -f /etc/patchmon/config.yml ]] && [[ -f /etc/patchmon/credentials.yml ]]; then + if [[ -f /usr/local/bin/patchmon-agent ]]; then + # Try to ping using existing configuration + if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then + echo 'ping_success' + else + echo 'ping_failed' + fi + else + echo 'binary_missing' + fi + else + echo 'not_configured' + fi + " 2>/dev/null /dev/null 2>&1 && echo 'installed' || echo 'missing'" 2>/dev/null { try { @@ -71,7 +76,11 @@ router.post( return res.status(400).json({ errors: errors.array() }); } - const { token } = req.body; + // Ensure token is a string (convert if needed) + let { token } = req.body; + if (typeof token !== "string") { + token = String(token); + } const userId = req.user.id; // Get user's TFA secret diff --git a/frontend/src/pages/Repositories.jsx b/frontend/src/pages/Repositories.jsx index 5bcf845..77d0300 100644 --- a/frontend/src/pages/Repositories.jsx +++ b/frontend/src/pages/Repositories.jsx @@ -237,8 +237,14 @@ const Repositories = () => { // Handle special cases if (sortField === "security") { - aValue = a.isSecure ? "Secure" : "Insecure"; - bValue = b.isSecure ? "Secure" : "Insecure"; + // Use the same logic as filtering to determine isSecure + const aIsSecure = + a.isSecure !== undefined ? a.isSecure : a.url.startsWith("https://"); + const bIsSecure = + b.isSecure !== undefined ? b.isSecure : b.url.startsWith("https://"); + // Sort by boolean: true (Secure) comes before false (Insecure) when ascending + aValue = aIsSecure ? 1 : 0; + bValue = bIsSecure ? 1 : 0; } else if (sortField === "status") { aValue = a.is_active ? "Active" : "Inactive"; bValue = b.is_active ? "Active" : "Inactive"; @@ -535,12 +541,12 @@ const Repositories = () => { {visibleColumns.map((column) => ( +
+
+ {/* Refresh Button */} + - {/* Period Selector */} - + {/* Period Selector */} + - {/* Host Selector */} - { + setPackageTrendsHost(e.target.value); + // Clear job ID message when host selection changes + setSystemStatsJobId(null); + }} + className="px-3 py-1.5 text-sm border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500" + > + + {packageTrendsData?.hosts?.length > 0 ? ( + packageTrendsData.hosts.map((host) => ( + + )) + ) : ( + - )) - ) : ( - - )} - + )} + +
+ {/* Job ID Message */} + {systemStatsJobId && packageTrendsHost === "all" && ( +

+ Ran collection job #{systemStatsJobId} +

+ )}
@@ -1167,13 +1221,40 @@ const Dashboard = () => { title: (context) => { const label = context[0].label; + // Handle "Now" label + if (label === "Now") { + return "Now"; + } + // Handle empty or invalid labels if (!label || typeof label !== "string") { return "Unknown Date"; } + // Check if it's a full ISO timestamp (for "Last 24 hours") + // Format: "2025-01-15T14:30:00.000Z" or "2025-01-15T14:30:00.000" + if (label.includes("T") && label.includes(":")) { + try { + const date = new Date(label); + // Check if date is valid + if (Number.isNaN(date.getTime())) { + return label; // Return original label if date is invalid + } + // Format full ISO timestamp with date and time + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + } catch (_error) { + return label; // Return original label if parsing fails + } + } + // Format hourly labels (e.g., "2025-10-07T14" -> "Oct 7, 2:00 PM") - if (label.includes("T")) { + if (label.includes("T") && !label.includes(":")) { try { const date = new Date(`${label}:00:00`); // Check if date is valid @@ -1233,13 +1314,41 @@ const Dashboard = () => { callback: function (value, _index, _ticks) { const label = this.getLabelForValue(value); + // Handle "Now" label + if (label === "Now") { + return "Now"; + } + // Handle empty or invalid labels if (!label || typeof label !== "string") { return "Unknown"; } + // Check if it's a full ISO timestamp (for "Last 24 hours") + // Format: "2025-01-15T14:30:00.000Z" or "2025-01-15T14:30:00.000" + if (label.includes("T") && label.includes(":")) { + try { + const date = new Date(label); + // Check if date is valid + if (Number.isNaN(date.getTime())) { + return label; // Return original label if date is invalid + } + // Extract hour from full ISO timestamp + const hourNum = date.getHours(); + return hourNum === 0 + ? "12 AM" + : hourNum < 12 + ? `${hourNum} AM` + : hourNum === 12 + ? "12 PM" + : `${hourNum - 12} PM`; + } catch (_error) { + return label; // Return original label if parsing fails + } + } + // Format hourly labels (e.g., "2025-10-07T14" -> "2 PM") - if (label.includes("T")) { + if (label.includes("T") && !label.includes(":")) { try { const hour = label.split("T")[1]; const hourNum = parseInt(hour, 10); diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 7b943b2..a625269 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -281,6 +281,67 @@ const HostDetail = () => { }, }); + // Fetch integration status + const { + data: integrationsData, + isLoading: isLoadingIntegrations, + refetch: refetchIntegrations, + } = useQuery({ + queryKey: ["host-integrations", hostId], + queryFn: () => + adminHostsAPI.getIntegrations(hostId).then((res) => res.data), + staleTime: 30 * 1000, // 30 seconds + refetchOnWindowFocus: false, + enabled: !!hostId && activeTab === "integrations", + }); + + // Refetch integrations when WebSocket status changes (e.g., after agent restart) + useEffect(() => { + if ( + wsStatus?.connected && + activeTab === "integrations" && + integrationsData?.data?.connected === false + ) { + // Agent just reconnected, refetch integrations to get updated connection status + refetchIntegrations(); + } + }, [ + wsStatus?.connected, + activeTab, + integrationsData?.data?.connected, + refetchIntegrations, + ]); + + // Toggle integration mutation + const toggleIntegrationMutation = useMutation({ + mutationFn: ({ integrationName, enabled }) => + adminHostsAPI + .toggleIntegration(hostId, integrationName, enabled) + .then((res) => res.data), + onSuccess: (data) => { + // Optimistically update the cache with the new state + queryClient.setQueryData(["host-integrations", hostId], (oldData) => { + if (!oldData) return oldData; + return { + ...oldData, + data: { + ...oldData.data, + integrations: { + ...oldData.data.integrations, + [data.data.integration]: data.data.enabled, + }, + }, + }; + }); + // Also invalidate to ensure we get fresh data + queryClient.invalidateQueries(["host-integrations", hostId]); + }, + onError: () => { + // On error, refetch to get the actual state + refetchIntegrations(); + }, + }); + const handleDeleteHost = async () => { if ( window.confirm( @@ -666,6 +727,17 @@ const HostDetail = () => { > Notes +
@@ -1446,6 +1518,101 @@ const HostDetail = () => { {/* Agent Queue */} {activeTab === "queue" && } + + {/* Integrations */} + {activeTab === "integrations" && ( +
+ {isLoadingIntegrations ? ( +
+ +
+ ) : ( +
+ {/* Docker Integration */} +
+
+
+
+ +

+ Docker +

+ {integrationsData?.data?.integrations?.docker ? ( + + Enabled + + ) : ( + + Disabled + + )} +
+

+ Monitor Docker containers, images, volumes, and + networks. Collects real-time container status + events. +

+
+
+ +
+
+ {!wsStatus?.connected && ( +

+ Agent must be connected via WebSocket to toggle + integrations +

+ )} + {toggleIntegrationMutation.isPending && ( +

+ Updating integration... +

+ )} +
+ + {/* Future integrations can be added here with the same pattern */} +
+ )} +
+ )}
@@ -1639,7 +1806,8 @@ const CredentialsModal = ({ host, isOpen, onClose }) => { > - + +

Select the architecture of the target host diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index f2bf4aa..59642de 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -99,6 +99,8 @@ export const dashboardAPI = { }, getRecentUsers: () => api.get("/dashboard/recent-users"), getRecentCollection: () => api.get("/dashboard/recent-collection"), + triggerSystemStatistics: () => + api.post("/automation/trigger/system-statistics"), }; // Admin Hosts API (for management interface) @@ -129,6 +131,11 @@ export const adminHostsAPI = { api.patch(`/hosts/${hostId}/notes`, { notes: notes, }), + getIntegrations: (hostId) => api.get(`/hosts/${hostId}/integrations`), + toggleIntegration: (hostId, integrationName, enabled) => + api.post(`/hosts/${hostId}/integrations/${integrationName}/toggle`, { + enabled, + }), }; // Host Groups API diff --git a/setup.sh b/setup.sh index bb2501e..920a9c1 100755 --- a/setup.sh +++ b/setup.sh @@ -443,7 +443,7 @@ generate_redis_password() { # Find next available Redis database find_next_redis_db() { - print_info "Finding next available Redis database..." + print_info "Finding next available Redis database..." >&2 # Start from database 0 and keep checking until we find an empty one local db_num=0 @@ -463,11 +463,11 @@ find_next_redis_db() { # Try to load admin credentials if ACL file exists if [ -f /etc/redis/users.acl ] && grep -q "^user admin" /etc/redis/users.acl; then # Redis is configured with ACL - try to extract admin password - print_info "Redis requires authentication, attempting with admin credentials..." + print_info "Redis requires authentication, attempting with admin credentials..." >&2 # For multi-instance setups, we can't know the admin password yet # So we'll just use database 0 as default - print_info "Using database 0 (Redis ACL already configured)" + print_info "Using database 0 (Redis ACL already configured)" >&2 echo "0" return 0 fi @@ -484,7 +484,7 @@ find_next_redis_db() { # Check for authentication errors if echo "$redis_output" | grep -q "NOAUTH\|WRONGPASS"; then # If we hit auth errors and haven't configured yet, use database 0 - print_info "Redis requires authentication, defaulting to database 0" + print_info "Redis requires authentication, defaulting to database 0" >&2 echo "0" return 0 fi @@ -492,10 +492,10 @@ find_next_redis_db() { # Check for other errors if echo "$redis_output" | grep -q "ERR"; then if echo "$redis_output" | grep -q "invalid DB index"; then - print_warning "Reached maximum database limit at database $db_num" + print_warning "Reached maximum database limit at database $db_num" >&2 break else - print_error "Error checking database $db_num: $redis_output" + print_error "Error checking database $db_num: $redis_output" >&2 return 1 fi fi @@ -504,17 +504,17 @@ find_next_redis_db() { # If database is empty, use it if [ "$key_count" = "0" ] || [ "$key_count" = "(integer) 0" ]; then - print_status "Found available Redis database: $db_num (empty)" + print_status "Found available Redis database: $db_num (empty)" >&2 echo "$db_num" return 0 fi - print_info "Database $db_num has $key_count keys, checking next..." + print_info "Database $db_num has $key_count keys, checking next..." >&2 db_num=$((db_num + 1)) done - print_warning "No available Redis databases found (checked 0-$max_attempts)" - print_info "Using database 0 (may have existing data)" + print_warning "No available Redis databases found (checked 0-$max_attempts)" >&2 + print_info "Using database 0 (may have existing data)" >&2 echo "0" return 0 } From 497aeb8068a8efff3177b56660a8183519ea1420 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Fri, 7 Nov 2025 22:04:27 +0000 Subject: [PATCH 6/7] fixed biome and added tz --- backend/src/utils/timezone.js | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 backend/src/utils/timezone.js diff --git a/backend/src/utils/timezone.js b/backend/src/utils/timezone.js new file mode 100644 index 0000000..7cece69 --- /dev/null +++ b/backend/src/utils/timezone.js @@ -0,0 +1,107 @@ +/** + * Timezone utility functions for consistent timestamp handling + * + * This module provides timezone-aware timestamp functions that use + * the TZ environment variable for consistent timezone handling across + * the application. If TZ is not set, defaults to UTC. + */ + +/** + * Get the configured timezone from environment variable + * Defaults to UTC if not set + * @returns {string} Timezone string (e.g., 'UTC', 'America/New_York', 'Europe/London') + */ +function get_timezone() { + return process.env.TZ || process.env.TIMEZONE || "UTC"; +} + +/** + * Get current date/time in the configured timezone + * Returns a Date object that represents the current time in the configured timezone + * @returns {Date} Current date/time + */ +function get_current_time() { + const tz = get_timezone(); + + // If UTC, use Date.now() which is always UTC + if (tz === "UTC" || tz === "Etc/UTC") { + return new Date(); + } + + // For other timezones, we need to create a date string with timezone info + // and parse it. This ensures the date represents the correct time in that timezone. + // For database storage, we always store UTC timestamps + // The timezone is primarily used for display purposes + return new Date(); +} + +/** + * Get current timestamp in milliseconds (UTC) + * This is always UTC for database storage consistency + * @returns {number} Current timestamp in milliseconds + */ +function get_current_timestamp() { + return Date.now(); +} + +/** + * Format a date to ISO string in the configured timezone + * @param {Date} date - Date to format (defaults to now) + * @returns {string} ISO formatted date string + */ +function format_date_iso(date = null) { + const d = date || get_current_time(); + return d.toISOString(); +} + +/** + * Parse a date string and return a Date object + * Handles various date formats and timezone conversions + * @param {string} date_string - Date string to parse + * @param {Date} fallback - Fallback date if parsing fails (defaults to now) + * @returns {Date} Parsed date or fallback + */ +function parse_date(date_string, fallback = null) { + if (!date_string) { + return fallback || get_current_time(); + } + + try { + const date = new Date(date_string); + if (Number.isNaN(date.getTime())) { + return fallback || get_current_time(); + } + return date; + } catch (_error) { + return fallback || get_current_time(); + } +} + +/** + * Convert a date to the configured timezone for display + * @param {Date} date - Date to convert + * @returns {string} Formatted date string in configured timezone + */ +function format_date_for_display(date) { + const tz = get_timezone(); + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: tz, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); + return formatter.format(date); +} + +module.exports = { + get_timezone, + get_current_time, + get_current_timestamp, + format_date_iso, + parse_date, + format_date_for_display, +}; From 90e56d62bba23431abb0e35ce54a54b2e899e25c Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Fri, 7 Nov 2025 22:07:21 +0000 Subject: [PATCH 7/7] Update biome to 2.3.4 to match CI --- biome.json | 2 +- package-lock.json | 153 +++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 3 files changed, 140 insertions(+), 17 deletions(-) diff --git a/biome.json b/biome.json index 29a6487..f81ca03 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/package-lock.json b/package-lock.json index fb91b82..b293bcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "patchmon", - "version": "1.3.1", + "version": "1.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "patchmon", - "version": "1.3.1", + "version": "1.3.2", "license": "AGPL-3.0", "workspaces": [ "backend", "frontend" ], "devDependencies": { - "@biomejs/biome": "^2.3.0", + "@biomejs/biome": "^2.3.4", "concurrently": "^8.2.2", "lefthook": "^1.13.4" }, @@ -23,7 +23,7 @@ }, "backend": { "name": "patchmon-backend", - "version": "1.3.1", + "version": "1.3.2", "license": "AGPL-3.0", "dependencies": { "@bull-board/api": "^6.13.1", @@ -59,7 +59,7 @@ }, "frontend": { "name": "patchmon-frontend", - "version": "1.3.1", + "version": "1.3.2", "license": "AGPL-3.0", "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -362,7 +362,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.0", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.4.tgz", + "integrity": "sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -376,18 +378,88 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.0", - "@biomejs/cli-darwin-x64": "2.3.0", - "@biomejs/cli-linux-arm64": "2.3.0", - "@biomejs/cli-linux-arm64-musl": "2.3.0", - "@biomejs/cli-linux-x64": "2.3.0", - "@biomejs/cli-linux-x64-musl": "2.3.0", - "@biomejs/cli-win32-arm64": "2.3.0", - "@biomejs/cli-win32-x64": "2.3.0" + "@biomejs/cli-darwin-arm64": "2.3.4", + "@biomejs/cli-darwin-x64": "2.3.4", + "@biomejs/cli-linux-arm64": "2.3.4", + "@biomejs/cli-linux-arm64-musl": "2.3.4", + "@biomejs/cli-linux-x64": "2.3.4", + "@biomejs/cli-linux-x64-musl": "2.3.4", + "@biomejs/cli-win32-arm64": "2.3.4", + "@biomejs/cli-win32-x64": "2.3.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.4.tgz", + "integrity": "sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.4.tgz", + "integrity": "sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.4.tgz", + "integrity": "sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.4.tgz", + "integrity": "sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.0", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.4.tgz", + "integrity": "sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==", "cpu": [ "x64" ], @@ -401,6 +473,57 @@ "node": ">=14.21.3" } }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.4.tgz", + "integrity": "sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.4.tgz", + "integrity": "sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.4.tgz", + "integrity": "sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@bull-board/api": { "version": "6.13.1", "license": "MIT", diff --git a/package.json b/package.json index 7c43d31..fa6772b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "lint:fix": "biome check --write ." }, "devDependencies": { - "@biomejs/biome": "^2.3.0", + "@biomejs/biome": "^2.3.4", "concurrently": "^8.2.2", "lefthook": "^1.13.4" },