Compare commits

..

2 Commits

4 changed files with 54 additions and 31 deletions

View File

@ -110,6 +110,13 @@ func triggerTimeSyncOnNetworkStateChange() {
}() }()
} }
func setPublicIPReadyState(ipv4Ready, ipv6Ready bool) {
if publicIPState == nil {
return
}
publicIPState.SetIPv4AndIPv6(ipv4Ready, ipv6Ready)
}
func networkStateChanged(_ string, state types.InterfaceState) { func networkStateChanged(_ string, state types.InterfaceState) {
// do not block the main thread // do not block the main thread
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
@ -121,15 +128,9 @@ func networkStateChanged(_ string, state types.InterfaceState) {
if state.Online { if state.Online {
networkLogger.Info().Msg("network state changed to online, triggering time sync") networkLogger.Info().Msg("network state changed to online, triggering time sync")
triggerTimeSyncOnNetworkStateChange() triggerTimeSyncOnNetworkStateChange()
}
if publicIPState != nil { setPublicIPReadyState(state.IPv4Ready, state.IPv6Ready)
publicIPState.SetIPv4AndIPv6(state.IPv4Ready, state.IPv6Ready)
}
} else {
if publicIPState != nil {
publicIPState.SetIPv4AndIPv6(false, false)
}
}
// always restart mDNS when the network state changes // always restart mDNS when the network state changes
if mDNS != nil { if mDNS != nil {

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -52,21 +53,38 @@ func (ps *PublicIPState) checkCloudflare(ctx context.Context, family int) (*Publ
return nil, err return nil, err
} }
values := make(map[string]string)
for line := range strings.SplitSeq(string(body), "\n") { for line := range strings.SplitSeq(string(body), "\n") {
key, value, ok := strings.Cut(line, "=") key, value, ok := strings.Cut(line, "=")
if !ok || key != "ip" { if !ok {
continue continue
} }
values[key] = value
}
ps.lastUpdated = time.Now()
if ts, ok := values["ts"]; ok {
if ts, err := strconv.ParseFloat(ts, 64); err == nil {
ps.lastUpdated = time.Unix(int64(ts), 0)
}
}
ipStr, ok := values["ip"]
if !ok {
return nil, fmt.Errorf("no IP address found")
}
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
}
return &PublicIP{ return &PublicIP{
IPAddress: net.ParseIP(value), IPAddress: ip,
LastUpdated: time.Now(), LastUpdated: ps.lastUpdated,
}, nil }, nil
} }
return nil, fmt.Errorf("no IP address found")
}
// checkAPI uses the API endpoint to get the public IP address // checkAPI uses the API endpoint to get the public IP address
func (ps *PublicIPState) checkAPI(_ context.Context, _ int) (*PublicIP, error) { func (ps *PublicIPState) checkAPI(_ context.Context, _ int) (*PublicIP, error) {
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")

View File

@ -92,22 +92,6 @@ func (ps *PublicIPState) SetIPv4AndIPv6(ipv4, ipv6 bool) {
ps.ipv6 = ipv6 ps.ipv6 = ipv6
} }
// SetIPv4 sets if we need to track IPv4 public IP addresses
func (ps *PublicIPState) SetIPv4(ipv4 bool) {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.ipv4 = ipv4
}
// SetIPv6 sets if we need to track IPv6 public IP addresses
func (ps *PublicIPState) SetIPv6(ipv6 bool) {
ps.mu.Lock()
defer ps.mu.Unlock()
ps.ipv6 = ipv6
}
// SetCloudflareEndpoint sets the Cloudflare endpoint // SetCloudflareEndpoint sets the Cloudflare endpoint
func (ps *PublicIPState) SetCloudflareEndpoint(endpoint string) { func (ps *PublicIPState) SetCloudflareEndpoint(endpoint string) {
ps.mu.Lock() ps.mu.Lock()

View File

@ -7,8 +7,25 @@ import { PublicIP } from "@hooks/stores";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { formatters } from "@/utils";
const TimeAgoLabel = ({ date }: { date: Date }) => {
const [timeAgo, setTimeAgo] = useState<string | undefined>(formatters.timeAgo(date));
useEffect(() => {
const interval = setInterval(() => {
setTimeAgo(formatters.timeAgo(date));
}, 1000);
return () => clearInterval(interval);
}, [date]);
return (
<span className="text-sm text-slate-600 dark:text-slate-400 select-none">
{timeAgo}
</span>
);
};
export default function PublicIPCard() { export default function PublicIPCard() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
@ -21,6 +38,8 @@ export default function PublicIPCard() {
return; return;
} }
const publicIPs = resp.result as PublicIP[]; const publicIPs = resp.result as PublicIP[];
// sort the public IPs by IP address
// IPv6 addresses are sorted after IPv4 addresses
setPublicIPs(publicIPs.sort(({ ip: aIp }, { ip: bIp }) => { setPublicIPs(publicIPs.sort(({ ip: aIp }, { ip: bIp }) => {
const aIsIPv6 = aIp.includes(":"); const aIsIPv6 = aIp.includes(":");
const bIsIPv6 = bIp.includes(":"); const bIsIPv6 = bIp.includes(":");
@ -74,6 +93,7 @@ export default function PublicIPCard() {
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{ip.ip} {ip.ip}
</span> </span>
{ip.last_updated && <TimeAgoLabel date={new Date(ip.last_updated)} />}
</div> </div>
))} ))}
</div> </div>