mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the timezone
This commit is contained in:
@@ -9,6 +9,8 @@ DATA_DIR=/var/lib/9router
|
|||||||
# Recommended runtime variables
|
# Recommended runtime variables
|
||||||
PORT=20128
|
PORT=20128
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
# Dashboard, logs, and daily usage reporting use Vietnam time (UTC+7).
|
||||||
|
TZ=Asia/Ho_Chi_Minh
|
||||||
|
|
||||||
# Recommended security and ops variables
|
# Recommended security and ops variables
|
||||||
API_KEY_SECRET=endpoint-proxy-api-key-secret
|
API_KEY_SECRET=endpoint-proxy-api-key-secret
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ LABEL org.opencontainers.image.title="9router"
|
|||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=20128
|
ENV PORT=20128
|
||||||
ENV HOSTNAME=0.0.0.0
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
ENV TZ=Asia/Ho_Chi_Minh
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV DATA_DIR=/app/data
|
ENV DATA_DIR=/app/data
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,18 @@ function formatDate(date) {
|
|||||||
return "Invalid Date";
|
return "Invalid Date";
|
||||||
}
|
}
|
||||||
|
|
||||||
const year = d.getFullYear();
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
timeZone: "Asia/Ho_Chi_Minh",
|
||||||
const day = String(d.getDate()).padStart(2, "0");
|
year: "numeric",
|
||||||
const hours = String(d.getHours()).padStart(2, "0");
|
month: "2-digit",
|
||||||
const minutes = String(d.getMinutes()).padStart(2, "0");
|
day: "2-digit",
|
||||||
const seconds = String(d.getSeconds()).padStart(2, "0");
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
second: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
}).formatToParts(d);
|
||||||
|
const value = Object.fromEntries(parts.filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
|
||||||
|
return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}:${value.second}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ services:
|
|||||||
PORT: "20128"
|
PORT: "20128"
|
||||||
HOSTNAME: "0.0.0.0"
|
HOSTNAME: "0.0.0.0"
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
TZ: Asia/Ho_Chi_Minh
|
||||||
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy environment variables}
|
JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy environment variables}
|
||||||
INITIAL_PASSWORD: ${INITIAL_PASSWORD:?Set INITIAL_PASSWORD in Dokploy environment variables}
|
INITIAL_PASSWORD: ${INITIAL_PASSWORD:?Set INITIAL_PASSWORD in Dokploy environment variables}
|
||||||
API_KEY_SECRET: ${API_KEY_SECRET:?Set API_KEY_SECRET in Dokploy environment variables}
|
API_KEY_SECRET: ${API_KEY_SECRET:?Set API_KEY_SECRET in Dokploy environment variables}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Application display and reporting timezone. Timestamps remain stored as UTC
|
||||||
|
// ISO strings; this value is only used when presenting or grouping instants.
|
||||||
|
export const VIETNAM_TIME_ZONE = "Asia/Ho_Chi_Minh";
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { VIETNAM_TIME_ZONE } from "../config/time.js";
|
||||||
|
|
||||||
// Debug logging utility — only active in dev mode (NODE_ENV !== "production")
|
// Debug logging utility — only active in dev mode (NODE_ENV !== "production")
|
||||||
// Outputs are tagged with [DBG:tag] for easy grep/filter
|
// Outputs are tagged with [DBG:tag] for easy grep/filter
|
||||||
const isDev = process.env.NODE_ENV !== "production";
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
|
|
||||||
function ts() {
|
function ts() {
|
||||||
return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
return new Date().toLocaleTimeString("en-US", { timeZone: VIETNAM_TIME_ZONE, hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dbg(tag, msg) {
|
export function dbg(tag, msg) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// Stream handler with disconnect detection - shared for all providers
|
// Stream handler with disconnect detection - shared for all providers
|
||||||
import { STREAM_STALL_TIMEOUT_MS } from "../config/runtimeConfig.js";
|
import { STREAM_STALL_TIMEOUT_MS } from "../config/runtimeConfig.js";
|
||||||
|
import { VIETNAM_TIME_ZONE } from "../config/time.js";
|
||||||
import { dbg, isDebugEnabled } from "./debugLog.js";
|
import { dbg, isDebugEnabled } from "./debugLog.js";
|
||||||
|
|
||||||
// Get HH:MM:SS timestamp
|
// Get HH:MM:SS timestamp
|
||||||
function getTimeString() {
|
function getTimeString() {
|
||||||
return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
return new Date().toLocaleTimeString("en-US", { timeZone: VIETNAM_TIME_ZONE, hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import EndpointRow from "./components/EndpointRow";
|
|||||||
import StatusAlert from "./components/StatusAlert";
|
import StatusAlert from "./components/StatusAlert";
|
||||||
import Tooltip from "./components/Tooltip";
|
import Tooltip from "./components/Tooltip";
|
||||||
import SecurityWarning from "./components/SecurityWarning";
|
import SecurityWarning from "./components/SecurityWarning";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
export default function APIPageClient({ machineId, isAdmin }) {
|
export default function APIPageClient({ machineId, isAdmin }) {
|
||||||
const [keys, setKeys] = useState([]);
|
const [keys, setKeys] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -1034,7 +1035,7 @@ export default function APIPageClient({ machineId, isAdmin }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden text-right text-xs text-text-muted sm:block">
|
<p className="hidden text-right text-xs text-text-muted sm:block">
|
||||||
Created<br />{new Date(key.createdAt).toLocaleDateString()}
|
Created<br />{formatVietnamDateTime(key.createdAt, { dateStyle: "medium" }) || "—"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
|
import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
|
||||||
import { useNotificationStore } from "@/store/notificationStore";
|
import { useNotificationStore } from "@/store/notificationStore";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
function getStatusVariant(status) {
|
function getStatusVariant(status) {
|
||||||
if (status === "active") return "success";
|
if (status === "active") return "success";
|
||||||
@@ -12,9 +13,7 @@ function getStatusVariant(status) {
|
|||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) return "Never";
|
if (!value) return "Never";
|
||||||
const date = new Date(value);
|
return formatVietnamDateTime(value, { dateStyle: "medium", timeStyle: "medium" }) || "Never";
|
||||||
if (Number.isNaN(date.getTime())) return "Never";
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFormData(data = {}) {
|
function normalizeFormData(data = {}) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { Card, Button } from "@/shared/components";
|
import { Card, Button } from "@/shared/components";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
const fmtTokens = (n) => {
|
const fmtTokens = (n) => {
|
||||||
if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M`;
|
if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M`;
|
||||||
@@ -222,7 +223,7 @@ export default function PxpipeClient() {
|
|||||||
{(stats?.recent || []).slice(0, 50).map((ev, i) => (
|
{(stats?.recent || []).slice(0, 50).map((ev, i) => (
|
||||||
<tr key={`${ev.ts}-${i}`} className="border-b border-border/50">
|
<tr key={`${ev.ts}-${i}`} className="border-b border-border/50">
|
||||||
<td className="py-1.5 pr-3 whitespace-nowrap text-text-muted">
|
<td className="py-1.5 pr-3 whitespace-nowrap text-text-muted">
|
||||||
{new Date(ev.ts).toLocaleString()}
|
{formatVietnamDateTime(ev.ts, { dateStyle: "medium", timeStyle: "medium" }) || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-1.5 pr-3 font-mono text-xs">{ev.provider ? `${ev.provider}/${ev.model}` : ev.model || "—"}</td>
|
<td className="py-1.5 pr-3 font-mono text-xs">{ev.provider ? `${ev.provider}/${ev.model}` : ev.model || "—"}</td>
|
||||||
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
<td className="py-1.5 pr-3 text-right font-mono text-xs">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { cn } from "@/shared/utils/cn";
|
import { cn } from "@/shared/utils/cn";
|
||||||
import { formatResetTime } from "./utils";
|
import { formatResetTime } from "./utils";
|
||||||
|
import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
// Calculate color based on remaining percentage
|
// Calculate color based on remaining percentage
|
||||||
const getColorClasses = (remainingPercentage) => {
|
const getColorClasses = (remainingPercentage) => {
|
||||||
@@ -38,25 +39,25 @@ const formatResetTimeDisplay = (resetTime) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resetDate = new Date(resetTime);
|
const resetDate = new Date(resetTime);
|
||||||
const now = new Date();
|
if (!Number.isFinite(resetDate.getTime())) return null;
|
||||||
const isToday = resetDate.toDateString() === now.toDateString();
|
const isToday = isSameVietnamDay(resetDate);
|
||||||
const isTomorrow = resetDate.toDateString() === new Date(now.getTime() + 86400000).toDateString();
|
const isTomorrow = isSameVietnamDay(resetDate, new Date(Date.now() + 86400000));
|
||||||
|
|
||||||
const timeStr = resetDate.toLocaleTimeString(undefined, {
|
const timeStr = formatVietnamTime(resetDate, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: true,
|
hourCycle: "h23",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isToday) return `Today, ${timeStr}`;
|
if (isToday) return `Today, ${timeStr}`;
|
||||||
if (isTomorrow) return `Tomorrow, ${timeStr}`;
|
if (isTomorrow) return `Tomorrow, ${timeStr}`;
|
||||||
|
|
||||||
return resetDate.toLocaleString(undefined, {
|
return formatVietnamDateTime(resetDate, {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: true,
|
hourCycle: "h23",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { formatResetTime, getRemainingPercentage } from "./utils";
|
import { formatResetTime, getRemainingPercentage } from "./utils";
|
||||||
|
import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
@@ -13,24 +14,21 @@ function formatResetTimeDisplay(resetTime) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(resetTime);
|
const date = new Date(resetTime);
|
||||||
const now = new Date();
|
if (!Number.isFinite(date.getTime())) return null;
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
||||||
const tomorrow = new Date(today);
|
|
||||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
||||||
|
|
||||||
let dayStr = "";
|
let dayStr = "";
|
||||||
if (date >= today && date < tomorrow) {
|
if (isSameVietnamDay(date)) {
|
||||||
dayStr = "Today";
|
dayStr = "Today";
|
||||||
} else if (date >= tomorrow && date < new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000)) {
|
} else if (isSameVietnamDay(date, new Date(Date.now() + 86400000))) {
|
||||||
dayStr = "Tomorrow";
|
dayStr = "Tomorrow";
|
||||||
} else {
|
} else {
|
||||||
dayStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
dayStr = formatVietnamDateTime(date, { month: "short", day: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeStr = date.toLocaleTimeString("en-US", {
|
const timeStr = formatVietnamTime(date, {
|
||||||
hour: "numeric",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: true,
|
hourCycle: "h23",
|
||||||
});
|
});
|
||||||
|
|
||||||
return `${dayStr}, ${timeStr}`;
|
return `${dayStr}, ${timeStr}`;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import Card from "@/shared/components/Card";
|
|||||||
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
|
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
|
||||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
// Maps the stored providerSpecificData.authMethod to a human label for Kiro.
|
// Maps the stored providerSpecificData.authMethod to a human label for Kiro.
|
||||||
// Values come from the Kiro connect flows: builder-id/idc (device code),
|
// Values come from the Kiro connect flows: builder-id/idc (device code),
|
||||||
@@ -99,15 +100,13 @@ function getCodexResetCreditCount(quota) {
|
|||||||
|
|
||||||
function formatCreditDate(value) {
|
function formatCreditDate(value) {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
const date = new Date(value);
|
return formatVietnamDateTime(value, {
|
||||||
if (!Number.isFinite(date.getTime())) return "N/A";
|
|
||||||
return date.toLocaleString(undefined, {
|
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
}) || "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeRemaining(value) {
|
function formatTimeRemaining(value) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Drawer from "@/shared/components/Drawer";
|
|||||||
import Pagination from "@/shared/components/Pagination";
|
import Pagination from "@/shared/components/Pagination";
|
||||||
import { cn } from "@/shared/utils/cn";
|
import { cn } from "@/shared/utils/cn";
|
||||||
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
|
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
let providerNameCache = null;
|
let providerNameCache = null;
|
||||||
let providerNodesCache = null;
|
let providerNodesCache = null;
|
||||||
@@ -286,7 +287,7 @@ export default function RequestDetailsTab() {
|
|||||||
className="border-b border-black/5 dark:border-white/5 last:border-b-0 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors"
|
className="border-b border-black/5 dark:border-white/5 last:border-b-0 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors"
|
||||||
>
|
>
|
||||||
<td className="whitespace-nowrap p-4 text-sm text-text-main">
|
<td className="whitespace-nowrap p-4 text-sm text-text-main">
|
||||||
{new Date(detail.timestamp).toLocaleString()}
|
{formatVietnamDateTime(detail.timestamp, { dateStyle: "medium", timeStyle: "medium" }) || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-w-[260px] truncate p-4 font-mono text-sm text-text-main">
|
<td className="max-w-[260px] truncate p-4 font-mono text-sm text-text-main">
|
||||||
{detail.model}
|
{detail.model}
|
||||||
@@ -358,7 +359,7 @@ export default function RequestDetailsTab() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-muted">Timestamp:</span>{" "}
|
<span className="text-text-muted">Timestamp:</span>{" "}
|
||||||
<span className="text-text-main">{new Date(selectedDetail.timestamp).toLocaleString()}</span>
|
<span className="text-text-main">{formatVietnamDateTime(selectedDetail.timestamp, { dateStyle: "medium", timeStyle: "medium" }) || "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-muted">Provider:</span>{" "}
|
<span className="text-text-muted">Provider:</span>{" "}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AI_PROVIDERS } from "@/shared/constants/providers";
|
|||||||
import { Button, Card, CardSkeleton } from "@/shared/components";
|
import { Button, Card, CardSkeleton } from "@/shared/components";
|
||||||
import ProviderIcon from "@/shared/components/ProviderIcon";
|
import ProviderIcon from "@/shared/components/ProviderIcon";
|
||||||
import { formatResetTime, REFRESH_INTERVAL_MS } from "./ProviderLimits/utils";
|
import { formatResetTime, REFRESH_INTERVAL_MS } from "./ProviderLimits/utils";
|
||||||
|
import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
function getProviderInfo(providerId) {
|
function getProviderInfo(providerId) {
|
||||||
return AI_PROVIDERS[providerId] || {
|
return AI_PROVIDERS[providerId] || {
|
||||||
@@ -16,13 +17,11 @@ function getProviderInfo(providerId) {
|
|||||||
function formatUpdatedAt(value) {
|
function formatUpdatedAt(value) {
|
||||||
if (!value) return "Not updated yet";
|
if (!value) return "Not updated yet";
|
||||||
|
|
||||||
const date = new Date(value);
|
const formattedTime = formatVietnamTime(value, {
|
||||||
if (!Number.isFinite(date.getTime())) return "Not updated yet";
|
|
||||||
|
|
||||||
return `Updated ${date.toLocaleTimeString(undefined, {
|
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})}`;
|
});
|
||||||
|
return formattedTime ? `Updated ${formattedTime}` : "Not updated yet";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatResetAt(value) {
|
function formatResetAt(value) {
|
||||||
@@ -31,14 +30,13 @@ function formatResetAt(value) {
|
|||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (!Number.isFinite(date.getTime())) return null;
|
if (!Number.isFinite(date.getTime())) return null;
|
||||||
|
|
||||||
const now = new Date();
|
const isToday = isSameVietnamDay(date);
|
||||||
const isToday = date.toDateString() === now.toDateString();
|
const isTomorrow = isSameVietnamDay(date, new Date(Date.now() + 86400000));
|
||||||
const isTomorrow = date.toDateString() === new Date(now.getTime() + 86400000).toDateString();
|
const time = formatVietnamTime(date, { hour: "2-digit", minute: "2-digit" });
|
||||||
const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
||||||
|
|
||||||
if (isToday) return `today at ${time}`;
|
if (isToday) return `today at ${time}`;
|
||||||
if (isTomorrow) return `tomorrow at ${time}`;
|
if (isTomorrow) return `tomorrow at ${time}`;
|
||||||
return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
return formatVietnamDateTime(date, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuotaTone(percentage) {
|
function getQuotaTone(percentage) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useMemo, Fragment } from "react";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import Card from "@/shared/components/Card";
|
import Card from "@/shared/components/Card";
|
||||||
import Badge from "@/shared/components/Badge";
|
import Badge from "@/shared/components/Badge";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
const fmt = (n) => new Intl.NumberFormat().format(n || 0);
|
const fmt = (n) => new Intl.NumberFormat().format(n || 0);
|
||||||
const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
|
const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
|
||||||
@@ -14,7 +15,7 @@ function fmtTime(iso) {
|
|||||||
if (diffMins < 1) return "Just now";
|
if (diffMins < 1) return "Just now";
|
||||||
if (diffMins < 60) return `${diffMins}m ago`;
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`;
|
if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`;
|
||||||
return new Date(iso).toLocaleDateString();
|
return formatVietnamDateTime(iso, { dateStyle: "medium" }) || "Never";
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortIcon({ field, currentSort, currentOrder }) {
|
function SortIcon({ field, currentSort, currentOrder }) {
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Button, Card, Input } from "@/shared/components";
|
import { Button, Card, Input } from "@/shared/components";
|
||||||
import Modal, { ConfirmModal } from "@/shared/components/Modal";
|
import Modal, { ConfirmModal } from "@/shared/components/Modal";
|
||||||
import useUserStore from "@/store/userStore";
|
import useUserStore from "@/store/userStore";
|
||||||
|
import { formatVietnamDateTime } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
const EMPTY_FORM = { username: "", password: "", role: "user", isActive: true };
|
const EMPTY_FORM = { username: "", password: "", role: "user", isActive: true };
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
return formatVietnamDateTime(value, { dateStyle: "medium", timeStyle: "short" }) || "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { getAdapter } from "../driver.js";
|
|||||||
import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
|
import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
|
||||||
import { getMeta, setMeta } from "../helpers/metaStore.js";
|
import { getMeta, setMeta } from "../helpers/metaStore.js";
|
||||||
import { appendUsageAccessClause, getUsageAccessScope } from "./usageAccessScope.js";
|
import { appendUsageAccessClause, getUsageAccessScope } from "./usageAccessScope.js";
|
||||||
|
import {
|
||||||
|
formatVietnamDateTime,
|
||||||
|
formatVietnamTime,
|
||||||
|
getVietnamDateKey,
|
||||||
|
getVietnamStartOfDay,
|
||||||
|
shiftVietnamDateKey,
|
||||||
|
} from "../../../shared/utils/dateTime.js";
|
||||||
|
|
||||||
function maskApiKey(key) {
|
function maskApiKey(key) {
|
||||||
if (!key || typeof key !== "string") return null;
|
if (!key || typeof key !== "string") return null;
|
||||||
@@ -47,8 +54,7 @@ function scheduleStatsEvent(event, delayMs = 150) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLocalDateKey(timestamp) {
|
function getLocalDateKey(timestamp) {
|
||||||
const d = timestamp ? new Date(timestamp) : new Date();
|
return getVietnamDateKey(timestamp || new Date());
|
||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCounter(target, key, values) {
|
function addToCounter(target, key, values) {
|
||||||
@@ -361,9 +367,7 @@ function loadDaysInRange(adapter, maxDays) {
|
|||||||
if (maxDays == null) {
|
if (maxDays == null) {
|
||||||
return adapter.all(`SELECT dateKey, data FROM usageDaily`);
|
return adapter.all(`SELECT dateKey, data FROM usageDaily`);
|
||||||
}
|
}
|
||||||
const today = new Date();
|
const cutoffKey = shiftVietnamDateKey(getVietnamDateKey(), -(maxDays - 1));
|
||||||
const cutoff = new Date(today.getFullYear(), today.getMonth(), today.getDate() - maxDays + 1);
|
|
||||||
const cutoffKey = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
|
|
||||||
return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ?`, [cutoffKey]);
|
return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ?`, [cutoffKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,17 +453,12 @@ function loadUserBreakdownRows(db, period) {
|
|||||||
|
|
||||||
function getUsagePeriodCutoff(period) {
|
function getUsagePeriodCutoff(period) {
|
||||||
if (period === "today") {
|
if (period === "today") {
|
||||||
const startOfDay = new Date();
|
return getVietnamStartOfDay().toISOString();
|
||||||
startOfDay.setHours(0, 0, 0, 0);
|
|
||||||
return startOfDay.toISOString();
|
|
||||||
}
|
}
|
||||||
if (period === "24h") return new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
|
if (period === "24h") return new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
|
||||||
const days = { "7d": 7, "30d": 30, "60d": 60 }[period];
|
const days = { "7d": 7, "30d": 30, "60d": 60 }[period];
|
||||||
if (!days) return null;
|
if (!days) return null;
|
||||||
const startOfRange = new Date();
|
return new Date(`${shiftVietnamDateKey(getVietnamDateKey(), -(days - 1))}T00:00:00+07:00`).toISOString();
|
||||||
startOfRange.setHours(0, 0, 0, 0);
|
|
||||||
startOfRange.setDate(startOfRange.getDate() - days + 1);
|
|
||||||
return startOfRange.toISOString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getScopedUsageStats(period, user, scope) {
|
async function getScopedUsageStats(period, user, scope) {
|
||||||
@@ -796,9 +795,7 @@ export async function getUsageStats(period = "all", user = null) {
|
|||||||
// 24h / today: live history
|
// 24h / today: live history
|
||||||
let cutoff;
|
let cutoff;
|
||||||
if (period === "today") {
|
if (period === "today") {
|
||||||
const startOfDay = new Date();
|
cutoff = getVietnamStartOfDay().toISOString();
|
||||||
startOfDay.setHours(0, 0, 0, 0);
|
|
||||||
cutoff = startOfDay.toISOString();
|
|
||||||
} else {
|
} else {
|
||||||
cutoff = new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
|
cutoff = new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
|
||||||
}
|
}
|
||||||
@@ -896,11 +893,11 @@ function buildChartDataFromRows(rows, period) {
|
|||||||
const bucketCount = isHourly ? 24 : period === "7d" ? 7 : period === "30d" ? 30 : 60;
|
const bucketCount = isHourly ? 24 : period === "7d" ? 7 : period === "30d" ? 30 : 60;
|
||||||
const bucketMs = isHourly ? 3600000 : 86400000;
|
const bucketMs = isHourly ? 3600000 : 86400000;
|
||||||
const startTime = period === "today"
|
const startTime = period === "today"
|
||||||
? new Date(new Date().setHours(0, 0, 0, 0)).getTime()
|
? getVietnamStartOfDay().getTime()
|
||||||
: isHourly ? now - bucketCount * bucketMs : new Date(new Date().setHours(0, 0, 0, 0) - (bucketCount - 1) * bucketMs).getTime();
|
: isHourly ? now - bucketCount * bucketMs : new Date(`${shiftVietnamDateKey(getVietnamDateKey(), -(bucketCount - 1))}T00:00:00+07:00`).getTime();
|
||||||
const labelFn = isHourly
|
const labelFn = isHourly
|
||||||
? (timestamp) => new Date(timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false })
|
? (timestamp) => formatVietnamTime(timestamp, { hour: "2-digit", minute: "2-digit" })
|
||||||
: (timestamp) => new Date(timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
: (timestamp) => formatVietnamDateTime(timestamp, { month: "short", day: "numeric" });
|
||||||
const buckets = Array.from({ length: bucketCount }, (_, index) => ({ label: labelFn(startTime + index * bucketMs), tokens: 0, cost: 0 }));
|
const buckets = Array.from({ length: bucketCount }, (_, index) => ({ label: labelFn(startTime + index * bucketMs), tokens: 0, cost: 0 }));
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -936,11 +933,9 @@ export async function getChartData(period = "7d", user = null) {
|
|||||||
if (period === "today") {
|
if (period === "today") {
|
||||||
const bucketCount = 24;
|
const bucketCount = 24;
|
||||||
const bucketMs = 3600000;
|
const bucketMs = 3600000;
|
||||||
const startOfDay = new Date();
|
const startTime = getVietnamStartOfDay().getTime();
|
||||||
startOfDay.setHours(0, 0, 0, 0);
|
|
||||||
const startTime = startOfDay.getTime();
|
|
||||||
const endTime = startTime + bucketCount * bucketMs;
|
const endTime = startTime + bucketCount * bucketMs;
|
||||||
const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
const labelFn = (ts) => formatVietnamTime(ts, { hour: "2-digit", minute: "2-digit" });
|
||||||
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 }));
|
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 }));
|
||||||
|
|
||||||
const rows = db.all(
|
const rows = db.all(
|
||||||
@@ -962,7 +957,7 @@ export async function getChartData(period = "7d", user = null) {
|
|||||||
if (period === "24h") {
|
if (period === "24h") {
|
||||||
const bucketCount = 24;
|
const bucketCount = 24;
|
||||||
const bucketMs = 3600000;
|
const bucketMs = 3600000;
|
||||||
const labelFn = (ts) => new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
const labelFn = (ts) => formatVietnamTime(ts, { hour: "2-digit", minute: "2-digit" });
|
||||||
const startTime = now - bucketCount * bucketMs;
|
const startTime = now - bucketCount * bucketMs;
|
||||||
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 }));
|
const buckets = Array.from({ length: bucketCount }, (_, i) => ({ label: labelFn(startTime + i * bucketMs), tokens: 0, cost: 0 }));
|
||||||
|
|
||||||
@@ -981,8 +976,8 @@ export async function getChartData(period = "7d", user = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bucketCount = period === "7d" ? 7 : period === "30d" ? 30 : 60;
|
const bucketCount = period === "7d" ? 7 : period === "30d" ? 30 : 60;
|
||||||
const today = new Date();
|
const todayKey = getVietnamDateKey();
|
||||||
const labelFn = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
const labelFn = (dateKey) => formatVietnamDateTime(`${dateKey}T00:00:00+07:00`, { month: "short", day: "numeric" });
|
||||||
|
|
||||||
// Build map of dateKey → day data
|
// Build map of dateKey → day data
|
||||||
const dayRows = loadDaysInRange(db, bucketCount);
|
const dayRows = loadDaysInRange(db, bucketCount);
|
||||||
@@ -990,12 +985,10 @@ export async function getChartData(period = "7d", user = null) {
|
|||||||
for (const r of dayRows) dayMap[r.dateKey] = parseJson(r.data, {});
|
for (const r of dayRows) dayMap[r.dateKey] = parseJson(r.data, {});
|
||||||
|
|
||||||
return Array.from({ length: bucketCount }, (_, i) => {
|
return Array.from({ length: bucketCount }, (_, i) => {
|
||||||
const d = new Date(today);
|
const dateKey = shiftVietnamDateKey(todayKey, -(bucketCount - 1 - i));
|
||||||
d.setDate(d.getDate() - (bucketCount - 1 - i));
|
|
||||||
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
||||||
const dayData = dayMap[dateKey];
|
const dayData = dayMap[dateKey];
|
||||||
return {
|
return {
|
||||||
label: labelFn(d),
|
label: labelFn(dateKey),
|
||||||
tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 0) : 0,
|
tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 0) : 0,
|
||||||
cost: dayData ? (dayData.cost || 0) : 0,
|
cost: dayData ? (dayData.cost || 0) : 0,
|
||||||
};
|
};
|
||||||
@@ -1003,8 +996,15 @@ export async function getChartData(period = "7d", user = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatLogDate(date = new Date()) {
|
function formatLogDate(date = new Date()) {
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
return formatVietnamDateTime(date, {
|
||||||
return `${pad(date.getDate())}-${pad(date.getMonth() + 1)}-${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// No-op: request log is now derived from usageHistory table on read.
|
// No-op: request log is now derived from usageHistory table on read.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { PXPIPE_DIR } from "./install.js";
|
import { PXPIPE_DIR } from "./install.js";
|
||||||
|
import { getVietnamDateKey, getVietnamStartOfDay, shiftVietnamDateKey } from "@/shared/utils/dateTime";
|
||||||
|
|
||||||
const EVENTS_FILE = path.join(PXPIPE_DIR, "events.jsonl");
|
const EVENTS_FILE = path.join(PXPIPE_DIR, "events.jsonl");
|
||||||
const ROTATED_FILE = path.join(PXPIPE_DIR, "events.jsonl.1");
|
const ROTATED_FILE = path.join(PXPIPE_DIR, "events.jsonl.1");
|
||||||
@@ -81,7 +82,7 @@ function finalize(totals) {
|
|||||||
export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
|
export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
|
||||||
const events = readPxpipeEvents();
|
const events = readPxpipeEvents();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const startOfToday = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
|
const startOfToday = getVietnamStartOfDay(now).getTime();
|
||||||
|
|
||||||
const windows = {
|
const windows = {
|
||||||
all: emptyTotals(),
|
all: emptyTotals(),
|
||||||
@@ -92,9 +93,10 @@ export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const timeline = new Map();
|
const timeline = new Map();
|
||||||
|
const todayKey = getVietnamDateKey(now);
|
||||||
for (let i = timelineDays - 1; i >= 0; i--) {
|
for (let i = timelineDays - 1; i >= 0; i--) {
|
||||||
const day = new Date(startOfToday - i * DAY_MS);
|
const dateKey = shiftVietnamDateKey(todayKey, -i);
|
||||||
timeline.set(day.toISOString().slice(0, 10), { date: day.toISOString().slice(0, 10), tokensSavedEst: 0, compressed: 0, requests: 0 });
|
timeline.set(dateKey, { date: dateKey, tokensSavedEst: 0, compressed: 0, requests: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
@@ -104,7 +106,7 @@ export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
|
|||||||
if (ev.ts >= now - 7 * DAY_MS) accumulate(windows.last7d, ev);
|
if (ev.ts >= now - 7 * DAY_MS) accumulate(windows.last7d, ev);
|
||||||
if (ev.ts >= now - 30 * DAY_MS) accumulate(windows.last30d, ev);
|
if (ev.ts >= now - 30 * DAY_MS) accumulate(windows.last30d, ev);
|
||||||
|
|
||||||
const key = new Date(ev.ts).toISOString().slice(0, 10);
|
const key = getVietnamDateKey(ev.ts);
|
||||||
const bucket = timeline.get(key);
|
const bucket = timeline.get(key);
|
||||||
if (bucket) {
|
if (bucket) {
|
||||||
bucket.requests++;
|
bucket.requests++;
|
||||||
|
|||||||
+3
-1
@@ -4,8 +4,10 @@ const zlib = require("zlib");
|
|||||||
const { DATA_DIR } = require("./paths");
|
const { DATA_DIR } = require("./paths");
|
||||||
const { LOG_BLACKLIST_URL_PARTS } = require("./config");
|
const { LOG_BLACKLIST_URL_PARTS } = require("./config");
|
||||||
|
|
||||||
|
const VIETNAM_TIME_ZONE = "Asia/Ho_Chi_Minh";
|
||||||
|
|
||||||
function time() {
|
function time() {
|
||||||
return new Date().toLocaleTimeString("en-US", { hour12: false });
|
return new Date().toLocaleTimeString("en-US", { timeZone: VIETNAM_TIME_ZONE, hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const log = (msg) => console.log(`[${time()}] [MITM] ${msg}`);
|
const log = (msg) => console.log(`[${time()}] [MITM] ${msg}`);
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { VIETNAM_TIME_ZONE } from "../../../open-sse/config/time.js";
|
||||||
|
|
||||||
|
export { VIETNAM_TIME_ZONE };
|
||||||
|
|
||||||
|
export const VIETNAM_LOCALE = "vi-VN";
|
||||||
|
|
||||||
|
function toValidDate(value) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
return Number.isFinite(date.getTime()) ? date : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPartMap(value) {
|
||||||
|
const date = toValidDate(value);
|
||||||
|
if (!date) return null;
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: VIETNAM_TIME_ZONE,
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).formatToParts(date);
|
||||||
|
|
||||||
|
return Object.fromEntries(parts.filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVietnamDateKey(value = new Date()) {
|
||||||
|
const parts = getPartMap(value);
|
||||||
|
return parts ? `${parts.year}-${parts.month}-${parts.day}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVietnamStartOfDay(value = new Date()) {
|
||||||
|
const dateKey = getVietnamDateKey(value);
|
||||||
|
return dateKey ? new Date(`${dateKey}T00:00:00+07:00`) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftVietnamDateKey(dateKey, days) {
|
||||||
|
const date = new Date(`${dateKey}T00:00:00+07:00`);
|
||||||
|
if (!Number.isFinite(date.getTime())) return null;
|
||||||
|
date.setUTCDate(date.getUTCDate() + days);
|
||||||
|
return getVietnamDateKey(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSameVietnamDay(first, second = new Date()) {
|
||||||
|
const firstKey = getVietnamDateKey(first);
|
||||||
|
return firstKey !== null && firstKey === getVietnamDateKey(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatVietnamDateTime(value, options = {}) {
|
||||||
|
const date = toValidDate(value);
|
||||||
|
if (!date) return null;
|
||||||
|
return new Intl.DateTimeFormat(VIETNAM_LOCALE, { timeZone: VIETNAM_TIME_ZONE, ...options }).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatVietnamTime(value, options = {}) {
|
||||||
|
return formatVietnamDateTime(value, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { VIETNAM_TIME_ZONE } from "open-sse/config/time.js";
|
||||||
|
|
||||||
// Logger utility for cloud
|
// Logger utility for cloud
|
||||||
|
|
||||||
const LOG_LEVELS = {
|
const LOG_LEVELS = {
|
||||||
@@ -10,7 +12,7 @@ const LOG_LEVELS = {
|
|||||||
const LEVEL = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase?.()] ?? LOG_LEVELS.INFO;
|
const LEVEL = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase?.()] ?? LOG_LEVELS.INFO;
|
||||||
|
|
||||||
function formatTime() {
|
function formatTime() {
|
||||||
return new Date().toLocaleTimeString("en-US", { hour12: false });
|
return new Date().toLocaleTimeString("en-US", { timeZone: VIETNAM_TIME_ZONE, hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colored-dot tags to correlate request lines by session (same session → same color)
|
// Colored-dot tags to correlate request lines by session (same session → same color)
|
||||||
|
|||||||
Reference in New Issue
Block a user