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 */}
|
{/* 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">
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue