mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-12 19:00:58 +08:00
c2e5b14bf8
Storage: every JS-side datetime('now') write becomes new Date().toISOString()
(messages_out all writers, processing_ack all stamps, delivered, retry
backoff, roles/members/destinations, skill SQL via strftime ISO-Z). Dormant
deliver_after raw string compares wrapped in datetime().
Display: ncl human output renders ISO instants as local "YYYY-MM-DD HH:mm"
(host + container renderers; --json stays ISO; the stamp shape round-trips
through parseZonedToUtc). Task run logs, conversation archive filenames,
clidash, the dashboard pusher, and the uninstall backup stamp all render
local; instruction texts updated in lockstep. CLAUDE.md documents the
convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
96 lines
2.6 KiB
TypeScript
96 lines
2.6 KiB
TypeScript
/**
|
|
* Check whether a timezone string is a valid IANA identifier
|
|
* that Intl.DateTimeFormat can use.
|
|
*/
|
|
export function isValidTimezone(tz: string): boolean {
|
|
try {
|
|
Intl.DateTimeFormat(undefined, { timeZone: tz });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the given timezone if valid IANA, otherwise fall back to UTC.
|
|
*/
|
|
export function resolveTimezone(tz: string): string {
|
|
return isValidTimezone(tz) ? tz : 'UTC';
|
|
}
|
|
|
|
/**
|
|
* Convert a UTC ISO timestamp to a localized display string.
|
|
* Uses the Intl API (no external dependencies).
|
|
* Falls back to UTC if the timezone is invalid.
|
|
*/
|
|
export function formatLocalTime(utcIso: string, timezone: string): string {
|
|
const date = new Date(utcIso);
|
|
return date.toLocaleString('en-US', {
|
|
timeZone: resolveTimezone(timezone),
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compact sortable local stamp for log lines: "YYYY-MM-DD HH:mm" in `timezone`.
|
|
* (sv-SE is the one locale whose default rendering is this exact shape.)
|
|
*/
|
|
export function formatLocalStamp(date: Date, timezone: string): string {
|
|
return date.toLocaleString('sv-SE', {
|
|
timeZone: resolveTimezone(timezone),
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Interpret a naive ISO-like timestamp (no trailing `Z`, no offset) as wall-clock
|
|
* time in `tz` and return the corresponding UTC Date. Strings that already carry
|
|
* offset info (`Z` or `+-HH:MM`) are passed through to the Date constructor.
|
|
*/
|
|
export function parseZonedToUtc(input: string, tz: string): Date {
|
|
const hasOffset = /Z$|[+-]\d{2}:?\d{2}$/.test(input.trim());
|
|
if (hasOffset) return new Date(input);
|
|
|
|
const zone = resolveTimezone(tz);
|
|
const asIfUtc = new Date(input + 'Z');
|
|
if (Number.isNaN(asIfUtc.getTime())) return asIfUtc;
|
|
|
|
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
timeZone: zone,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
});
|
|
const parts = Object.fromEntries(
|
|
fmt
|
|
.formatToParts(asIfUtc)
|
|
.filter((p) => p.type !== 'literal')
|
|
.map((p) => [p.type, p.value]),
|
|
);
|
|
const hour = parts.hour === '24' ? '00' : parts.hour;
|
|
const zonedAsUtcMs = Date.UTC(
|
|
Number(parts.year),
|
|
Number(parts.month) - 1,
|
|
Number(parts.day),
|
|
Number(hour),
|
|
Number(parts.minute),
|
|
Number(parts.second),
|
|
);
|
|
const offsetMs = zonedAsUtcMs - asIfUtc.getTime();
|
|
return new Date(asIfUtc.getTime() - offsetMs);
|
|
}
|