diff --git a/pkg/metrics/mem/server.go b/pkg/metrics/mem/server.go index 86ca0ad3..fad8baef 100644 --- a/pkg/metrics/mem/server.go +++ b/pkg/metrics/mem/server.go @@ -239,9 +239,11 @@ func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats { } if !proxyStats.LastStartTime.IsZero() { ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05") + ps.LastStartAt = proxyStats.LastStartTime.Unix() } if !proxyStats.LastCloseTime.IsZero() { ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05") + ps.LastCloseAt = proxyStats.LastCloseTime.Unix() } return ps } diff --git a/pkg/metrics/mem/server_test.go b/pkg/metrics/mem/server_test.go index ee82b68a..12040c9f 100644 --- a/pkg/metrics/mem/server_test.go +++ b/pkg/metrics/mem/server_test.go @@ -22,6 +22,12 @@ func TestServerMetricsUsesClockForProxyTimestamps(t *testing.T) { clk.SetTime(closedAt) metrics.CloseProxy("proxy", "tcp") require.Equal(closedAt, metrics.info.ProxyStatistics["proxy"].LastCloseTime) + + stats := metrics.GetProxyByName("proxy") + require.Equal(start.Format("01-02 15:04:05"), stats.LastStartTime) + require.Equal(closedAt.Format("01-02 15:04:05"), stats.LastCloseTime) + require.Equal(start.Unix(), stats.LastStartAt) + require.Equal(closedAt.Unix(), stats.LastCloseAt) } func TestServerMetricsClearUselessInfoUsesClock(t *testing.T) { diff --git a/pkg/metrics/mem/types.go b/pkg/metrics/mem/types.go index b566f4f2..6361693b 100644 --- a/pkg/metrics/mem/types.go +++ b/pkg/metrics/mem/types.go @@ -41,6 +41,8 @@ type ProxyStats struct { TodayTrafficOut int64 LastStartTime string LastCloseTime string + LastStartAt int64 + LastCloseAt int64 CurConns int64 } diff --git a/server/http/controller_v2.go b/server/http/controller_v2.go index e4d10ca8..9b2e241b 100644 --- a/server/http/controller_v2.go +++ b/server/http/controller_v2.go @@ -545,8 +545,8 @@ func (c *Controller) buildV2ProxyResp(ps *mem.ProxyStats) model.V2ProxyResp { TodayTrafficIn: ps.TodayTrafficIn, TodayTrafficOut: ps.TodayTrafficOut, CurConns: ps.CurConns, - LastStartTime: ps.LastStartTime, - LastCloseTime: ps.LastCloseTime, + LastStartAt: ps.LastStartAt, + LastCloseAt: ps.LastCloseAt, }, } } diff --git a/server/http/controller_v2_test.go b/server/http/controller_v2_test.go index fe37e616..a2fc8b13 100644 --- a/server/http/controller_v2_test.go +++ b/server/http/controller_v2_test.go @@ -421,10 +421,22 @@ func TestAPIV2ProxyListDetailAndUsers(t *testing.T) { } resp = performRequest(router, "/api/v2/proxies/tcp-alice") + rawProxyDetailResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + assertRawJSONKeysFromMessage(t, rawProxyDetailResp.Data["status"], + "curConns", + "lastCloseAt", + "lastStartAt", + "phase", + "todayTrafficIn", + "todayTrafficOut", + ) proxyDetailResp := decodeResponse[v2EnvelopeForTest[model.V2ProxyResp]](t, resp) if proxyDetailResp.Data.Name != "tcp-alice" || proxyDetailResp.Data.User != "alice" { t.Fatalf("proxy detail mismatch: %#v", proxyDetailResp.Data) } + if proxyDetailResp.Data.Status.LastStartAt != 1783504200 || proxyDetailResp.Data.Status.LastCloseAt != 1783504300 { + t.Fatalf("proxy detail timestamp mismatch: %#v", proxyDetailResp.Data.Status) + } resp = performRequest(router, "/api/v2/users?page=1&pageSize=50") userResp := decodeResponse[v2EnvelopeForTest[model.V2PageResp[model.V2UserResp]]](t, resp) @@ -744,6 +756,10 @@ func newV2TestController(t *testing.T) *Controller { TodayTrafficIn: 30, TodayTrafficOut: 40, CurConns: 2, + LastStartTime: "07-08 12:30:00", + LastCloseTime: "07-08 12:31:40", + LastStartAt: 1783504200, + LastCloseAt: 1783504300, }, "http-alice": { Name: "http-alice", diff --git a/server/http/model/v2.go b/server/http/model/v2.go index 74883370..e5c06802 100644 --- a/server/http/model/v2.go +++ b/server/http/model/v2.go @@ -87,8 +87,8 @@ type V2ProxyStatusResp struct { TodayTrafficIn int64 `json:"todayTrafficIn"` TodayTrafficOut int64 `json:"todayTrafficOut"` CurConns int64 `json:"curConns"` - LastStartTime string `json:"lastStartTime"` - LastCloseTime string `json:"lastCloseTime"` + LastStartAt int64 `json:"lastStartAt,omitempty"` + LastCloseAt int64 `json:"lastCloseAt,omitempty"` } type V2ProxyTrafficResp struct { diff --git a/web/frps/src/api/proxy.ts b/web/frps/src/api/proxy.ts index 282040aa..47f00e79 100644 --- a/web/frps/src/api/proxy.ts +++ b/web/frps/src/api/proxy.ts @@ -1,4 +1,5 @@ import { buildQueryString, http } from './http' +import { formatUnixSeconds } from '../utils/format' import type { V2Page } from './http' import type { GetProxyResponse, @@ -47,8 +48,8 @@ export const toLegacyProxyStats = (proxy: ProxyV2Info): ProxyStatsInfo => ({ todayTrafficIn: proxy.status.todayTrafficIn, todayTrafficOut: proxy.status.todayTrafficOut, curConns: proxy.status.curConns, - lastStartTime: proxy.status.lastStartTime, - lastCloseTime: proxy.status.lastCloseTime, + lastStartTime: formatUnixSeconds(proxy.status.lastStartAt), + lastCloseTime: formatUnixSeconds(proxy.status.lastCloseAt), status: proxy.status.phase, }) diff --git a/web/frps/src/types/proxy.ts b/web/frps/src/types/proxy.ts index 150890f6..1d2a1036 100644 --- a/web/frps/src/types/proxy.ts +++ b/web/frps/src/types/proxy.ts @@ -40,8 +40,8 @@ export interface ProxyV2Status { todayTrafficIn: number todayTrafficOut: number curConns: number - lastStartTime: string - lastCloseTime: string + lastStartAt?: number + lastCloseAt?: number } export interface TrafficResponse { diff --git a/web/frps/src/utils/format.ts b/web/frps/src/utils/format.ts index 11cd398f..cca9e31a 100644 --- a/web/frps/src/utils/format.ts +++ b/web/frps/src/utils/format.ts @@ -19,6 +19,14 @@ export function formatDistanceToNow(date: Date): string { return Math.floor(seconds) + ' seconds ago' } +export function formatUnixSeconds(seconds?: number): string { + if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return '' + + const date = new Date(seconds * 1000) + const pad = (value: number) => value.toString().padStart(2, '0') + return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +} + export function formatFileSize(bytes: number): string { if (!Number.isFinite(bytes) || bytes < 0) return '0 B' if (bytes === 0) return '0 B'