|
- {new Date(ev.ts).toLocaleString()}
+ {formatVietnamDateTime(ev.ts, { dateStyle: "medium", timeStyle: "medium" }) || "—"}
|
{ev.provider ? `${ev.provider}/${ev.model}` : ev.model || "—"} |
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js
index 0078ccf3..e60638ee 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js
@@ -2,6 +2,7 @@
import { cn } from "@/shared/utils/cn";
import { formatResetTime } from "./utils";
+import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
// Calculate color based on remaining percentage
const getColorClasses = (remainingPercentage) => {
@@ -38,25 +39,25 @@ const formatResetTimeDisplay = (resetTime) => {
try {
const resetDate = new Date(resetTime);
- const now = new Date();
- const isToday = resetDate.toDateString() === now.toDateString();
- const isTomorrow = resetDate.toDateString() === new Date(now.getTime() + 86400000).toDateString();
+ if (!Number.isFinite(resetDate.getTime())) return null;
+ const isToday = isSameVietnamDay(resetDate);
+ const isTomorrow = isSameVietnamDay(resetDate, new Date(Date.now() + 86400000));
- const timeStr = resetDate.toLocaleTimeString(undefined, {
+ const timeStr = formatVietnamTime(resetDate, {
hour: "2-digit",
minute: "2-digit",
- hour12: true,
+ hourCycle: "h23",
});
if (isToday) return `Today, ${timeStr}`;
if (isTomorrow) return `Tomorrow, ${timeStr}`;
- return resetDate.toLocaleString(undefined, {
+ return formatVietnamDateTime(resetDate, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
- hour12: true,
+ hourCycle: "h23",
});
} catch {
return null;
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
index 8f2a1bc3..cbbd6ce1 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js
@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from "react";
import { formatResetTime, getRemainingPercentage } from "./utils";
+import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
const PAGE_SIZE = 10;
@@ -13,24 +14,21 @@ function formatResetTimeDisplay(resetTime) {
try {
const date = new Date(resetTime);
- const now = new Date();
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
- const tomorrow = new Date(today);
- tomorrow.setDate(tomorrow.getDate() + 1);
+ if (!Number.isFinite(date.getTime())) return null;
let dayStr = "";
- if (date >= today && date < tomorrow) {
+ if (isSameVietnamDay(date)) {
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";
} else {
- dayStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+ dayStr = formatVietnamDateTime(date, { month: "short", day: "numeric" });
}
- const timeStr = date.toLocaleTimeString("en-US", {
- hour: "numeric",
+ const timeStr = formatVietnamTime(date, {
+ hour: "2-digit",
minute: "2-digit",
- hour12: true,
+ hourCycle: "h23",
});
return `${dayStr}, ${timeStr}`;
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
index 95300c92..ee0a03cb 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
@@ -39,6 +39,7 @@ import Card from "@/shared/components/Card";
import { ConfirmModal, EditConnectionModal } from "@/shared/components";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+import { formatVietnamDateTime } from "@/shared/utils/dateTime";
// Maps the stored providerSpecificData.authMethod to a human label for Kiro.
// Values come from the Kiro connect flows: builder-id/idc (device code),
@@ -99,15 +100,13 @@ function getCodexResetCreditCount(quota) {
function formatCreditDate(value) {
if (!value) return "N/A";
- const date = new Date(value);
- if (!Number.isFinite(date.getTime())) return "N/A";
- return date.toLocaleString(undefined, {
+ return formatVietnamDateTime(value, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
- });
+ }) || "N/A";
}
function formatTimeRemaining(value) {
diff --git a/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js b/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js
index af8eb736..87bf05ab 100644
--- a/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js
+++ b/src/app/(dashboard)/dashboard/usage/components/RequestDetailsTab.js
@@ -7,6 +7,7 @@ import Drawer from "@/shared/components/Drawer";
import Pagination from "@/shared/components/Pagination";
import { cn } from "@/shared/utils/cn";
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
+import { formatVietnamDateTime } from "@/shared/utils/dateTime";
let providerNameCache = 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"
>
|
- {new Date(detail.timestamp).toLocaleString()}
+ {formatVietnamDateTime(detail.timestamp, { dateStyle: "medium", timeStyle: "medium" }) || "—"}
|
{detail.model}
@@ -358,7 +359,7 @@ export default function RequestDetailsTab() {
Timestamp:{" "}
- {new Date(selectedDetail.timestamp).toLocaleString()}
+ {formatVietnamDateTime(selectedDetail.timestamp, { dateStyle: "medium", timeStyle: "medium" }) || "—"}
Provider:{" "}
diff --git a/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js b/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js
index c541ff0c..c0b9b9ea 100644
--- a/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js
+++ b/src/app/(dashboard)/dashboard/usage/components/SystemQuotaOverview.js
@@ -5,6 +5,7 @@ import { AI_PROVIDERS } from "@/shared/constants/providers";
import { Button, Card, CardSkeleton } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { formatResetTime, REFRESH_INTERVAL_MS } from "./ProviderLimits/utils";
+import { formatVietnamDateTime, formatVietnamTime, isSameVietnamDay } from "@/shared/utils/dateTime";
function getProviderInfo(providerId) {
return AI_PROVIDERS[providerId] || {
@@ -16,13 +17,11 @@ function getProviderInfo(providerId) {
function formatUpdatedAt(value) {
if (!value) return "Not updated yet";
- const date = new Date(value);
- if (!Number.isFinite(date.getTime())) return "Not updated yet";
-
- return `Updated ${date.toLocaleTimeString(undefined, {
+ const formattedTime = formatVietnamTime(value, {
hour: "2-digit",
minute: "2-digit",
- })}`;
+ });
+ return formattedTime ? `Updated ${formattedTime}` : "Not updated yet";
}
function formatResetAt(value) {
@@ -31,14 +30,13 @@ function formatResetAt(value) {
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return null;
- const now = new Date();
- const isToday = date.toDateString() === now.toDateString();
- const isTomorrow = date.toDateString() === new Date(now.getTime() + 86400000).toDateString();
- const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
+ const isToday = isSameVietnamDay(date);
+ const isTomorrow = isSameVietnamDay(date, new Date(Date.now() + 86400000));
+ const time = formatVietnamTime(date, { hour: "2-digit", minute: "2-digit" });
if (isToday) return `today 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) {
diff --git a/src/app/(dashboard)/dashboard/usage/components/UsageTable.js b/src/app/(dashboard)/dashboard/usage/components/UsageTable.js
index 9da3c3e6..d7e9e31e 100644
--- a/src/app/(dashboard)/dashboard/usage/components/UsageTable.js
+++ b/src/app/(dashboard)/dashboard/usage/components/UsageTable.js
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useMemo, Fragment } from "react";
import PropTypes from "prop-types";
import Card from "@/shared/components/Card";
import Badge from "@/shared/components/Badge";
+import { formatVietnamDateTime } from "@/shared/utils/dateTime";
const fmt = (n) => new Intl.NumberFormat().format(n || 0);
const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
@@ -14,7 +15,7 @@ function fmtTime(iso) {
if (diffMins < 1) return "Just now";
if (diffMins < 60) return `${diffMins}m 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 }) {
diff --git a/src/app/(dashboard)/dashboard/users/page.js b/src/app/(dashboard)/dashboard/users/page.js
index 341ded68..da038cdc 100644
--- a/src/app/(dashboard)/dashboard/users/page.js
+++ b/src/app/(dashboard)/dashboard/users/page.js
@@ -5,12 +5,13 @@ import { useRouter } from "next/navigation";
import { Button, Card, Input } from "@/shared/components";
import Modal, { ConfirmModal } from "@/shared/components/Modal";
import useUserStore from "@/store/userStore";
+import { formatVietnamDateTime } from "@/shared/utils/dateTime";
const EMPTY_FORM = { username: "", password: "", role: "user", isActive: true };
function formatDate(value) {
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() {
diff --git a/src/lib/db/repos/usageRepo.js b/src/lib/db/repos/usageRepo.js
index 51980e80..fe5bbb51 100644
--- a/src/lib/db/repos/usageRepo.js
+++ b/src/lib/db/repos/usageRepo.js
@@ -3,6 +3,13 @@ import { getAdapter } from "../driver.js";
import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
import { getMeta, setMeta } from "../helpers/metaStore.js";
import { appendUsageAccessClause, getUsageAccessScope } from "./usageAccessScope.js";
+import {
+ formatVietnamDateTime,
+ formatVietnamTime,
+ getVietnamDateKey,
+ getVietnamStartOfDay,
+ shiftVietnamDateKey,
+} from "../../../shared/utils/dateTime.js";
function maskApiKey(key) {
if (!key || typeof key !== "string") return null;
@@ -47,8 +54,7 @@ function scheduleStatsEvent(event, delayMs = 150) {
}
function getLocalDateKey(timestamp) {
- const d = timestamp ? new Date(timestamp) : new Date();
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
+ return getVietnamDateKey(timestamp || new Date());
}
function addToCounter(target, key, values) {
@@ -361,9 +367,7 @@ function loadDaysInRange(adapter, maxDays) {
if (maxDays == null) {
return adapter.all(`SELECT dateKey, data FROM usageDaily`);
}
- const today = new Date();
- 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")}`;
+ const cutoffKey = shiftVietnamDateKey(getVietnamDateKey(), -(maxDays - 1));
return adapter.all(`SELECT dateKey, data FROM usageDaily WHERE dateKey >= ?`, [cutoffKey]);
}
@@ -449,17 +453,12 @@ function loadUserBreakdownRows(db, period) {
function getUsagePeriodCutoff(period) {
if (period === "today") {
- const startOfDay = new Date();
- startOfDay.setHours(0, 0, 0, 0);
- return startOfDay.toISOString();
+ return getVietnamStartOfDay().toISOString();
}
if (period === "24h") return new Date(Date.now() - PERIOD_MS["24h"]).toISOString();
const days = { "7d": 7, "30d": 30, "60d": 60 }[period];
if (!days) return null;
- const startOfRange = new Date();
- startOfRange.setHours(0, 0, 0, 0);
- startOfRange.setDate(startOfRange.getDate() - days + 1);
- return startOfRange.toISOString();
+ return new Date(`${shiftVietnamDateKey(getVietnamDateKey(), -(days - 1))}T00:00:00+07:00`).toISOString();
}
async function getScopedUsageStats(period, user, scope) {
@@ -796,9 +795,7 @@ export async function getUsageStats(period = "all", user = null) {
// 24h / today: live history
let cutoff;
if (period === "today") {
- const startOfDay = new Date();
- startOfDay.setHours(0, 0, 0, 0);
- cutoff = startOfDay.toISOString();
+ cutoff = getVietnamStartOfDay().toISOString();
} else {
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 bucketMs = isHourly ? 3600000 : 86400000;
const startTime = period === "today"
- ? new Date(new Date().setHours(0, 0, 0, 0)).getTime()
- : isHourly ? now - bucketCount * bucketMs : new Date(new Date().setHours(0, 0, 0, 0) - (bucketCount - 1) * bucketMs).getTime();
+ ? getVietnamStartOfDay().getTime()
+ : isHourly ? now - bucketCount * bucketMs : new Date(`${shiftVietnamDateKey(getVietnamDateKey(), -(bucketCount - 1))}T00:00:00+07:00`).getTime();
const labelFn = isHourly
- ? (timestamp) => new Date(timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false })
- : (timestamp) => new Date(timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric" });
+ ? (timestamp) => formatVietnamTime(timestamp, { hour: "2-digit", minute: "2-digit" })
+ : (timestamp) => formatVietnamDateTime(timestamp, { month: "short", day: "numeric" });
const buckets = Array.from({ length: bucketCount }, (_, index) => ({ label: labelFn(startTime + index * bucketMs), tokens: 0, cost: 0 }));
for (const row of rows) {
@@ -936,11 +933,9 @@ export async function getChartData(period = "7d", user = null) {
if (period === "today") {
const bucketCount = 24;
const bucketMs = 3600000;
- const startOfDay = new Date();
- startOfDay.setHours(0, 0, 0, 0);
- const startTime = startOfDay.getTime();
+ const startTime = getVietnamStartOfDay().getTime();
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 rows = db.all(
@@ -962,7 +957,7 @@ export async function getChartData(period = "7d", user = null) {
if (period === "24h") {
const bucketCount = 24;
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 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 today = new Date();
- const labelFn = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+ const todayKey = getVietnamDateKey();
+ const labelFn = (dateKey) => formatVietnamDateTime(`${dateKey}T00:00:00+07:00`, { month: "short", day: "numeric" });
// Build map of dateKey → day data
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, {});
return Array.from({ length: bucketCount }, (_, i) => {
- const d = new Date(today);
- 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 dateKey = shiftVietnamDateKey(todayKey, -(bucketCount - 1 - i));
const dayData = dayMap[dateKey];
return {
- label: labelFn(d),
+ label: labelFn(dateKey),
tokens: dayData ? (dayData.promptTokens || 0) + (dayData.completionTokens || 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()) {
- const pad = (n) => String(n).padStart(2, "0");
- return `${pad(date.getDate())}-${pad(date.getMonth() + 1)}-${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
+ return formatVietnamDateTime(date, {
+ 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.
diff --git a/src/lib/pxpipe/events.js b/src/lib/pxpipe/events.js
index b1bb880d..16a8b13c 100644
--- a/src/lib/pxpipe/events.js
+++ b/src/lib/pxpipe/events.js
@@ -1,6 +1,7 @@
import fs from "fs";
import path from "path";
import { PXPIPE_DIR } from "./install.js";
+import { getVietnamDateKey, getVietnamStartOfDay, shiftVietnamDateKey } from "@/shared/utils/dateTime";
const EVENTS_FILE = path.join(PXPIPE_DIR, "events.jsonl");
const ROTATED_FILE = path.join(PXPIPE_DIR, "events.jsonl.1");
@@ -81,7 +82,7 @@ function finalize(totals) {
export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
const events = readPxpipeEvents();
const now = Date.now();
- const startOfToday = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
+ const startOfToday = getVietnamStartOfDay(now).getTime();
const windows = {
all: emptyTotals(),
@@ -92,9 +93,10 @@ export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
};
const timeline = new Map();
+ const todayKey = getVietnamDateKey(now);
for (let i = timelineDays - 1; i >= 0; i--) {
- const day = new Date(startOfToday - i * DAY_MS);
- timeline.set(day.toISOString().slice(0, 10), { date: day.toISOString().slice(0, 10), tokensSavedEst: 0, compressed: 0, requests: 0 });
+ const dateKey = shiftVietnamDateKey(todayKey, -i);
+ timeline.set(dateKey, { date: dateKey, tokensSavedEst: 0, compressed: 0, requests: 0 });
}
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 - 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);
if (bucket) {
bucket.requests++;
diff --git a/src/mitm/logger.js b/src/mitm/logger.js
index 5e53210e..30d6d460 100644
--- a/src/mitm/logger.js
+++ b/src/mitm/logger.js
@@ -4,8 +4,10 @@ const zlib = require("zlib");
const { DATA_DIR } = require("./paths");
const { LOG_BLACKLIST_URL_PARTS } = require("./config");
+const VIETNAM_TIME_ZONE = "Asia/Ho_Chi_Minh";
+
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}`);
diff --git a/src/shared/utils/dateTime.js b/src/shared/utils/dateTime.js
new file mode 100644
index 00000000..5d699b86
--- /dev/null
+++ b/src/shared/utils/dateTime.js
@@ -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,
+ });
+}
\ No newline at end of file
diff --git a/src/sse/utils/logger.js b/src/sse/utils/logger.js
index a9c631ed..2b2767b0 100644
--- a/src/sse/utils/logger.js
+++ b/src/sse/utils/logger.js
@@ -1,3 +1,5 @@
+import { VIETNAM_TIME_ZONE } from "open-sse/config/time.js";
+
// Logger utility for cloud
const LOG_LEVELS = {
@@ -10,7 +12,7 @@ const LOG_LEVELS = {
const LEVEL = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase?.()] ?? LOG_LEVELS.INFO;
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)
|