Compare commits

..

11 Commits

Author SHA1 Message Date
Marc Brooks 0e1c49c2f6
Upgraded UI packages. 2025-10-30 04:04:42 -05:00
Marc Brooks c8129405bd
Add variables and MAC address to device screen
Also cleaned up all the scrolling and click/gesture handling so all the menus work correctly.
2025-10-30 04:04:41 -05:00
Marc Brooks 1138ffc210
Hide MAC and show hostname when IP address is available 2025-10-30 04:04:41 -05:00
Marc Brooks 0d7cfec3e9
feat/Add active hostname to device display
Also regularized up the layouts
2025-10-30 04:04:40 -05:00
Marc Brooks 4b049c4b7c
Add iputils-ping to install dependencies script (#917)
The new dev_deploy.sh uses ping to check if we can see the JetKVM, but mcr.microsoft.com/devcontainers/go:1.25-trixie does not have ping installed.
2025-10-30 09:53:32 +01:00
Marc Brooks 7955ee9d35
Improves OTA update reporting and process (#838) 2025-10-29 23:10:23 +01:00
Adam Shiervani 1ce63664c0
fix: video quality (#913) 2025-10-29 16:11:07 +01:00
Adam Shiervani 4b6e796a0e
fix: ensure proper redirection and page reload (#909) 2025-10-29 02:04:58 +01:00
Adam Shiervani 79098d3546
feat: Enhance DHCP client timeout and retry logic (#908) 2025-10-28 18:50:29 +01:00
Adam Shiervani 50fc88aae1
bug: fix null pointer in wakeDisplay (#907) 2025-10-28 18:48:30 +01:00
Adam Shiervani 204909b49a
feat: Add connectivity checks, ensure killing of jetkvm process, and disable SSH host key verification (#905) 2025-10-28 07:11:16 +01:00
39 changed files with 1351 additions and 3092 deletions

View File

@ -14,6 +14,7 @@ set -ex
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
sudo apt-get update && \ sudo apt-get update && \
sudo apt-get install -y --no-install-recommends \ sudo apt-get install -y --no-install-recommends \
iputils-ping \
build-essential \ build-essential \
device-tree-compiler \ device-tree-compiler \
gperf g++-multilib gcc-multilib \ gperf g++-multilib gcc-multilib \

View File

@ -178,6 +178,7 @@ func getDefaultConfig() Config {
return c return c
}(), }(),
DefaultLogLevel: "INFO", DefaultLogLevel: "INFO",
VideoQualityFactor: 1.0,
} }
} }

View File

@ -41,10 +41,16 @@ func switchToMainScreen() {
func updateDisplay() { func updateDisplay() {
if networkManager != nil { if networkManager != nil {
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv4_addr", networkManager.IPv4String()) ipv4 := networkManager.IPv4String()
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String()) nativeInstance.UISetVar("ip_v4_address", ipv4)
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString()) nativeInstance.ChangeVisibility("home_info_ipv4_addr", ipv4 != "")
nativeInstance.UpdateLabelIfChanged("home_info_hostname", networkManager.Hostname())
ipv6 := networkManager.IPv6String()
nativeInstance.UISetVar("ip_v6_address", ipv6)
nativeInstance.ChangeVisibility("home_info_ipv6_addr", ipv6 != "")
nativeInstance.UISetVar("mac_address", networkManager.MACString())
nativeInstance.UISetVar("hostname", networkManager.Hostname())
// we either show the MAC address (if no IP yet) or the hostname (if either IPv4 or IPv6 are available) // we either show the MAC address (if no IP yet) or the hostname (if either IPv4 or IPv6 are available)
hasIP := networkManager.IPv4Ready() || networkManager.IPv6Ready() hasIP := networkManager.IPv4Ready() || networkManager.IPv6Ready()
@ -210,7 +216,8 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
func updateStaticContents() { func updateStaticContents() {
//contents that never change //contents that never change
if networkManager != nil { if networkManager != nil {
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString()) mac := networkManager.MACString()
nativeInstance.UISetVar("mac_address", mac)
} }
// get cpu info // get cpu info
@ -236,7 +243,7 @@ func updateStaticContents() {
nativeInstance.UpdateLabelAndChangeVisibility("build_date", version.BuildDate) nativeInstance.UpdateLabelAndChangeVisibility("build_date", version.BuildDate)
nativeInstance.UpdateLabelAndChangeVisibility("golang_version", version.GoVersion) nativeInstance.UpdateLabelAndChangeVisibility("golang_version", version.GoVersion)
// nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID()) nativeInstance.UpdateLabelAndChangeVisibility("device_id", GetDeviceID())
} }
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter // setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
@ -312,11 +319,11 @@ func wakeDisplay(force bool, reason string) {
displayLogger.Warn().Err(err).Msg("failed to wake display") displayLogger.Warn().Err(err).Msg("failed to wake display")
} }
if config.DisplayDimAfterSec != 0 { if config.DisplayDimAfterSec != 0 && dimTicker != nil {
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second) dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
} }
if config.DisplayOffAfterSec != 0 { if config.DisplayOffAfterSec != 0 && offTicker != nil {
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second) offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
} }
backlightState = 0 backlightState = 0

View File

@ -368,7 +368,7 @@ void jetkvm_video_stop() {
} }
int jetkvm_video_set_quality_factor(float quality_factor) { int jetkvm_video_set_quality_factor(float quality_factor) {
if (quality_factor < 0 || quality_factor > 1) { if (quality_factor <= 0 || quality_factor > 1) {
return -1; return -1;
} }
video_set_quality_factor(quality_factor); video_set_quality_factor(quality_factor);

View File

@ -235,7 +235,7 @@ int video_init(float factor)
{ {
detect_sleep_mode(); detect_sleep_mode();
if (factor < 0 || factor > 1) { if (factor <= 0 || factor > 1) {
factor = 1.0f; factor = 1.0f;
} }
quality_factor = factor; quality_factor = factor;

View File

@ -98,6 +98,10 @@ func uiGetLVGLVersion() string {
return "" return ""
} }
func uiTick() {
panicPlatformNotSupported()
}
func videoGetStreamQualityFactor() (float64, error) { func videoGetStreamQualityFactor() (float64, error) {
panicPlatformNotSupported() panicPlatformNotSupported()
return 0, nil return 0, nil

View File

@ -109,6 +109,7 @@ func (n *Native) UpdateLabelIfChanged(objName string, newText string) {
if changed { if changed {
l.Msg("label changed") l.Msg("label changed")
uiTick()
} else { } else {
l.Msg("label not changed") l.Msg("label not changed")
} }
@ -130,6 +131,8 @@ func (n *Native) ChangeVisibility(objName string, show bool) {
_, _ = n.UIObjHide(objName) _, _ = n.UIObjHide(objName)
_, _ = n.UIObjHide(containerName) _, _ = n.UIObjHide(containerName)
} }
uiTick()
} }
// SwitchToScreenIf switches to the screen if the screen name is different from the current screen and the screen name is in the shouldSwitch list // SwitchToScreenIf switches to the screen if the screen name is different from the current screen and the screen name is in the shouldSwitch list

File diff suppressed because one or more lines are too long

View File

@ -56,6 +56,10 @@ void action_switch_to_reboot(lv_event_t *e) {
loadScreen(SCREEN_ID_REBOOT_SCREEN); loadScreen(SCREEN_ID_REBOOT_SCREEN);
} }
void action_switch_to_network(lv_event_t *e) {
loadScreen(SCREEN_ID_MENU_NETWORK_SCREEN);
}
void action_menu_screen_gesture(lv_event_t * e) { void action_menu_screen_gesture(lv_event_t * e) {
handle_gesture_main_screen_switch(e, LV_DIR_RIGHT); handle_gesture_main_screen_switch(e, LV_DIR_RIGHT);
} }
@ -76,6 +80,11 @@ void action_about_screen_gesture(lv_event_t * e) {
handle_gesture_screen_switch(e, LV_DIR_RIGHT, SCREEN_ID_MENU_SCREEN); handle_gesture_screen_switch(e, LV_DIR_RIGHT, SCREEN_ID_MENU_SCREEN);
} }
void action_status_screen_gesture(lv_event_t *e) {
handle_gesture_screen_switch(e, LV_DIR_RIGHT, SCREEN_ID_MENU_SCREEN);
}
// user_data doesn't seem to be working, so we use a global variable here // user_data doesn't seem to be working, so we use a global variable here
static uint32_t t_reset_config; static uint32_t t_reset_config;
static uint32_t t_reboot; static uint32_t t_reboot;
@ -168,9 +177,9 @@ void action_dhcpc(lv_event_t * e) {
.lock = &b_dhcpc_lock, .lock = &b_dhcpc_lock,
.hold_time_seconds = DHCPC_HOLD_TIME, .hold_time_seconds = DHCPC_HOLD_TIME,
.rpc_method = "toggleDHCPClient", .rpc_method = "toggleDHCPClient",
.button_obj = NULL, // No button/spinner for reboot .button_obj = NULL, // No button/spinner for dhcp client change
.spinner_obj = NULL, .spinner_obj = NULL,
.label_obj = objects.dhcpc_label, .label_obj = objects.dhcp_client_label,
.default_text = "Press and hold for\n5 seconds" .default_text = "Press and hold for\n5 seconds"
}; };

View File

@ -26,6 +26,8 @@ extern void action_reboot(lv_event_t * e);
extern void action_switch_to_reboot(lv_event_t * e); extern void action_switch_to_reboot(lv_event_t * e);
extern void action_dhcpc(lv_event_t * e); extern void action_dhcpc(lv_event_t * e);
extern void action_switch_to_dhcpc(lv_event_t * e); extern void action_switch_to_dhcpc(lv_event_t * e);
extern void action_status_screen_gesture(lv_event_t * e);
extern void action_switch_to_network(lv_event_t * e);
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -8,7 +8,6 @@ extern "C" {
#endif #endif
extern const lv_font_t ui_font_font_bold30; extern const lv_font_t ui_font_font_bold30;
extern const lv_font_t ui_font_font_bold24;
extern const lv_font_t ui_font_font_book16; extern const lv_font_t ui_font_font_book16;
extern const lv_font_t ui_font_font_book18; extern const lv_font_t ui_font_font_book18;
extern const lv_font_t ui_font_font_book20; extern const lv_font_t ui_font_font_book20;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ typedef struct _objects_t {
lv_obj_t *no_network_header_logo; lv_obj_t *no_network_header_logo;
lv_obj_t *no_network_content_container; lv_obj_t *no_network_content_container;
lv_obj_t *no_network_title; lv_obj_t *no_network_title;
lv_obj_t *home_info_ipv6_addr_1; lv_obj_t *no_network_connect_cable;
lv_obj_t *home_header_container; lv_obj_t *home_header_container;
lv_obj_t *home_header_logo; lv_obj_t *home_header_logo;
lv_obj_t *cloud_status_icon; lv_obj_t *cloud_status_icon;
@ -51,15 +51,15 @@ typedef struct _objects_t {
lv_obj_t *menu_btn_access; lv_obj_t *menu_btn_access;
lv_obj_t *menu_btn_advanced; lv_obj_t *menu_btn_advanced;
lv_obj_t *menu_btn_about; lv_obj_t *menu_btn_about;
lv_obj_t *menu_header_container_1; lv_obj_t *menu_advanced_header_container;
lv_obj_t *menu_items_container_1; lv_obj_t *menu_advanced_items_container;
lv_obj_t *menu_btn_advanced_developer_mode; lv_obj_t *menu_btn_advanced_developer_mode;
lv_obj_t *menu_btn_advanced_usb_emulation; lv_obj_t *menu_btn_advanced_usb_emulation;
lv_obj_t *menu_btn_advanced_reboot; lv_obj_t *menu_btn_advanced_reboot;
lv_obj_t *menu_btn_dhcp_client; lv_obj_t *menu_btn_dhcp_client;
lv_obj_t *menu_btn_advanced_reset_config; lv_obj_t *menu_btn_advanced_reset_config;
lv_obj_t *menu_header_container_2; lv_obj_t *menu_network_header_container;
lv_obj_t *menu_items_container_2; lv_obj_t *menu_network_items_container;
lv_obj_t *menu_btn_network_ipv4; lv_obj_t *menu_btn_network_ipv4;
lv_obj_t *menu_btn_network_ipv6; lv_obj_t *menu_btn_network_ipv6;
lv_obj_t *menu_btn_network_lldp; lv_obj_t *menu_btn_network_lldp;
@ -85,32 +85,39 @@ typedef struct _objects_t {
lv_obj_t *status_items_container; lv_obj_t *status_items_container;
lv_obj_t *device_id_container; lv_obj_t *device_id_container;
lv_obj_t *device_id; lv_obj_t *device_id;
lv_obj_t *device_mac_address_container;
lv_obj_t *device_mac_address;
lv_obj_t *cloud_account_id_container; lv_obj_t *cloud_account_id_container;
lv_obj_t *app_version_1; lv_obj_t *cloud_account_id;
lv_obj_t *cloud_domain_container; lv_obj_t *cloud_domain_container;
lv_obj_t *cloud_domain; lv_obj_t *cloud_domain;
lv_obj_t *reset_config_header; lv_obj_t *reset_config_header;
lv_obj_t *reset_config_container; lv_obj_t *reset_config_container;
lv_obj_t *reset_config_label_container; lv_obj_t *reset_config_label_container;
lv_obj_t *reset_config_label; lv_obj_t *reset_config_label;
lv_obj_t *reset_config_spinner_container;
lv_obj_t *reset_config_spinner; lv_obj_t *reset_config_spinner;
lv_obj_t *reset_config_button_container;
lv_obj_t *reset_config_button; lv_obj_t *reset_config_button;
lv_obj_t *obj0; lv_obj_t *reset_config_button_label;
lv_obj_t *reboot_header; lv_obj_t *reboot_header;
lv_obj_t *reboot_container; lv_obj_t *reboot_container;
lv_obj_t *reboot_label_container; lv_obj_t *reboot_label_container;
lv_obj_t *reboot_label; lv_obj_t *reboot_label;
lv_obj_t *reboot_config_button; lv_obj_t *reboot_device_button_container;
lv_obj_t *obj1; lv_obj_t *reboot_device_button;
lv_obj_t *reboot_device_button_label;
lv_obj_t *reboot_in_progress_container;
lv_obj_t *reboot_in_progress_logo; lv_obj_t *reboot_in_progress_logo;
lv_obj_t *reboot_in_progress_label; lv_obj_t *reboot_in_progress_label;
lv_obj_t *dhcp_client_header; lv_obj_t *dhcp_client_header;
lv_obj_t *dhcp_client_container; lv_obj_t *dhcp_client_container;
lv_obj_t *dhcp_client_label_container; lv_obj_t *dhcp_client_label_container;
lv_obj_t *dhcpc_label; lv_obj_t *dhcp_client_label;
lv_obj_t *dhcp_client_spinner_container;
lv_obj_t *dhcp_client_spinner; lv_obj_t *dhcp_client_spinner;
lv_obj_t *dhcp_client_button; lv_obj_t *dhcp_client_change_button_container;
lv_obj_t *obj2; lv_obj_t *dhcp_client_change_button;
lv_obj_t *dhcp_client_change_label; lv_obj_t *dhcp_client_change_label;
} objects_t; } objects_t;

View File

@ -170,11 +170,9 @@ void remove_style_flow_row_start_center(lv_obj_t *obj) {
void init_style_flex_column_start_MAIN_DEFAULT(lv_style_t *style) { void init_style_flex_column_start_MAIN_DEFAULT(lv_style_t *style) {
init_style_flow_row_space_between_MAIN_DEFAULT(style); init_style_flow_row_space_between_MAIN_DEFAULT(style);
lv_style_set_layout(style, LV_LAYOUT_FLEX);
lv_style_set_flex_flow(style, LV_FLEX_FLOW_COLUMN);
lv_style_set_flex_track_place(style, LV_FLEX_ALIGN_START);
lv_style_set_flex_cross_place(style, LV_FLEX_ALIGN_START); lv_style_set_flex_cross_place(style, LV_FLEX_ALIGN_START);
lv_style_set_flex_main_place(style, LV_FLEX_ALIGN_SPACE_EVENLY); lv_style_set_flex_main_place(style, LV_FLEX_ALIGN_SPACE_EVENLY);
lv_style_set_flex_flow(style, LV_FLEX_FLOW_COLUMN);
}; };
lv_style_t *get_style_flex_column_start_MAIN_DEFAULT() { lv_style_t *get_style_flex_column_start_MAIN_DEFAULT() {
@ -294,37 +292,6 @@ void remove_style_label_font16(lv_obj_t *obj) {
lv_obj_remove_style(obj, get_style_label_font16_MAIN_DEFAULT(), LV_PART_MAIN | LV_STATE_DEFAULT); lv_obj_remove_style(obj, get_style_label_font16_MAIN_DEFAULT(), LV_PART_MAIN | LV_STATE_DEFAULT);
}; };
//
// Style: LabelFontBold24
//
void init_style_label_font_bold24_MAIN_DEFAULT(lv_style_t *style) {
init_style_label_font16_MAIN_DEFAULT(style);
lv_style_set_text_font(style, &ui_font_font_bold24);
lv_style_set_length(style, 0);
};
lv_style_t *get_style_label_font_bold24_MAIN_DEFAULT() {
static lv_style_t *style;
if (!style) {
style = lv_malloc(sizeof(lv_style_t));
lv_style_init(style);
init_style_label_font_bold24_MAIN_DEFAULT(style);
}
return style;
};
void add_style_label_font_bold24(lv_obj_t *obj) {
(void)obj;
lv_obj_add_style(obj, get_style_label_font_bold24_MAIN_DEFAULT(), LV_PART_MAIN | LV_STATE_DEFAULT);
};
void remove_style_label_font_bold24(lv_obj_t *obj) {
(void)obj;
lv_obj_remove_style(obj, get_style_label_font_bold24_MAIN_DEFAULT(), LV_PART_MAIN | LV_STATE_DEFAULT);
};
// //
// Style: LabelFontBold30 // Style: LabelFontBold30
// //
@ -558,7 +525,6 @@ void add_style(lv_obj_t *obj, int32_t styleIndex) {
add_style_flex_screen, add_style_flex_screen,
add_style_flex_screen_menu, add_style_flex_screen_menu,
add_style_label_font16, add_style_label_font16,
add_style_label_font_bold24,
add_style_label_font_bold30, add_style_label_font_bold30,
add_style_header_link, add_style_header_link,
add_style_menu_button, add_style_menu_button,
@ -582,7 +548,6 @@ void remove_style(lv_obj_t *obj, int32_t styleIndex) {
remove_style_flex_screen, remove_style_flex_screen,
remove_style_flex_screen_menu, remove_style_flex_screen_menu,
remove_style_label_font16, remove_style_label_font16,
remove_style_label_font_bold24,
remove_style_label_font_bold30, remove_style_label_font_bold30,
remove_style_header_link, remove_style_header_link,
remove_style_menu_button, remove_style_menu_button,

View File

@ -52,11 +52,6 @@ lv_style_t *get_style_label_font16_MAIN_DEFAULT();
void add_style_label_font16(lv_obj_t *obj); void add_style_label_font16(lv_obj_t *obj);
void remove_style_label_font16(lv_obj_t *obj); void remove_style_label_font16(lv_obj_t *obj);
// Style: LabelFontBold24
lv_style_t *get_style_label_font_bold24_MAIN_DEFAULT();
void add_style_label_font_bold24(lv_obj_t *obj);
void remove_style_label_font_bold24(lv_obj_t *obj);
// Style: LabelFontBold30 // Style: LabelFontBold30
lv_style_t *get_style_label_font_bold30_MAIN_DEFAULT(); lv_style_t *get_style_label_font_bold30_MAIN_DEFAULT();
void add_style_label_font_bold30(lv_obj_t *obj); void add_style_label_font_bold30(lv_obj_t *obj);

View File

@ -10,8 +10,6 @@ void ui_call_rpc_handler(const char *method, const char *params);
#if defined(EEZ_FOR_LVGL) #if defined(EEZ_FOR_LVGL)
#include <eez/flow/lvgl_api.h> #include <eez/flow/lvgl_api.h>
#endif #endif

View File

@ -7,15 +7,79 @@ char app_version[100] = { 0 };
char system_version[100] = { 0 }; char system_version[100] = { 0 };
char lvgl_version[32] = { 0 }; char lvgl_version[32] = { 0 };
char main_screen[32] = "home_screen"; char main_screen[32] = "home_screen";
char mac_address[18] = { 0 };
char ip_v4_address[22] = { 0 };
char ip_v6_address[46] = { 0 };
char hostname[262] = { 0 };
const char *get_var_ip_v4_address() {
return ip_v4_address;
}
void set_var_ip_v4_address(const char *value) {
strncpy(ip_v4_address, value, sizeof(ip_v4_address) / sizeof(char));
ip_v4_address[sizeof(ip_v4_address) / sizeof(char) - 1] = 0;
tick_screen_home_screen();
}
const char *get_var_ip_v6_address() {
return ip_v6_address;
}
void set_var_ip_v6_address(const char *value) {
strncpy(ip_v6_address, value, sizeof(ip_v6_address) / sizeof(char));
ip_v6_address[sizeof(ip_v6_address) / sizeof(char) - 1] = 0;
tick_screen_home_screen();
}
const char *get_var_mac_address() {
return mac_address;
}
void set_var_mac_address(const char *value) {
strncpy(mac_address, value, sizeof(mac_address) / sizeof(char));
mac_address[sizeof(mac_address) / sizeof(char) - 1] = 0;
tick_screen_home_screen();
tick_screen_status_screen();
}
const char *get_var_hostname() {
return hostname;
}
void set_var_hostname(const char *value) {
strncpy(hostname, value, sizeof(hostname) / sizeof(char));
hostname[sizeof(hostname) / sizeof(char) - 1] = 0;
tick_screen_home_screen();
}
const char *get_var_app_version() { const char *get_var_app_version() {
return app_version; return app_version;
} }
void set_var_app_version(const char *value) {
strncpy(app_version, value, sizeof(app_version) / sizeof(char));
app_version[sizeof(app_version) / sizeof(char) - 1] = 0;
tick_screen_boot_screen();
tick_screen_about_screen();
}
const char *get_var_system_version() { const char *get_var_system_version() {
return system_version; return system_version;
} }
void set_var_system_version(const char *value) {
strncpy(system_version, value, sizeof(system_version) / sizeof(char));
system_version[sizeof(system_version) / sizeof(char) - 1] = 0;
tick_screen_about_screen();
}
const char *get_var_lvgl_version() { const char *get_var_lvgl_version() {
if (lvgl_version[0] == '\0') { if (lvgl_version[0] == '\0') {
char buf[32]; char buf[32];
@ -28,23 +92,17 @@ const char *get_var_lvgl_version() {
return lvgl_version; return lvgl_version;
} }
void set_var_app_version(const char *value) { void set_var_lvgl_version(const char *value) {
strncpy(app_version, value, sizeof(app_version) / sizeof(char)); // intentional NOP since this is actually generated
app_version[sizeof(app_version) / sizeof(char) - 1] = 0;
}
void set_var_system_version(const char *value) { tick_screen_about_screen();
strncpy(system_version, value, sizeof(system_version) / sizeof(char));
system_version[sizeof(system_version) / sizeof(char) - 1] = 0;
}
void set_var_lvgl_version(const char *value) {}
void set_var_main_screen(const char *value) {
strncpy(main_screen, value, sizeof(main_screen) / sizeof(char));
main_screen[sizeof(main_screen) / sizeof(char) - 1] = 0;
} }
const char *get_var_main_screen() { const char *get_var_main_screen() {
return main_screen; return main_screen;
} }
void set_var_main_screen(const char *value) {
strncpy(main_screen, value, sizeof(main_screen) / sizeof(char));
main_screen[sizeof(main_screen) / sizeof(char) - 1] = 0;
}

View File

@ -4,6 +4,11 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
void tick_screen_home_screen();
void tick_screen_status_screen();
void tick_screen_boot_screen();
void tick_screen_about_screen();
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -18,7 +23,11 @@ enum FlowGlobalVariables {
FLOW_GLOBAL_VARIABLE_APP_VERSION = 0, FLOW_GLOBAL_VARIABLE_APP_VERSION = 0,
FLOW_GLOBAL_VARIABLE_SYSTEM_VERSION = 1, FLOW_GLOBAL_VARIABLE_SYSTEM_VERSION = 1,
FLOW_GLOBAL_VARIABLE_LVGL_VERSION = 2, FLOW_GLOBAL_VARIABLE_LVGL_VERSION = 2,
FLOW_GLOBAL_VARIABLE_MAIN_SCREEN = 3 FLOW_GLOBAL_VARIABLE_MAIN_SCREEN = 3,
FLOW_GLOBAL_VARIABLE_MAC_ADDRESS = 4,
FLOW_GLOBAL_VARIABLE_IP_V6_ADDRESS = 5,
FLOW_GLOBAL_VARIABLE_IP_V4_ADDRESS = 6,
FLOW_GLOBAL_VARIABLE_HOSTNAME = 7
}; };
// Native global variables // Native global variables
@ -31,6 +40,14 @@ extern const char *get_var_lvgl_version();
extern void set_var_lvgl_version(const char *value); extern void set_var_lvgl_version(const char *value);
extern const char *get_var_main_screen(); extern const char *get_var_main_screen();
extern void set_var_main_screen(const char *value); extern void set_var_main_screen(const char *value);
extern const char *get_var_mac_address();
extern void set_var_mac_address(const char *value);
extern const char *get_var_ip_v6_address();
extern void set_var_ip_v6_address(const char *value);
extern const char *get_var_ip_v4_address();
extern void set_var_ip_v4_address(const char *value);
extern const char *get_var_hostname();
extern void set_var_hostname(const char *value);
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -69,7 +69,7 @@ func NewNative(opts NativeOptions) *Native {
sleepModeSupported := isSleepModeSupported() sleepModeSupported := isSleepModeSupported()
defaultQualityFactor := opts.DefaultQualityFactor defaultQualityFactor := opts.DefaultQualityFactor
if defaultQualityFactor < 0 || defaultQualityFactor > 1 { if defaultQualityFactor <= 0 || defaultQualityFactor > 1 {
defaultQualityFactor = 1.0 defaultQualityFactor = 1.0
} }

View File

@ -177,10 +177,8 @@ func rpcReboot(force bool) error {
return hwReboot(force, nil, 0) return hwReboot(force, nil, 0)
} }
var streamFactor = 1.0
func rpcGetStreamQualityFactor() (float64, error) { func rpcGetStreamQualityFactor() (float64, error) {
return streamFactor, nil return config.VideoQualityFactor, nil
} }
func rpcSetStreamQualityFactor(factor float64) error { func rpcSetStreamQualityFactor(factor float64) error {
@ -190,7 +188,10 @@ func rpcSetStreamQualityFactor(factor float64) error {
return err return err
} }
streamFactor = factor config.VideoQualityFactor = factor
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil return nil
} }

27
main.go
View File

@ -14,6 +14,7 @@ import (
var appCtx context.Context var appCtx context.Context
func Main() { func Main() {
logger.Log().Msg("JetKVM Starting Up")
LoadConfig() LoadConfig()
var cancel context.CancelFunc var cancel context.CancelFunc
@ -79,16 +80,16 @@ func Main() {
startVideoSleepModeTicker() startVideoSleepModeTicker()
go func() { go func() {
// wait for 15 minutes before starting auto-update checks
// this is to avoid interfering with initial setup processes
// and to ensure the system is stable before checking for updates
time.Sleep(15 * time.Minute) time.Sleep(15 * time.Minute)
for {
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
if !config.AutoUpdateEnabled {
return
}
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { for {
logger.Debug().Msg("system time is not synced, will retry in 30 seconds") logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
time.Sleep(30 * time.Second) if !config.AutoUpdateEnabled {
logger.Debug().Msg("auto-update disabled")
time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
continue continue
} }
@ -98,6 +99,12 @@ func Main() {
continue continue
} }
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
time.Sleep(30 * time.Second)
continue
}
includePreRelease := config.IncludePreRelease includePreRelease := config.IncludePreRelease
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
if err != nil { if err != nil {
@ -107,6 +114,7 @@ func Main() {
time.Sleep(1 * time.Hour) time.Sleep(1 * time.Hour)
} }
}() }()
//go RunFuseServer() //go RunFuseServer()
go RunWebServer() go RunWebServer()
@ -123,7 +131,8 @@ func Main() {
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs <-sigs
logger.Info().Msg("JetKVM Shutting Down")
logger.Log().Msg("JetKVM Shutting Down")
//if fuseServer != nil { //if fuseServer != nil {
// err := setMassStorageImage(" ") // err := setMassStorageImage(" ")
// if err != nil { // if err != nil {

40
ota.go
View File

@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
if nr > 0 { if nr > 0 {
nw, ew := file.Write(buf[0:nr]) nw, ew := file.Write(buf[0:nr])
if nw < nr { if nw < nr {
return fmt.Errorf("short write: %d < %d", nw, nr) return fmt.Errorf("short file write: %d < %d", nw, nr)
} }
written += int64(nw) written += int64(nw)
if ew != nil { if ew != nil {
@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
if nr > 0 { if nr > 0 {
nw, ew := hash.Write(buf[0:nr]) nw, ew := hash.Write(buf[0:nr])
if nw < nr { if nw < nr {
return fmt.Errorf("short write: %d < %d", nw, nr) return fmt.Errorf("short hash write: %d < %d", nw, nr)
} }
verified += int64(nw) verified += int64(nw)
if ew != nil { if ew != nil {
@ -260,11 +260,16 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
} }
} }
hashSum := hash.Sum(nil) // close the file so we can rename below
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") if err := fileToHash.Close(); err != nil {
return fmt.Errorf("error closing file: %w", err)
}
if hex.EncodeToString(hashSum) != expectedHash { hashSum := hex.EncodeToString(hash.Sum(nil))
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of")
if hashSum != expectedHash {
return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash)
} }
if err := os.Rename(unverifiedPath, path); err != nil { if err := os.Rename(unverifiedPath, path); err != nil {
@ -313,7 +318,7 @@ func triggerOTAStateUpdate() {
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
scopedLogger := otaLogger.With(). scopedLogger := otaLogger.With().
Str("deviceId", deviceId). Str("deviceId", deviceId).
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)). Bool("includePreRelease", includePreRelease).
Logger() Logger()
scopedLogger.Info().Msg("Trying to update...") scopedLogger.Info().Msg("Trying to update...")
@ -362,8 +367,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading app update") scopedLogger.Error().Err(err).Msg("Error downloading app update")
triggerOTAStateUpdate() triggerOTAStateUpdate()
return err return fmt.Errorf("error downloading app update: %w", err)
} }
downloadFinished := time.Now() downloadFinished := time.Now()
otaState.AppDownloadFinishedAt = &downloadFinished otaState.AppDownloadFinishedAt = &downloadFinished
otaState.AppDownloadProgress = 1 otaState.AppDownloadProgress = 1
@ -379,17 +385,21 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying app update hash") scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
triggerOTAStateUpdate() triggerOTAStateUpdate()
return err return fmt.Errorf("error verifying app update: %w", err)
} }
verifyFinished := time.Now() verifyFinished := time.Now()
otaState.AppVerifiedAt = &verifyFinished otaState.AppVerifiedAt = &verifyFinished
otaState.AppVerificationProgress = 1 otaState.AppVerificationProgress = 1
triggerOTAStateUpdate()
otaState.AppUpdatedAt = &verifyFinished otaState.AppUpdatedAt = &verifyFinished
otaState.AppUpdateProgress = 1 otaState.AppUpdateProgress = 1
triggerOTAStateUpdate() triggerOTAStateUpdate()
scopedLogger.Info().Msg("App update downloaded") scopedLogger.Info().Msg("App update downloaded")
rebootNeeded = true rebootNeeded = true
triggerOTAStateUpdate()
} else { } else {
scopedLogger.Info().Msg("App is up to date") scopedLogger.Info().Msg("App is up to date")
} }
@ -405,8 +415,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading system update") scopedLogger.Error().Err(err).Msg("Error downloading system update")
triggerOTAStateUpdate() triggerOTAStateUpdate()
return err return fmt.Errorf("error downloading system update: %w", err)
} }
downloadFinished := time.Now() downloadFinished := time.Now()
otaState.SystemDownloadFinishedAt = &downloadFinished otaState.SystemDownloadFinishedAt = &downloadFinished
otaState.SystemDownloadProgress = 1 otaState.SystemDownloadProgress = 1
@ -422,8 +433,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying system update hash") scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
triggerOTAStateUpdate() triggerOTAStateUpdate()
return err return fmt.Errorf("error verifying system update: %w", err)
} }
scopedLogger.Info().Msg("System update downloaded") scopedLogger.Info().Msg("System update downloaded")
verifyFinished := time.Now() verifyFinished := time.Now()
otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerifiedAt = &verifyFinished
@ -439,8 +451,10 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err) otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
triggerOTAStateUpdate()
return fmt.Errorf("error starting rk_ota command: %w", err) return fmt.Errorf("error starting rk_ota command: %w", err)
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -475,13 +489,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
Str("output", output). Str("output", output).
Int("exitCode", cmd.ProcessState.ExitCode()). Int("exitCode", cmd.ProcessState.ExitCode()).
Msg("Error executing rk_ota command") Msg("Error executing rk_ota command")
triggerOTAStateUpdate()
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
} }
scopedLogger.Info().Str("output", output).Msg("rk_ota success") scopedLogger.Info().Str("output", output).Msg("rk_ota success")
otaState.SystemUpdateProgress = 1 otaState.SystemUpdateProgress = 1
otaState.SystemUpdatedAt = &verifyFinished otaState.SystemUpdatedAt = &verifyFinished
triggerOTAStateUpdate()
rebootNeeded = true rebootNeeded = true
triggerOTAStateUpdate()
} else { } else {
scopedLogger.Info().Msg("System is up to date") scopedLogger.Info().Msg("System is up to date")
} }

View File

@ -111,6 +111,7 @@ type Client struct {
var ( var (
defaultTimerDuration = 1 * time.Second defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second defaultLinkUpTimeout = 30 * time.Second
defaultDHCPTimeout = 5 * time.Second // DHCP request timeout (not link up timeout)
maxRenewalAttemptDuration = 2 * time.Hour maxRenewalAttemptDuration = 2 * time.Hour
) )
@ -125,11 +126,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
} }
if cfg.Timeout == 0 { if cfg.Timeout == 0 {
cfg.Timeout = defaultLinkUpTimeout cfg.Timeout = defaultDHCPTimeout
} }
if cfg.Retries == 0 { if cfg.Retries == 0 {
cfg.Retries = 3 cfg.Retries = 4
} }
return &Client{ return &Client{
@ -153,9 +154,15 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
}, nil }, nil
} }
func resetTimer(t *time.Timer, l *zerolog.Logger) { func resetTimer(t *time.Timer, attempt int, l *zerolog.Logger) {
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later") // Exponential backoff: 1s, 2s, 4s, 8s, max 8s
t.Reset(defaultTimerDuration) backoffAttempt := attempt
if backoffAttempt > 3 {
backoffAttempt = 3
}
delay := time.Duration(1<<backoffAttempt) * time.Second
l.Debug().Dur("delay", delay).Int("attempt", attempt).Msg("will retry later")
t.Reset(delay)
} }
func getRenewalTime(lease *Lease) time.Duration { func getRenewalTime(lease *Lease) time.Duration {
@ -168,12 +175,14 @@ func getRenewalTime(lease *Lease) time.Duration {
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) { func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
l := c.l.With().Str("interface", ifname).Int("family", family).Logger() l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
attempt := 0
for range t.C { for range t.C {
l.Info().Msg("requesting lease") l.Info().Int("attempt", attempt).Msg("requesting lease")
if _, err := c.ensureInterfaceUp(ifname); err != nil { if _, err := c.ensureInterfaceUp(ifname); err != nil {
l.Error().Err(err).Msg("failed to ensure interface up") l.Error().Err(err).Int("attempt", attempt).Msg("failed to ensure interface up")
resetTimer(t, c.l) resetTimer(t, attempt, c.l)
attempt++
continue continue
} }
@ -188,11 +197,14 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
lease, err = c.requestLease6(ifname) lease, err = c.requestLease6(ifname)
} }
if err != nil { if err != nil {
l.Error().Err(err).Msg("failed to request lease") l.Error().Err(err).Int("attempt", attempt).Msg("failed to request lease")
resetTimer(t, c.l) resetTimer(t, attempt, c.l)
attempt++
continue continue
} }
// Successfully obtained lease, reset attempt counter
attempt = 0
c.handleLeaseChange(lease) c.handleLeaseChange(lease)
nextRenewal := getRenewalTime(lease) nextRenewal := getRenewalTime(lease)

View File

@ -26,6 +26,31 @@ show_help() {
echo " $0 -r 192.168.0.17 -u admin" echo " $0 -r 192.168.0.17 -u admin"
} }
# Function to check if device is pingable
check_ping() {
local host=$1
msg_info "▶ Checking if device is reachable at ${host}..."
if ! ping -c 3 -W 5 "${host}" > /dev/null 2>&1; then
msg_err "Error: Cannot reach device at ${host}"
msg_err "Please verify the IP address and network connectivity"
exit 1
fi
msg_info "✓ Device is reachable"
}
# Function to check if SSH is accessible
check_ssh() {
local user=$1
local host=$2
msg_info "▶ Checking SSH connectivity to ${user}@${host}..."
if ! ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${host}" "echo 'SSH connection successful'" > /dev/null 2>&1; then
msg_err "Error: Cannot establish SSH connection to ${user}@${host}"
msg_err "Please verify SSH access and credentials"
exit 1
fi
msg_info "✓ SSH connection successful"
}
# Default values # Default values
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
REMOTE_USER="root" REMOTE_USER="root"
@ -113,6 +138,10 @@ if [ -z "$REMOTE_HOST" ]; then
exit 1 exit 1
fi fi
# Check device connectivity before proceeding
check_ping "${REMOTE_HOST}"
check_ssh "${REMOTE_USER}" "${REMOTE_HOST}"
# check if the current CPU architecture is x86_64 # check if the current CPU architecture is x86_64
if [ "$(uname -m)" != "x86_64" ]; then if [ "$(uname -m)" != "x86_64" ]; then
msg_warn "Warning: This script is only supported on x86_64 architecture" msg_warn "Warning: This script is only supported on x86_64 architecture"
@ -147,10 +176,10 @@ if [ "$RUN_GO_TESTS" = true ]; then
make build_dev_test make build_dev_test
msg_info "▶ Copying device-tests.tar.gz to remote host" msg_info "▶ Copying device-tests.tar.gz to remote host"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
msg_info "▶ Running go tests" msg_info "▶ Running go tests"
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
set -e set -e
TMP_DIR=$(mktemp -d) TMP_DIR=$(mktemp -d)
cd ${TMP_DIR} cd ${TMP_DIR}
@ -193,10 +222,10 @@ then
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Copy the binary to the remote host as if we were the OTA updater. # Copy the binary to the remote host as if we were the OTA updater.
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
# Reboot the device, the new app will be deployed by the startup process. # Reboot the device, the new app will be deployed by the startup process.
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
else else
msg_info "▶ Building development binary" msg_info "▶ Building development binary"
do_make build_dev \ do_make build_dev \
@ -205,21 +234,21 @@ else
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Kill any existing instances of the application # Kill any existing instances of the application
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
# Copy the binary to the remote host # Copy the binary to the remote host
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
if [ "$RESET_USB_HID_DEVICE" = true ]; then if [ "$RESET_USB_HID_DEVICE" = true ]; then
msg_info "▶ Resetting USB HID device" msg_info "▶ Resetting USB HID device"
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
# Remove the old USB gadget configuration # Remove the old USB gadget configuration
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
fi fi
# Deploy and run the application on the remote host # Deploy and run the application on the remote host
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
set -e set -e
# Set the library path to include the directory where librockit.so is located # Set the library path to include the directory where librockit.so is located
@ -229,6 +258,17 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
killall jetkvm_app || true killall jetkvm_app || true
killall jetkvm_app_debug || true killall jetkvm_app_debug || true
# Wait until both binaries are killed, max 10 seconds
i=1
while [ \$i -le 10 ]; do
echo "Waiting for jetkvm_app and jetkvm_app_debug to be killed, \$i/10 ..."
if ! pgrep -f "jetkvm_app" > /dev/null && ! pgrep -f "jetkvm_app_debug" > /dev/null; then
break
fi
sleep 1
i=\$((i + 1))
done
# Navigate to the directory where the binary will be stored # Navigate to the directory where the binary will be stored
cd "${REMOTE_PATH}" cd "${REMOTE_PATH}"

236
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.24.2140", "version": "2025.10.30.0830",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.24.2140", "version": "2025.10.30.0830",
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2", "@headlessui/tailwindcss": "^0.2.2",
@ -31,18 +31,18 @@
"react-hook-form": "^7.65.0", "react-hook-form": "^7.65.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.5",
"react-simple-keyboard": "^3.8.131", "react-simple-keyboard": "^3.8.132",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.3.0", "recharts": "^3.3.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.20",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.1",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.38.0",
"@inlang/cli": "^3.0.12", "@inlang/cli": "^3.0.12",
@ -57,7 +57,7 @@
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.4",
"@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.2.0", "@vitejs/plugin-react-swc": "^4.2.0",
@ -126,6 +126,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -799,13 +800,13 @@
} }
}, },
"node_modules/@eslint/compat": { "node_modules/@eslint/compat": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
"integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==", "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.16.0" "@eslint/core": "^0.17.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -834,21 +835,21 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.4.1", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.16.0" "@eslint/core": "^0.17.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.16.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
@ -914,12 +915,12 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.4.0", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.16.0", "@eslint/core": "^0.17.0",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@ -1728,9 +1729,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz",
"integrity": "sha512-umBaSb65O1v6Lt8RV3o5srw0nKr25amf/yRIGFPug63sAerL9n2UkmfGywA1l1aN81W7faXIynF0JmlQ2wPSdw==", "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
@ -1746,16 +1747,16 @@
"url": "https://opencollective.com/swc" "url": "https://opencollective.com/swc"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-darwin-arm64": "1.13.21", "@swc/core-darwin-arm64": "1.14.0",
"@swc/core-darwin-x64": "1.13.21", "@swc/core-darwin-x64": "1.14.0",
"@swc/core-linux-arm-gnueabihf": "1.13.21", "@swc/core-linux-arm-gnueabihf": "1.14.0",
"@swc/core-linux-arm64-gnu": "1.13.21", "@swc/core-linux-arm64-gnu": "1.14.0",
"@swc/core-linux-arm64-musl": "1.13.21", "@swc/core-linux-arm64-musl": "1.14.0",
"@swc/core-linux-x64-gnu": "1.13.21", "@swc/core-linux-x64-gnu": "1.14.0",
"@swc/core-linux-x64-musl": "1.13.21", "@swc/core-linux-x64-musl": "1.14.0",
"@swc/core-win32-arm64-msvc": "1.13.21", "@swc/core-win32-arm64-msvc": "1.14.0",
"@swc/core-win32-ia32-msvc": "1.13.21", "@swc/core-win32-ia32-msvc": "1.14.0",
"@swc/core-win32-x64-msvc": "1.13.21" "@swc/core-win32-x64-msvc": "1.14.0"
}, },
"peerDependencies": { "peerDependencies": {
"@swc/helpers": ">=0.5.17" "@swc/helpers": ">=0.5.17"
@ -1767,9 +1768,9 @@
} }
}, },
"node_modules/@swc/core-darwin-arm64": { "node_modules/@swc/core-darwin-arm64": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.14.0.tgz",
"integrity": "sha512-0jaz9r7f0PDK8OyyVooadv8dkFlQmVmBK6DtAnWSRjkCbNt4sdqsc9ZkyEDJXaxOVcMQ3pJx/Igniyw5xqACLw==", "integrity": "sha512-uHPC8rlCt04nvYNczWzKVdgnRhxCa3ndKTBBbBpResOZsRmiwRAvByIGh599j+Oo6Z5eyTPrgY+XfJzVmXnN7Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1784,9 +1785,9 @@
} }
}, },
"node_modules/@swc/core-darwin-x64": { "node_modules/@swc/core-darwin-x64": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.14.0.tgz",
"integrity": "sha512-pLeZn+NTGa7oW/ysD6oM82BjKZl71WNJR9BKXRsOhrNQeUWv55DCoZT2P4DzeU5Xgjmos+iMoDLg/9R6Ngc0PA==", "integrity": "sha512-2SHrlpl68vtePRknv9shvM9YKKg7B9T13tcTg9aFCwR318QTYo+FzsKGmQSv9ox/Ua0Q2/5y2BNjieffJoo4nA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1801,9 +1802,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm-gnueabihf": { "node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.14.0.tgz",
"integrity": "sha512-p9aYzTmP7qVDPkXxnbekOfbT11kxnPiuLrUbgpN/vn6sxXDCObMAiY63WlDR0IauBK571WUdmgb04goe/xTQWw==", "integrity": "sha512-SMH8zn01dxt809svetnxpeg/jWdpi6dqHKO3Eb11u4OzU2PK7I5uKS6gf2hx5LlTbcJMFKULZiVwjlQLe8eqtg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1818,9 +1819,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm64-gnu": { "node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.14.0.tgz",
"integrity": "sha512-yRqFoGlCwEX1nS7OajBE23d0LPeONmFAgoe4rgRYvaUb60qGxIJoMMdvF2g3dum9ZyVDYAb3kP09hbXFbMGr4A==", "integrity": "sha512-q2JRu2D8LVqGeHkmpVCljVNltG0tB4o4eYg+dElFwCS8l2Mnt9qurMCxIeo9mgoqz0ax+k7jWtIRHktnVCbjvQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1835,9 +1836,9 @@
} }
}, },
"node_modules/@swc/core-linux-arm64-musl": { "node_modules/@swc/core-linux-arm64-musl": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.14.0.tgz",
"integrity": "sha512-wu5EGA86gtdYMW69eU80jROzArzD3/6G6zzK0VVR+OFt/0zqbajiiszIpaniOVACObLfJEcShQ05B3q0+CpUEg==", "integrity": "sha512-uofpVoPCEUjYIv454ZEZ3sLgMD17nIwlz2z7bsn7rl301Kt/01umFA7MscUovFfAK2IRGck6XB+uulMu6aFhKQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1852,9 +1853,9 @@
} }
}, },
"node_modules/@swc/core-linux-x64-gnu": { "node_modules/@swc/core-linux-x64-gnu": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.14.0.tgz",
"integrity": "sha512-AoGGVPNXH3C4S7WlJOxN1nGW5nj//J9uKysS7CIBotRmHXfHO4wPK3TVFRTA4cuouAWBBn7O8m3A99p/GR+iaw==", "integrity": "sha512-quTTx1Olm05fBfv66DEBuOsOgqdypnZ/1Bh3yGXWY7ANLFeeRpCDZpljD9BSjdsNdPOlwJmEUZXMHtGm3v1TZQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1869,9 +1870,9 @@
} }
}, },
"node_modules/@swc/core-linux-x64-musl": { "node_modules/@swc/core-linux-x64-musl": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.14.0.tgz",
"integrity": "sha512-cBy2amuDuxMZnEq16MqGu+DUlEFqI+7F/OACNlk7zEJKq48jJKGEMqJz3X2ucJE5jqUIg6Pos6Uo/y+vuWQymQ==", "integrity": "sha512-caaNAu+aIqT8seLtCf08i8C3/UC5ttQujUjejhMcuS1/LoCKtNiUs4VekJd2UGt+pyuuSrQ6dKl8CbCfWvWeXw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1886,9 +1887,9 @@
} }
}, },
"node_modules/@swc/core-win32-arm64-msvc": { "node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.14.0.tgz",
"integrity": "sha512-2xfR5gnqBGOMOlY3s1QiFTXZaivTILMwX67FD2uzT6OCbT/3lyAM/4+3BptBXD8pUkkOGMFLsdeHw4fbO1GrpQ==", "integrity": "sha512-EeW3jFlT3YNckJ6V/JnTfGcX7UHGyh6/AiCPopZ1HNaGiXVCKHPpVQZicmtyr/UpqxCXLrTgjHOvyMke7YN26A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1903,9 +1904,9 @@
} }
}, },
"node_modules/@swc/core-win32-ia32-msvc": { "node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.14.0.tgz",
"integrity": "sha512-0pkpgKlBDwUImWTQxLakKbzZI6TIGVVAxk658oxrY8VK+hxRy2iezFY6m5Urmeds47M/cnW3dO+OY4C2caOF8A==", "integrity": "sha512-dPai3KUIcihV5hfoO4QNQF5HAaw8+2bT7dvi8E5zLtecW2SfL3mUZipzampXq5FHll0RSCLzlrXnSx+dBRZIIQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1920,9 +1921,9 @@
} }
}, },
"node_modules/@swc/core-win32-x64-msvc": { "node_modules/@swc/core-win32-x64-msvc": {
"version": "1.13.21", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.21.tgz", "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.14.0.tgz",
"integrity": "sha512-DAnIw2J95TOW4Kr7NBx12vlZPW3QndbpFMmuC7x+fPoozoLpEscaDkiYhk7/sTtY9pubPMfHFPBORlbqyQCfOQ==", "integrity": "sha512-nm+JajGrTqUA6sEHdghDlHMNfH1WKSiuvljhdmBACW4ta4LC3gKurX2qZuiBARvPkephW9V/i5S8QPY1PzFEqg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2387,6 +2388,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2396,6 +2398,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -2414,9 +2417,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/validator": { "node_modules/@types/validator": {
"version": "13.15.3", "version": "13.15.4",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz",
"integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", "integrity": "sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2466,6 +2469,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@ -2772,13 +2776,15 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3062,9 +3068,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.20", "version": "2.8.21",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz",
"integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@ -3114,6 +3120,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@ -3343,7 +3350,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/cva": { "node_modules/cva": {
"version": "1.0.0-beta.4", "version": "1.0.0-beta.4",
@ -3658,9 +3666,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.240", "version": "1.5.243",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.243.tgz",
"integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", "integrity": "sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -3939,6 +3947,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -3999,6 +4008,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@ -4072,6 +4082,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -4268,6 +4279,18 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": { "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@ -4510,12 +4533,12 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/focus-trap": { "node_modules/focus-trap": {
"version": "7.6.5", "version": "7.6.6",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz",
"integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tabbable": "^6.2.0" "tabbable": "^6.3.0"
} }
}, },
"node_modules/focus-trap-react": { "node_modules/focus-trap-react": {
@ -4922,9 +4945,9 @@
} }
}, },
"node_modules/immer": { "node_modules/immer": {
"version": "10.1.3", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -5494,6 +5517,7 @@
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@ -5936,9 +5960,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.26", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -6216,6 +6240,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -6261,6 +6286,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@ -6417,6 +6443,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -6439,6 +6466,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -6500,6 +6528,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -6519,9 +6548,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.9.4", "version": "7.9.5",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@ -6541,9 +6570,9 @@
} }
}, },
"node_modules/react-simple-keyboard": { "node_modules/react-simple-keyboard": {
"version": "3.8.131", "version": "3.8.132",
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.131.tgz", "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.132.tgz",
"integrity": "sha512-gICYtaV38AU/E1PTTwzJOF6s5fu6Nu3GZQwnaSNB4VGOO3UwOn8rioDEFBLvjMWpP8kwfWp2of8xywY647rTxA==", "integrity": "sha512-GoXK+6SRu72Jn8qT8fy+PxstIdZEACyIi/7zy0qXcrB6EJaN6zZk0/w3Sv3ALLwXqQd/3t3yUL4DQOwoNO1cbw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
@ -6596,7 +6625,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@ -6832,9 +6862,9 @@
} }
}, },
"node_modules/set-cookie-parser": { "node_modules/set-cookie-parser": {
"version": "2.7.1", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/set-function-length": { "node_modules/set-function-length": {
@ -7186,7 +7216,8 @@
"version": "4.1.16", "version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@ -7246,6 +7277,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7422,6 +7454,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -7570,9 +7603,9 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.15", "version": "13.15.20",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
@ -7605,6 +7638,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -7716,6 +7750,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -7864,6 +7899,7 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.24.2140", "version": "2025.10.30.0830",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.20.0" "node": "^22.20.0"
@ -50,18 +50,18 @@
"react-hook-form": "^7.65.0", "react-hook-form": "^7.65.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.5",
"react-simple-keyboard": "^3.8.131", "react-simple-keyboard": "^3.8.132",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.3.0", "recharts": "^3.3.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.20",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.1",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0", "@eslint/js": "^9.38.0",
"@inlang/cli": "^3.0.12", "@inlang/cli": "^3.0.12",
@ -76,7 +76,7 @@
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.4",
"@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.2.0", "@vitejs/plugin-react-swc": "^4.2.0",

View File

@ -212,7 +212,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
Button.displayName = "Button"; Button.displayName = "Button";
type LinkPropsType = Pick<LinkProps, "to"> & type LinkPropsType = Pick<LinkProps, "to"> &
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean }; React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
export const LinkButton = ({ to, ...props }: LinkPropsType) => { export const LinkButton = ({ to, ...props }: LinkPropsType) => {
const classes = cx( const classes = cx(
"group outline-hidden", "group outline-hidden",
@ -230,7 +230,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
); );
} else { } else {
return ( return (
<Link to={to} className={classes}> <Link to={to} reloadDocument={props.reloadDocument} className={classes}>
<ButtonContent {...props} /> <ButtonContent {...props} />
</Link> </Link>
); );

View File

@ -13,6 +13,7 @@ import { useRTCStore, PostRebootAction } from "@/hooks/stores";
import LogoBlue from "@/assets/logo-blue.svg"; import LogoBlue from "@/assets/logo-blue.svg";
import LogoWhite from "@/assets/logo-white.svg"; import LogoWhite from "@/assets/logo-white.svg";
import { isOnDevice } from "@/main"; import { isOnDevice } from "@/main";
import { sleep } from "@/utils";
interface OverlayContentProps { interface OverlayContentProps {
@ -481,8 +482,11 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro
// - Protocol-relative URLs: resolved with current protocol // - Protocol-relative URLs: resolved with current protocol
// - Fully qualified URLs: used as-is // - Fully qualified URLs: used as-is
const targetUrl = new URL(postRebootAction.redirectTo, window.location.origin); const targetUrl = new URL(postRebootAction.redirectTo, window.location.origin);
clearInterval(intervalId); // Stop polling before redirect
window.location.href = targetUrl.href; window.location.href = targetUrl.href;
// Add 1s delay between setting location.href and calling reload() to prevent reload from interrupting the navigation.
await sleep(1000);
window.location.reload(); window.location.reload();
} }
} catch (err) { } catch (err) {

View File

@ -573,14 +573,18 @@ export interface OtaState {
export interface UpdateState { export interface UpdateState {
isUpdatePending: boolean; isUpdatePending: boolean;
setIsUpdatePending: (isPending: boolean) => void; setIsUpdatePending: (isPending: boolean) => void;
updateDialogHasBeenMinimized: boolean; updateDialogHasBeenMinimized: boolean;
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
otaState: OtaState; otaState: OtaState;
setOtaState: (state: OtaState) => void; setOtaState: (state: OtaState) => void;
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
modalView: UpdateModalViews modalView: UpdateModalViews
setModalView: (view: UpdateModalViews) => void; setModalView: (view: UpdateModalViews) => void;
setUpdateErrorMessage: (errorMessage: string) => void;
updateErrorMessage: string | null; updateErrorMessage: string | null;
setUpdateErrorMessage: (errorMessage: string) => void;
} }
export const useUpdateStore = create<UpdateState>(set => ({ export const useUpdateStore = create<UpdateState>(set => ({
@ -611,8 +615,10 @@ export const useUpdateStore = create<UpdateState>(set => ({
updateDialogHasBeenMinimized: false, updateDialogHasBeenMinimized: false,
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
set({ updateDialogHasBeenMinimized: hasBeenMinimized }), set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
modalView: "loading", modalView: "loading",
setModalView: (view: UpdateModalViews) => set({ modalView: view }), setModalView: (view: UpdateModalViews) => set({ modalView: view }),
updateErrorMessage: null, updateErrorMessage: null,
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
})); }));

View File

@ -73,10 +73,10 @@ export async function checkDeviceAuth() {
.GET(`${DEVICE_API}/device/status`) .GET(`${DEVICE_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (!res.isSetup) return redirect("/welcome"); if (!res.isSetup) throw redirect("/welcome");
const deviceRes = await api.GET(`${DEVICE_API}/device`); const deviceRes = await api.GET(`${DEVICE_API}/device`);
if (deviceRes.status === 401) return redirect("/login-local"); if (deviceRes.status === 401) throw redirect("/login-local");
if (deviceRes.ok) { if (deviceRes.ok) {
const device = (await deviceRes.json()) as LocalDevice; const device = (await deviceRes.json()) as LocalDevice;
return { authMode: device.authMode }; return { authMode: device.authMode };
@ -86,7 +86,7 @@ export async function checkDeviceAuth() {
} }
export async function checkAuth() { export async function checkAuth() {
return import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth(); return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
} }
let router; let router;

View File

@ -58,7 +58,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
return { device, user }; return { device, user };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return { devices: [] }; return { user };
} }
}; };

View File

@ -54,7 +54,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
return { device, user }; return { device, user };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return { devices: [] }; return { user };
} }
}; };

View File

@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
} }
getCloudState(); getCloudState();
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore // In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
if (!isOnDevice) navigate("/"); if (!isOnDevice) navigate("/");
return; return;
}); });

View File

@ -9,11 +9,17 @@ export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
send("reboot", { force: true}); send("reboot", { force: true});
}, [send]); }, [send]);
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({

View File

@ -21,6 +21,11 @@ export default function SettingsGeneralUpdateRoute() {
const { setModalView, otaState } = useUpdateStore(); const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const onClose = useCallback(() => {
navigate(".."); // back to the devices.$id.settings page
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}, [navigate]);
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
send("tryUpdate", {}); send("tryUpdate", {});
setModalView("updating"); setModalView("updating");
@ -36,9 +41,9 @@ export default function SettingsGeneralUpdateRoute() {
} else { } else {
setModalView("loading"); setModalView("loading");
} }
}, [otaState.updating, otaState.error, setModalView, updateSuccess]); }, [otaState.error, otaState.updating, setModalView, updateSuccess]);
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({

View File

@ -46,10 +46,10 @@ export default function SettingsHardwareRoute() {
} }
setBacklightSettings(settings); setBacklightSettings(settings);
handleBacklightSettingsSave(); handleBacklightSettingsSave(settings);
}; };
const handleBacklightSettingsSave = () => { const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => { send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(

View File

@ -1,7 +1,6 @@
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
Outlet, Outlet,
redirect,
useLoaderData, useLoaderData,
useLocation, useLocation,
useNavigate, useNavigate,
@ -16,7 +15,7 @@ import { motion, AnimatePresence } from "framer-motion";
import useWebSocket from "react-use-websocket"; import useWebSocket from "react-use-websocket";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { CLOUD_API, DEVICE_API } from "@/ui.config"; import { CLOUD_API } from "@/ui.config";
import api from "@/api"; import api from "@/api";
import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { checkAuth, isInCloud, isOnDevice } from "@/main";
import { import {
@ -51,9 +50,14 @@ import {
RebootingOverlay, RebootingOverlay,
} from "@components/VideoOverlay"; } from "@components/VideoOverlay";
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider"; import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
export type AuthMode = "password" | "noPassword" | null;
interface LocalLoaderResp {
authMode: AuthMode;
}
interface CloudLoaderResp { interface CloudLoaderResp {
deviceName: string; deviceName: string;
user: User | null; user: User | null;
@ -62,35 +66,20 @@ interface CloudLoaderResp {
} | null; } | null;
} }
export type AuthMode = "password" | "noPassword" | null;
export interface LocalDevice { export interface LocalDevice {
authMode: AuthMode; authMode: AuthMode;
deviceId: string; deviceId: string;
} }
const deviceLoader = async () => { const deviceLoader = async () => {
const res = await api const device = await checkAuth();
.GET(`${DEVICE_API}/device/status`) return { authMode: device.authMode } as LocalLoaderResp;
.then(res => res.json() as Promise<DeviceStatus>);
if (!res.isSetup) return redirect("/welcome");
const deviceRes = await api.GET(`${DEVICE_API}/device`);
if (deviceRes.status === 401) return redirect("/login-local");
if (deviceRes.ok) {
const device = (await deviceRes.json()) as LocalDevice;
return { authMode: device.authMode };
}
throw new Error("Error fetching device");
}; };
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => { const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
const user = await checkAuth(); const user = await checkAuth();
const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`); const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
const iceConfig = await iceResp.json(); const iceConfig = await iceResp.json();
const deviceResp = await api.GET(`${CLOUD_API}/devices/${params.id}`); const deviceResp = await api.GET(`${CLOUD_API}/devices/${params.id}`);
if (!deviceResp.ok) { if (!deviceResp.ok) {
@ -105,11 +94,11 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
device: { id: string; name: string; user: { googleId: string } }; device: { id: string; name: string; user: { googleId: string } };
}; };
return { user, iceConfig, deviceName: device.name || device.id }; return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
}; };
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params); return isOnDevice ? deviceLoader() : cloudLoader(params);
}; };
export default function KvmIdRoute() { export default function KvmIdRoute() {
@ -185,7 +174,7 @@ export default function KvmIdRoute() {
try { try {
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription)); await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
console.log("[setRemoteSessionDescription] Remote description set successfully"); console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
setLoadingMessage(m.establishing_secure_connection()); setLoadingMessage(m.establishing_secure_connection());
} catch (error) { } catch (error) {
console.error( console.error(
@ -230,9 +219,14 @@ export default function KvmIdRoute() {
const ignoreOffer = useRef(false); const ignoreOffer = useRef(false);
const isSettingRemoteAnswerPending = useRef(false); const isSettingRemoteAnswerPending = useRef(false);
const makingOffer = useRef(false); const makingOffer = useRef(false);
const reconnectAttemptsRef = useRef(2000);
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const reconnectInterval = (attempt: number) => {
// Exponential backoff with a max of 10 seconds between attempts
return Math.min(500 * 2 ** attempt, 10000);
}
const { sendMessage, getWebSocket } = useWebSocket( const { sendMessage, getWebSocket } = useWebSocket(
isOnDevice isOnDevice
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client` ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
@ -240,10 +234,10 @@ export default function KvmIdRoute() {
{ {
heartbeat: true, heartbeat: true,
retryOnError: true, retryOnError: true,
reconnectAttempts: 2000, reconnectAttempts: reconnectAttemptsRef.current,
reconnectInterval: 1000, reconnectInterval: reconnectInterval,
onReconnectStop: (numAttempts: number) => { onReconnectStop: (numAttempts: number) => {
console.debug("Reconnect stopped", numAttempts); console.debug("Reconnect stopped after ", numAttempts, "attempts");
cleanupAndStopReconnecting(); cleanupAndStopReconnecting();
}, },
@ -261,6 +255,7 @@ export default function KvmIdRoute() {
console.error("[Websocket] onError", event); console.error("[Websocket] onError", event);
// We don't want to close everything down, we wait for the reconnect to stop instead // We don't want to close everything down, we wait for the reconnect to stop instead
}, },
onOpen() { onOpen() {
console.debug("[Websocket] onOpen"); console.debug("[Websocket] onOpen");
// We want to clear the reboot state when the websocket connection is opened // We want to clear the reboot state when the websocket connection is opened
@ -293,6 +288,7 @@ export default function KvmIdRoute() {
*/ */
const parsedMessage = JSON.parse(message.data); const parsedMessage = JSON.parse(message.data);
if (parsedMessage.type === "device-metadata") { if (parsedMessage.type === "device-metadata") {
const { deviceVersion } = parsedMessage.data; const { deviceVersion } = parsedMessage.data;
console.debug("[Websocket] Received device-metadata message"); console.debug("[Websocket] Received device-metadata message");
@ -309,10 +305,12 @@ export default function KvmIdRoute() {
console.log("[Websocket] Device is using new signaling"); console.log("[Websocket] Device is using new signaling");
isLegacySignalingEnabled.current = false; isLegacySignalingEnabled.current = false;
} }
setupPeerConnection(); setupPeerConnection();
} }
if (!peerConnection) return; if (!peerConnection) return;
if (parsedMessage.type === "answer") { if (parsedMessage.type === "answer") {
console.debug("[Websocket] Received answer"); console.debug("[Websocket] Received answer");
const readyForOffer = const readyForOffer =
@ -875,7 +873,7 @@ export default function KvmIdRoute() {
style={{ animationDuration: "500ms" }} style={{ animationDuration: "500ms" }}
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4" className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
> >
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md"> <div className="relative h-full max-h-[720px] w-full max-w-7xl rounded-md">
{!!ConnectionStatusElement && ConnectionStatusElement} {!!ConnectionStatusElement && ConnectionStatusElement}
</div> </div>
</div> </div>

View File

@ -30,7 +30,7 @@ const loader: LoaderFunction = async ()=> {
return { devices, user }; return { devices, user };
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return { devices: [] }; return { devices: [], user };
} }
}; };