diff --git a/frontend/src/components/git/ForgejoHeatmap.tsx b/frontend/src/components/git/ForgejoHeatmap.tsx
index ece91fc..b961e5e 100644
--- a/frontend/src/components/git/ForgejoHeatmap.tsx
+++ b/frontend/src/components/git/ForgejoHeatmap.tsx
@@ -523,12 +523,10 @@ function HeatmapGrid({ days, range, onRangeChange }: {
{/* Contributions summary */}
diff --git a/frontend/src/components/git/ForgejoIssueMetricCards.tsx b/frontend/src/components/git/ForgejoIssueMetricCards.tsx
index d341794..2c01279 100644
--- a/frontend/src/components/git/ForgejoIssueMetricCards.tsx
+++ b/frontend/src/components/git/ForgejoIssueMetricCards.tsx
@@ -1,6 +1,6 @@
"use client";
-import type { ComponentType } from "react";
+import { useEffect, useState, type ComponentType } from "react";
import Link from "next/link";
import {
AlertCircle,
@@ -14,7 +14,6 @@ import {
} from "lucide-react";
import type { ForgejoIssueMetrics, ForgejoRepository } from "@/lib/api-forgejo";
-import { formatRelativeTimestamp } from "@/lib/formatters";
import { cn } from "@/lib/utils";
type ForgejoIssueMetricCardsProps = {
@@ -48,6 +47,17 @@ const parseDate = (value: string | null | undefined): Date | null => {
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 => {
if (dates.length === 0) return null;
return dates.reduce((latest, date) =>
@@ -89,6 +99,7 @@ const toneValueClasses: Record = {
function buildSyncHealthCard(
metrics: ForgejoIssueMetrics | null,
repositories: ForgejoRepository[],
+ nowMs: number,
): MetricCard {
const repositoryCount = repositories.length;
const syncErrorCount = Object.values(metrics?.sync_error_counts ?? {}).filter(
@@ -104,7 +115,7 @@ function buildSyncHealthCard(
metricSyncDates.length > 0 ? metricSyncDates : repositorySyncDates,
);
const latestSyncAge = latestSync
- ? Date.now() - latestSync.getTime()
+ ? Math.max(0, nowMs - latestSync.getTime())
: Number.POSITIVE_INFINITY;
if (repositoryCount === 0) {
@@ -141,7 +152,7 @@ function buildSyncHealthCard(
return {
title: "Last Sync Health",
value: "Stale",
- caption: `Last sync ${formatRelativeTimestamp(latestSync.toISOString())}.`,
+ caption: `Last sync ${formatRelativeTimestampLive(latestSync, nowMs)}.`,
href: "/git-projects/repositories",
tone: "amber",
icon: Clock3,
@@ -150,7 +161,7 @@ function buildSyncHealthCard(
return {
title: "Last Sync Health",
value: "Healthy",
- caption: `Last sync ${formatRelativeTimestamp(latestSync.toISOString())}.`,
+ caption: `Last sync ${formatRelativeTimestampLive(latestSync, nowMs)}.`,
href: "/git-projects/repositories",
tone: "cyan",
icon: ShieldCheck,
@@ -213,6 +224,13 @@ export function ForgejoIssueMetricCards({
isLoading = false,
error,
}: 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 recentlyClosed = metrics?.closed_last_7_days ?? 0;
const staleOpen = metrics?.stale_open_issues ?? 0;
@@ -255,7 +273,7 @@ export function ForgejoIssueMetricCards({
tone: staleTone,
icon: staleOpen === 0 ? CheckCircle2 : Clock3,
},
- buildSyncHealthCard(metrics, repositories),
+ buildSyncHealthCard(metrics, repositories, nowMs),
];
return (