Jelajahi Sumber

dev: automated commit - 2026-03-08 11:19:25

Mariano Z. 2 minggu lalu
induk
melakukan
5c9f9c9ebe

+ 1 - 0
frontend/src/api/types.ts

@@ -28,6 +28,7 @@ export interface SetupStatus {
 
 export interface IpResponse {
   ip: string
+  ip_updated_at?: string
 }
 
 export interface Settings {

+ 17 - 0
frontend/src/views/DashboardView.vue

@@ -9,10 +9,23 @@ import type { IpResponse, Zone, DnsRecord } from '@/api/types'
 import { toast } from 'vue-sonner'
 
 const ip = ref('')
+const ipUpdatedAt = ref<string | undefined>(undefined)
 const zones = ref<Zone[]>([])
 const records = ref<DnsRecord[]>([])
 const loading = ref(true)
 
+function formatIpUpdatedAt(iso: string): string {
+  try {
+    const date = new Date(iso)
+    return new Intl.DateTimeFormat(undefined, {
+      dateStyle: 'medium',
+      timeStyle: 'short',
+    }).format(date)
+  } catch {
+    return iso
+  }
+}
+
 onMounted(async () => {
   try {
     const [ipData, zonesData, recordsData] = await Promise.all([
@@ -21,6 +34,7 @@ onMounted(async () => {
       api.get<DnsRecord[]>('/api/records'),
     ])
     ip.value = ipData.ip
+    ipUpdatedAt.value = ipData.ip_updated_at
     zones.value = zonesData
     records.value = recordsData
   } catch (err) {
@@ -51,6 +65,9 @@ const staticRecords = () => records.value.filter((r) => r.is_static === 1).lengt
           <Skeleton v-if="loading" class="h-7 w-36" />
           <div v-else class="text-2xl font-bold font-mono">{{ ip }}</div>
           <CardDescription class="mt-1">Current detected IP address</CardDescription>
+          <p v-if="!loading && ipUpdatedAt" class="mt-1 text-xs text-muted-foreground">
+            Last changed: {{ formatIpUpdatedAt(ipUpdatedAt) }}
+          </p>
         </CardContent>
       </Card>
 

+ 9 - 1
internal/cron/cron.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"log/slog"
 	"sync"
+	"time"
 
 	cf "goflare/internal/cloudflare"
 	"goflare/internal/database/queries"
@@ -14,7 +15,10 @@ import (
 	"github.com/robfig/cron/v3"
 )
 
-const settingKeyCurrentIP = "current_ip"
+const (
+	settingKeyCurrentIP         = "current_ip"
+	settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
+)
 
 type DDNSUpdater struct {
 	q        *queries.Queries
@@ -121,6 +125,8 @@ func (u *DDNSUpdater) run() {
 	if len(records) == 0 {
 		if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
 			slog.Error("failed to persist current_ip", "error", err)
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
+			slog.Error("failed to persist current_ip_updated_at", "error", err)
 		}
 		u.mu.Lock()
 		u.lastIP = ip
@@ -166,6 +172,8 @@ func (u *DDNSUpdater) run() {
 	if allOK {
 		if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: ip}); err != nil {
 			slog.Error("failed to persist current_ip", "error", err)
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: time.Now().UTC().Format(time.RFC3339)}); err != nil {
+			slog.Error("failed to persist current_ip_updated_at", "error", err)
 		}
 		u.mu.Lock()
 		u.lastIP = ip

+ 9 - 2
internal/handler/ip.go

@@ -17,13 +17,20 @@ func NewIPHandler(q *queries.Queries) *IPHandler {
 	return &IPHandler{q: q}
 }
 
-const settingKeyCurrentIP = "current_ip"
+const (
+	settingKeyCurrentIP         = "current_ip"
+	settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
+)
 
 func (h *IPHandler) Get(c echo.Context) error {
 	ctx := c.Request().Context()
 	cached, err := h.q.GetSetting(ctx, settingKeyCurrentIP)
 	if err == nil && cached != "" {
-		return c.JSON(http.StatusOK, map[string]string{"ip": cached})
+		resp := map[string]string{"ip": cached}
+		if updatedAt, err := h.q.GetSetting(ctx, settingKeyCurrentIPUpdatedAt); err == nil && updatedAt != "" {
+			resp["ip_updated_at"] = updatedAt
+		}
+		return c.JSON(http.StatusOK, resp)
 	}
 	urls, err := h.q.ListEnabledIpProviderUrls(ctx)
 	if err != nil {