This commit is contained in:
Marc Brooks 2025-10-07 23:47:41 +00:00 committed by GitHub
commit ca4c1b393d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 2034 additions and 593 deletions

View File

@ -8,7 +8,8 @@
} }
}, },
"mounts": [ "mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
], ],
"onCreateCommand": ".devcontainer/install-deps.sh", "onCreateCommand": ".devcontainer/install-deps.sh",
"customizations": { "customizations": {
@ -31,8 +32,11 @@
// Frontend // Frontend
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss",
"codeandstuff.package-json-upgrade",
// Localization
"inlang.vs-code-extension"
] ]
} }
} }
} }

25
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
"recommendations": [
// coding styles
"chrislajoie.vscode-modelines",
"editorconfig.editorconfig",
// GitHub
"GitHub.vscode-pull-request-github",
"github.vscode-github-actions",
// Golang
"golang.go",
// C / C++
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
// CMake / Makefile
"ms-vscode.makefile-tools",
"ms-vscode.cmake-tools",
// Frontend
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"codeandstuff.package-json-upgrade",
// Localization
"inlang.vs-code-extension"
]
}

View File

@ -97,38 +97,42 @@ tail -f /var/log/jetkvm.log
``` ```
/kvm/ /kvm/
├── main.go # App entry point ├── main.go # App entry point
├── config.go # Settings & configuration ├── config.go # Settings & configuration
├── display.go # Device UI control ├── display.go # Device UI control
├── web.go # API endpoints ├── web.go # API endpoints
├── cmd/ # Command line main ├── cmd/ # Command line main
├── internal/ # Internal Go packages ├── internal/ # Internal Go packages
│ ├── confparser/ # Configuration file implementation │ ├── confparser/ # Configuration file implementation
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.) │ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
│ ├── logging/ # Logging implementation │ ├── logging/ # Logging implementation
│ ├── mdns/ # mDNS implementation │ ├── mdns/ # mDNS implementation
│ ├── native/ # CGO / Native code glue layer (on-device hardware) │ ├── native/ # CGO / Native code glue layer (on-device hardware)
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.) │ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen) │ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
│ ├── network/ # Network implementation │ ├── network/ # Network implementation
│ ├── timesync/ # Time sync/NTP implementation │ ├── timesync/ # Time sync/NTP implementation
│ ├── tzdata/ # Timezone data and generation │ ├── tzdata/ # Timezone data and generation
│ ├── udhcpc/ # DHCP implementation │ ├── udhcpc/ # DHCP implementation
│ ├── usbgadget/ # USB gadget │ ├── usbgadget/ # USB gadget
│ ├── utils/ # SSH handling │ ├── utils/ # SSH handling
│ └── websecure/ # TLS certificate management │ └── websecure/ # TLS certificate management
├── resource/ # netboot iso and other resources ├── resource/ # netboot iso and other resources
├── scripts/ # Bash shell scripts for building and deploying ├── scripts/ # Bash shell scripts for building and deploying
└── static/ # (react client build output) └── static/ # (react client build output)
└── ui/ # React frontend └── ui/ # React frontend
├── public/ # UI website static images and fonts ├── localization/ # Client UI localization (i18n)
└── src/ # Client React UI │ ├── jetKVM.UI.inlang/ # Settings for inlang
├── assets/ # UI in-page images │ └── messages/ # Messages localized
├── components/ # UI components ├── public/ # UI website static images and fonts
├── hooks/ # Hooks (stores, RPC handling, virtual devices) └── src/ # Client React UI
├── keyboardLayouts/ # Keyboard layout definitions ├── assets/ # UI in-page images
├── providers/ # Feature flags ├── components/ # UI components
└── routes/ # Pages (login, settings, etc.) ├── hooks/ # Hooks (stores, RPC handling, virtual devices)
├── keyboardLayouts/ # Keyboard layout definitions
│ ├── paraglide/ # (localization compiled messages output)
├── providers/ # Feature flags
└── routes/ # Pages (login, settings, etc.)
``` ```
**Key files for beginners:** **Key files for beginners:**

View File

@ -66,7 +66,7 @@ module.exports = defineConfig([{
groups: ["builtin", "external", "internal", "parent", "sibling"], groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always", "newlines-between": "always",
}], }],
"@typescript-eslint/no-unused-vars": ["warn", { "@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_", "varsIgnorePattern": "^_" "argsIgnorePattern": "^_", "varsIgnorePattern": "^_"
}], }],
@ -81,7 +81,10 @@ module.exports = defineConfig([{
map: [ map: [
["@components", "./src/components"], ["@components", "./src/components"],
["@routes", "./src/routes"], ["@routes", "./src/routes"],
["@hooks", "./src/hooks"],
["@providers", "./src/providers"],
["@assets", "./src/assets"], ["@assets", "./src/assets"],
["@localizations", "./localization/paraglide"],
["@", "./src"], ["@", "./src"],
], ],

View File

@ -45,31 +45,39 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="JetKVM" /> <meta name="apple-mobile-web-app-title" content="JetKVM" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/public/site.webmanifest" />
<meta name="theme-color" content="#051946" /> <meta name="theme-color" content="#051946" />
<meta name="description" content="A web-based KVM console for managing remote servers." /> <meta
name="description"
content="A web-based KVM console for managing remote servers."
/>
<script> <script>
function applyThemeFromPreference() { function applyThemeFromPreference() {
// dark theme setup // dark theme setup
var darkDesired = localStorage.theme === "dark" || var darkDesired =
localStorage.theme === "dark" ||
(!("theme" in localStorage) && (!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches) window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", darkDesired) document.documentElement.classList.toggle("dark", darkDesired);
} }
// initial theme application // initial theme application
applyThemeFromPreference(); applyThemeFromPreference();
// Listen for system theme changes // Listen for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyThemeFromPreference); window
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", applyThemeFromPreference); .matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", applyThemeFromPreference);
window
.matchMedia("(prefers-color-scheme: light)")
.addEventListener("change", applyThemeFromPreference);
</script> </script>
</head> </head>
<body <body
class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased dark:bg-slate-900 md:text-base" class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased md:text-base dark:bg-slate-900"
> >
<div id="root" class="w-full h-full"></div> <div id="root" class="h-full w-full"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
TI1a2RjjH4qkImNj0w

View File

@ -0,0 +1,40 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"sourceLanguageTag": "en",
"locales": [
"en",
"da",
"de",
"es",
"fr",
"it",
"nb",
"sv",
"zh"
],
"languageTags": [
"en",
"da",
"de",
"es",
"fr",
"it",
"nb",
"sv",
"zh"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"strategy": [
"cookie",
"localStorage",
"preferredLanguage",
"baseLocale"
]
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Åh nej!",
"something_went_wrong": "Noget gik galt. Prøv igen senere, eller kontakt support.",
"jetkvm_logo": "JetKVM-logo",
"load": "Indlæs",
"unknown_error": "Ukendt fejl",
"action_bar_virtual_media": "Virtuelle medier",
"action_bar_paste_text": "Indsæt tekst",
"action_bar_web_terminal": "Webterminal",
"action_bar_wake_on_lan": "Vågn på LAN",
"action_bar_virtual_keyboard": "Virtuelt tastatur",
"action_bar_extension": "Udvidelse",
"action_bar_connection_stats": "Forbindelsesstatistik",
"action_bar_settings": "Indstillinger",
"action_bar_fullscreen": "Fuldskærm",
"action_bar_exit_fullscreen": "Afslut fuldskærm",
"extensions_popover_extensions": "Udvidelser",
"extension_popover_set_error_notification": "Kunne ikke angive aktiv udvidelse: {error}",
"extension_popover_unload_extension": "Fjern udvidelse",
"extension_popover_load_and_manage_extensions": "Indlæs og administrer dine udvidelser",
"extensions_atx_power_control": "ATX-strømstyring",
"extensions_atx_power_control_description": "Styr din maskines strømtilstand via ATX-strømstyring.",
"extensions_dc_power_control": "DC-strømstyring",
"extensions_dc_power_control_description": "Styr din DC-strømforlænger",
"extension_serial_console": "Seriel konsol",
"extension_serial_console_description": "Få adgang til din serielle konsoludvidelse",
"atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}",
"atx_power_control_power_button": "Magt",
"atx_power_control_short_power_button": "Kort tryk",
"atx_power_control_long_power_button": "Langt tryk",
"atx_power_control_reset_button": "Nulstil",
"atx_power_control_power_led": "Strøm-LED",
"atx_power_control_hdd_led": "HDD-LED",
"dc_power_control_get_state_error": "Kunne ikke hente DC-strømtilstand: {error}",
"dc_power_control_set_power_state_error": "Kunne ikke sende DC-strømstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende DC-strømgendannelsesstatus til {state} : {error}",
"dc_power_control_power_on_button": "Tænd",
"dc_power_control_power_off_button": "Sluk",
"dc_power_control_restore_power_state": "Gendan strømtab",
"dc_power_control_power_on_state": "Tænd",
"dc_power_control_power_off_state": "Sluk",
"dc_power_control_voltage": "Spænding",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Strøm",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Magt",
"dc_power_control_power_unit": "W",
"serial_console_get_state_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
"serial_console_set_power_state_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}",
"serial_console_configure_description": "Konfigurer dine serielle konsolindstillinger",
"serial_console_open_console": "Åbn konsol",
"serial_console_baud_rate": "Baudhastighed",
"serial_console_data_bits": "Databits",
"serial_console_stop_bits": "Stopbits",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Lige paritet",
"serial_console_parity_odd": "Ulige paritet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_space": "Rumparitet",
"serial_console_get_settings_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
"serial_console_set_settings_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Oh nein!",
"something_went_wrong": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an den Support.",
"jetkvm_logo": "JetKVM Logo",
"load": "Laden",
"unknown_error": "Unbekannter Fehler",
"action_bar_virtual_media": "Virtuelle Medien",
"action_bar_paste_text": "Text einfügen",
"action_bar_web_terminal": "Web-Terminal",
"action_bar_wake_on_lan": "Wake-on-LAN",
"action_bar_virtual_keyboard": "Virtuelle Tastatur",
"action_bar_extension": "Verlängerung",
"action_bar_connection_stats": "Verbindungsstatistiken",
"action_bar_settings": "Einstellungen",
"action_bar_fullscreen": "Vollbild",
"action_bar_exit_fullscreen": "Vollbildmodus beenden",
"extensions_popover_extensions": "Erweiterungen",
"extension_popover_set_error_notification": "Fehler beim Festlegen der aktiven Erweiterung: {error}",
"extension_popover_unload_extension": "Erweiterung entladen",
"extension_popover_load_and_manage_extensions": "Laden und verwalten Sie Ihre Erweiterungen",
"extensions_atx_power_control": "ATX-Stromsteuerung",
"extensions_atx_power_control_description": "Steuern Sie den Energiezustand Ihrer Maschine über die ATX-Energiesteuerung.",
"extensions_dc_power_control": "Gleichstromsteuerung",
"extensions_dc_power_control_description": "Steuern Sie Ihre DC-Stromerweiterung",
"extension_serial_console": "Serielle Konsole",
"extension_serial_console_description": "Greifen Sie auf Ihre serielle Konsolenerweiterung zu",
"atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}",
"atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}",
"atx_power_control_power_button": "Leistung",
"atx_power_control_short_power_button": "Kurzes Drücken",
"atx_power_control_long_power_button": "Langes Drücken",
"atx_power_control_reset_button": "Zurücksetzen",
"atx_power_control_power_led": "Betriebs-LED",
"atx_power_control_hdd_led": "Festplatten-LED",
"dc_power_control_get_state_error": "Der Gleichstromstatus konnte nicht abgerufen werden: {error}",
"dc_power_control_set_power_state_error": "Der DC-Stromversorgungsstatus konnte nicht an {enabled} werden: {error}",
"dc_power_control_set_restore_state_error": "Der Status zur Wiederherstellung der Gleichstromversorgung konnte nicht an {state} gesendet werden: {error}",
"dc_power_control_power_on_button": "Einschalten",
"dc_power_control_power_off_button": "Ausschalten",
"dc_power_control_restore_power_state": "Wiederherstellung nach Stromausfall",
"dc_power_control_power_on_state": "Einschalten",
"dc_power_control_power_off_state": "Ausschalten",
"dc_power_control_voltage": "Stromspannung",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Aktuell",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Leistung",
"dc_power_control_power_unit": "W",
"serial_console_get_state_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
"serial_console_set_power_state_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}",
"serial_console_configure_description": "Konfigurieren Sie die Einstellungen Ihrer seriellen Konsole",
"serial_console_open_console": "Konsole öffnen",
"serial_console_baud_rate": "Baudrate",
"serial_console_data_bits": "Datenbits",
"serial_console_stop_bits": "Stoppbits",
"serial_console_parity": "Parität",
"serial_console_parity_even": "Gerade Parität",
"serial_console_parity_odd": "Ungerade Parität",
"serial_console_parity_none": "Keine Parität",
"serial_console_parity_mark": "Parität markieren",
"serial_console_parity_space": "Raumparität",
"serial_console_get_settings_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
"serial_console_set_settings_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}"
}

View File

@ -0,0 +1,144 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Oh no!",
"something_went_wrong": "Something went wrong. Please try again later or contact support",
"jetkvm_logo": "JetKVM Logo",
"load": "Load",
"unknown_error": "Unknown error",
"close": "Close",
"cancel": "Cancel",
"action_bar_virtual_media": "Virtual Media",
"action_bar_paste_text": "Paste text",
"action_bar_web_terminal": "Web Terminal",
"action_bar_wake_on_lan": "Wake on LAN",
"action_bar_virtual_keyboard": "Virtual Keyboard",
"action_bar_extension": "Extension",
"action_bar_connection_stats": "Connection Stats",
"action_bar_settings": "Settings",
"action_bar_fullscreen": "Fullscreen",
"action_bar_exit_fullscreen": "Exit Fullscreen",
"extensions_popover_extensions": "Extensions",
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
"extension_popover_unload_extension": "Unload Extension",
"extension_popover_load_and_manage_extensions": "Load and manage your extensions",
"extensions_atx_power_control": "ATX Power Control",
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
"extensions_dc_power_control": "DC Power Control",
"extensions_dc_power_control_description": "Control your DC Power extension",
"extension_serial_console": "Serial Console",
"extension_serial_console_description": "Access your serial console extension",
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_power_button": "Power",
"atx_power_control_short_power_button": "Short Press",
"atx_power_control_long_power_button": "Long Press",
"atx_power_control_reset_button": "Reset",
"atx_power_control_power_led": "Power LED",
"atx_power_control_hdd_led": "HDD LED",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
"dc_power_control_power_on_button": "Power On",
"dc_power_control_power_off_button": "Power Off",
"dc_power_control_restore_power_state": "Restore Power Loss",
"dc_power_control_power_on_state": "Power ON",
"dc_power_control_power_off_state": "Power OFF",
"dc_power_control_restore_last_state": "Last State",
"dc_power_control_voltage": "Voltage",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Current",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Power",
"dc_power_control_power_unit": "W",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_configure_description": "Configure your serial console settings",
"serial_console_open_console": "Open Console",
"serial_console_baud_rate": "Baud Rate",
"serial_console_data_bits": "Data Bits",
"serial_console_stop_bits": "Stop Bits",
"serial_console_parity": "Parity",
"serial_console_parity_even": "Even Parity",
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_mark": "Mark Parity",
"serial_console_parity_space": "Space Parity",
"wake_on_lan_add_device_device_name": "Device Name",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC Address",
"wake_on_lan_add_device_back": "Back",
"wake_on_lan_add_device_save_device": "Save Device",
"paste_modal_paste_text": "Paste text",
"paste_modal_paste_text_description": "Paste text from your client to the remote host",
"paste_modal_paste_from_host": "Paste from host",
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
"paste_modal_delay_between_keys": "Delay between keys",
"paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
"paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
"paste_modal_confirm_paste": "Confirm Paste",
"mount_virtual_media": "Virtual Media",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_no_mounted_media": "No mounted media",
"mount_add_file_to_get_started": "Add a file to get started",
"mount_streaming_from_url": "Streaming from URL",
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
"mount_unmount": "Unmount",
"mount_add_new_media": "Add New Media",
"mount_get_state_error": "Failed to get virtual media state: {error}",
"mount_unmount_error": "Failed to unmount image: {error}",
"mount_mounted_as": "Mounted as",
"mount_mode_disk": "Disk",
"mount_mode_cdrom": "CD-ROM",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan_failed_add_device": "Failed to add device",
"wake_on_lan_empty_no_devices_added": "No devices added",
"wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Add New Device",
"wake_on_lan_device_list_wake": "Wake",
"wake_on_lan_device_list_delete_device": "Delete device",
"wake_on_lan_device_list_add_new_device": "Add New Device",
"connection_stats_sidebar": "Connection Stats",
"connection_stats_connection": "Connection",
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_video": "Video",
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_network_stability": "Network Stability",
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_playback_delay": "Playback Delay",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
"connection_stats_packets_lost": "Packets Lost",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_frames_per_second": "Frames per second",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second."
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "¡Oh, no!",
"something_went_wrong": "Algo salió mal. Inténtalo de nuevo más tarde o contacta con el servicio de asistencia.",
"jetkvm_logo": "Logotipo de JetKVM",
"load": "Carga",
"unknown_error": "Error desconocido",
"action_bar_virtual_media": "Medios virtuales",
"action_bar_paste_text": "Pegar texto",
"action_bar_web_terminal": "Terminal web",
"action_bar_wake_on_lan": "Activación en LAN",
"action_bar_virtual_keyboard": "Teclado virtual",
"action_bar_extension": "Extensión",
"action_bar_connection_stats": "Estadísticas de conexión",
"action_bar_settings": "Ajustes",
"action_bar_fullscreen": "Pantalla completa",
"action_bar_exit_fullscreen": "Salir de pantalla completa",
"extensions_popover_extensions": "Extensiones",
"extension_popover_set_error_notification": "No se pudo establecer la extensión activa: {error}",
"extension_popover_unload_extension": "Extensión de descarga",
"extension_popover_load_and_manage_extensions": "Cargar y administrar sus extensiones",
"extensions_atx_power_control": "Control de alimentación ATX",
"extensions_atx_power_control_description": "Controle el estado de energía de su máquina a través del control de energía ATX.",
"extensions_dc_power_control": "Control de potencia de CC",
"extensions_dc_power_control_description": "Controle su extensión de alimentación de CC",
"extension_serial_console": "Consola serial",
"extension_serial_console_description": "Acceda a la extensión de su consola serie",
"atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}",
"atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}",
"atx_power_control_power_button": "Fuerza",
"atx_power_control_short_power_button": "Prensa corta",
"atx_power_control_long_power_button": "Pulsación larga",
"atx_power_control_reset_button": "Reiniciar",
"atx_power_control_power_led": "LED de encendido",
"atx_power_control_hdd_led": "LED del disco duro",
"dc_power_control_get_state_error": "No se pudo obtener el estado de la alimentación de CC: {error}",
"dc_power_control_set_power_state_error": "No se pudo enviar el estado de alimentación de CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "No se pudo enviar el estado de restauración de energía de CC a {state} : {error}",
"dc_power_control_power_on_button": "Encendido",
"dc_power_control_power_off_button": "Apagado",
"dc_power_control_restore_power_state": "Restaurar pérdida de energía",
"dc_power_control_power_on_state": "Encendido",
"dc_power_control_power_off_state": "Apagado",
"dc_power_control_voltage": "Voltaje",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Amperio",
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Vatio",
"dc_power_control_power_unit": "O",
"serial_console_get_state_error": "No se pudo obtener la configuración de la consola serial: {error}",
"serial_console_set_power_state_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}",
"serial_console_configure_description": "Configure los ajustes de su consola serie",
"serial_console_open_console": "Consola abierta",
"serial_console_baud_rate": "Tasa de Baud",
"serial_console_data_bits": "Bits de datos",
"serial_console_stop_bits": "Bits de parada",
"serial_console_parity": "Paridad",
"serial_console_parity_even": "Paridad uniforme",
"serial_console_parity_odd": "Paridad impar",
"serial_console_parity_none": "Sin paridad",
"serial_console_parity_mark": "Paridad de marca",
"serial_console_parity_space": "Paridad espacial",
"serial_console_get_settings_error": "No se pudo obtener la configuración de la consola serial: {error}",
"serial_console_set_settings_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Oh non!",
"something_went_wrong": "Une erreur s'est produite. Veuillez réessayer ultérieurement ou contacter le support.",
"jetkvm_logo": "Logo JetKVM",
"load": "Charger",
"unknown_error": "Erreur inconnue",
"action_bar_virtual_media": "Médias virtuels",
"action_bar_paste_text": "Coller du texte",
"action_bar_web_terminal": "Terminal Web",
"action_bar_wake_on_lan": "Réveil sur LAN",
"action_bar_virtual_keyboard": "Clavier virtuel",
"action_bar_extension": "Extension",
"action_bar_connection_stats": "Statistiques de connexion",
"action_bar_settings": "Paramètres",
"action_bar_fullscreen": "Plein écran",
"action_bar_exit_fullscreen": "Quitter le plein écran",
"extensions_popover_extensions": "Extensions",
"extension_popover_set_error_notification": "Échec de la définition de l'extension active: {error}",
"extension_popover_unload_extension": "Extension de déchargement",
"extension_popover_load_and_manage_extensions": "Chargez et gérez vos extensions",
"extensions_atx_power_control": "Contrôle d'alimentation ATX",
"extensions_atx_power_control_description": "Contrôlez l'état d'alimentation de votre machine via le contrôle d'alimentation ATX.",
"extensions_dc_power_control": "Contrôle de l'alimentation CC",
"extensions_dc_power_control_description": "Contrôlez votre extension d'alimentation CC",
"extension_serial_console": "Console série",
"extension_serial_console_description": "Accédez à votre extension de console série",
"atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}",
"atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}",
"atx_power_control_power_button": "Pouvoir",
"atx_power_control_short_power_button": "Appui court",
"atx_power_control_long_power_button": "Appui long",
"atx_power_control_reset_button": "Réinitialiser",
"atx_power_control_power_led": "LED d'alimentation",
"atx_power_control_hdd_led": "Voyant du disque dur",
"dc_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation CC : {error}",
"dc_power_control_set_power_state_error": "Échec de l'envoi de l'état d'alimentation CC à {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Échec de l'envoi de l'état de restauration de l'alimentation CC à {state} : {error}",
"dc_power_control_power_on_button": "Mise sous tension",
"dc_power_control_power_off_button": "Éteindre",
"dc_power_control_restore_power_state": "Restaurer la perte de puissance",
"dc_power_control_power_on_state": "Mise sous tension",
"dc_power_control_power_off_state": "Éteindre",
"dc_power_control_voltage": "Tension",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Ampère",
"dc_power_control_current_unit": "UN",
"dc_power_control_power": "Pouvoir",
"dc_power_control_power_unit": "W",
"serial_console_get_state_error": "Échec de l'obtention des paramètres de la console série : {error}",
"serial_console_set_power_state_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}",
"serial_console_configure_description": "Configurez les paramètres de votre console série",
"serial_console_open_console": "Ouvrir la console",
"serial_console_baud_rate": "Débit en bauds",
"serial_console_data_bits": "Bits de données",
"serial_console_stop_bits": "Bits d'arrêt",
"serial_console_parity": "Parité",
"serial_console_parity_even": "Parité égale",
"serial_console_parity_odd": "Parité impaire",
"serial_console_parity_none": "Pas de parité",
"serial_console_parity_mark": "Marquer la parité",
"serial_console_parity_space": "Parité spatiale",
"serial_console_get_settings_error": "Échec de l'obtention des paramètres de la console série : {error}",
"serial_console_set_settings_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Oh no!",
"something_went_wrong": "Qualcosa è andato storto. Riprova più tardi o contatta l'assistenza.",
"jetkvm_logo": "Logo JetKVM",
"load": "Carico",
"unknown_error": "Errore sconosciuto",
"action_bar_virtual_media": "Media virtuali",
"action_bar_paste_text": "Incolla il testo",
"action_bar_web_terminal": "Terminale Web",
"action_bar_wake_on_lan": "Wake on LAN",
"action_bar_virtual_keyboard": "Tastiera virtuale",
"action_bar_extension": "Estensione",
"action_bar_connection_stats": "Statistiche di connessione",
"action_bar_settings": "Impostazioni",
"action_bar_fullscreen": "A schermo intero",
"action_bar_exit_fullscreen": "Esci dalla modalità a schermo intero",
"extensions_popover_extensions": "Estensioni",
"extension_popover_set_error_notification": "Impossibile impostare l'estensione attiva: {error}",
"extension_popover_unload_extension": "Estensione di scaricamento",
"extension_popover_load_and_manage_extensions": "Carica e gestisci le tue estensioni",
"extensions_atx_power_control": "Controllo di potenza ATX",
"extensions_atx_power_control_description": "Controlla lo stato di alimentazione del tuo computer tramite il controllo di alimentazione ATX.",
"extensions_dc_power_control": "Controllo di potenza CC",
"extensions_dc_power_control_description": "Controlla la tua estensione di alimentazione CC",
"extension_serial_console": "Console seriale",
"extension_serial_console_description": "Accedi all'estensione della tua console seriale",
"atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}",
"atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}",
"atx_power_control_power_button": "Energia",
"atx_power_control_short_power_button": "Pressione breve",
"atx_power_control_long_power_button": "Pressione lunga",
"atx_power_control_reset_button": "Reset",
"atx_power_control_power_led": "LED di potenza",
"atx_power_control_hdd_led": "LED dell'HDD",
"dc_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione CC: {error}",
"dc_power_control_set_power_state_error": "Impossibile inviare lo stato di alimentazione CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Impossibile inviare lo stato di ripristino dell'alimentazione CC a {state} : {error}",
"dc_power_control_power_on_button": "Accensione",
"dc_power_control_power_off_button": "Spegnimento",
"dc_power_control_restore_power_state": "Ripristinare la perdita di potenza",
"dc_power_control_power_on_state": "Accensione",
"dc_power_control_power_off_state": "Spegnimento",
"dc_power_control_voltage": "Voltaggio",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Attuale",
"dc_power_control_current_unit": "UN",
"dc_power_control_power": "Energia",
"dc_power_control_power_unit": "O",
"serial_console_get_state_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
"serial_console_set_power_state_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}",
"serial_console_configure_description": "Configura le impostazioni della tua console seriale",
"serial_console_open_console": "Apri console",
"serial_console_baud_rate": "Velocità in baud",
"serial_console_data_bits": "Bit di dati",
"serial_console_stop_bits": "Bit di stop",
"serial_console_parity": "Parità",
"serial_console_parity_even": "Parità pari",
"serial_console_parity_odd": "Parità dispari",
"serial_console_parity_none": "Nessuna parità",
"serial_console_parity_mark": "Segna la parità",
"serial_console_parity_space": "Parità spaziale",
"serial_console_get_settings_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
"serial_console_set_settings_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "Å nei!",
"something_went_wrong": "Noe gikk galt. Prøv igjen senere, eller kontakt kundestøtte.",
"jetkvm_logo": "JetKVM-logo",
"load": "Laste",
"unknown_error": "Ukjent feil",
"action_bar_virtual_media": "Virtuelle medier",
"action_bar_paste_text": "Lim inn tekst",
"action_bar_web_terminal": "Nettterminal",
"action_bar_wake_on_lan": "Vekk på LAN",
"action_bar_virtual_keyboard": "Virtuelt tastatur",
"action_bar_extension": "Forlengelse",
"action_bar_connection_stats": "Tilkoblingsstatistikk",
"action_bar_settings": "Innstillinger",
"action_bar_fullscreen": "Fullskjerm",
"action_bar_exit_fullscreen": "Avslutt fullskjerm",
"extensions_popover_extensions": "Utvidelser",
"extension_popover_set_error_notification": "Klarte ikke å angi aktiv utvidelse: {error}",
"extension_popover_unload_extension": "Fjern utvidelse",
"extension_popover_load_and_manage_extensions": "Last inn og administrer utvidelsene dine",
"extensions_atx_power_control": "ATX-strømstyring",
"extensions_atx_power_control_description": "Kontroller maskinens strømstatus via ATX-strømkontroll.",
"extensions_dc_power_control": "DC-strømkontroll",
"extensions_dc_power_control_description": "Kontroller DC-strømutvidelsen din",
"extension_serial_console": "Seriell konsoll",
"extension_serial_console_description": "Få tilgang til seriekonsollutvidelsen din",
"atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}",
"atx_power_control_power_button": "Makt",
"atx_power_control_short_power_button": "Kort trykk",
"atx_power_control_long_power_button": "Langt trykk",
"atx_power_control_reset_button": "Tilbakestill",
"atx_power_control_power_led": "Strøm-LED",
"atx_power_control_hdd_led": "HDD-LED",
"dc_power_control_get_state_error": "Klarte ikke å hente likestrømsstatus: {error}",
"dc_power_control_set_power_state_error": "Kunne ikke sende likestrømsstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende gjenopprettingsstatus for likestrøm til {state} : {error}",
"dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_off_button": "Slå av",
"dc_power_control_restore_power_state": "Gjenopprett strømtap",
"dc_power_control_power_on_state": "Slå PÅ",
"dc_power_control_power_off_state": "Slå av",
"dc_power_control_voltage": "Spenning",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Nåværende",
"dc_power_control_current_unit": "EN",
"dc_power_control_power": "Makt",
"dc_power_control_power_unit": "V",
"serial_console_get_state_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
"serial_console_set_power_state_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}",
"serial_console_configure_description": "Konfigurer innstillingene for seriekonsollen",
"serial_console_open_console": "Åpne konsollen",
"serial_console_baud_rate": "Baudhastighet",
"serial_console_data_bits": "Databiter",
"serial_console_stop_bits": "Stoppbiter",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Paritet",
"serial_console_parity_odd": "Oddeparitet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_space": "Romparitet",
"serial_console_get_settings_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
"serial_console_set_settings_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "nej då!",
"something_went_wrong": "Något gick fel. Försök igen senare eller kontakta supporten.",
"jetkvm_logo": "JetKVM-logotyp",
"load": "Ladda",
"unknown_error": "Okänt fel",
"action_bar_virtual_media": "Virtuella medier",
"action_bar_paste_text": "Klistra in text",
"action_bar_web_terminal": "Webbterminal",
"action_bar_wake_on_lan": "Vakna på LAN",
"action_bar_virtual_keyboard": "Virtuellt tangentbord",
"action_bar_extension": "Förlängning",
"action_bar_connection_stats": "Anslutningsstatistik",
"action_bar_settings": "Inställningar",
"action_bar_fullscreen": "Helskärm",
"action_bar_exit_fullscreen": "Avsluta helskärm",
"extensions_popover_extensions": "Tillägg",
"extension_popover_set_error_notification": "Misslyckades med att ange aktivt tillägg: {error}",
"extension_popover_unload_extension": "Avlasta tillägg",
"extension_popover_load_and_manage_extensions": "Ladda och hantera dina tillägg",
"extensions_atx_power_control": "ATX-strömkontroll",
"extensions_atx_power_control_description": "Styr din maskins strömförsörjning via ATX-strömkontroll.",
"extensions_dc_power_control": "DC-strömstyrning",
"extensions_dc_power_control_description": "Styr din DC-strömförlängning",
"extension_serial_console": "Seriell konsol",
"extension_serial_console_description": "Åtkomst till din seriella konsoltillägg",
"atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}",
"atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}",
"atx_power_control_power_button": "Driva",
"atx_power_control_short_power_button": "Kort tryck",
"atx_power_control_long_power_button": "Långt tryck",
"atx_power_control_reset_button": "Återställa",
"atx_power_control_power_led": "Ström-LED",
"atx_power_control_hdd_led": "Hårddisk-LED",
"dc_power_control_get_state_error": "Misslyckades med att hämta likströmsstatus: {error}",
"dc_power_control_set_power_state_error": "Misslyckades med att skicka likströmsstatus till {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Misslyckades med att skicka återställningsstatus för likström till {state} : {error}",
"dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_off_button": "Stäng av",
"dc_power_control_restore_power_state": "Återställ strömförlust",
"dc_power_control_power_on_state": "Slå på",
"dc_power_control_power_off_state": "Stäng av",
"dc_power_control_voltage": "Spänning",
"dc_power_control_voltage_unit": "V",
"dc_power_control_current": "Nuvarande",
"dc_power_control_current_unit": "En",
"dc_power_control_power": "Driva",
"dc_power_control_power_unit": "V",
"serial_console_get_state_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
"serial_console_set_power_state_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}",
"serial_console_configure_description": "Konfigurera dina seriella konsolinställningar",
"serial_console_open_console": "Öppna konsolen",
"serial_console_baud_rate": "Baudhastighet",
"serial_console_data_bits": "Databitar",
"serial_console_stop_bits": "Stoppbitar",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Jämn paritet",
"serial_console_parity_odd": "Udda paritet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_mark": "Markera paritet",
"serial_console_parity_space": "Rymdparitet",
"serial_console_get_settings_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
"serial_console_set_settings_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}"
}

View File

@ -0,0 +1,66 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"jetkvm": "JetKVM",
"oh_no": "噢不!",
"something_went_wrong": "出了点问题。请稍后重试或联系客服",
"jetkvm_logo": "JetKVM 徽标",
"load": "加载",
"unknown_error": "未知错误",
"action_bar_virtual_media": "虚拟媒体",
"action_bar_paste_text": "粘贴文本",
"action_bar_web_terminal": "网页终端",
"action_bar_wake_on_lan": "局域网唤醒",
"action_bar_virtual_keyboard": "虚拟键盘",
"action_bar_extension": "扩大",
"action_bar_connection_stats": "连接统计",
"action_bar_settings": "设置",
"action_bar_fullscreen": "全屏",
"action_bar_exit_fullscreen": "退出全屏",
"extensions_popover_extensions": "扩展",
"extension_popover_set_error_notification": "无法设置活动扩展:{error}",
"extension_popover_unload_extension": "卸载扩展",
"extension_popover_load_and_manage_extensions": "加载和管理您的扩展",
"extensions_atx_power_control": "ATX电源控制",
"extensions_atx_power_control_description": "通过 ATX 电源控制来控制机器的电源状态。",
"extensions_dc_power_control": "直流电源控制",
"extensions_dc_power_control_description": "控制您的直流电源扩展",
"extension_serial_console": "串行控制台",
"extension_serial_console_description": "访问串行控制台扩展",
"atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}",
"atx_power_control_send_action_error": "无法发送 ATX 电源操作{action} : {error}",
"atx_power_control_power_button": "力量",
"atx_power_control_short_power_button": "短按",
"atx_power_control_long_power_button": "长按",
"atx_power_control_reset_button": "重置",
"atx_power_control_power_led": "电源 LED",
"atx_power_control_hdd_led": "硬盘指示灯",
"dc_power_control_get_state_error": "无法获取直流电源状态:{error}",
"dc_power_control_set_power_state_error": "无法将直流电源状态发送到 {enabled} : {error}",
"dc_power_control_set_restore_state_error": "无法将直流电源恢复状态发送到 {state} : {error}",
"dc_power_control_power_on_button": "开机",
"dc_power_control_power_off_button": "关闭电源",
"dc_power_control_restore_power_state": "恢复断电",
"dc_power_control_power_on_state": "开启电源",
"dc_power_control_power_off_state": "关闭电源",
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "五",
"dc_power_control_current": "安培",
"dc_power_control_current_unit": "一个",
"dc_power_control_power": "瓦特",
"dc_power_control_power_unit": "西",
"serial_console_get_state_error": "无法获取串行控制台设置: {error}",
"serial_console_set_power_state_error": "无法将串行控制台设置设置为{settings} : {error}",
"serial_console_configure_description": "配置串行控制台设置",
"serial_console_open_console": "打开控制台",
"serial_console_baud_rate": "波特率",
"serial_console_data_bits": "数据位",
"serial_console_stop_bits": "停止位",
"serial_console_parity": "平价",
"serial_console_parity_even": "偶校验",
"serial_console_parity_odd": "奇校验",
"serial_console_parity_none": "无奇偶校验",
"serial_console_parity_mark": "马克·帕里蒂",
"serial_console_parity_space": "空间平价",
"serial_console_get_settings_error": "无法获取串行控制台设置: {error}",
"serial_console_set_settings_error": "无法将串行控制台设置设置为{settings} : {error}"
}

1236
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.01.1900", "version": "2025.10.07.1700",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.15.0" "node": "^22.15.0"
@ -11,12 +11,14 @@
"dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:ssl": "USE_SSL=true ./dev_device.sh",
"dev:cloud": "vite dev --mode=cloud-development", "dev:cloud": "vite dev --mode=cloud-development",
"build": "npm run build:prod", "build": "npm run build:prod",
"build:device": "tsc && vite build --mode=device --emptyOutDir", "build:device": "npm run paraglide && tsc && vite build --mode=device --emptyOutDir",
"build:staging": "tsc && vite build --mode=cloud-staging", "build:staging": "npm run paraglide && tsc && vite build --mode=cloud-staging",
"build:prod": "tsc && vite build --mode=cloud-production", "build:prod": "npm run paraglide && tsc && vite build --mode=cloud-production",
"lint": "eslint './src/**/*.{ts,tsx}'", "lint": "npm run paraglide && eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix", "lint:fix": "npm run paraglide && eslint './src/**/*.{ts,tsx}' --fix",
"preview": "vite preview" "paraglide": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
"validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
"machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
@ -36,13 +38,13 @@
"framer-motion": "^12.23.22", "framer-motion": "^12.23.22",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"react": "^19.1.1", "react": "^19.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.1.1", "react-dom": "^19.2.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.3", "react-router": "^7.9.3",
"react-simple-keyboard": "^3.8.125", "react-simple-keyboard": "^3.8.126",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1", "recharts": "^3.2.1",
@ -54,32 +56,37 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.37.0",
"@inlang/cli": "^3.0.12",
"@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@inlang/sdk": "^2.4.9",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.1.17", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.10", "@types/react-dom": "^19.2.1",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.3",
"@typescript-eslint/eslint-plugin": "^8.45.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.45.0", "@typescript-eslint/parser": "^8.46.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.36.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^6.1.1",
"eslint-plugin-react-refresh": "^0.4.22", "eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.9",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

View File

@ -1,24 +1,25 @@
import { Fragment, useCallback, useRef } from "react";
import { MdOutlineContentPasteGo } from "react-icons/md"; import { MdOutlineContentPasteGo } from "react-icons/md";
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
import { FaKeyboard } from "react-icons/fa6"; import { FaKeyboard } from "react-icons/fa6";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment, useCallback, useRef } from "react";
import { CommandLineIcon } from "@heroicons/react/20/solid"; import { CommandLineIcon } from "@heroicons/react/20/solid";
import { Button } from "@components/Button";
import { import {
useHidStore, useHidStore,
useMountMediaStore, useMountMediaStore,
useSettingsStore, useSettingsStore,
useUiStore, useUiStore,
} from "@/hooks/stores"; } from "@hooks/stores";
import Container from "@components/Container";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import PasteModal from "@/components/popovers/PasteModal"; import { Button } from "@components/Button";
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; import Container from "@components/Container";
import MountPopopover from "@/components/popovers/MountPopover"; import PasteModal from "@components/popovers/PasteModal";
import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import WakeOnLanModal from "@components/popovers/WakeOnLan/Index";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import MountPopopover from "@components/popovers/MountPopover";
import ExtensionPopover from "@components/popovers/ExtensionPopover";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { m } from "@localizations/messages.js";
export default function Actionbar({ export default function Actionbar({
requestFullscreen, requestFullscreen,
@ -28,10 +29,7 @@ export default function Actionbar({
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore(); const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
const { remoteVirtualMediaState } = useMountMediaStore();
const remoteVirtualMediaState = useMountMediaStore(
state => state.remoteVirtualMediaState,
);
const { developerMode } = useSettingsStore(); const { developerMode } = useSettingsStore();
// This is the only way to get a reliable state change for the popover // This is the only way to get a reliable state change for the popover
@ -64,7 +62,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Web Terminal" text={m.action_bar_web_terminal()}
LeadingIcon={({ className }) => <CommandLineIcon className={className} />} LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")} onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
/> />
@ -74,7 +72,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Paste text" text={m.action_bar_paste_text()}
LeadingIcon={MdOutlineContentPasteGo} LeadingIcon={MdOutlineContentPasteGo}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -105,7 +103,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Media" text={m.action_bar_virtual_media()}
LeadingIcon={({ className }) => { LeadingIcon={({ className }) => {
return ( return (
<> <>
@ -148,7 +146,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Wake on LAN" text={m.action_bar_wake_on_lan()}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
}} }}
@ -198,7 +196,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Keyboard" text={m.action_bar_virtual_keyboard()}
LeadingIcon={FaKeyboard} LeadingIcon={FaKeyboard}
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
/> />
@ -211,7 +209,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Extension" text={m.action_bar_extension()}
LeadingIcon={LuCable} LeadingIcon={LuCable}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -237,7 +235,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Keyboard" text={m.action_bar_virtual_keyboard()}
LeadingIcon={FaKeyboard} LeadingIcon={FaKeyboard}
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
/> />
@ -246,7 +244,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Connection Stats" text={m.action_bar_connection_stats()}
LeadingIcon={({ className }) => ( LeadingIcon={({ className }) => (
<LuSignal <LuSignal
className={cx(className, "mb-0.5 text-green-500")} className={cx(className, "mb-0.5 text-green-500")}
@ -262,7 +260,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Settings" text={m.action_bar_settings()}
LeadingIcon={LuSettings} LeadingIcon={LuSettings}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -276,7 +274,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Fullscreen" text={m.action_bar_fullscreen()}
LeadingIcon={LuMaximize} LeadingIcon={LuMaximize}
onClick={() => requestFullscreen()} onClick={() => requestFullscreen()}
/> />

View File

@ -1,13 +1,13 @@
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import LoadingSpinner from "@components/LoadingSpinner";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications"; import notifications from "@/notifications";
import LoadingSpinner from "@/components/LoadingSpinner";
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
@ -33,9 +33,7 @@ export function ATXPowerControl() {
useEffect(() => { useEffect(() => {
send("getATXState", {}, (resp: JsonRpcResponse) => { send("getATXState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setAtxState(resp.result as ATXState); setAtxState(resp.result as ATXState);
@ -56,9 +54,7 @@ export function ATXPowerControl() {
console.log("Sending long press ATX power action"); console.log("Sending long press ATX power action");
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_long_power_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
} }
setIsPowerPressed(false); setIsPowerPressed(false);
}); });
@ -77,9 +73,7 @@ export function ATXPowerControl() {
console.log("Sending short press ATX power action"); console.log("Sending short press ATX power action");
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_short_power_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
} }
}); });
} }
@ -98,8 +92,8 @@ export function ATXPowerControl() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="ATX Power Control" title={m.extensions_atx_power_control()}
description="Control your ATX power settings" description={m.extensions_atx_power_control_description()}
/> />
{atxState === null ? ( {atxState === null ? (
@ -115,7 +109,7 @@ export function ATXPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power" text={m.atx_power_control_power_button()}
onMouseDown={() => handlePowerPress(true)} onMouseDown={() => handlePowerPress(true)}
onMouseUp={() => handlePowerPress(false)} onMouseUp={() => handlePowerPress(false)}
onMouseLeave={() => handlePowerPress(false)} onMouseLeave={() => handlePowerPress(false)}
@ -125,13 +119,11 @@ export function ATXPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuRotateCcw} LeadingIcon={LuRotateCcw}
text="Reset" text={m.atx_power_control_reset_button()}
onClick={() => { onClick={() => {
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_reset_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
}); });
@ -150,7 +142,7 @@ export function ATXPowerControl() {
atxState?.power ? "text-green-600" : "text-slate-300" atxState?.power ? "text-green-600" : "text-slate-300"
}`} }`}
/> />
Power LED {m.atx_power_control_power_led()}
</span> </span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -161,7 +153,7 @@ export function ATXPowerControl() {
atxState?.hdd ? "text-blue-400" : "text-slate-300" atxState?.hdd ? "text-blue-400" : "text-slate-300"
}`} }`}
/> />
HDD LED {m.atx_power_control_hdd_led()}
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,14 +1,15 @@
import { LuPower } from "react-icons/lu";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { LuPower } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import FieldLabel from "@components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import {SelectMenuBasic} from "@components/SelectMenuBasic"; import {SelectMenuBasic} from "@components/SelectMenuBasic";
import notifications from "@/notifications";
interface DCPowerState { interface DCPowerState {
isOn: boolean; isOn: boolean;
@ -25,9 +26,7 @@ export function DCPowerControl() {
const getDCPowerState = useCallback(() => { const getDCPowerState = useCallback(() => {
send("getDCPowerState", {}, (resp: JsonRpcResponse) => { send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setPowerState(resp.result as DCPowerState); setPowerState(resp.result as DCPowerState);
@ -37,9 +36,7 @@ export function DCPowerControl() {
const handlePowerToggle = (enabled: boolean) => { const handlePowerToggle = (enabled: boolean) => {
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => { send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_set_power_state_error({ enabled: enabled, error: resp.error.data || m.unknown_error() }));
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
getDCPowerState(); // Refresh state after change getDCPowerState(); // Refresh state after change
@ -49,17 +46,13 @@ export function DCPowerControl() {
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0; // const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => { send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_set_restore_state_error({ state: state, error: resp.error.data || m.unknown_error() }));
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
getDCPowerState(); // Refresh state after change getDCPowerState(); // Refresh state after change
}); });
}; };
useEffect(() => { useEffect(() => {
getDCPowerState(); getDCPowerState();
// Set up polling interval to update status // Set up polling interval to update status
@ -70,8 +63,8 @@ export function DCPowerControl() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="DC Power Control" title={m.extensions_dc_power_control()}
description="Control your DC power settings" description={m.extensions_dc_power_control_description()}
/> />
{powerState === null ? ( {powerState === null ? (
@ -87,7 +80,7 @@ export function DCPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power On" text={m.dc_power_control_power_on_button()}
onClick={() => handlePowerToggle(true)} onClick={() => handlePowerToggle(true)}
disabled={powerState.isOn} disabled={powerState.isOn}
/> />
@ -95,7 +88,7 @@ export function DCPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power Off" text={m.dc_power_control_power_off_button()}
disabled={!powerState.isOn} disabled={!powerState.isOn}
onClick={() => handlePowerToggle(false)} onClick={() => handlePowerToggle(false)}
/> />
@ -104,13 +97,13 @@ export function DCPowerControl() {
<div className="flex items-center"> <div className="flex items-center">
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="Restore Power Loss" label={m.dc_power_control_restore_power_state()}
value={powerState.restoreState} value={powerState.restoreState}
onChange={e => handleRestoreChange(parseInt(e.target.value))} onChange={e => handleRestoreChange(parseInt(e.target.value))}
options={[ options={[
{ value: '0', label: "Power OFF" }, { value: '0', label: m.dc_power_control_power_off_state()},
{ value: '1', label: "Power ON" }, { value: '1', label: m.dc_power_control_power_on_state()},
{ value: '2', label: "Last State" }, { value: '2', label: m.dc_power_control_restore_last_state()},
]} ]}
/> />
</div> </div>
@ -120,21 +113,21 @@ export function DCPowerControl() {
{/* Status Display */} {/* Status Display */}
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Voltage" /> <FieldLabel label={m.dc_power_control_voltage()} />
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.voltage.toFixed(1)}V {powerState.voltage.toFixed(1)}&nbsp;{m.dc_power_control_voltage_unit()}
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Current" /> <FieldLabel label={m.dc_power_control_current()} />
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.current.toFixed(1)}A {powerState.current.toFixed(1)}&nbsp;{m.dc_power_control_current_unit()}
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Power" /> <FieldLabel label={m.dc_power_control_power()}/>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.power.toFixed(1)}W {powerState.power.toFixed(1)}&nbsp;{m.dc_power_control_power_unit()}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,13 +1,14 @@
import { LuTerminal } from "react-icons/lu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuTerminal } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useUiStore } from "@hooks/stores";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { useUiStore } from "@/hooks/stores";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
interface SerialSettings { interface SerialSettings {
baudRate: string; baudRate: string;
@ -28,9 +29,7 @@ export function SerialConsole() {
useEffect(() => { useEffect(() => {
send("getSerialSettings", {}, (resp: JsonRpcResponse) => { send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.serial_console_get_settings_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setSettings(resp.result as SerialSettings); setSettings(resp.result as SerialSettings);
@ -41,9 +40,7 @@ export function SerialConsole() {
const newSettings = { ...settings, [setting]: value }; const newSettings = { ...settings, [setting]: value };
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => { send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.serial_console_set_settings_error({ settings: setting, error: resp.error.data || m.unknown_error() }));
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setSettings(newSettings); setSettings(newSettings);
@ -54,8 +51,8 @@ export function SerialConsole() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Serial Console" title={m.extension_serial_console()}
description="Configure your serial console settings" description={m.serial_console_configure_description()}
/> />
<Card className="animate-fadeIn opacity-0"> <Card className="animate-fadeIn opacity-0">
@ -66,10 +63,10 @@ export function SerialConsole() {
size="SM" size="SM"
theme="primary" theme="primary"
LeadingIcon={LuTerminal} LeadingIcon={LuTerminal}
text="Open Console" text={m.serial_console_open_console()}
onClick={() => { onClick={() => {
setTerminalType("serial");
console.log("Opening serial console with settings: ", settings); console.log("Opening serial console with settings: ", settings);
setTerminalType("serial");
}} }}
/> />
</div> </div>
@ -77,7 +74,7 @@ export function SerialConsole() {
{/* Settings */} {/* Settings */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<SelectMenuBasic <SelectMenuBasic
label="Baud Rate" label={m.serial_console_baud_rate()}
options={[ options={[
{ label: "1200", value: "1200" }, { label: "1200", value: "1200" },
{ label: "2400", value: "2400" }, { label: "2400", value: "2400" },
@ -93,7 +90,7 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Data Bits" label={m.serial_console_data_bits()}
options={[ options={[
{ label: "8", value: "8" }, { label: "8", value: "8" },
{ label: "7", value: "7" }, { label: "7", value: "7" },
@ -103,7 +100,7 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Stop Bits" label={m.serial_console_stop_bits()}
options={[ options={[
{ label: "1", value: "1" }, { label: "1", value: "1" },
{ label: "1.5", value: "1.5" }, { label: "1.5", value: "1.5" },
@ -114,11 +111,13 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Parity" label={m.serial_console_parity()}
options={[ options={[
{ label: "None", value: "none" }, { label: m.serial_console_parity_none(), value: "none" },
{ label: "Even", value: "even" }, { label: m.serial_console_parity_even(), value: "even" },
{ label: "Odd", value: "odd" }, { label: m.serial_console_parity_odd(), value: "odd" },
{ label: m.serial_console_parity_mark(), value: "mark" },
{ label: m.serial_console_parity_space(), value: "space" },
]} ]}
value={settings.parity} value={settings.parity}
onChange={e => handleSettingChange("parity", e.target.value)} onChange={e => handleSettingChange("parity", e.target.value)}

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu"; import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { ATXPowerControl } from "@components/extensions/ATXPowerControl"; import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
@ -20,20 +21,20 @@ interface Extension {
const AVAILABLE_EXTENSIONS: Extension[] = [ const AVAILABLE_EXTENSIONS: Extension[] = [
{ {
id: "atx-power", id: "atx-power",
name: "ATX Power Control", name: m.extensions_atx_power_control(),
description: "Control your ATX Power extension", description: m.extensions_atx_power_control_description(),
icon: LuPower, icon: LuPower,
}, },
{ {
id: "dc-power", id: "dc-power",
name: "DC Power Control", name: m.extensions_dc_power_control(),
description: "Control your DC Power extension", description: m.extensions_dc_power_control(),
icon: LuPlugZap, icon: LuPlugZap,
}, },
{ {
id: "serial-console", id: "serial-console",
name: "Serial Console", name: m.extension_serial_console(),
description: "Access your serial console extension", description: m.extension_serial_console_description(),
icon: LuTerminal, icon: LuTerminal,
}, },
]; ];
@ -60,7 +61,7 @@ export default function ExtensionPopover() {
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => { send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set active extension: ${resp.error.data || "Unknown error"}`, m.extension_popover_set_error_notification({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
@ -101,7 +102,7 @@ export default function ExtensionPopover() {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Unload Extension" text={m.extension_popover_unload_extension()}
onClick={() => handleSetActiveExtension(null)} onClick={() => handleSetActiveExtension(null)}
/> />
</div> </div>
@ -110,8 +111,8 @@ export default function ExtensionPopover() {
// Extensions List View // Extensions List View
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Extensions" title={m.extensions_popover_extensions()}
description="Load and manage your extensions" description={m.extension_popover_load_and_manage_extensions()}
/> />
<Card className="animate-fadeIn opacity-0" > <Card className="animate-fadeIn opacity-0" >
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30"> <div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
@ -131,7 +132,7 @@ export default function ExtensionPopover() {
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Load" text={m.load()}
onClick={() => handleSetActiveExtension(extension)} onClick={() => handleSetActiveExtension(extension)}
/> />
</div> </div>

View File

@ -1,20 +1,17 @@
import { PlusCircleIcon } from "@heroicons/react/20/solid"; import { PlusCircleIcon } from "@heroicons/react/20/solid";
import { forwardRef, useEffect, useCallback } from "react"; import { forwardRef, useEffect, useCallback } from "react";
import { import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
LuLink,
LuPlus,
LuRadioReceiver,
} from "react-icons/lu";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
import { formatters } from "@/utils"; import { formatters } from "@/utils";
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores"; import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import notifications from "@/notifications"; import notifications from "@/notifications";
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => { const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const syncRemoteVirtualMediaState = useCallback(() => { const syncRemoteVirtualMediaState = useCallback(() => {
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => { send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
if ("error" in response) { if ("error" in response) {
notifications.error( notifications.error(m.mount_get_state_error({ error: response.error.message }));
`Failed to get virtual media state: ${response.error.message}`,
);
} else { } else {
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState); setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
} }
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const handleUnmount = () => { const handleUnmount = () => {
send("unmountImage", {}, (response: JsonRpcResponse) => { send("unmountImage", {}, (response: JsonRpcResponse) => {
if ("error" in response) { if ("error" in response) {
notifications.error(`Failed to unmount image: ${response.error.message}`); notifications.error(m.mount_unmount_error({ error: response.error.message }));
} else { } else {
syncRemoteVirtualMediaState(); syncRemoteVirtualMediaState();
} }
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-sm font-semibold leading-none text-black dark:text-white"> <h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No mounted media {m.mount_no_mounted_media()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a file to get started {m.mount_add_file_to_get_started()}
</p> </p>
</div> </div>
</div> </div>
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card> </Card>
</div> </div>
<h3 className="text-base font-semibold text-black dark:text-white"> <h3 className="text-base font-semibold text-black dark:text-white">
Streaming from URL {m.mount_streaming_from_url()}
</h3> </h3>
<p className="truncate text-sm text-slate-900 dark:text-slate-100"> <p className="truncate text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(url, 55)} {formatters.truncateMiddle(url, 55)}
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card> </Card>
</div> </div>
<h3 className="text-base font-semibold text-black dark:text-white"> <h3 className="text-base font-semibold text-black dark:text-white">
Mounted from JetKVM Storage {m.mount_mounted_from_storage()}
</h3> </h3>
<p className="text-sm text-slate-900 dark:text-slate-100"> <p className="text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(path, 50)} {formatters.truncateMiddle(path, 50)}
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<div className="h-full space-y-4"> <div className="h-full space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Virtual Media" title={m.mount_virtual_media()}
description="Mount an image to boot from or install an operating system." description={m.mount_virtual_media_description()}
/> />
<div <div
@ -162,10 +157,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div> </div>
{remoteVirtualMediaState ? ( {remoteVirtualMediaState ? (
<div className="flex select-none items-center justify-between text-xs"> <div className="flex select-none items-center justify-between text-xs">
<div className="select-none text-white dark:text-slate-300"> <div className="select-none text-white dark:text-slate-300">
<span>Mounted as</span>{" "} <span>{m.mount_mounted_as()}</span>{" "}
<span className="font-semibold"> <span className="font-semibold">
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"} {remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
</span> </span>
</div> </div>
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Close" text={m.close()}
onClick={() => { onClick={() => {
close(); close();
}} }}
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Unmount" text={m.mount_unmount()}
LeadingIcon={({ className }) => ( LeadingIcon={({ className }) => (
<svg <svg
className={`${className} h-2.5 w-2.5 shrink-0`} className={`${className} h-2.5 w-2.5 shrink-0`}
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Close" text={m.close()}
onClick={() => { onClick={() => {
close(); close();
}} }}
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Media" text={m.mount_add_new_media()}
onClick={() => { onClick={() => {
setModalView("mode"); setModalView("mode");
navigateTo("/mount"); navigateTo("/mount");

View File

@ -1,13 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuCornerDownLeft } from "react-icons/lu"; import { LuCornerDownLeft } from "react-icons/lu";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
@ -122,8 +123,8 @@ export default function PasteModal() {
<div className="h-full space-y-4"> <div className="h-full space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Paste text" title={m.paste_modal_paste_text()}
description="Paste text from your client to the remote host" description={m.paste_modal_paste_text_description()}
/> />
<div <div
@ -143,7 +144,7 @@ export default function PasteModal() {
> >
<TextAreaWithLabel <TextAreaWithLabel
ref={TextAreaRef} ref={TextAreaRef}
label="Paste from host" label={m.paste_modal_paste_from_host()}
rows={4} rows={4}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
maxLength={pasteMaxLength} maxLength={pasteMaxLength}
@ -176,7 +177,7 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
The following characters won&apos;t be pasted:{" "} {m.paste_modal_invalid_chars_intro()}{" "}
{invalidChars.join(", ")} {invalidChars.join(", ")}
</span> </span>
</div> </div>
@ -186,8 +187,8 @@ export default function PasteModal() {
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}> <div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
<InputFieldWithLabel <InputFieldWithLabel
type="number" type="number"
label="Delay between keys" label={m.paste_modal_delay_between_keys()}
placeholder="Delay between keys" placeholder={m.paste_modal_delay_between_keys()}
min={50} min={50}
max={65534} max={65534}
value={delayValue} value={delayValue}
@ -199,15 +200,14 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
Delay must be between 50 and 65534 {m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
</span> </span>
</div> </div>
)} )}
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {selectedKeyboard.isoCode}- {m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
{selectedKeyboard.name}
</p> </p>
</div> </div>
</div> </div>
@ -224,7 +224,7 @@ export default function PasteModal() {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Cancel" text={m.cancel()}
onClick={() => { onClick={() => {
onCancelPasteMode(); onCancelPasteMode();
close(); close();
@ -233,7 +233,7 @@ export default function PasteModal() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Confirm Paste" text={m.paste_modal_confirm_paste()}
disabled={isPasteInProgress} disabled={isPasteInProgress}
onClick={onConfirmPaste} onClick={onConfirmPaste}
LeadingIcon={LuCornerDownLeft} LeadingIcon={LuCornerDownLeft}

View File

@ -1,8 +1,9 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { LuPlus, LuArrowLeft } from "react-icons/lu"; import { LuPlus, LuArrowLeft } from "react-icons/lu";
import { InputFieldWithLabel } from "@/components/InputField"; import { m } from "@localizations/messages.js";
import { Button } from "@/components/Button"; import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button";
interface AddDeviceFormProps { interface AddDeviceFormProps {
onAddDevice: (name: string, macAddress: string) => void; onAddDevice: (name: string, macAddress: string) => void;
@ -34,8 +35,8 @@ export default function AddDeviceForm({
> >
<InputFieldWithLabel <InputFieldWithLabel
ref={nameInputRef} ref={nameInputRef}
placeholder="Plex Media Server" placeholder={m.wake_on_lan_add_device_example_device_name()}
label="Device Name" label={m.wake_on_lan_add_device_device_name()}
required required
onChange={e => { onChange={e => {
setIsDeviceNameValid(e.target.validity.valid); setIsDeviceNameValid(e.target.validity.valid);
@ -46,7 +47,7 @@ export default function AddDeviceForm({
<InputFieldWithLabel <InputFieldWithLabel
ref={macInputRef} ref={macInputRef}
placeholder="00:b0:d0:63:c2:26" placeholder="00:b0:d0:63:c2:26"
label="MAC Address" label={m.wake_on_lan_add_device_mac_address()}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
required required
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$" pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
@ -82,14 +83,14 @@ export default function AddDeviceForm({
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Back" text={m.wake_on_lan_add_device_back()}
LeadingIcon={LuArrowLeft} LeadingIcon={LuArrowLeft}
onClick={() => setShowAddForm(false)} onClick={() => setShowAddForm(false)}
/> />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Save Device" text={m.wake_on_lan_add_device_save_device()}
disabled={!isDeviceNameValid || !isMacAddressValid} disabled={!isDeviceNameValid || !isMacAddressValid}
onClick={() => { onClick={() => {
const deviceName = nameInputRef.current?.value || ""; const deviceName = nameInputRef.current?.value || "";

View File

@ -1,8 +1,9 @@
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu"; import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button"; import { m } from "@localizations/messages.js";
import Card from "@/components/Card"; import { Button } from "@components/Button";
import { FieldError } from "@/components/InputField"; import Card from "@components/Card";
import { FieldError } from "@components/InputField";
export interface StoredDevice { export interface StoredDevice {
name: string; name: string;
@ -46,7 +47,7 @@ export default function DeviceList({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Wake" text={m.wake_on_lan_device_list_wake()}
LeadingIcon={LuSend} LeadingIcon={LuSend}
onClick={() => onSendMagicPacket(device.macAddress)} onClick={() => onSendMagicPacket(device.macAddress)}
/> />
@ -55,7 +56,7 @@ export default function DeviceList({
theme="danger" theme="danger"
LeadingIcon={LuTrash2} LeadingIcon={LuTrash2}
onClick={() => onDeleteDevice(index)} onClick={() => onDeleteDevice(index)}
aria-label="Delete device" aria-label={m.wake_on_lan_device_list_delete_device()}
/> />
</div> </div>
</div> </div>
@ -69,11 +70,11 @@ export default function DeviceList({
animationDelay: "0.2s", animationDelay: "0.2s",
}} }}
> >
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} /> <Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Device" text={m.wake_on_lan_device_list_add_new_device()}
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
/> />

View File

@ -1,8 +1,9 @@
import { PlusCircleIcon } from "@heroicons/react/16/solid"; import { PlusCircleIcon } from "@heroicons/react/16/solid";
import { LuPlus } from "react-icons/lu"; import { LuPlus } from "react-icons/lu";
import Card from "@/components/Card"; import { m } from "@localizations/messages.js";
import { Button } from "@/components/Button"; import Card from "@components/Card";
import { Button } from "@components/Button";
export default function EmptyStateCard({ export default function EmptyStateCard({
onCancelWakeOnLanModal, onCancelWakeOnLanModal,
@ -25,10 +26,10 @@ export default function EmptyStateCard({
</Card> </Card>
</div> </div>
<h3 className="text-sm font-semibold leading-none text-black dark:text-white"> <h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No devices added {m.wake_on_lan_empty_no_devices_added()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a device to start using Wake-on-LAN {m.wake_on_lan_empty_add_device_to_start()}
</p> </p>
</div> </div>
</div> </div>
@ -41,11 +42,11 @@ export default function EmptyStateCard({
animationDelay: "0.2s", animationDelay: "0.2s",
}} }}
> >
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} /> <Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Device" text={m.wake_on_lan_empty_add_new_device()}
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
/> />

View File

@ -1,10 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { m } from "@localizations/messages.js";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@/hooks/stores"; import { useRTCStore, useUiStore } from "@hooks/stores";
import notifications from "@/notifications"; import notifications from "@/notifications";
import EmptyStateCard from "./EmptyStateCard"; import EmptyStateCard from "./EmptyStateCard";
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
if ("error" in resp) { if ("error" in resp) {
const isInvalid = resp.error.data?.includes("invalid MAC address"); const isInvalid = resp.error.data?.includes("invalid MAC address");
if (isInvalid) { if (isInvalid) {
setErrorMessage("Invalid MAC address"); setErrorMessage(m.wake_on_lan_invalid_mac());
} else { } else {
setErrorMessage("Failed to send Magic Packet"); setErrorMessage(m.wake_on_lan_failed_send_magic());
} }
} else { } else {
notifications.success("Magic Packet sent successfully"); notifications.success(m.wake_on_lan_magic_sent_success());
setDisableVideoFocusTrap(false); setDisableVideoFocusTrap(false);
close(); close();
} }
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => { send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
console.error("Failed to add Wake-on-LAN device:", resp.error); console.error("Failed to add Wake-on-LAN device:", resp.error);
setAddDeviceErrorMessage("Failed to add device"); setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
} else { } else {
setShowAddForm(false); setShowAddForm(false);
syncStoredDevices(); syncStoredDevices();
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
<div className="grid h-full grid-rows-(--grid-headerBody)"> <div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Wake On LAN" title={m.wake_on_lan()}
description="Send a Magic Packet to wake up a remote device." description={m.wake_on_lan_description()}
/> />
{showAddForm ? ( {showAddForm ? (

View File

@ -1,12 +1,12 @@
import { useInterval } from "usehooks-ts"; import { useInterval } from "usehooks-ts";
import SidebarHeader from "@/components/SidebarHeader"; import { m } from "@localizations/messages.js";
import { useRTCStore, useUiStore } from "@/hooks/stores"; import { createChartArray, Metric } from "@components/Metric";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import SidebarHeader from "@components/SidebarHeader";
import { useRTCStore, useUiStore } from "@hooks/stores";
import { someIterable } from "@/utils"; import { someIterable } from "@/utils";
import { createChartArray, Metric } from "../Metric";
import { SettingsSectionHeader } from "../SettingsSectionHeader";
export default function ConnectionStatsSidebar() { export default function ConnectionStatsSidebar() {
const { sidebarView, setSidebarView } = useUiStore(); const { sidebarView, setSidebarView } = useUiStore();
const { const {
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
return ( return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs"> <div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} /> <SidebarHeader title={m.connection_stats_sidebar()} setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900"> <div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4"> <div className="space-y-4">
{sidebarView === "connection-stats" && ( {sidebarView === "connection-stats" && (
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
{/* Connection Group */} {/* Connection Group */}
<div className="space-y-3"> <div className="space-y-3">
<SettingsSectionHeader <SettingsSectionHeader
title="Connection" title={m.connection_stats_connection()}
description="The connection between the client and the JetKVM." description={m.connection_stats_connection_description()}
/> />
<Metric <Metric
title="Round-Trip Time" title={m.connection_stats_round_trip_time()}
description="Round-trip time for the active ICE candidate pair between peers." description={m.connection_stats_round_trip_time_description()}
stream={iceCandidatePairStats} stream={iceCandidatePairStats}
metric="currentRoundTripTime" metric="currentRoundTripTime"
map={x => ({ map={x => ({
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
{/* Video Group */} {/* Video Group */}
<div className="space-y-3"> <div className="space-y-3">
<SettingsSectionHeader <SettingsSectionHeader
title="Video" title={m.connection_stats_video()}
description="The video stream from the JetKVM to the client." description={m.connection_stats_video_description()}
/> />
{/* RTP Jitter */} {/* RTP Jitter */}
<Metric <Metric
title="Network Stability" title={m.connection_stats_network_stability()}
badge="Jitter" badge={m.connection_stats_badge_jitter()}
badgeTheme="light" badgeTheme="light"
description="How steady the flow of inbound video packets is across the network." description={m.connection_stats_network_stability_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="jitter" metric="jitter"
map={x => ({ map={x => ({
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
{/* Playback Delay */} {/* Playback Delay */}
<Metric <Metric
title="Playback Delay" title={m.connection_stats_playback_delay()}
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly." description={m.connection_stats_playback_delay_description()}
badge="Jitter Buffer Avg. Delay" badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
badgeTheme="light" badgeTheme="light"
data={jitterBufferAvgDelayData} data={jitterBufferAvgDelayData}
gate={inboundVideoRtpStats} gate={inboundVideoRtpStats}
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
{/* Packets Lost */} {/* Packets Lost */}
<Metric <Metric
title="Packets Lost" title={m.connection_stats_packets_lost()}
description="Count of lost inbound video RTP packets." description={m.connection_stats_packets_lost_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="packetsLost" metric="packetsLost"
domain={[0, 100]} domain={[0, 100]}
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
{/* Frames Per Second */} {/* Frames Per Second */}
<Metric <Metric
title="Frames per second" title={m.connection_stats_frames_per_second()}
description="Number of inbound video frames displayed per second." description={m.connection_stats_frames_per_second_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="framesPerSecond" metric="framesPerSecond"
domain={[0, 80]} domain={[0, 80]}

View File

@ -1,6 +1,5 @@
import { lazy } from "react"; import { lazy } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css";
import { import {
createBrowserRouter, createBrowserRouter,
isRouteErrorResponse, isRouteErrorResponse,
@ -8,11 +7,13 @@ import {
RouterProvider, RouterProvider,
useRouteError, useRouteError,
} from "react-router"; } from "react-router";
import "./index.css";
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid"; import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
import { CLOUD_API, DEVICE_API } from "@/ui.config"; import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "@/api"; import api from "@/api";
import Root from "@/root"; import Root from "@/root";
import { m } from "@localizations/messages.js";
import Card from "@components/Card"; import Card from "@components/Card";
import EmptyCard from "@components/EmptyCard"; import EmptyCard from "@components/EmptyCard";
import NotFoundPage from "@components/NotFoundPage"; import NotFoundPage from "@components/NotFoundPage";
@ -28,12 +29,12 @@ import DeviceIdRename from "@routes/devices.$id.rename";
import DevicesRoute from "@routes/devices"; import DevicesRoute from "@routes/devices";
import SettingsIndexRoute from "@routes/devices.$id.settings._index"; import SettingsIndexRoute from "@routes/devices.$id.settings._index";
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index"; import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
import Notifications from "@/notifications"; import Notifications from "@/notifications";
const SignupRoute = lazy(() => import("@routes/signup")); const SignupRoute = lazy(() => import("@routes/signup"));
const LoginRoute = lazy(() => import("@routes/login")); const LoginRoute = lazy(() => import("@routes/login"));
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted")); const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session")); const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session"));
const MountRoute = lazy(() => import("./routes/devices.$id.mount")); const MountRoute = lazy(() => import("@routes/devices.$id.mount"));
const SettingsRoute = lazy(() => import("@routes/devices.$id.settings")); const SettingsRoute = lazy(() => import("@routes/devices.$id.settings"));
const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse")); const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse"));
const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard")); const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard"));
@ -404,8 +405,8 @@ function ErrorBoundary() {
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
<EmptyCard <EmptyCard
IconElm={ExclamationTriangleIcon} IconElm={ExclamationTriangleIcon}
headline="Oh no!" headline={m.oh_no()}
description="Something went wrong. Please try again later or contact support" description={m.something_went_wrong()}
BtnElm={ BtnElm={
errorMessage && ( errorMessage && (
<Card> <Card>

View File

@ -4,7 +4,6 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card"; import Card from "@/components/Card";
interface NotificationOptions { interface NotificationOptions {
duration?: number; duration?: number;
// Add other options as needed // Add other options as needed
@ -34,7 +33,7 @@ const ToastContent = ({
const notifications = { const notifications = {
success: (message: string, options?: NotificationOptions) => { success: (message: string, options?: NotificationOptions) => {
return toast.custom( return toast.custom(
t => ( (t: Toast) => (
<ToastContent <ToastContent
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />} icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
message={message} message={message}
@ -47,7 +46,7 @@ const notifications = {
error: (message: string, options?: NotificationOptions) => { error: (message: string, options?: NotificationOptions) => {
return toast.custom( return toast.custom(
t => ( (t: Toast) => (
<ToastContent <ToastContent
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />} icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
message={message} message={message}
@ -64,9 +63,9 @@ function useMaxToasts(max: number) {
useEffect(() => { useEffect(() => {
toasts toasts
.filter(t => t.visible) // Only consider visible toasts .filter((t: Toast) => t.visible) // Only consider visible toasts
.filter((_, i) => i >= max) // Is toast index over limit? .filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
.forEach(t => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation .forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation
}, [toasts, max]); }, [toasts, max]);
} }

View File

@ -1,7 +1,7 @@
import { useNavigate } from "react-router";
import { useCallback } from "react"; import { useCallback } from "react";
import { useNavigate } from "react-router";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() { export default function SettingsGeneralRebootRoute() {
@ -10,7 +10,7 @@ export default function SettingsGeneralRebootRoute() {
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary // This is where we send the RPC to the golang binary
send("reboot", {force: true}); send("reboot", { force: true });
}, [send]); }, [send]);
{ {
@ -30,10 +30,10 @@ export function Dialog({
return ( return (
<div className="pointer-events-auto relative mx-auto text-left"> <div className="pointer-events-auto relative mx-auto text-left">
<div> <div>
<ConfirmationBox <ConfirmationBox
onYes={onConfirmUpdate} onYes={onConfirmUpdate}
onNo={onClose} onNo={onClose}
/> />
</div> </div>
</div> </div>
); );

View File

@ -1,18 +1,18 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cx } from "cva";
import { redirect } from "react-router"; import { redirect } from "react-router";
import type { LoaderFunction } from "react-router"; import type { LoaderFunction } from "react-router";
import { cx } from "cva";
import api from "@/api";
import { DEVICE_API } from "@/ui.config";
import GridBackground from "@components/GridBackground"; import GridBackground from "@components/GridBackground";
import Container from "@components/Container"; import Container from "@components/Container";
import { LinkButton } from "@components/Button"; import { LinkButton } from "@components/Button";
import LogoBlueIcon from "@/assets/logo-blue.png"; import LogoBlueIcon from "@assets/logo-blue.png";
import LogoWhiteIcon from "@/assets/logo-white.svg"; import LogoWhiteIcon from "@assets/logo-white.svg";
import DeviceImage from "@/assets/jetkvm-device-still.png"; import DeviceImage from "@assets/jetkvm-device-still.png";
import LogoMark from "@/assets/logo-mark.png"; import LogoMark from "@assets/logo-mark.png";
import { DEVICE_API } from "@/ui.config"; import { m } from "@localizations/messages.js";
import api from "../api";
export interface DeviceStatus { export interface DeviceStatus {
isSetup: boolean; isSetup: boolean;
@ -49,7 +49,7 @@ export default function WelcomeRoute() {
<div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0"> <div className="animate-fadeIn animation-delay-1000 flex items-center justify-center opacity-0">
<img <img
src={LogoWhiteIcon} src={LogoWhiteIcon}
alt="JetKVM Logo" alt={m.jetkvm_logo()}
className="hidden h-[32px] dark:block" className="hidden h-[32px] dark:block"
/> />
<img <img

View File

@ -3,9 +3,14 @@
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"lib": ["ES2021", "DOM", "DOM.Iterable"], "lib": [
"ES2021",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"allowJs": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": false, "allowImportingTsExtensions": false,
@ -18,19 +23,42 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"types": ["vite/client"], "erasableSyntaxOnly": true,
"noUncheckedSideEffectImports": true,
"types": [
"vite/client"
],
/* Import Aliases */ /* Import Aliases */
"paths": { "paths": {
"@components/*": ["./src/components/*"], "@components/*": [
"@routes/*": ["./src/routes/*"], "./src/components/*"
"@assets/*": ["./src/assets/*"], ],
"@/*": ["./src/*"] "@routes/*": [
"./src/routes/*"
],
"@hooks/*": [
"./src/hooks/*"
],
"@providers/*": [
"./src/providers/*"
],
"@assets/*": [
"./src/assets/*"
],
"@localizations/*": [
"./localization/paraglide/*"
],
"@/*": [
"./src/*"
]
} }
}, },
"include": ["src"], "include": [
"src"
],
"references": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
} }
] ]
} }

View File

@ -1,11 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"skipLibCheck": true, /* Bundler mode */
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true
}, },
"include": ["vite.config.ts"] "include": [
} "vite.config.ts"
]
}

View File

@ -3,6 +3,7 @@ import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import basicSsl from "@vitejs/plugin-basic-ssl"; import basicSsl from "@vitejs/plugin-basic-ssl";
import { paraglideVitePlugin } from "@inlang/paraglide-js";
declare const process: { declare const process: {
env: { env: {
@ -22,10 +23,20 @@ export default defineConfig(({ mode, command }) => {
tsconfigPaths(), tsconfigPaths(),
react() react()
]; ];
if (useSSL) { if (useSSL) {
plugins.push(basicSsl()); plugins.push(basicSsl());
} }
plugins.push(paraglideVitePlugin({
project: "./localization/jetKVM.UI.inlang",
outdir: "./localization/paraglide",
outputStructure: 'message-modules',
cookieName: 'JETKVM_LOCALE',
localStorageKey: 'JETKVM_LOCALE',
strategy: ['cookie', 'localStorage', 'preferredLanguage', 'baseLocale'],
}))
return { return {
plugins, plugins,
esbuild: { esbuild: {