فهرست منبع

dev: automated commit - 2026-03-08 11:30:24

Mariano Z. 2 هفته پیش
والد
کامیت
0d4778c806
5فایلهای تغییر یافته به همراه49 افزوده شده و 10 حذف شده
  1. 1 0
      frontend/src/api/types.ts
  2. 15 1
      frontend/src/views/DashboardView.vue
  3. 11 3
      internal/cron/cron.go
  4. 12 2
      internal/handler/ip.go
  5. 10 4
      internal/ipfetcher/ipfetcher.go

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

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

+ 15 - 1
frontend/src/views/DashboardView.vue

@@ -4,12 +4,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
 import { Badge } from '@/components/ui/badge'
 import { Skeleton } from '@/components/ui/skeleton'
 import { Globe, Layers, FileText } from 'lucide-vue-next'
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
 import { api } from '@/api/client'
 import type { IpResponse, Zone, DnsRecord } from '@/api/types'
 import { toast } from 'vue-sonner'
 
 const ip = ref('')
 const ipUpdatedAt = ref<string | undefined>(undefined)
+const ipSource = ref<string | undefined>(undefined)
 const zones = ref<Zone[]>([])
 const records = ref<DnsRecord[]>([])
 const loading = ref(true)
@@ -35,6 +37,7 @@ onMounted(async () => {
     ])
     ip.value = ipData.ip
     ipUpdatedAt.value = ipData.ip_updated_at
+    ipSource.value = ipData.ip_source
     zones.value = zonesData
     records.value = recordsData
   } catch (err) {
@@ -59,7 +62,18 @@ const staticRecords = () => records.value.filter((r) => r.is_static === 1).lengt
       <Card>
         <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
           <CardTitle class="text-sm font-medium">Public IP</CardTitle>
-          <Globe class="h-4 w-4 text-muted-foreground" />
+          <TooltipProvider>
+            <Tooltip>
+              <TooltipTrigger asChild>
+                <span class="inline-flex cursor-default">
+                  <Globe class="h-4 w-4 text-muted-foreground" />
+                </span>
+              </TooltipTrigger>
+              <TooltipContent>
+                <p>Fetched from: {{ ipSource || 'Unknown' }}</p>
+              </TooltipContent>
+            </Tooltip>
+          </TooltipProvider>
         </CardHeader>
         <CardContent>
           <Skeleton v-if="loading" class="h-7 w-36" />

+ 11 - 3
internal/cron/cron.go

@@ -18,6 +18,7 @@ import (
 const (
 	settingKeyCurrentIP         = "current_ip"
 	settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
+	settingKeyCurrentIPSource   = "current_ip_source"
 )
 
 type DDNSUpdater struct {
@@ -99,11 +100,12 @@ func (u *DDNSUpdater) run() {
 	if len(urls) > 0 {
 		urlsOrNil = urls
 	}
-	ip, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
+	res, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
 	if err != nil {
 		slog.Error("failed to fetch public IP", "error", err)
 		return
 	}
+	ip := res.IP
 
 	u.mu.Lock()
 	lastIP := u.lastIP
@@ -123,10 +125,13 @@ func (u *DDNSUpdater) run() {
 	}
 
 	if len(records) == 0 {
+		now := time.Now().UTC().Format(time.RFC3339)
 		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 {
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: now}); err != nil {
 			slog.Error("failed to persist current_ip_updated_at", "error", err)
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPSource, Value: res.Source}); err != nil {
+			slog.Error("failed to persist current_ip_source", "error", err)
 		}
 		u.mu.Lock()
 		u.lastIP = ip
@@ -170,10 +175,13 @@ func (u *DDNSUpdater) run() {
 	}
 
 	if allOK {
+		now := time.Now().UTC().Format(time.RFC3339)
 		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 {
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: now}); err != nil {
 			slog.Error("failed to persist current_ip_updated_at", "error", err)
+		} else if err := u.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPSource, Value: res.Source}); err != nil {
+			slog.Error("failed to persist current_ip_source", "error", err)
 		}
 		u.mu.Lock()
 		u.lastIP = ip

+ 12 - 2
internal/handler/ip.go

@@ -2,6 +2,7 @@ package handler
 
 import (
 	"net/http"
+	"time"
 
 	"goflare/internal/database/queries"
 	"goflare/internal/ipfetcher"
@@ -20,6 +21,7 @@ func NewIPHandler(q *queries.Queries) *IPHandler {
 const (
 	settingKeyCurrentIP         = "current_ip"
 	settingKeyCurrentIPUpdatedAt = "current_ip_updated_at"
+	settingKeyCurrentIPSource   = "current_ip_source"
 )
 
 func (h *IPHandler) Get(c echo.Context) error {
@@ -30,6 +32,9 @@ func (h *IPHandler) Get(c echo.Context) error {
 		if updatedAt, err := h.q.GetSetting(ctx, settingKeyCurrentIPUpdatedAt); err == nil && updatedAt != "" {
 			resp["ip_updated_at"] = updatedAt
 		}
+		if source, err := h.q.GetSetting(ctx, settingKeyCurrentIPSource); err == nil && source != "" {
+			resp["ip_source"] = source
+		}
 		return c.JSON(http.StatusOK, resp)
 	}
 	urls, err := h.q.ListEnabledIpProviderUrls(ctx)
@@ -40,9 +45,14 @@ func (h *IPHandler) Get(c echo.Context) error {
 	if len(urls) > 0 {
 		urlsOrNil = urls
 	}
-	ip, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
+	res, err := ipfetcher.FetchPublicIPFrom(ctx, urlsOrNil)
 	if err != nil {
 		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to fetch public IP"})
 	}
-	return c.JSON(http.StatusOK, map[string]string{"ip": ip})
+	now := time.Now().UTC().Format(time.RFC3339)
+	_ = h.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIP, Value: res.IP})
+	_ = h.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPUpdatedAt, Value: now})
+	_ = h.q.UpsertSetting(ctx, queries.UpsertSettingParams{Key: settingKeyCurrentIPSource, Value: res.Source})
+	resp := map[string]string{"ip": res.IP, "ip_updated_at": now, "ip_source": res.Source}
+	return c.JSON(http.StatusOK, resp)
 }

+ 10 - 4
internal/ipfetcher/ipfetcher.go

@@ -19,9 +19,15 @@ var defaultProviders = []string{
 
 var httpClient = &http.Client{Timeout: 5 * time.Second}
 
+// Result holds the outcome of a successful public IP fetch.
+type Result struct {
+	IP     string // The detected public IP.
+	Source string // The URL that returned the IP (e.g. https://api.ipify.org).
+}
+
 // FetchPublicIPFrom fetches the public IP by trying each URL in order.
 // If urls is nil or empty, the built-in default providers are used.
-func FetchPublicIPFrom(ctx context.Context, urls []string) (string, error) {
+func FetchPublicIPFrom(ctx context.Context, urls []string) (*Result, error) {
 	slog.Debug("fetching public IP...")
 	if len(urls) == 0 {
 		urls = defaultProviders
@@ -33,10 +39,10 @@ func FetchPublicIPFrom(ctx context.Context, urls []string) (string, error) {
 			lastErr = err
 			continue
 		}
-		slog.Default().Debug("public IP fetched", "ip", ip)
-		return ip, nil
+		slog.Default().Debug("public IP fetched", "ip", ip, "source", url)
+		return &Result{IP: ip, Source: url}, nil
 	}
-	return "", fmt.Errorf("all IP providers failed, last error: %w", lastErr)
+	return nil, fmt.Errorf("all IP providers failed, last error: %w", lastErr)
 }
 
 func fetchFrom(ctx context.Context, url string) (string, error) {