mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 17:30:26 +00:00
Issue 17774 - Usage - Local - Show data from midnight to midnight of selected dates for browser time zone (AI assisted) (openclaw#19357) thanks @huntharo
Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini (override approved by Tak for this run; local baseline failures outside PR scope) Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -39,8 +39,12 @@ import {
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
|
||||
const COST_USAGE_CACHE_TTL_MS = 30_000;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
type DateRange = { startMs: number; endMs: number };
|
||||
type DateInterpretation =
|
||||
| { mode: "utc" | "gateway" }
|
||||
| { mode: "specific"; utcOffsetMinutes: number };
|
||||
|
||||
type CostUsageCacheEntry = {
|
||||
summary?: CostUsageSummary;
|
||||
@@ -84,11 +88,9 @@ function resolveSessionUsageFileOrRespond(
|
||||
return { config, entry, agentId, sessionId, sessionFile };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date string (YYYY-MM-DD) to start of day timestamp in UTC.
|
||||
* Returns undefined if invalid.
|
||||
*/
|
||||
const parseDateToMs = (raw: unknown): number | undefined => {
|
||||
const parseDateParts = (
|
||||
raw: unknown,
|
||||
): { year: number; monthIndex: number; day: number } | undefined => {
|
||||
if (typeof raw !== "string" || !raw.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -96,13 +98,98 @@ const parseDateToMs = (raw: unknown): number | undefined => {
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, year, month, day] = match;
|
||||
// Use UTC to ensure consistent behavior across timezones
|
||||
const ms = Date.UTC(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
if (Number.isNaN(ms)) {
|
||||
const [, yearStr, monthStr, dayStr] = match;
|
||||
const year = Number(yearStr);
|
||||
const monthIndex = Number(monthStr) - 1;
|
||||
const day = Number(dayStr);
|
||||
if (!Number.isFinite(year) || !Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
||||
return undefined;
|
||||
}
|
||||
return ms;
|
||||
return { year, monthIndex, day };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a UTC offset string in the format UTC+H, UTC-H, UTC+HH, UTC-HH, UTC+H:MM, UTC-HH:MM.
|
||||
* Returns the UTC offset in minutes (east-positive), or undefined if invalid.
|
||||
*/
|
||||
const parseUtcOffsetToMinutes = (raw: unknown): number | undefined => {
|
||||
if (typeof raw !== "string" || !raw.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const match = /^UTC([+-])(\d{1,2})(?::([0-5]\d))?$/.exec(raw.trim());
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const sign = match[1] === "+" ? 1 : -1;
|
||||
const hours = Number(match[2]);
|
||||
const minutes = Number(match[3] ?? "0");
|
||||
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
|
||||
return undefined;
|
||||
}
|
||||
if (hours > 14 || (hours === 14 && minutes !== 0)) {
|
||||
return undefined;
|
||||
}
|
||||
const totalMinutes = sign * (hours * 60 + minutes);
|
||||
if (totalMinutes < -12 * 60 || totalMinutes > 14 * 60) {
|
||||
return undefined;
|
||||
}
|
||||
return totalMinutes;
|
||||
};
|
||||
|
||||
const resolveDateInterpretation = (params: {
|
||||
mode?: unknown;
|
||||
utcOffset?: unknown;
|
||||
}): DateInterpretation => {
|
||||
if (params.mode === "gateway") {
|
||||
return { mode: "gateway" };
|
||||
}
|
||||
if (params.mode === "specific") {
|
||||
const utcOffsetMinutes = parseUtcOffsetToMinutes(params.utcOffset);
|
||||
if (utcOffsetMinutes !== undefined) {
|
||||
return { mode: "specific", utcOffsetMinutes };
|
||||
}
|
||||
}
|
||||
// Backward compatibility: when mode is missing (or invalid), keep current UTC interpretation.
|
||||
return { mode: "utc" };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a date string (YYYY-MM-DD) to start-of-day timestamp based on interpretation mode.
|
||||
* Returns undefined if invalid.
|
||||
*/
|
||||
const parseDateToMs = (
|
||||
raw: unknown,
|
||||
interpretation: DateInterpretation = { mode: "utc" },
|
||||
): number | undefined => {
|
||||
const parts = parseDateParts(raw);
|
||||
if (!parts) {
|
||||
return undefined;
|
||||
}
|
||||
const { year, monthIndex, day } = parts;
|
||||
if (interpretation.mode === "gateway") {
|
||||
const ms = new Date(year, monthIndex, day).getTime();
|
||||
return Number.isNaN(ms) ? undefined : ms;
|
||||
}
|
||||
if (interpretation.mode === "specific") {
|
||||
const ms = Date.UTC(year, monthIndex, day) - interpretation.utcOffsetMinutes * 60 * 1000;
|
||||
return Number.isNaN(ms) ? undefined : ms;
|
||||
}
|
||||
const ms = Date.UTC(year, monthIndex, day);
|
||||
return Number.isNaN(ms) ? undefined : ms;
|
||||
};
|
||||
|
||||
const getTodayStartMs = (now: Date, interpretation: DateInterpretation): number => {
|
||||
if (interpretation.mode === "gateway") {
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
}
|
||||
if (interpretation.mode === "specific") {
|
||||
const shifted = new Date(now.getTime() + interpretation.utcOffsetMinutes * 60 * 1000);
|
||||
return (
|
||||
Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate()) -
|
||||
interpretation.utcOffsetMinutes * 60 * 1000
|
||||
);
|
||||
}
|
||||
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
};
|
||||
|
||||
const parseDays = (raw: unknown): number | undefined => {
|
||||
@@ -126,29 +213,31 @@ const parseDateRange = (params: {
|
||||
startDate?: unknown;
|
||||
endDate?: unknown;
|
||||
days?: unknown;
|
||||
mode?: unknown;
|
||||
utcOffset?: unknown;
|
||||
}): DateRange => {
|
||||
const now = new Date();
|
||||
// Use UTC for consistent date handling
|
||||
const todayStartMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
||||
const todayEndMs = todayStartMs + 24 * 60 * 60 * 1000 - 1;
|
||||
const interpretation = resolveDateInterpretation(params);
|
||||
const todayStartMs = getTodayStartMs(now, interpretation);
|
||||
const todayEndMs = todayStartMs + DAY_MS - 1;
|
||||
|
||||
const startMs = parseDateToMs(params.startDate);
|
||||
const endMs = parseDateToMs(params.endDate);
|
||||
const startMs = parseDateToMs(params.startDate, interpretation);
|
||||
const endMs = parseDateToMs(params.endDate, interpretation);
|
||||
|
||||
if (startMs !== undefined && endMs !== undefined) {
|
||||
// endMs should be end of day
|
||||
return { startMs, endMs: endMs + 24 * 60 * 60 * 1000 - 1 };
|
||||
return { startMs, endMs: endMs + DAY_MS - 1 };
|
||||
}
|
||||
|
||||
const days = parseDays(params.days);
|
||||
if (days !== undefined) {
|
||||
const clampedDays = Math.max(1, days);
|
||||
const start = todayStartMs - (clampedDays - 1) * 24 * 60 * 60 * 1000;
|
||||
const start = todayStartMs - (clampedDays - 1) * DAY_MS;
|
||||
return { startMs: start, endMs: todayEndMs };
|
||||
}
|
||||
|
||||
// Default to last 30 days
|
||||
const defaultStartMs = todayStartMs - 29 * 24 * 60 * 60 * 1000;
|
||||
const defaultStartMs = todayStartMs - 29 * DAY_MS;
|
||||
return { startMs: defaultStartMs, endMs: todayEndMs };
|
||||
};
|
||||
|
||||
@@ -239,7 +328,11 @@ async function loadCostUsageSummaryCached(params: {
|
||||
|
||||
// Exposed for unit tests (kept as a single export to avoid widening the public API surface).
|
||||
export const __test = {
|
||||
parseDateParts,
|
||||
parseUtcOffsetToMinutes,
|
||||
resolveDateInterpretation,
|
||||
parseDateToMs,
|
||||
getTodayStartMs,
|
||||
parseDays,
|
||||
parseDateRange,
|
||||
discoverAllSessionsForUsage,
|
||||
@@ -313,6 +406,8 @@ export const usageHandlers: GatewayRequestHandlers = {
|
||||
startDate: params?.startDate,
|
||||
endDate: params?.endDate,
|
||||
days: params?.days,
|
||||
mode: params?.mode,
|
||||
utcOffset: params?.utcOffset,
|
||||
});
|
||||
const summary = await loadCostUsageSummaryCached({ startMs, endMs, config });
|
||||
respond(true, summary, undefined);
|
||||
@@ -335,6 +430,8 @@ export const usageHandlers: GatewayRequestHandlers = {
|
||||
const { startMs, endMs } = parseDateRange({
|
||||
startDate: p.startDate,
|
||||
endDate: p.endDate,
|
||||
mode: p.mode,
|
||||
utcOffset: p.utcOffset,
|
||||
});
|
||||
const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? p.limit : 50;
|
||||
const includeContextWeight = p.includeContextWeight ?? false;
|
||||
|
||||
Reference in New Issue
Block a user