mirror of https://github.com/jetkvm/kvm.git
Compare commits
8 Commits
bbdc4cb6a4
...
a9be36d253
Author | SHA1 | Date |
---|---|---|
|
a9be36d253 | |
|
af265d6c54 | |
|
4a23f22a55 | |
|
11a095c0f6 | |
|
584768bacf | |
|
488276f3a8 | |
|
7267347261 | |
|
393bc122d4 |
|
@ -43,9 +43,11 @@ type testNetworkConfig struct {
|
||||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||||
|
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||||
|
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateConfig(t *testing.T) {
|
func TestValidateConfig(t *testing.T) {
|
||||||
|
|
|
@ -45,9 +45,11 @@ type NetworkConfig struct {
|
||||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||||
|
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||||
|
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||||
|
|
|
@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
|
||||||
ipv6Addr *net.IP
|
ipv6Addr *net.IP
|
||||||
ipv6Addresses []IPv6Address
|
ipv6Addresses []IPv6Address
|
||||||
ipv6LinkLocal *net.IP
|
ipv6LinkLocal *net.IP
|
||||||
|
ntpAddresses []*net.IP
|
||||||
macAddr *net.HardwareAddr
|
macAddr *net.HardwareAddr
|
||||||
|
|
||||||
l *zerolog.Logger
|
l *zerolog.Logger
|
||||||
|
@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
||||||
onInitialCheck: opts.OnInitialCheck,
|
onInitialCheck: opts.OnInitialCheck,
|
||||||
cbConfigChange: opts.OnConfigChange,
|
cbConfigChange: opts.OnConfigChange,
|
||||||
config: opts.NetworkConfig,
|
config: opts.NetworkConfig,
|
||||||
|
ntpAddresses: make([]*net.IP, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the dhcp client
|
// create the dhcp client
|
||||||
|
@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
||||||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_ = s.updateNtpServersFromLease(lease)
|
||||||
_ = s.setHostnameIfNotSame()
|
_ = s.setHostnameIfNotSame()
|
||||||
|
|
||||||
opts.OnDhcpLeaseChange(lease)
|
opts.OnDhcpLeaseChange(lease)
|
||||||
|
@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
|
||||||
return s.ipv6Addr.String()
|
return s.ipv6Addr.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
|
||||||
|
return s.ntpAddresses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NetworkInterfaceState) NtpAddressesString() []string {
|
||||||
|
ntpServers := []string{}
|
||||||
|
|
||||||
|
if s != nil {
|
||||||
|
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
|
||||||
|
|
||||||
|
if len(s.ntpAddresses) > 0 {
|
||||||
|
for _, server := range s.ntpAddresses {
|
||||||
|
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
|
||||||
|
ntpServers = append(ntpServers, server.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ntpServers
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
||||||
return s.macAddr
|
return s.macAddr
|
||||||
}
|
}
|
||||||
|
@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
return dhcpTargetState, nil
|
return dhcpTargetState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
|
||||||
|
if lease != nil && len(lease.NTPServers) > 0 {
|
||||||
|
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
|
||||||
|
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
|
||||||
|
|
||||||
|
for _, ntpServer := range lease.NTPServers {
|
||||||
|
if ntpServer != nil {
|
||||||
|
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
|
||||||
|
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.l.Info().Msg("no NTP servers found in lease")
|
||||||
|
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
||||||
dhcpTargetState, err := s.update()
|
dhcpTargetState, err := s.update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{
|
||||||
// "http://www.msftconnecttest.com/connecttest.txt",
|
// "http://www.msftconnecttest.com/connecttest.txt",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
|
||||||
chunkSize := 4
|
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||||
httpUrls := t.httpUrls
|
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
|
||||||
|
|
||||||
// shuffle the http urls to avoid always querying the same servers
|
// shuffle the http urls to avoid always querying the same servers
|
||||||
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })
|
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })
|
||||||
|
|
|
@ -73,6 +73,7 @@ var (
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
|
|
||||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
metricNtpServerInfo = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_timesync_ntp_server_info",
|
Name: "jetkvm_timesync_ntp_server_info",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package timesync
|
package timesync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -21,9 +22,9 @@ var defaultNTPServers = []string{
|
||||||
"3.pool.ntp.org",
|
"3.pool.ntp.org",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||||
chunkSize := 4
|
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||||
ntpServers := t.ntpServers
|
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
|
||||||
|
|
||||||
// shuffle the ntp servers to avoid always querying the same servers
|
// shuffle the ntp servers to avoid always querying the same servers
|
||||||
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
|
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
|
||||||
|
@ -46,6 +47,10 @@ type ntpResult struct {
|
||||||
|
|
||||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
||||||
results := make(chan *ntpResult, len(servers))
|
results := make(chan *ntpResult, len(servers))
|
||||||
|
|
||||||
|
_, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
go func(server string) {
|
go func(server string) {
|
||||||
scopedLogger := t.l.With().
|
scopedLogger := t.l.With().
|
||||||
|
@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if response.IsKissOfDeath() {
|
||||||
|
scopedLogger.Warn().
|
||||||
|
Str("kiss_code", response.KissCode).
|
||||||
|
Msg("ignoring NTP server kiss of death")
|
||||||
|
results <- nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rtt := float64(response.RTT.Milliseconds())
|
||||||
|
|
||||||
// set the last RTT
|
// set the last RTT
|
||||||
metricNtpServerLastRTT.WithLabelValues(
|
metricNtpServerLastRTT.WithLabelValues(
|
||||||
server,
|
server,
|
||||||
).Set(float64(response.RTT.Milliseconds()))
|
).Set(rtt)
|
||||||
|
|
||||||
// set the RTT histogram
|
// set the RTT histogram
|
||||||
metricNtpServerRttHistogram.WithLabelValues(
|
metricNtpServerRttHistogram.WithLabelValues(
|
||||||
server,
|
server,
|
||||||
).Observe(float64(response.RTT.Milliseconds()))
|
).Observe(rtt)
|
||||||
|
|
||||||
// set the server info
|
// set the server info
|
||||||
metricNtpServerInfo.WithLabelValues(
|
metricNtpServerInfo.WithLabelValues(
|
||||||
|
@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
||||||
scopedLogger.Info().
|
scopedLogger.Info().
|
||||||
Str("time", now.Format(time.RFC3339)).
|
Str("time", now.Format(time.RFC3339)).
|
||||||
Str("reference", response.ReferenceString()).
|
Str("reference", response.ReferenceString()).
|
||||||
Str("rtt", response.RTT.String()).
|
Float64("rtt", rtt).
|
||||||
Str("clockOffset", response.ClockOffset.String()).
|
Str("clockOffset", response.ClockOffset.String()).
|
||||||
Uint8("stratum", response.Stratum).
|
Uint8("stratum", response.Stratum).
|
||||||
Msg("NTP server returned time")
|
Msg("NTP server returned time")
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
results <- &ntpResult{
|
results <- &ntpResult{
|
||||||
now: now,
|
now: now,
|
||||||
offset: &response.ClockOffset,
|
offset: &response.ClockOffset,
|
||||||
|
|
|
@ -28,9 +28,8 @@ type TimeSync struct {
|
||||||
syncLock *sync.Mutex
|
syncLock *sync.Mutex
|
||||||
l *zerolog.Logger
|
l *zerolog.Logger
|
||||||
|
|
||||||
ntpServers []string
|
networkConfig *network.NetworkConfig
|
||||||
httpUrls []string
|
dhcpNtpAddresses []string
|
||||||
networkConfig *network.NetworkConfig
|
|
||||||
|
|
||||||
rtcDevicePath string
|
rtcDevicePath string
|
||||||
rtcDevice *os.File //nolint:unused
|
rtcDevice *os.File //nolint:unused
|
||||||
|
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &TimeSync{
|
t := &TimeSync{
|
||||||
syncLock: &sync.Mutex{},
|
syncLock: &sync.Mutex{},
|
||||||
l: opts.Logger,
|
l: opts.Logger,
|
||||||
rtcDevicePath: rtcDevice,
|
dhcpNtpAddresses: []string{},
|
||||||
rtcLock: &sync.Mutex{},
|
rtcDevicePath: rtcDevice,
|
||||||
preCheckFunc: opts.PreCheckFunc,
|
rtcLock: &sync.Mutex{},
|
||||||
ntpServers: defaultNTPServers,
|
preCheckFunc: opts.PreCheckFunc,
|
||||||
httpUrls: defaultHTTPUrls,
|
networkConfig: opts.NetworkConfig,
|
||||||
networkConfig: opts.NetworkConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.rtcDevicePath != "" {
|
if t.rtcDevicePath != "" {
|
||||||
|
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
|
||||||
|
t.dhcpNtpAddresses = addresses
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TimeSync) getSyncMode() SyncMode {
|
func (t *TimeSync) getSyncMode() SyncMode {
|
||||||
syncMode := SyncMode{
|
syncMode := SyncMode{
|
||||||
|
Ntp: true,
|
||||||
|
Http: true,
|
||||||
|
Ordering: []string{"ntp_dhcp", "ntp", "http"},
|
||||||
NtpUseFallback: true,
|
NtpUseFallback: true,
|
||||||
HttpUseFallback: true,
|
HttpUseFallback: true,
|
||||||
}
|
}
|
||||||
var syncModeString string
|
|
||||||
|
|
||||||
if t.networkConfig != nil {
|
if t.networkConfig != nil {
|
||||||
syncModeString = t.networkConfig.TimeSyncMode.String
|
switch t.networkConfig.TimeSyncMode.String {
|
||||||
|
case "ntp_only":
|
||||||
|
syncMode.Http = false
|
||||||
|
case "http_only":
|
||||||
|
syncMode.Ntp = false
|
||||||
|
}
|
||||||
|
|
||||||
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
||||||
syncMode.NtpUseFallback = false
|
syncMode.NtpUseFallback = false
|
||||||
syncMode.HttpUseFallback = false
|
syncMode.HttpUseFallback = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var syncOrdering = t.networkConfig.TimeSyncOrdering
|
||||||
|
if len(syncOrdering) > 0 {
|
||||||
|
syncMode.Ordering = syncOrdering
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch syncModeString {
|
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
|
||||||
case "ntp_only":
|
|
||||||
syncMode.Ntp = true
|
|
||||||
case "http_only":
|
|
||||||
syncMode.Http = true
|
|
||||||
default:
|
|
||||||
syncMode.Ntp = true
|
|
||||||
syncMode.Http = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncMode
|
return syncMode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TimeSync) doTimeSync() {
|
func (t *TimeSync) doTimeSync() {
|
||||||
metricTimeSyncStatus.Set(0)
|
metricTimeSyncStatus.Set(0)
|
||||||
for {
|
for {
|
||||||
|
@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
|
||||||
offset *time.Duration
|
offset *time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
syncMode := t.getSyncMode()
|
|
||||||
|
|
||||||
metricTimeSyncCount.Inc()
|
metricTimeSyncCount.Inc()
|
||||||
|
|
||||||
if syncMode.Ntp {
|
syncMode := t.getSyncMode()
|
||||||
now, offset = t.queryNetworkTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
if syncMode.Http && now == nil {
|
Orders:
|
||||||
now = t.queryAllHttpTime()
|
for _, mode := range syncMode.Ordering {
|
||||||
|
switch mode {
|
||||||
|
case "ntp_user_provided":
|
||||||
|
if syncMode.Ntp {
|
||||||
|
t.l.Info().Msg("using NTP custom servers")
|
||||||
|
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
|
||||||
|
if now != nil {
|
||||||
|
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
|
||||||
|
break Orders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ntp_dhcp":
|
||||||
|
if syncMode.Ntp {
|
||||||
|
t.l.Info().Msg("using NTP servers from DHCP")
|
||||||
|
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
|
||||||
|
if now != nil {
|
||||||
|
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
|
||||||
|
break Orders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ntp":
|
||||||
|
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||||
|
t.l.Info().Msg("using NTP fallback")
|
||||||
|
now, offset = t.queryNetworkTime(defaultNTPServers)
|
||||||
|
if now != nil {
|
||||||
|
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
|
||||||
|
break Orders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "http_user_provided":
|
||||||
|
if syncMode.Http {
|
||||||
|
t.l.Info().Msg("using HTTP custom URLs")
|
||||||
|
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
|
||||||
|
if now != nil {
|
||||||
|
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
|
||||||
|
break Orders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "http":
|
||||||
|
if syncMode.Http && syncMode.HttpUseFallback {
|
||||||
|
t.l.Info().Msg("using HTTP fallback")
|
||||||
|
now = t.queryAllHttpTime(defaultHTTPUrls)
|
||||||
|
if now != nil {
|
||||||
|
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
|
||||||
|
break Orders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if now == nil {
|
if now == nil {
|
||||||
|
|
|
@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
||||||
attrs: gadgetAttributes{
|
attrs: gadgetAttributes{
|
||||||
"bcdUSB": "0x0200", // USB 2.0
|
"bcdUSB": "0x0200", // USB 2.0
|
||||||
"idVendor": "0x1d6b", // The Linux Foundation
|
"idVendor": "0x1d6b", // The Linux Foundation
|
||||||
"idProduct": "0104", // Multifunction Composite Gadget
|
"idProduct": "0x0104", // Multifunction Composite Gadget
|
||||||
"bcdDevice": "0100",
|
"bcdDevice": "0x0100", // USB2
|
||||||
},
|
},
|
||||||
configAttrs: gadgetAttributes{
|
configAttrs: gadgetAttributes{
|
||||||
"MaxPower": "250", // in unit of 2mA
|
"MaxPower": "250", // in unit of 2mA
|
||||||
|
|
19
jsonrpc.go
19
jsonrpc.go
|
@ -681,10 +681,11 @@ func rpcResetConfig() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DCPowerState struct {
|
type DCPowerState struct {
|
||||||
IsOn bool `json:"isOn"`
|
IsOn bool `json:"isOn"`
|
||||||
Voltage float64 `json:"voltage"`
|
Voltage float64 `json:"voltage"`
|
||||||
Current float64 `json:"current"`
|
Current float64 `json:"current"`
|
||||||
Power float64 `json:"power"`
|
Power float64 `json:"power"`
|
||||||
|
RestoreState int `json:"restoreState"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetDCPowerState() (DCPowerState, error) {
|
func rpcGetDCPowerState() (DCPowerState, error) {
|
||||||
|
@ -700,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcSetDCRestoreState(state int) error {
|
||||||
|
logger.Info().Int("state", state).Msg("Setting DC restore state")
|
||||||
|
err := setDCRestoreState(state)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set DC restore state: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetActiveExtension() (string, error) {
|
func rpcGetActiveExtension() (string, error) {
|
||||||
return config.ActiveExtension, nil
|
return config.ActiveExtension, nil
|
||||||
}
|
}
|
||||||
|
@ -1088,6 +1098,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||||
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
||||||
|
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
|
||||||
"getActiveExtension": {Func: rpcGetActiveExtension},
|
"getActiveExtension": {Func: rpcGetActiveExtension},
|
||||||
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
||||||
"getATXState": {Func: rpcGetATXState},
|
"getATXState": {Func: rpcGetATXState},
|
||||||
|
|
|
@ -19,6 +19,14 @@ func networkStateChanged() {
|
||||||
// do not block the main thread
|
// do not block the main thread
|
||||||
go waitCtrlAndRequestDisplayUpdate(true)
|
go waitCtrlAndRequestDisplayUpdate(true)
|
||||||
|
|
||||||
|
if timeSync != nil {
|
||||||
|
if networkState != nil {
|
||||||
|
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSync.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
// always restart mDNS when the network state changes
|
// always restart mDNS when the network state changes
|
||||||
if mDNS != nil {
|
if mDNS != nil {
|
||||||
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||||
|
|
39
serial.go
39
serial.go
|
@ -142,6 +142,7 @@ var dcState DCPowerState
|
||||||
func runDCControl() {
|
func runDCControl() {
|
||||||
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
||||||
reader := bufio.NewReader(port)
|
reader := bufio.NewReader(port)
|
||||||
|
hasRestoreFeature := false
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -151,7 +152,13 @@ func runDCControl() {
|
||||||
|
|
||||||
// Split the line by semicolon
|
// Split the line by semicolon
|
||||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||||
if len(parts) != 4 {
|
if len(parts) == 5 {
|
||||||
|
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature")
|
||||||
|
hasRestoreFeature = true
|
||||||
|
} else if len(parts) == 4 {
|
||||||
|
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature")
|
||||||
|
hasRestoreFeature = false
|
||||||
|
} else {
|
||||||
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -163,6 +170,17 @@ func runDCControl() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dcState.IsOn = powerState == 1
|
dcState.IsOn = powerState == 1
|
||||||
|
if hasRestoreFeature {
|
||||||
|
restoreState, err := strconv.Atoi(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Invalid restore state")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dcState.RestoreState = restoreState
|
||||||
|
} else {
|
||||||
|
// -1 means not supported
|
||||||
|
dcState.RestoreState = -1
|
||||||
|
}
|
||||||
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
||||||
|
@ -210,6 +228,25 @@ func setDCPowerState(on bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDCRestoreState(state int) error {
|
||||||
|
_, err := port.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
command := "RESTORE_MODE_OFF\n"
|
||||||
|
switch state {
|
||||||
|
case 1:
|
||||||
|
command = "RESTORE_MODE_ON\n"
|
||||||
|
case 2:
|
||||||
|
command = "RESTORE_MODE_LAST_STATE\n"
|
||||||
|
}
|
||||||
|
_, err = port.Write([]byte(command))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var defaultMode = &serial.Mode{
|
var defaultMode = &serial.Mode{
|
||||||
BaudRate: 115200,
|
BaudRate: 115200,
|
||||||
DataBits: 8,
|
DataBits: 8,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,21 +19,21 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.3",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.2",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-unicode11": "^0.8.0",
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.3",
|
"cva": "^1.0.0-beta.4",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.3",
|
"focus-trap-react": "^11.0.4",
|
||||||
"framer-motion": "^12.11.4",
|
"framer-motion": "^12.23.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
@ -41,43 +41,43 @@
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^7.6.3",
|
||||||
"react-simple-keyboard": "^3.8.72",
|
"react-simple-keyboard": "^3.8.89",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.0",
|
"validator": "^13.15.15",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.9",
|
"@eslint/compat": "^1.3.1",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.30.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/validator": "^13.15.0",
|
"@types/validator": "^13.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.35.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.30.1",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.3.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
|
|
@ -657,6 +657,16 @@ export default function WebRTCVideo() {
|
||||||
return true;
|
return true;
|
||||||
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||||
|
|
||||||
|
// Conditionally set the filter style so we don't fallback to software rendering if these values are default of 1.0
|
||||||
|
const videoStyle = useMemo(() => {
|
||||||
|
const isDefault = videoSaturation === 1.0 && videoBrightness === 1.0 && videoContrast === 1.0;
|
||||||
|
return isDefault
|
||||||
|
? {} // No filter if all settings are default (1.0)
|
||||||
|
: {
|
||||||
|
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||||
|
};
|
||||||
|
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||||
<div className="flex min-h-[39.5px] flex-col">
|
<div className="flex min-h-[39.5px] flex-col">
|
||||||
|
@ -691,17 +701,15 @@ export default function WebRTCVideo() {
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
<video
|
<video
|
||||||
ref={videoElm}
|
ref={videoElm}
|
||||||
autoPlay={true}
|
autoPlay
|
||||||
controls={false}
|
controls={false}
|
||||||
onPlaying={onVideoPlaying}
|
onPlaying={onVideoPlaying}
|
||||||
onPlay={onVideoPlaying}
|
onPlay={onVideoPlaying}
|
||||||
muted={true}
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
controlsList="nofullscreen"
|
controlsList="nofullscreen"
|
||||||
style={{
|
style={videoStyle}
|
||||||
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
|
||||||
}}
|
|
||||||
className={cx(
|
className={cx(
|
||||||
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import FieldLabel from "@components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
|
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||||
|
|
||||||
interface DCPowerState {
|
interface DCPowerState {
|
||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
voltage: number;
|
voltage: number;
|
||||||
current: number;
|
current: number;
|
||||||
power: number;
|
power: number;
|
||||||
|
restoreState: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DCPowerControl() {
|
export function DCPowerControl() {
|
||||||
|
@ -43,6 +45,20 @@ export function DCPowerControl() {
|
||||||
getDCPowerState(); // Refresh state after change
|
getDCPowerState(); // Refresh state after change
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const handleRestoreChange = (state: number) => {
|
||||||
|
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||||
|
send("setDCRestoreState", { state }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getDCPowerState(); // Refresh state after change
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDCPowerState();
|
getDCPowerState();
|
||||||
|
@ -63,7 +79,7 @@ export function DCPowerControl() {
|
||||||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="h-[160px] animate-fadeIn opacity-0">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
{/* Power Controls */}
|
{/* Power Controls */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
@ -84,6 +100,21 @@ export function DCPowerControl() {
|
||||||
onClick={() => handlePowerToggle(false)}
|
onClick={() => handlePowerToggle(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{powerState.restoreState > -1 ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label="Restore Power Loss"
|
||||||
|
value={powerState.restoreState}
|
||||||
|
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||||
|
options={[
|
||||||
|
{ value: '0', label: "Power OFF" },
|
||||||
|
{ value: '1', label: "Power ON" },
|
||||||
|
{ value: '2', label: "Last State" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||||
|
|
||||||
{/* Status Display */}
|
{/* Status Display */}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
|
||||||
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
||||||
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
||||||
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
||||||
|
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
|
||||||
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
||||||
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
||||||
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
||||||
|
@ -140,6 +141,10 @@ if (isOnDevice) {
|
||||||
index: true,
|
index: true,
|
||||||
element: <SettingsGeneralIndexRoute.default />,
|
element: <SettingsGeneralIndexRoute.default />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "reboot",
|
||||||
|
element: <SettingsGeneralRebootRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "update",
|
path: "update",
|
||||||
element: <SettingsGeneralUpdateRoute />,
|
element: <SettingsGeneralUpdateRoute />,
|
||||||
|
|
|
@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||||
|
<SettingsItem
|
||||||
|
title="Reboot Device"
|
||||||
|
description="Power cycle the JetKVM"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Reboot Device"
|
||||||
|
onClick={() => navigateTo("./reboot")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
|
export default function SettingsGeneralRebootRoute() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const onConfirmUpdate = useCallback(() => {
|
||||||
|
// This is where we send the RPC to the golang binary
|
||||||
|
send("reboot", {force: true});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
{
|
||||||
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
|
}
|
||||||
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
onClose,
|
||||||
|
onConfirmUpdate,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirmUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-auto relative mx-auto text-left">
|
||||||
|
<div>
|
||||||
|
<ConfirmationBox
|
||||||
|
onYes={onConfirmUpdate}
|
||||||
|
onNo={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmationBox({
|
||||||
|
onYes,
|
||||||
|
onNo,
|
||||||
|
}: {
|
||||||
|
onYes: () => void;
|
||||||
|
onNo: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-base font-semibold text-black dark:text-white">
|
||||||
|
Reboot JetKVM
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
Do you want to proceed with rebooting the system?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-x-2">
|
||||||
|
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||||
|
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
18
web.go
18
web.go
|
@ -97,9 +97,6 @@ func setupRouter() *gin.Engine {
|
||||||
// We use this to determine if the device is setup
|
// We use this to determine if the device is setup
|
||||||
r.GET("/device/status", handleDeviceStatus)
|
r.GET("/device/status", handleDeviceStatus)
|
||||||
|
|
||||||
// We use this to provide the UI with the device configuration
|
|
||||||
r.GET("/device/ui-config.js", handleDeviceUIConfig)
|
|
||||||
|
|
||||||
// We use this to setup the device in the welcome page
|
// We use this to setup the device in the welcome page
|
||||||
r.POST("/device/setup", handleSetup)
|
r.POST("/device/setup", handleSetup)
|
||||||
|
|
||||||
|
@ -694,21 +691,6 @@ func handleCloudState(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeviceUIConfig(c *gin.Context) {
|
|
||||||
config, _ := json.Marshal(gin.H{
|
|
||||||
"CLOUD_API": config.CloudURL,
|
|
||||||
"DEVICE_VERSION": builtAppVersion,
|
|
||||||
})
|
|
||||||
if config == nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSetup(c *gin.Context) {
|
func handleSetup(c *gin.Context) {
|
||||||
// Check if the device is already set up
|
// Check if the device is already set up
|
||||||
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
||||||
|
|
Loading…
Reference in New Issue