feat(issue-metrics): add live timestamp formatting for last sync health and update state management

This commit is contained in:
null 2026-05-22 19:40:08 -05:00
parent f0a5c430f1
commit 74056664d4
2 changed files with 27 additions and 11 deletions

View File

@ -523,12 +523,10 @@ function HeatmapGrid({ days, range, onRangeChange }: {
{/* Contributions summary */} {/* Contributions summary */}
<p className="text-center"> <p className="text-center">
<span className="text-2xl font-bold tabular-nums" style={{color:GREEN}}> <span className="text-2xl font-bold tabular-nums" style={{color:GREEN}}>
{displayedCount.toLocaleString()} {heatmap.totalEvents.toLocaleString()}
</span> </span>
<span className="ml-1.5 text-sm" style={{color:W70}}> <span className="ml-1.5 text-sm" style={{color:W70}}>
{hoveredDay contributions across all tracked repositories in the last {RANGE_SUMMARY[range]}
? `contributions on ${displayedLabel}`
: `contributions across all tracked repositories in the last ${RANGE_SUMMARY[range]}`}
</span> </span>
</p> </p>
@ -583,7 +581,7 @@ export function ForgejoHeatmap({
style={{background:"rgba(139,92,246,0.07)", border:"1px solid rgba(139,92,246,0.20)"}}> style={{background:"rgba(139,92,246,0.07)", border:"1px solid rgba(139,92,246,0.20)"}}>
<div className="flex flex-col items-center gap-0.5"> <div className="flex flex-col items-center gap-0.5">
<span className="text-2xl font-bold tabular-nums" style={{color:GREEN}}>+{fmtLines(totalAdditions)}</span> <span className="text-2xl font-bold tabular-nums" style={{color:GREEN}}>+{fmtLines(totalAdditions)}</span>
<span className="text-xs font-medium" style={{color:"rgba(52,211,153,0.70)"}}>lines added</span> <span className="text-xs font-medium" style={{color:"rgba(52,211,153,0.70)"}}>lines of code added</span>
</div> </div>
<div style={{width:1,height:40,background:"rgba(139,92,246,0.25)"}}/> <div style={{width:1,height:40,background:"rgba(139,92,246,0.25)"}}/>
<div className="flex flex-col items-center gap-0.5"> <div className="flex flex-col items-center gap-0.5">

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import type { ComponentType } from "react"; import { useEffect, useState, type ComponentType } from "react";
import Link from "next/link"; import Link from "next/link";
import { import {
AlertCircle, AlertCircle,
@ -14,7 +14,6 @@ import {
} from "lucide-react"; } from "lucide-react";
import type { ForgejoIssueMetrics, ForgejoRepository } from "@/lib/api-forgejo"; import type { ForgejoIssueMetrics, ForgejoRepository } from "@/lib/api-forgejo";
import { formatRelativeTimestamp } from "@/lib/formatters";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
type ForgejoIssueMetricCardsProps = { type ForgejoIssueMetricCardsProps = {
@ -48,6 +47,17 @@ const parseDate = (value: string | null | undefined): Date | null => {
return Number.isNaN(date.getTime()) ? null : date; return Number.isNaN(date.getTime()) ? null : date;
}; };
const formatRelativeTimestampLive = (date: Date, nowMs: number): string => {
const diff = Math.max(0, nowMs - date.getTime());
const minutes = Math.round(diff / 60000);
if (minutes < 1) return "Just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.round(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.round(hours / 24);
return `${days}d ago`;
};
const newestDate = (dates: Date[]): Date | null => { const newestDate = (dates: Date[]): Date | null => {
if (dates.length === 0) return null; if (dates.length === 0) return null;
return dates.reduce((latest, date) => return dates.reduce((latest, date) =>
@ -89,6 +99,7 @@ const toneValueClasses: Record<MetricTone, string> = {
function buildSyncHealthCard( function buildSyncHealthCard(
metrics: ForgejoIssueMetrics | null, metrics: ForgejoIssueMetrics | null,
repositories: ForgejoRepository[], repositories: ForgejoRepository[],
nowMs: number,
): MetricCard { ): MetricCard {
const repositoryCount = repositories.length; const repositoryCount = repositories.length;
const syncErrorCount = Object.values(metrics?.sync_error_counts ?? {}).filter( const syncErrorCount = Object.values(metrics?.sync_error_counts ?? {}).filter(
@ -104,7 +115,7 @@ function buildSyncHealthCard(
metricSyncDates.length > 0 ? metricSyncDates : repositorySyncDates, metricSyncDates.length > 0 ? metricSyncDates : repositorySyncDates,
); );
const latestSyncAge = latestSync const latestSyncAge = latestSync
? Date.now() - latestSync.getTime() ? Math.max(0, nowMs - latestSync.getTime())
: Number.POSITIVE_INFINITY; : Number.POSITIVE_INFINITY;
if (repositoryCount === 0) { if (repositoryCount === 0) {
@ -141,7 +152,7 @@ function buildSyncHealthCard(
return { return {
title: "Last Sync Health", title: "Last Sync Health",
value: "Stale", value: "Stale",
caption: `Last sync ${formatRelativeTimestamp(latestSync.toISOString())}.`, caption: `Last sync ${formatRelativeTimestampLive(latestSync, nowMs)}.`,
href: "/git-projects/repositories", href: "/git-projects/repositories",
tone: "amber", tone: "amber",
icon: Clock3, icon: Clock3,
@ -150,7 +161,7 @@ function buildSyncHealthCard(
return { return {
title: "Last Sync Health", title: "Last Sync Health",
value: "Healthy", value: "Healthy",
caption: `Last sync ${formatRelativeTimestamp(latestSync.toISOString())}.`, caption: `Last sync ${formatRelativeTimestampLive(latestSync, nowMs)}.`,
href: "/git-projects/repositories", href: "/git-projects/repositories",
tone: "cyan", tone: "cyan",
icon: ShieldCheck, icon: ShieldCheck,
@ -213,6 +224,13 @@ export function ForgejoIssueMetricCards({
isLoading = false, isLoading = false,
error, error,
}: ForgejoIssueMetricCardsProps) { }: ForgejoIssueMetricCardsProps) {
const [nowMs, setNowMs] = useState(0);
useEffect(() => {
const id = window.setInterval(() => setNowMs(Date.now()), 1000);
return () => window.clearInterval(id);
}, []);
const openIssues = metrics?.open_issues ?? 0; const openIssues = metrics?.open_issues ?? 0;
const recentlyClosed = metrics?.closed_last_7_days ?? 0; const recentlyClosed = metrics?.closed_last_7_days ?? 0;
const staleOpen = metrics?.stale_open_issues ?? 0; const staleOpen = metrics?.stale_open_issues ?? 0;
@ -255,7 +273,7 @@ export function ForgejoIssueMetricCards({
tone: staleTone, tone: staleTone,
icon: staleOpen === 0 ? CheckCircle2 : Clock3, icon: staleOpen === 0 ? CheckCircle2 : Clock3,
}, },
buildSyncHealthCard(metrics, repositories), buildSyncHealthCard(metrics, repositories, nowMs),
]; ];
return ( return (