feat(issue-metrics): add live timestamp formatting for last sync health and update state management
This commit is contained in:
parent
f0a5c430f1
commit
74056664d4
|
|
@ -523,12 +523,10 @@ function HeatmapGrid({ days, range, onRangeChange }: {
|
|||
{/* Contributions summary */}
|
||||
<p className="text-center">
|
||||
<span className="text-2xl font-bold tabular-nums" style={{color:GREEN}}>
|
||||
{displayedCount.toLocaleString()}
|
||||
{heatmap.totalEvents.toLocaleString()}
|
||||
</span>
|
||||
<span className="ml-1.5 text-sm" style={{color:W70}}>
|
||||
{hoveredDay
|
||||
? `contributions on ${displayedLabel}`
|
||||
: `contributions across all tracked repositories in the last ${RANGE_SUMMARY[range]}`}
|
||||
contributions across all tracked repositories in the last {RANGE_SUMMARY[range]}
|
||||
</span>
|
||||
</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)"}}>
|
||||
<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-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 style={{width:1,height:40,background:"rgba(139,92,246,0.25)"}}/>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
|
|
|
|||
|
|
@ -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<MetricTone, string> = {
|
|||
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 (
|
||||
|
|
|
|||
Loading…
Reference in New Issue