mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(audit): add timeline helpers (groupByDay + relativeTime) (B/F4)
This commit is contained in:
132
src/lib/audit/timeline.ts
Normal file
132
src/lib/audit/timeline.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user