fix docker error handling

fix websocket routes
Add timezone variable in code
changed the env.example to suit
This commit is contained in:
Muhammad Ibrahim
2025-11-06 22:08:00 +00:00
parent 913976b7f6
commit a8eb3ec21c
6 changed files with 228 additions and 40 deletions

View File

@@ -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

View File

@@ -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,
}, },

View File

@@ -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(),
}, },
}); });

View File

@@ -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,

View File

@@ -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}`);

View 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,
};