mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-06 23:13:17 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5eb54e02 | ||
|
|
a8eb3ec21c |
@@ -54,3 +54,8 @@ ENABLE_LOGGING=true
|
||||
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||
TFA_MAX_REMEMBER_SESSIONS=5
|
||||
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 { getPrismaClient } = require("../config/prisma");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { get_current_time, parse_date } = require("../utils/timezone");
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const router = express.Router();
|
||||
@@ -537,14 +538,7 @@ router.post("/collect", async (req, res) => {
|
||||
return res.status(401).json({ error: "Invalid API credentials" });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// 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;
|
||||
};
|
||||
const now = get_current_time();
|
||||
|
||||
// Process containers
|
||||
if (containers && Array.isArray(containers)) {
|
||||
@@ -572,7 +566,7 @@ router.post("/collect", async (req, res) => {
|
||||
tag: containerData.image_tag,
|
||||
image_id: containerData.image_id || "unknown",
|
||||
source: containerData.image_source || "docker-hub",
|
||||
created_at: parseDate(containerData.created_at),
|
||||
created_at: parse_date(containerData.created_at, now),
|
||||
last_checked: now,
|
||||
updated_at: now,
|
||||
},
|
||||
@@ -597,7 +591,7 @@ router.post("/collect", async (req, res) => {
|
||||
state: containerData.state,
|
||||
ports: containerData.ports || null,
|
||||
started_at: containerData.started_at
|
||||
? parseDate(containerData.started_at)
|
||||
? parse_date(containerData.started_at, null)
|
||||
: null,
|
||||
updated_at: now,
|
||||
last_checked: now,
|
||||
@@ -613,9 +607,9 @@ router.post("/collect", async (req, res) => {
|
||||
status: containerData.status,
|
||||
state: containerData.state,
|
||||
ports: containerData.ports || null,
|
||||
created_at: parseDate(containerData.created_at),
|
||||
created_at: parse_date(containerData.created_at, now),
|
||||
started_at: containerData.started_at
|
||||
? parseDate(containerData.started_at)
|
||||
? parse_date(containerData.started_at, null)
|
||||
: null,
|
||||
updated_at: now,
|
||||
},
|
||||
@@ -651,7 +645,7 @@ router.post("/collect", async (req, res) => {
|
||||
? BigInt(imageData.size_bytes)
|
||||
: null,
|
||||
source: imageData.source || "docker-hub",
|
||||
created_at: parseDate(imageData.created_at),
|
||||
created_at: parse_date(imageData.created_at, now),
|
||||
updated_at: now,
|
||||
},
|
||||
});
|
||||
@@ -780,14 +774,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
||||
`[Docker Integration] Processing for host: ${host.friendly_name}`,
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// 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;
|
||||
};
|
||||
const now = get_current_time();
|
||||
|
||||
let containersProcessed = 0;
|
||||
let imagesProcessed = 0;
|
||||
@@ -822,7 +809,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
||||
tag: containerData.image_tag,
|
||||
image_id: containerData.image_id || "unknown",
|
||||
source: containerData.image_source || "docker-hub",
|
||||
created_at: parseDate(containerData.created_at),
|
||||
created_at: parse_date(containerData.created_at, now),
|
||||
last_checked: now,
|
||||
updated_at: now,
|
||||
},
|
||||
@@ -847,7 +834,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
||||
state: containerData.state || containerData.status,
|
||||
ports: containerData.ports || null,
|
||||
started_at: containerData.started_at
|
||||
? parseDate(containerData.started_at)
|
||||
? parse_date(containerData.started_at, null)
|
||||
: null,
|
||||
updated_at: now,
|
||||
last_checked: now,
|
||||
@@ -863,9 +850,9 @@ router.post("/../integrations/docker", async (req, res) => {
|
||||
status: containerData.status,
|
||||
state: containerData.state || containerData.status,
|
||||
ports: containerData.ports || null,
|
||||
created_at: parseDate(containerData.created_at),
|
||||
created_at: parse_date(containerData.created_at, now),
|
||||
started_at: containerData.started_at
|
||||
? parseDate(containerData.started_at)
|
||||
? parse_date(containerData.started_at, null)
|
||||
: null,
|
||||
updated_at: now,
|
||||
},
|
||||
@@ -911,7 +898,7 @@ router.post("/../integrations/docker", async (req, res) => {
|
||||
? BigInt(imageData.size_bytes)
|
||||
: null,
|
||||
source: imageSource,
|
||||
created_at: parseDate(imageData.created_at),
|
||||
created_at: parse_date(imageData.created_at, now),
|
||||
last_checked: now,
|
||||
updated_at: now,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const WebSocket = require("ws");
|
||||
const url = require("node:url");
|
||||
const { get_current_time } = require("../utils/timezone");
|
||||
|
||||
// Connection registry by api_id
|
||||
const apiIdToSocket = new Map();
|
||||
@@ -49,7 +50,29 @@ function init(server, prismaClient) {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
ws.on("message", (message) => {
|
||||
// Echo back for Bull Board WebSocket
|
||||
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;
|
||||
@@ -117,7 +140,58 @@ 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);
|
||||
if (existing === ws) {
|
||||
apiIdToSocket.delete(apiId);
|
||||
@@ -126,7 +200,7 @@ function init(server, prismaClient) {
|
||||
notifyConnectionChange(apiId, false);
|
||||
}
|
||||
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 +388,7 @@ async function handleDockerStatusEvent(apiId, message) {
|
||||
status: status,
|
||||
state: status,
|
||||
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...");
|
||||
|
||||
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({
|
||||
where: {
|
||||
digest: {
|
||||
not: null,
|
||||
},
|
||||
repository: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
docker_image_updates: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ const { redis, redisConnection } = require("./shared/redis");
|
||||
const { prisma } = require("./shared/prisma");
|
||||
const agentWs = require("../agentWs");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { get_current_time } = require("../../utils/timezone");
|
||||
|
||||
// Import automation classes
|
||||
const GitHubUpdateCheck = require("./githubUpdateCheck");
|
||||
@@ -216,8 +217,8 @@ class QueueManager {
|
||||
api_id: api_id,
|
||||
status: "active",
|
||||
attempt_number: job.attemptsMade + 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
created_at: get_current_time(),
|
||||
updated_at: get_current_time(),
|
||||
},
|
||||
});
|
||||
console.log(`📝 Logged job to job_history: ${job.id} (${type})`);
|
||||
@@ -257,8 +258,8 @@ class QueueManager {
|
||||
where: { job_id: job.id },
|
||||
data: {
|
||||
status: "completed",
|
||||
completed_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
completed_at: get_current_time(),
|
||||
updated_at: get_current_time(),
|
||||
},
|
||||
});
|
||||
console.log(`✅ Marked job as completed in job_history: ${job.id}`);
|
||||
@@ -271,8 +272,8 @@ class QueueManager {
|
||||
data: {
|
||||
status: "failed",
|
||||
error_message: error.message,
|
||||
completed_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
completed_at: get_current_time(),
|
||||
updated_at: get_current_time(),
|
||||
},
|
||||
});
|
||||
console.log(`❌ Marked job as failed in job_history: ${job.id}`);
|
||||
|
||||
107
backend/src/utils/timezone.js
Normal file
107
backend/src/utils/timezone.js
Normal file
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user