mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-09 16:37:29 +00:00
fix docker error handling
fix websocket routes Add timezone variable in code changed the env.example to suit
This commit is contained in:
@@ -54,3 +54,8 @@ ENABLE_LOGGING=true
|
|||||||
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||||
TFA_MAX_REMEMBER_SESSIONS=5
|
TFA_MAX_REMEMBER_SESSIONS=5
|
||||||
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
||||||
|
|
||||||
|
# Timezone Configuration
|
||||||
|
# Set the timezone for timestamps and logs (e.g., 'UTC', 'America/New_York', 'Europe/London')
|
||||||
|
# Defaults to UTC if not set. This ensures consistent timezone handling across the application.
|
||||||
|
TZ=UTC
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require("express");
|
|||||||
const { authenticateToken } = require("../middleware/auth");
|
const { authenticateToken } = require("../middleware/auth");
|
||||||
const { getPrismaClient } = require("../config/prisma");
|
const { getPrismaClient } = require("../config/prisma");
|
||||||
const { v4: uuidv4 } = require("uuid");
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
const { get_current_time, parse_date } = require("../utils/timezone");
|
||||||
|
|
||||||
const prisma = getPrismaClient();
|
const prisma = getPrismaClient();
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -537,14 +538,7 @@ router.post("/collect", async (req, res) => {
|
|||||||
return res.status(401).json({ error: "Invalid API credentials" });
|
return res.status(401).json({ error: "Invalid API credentials" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = get_current_time();
|
||||||
|
|
||||||
// Helper function to validate and parse dates
|
|
||||||
const parseDate = (dateString) => {
|
|
||||||
if (!dateString) return now;
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return Number.isNaN(date.getTime()) ? now : date;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process containers
|
// Process containers
|
||||||
if (containers && Array.isArray(containers)) {
|
if (containers && Array.isArray(containers)) {
|
||||||
@@ -572,7 +566,7 @@ router.post("/collect", async (req, res) => {
|
|||||||
tag: containerData.image_tag,
|
tag: containerData.image_tag,
|
||||||
image_id: containerData.image_id || "unknown",
|
image_id: containerData.image_id || "unknown",
|
||||||
source: containerData.image_source || "docker-hub",
|
source: containerData.image_source || "docker-hub",
|
||||||
created_at: parseDate(containerData.created_at),
|
created_at: parse_date(containerData.created_at, now),
|
||||||
last_checked: now,
|
last_checked: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
@@ -597,7 +591,7 @@ router.post("/collect", async (req, res) => {
|
|||||||
state: containerData.state,
|
state: containerData.state,
|
||||||
ports: containerData.ports || null,
|
ports: containerData.ports || null,
|
||||||
started_at: containerData.started_at
|
started_at: containerData.started_at
|
||||||
? parseDate(containerData.started_at)
|
? parse_date(containerData.started_at, null)
|
||||||
: null,
|
: null,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
last_checked: now,
|
last_checked: now,
|
||||||
@@ -613,9 +607,9 @@ router.post("/collect", async (req, res) => {
|
|||||||
status: containerData.status,
|
status: containerData.status,
|
||||||
state: containerData.state,
|
state: containerData.state,
|
||||||
ports: containerData.ports || null,
|
ports: containerData.ports || null,
|
||||||
created_at: parseDate(containerData.created_at),
|
created_at: parse_date(containerData.created_at, now),
|
||||||
started_at: containerData.started_at
|
started_at: containerData.started_at
|
||||||
? parseDate(containerData.started_at)
|
? parse_date(containerData.started_at, null)
|
||||||
: null,
|
: null,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
@@ -651,7 +645,7 @@ router.post("/collect", async (req, res) => {
|
|||||||
? BigInt(imageData.size_bytes)
|
? BigInt(imageData.size_bytes)
|
||||||
: null,
|
: null,
|
||||||
source: imageData.source || "docker-hub",
|
source: imageData.source || "docker-hub",
|
||||||
created_at: parseDate(imageData.created_at),
|
created_at: parse_date(imageData.created_at, now),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -780,14 +774,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
|||||||
`[Docker Integration] Processing for host: ${host.friendly_name}`,
|
`[Docker Integration] Processing for host: ${host.friendly_name}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const now = new Date();
|
const now = get_current_time();
|
||||||
|
|
||||||
// Helper function to validate and parse dates
|
|
||||||
const parseDate = (dateString) => {
|
|
||||||
if (!dateString) return now;
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return Number.isNaN(date.getTime()) ? now : date;
|
|
||||||
};
|
|
||||||
|
|
||||||
let containersProcessed = 0;
|
let containersProcessed = 0;
|
||||||
let imagesProcessed = 0;
|
let imagesProcessed = 0;
|
||||||
@@ -822,7 +809,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
|||||||
tag: containerData.image_tag,
|
tag: containerData.image_tag,
|
||||||
image_id: containerData.image_id || "unknown",
|
image_id: containerData.image_id || "unknown",
|
||||||
source: containerData.image_source || "docker-hub",
|
source: containerData.image_source || "docker-hub",
|
||||||
created_at: parseDate(containerData.created_at),
|
created_at: parse_date(containerData.created_at, now),
|
||||||
last_checked: now,
|
last_checked: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
@@ -847,7 +834,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
|||||||
state: containerData.state || containerData.status,
|
state: containerData.state || containerData.status,
|
||||||
ports: containerData.ports || null,
|
ports: containerData.ports || null,
|
||||||
started_at: containerData.started_at
|
started_at: containerData.started_at
|
||||||
? parseDate(containerData.started_at)
|
? parse_date(containerData.started_at, null)
|
||||||
: null,
|
: null,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
last_checked: now,
|
last_checked: now,
|
||||||
@@ -863,9 +850,9 @@ router.post("/../integrations/docker", async (req, res) => {
|
|||||||
status: containerData.status,
|
status: containerData.status,
|
||||||
state: containerData.state || containerData.status,
|
state: containerData.state || containerData.status,
|
||||||
ports: containerData.ports || null,
|
ports: containerData.ports || null,
|
||||||
created_at: parseDate(containerData.created_at),
|
created_at: parse_date(containerData.created_at, now),
|
||||||
started_at: containerData.started_at
|
started_at: containerData.started_at
|
||||||
? parseDate(containerData.started_at)
|
? parse_date(containerData.started_at, null)
|
||||||
: null,
|
: null,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
@@ -911,7 +898,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
|||||||
? BigInt(imageData.size_bytes)
|
? BigInt(imageData.size_bytes)
|
||||||
: null,
|
: null,
|
||||||
source: imageSource,
|
source: imageSource,
|
||||||
created_at: parseDate(imageData.created_at),
|
created_at: parse_date(imageData.created_at, now),
|
||||||
last_checked: now,
|
last_checked: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
const url = require("node:url");
|
const url = require("node:url");
|
||||||
|
const { get_current_time } = require("../utils/timezone");
|
||||||
|
|
||||||
// Connection registry by api_id
|
// Connection registry by api_id
|
||||||
const apiIdToSocket = new Map();
|
const apiIdToSocket = new Map();
|
||||||
@@ -49,7 +50,29 @@ function init(server, prismaClient) {
|
|||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
ws.on("message", (message) => {
|
ws.on("message", (message) => {
|
||||||
// Echo back for Bull Board WebSocket
|
// Echo back for Bull Board WebSocket
|
||||||
ws.send(message);
|
try {
|
||||||
|
ws.send(message);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore send errors (connection may be closed)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (err) => {
|
||||||
|
// Handle WebSocket errors gracefully for Bull Board
|
||||||
|
if (
|
||||||
|
err.code === "WS_ERR_INVALID_CLOSE_CODE" ||
|
||||||
|
err.code === "ECONNRESET" ||
|
||||||
|
err.code === "EPIPE"
|
||||||
|
) {
|
||||||
|
// These are expected errors, just log quietly
|
||||||
|
console.log("[bullboard-ws] connection error:", err.code);
|
||||||
|
} else {
|
||||||
|
console.error("[bullboard-ws] error:", err.message || err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
// Connection closed, no action needed
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -117,7 +140,57 @@ function init(server, prismaClient) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("error", (err) => {
|
||||||
|
// Handle WebSocket errors gracefully without crashing
|
||||||
|
// Common errors: invalid close codes (1006), connection resets, etc.
|
||||||
|
if (
|
||||||
|
err.code === "WS_ERR_INVALID_CLOSE_CODE" ||
|
||||||
|
err.message?.includes("invalid status code 1006") ||
|
||||||
|
err.message?.includes("Invalid WebSocket frame")
|
||||||
|
) {
|
||||||
|
// 1006 is a special close code indicating abnormal closure
|
||||||
|
// It cannot be sent in a close frame, but can occur when connection is lost
|
||||||
|
console.log(
|
||||||
|
`[agent-ws] connection error for ${apiId} (abnormal closure):`,
|
||||||
|
err.message || err.code,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
err.code === "ECONNRESET" ||
|
||||||
|
err.code === "EPIPE" ||
|
||||||
|
err.message?.includes("read ECONNRESET")
|
||||||
|
) {
|
||||||
|
// Connection reset errors are common and expected
|
||||||
|
console.log(
|
||||||
|
`[agent-ws] connection reset for ${apiId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Log other errors for debugging
|
||||||
|
console.error(
|
||||||
|
`[agent-ws] error for ${apiId}:`,
|
||||||
|
err.message || err.code || err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up connection on error
|
||||||
|
const existing = apiIdToSocket.get(apiId);
|
||||||
|
if (existing === ws) {
|
||||||
|
apiIdToSocket.delete(apiId);
|
||||||
|
connectionMetadata.delete(apiId);
|
||||||
|
// Notify subscribers of disconnection
|
||||||
|
notifyConnectionChange(apiId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to close the connection gracefully if still open
|
||||||
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
try {
|
||||||
|
ws.close(1000); // Normal closure
|
||||||
|
} catch {
|
||||||
|
// Ignore errors when closing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", (code, reason) => {
|
||||||
const existing = apiIdToSocket.get(apiId);
|
const existing = apiIdToSocket.get(apiId);
|
||||||
if (existing === ws) {
|
if (existing === ws) {
|
||||||
apiIdToSocket.delete(apiId);
|
apiIdToSocket.delete(apiId);
|
||||||
@@ -126,7 +199,7 @@ function init(server, prismaClient) {
|
|||||||
notifyConnectionChange(apiId, false);
|
notifyConnectionChange(apiId, false);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[agent-ws] disconnected api_id=${apiId} total=${apiIdToSocket.size}`,
|
`[agent-ws] disconnected api_id=${apiId} code=${code} reason=${reason || "none"} total=${apiIdToSocket.size}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,7 +387,7 @@ async function handleDockerStatusEvent(apiId, message) {
|
|||||||
status: status,
|
status: status,
|
||||||
state: status,
|
state: status,
|
||||||
updated_at: new Date(timestamp || Date.now()),
|
updated_at: new Date(timestamp || Date.now()),
|
||||||
last_checked: new Date(),
|
last_checked: get_current_time(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -139,15 +139,13 @@ class DockerImageUpdateCheck {
|
|||||||
console.log("🐳 Starting Docker image update check...");
|
console.log("🐳 Starting Docker image update check...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all Docker images that have a digest and repository
|
// Get all Docker images that have a digest
|
||||||
|
// Note: repository is required (non-nullable) in schema, so we don't need to check it
|
||||||
const images = await prisma.docker_images.findMany({
|
const images = await prisma.docker_images.findMany({
|
||||||
where: {
|
where: {
|
||||||
digest: {
|
digest: {
|
||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
repository: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
docker_image_updates: true,
|
docker_image_updates: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { redis, redisConnection } = require("./shared/redis");
|
|||||||
const { prisma } = require("./shared/prisma");
|
const { prisma } = require("./shared/prisma");
|
||||||
const agentWs = require("../agentWs");
|
const agentWs = require("../agentWs");
|
||||||
const { v4: uuidv4 } = require("uuid");
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
const { get_current_time } = require("../../utils/timezone");
|
||||||
|
|
||||||
// Import automation classes
|
// Import automation classes
|
||||||
const GitHubUpdateCheck = require("./githubUpdateCheck");
|
const GitHubUpdateCheck = require("./githubUpdateCheck");
|
||||||
@@ -216,8 +217,8 @@ class QueueManager {
|
|||||||
api_id: api_id,
|
api_id: api_id,
|
||||||
status: "active",
|
status: "active",
|
||||||
attempt_number: job.attemptsMade + 1,
|
attempt_number: job.attemptsMade + 1,
|
||||||
created_at: new Date(),
|
created_at: get_current_time(),
|
||||||
updated_at: new Date(),
|
updated_at: get_current_time(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(`📝 Logged job to job_history: ${job.id} (${type})`);
|
console.log(`📝 Logged job to job_history: ${job.id} (${type})`);
|
||||||
@@ -257,8 +258,8 @@ class QueueManager {
|
|||||||
where: { job_id: job.id },
|
where: { job_id: job.id },
|
||||||
data: {
|
data: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
completed_at: new Date(),
|
completed_at: get_current_time(),
|
||||||
updated_at: new Date(),
|
updated_at: get_current_time(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(`✅ Marked job as completed in job_history: ${job.id}`);
|
console.log(`✅ Marked job as completed in job_history: ${job.id}`);
|
||||||
@@ -271,8 +272,8 @@ class QueueManager {
|
|||||||
data: {
|
data: {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error_message: error.message,
|
error_message: error.message,
|
||||||
completed_at: new Date(),
|
completed_at: get_current_time(),
|
||||||
updated_at: new Date(),
|
updated_at: get_current_time(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(`❌ Marked job as failed in job_history: ${job.id}`);
|
console.log(`❌ Marked job as failed in job_history: ${job.id}`);
|
||||||
|
|||||||
124
backend/src/utils/timezone.js
Normal file
124
backend/src/utils/timezone.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
const now = new Date();
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = formatter.formatToParts(now);
|
||||||
|
const date_str = `${parts.find((p) => p.type === "year").value}-${parts.find((p) => p.type === "month").value}-${parts.find((p) => p.type === "day").value}T${parts.find((p) => p.type === "hour").value}:${parts.find((p) => p.type === "minute").value}:${parts.find((p) => p.type === "second").value}`;
|
||||||
|
|
||||||
|
// Create date in UTC, then adjust to represent the same moment in the target timezone
|
||||||
|
// This is a bit tricky - we'll use a simpler approach: store as UTC but display in timezone
|
||||||
|
// For database storage, we always store UTC timestamps
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user