17 KiB
JetKVM Development Guide
Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
Get Started
Prerequisites
- A JetKVM device (for full development)
- Go 1.24.4+ and Node.js 22.15.0
- Git for version control
- SSH access to your JetKVM device
Development Environment
Recommended: Development is best done on Linux or macOS.
If you're using Windows, we strongly recommend using WSL (Windows Subsystem for Linux) for the best development experience:
This ensures compatibility with shell scripts and build tools used in the project.
Project Setup
-
Clone the repository:
git clone https://github.com/jetkvm/kvm.git cd kvm -
Check your tools:
go version && node --version -
Find your JetKVM IP address (check your router or device screen)
-
Deploy and test:
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP -
Open in browser:
http://192.168.1.100
That's it! You're now running your own development version of JetKVM.
Common Tasks
Modify the UI
cd ui
npm install
./dev_device.sh 192.168.1.100 # Replace with your device IP
Now edit files in ui/src/ and see changes live in your browser!
Modify the backend
# Edit Go files (config.go, web.go, etc.)
./dev_deploy.sh -r 192.168.1.100 --skip-ui-build
Run tests
./dev_deploy.sh -r 192.168.1.100 --run-go-tests
View logs
ssh root@192.168.1.100
tail -f /var/log/jetkvm.log
Project Layout
/kvm/
├── main.go # App entry point
├── config.go # Settings & configuration
├── display.go # Device UI control
├── web.go # API endpoints
├── cmd/ # Command line main
├── internal/ # Internal Go packages
│ ├── confparser/ # Configuration file implementation
│ ├── hidrpc/ # HIDRPC implementation for HID devices (keyboard, mouse, etc.)
│ ├── logging/ # Logging implementation
│ ├── mdns/ # mDNS implementation
│ ├── native/ # CGO / Native code glue layer (on-device hardware)
│ │ ├── cgo/ # C files for the native library (HDMI, Touchscreen, etc.)
│ │ └── eez/ # EEZ Studio Project files (for Touchscreen)
│ ├── network/ # Network implementation
│ ├── timesync/ # Time sync/NTP implementation
│ ├── tzdata/ # Timezone data and generation
│ ├── udhcpc/ # DHCP implementation
│ ├── usbgadget/ # USB gadget
│ ├── utils/ # SSH handling
│ └── websecure/ # TLS certificate management
├── resource/ # netboot iso and other resources
├── scripts/ # Bash shell scripts for building and deploying
└── static/ # (react client build output)
└── ui/ # React frontend
├── localization/ # Client UI localization (i18n)
│ ├── jetKVM.UI.inlang/ # Settings for inlang
│ └── messages/ # Messages localized
├── public/ # UI website static images and fonts
└── src/ # Client React UI
├── assets/ # UI in-page images
├── components/ # UI components
├── 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:
web.go- Add new API endpoints hereconfig.go- Add new settings hereui/src/routes/- Add new pages hereui/src/components/- Add new UI components here
Development Modes
Full Development (Recommended)
Best for: Complete feature development
# Deploy everything to your JetKVM device
./dev_deploy.sh -r <YOUR_DEVICE_IP>
Frontend Only
Best for: UI changes without device
cd ui
npm install
./dev_device.sh <YOUR_DEVICE_IP>
Touchscreen Changes
Please click the Build button in EEZ Studio then run ./dev_deploy.sh -r <YOUR_DEVICE_IP> --skip-ui-build to deploy the changes to your device. Initial build might take more than 10 minutes as it will also need to fetch and build LVGL and other dependencies.
Quick Backend Changes
Best for: API or backend logic changes
# Skip frontend build for faster deployment
./dev_deploy.sh -r <YOUR_DEVICE_IP> --skip-ui-build
Debugging Made Easy
Check if everything is working
# Test connection to device
ping 192.168.1.100
# Check if JetKVM is running
ssh root@192.168.1.100 ps aux | grep jetkvm
View live logs
ssh root@192.168.1.100
tail -f /var/log/jetkvm.log
Reset everything (if stuck)
ssh root@192.168.1.100
rm /userdata/kvm_config.json
systemctl restart jetkvm
Testing Your Changes
Manual Testing
- Deploy your changes:
./dev_deploy.sh -r <IP> - Open browser:
http://<IP> - Test your feature
- Check logs:
ssh root@<IP> tail -f /var/log/jetkvm.log
Automated Testing
# Run all tests
./dev_deploy.sh -r <IP> --run-go-tests
# Frontend linting
cd ui && npm run lint
API Testing
# Test login endpoint
curl -X POST http://<IP>/auth/password-local \
-H "Content-Type: application/json" \
-d '{"password": "test123"}'
Common Issues & Solutions
"Build failed" or "Permission denied"
# Fix permissions
ssh root@<IP> chmod +x /userdata/jetkvm/bin/jetkvm_app_debug
# Clean and rebuild
go clean -modcache
go mod tidy
make build_dev
"Can't connect to device"
# Check network
ping <IP>
# Check SSH
ssh root@<IP> echo "Connection OK"
"Frontend not updating"
# Clear cache and rebuild
cd ui
npm cache clean --force
rm -rf node_modules
npm install
"Device UI Fails to Build"
If while trying to build you run into an error message similar to :
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
#include "ui/ui.h"
^~~~~~~~~
compilation terminated.
This means that your system didn't create the directory-link to from ./internal/native/cgo/ui to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if ./internal/native/cgo/ui appears as a plain text file with only the textual contents:
../eez/src/ui
If this happens to you need to enable git creation of symbolic links either globally or for the KVM repository:
# Globally enable git to create symlinks
git config --global core.symlinks true
git restore internal/native/cgo/ui
# Enable git to create symlinks only in this project
git config core.symlinks true
git restore internal/native/cgo/ui
Or if you want to manually create the symlink use:
# linux
cd internal/native/cgo
rm ui
ln -s ../eez/src/ui ui
rem Windows
cd internal/native/cgo
del ui
mklink /d ui ..\eez\src\ui
Next Steps
Adding a New Feature
- Backend: Add API endpoint in
web.go - Config: Add settings in
config.go - Frontend: Add UI in
ui/src/routes/ - Test: Deploy and test with
./dev_deploy.sh
Code Style
- Go: Follow standard Go conventions
- TypeScript: Use TypeScript for type safety
- React: Keep components small and reusable
- Localization: Ensure all user-facing strings in the frontend are localized
Environment Variables
# Enable debug logging
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
# Frontend development
export JETKVM_PROXY_URL="ws://<IP>"
Need Help?
- Check logs first:
ssh root@<IP> tail -f /var/log/jetkvm.log - Search issues: GitHub Issues
- Ask on Discord: JetKVM Discord
- Read docs: JetKVM Documentation
Contributing
Ready to contribute?
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
Before submitting
- Code works on device
- Tests pass
- Code follows style guidelines
- Frontend user-facing strings localized
- Documentation updated (if needed)
Advanced Topics
Performance Profiling
- Enable
Developer Modeon your JetKVM device - Add a password on the
Accesstab
# Access profiling
curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/
Advanced Environment Variables
# Enable trace logging (useful for debugging)
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
# For frontend development
export JETKVM_PROXY_URL="ws://<JETKVM_IP>"
# Enable SSL in development
export USE_SSL=true
Configuration Management
The application uses a JSON configuration file stored at /userdata/kvm_config.json.
Adding New Configuration Options
-
Update the Config struct in
config.go:type Config struct { // ... existing fields NewFeatureEnabled bool `json:"new_feature_enabled"` } -
Update the default configuration:
var defaultConfig = &Config{ // ... existing defaults NewFeatureEnabled: false, } -
Add migration logic if needed for existing installations
LVGL Build
We modified the LVGL code a little bit to remove unused fonts and examples. The patches are generated by
git diff --cached --diff-filter=d > ../internal/native/cgo/lvgl-minify.patch && \
git diff --name-only --diff-filter=D --cached > ../internal/native/cgo/lvgl-minify.del
Localization
The browser/client frontend uses the paraglide-js plug-in from the inlang.com project to allow compile-time validated localization of all user-facing UI strings in the browser/client UI. This includes title, text, name, description, placeholder, label, aria-label, message attributes (such as confirmText, unit, badge, tag, or flag), HTML element text (such as <h?>, <span>, or <p> elements), notifications messages, and option label strings, etc.
We do not translate the console log messages, CSS class names, theme names, nor the various value strings (e.g. for value/label pair options), nor URL routes.
The localizations are stored in .json files in the ui/localizations/messages directory, with one language-per-file using the ISO 3166-1 alpha-2 country code (e.g. en for English, de for German, etc.)
m-function-matcher
The translations are extracted into language files (e.g. en.json for English) and then paraglide-js compiles them into helpers for use with the m-function-matcher. An example:
<SettingsPageHeader
title={m.extensions_atx_power_control()}
description={m.extensions_atx_power_control_description()}
/>
shakespere plug-in
If you enable the Sherlock plug-in, the localized text "tooltip" is shown in the VSCode editor after any localized text in the language you've selected for preview. In this image, it's the blue text at the end of the line :
Process
Localizing a UI
-
Locate a string that is visible to the end user on the client/browser
-
Assign that string a "key" that reflects the logical meaning of the string in snake-case (look at existing localizations for examples), for example if there's a string
This is a teston the thing edit page it would be "thing_edit_this_is_a_test""thing_edit_this_is_a_test": "This is a test", -
Add the key and string to the en.json like this:
- Note if the string has replacement parameters (line a user-entered name), the syntax for the localized string has
{ }around the replacement token (e.g. This is your name: {name}). An complex example:
{m.mount_button_showing_results({ from: indexOfFirstFile + 1, to: Math.min(indexOfLastFile, onStorageFiles.length), total: onStorageFiles.length })} - Note if the string has replacement parameters (line a user-entered name), the syntax for the localized string has
-
Save the en.json file and execute
npm run i18nto resort the language files, validate the translations, and create the m-functions -
Edit the .tsx file and replace the string with the calls to the new m-function which will be the key-string you chose in snake-case. For example
This is a testin thing edit page turns intom.thing_edit_this_is_a_test()- Note if the string has a replacement token, supply that to the m-function, for example for the literal
I will call you {name}, usem.profile_i_will_call_you({ name: edit.value })
- Note if the string has a replacement token, supply that to the m-function, for example for the literal
-
When all your strings are extracted, run
npm run i18n:machine-translateto get a first-stab at the translations for the other supported languages. Make sure you use an LLM (you can use aifiesta to use multiple LLMs) or a translator of some form to back-translate each new machine-generation in each language to ensure those terms translate reasonably.
Adding a new language
- Get the ISO 3166-1 alpha-2 country code (for example AT for Austria)
- Create a new file in the ui/localization/messages directory (example at.json)
- Add the new country code to the ui/localizations/settings.json file in both the
"locales"and the"languageTags"section (inlang and Sherlock aren't exactly current to each other, so we need it in both places). - That file also declares the baseLocale/sourceLanguageTag which is
"en"because this project started out in English. Do NOT change that. - Run
npm run i18n:machine-translateto do an initial pass at localizing all existing messages to the new language.- Note you will get an error DB has been closed, ignore that message, we're not using a database.
- Note you likely will get errors while running this command due to rate limits and such (it uses anonymous Google Translate). Just keep running the command over and over... it'll translate a bunch each time until it says Machine translate complete
Other notes
- Run
npm run i18n:validateto ensure that language files and settings are well-formed. - Run
npm run i18n:find-excessto look for extra keys in other language files that have been deleted from the master-list in en.json. - Run
npm run i18n:find-dupesto look for multiple keys in en.json that have the same translated value (this is normal) - Run
npm run i18n:find-unusedto look for keys in en.json that are not referenced in the UI anywhere.- Note there are a few that are not currently used, only concern yourself with ones you obsoleted.
- Run
npm run i18n:auditto do all the above checks. - Using inlang CLI to support the npm commands.
- You can install the Sherlock VS Code extension in your devcontainer.
Happy coding!
For more information, visit the JetKVM Documentation or join our Discord Server.