mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
133 lines
4.0 KiB
TypeScript
133 lines
4.0 KiB
TypeScript
/**
|
|
* Timeline helpers — pure functions for grouping AuditLogEntry arrays by day
|
|
* and producing human-readable relative timestamps.
|
|
*
|
|
* No I/O, no side-effects — safe to import in both server and client contexts.
|
|
*/
|
|
|
|
import type { AuditLogEntry } from "@/lib/compliance/index";
|
|
|
|
export interface DayGroup {
|
|
/** YYYY-MM-DD in local server time */
|
|
dayKey: string;
|
|
/** "today" | "yesterday" | ISO date string for older days */
|
|
label: "today" | "yesterday" | string;
|
|
entries: AuditLogEntry[];
|
|
}
|
|
|
|
/**
|
|
* Returns YYYY-MM-DD for a given ISO timestamp (using local server time).
|
|
*/
|
|
function toDayKey(iso: string): string {
|
|
const d = new Date(iso);
|
|
const year = d.getFullYear();
|
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Returns YYYY-MM-DD for a given epoch ms (local server time).
|
|
*/
|
|
function epochToDayKey(ms: number): string {
|
|
const d = new Date(ms);
|
|
const year = d.getFullYear();
|
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
const day = String(d.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Groups audit log entries by calendar day (local server time), sorted
|
|
* descending (most recent day first). Each group has a human label:
|
|
* - "today" for the current day
|
|
* - "yesterday" for the previous day
|
|
* - ISO date string "YYYY-MM-DD" for older days
|
|
*
|
|
* @param entries - Flat list of audit entries (order not assumed)
|
|
* @param referenceNowMs - Override for "now" (ms since epoch). Defaults to Date.now().
|
|
*/
|
|
export function groupByDay(entries: AuditLogEntry[], referenceNowMs?: number): DayGroup[] {
|
|
if (!entries.length) return [];
|
|
|
|
const nowMs = referenceNowMs ?? Date.now();
|
|
const todayKey = epochToDayKey(nowMs);
|
|
const yesterdayKey = epochToDayKey(nowMs - 24 * 60 * 60 * 1000);
|
|
|
|
// Sort descending by timestamp
|
|
const sorted = [...entries].sort((a, b) => {
|
|
const ta = a.timestamp ?? "";
|
|
const tb = b.timestamp ?? "";
|
|
if (ta > tb) return -1;
|
|
if (ta < tb) return 1;
|
|
// tiebreak by id desc
|
|
const ia = typeof a.id === "number" ? a.id : 0;
|
|
const ib = typeof b.id === "number" ? b.id : 0;
|
|
return ib - ia;
|
|
});
|
|
|
|
const groupMap = new Map<string, AuditLogEntry[]>();
|
|
const dayOrder: string[] = [];
|
|
|
|
for (const entry of sorted) {
|
|
const dk = toDayKey(entry.timestamp ?? "");
|
|
if (!groupMap.has(dk)) {
|
|
groupMap.set(dk, []);
|
|
dayOrder.push(dk);
|
|
}
|
|
groupMap.get(dk)!.push(entry);
|
|
}
|
|
|
|
return dayOrder.map((dk) => {
|
|
let label: string;
|
|
if (dk === todayKey) {
|
|
label = "today";
|
|
} else if (dk === yesterdayKey) {
|
|
label = "yesterday";
|
|
} else {
|
|
label = dk;
|
|
}
|
|
return { dayKey: dk, label, entries: groupMap.get(dk)! };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable relative time string for the given ISO timestamp.
|
|
*
|
|
* @param iso - ISO 8601 timestamp string
|
|
* @param locale - "en" or "pt-BR"
|
|
* @param referenceNowMs - Override for "now" (ms since epoch). Defaults to Date.now().
|
|
*/
|
|
export function relativeTime(
|
|
iso: string,
|
|
locale: "en" | "pt-BR",
|
|
referenceNowMs?: number
|
|
): string {
|
|
const nowMs = referenceNowMs ?? Date.now();
|
|
const then = new Date(iso).getTime();
|
|
if (!Number.isFinite(then)) {
|
|
return locale === "pt-BR" ? "agora há pouco" : "just now";
|
|
}
|
|
|
|
const diffMs = nowMs - then;
|
|
const diffSec = Math.floor(diffMs / 1000);
|
|
const diffMin = Math.floor(diffSec / 60);
|
|
const diffHour = Math.floor(diffMin / 60);
|
|
const diffDay = Math.floor(diffHour / 24);
|
|
|
|
if (locale === "pt-BR") {
|
|
if (diffSec < 60) return "agora há pouco";
|
|
if (diffMin < 60) return `há ${diffMin} min`;
|
|
if (diffHour < 24) return `há ${diffHour} h`;
|
|
if (diffDay === 1) return "ontem";
|
|
return `há ${diffDay} dias`;
|
|
}
|
|
|
|
// English
|
|
if (diffSec < 60) return "just now";
|
|
if (diffMin < 60) return `${diffMin} min ago`;
|
|
if (diffHour < 24) return `${diffHour} h ago`;
|
|
if (diffDay === 1) return "yesterday";
|
|
return `${diffDay} days ago`;
|
|
}
|