Docker hosts default to UTC, so bare toLocaleTimeString() showed wrong "Last checked" times in avstatus and other commands. Add formatDisplayTime() (default America/New_York, overridable via DISPLAY_TIMEZONE) and use it across renderers and command footers.
117 lines
No EOL
3.7 KiB
JavaScript
117 lines
No EOL
3.7 KiB
JavaScript
// utils/time.js
|
|
import { logger } from './logger.js';
|
|
|
|
/**
|
|
* IANA timezone for human-facing footer timestamps ("Last checked", etc.).
|
|
* The bot process often runs in UTC (Docker default); this keeps chat
|
|
* output in operator-local time without requiring TZ on the container.
|
|
* Override via DISPLAY_TIMEZONE in .env.
|
|
*/
|
|
export const DISPLAY_TIMEZONE = process.env.DISPLAY_TIMEZONE || 'America/New_York';
|
|
|
|
/**
|
|
* Format a Date for chat footers ("Last checked: …"). Uses DISPLAY_TIMEZONE
|
|
* and includes a short zone label (e.g. "EDT") so UTC-looking output is obvious.
|
|
*
|
|
* @param {Date} [date=new Date()]
|
|
* @returns {string} e.g. "2:13:45 PM EDT"
|
|
*/
|
|
export function formatDisplayTime(date = new Date()) {
|
|
return date.toLocaleTimeString('en-US', {
|
|
timeZone: DISPLAY_TIMEZONE,
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true,
|
|
timeZoneName: 'short',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Human-readable "X time ago" from ISO string or Date
|
|
* @param {string|Date|number} input - ISO string, Date object, or timestamp
|
|
* @returns {string} e.g. "2 hours ago", "just now", "3 days ago"
|
|
*/
|
|
export function simpleTimeAgo(input) {
|
|
if (!input) return 'never';
|
|
|
|
let date;
|
|
|
|
try {
|
|
if (input instanceof Date) {
|
|
date = input;
|
|
} else if (typeof input === 'number') {
|
|
date = new Date(input);
|
|
} else if (typeof input === 'string') {
|
|
let cleaned = input.trim();
|
|
|
|
// If no timezone (no Z or offset), assume UTC and append Z
|
|
if (!cleaned.endsWith('Z') && !cleaned.match(/[+-]\d{2}:\d{2}$/)) {
|
|
cleaned += 'Z';
|
|
}
|
|
|
|
date = new Date(cleaned);
|
|
} else {
|
|
logger('time', `Invalid input type to simpleTimeAgo: ${typeof input}`, 'warn');
|
|
return 'invalid';
|
|
}
|
|
|
|
if (isNaN(date.getTime())) {
|
|
logger('time', `Invalid date parsed from: ${input}`, 'warn');
|
|
return 'invalid';
|
|
}
|
|
|
|
const now = Date.now();
|
|
const diffMs = now - date.getTime();
|
|
|
|
// Small clock skew tolerance (< 5 minutes in the future → treat as "just now")
|
|
if (diffMs < 0 && diffMs > -300000) {
|
|
return 'just now';
|
|
}
|
|
|
|
// Future dates
|
|
if (diffMs < 0) {
|
|
const futureMs = -diffMs;
|
|
const futureSeconds = Math.floor(futureMs / 1000);
|
|
const futureMinutes = Math.floor(futureSeconds / 60);
|
|
const futureHours = Math.floor(futureMinutes / 60);
|
|
const futureDays = Math.floor(futureHours / 24);
|
|
|
|
if (futureSeconds < 60) return `in ${futureSeconds} seconds`;
|
|
if (futureMinutes < 60) return `in ${futureMinutes} minutes`;
|
|
if (futureHours < 24) return `in ${futureHours} hours`;
|
|
return `in ${futureDays} days`;
|
|
}
|
|
|
|
// Past dates
|
|
const seconds = Math.floor(diffMs / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (seconds < 60) return `${seconds} seconds ago`;
|
|
if (minutes < 60) return `${minutes} minutes ago`;
|
|
if (hours < 24) return `${hours} hours ago`;
|
|
if (days < 30) return `${days} days ago`;
|
|
|
|
// Fallback for very old dates
|
|
return date.toLocaleDateString();
|
|
|
|
} catch (err) {
|
|
logger('time', `Error in simpleTimeAgo: ${err.message}`, 'warn');
|
|
return 'invalid';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format a byte count into a human-readable string (e.g. "172 KB").
|
|
* Used for Meraki client usage in phone status (and reusable elsewhere).
|
|
*/
|
|
export function formatBytes(bytes, decimals = 1) {
|
|
if (bytes == null || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
} |