├── bin └── .gitkeep ├── internal ├── native │ ├── ctrl.h │ ├── cgo │ │ ├── ui │ │ ├── .gitignore │ │ ├── log_handler.c │ │ ├── lvgl_defconfig │ │ ├── log_handler.h │ │ ├── edid.h │ │ ├── main.h │ │ ├── ui_index.h │ │ ├── video.h │ │ ├── screen.h │ │ └── ui_index.gen.sh │ ├── eez │ │ ├── .gitignore │ │ └── src │ │ │ └── ui │ │ │ ├── structs.h │ │ │ ├── fonts.h │ │ │ ├── images.c │ │ │ ├── ui.h │ │ │ ├── vars.h │ │ │ ├── images.h │ │ │ ├── actions.h │ │ │ ├── vars.c │ │ │ └── ui.c │ ├── single.go │ ├── log.go │ ├── README.md │ ├── proto │ │ └── README.md │ ├── interface.go │ └── chan.go ├── mdns │ └── utils.go ├── usbgadget │ ├── consts.go │ ├── hid.go │ ├── log.go │ └── mass_storage.go ├── websecure │ ├── log.go │ ├── ed25519_test.go │ └── utils.go ├── timesync │ ├── rtc_notlinux.go │ └── rtc.go ├── sync │ ├── once.go │ ├── waitgroup.go │ ├── mutex.go │ └── release.go ├── ota │ ├── testdata │ │ └── ota │ │ │ ├── without_certs.json │ │ │ ├── no_components.json │ │ │ ├── app_only_upgrade.json │ │ │ ├── system_only_upgrade.json │ │ │ ├── both_upgrade.json │ │ │ ├── app_only_downgrade.json │ │ │ ├── system_only_downgrade.json │ │ │ └── both_downgrade.json │ ├── errors.go │ └── app.go ├── logging │ ├── root.go │ ├── utils.go │ └── pion.go ├── confparser │ └── utils.go ├── supervisor │ └── consts.go ├── network │ └── types │ │ └── resolvconf.go ├── tzdata │ └── gen.go └── utils │ ├── env_test.go │ ├── ssh.go │ └── env.go ├── dev_deploy.sh ├── ui ├── localization │ └── jetKVM.UI.inlang │ │ ├── .gitignore │ │ ├── project_id │ │ └── settings.json ├── public │ ├── sse.html │ ├── favicon.ico │ ├── favicon.png │ ├── favicon-96x96.png │ ├── apple-touch-icon.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ ├── fonts │ │ ├── CircularXXWeb-Black.woff2 │ │ ├── CircularXXWeb-Bold.woff2 │ │ ├── CircularXXWeb-Book.woff2 │ │ ├── CircularXXWeb-Light.woff2 │ │ ├── CircularXXWeb-Thin.woff2 │ │ ├── CircularXXWeb-Italic.woff2 │ │ ├── CircularXXWeb-Medium.woff2 │ │ ├── CircularXXWeb-Regular.woff2 │ │ ├── CircularXXWeb-BoldItalic.woff2 │ │ ├── CircularXXWeb-BookItalic.woff2 │ │ ├── CircularXXWeb-ExtraBlack.woff2 │ │ ├── CircularXXWeb-ThinItalic.woff2 │ │ ├── CircularXXWeb-BlackItalic.woff2 │ │ ├── CircularXXWeb-LightItalic.woff2 │ │ ├── CircularXXWeb-MediumItalic.woff2 │ │ └── CircularXXWeb-ExtraBlackItalic.woff2 │ ├── site.webmanifest │ ├── jetkvm.svg │ └── favicon.svg ├── vite-env.d.ts ├── postcss.config.js ├── src │ ├── assets │ │ ├── arch-icon.png │ │ ├── kali-icon.png │ │ ├── logo-blue.png │ │ ├── logo-mark.png │ │ ├── centos-icon.png │ │ ├── coreos-icon.png │ │ ├── debian-icon.png │ │ ├── fedora-icon.png │ │ ├── gentoo-icon.png │ │ ├── opensuse-icon.png │ │ ├── ubuntu-icon.png │ │ ├── windows-icon.png │ │ ├── monitor-connected.png │ │ ├── jetkvm-device-still.png │ │ ├── keyboard-and-mouse-connected.png │ │ ├── mouse-icon.svg │ │ ├── detach-icon.svg │ │ └── pointing-finger.svg │ ├── root.tsx │ ├── constants │ │ └── macros.ts │ ├── cva.config.ts │ ├── utils │ │ └── ip.ts │ ├── components │ │ ├── SettingsNestedSection.tsx │ │ ├── SettingsSectionHeader.tsx │ │ ├── NestedSettingsGroup.tsx │ │ ├── ExtLink.tsx │ │ ├── NotFoundPage.tsx │ │ ├── CardHeader.tsx │ │ ├── SettingsPageheader.tsx │ │ ├── Container.tsx │ │ ├── LoadingSpinner.tsx │ │ ├── FeatureFlag.tsx │ │ ├── Fieldset.tsx │ │ ├── FailSafeModeBanner.tsx │ │ ├── AutoHeight.tsx │ │ ├── SimpleNavbar.tsx │ │ ├── CustomTooltip.tsx │ │ ├── Card.tsx │ │ ├── GridBackground.tsx │ │ ├── EmptyCard.tsx │ │ ├── StatusCards.tsx │ │ ├── useCopyToClipBoard.tsx │ │ ├── FieldLabel.tsx │ │ ├── MacroBar.tsx │ │ ├── UpdateInProgressStatusCard.tsx │ │ ├── SettingsItem.tsx │ │ ├── UpdatingStatusCard.tsx │ │ ├── Modal.tsx │ │ ├── TextArea.tsx │ │ ├── PeerConnectionStatusCard.tsx │ │ ├── popovers │ │ │ └── WakeOnLan │ │ │ │ └── EmptyStateCard.tsx │ │ └── StepCounter.tsx │ ├── providers │ │ ├── FeatureFlagContext.tsx │ │ └── FeatureFlagProvider.tsx │ ├── routes │ │ ├── devices.$id.settings._index.tsx │ │ ├── login.tsx │ │ ├── signup.tsx │ │ ├── adopt.tsx │ │ ├── devices.already-adopted.tsx │ │ ├── devices.$id.other-session.tsx │ │ ├── devices.$id.settings.appearance.tsx │ │ └── devices.$id.settings.macros.add.tsx │ ├── hooks │ │ ├── useFeatureFlag.ts │ │ ├── useKeyboardLayout.ts │ │ ├── useAppNavigation.ts │ │ └── useVersion.tsx │ ├── ui.config.ts │ ├── api.ts │ ├── keyboardLayouts │ │ └── fr_CH.ts │ ├── keyboardLayouts.ts │ └── webrtc.d.ts ├── .env.cloud-development ├── .env.cloud-production ├── .env.cloud-staging ├── .prettierrc ├── tsconfig.node.json ├── .gitignore ├── playwright.config.ts ├── dev_device.sh ├── tsconfig.json ├── tools │ └── resort_messages.py ├── tailwind.config.js ├── e2e │ └── led-roundtrip.spec.ts └── vite.config.ts ├── resource ├── netboot.xyz-multiarch.iso ├── embed.go └── dev_test.sh ├── pkg └── nmlite │ ├── netlink.go │ ├── link │ ├── types.go │ ├── consts.go │ ├── sysctl.go │ └── utils.go │ ├── udhcpc │ ├── options.go │ └── parser_test.go │ ├── jetdhcpc │ ├── utils.go │ ├── logging.go │ └── legacy.go │ └── utils.go ├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── bugfix.md │ └── feature.md ├── ISSUE_TEMPLATE │ ├── config.yml │ └── feature.yml ├── dependabot.yml └── workflows │ ├── ui-lint.yml │ └── golangci-lint.yml ├── block_device_notlinux.go ├── prometheus.go ├── .vscode ├── c_cpp_properties.json ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── Dockerfile.build ├── mdns.go ├── block_device_linux.go ├── scripts ├── ci_helper.sh ├── generate_proto.sh ├── build_cgo.sh └── configure_vscode.py ├── .golangci.yml ├── .devcontainer ├── install-deps.sh ├── docker │ └── devcontainer.json └── podman │ └── devcontainer.json ├── publish_source.sh ├── log.go ├── version.go ├── dc_metrics.go ├── timesync.go ├── wol.go └── terminal.go /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/native/ctrl.h: -------------------------------------------------------------------------------- 1 | cgo/ctrl.h -------------------------------------------------------------------------------- /dev_deploy.sh: -------------------------------------------------------------------------------- 1 | scripts/dev_deploy.sh -------------------------------------------------------------------------------- /internal/native/cgo/ui: -------------------------------------------------------------------------------- 1 | ../eez/src/ui -------------------------------------------------------------------------------- /internal/native/eez/.gitignore: -------------------------------------------------------------------------------- 1 | src/ui -------------------------------------------------------------------------------- /internal/mdns/utils.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | -------------------------------------------------------------------------------- /ui/localization/jetKVM.UI.inlang/.gitignore: -------------------------------------------------------------------------------- 1 | cache -------------------------------------------------------------------------------- /ui/public/sse.html: -------------------------------------------------------------------------------- 1 | ../../internal/logging/sse.html -------------------------------------------------------------------------------- /ui/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/localization/jetKVM.UI.inlang/project_id: -------------------------------------------------------------------------------- 1 | TI1a2RjjH4qkImNj0w -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/favicon.png -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /ui/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/favicon-96x96.png -------------------------------------------------------------------------------- /ui/src/assets/arch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/arch-icon.png -------------------------------------------------------------------------------- /ui/src/assets/kali-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/kali-icon.png -------------------------------------------------------------------------------- /ui/src/assets/logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/logo-blue.png -------------------------------------------------------------------------------- /ui/src/assets/logo-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/logo-mark.png -------------------------------------------------------------------------------- /ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/src/assets/centos-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/centos-icon.png -------------------------------------------------------------------------------- /ui/src/assets/coreos-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/coreos-icon.png -------------------------------------------------------------------------------- /ui/src/assets/debian-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/debian-icon.png -------------------------------------------------------------------------------- /ui/src/assets/fedora-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/fedora-icon.png -------------------------------------------------------------------------------- /ui/src/assets/gentoo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/gentoo-icon.png -------------------------------------------------------------------------------- /ui/src/assets/opensuse-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/opensuse-icon.png -------------------------------------------------------------------------------- /ui/src/assets/ubuntu-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/ubuntu-icon.png -------------------------------------------------------------------------------- /ui/src/assets/windows-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/windows-icon.png -------------------------------------------------------------------------------- /resource/netboot.xyz-multiarch.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/resource/netboot.xyz-multiarch.iso -------------------------------------------------------------------------------- /ui/src/assets/monitor-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/monitor-connected.png -------------------------------------------------------------------------------- /ui/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /ui/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /ui/src/assets/jetkvm-device-still.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/jetkvm-device-still.png -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Black.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Bold.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Book.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Light.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Thin.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Italic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Medium.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-Regular.woff2 -------------------------------------------------------------------------------- /internal/native/cgo/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | deps 3 | ui_index.c 4 | include/lvgl 5 | lib 6 | # Makefile is generated by CMake 7 | Makefile -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-BoldItalic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-BookItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-BookItalic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-ExtraBlack.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-ExtraBlack.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-ThinItalic.woff2 -------------------------------------------------------------------------------- /ui/src/assets/keyboard-and-mouse-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/src/assets/keyboard-and-mouse-connected.png -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-BlackItalic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-LightItalic.woff2 -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-MediumItalic.woff2 -------------------------------------------------------------------------------- /ui/src/root.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | function Root() { 4 | return ; 5 | } 6 | 7 | export default Root; 8 | -------------------------------------------------------------------------------- /resource/embed.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed netboot.xyz-multiarch.iso 8 | var ResourceFS embed.FS 9 | -------------------------------------------------------------------------------- /ui/public/fonts/CircularXXWeb-ExtraBlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jetkvm/kvm/HEAD/ui/public/fonts/CircularXXWeb-ExtraBlackItalic.woff2 -------------------------------------------------------------------------------- /internal/usbgadget/consts.go: -------------------------------------------------------------------------------- 1 | package usbgadget 2 | 3 | import "time" 4 | 5 | const dwc3Path = "/sys/bus/platform/drivers/dwc3" 6 | 7 | const hidWriteTimeout = 10 * time.Millisecond 8 | -------------------------------------------------------------------------------- /pkg/nmlite/netlink.go: -------------------------------------------------------------------------------- 1 | package nmlite 2 | 3 | import "github.com/jetkvm/kvm/pkg/nmlite/link" 4 | 5 | func getNetlinkManager() *link.NetlinkManager { 6 | return link.GetNetlinkManager() 7 | } 8 | -------------------------------------------------------------------------------- /ui/.env.cloud-development: -------------------------------------------------------------------------------- 1 | # No need for VITE_CLOUD_APP it's only needed for the device build 2 | 3 | # We use this for all the cloud API requests from the browser 4 | VITE_CLOUD_API=http://localhost:3000 5 | -------------------------------------------------------------------------------- /ui/.env.cloud-production: -------------------------------------------------------------------------------- 1 | # No need for VITE_CLOUD_APP it's only needed for the device build 2 | 3 | # We use this for all the cloud API requests from the browser 4 | VITE_CLOUD_API=https://api.jetkvm.com 5 | -------------------------------------------------------------------------------- /ui/.env.cloud-staging: -------------------------------------------------------------------------------- 1 | # No need for VITE_CLOUD_APP it's only needed for the device build 2 | 3 | # We use this for all the cloud API requests from the browser 4 | VITE_CLOUD_API=https://staging-api.jetkvm.com 5 | -------------------------------------------------------------------------------- /internal/websecure/log.go: -------------------------------------------------------------------------------- 1 | package websecure 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger() 10 | -------------------------------------------------------------------------------- /ui/src/constants/macros.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_DELAY = 50; 2 | export const MAX_STEPS_PER_MACRO = 10; 3 | export const MAX_KEYS_PER_STEP = 10; 4 | export const MAX_TOTAL_MACROS = 25; 5 | export const COPY_SUFFIX = "(copy)"; -------------------------------------------------------------------------------- /ui/src/cva.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cva"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export const { cva, cx, compose } = defineConfig({ 5 | hooks: { 6 | onComplete: className => twMerge(className), 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /internal/usbgadget/hid.go: -------------------------------------------------------------------------------- 1 | package usbgadget 2 | 3 | import "time" 4 | 5 | func (u *UsbGadget) resetUserInputTime() { 6 | u.lastUserInput = time.Now() 7 | } 8 | 9 | func (u *UsbGadget) GetLastUserInputTime() time.Time { 10 | return u.lastUserInput 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | static/* 3 | .idea 4 | .DS_Store 5 | 6 | .cache 7 | .vite 8 | .pnpm-store 9 | 10 | device-tests.tar.gz 11 | node_modules 12 | 13 | # generated during the build process 14 | #internal/native/include 15 | #internal/native/lib 16 | 17 | ui/reports -------------------------------------------------------------------------------- /pkg/nmlite/link/types.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // IPv4Address represents an IPv4 address and its gateway 8 | type IPv4Address struct { 9 | Address net.IPNet 10 | Gateway net.IP 11 | Secondary bool 12 | Permanent bool 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/utils/ip.ts: -------------------------------------------------------------------------------- 1 | export const netMaskFromCidr4 = (cidr: number) => { 2 | const mask = []; 3 | let bitCount = cidr; 4 | for(let i=0; i<4; i++) { 5 | const n = Math.min(bitCount, 8); 6 | mask.push(256 - Math.pow(2, 8-n)); 7 | bitCount -= n; 8 | } 9 | return mask.join('.'); 10 | }; -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bugfix.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ### Summary 4 | - What changed and why in 1–3 sentences. 5 | 6 | ### Checklist 7 | - [ ] Linked to issue(s) above by issue number (e.g. `Closes #`) 8 | - [ ] One problem per PR (no unrelated changes) 9 | - [ ] Lints pass; CI green 10 | -------------------------------------------------------------------------------- /ui/src/components/SettingsNestedSection.tsx: -------------------------------------------------------------------------------- 1 | export default function SettingsNestedSection({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /block_device_notlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package kvm 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func (d *NBDDevice) runClientConn() { 10 | d.l.Error().Msg("platform not supported") 11 | os.Exit(1) 12 | } 13 | 14 | func (d *NBDDevice) Close() { 15 | d.l.Error().Msg("platform not supported") 16 | os.Exit(1) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/nmlite/udhcpc/options.go: -------------------------------------------------------------------------------- 1 | package udhcpc 2 | 3 | func (u *DHCPClient) GetNtpServers() []string { 4 | if u.lease == nil { 5 | return nil 6 | } 7 | servers := make([]string, len(u.lease.NTPServers)) 8 | for i, server := range u.lease.NTPServers { 9 | servers[i] = server.String() 10 | } 11 | return servers 12 | } 13 | -------------------------------------------------------------------------------- /internal/timesync/rtc_notlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package timesync 4 | 5 | import ( 6 | "errors" 7 | "time" 8 | ) 9 | 10 | func (t *TimeSync) readRtcTime() (time.Time, error) { 11 | return time.Now(), nil 12 | } 13 | 14 | func (t *TimeSync) setRtcTime(tu time.Time) error { 15 | return errors.New("not supported") 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/providers/FeatureFlagContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import { FeatureFlagContextType } from "./FeatureFlagProvider"; 4 | 5 | // Create the context 6 | 7 | export const FeatureFlagContext = createContext({ 8 | appVersion: null, 9 | isFeatureEnabled: () => false, 10 | }); 11 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "useTabs": false, 6 | "arrowParens": "avoid", 7 | "singleQuote": false, 8 | "plugins": ["prettier-plugin-tailwindcss"], 9 | "tailwindFunctions": ["clsx", "cx"], 10 | "printWidth": 90, 11 | "tailwindStylesheet": "./src/index.css" 12 | } 13 | -------------------------------------------------------------------------------- /pkg/nmlite/link/consts.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | const ( 4 | // AfUnspec is the unspecified address family constant 5 | AfUnspec = 0 6 | // AfInet is the IPv4 address family constant 7 | AfInet = 2 8 | // AfInet6 is the IPv6 address family constant 9 | AfInet6 = 10 10 | 11 | sysctlBase = "/proc/sys" 12 | sysctlFileMode = 0640 13 | ) 14 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": [ 11 | "vite.config.ts", 12 | "playwright.config.ts", 13 | "e2e/**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | 3 | contact_links: 4 | - name: Hardware Issues 5 | url: https://jetkvm.com/contact 6 | about: If your hardware is not powering on or is not working, please contact us. 7 | 8 | - name: Discord 9 | url: https://jetkvm.com/discord 10 | about: Engage with the JetKVM team and other community members. -------------------------------------------------------------------------------- /internal/native/eez/src/ui/structs.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_STRUCTS_H 2 | #define EEZ_LVGL_UI_STRUCTS_H 3 | 4 | 5 | 6 | #if defined(EEZ_FOR_LVGL) 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "vars.h" 13 | 14 | using namespace eez; 15 | 16 | 17 | 18 | 19 | 20 | #endif 21 | 22 | #endif /*EEZ_LVGL_UI_STRUCTS_H*/ 23 | -------------------------------------------------------------------------------- /internal/native/single.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import "sync" 4 | 5 | var ( 6 | instance *Native 7 | instanceLock sync.RWMutex 8 | ) 9 | 10 | func setInstance(n *Native) { 11 | instanceLock.Lock() 12 | defer instanceLock.Unlock() 13 | 14 | if instance == nil { 15 | instance = n 16 | } 17 | 18 | if instance != n { 19 | panic("instance is already set") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | test-results 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui/src/assets/mouse-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/sync/once.go: -------------------------------------------------------------------------------- 1 | //go:build synctrace 2 | 3 | package sync 4 | 5 | import ( 6 | gosync "sync" 7 | ) 8 | 9 | // Once is a wrapper around the sync.Once 10 | type Once struct { 11 | mu gosync.Once 12 | } 13 | 14 | // Do calls the function f if and only if Do has not been called before for this instance of Once. 15 | func (o *Once) Do(f func()) { 16 | logTrace("Doing once") 17 | o.mu.Do(f) 18 | } 19 | -------------------------------------------------------------------------------- /internal/native/log.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "github.com/jetkvm/kvm/internal/logging" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | var nativeLogger = logging.GetSubsystemLogger("native") 9 | var displayLogger = logging.GetSubsystemLogger("display") 10 | 11 | type nativeLogMessage struct { 12 | Level zerolog.Level 13 | Message string 14 | File string 15 | FuncName string 16 | Line int 17 | } 18 | -------------------------------------------------------------------------------- /prometheus.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 6 | "github.com/prometheus/common/version" 7 | ) 8 | 9 | func initPrometheus() { 10 | // A Prometheus metrics endpoint. 11 | version.Version = builtAppVersion 12 | prometheus.MustRegister(versioncollector.NewCollector("jetkvm")) 13 | } 14 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/without_certs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Without Certs", 3 | "localMetadata": { 4 | "systemVersion": "0.2.5", 5 | "appVersion": "0.4.7" 6 | }, 7 | "updateParams": { 8 | "includePreRelease": false, 9 | "components": {} 10 | }, 11 | "expected": { 12 | "system": false, 13 | "app": false, 14 | "error": "certificate signed by unknown authority" 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | open-pull-requests-limit: 10 13 | - package-ecosystem: npm 14 | directory: /ui 15 | open-pull-requests-limit: 10 16 | schedule: 17 | interval: monthly 18 | -------------------------------------------------------------------------------- /internal/native/cgo/log_handler.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "log_handler.h" 3 | 4 | /* Log handler */ 5 | jetkvm_log_handler_t *log_handler = NULL; 6 | 7 | void log_message(int level, const char *filename, const char *funcname, const int line, const char *message) { 8 | if (log_handler != NULL) { 9 | log_handler(level, filename, funcname, line, message); 10 | } 11 | } 12 | 13 | void log_set_handler(jetkvm_log_handler_t *handler) { 14 | log_handler = handler; 15 | } -------------------------------------------------------------------------------- /internal/logging/root.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "github.com/rs/zerolog" 4 | 5 | var ( 6 | rootZerologLogger = zerolog.New(defaultLogOutput).With(). 7 | Str("scope", "jetkvm"). 8 | Timestamp(). 9 | Stack(). 10 | Logger() 11 | rootLogger = NewLogger(rootZerologLogger) 12 | ) 13 | 14 | func GetRootLogger() *Logger { 15 | return rootLogger 16 | } 17 | 18 | func GetSubsystemLogger(subsystem string) *zerolog.Logger { 19 | return rootLogger.getLogger(subsystem) 20 | } 21 | -------------------------------------------------------------------------------- /ui/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@playwright/test"; 2 | 3 | if (!process.env.JETKVM_URL) { 4 | throw new Error("JETKVM_URL environment variable is required"); 5 | } 6 | 7 | export default defineConfig({ 8 | testDir: "./e2e", 9 | timeout: 60000, 10 | workers: 1, 11 | reporter: "list", 12 | use: { 13 | baseURL: process.env.JETKVM_URL, 14 | trace: "retain-on-failure", 15 | video: "retain-on-failure", 16 | screenshot: "only-on-failure", 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /ui/src/components/SettingsSectionHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export function SettingsSectionHeader({ 4 | title, 5 | description, 6 | }: { 7 | title: string | ReactNode; 8 | description: string | ReactNode; 9 | }) { 10 | return ( 11 |
12 |

{title}

13 |
{description}
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/routes/devices.$id.settings._index.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "react-router"; 2 | import type { LoaderFunction, LoaderFunctionArgs } from "react-router"; 3 | 4 | import { getDeviceUiPath } from "@hooks/useAppNavigation"; 5 | 6 | const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { 7 | return redirect(getDeviceUiPath("/settings/general", params.id)); 8 | } 9 | 10 | export default function SettingIndexRoute() { 11 | return (<>); 12 | } 13 | 14 | SettingIndexRoute.loader = loader; -------------------------------------------------------------------------------- /internal/native/eez/src/ui/fonts.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_FONTS_H 2 | #define EEZ_LVGL_UI_FONTS_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | extern const lv_font_t ui_font_font_bold30; 11 | extern const lv_font_t ui_font_font_book16; 12 | extern const lv_font_t ui_font_font_book18; 13 | extern const lv_font_t ui_font_font_book20; 14 | extern const lv_font_t ui_font_font_book24; 15 | 16 | 17 | #ifdef __cplusplus 18 | } 19 | #endif 20 | 21 | #endif /*EEZ_LVGL_UI_FONTS_H*/ -------------------------------------------------------------------------------- /internal/timesync/rtc.go: -------------------------------------------------------------------------------- 1 | package timesync 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | var ( 9 | rtcDeviceSearchPaths = []string{ 10 | "/dev/rtc", 11 | "/dev/rtc0", 12 | "/dev/rtc1", 13 | "/dev/misc/rtc", 14 | "/dev/misc/rtc0", 15 | "/dev/misc/rtc1", 16 | } 17 | ) 18 | 19 | func getRtcDevicePath() (string, error) { 20 | for _, path := range rtcDeviceSearchPaths { 21 | if _, err := os.Stat(path); err == nil { 22 | return path, nil 23 | } 24 | } 25 | return "", fmt.Errorf("rtc device not found") 26 | } 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | ### Summary 4 | 5 | - What and why in 1–3 sentences. 6 | 7 | ### UI Changes 8 | 9 | - Add before/after images or a short clip. 10 | 11 | ### Checklist 12 | 13 | - [ ] Linked to issue(s) above by issue number (e.g. `Closes #`) 14 | - [ ] One problem per PR (no unrelated changes) 15 | - [ ] Lints pass; CI green 16 | - [ ] Tricky parts are commented in code 17 | - [ ] Backward compatible with existing device firmware (See `DEVELOPMENT.md` for details) 18 | -------------------------------------------------------------------------------- /ui/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JetKVM", 3 | "short_name": "JetKVM", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#002b36", 19 | "background_color": "#051946", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /internal/usbgadget/log.go: -------------------------------------------------------------------------------- 1 | package usbgadget 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | func (u *UsbGadget) logWarn(msg string, err error) error { 8 | if err == nil { 9 | err = errors.New(msg) 10 | } 11 | if u.strictMode { 12 | return err 13 | } 14 | u.log.Warn().Err(err).Msg(msg) 15 | return nil 16 | } 17 | 18 | func (u *UsbGadget) logError(msg string, err error) error { 19 | if err == nil { 20 | err = errors.New(msg) 21 | } 22 | if u.strictMode { 23 | return err 24 | } 25 | u.log.Error().Err(err).Msg(msg) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/hooks/useFeatureFlag.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { FeatureFlagContext } from "@/providers/FeatureFlagContext"; 4 | 5 | export const useFeatureFlag = (minAppVersion: string) => { 6 | const context = useContext(FeatureFlagContext); 7 | 8 | if (!context) { 9 | throw new Error("useFeatureFlag must be used within a FeatureFlagProvider"); 10 | } 11 | 12 | const { isFeatureEnabled, appVersion } = context; 13 | 14 | const isEnabled = isFeatureEnabled(minAppVersion); 15 | return { isEnabled, appVersion }; 16 | }; 17 | -------------------------------------------------------------------------------- /internal/confparser/utils.go: -------------------------------------------------------------------------------- 1 | package confparser 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/guregu/null/v6" 9 | ) 10 | 11 | func splitString(s string) []string { 12 | if s == "" { 13 | return []string{} 14 | } 15 | 16 | return strings.Split(s, ",") 17 | } 18 | 19 | func toString(v interface{}) (string, error) { 20 | switch v := v.(type) { 21 | case string: 22 | return v, nil 23 | case null.String: 24 | return v.String, nil 25 | } 26 | 27 | return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v)) 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/components/NestedSettingsGroup.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from "@/cva.config"; 2 | 3 | interface NestedSettingsGroupProps { 4 | readonly children: React.ReactNode; 5 | readonly className?: string; 6 | } 7 | 8 | export function NestedSettingsGroup(props: NestedSettingsGroupProps) { 9 | const { children, className } = props; 10 | 11 | return ( 12 |
18 | {children} 19 |
20 | ); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc", 10 | "cStandard": "c17", 11 | "cppStandard": "gnu++17", 12 | "intelliSenseMode": "linux-gcc-arm", 13 | "configurationProvider": "ms-vscode.cmake-tools" 14 | } 15 | ], 16 | "version": 4 17 | } -------------------------------------------------------------------------------- /internal/ota/errors.go: -------------------------------------------------------------------------------- 1 | package ota 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | var ( 11 | // ErrVersionNotFound is returned when the specified version is not found 12 | ErrVersionNotFound = errors.New("specified version not found") 13 | ) 14 | 15 | func (s *State) componentUpdateError(prefix string, err error, l *zerolog.Logger) error { 16 | if l == nil { 17 | l = s.l 18 | } 19 | l.Error().Err(err).Msg(prefix) 20 | s.error = fmt.Sprintf("%s: %v", prefix, err) 21 | s.updating = false 22 | s.triggerStateUpdate() 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM --platform=${BUILDPLATFORM} golang:1.25.1-trixie AS builder 3 | 4 | ENV GOTOOLCHAIN=local 5 | ENV GOPATH=/go 6 | ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH 7 | 8 | COPY install-deps.sh /install-deps.sh 9 | RUN /install-deps.sh 10 | 11 | # Create build directory 12 | RUN mkdir -p /build/ 13 | 14 | # Copy go.mod and go.sum 15 | COPY go.mod go.sum /build/ 16 | 17 | WORKDIR /build 18 | 19 | RUN go mod download && go mod verify 20 | 21 | COPY entrypoint.sh /entrypoint.sh 22 | RUN chmod +x /entrypoint.sh 23 | 24 | ENTRYPOINT [ "/entrypoint.sh" ] -------------------------------------------------------------------------------- /ui/src/components/ExtLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cx } from "@/cva.config"; 4 | 5 | export default function ExtLink({ 6 | className, 7 | href, 8 | id, 9 | target, 10 | children, 11 | }: { 12 | className?: string; 13 | href: string; 14 | id?: string; 15 | target?: string; 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 26 | {children} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/images.c: -------------------------------------------------------------------------------- 1 | #include "images.h" 2 | 3 | const ext_img_desc_t images[14] = { 4 | { "logo", &img_logo }, 5 | { "boot-logo-2", &img_boot_logo_2 }, 6 | { "arrow-icon", &img_arrow_icon }, 7 | { "back-caret", &img_back_caret }, 8 | { "back-icon", &img_back_icon }, 9 | { "check-icon", &img_check_icon }, 10 | { "cloud_disconnected", &img_cloud_disconnected }, 11 | { "cloud", &img_cloud }, 12 | { "d2", &img_d2 }, 13 | { "ethernet", &img_ethernet }, 14 | { "hdmi", &img_hdmi }, 15 | { "jetkvm", &img_jetkvm }, 16 | { "usb", &img_usb }, 17 | { "x-icon", &img_x_icon }, 18 | }; 19 | -------------------------------------------------------------------------------- /mdns.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jetkvm/kvm/internal/mdns" 7 | ) 8 | 9 | var mDNS *mdns.MDNS 10 | 11 | func initMdns() error { 12 | options := getMdnsOptions() 13 | if options == nil { 14 | return fmt.Errorf("failed to get mDNS options") 15 | } 16 | 17 | m, err := mdns.NewMDNS(&mdns.MDNSOptions{ 18 | Logger: logger, 19 | LocalNames: options.LocalNames, 20 | ListenOptions: options.ListenOptions, 21 | }) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // do not start the server yet, as we need to wait for the network state to be set 27 | mDNS = m 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/assets/detach-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/src/assets/pointing-finger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/sync/waitgroup.go: -------------------------------------------------------------------------------- 1 | //go:build synctrace 2 | 3 | package sync 4 | 5 | import ( 6 | gosync "sync" 7 | ) 8 | 9 | // WaitGroup is a wrapper around the sync.WaitGroup 10 | type WaitGroup struct { 11 | wg gosync.WaitGroup 12 | } 13 | 14 | // Add adds a function to the wait group 15 | func (w *WaitGroup) Add(delta int) { 16 | logTrace("Adding to wait group") 17 | w.wg.Add(delta) 18 | } 19 | 20 | // Done decrements the wait group counter 21 | func (w *WaitGroup) Done() { 22 | logTrace("Done with wait group") 23 | w.wg.Done() 24 | } 25 | 26 | // Wait waits for the wait group to finish 27 | func (w *WaitGroup) Wait() { 28 | logTrace("Waiting for wait group") 29 | w.wg.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/components/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; 2 | 3 | import EmptyCard from "@components/EmptyCard"; 4 | import { m } from "@localizations/messages.js"; 5 | 6 | export default function NotFoundPage() { 7 | return ( 8 |
9 |
10 |
11 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/components/CardHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props { 4 | headline: string; 5 | description?: string | React.ReactNode; 6 | Button?: React.ReactNode; 7 | } 8 | 9 | export const CardHeader = ({ headline, description, Button }: Props) => { 10 | return ( 11 |
12 |
13 |

{headline}

14 | {description &&
{description}
} 15 |
16 | {Button &&
{Button}
} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /ui/src/components/SettingsPageheader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export function SettingsPageHeader({ 4 | title, 5 | description, 6 | action, 7 | }: { 8 | title: string | ReactNode; 9 | description: string | ReactNode; 10 | action?: ReactNode; 11 | }) { 12 | return ( 13 |
14 |
15 |

{title}

16 |
{description}
17 |
18 | {action &&
{action}
} 19 |
20 | ); 21 | } -------------------------------------------------------------------------------- /ui/src/ui.config.ts: -------------------------------------------------------------------------------- 1 | const toBoolean = (value: string | undefined) => { 2 | if (!value) return false; 3 | return ["1", "true", "yes", "y"].includes(value.toLowerCase().trim()); 4 | } 5 | export const CLOUD_API = import.meta.env.VITE_CLOUD_API; 6 | 7 | export const CLOUD_BACKWARDS_COMPATIBLE_VERSION = import.meta.env.VITE_CLOUD_BACKWARDS_COMPATIBLE_VERSION || "0.5.0"; 8 | 9 | export const CLOUD_ENABLE_VERSIONED_UI = toBoolean(import.meta.env.VITE_CLOUD_ENABLE_VERSIONED_UI); 10 | 11 | export const DOWNGRADE_VERSION = import.meta.env.VITE_DOWNGRADE_VERSION || "0.4.8"; 12 | 13 | // In device mode, an empty string uses the current hostname (the JetKVM device's IP) as the API endpoint 14 | export const DEVICE_API = ""; 15 | -------------------------------------------------------------------------------- /internal/logging/utils.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) 11 | 12 | func GetDefaultLogger() *zerolog.Logger { 13 | return &defaultLogger 14 | } 15 | 16 | func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error { 17 | // TODO: move rootLogger to logging package 18 | if l == nil { 19 | l = &defaultLogger 20 | } 21 | 22 | l.Error().Err(err).Msgf(format, args...) 23 | 24 | if err == nil { 25 | return fmt.Errorf(format, args...) 26 | } 27 | 28 | err_msg := err.Error() + ": %v" 29 | err_args := append(args, err) 30 | 31 | return fmt.Errorf(err_msg, err_args...) 32 | } 33 | -------------------------------------------------------------------------------- /internal/supervisor/consts.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | const ( 4 | EnvChildID = "JETKVM_CHILD_ID" // The child ID is the version of the app that is running 5 | EnvSubcomponent = "JETKVM_SUBCOMPONENT" // The subcomponent is the component that is running 6 | ErrorDumpDir = "/userdata/jetkvm/crashdump" // The error dump directory is the directory where the error dumps are stored 7 | ErrorDumpLastFile = "last-crash.log" // The error dump last file is the last error dump file 8 | ErrorDumpTemplate = "jetkvm-%s.log" // The error dump template is the template for the error dump file 9 | 10 | FailsafeReasonVideoMaxRestartAttemptsReached = "failsafe::video.max_restart_attempts_reached" 11 | ) 12 | -------------------------------------------------------------------------------- /ui/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import React, { ReactNode } from "react"; 3 | 4 | import { cx } from "@/cva.config"; 5 | 6 | function Container({ children, className }: { children: ReactNode; className?: string }) { 7 | return
{children}
; 8 | } 9 | 10 | function Article({ children }: { children: React.ReactNode }) { 11 | return ( 12 | 13 |
14 |
{children}
15 |
16 |
17 | ); 18 | } 19 | 20 | export default Object.assign(Container, { 21 | Article, 22 | }); 23 | -------------------------------------------------------------------------------- /internal/native/README.md: -------------------------------------------------------------------------------- 1 | # jetkvm-native 2 | 3 | This component (`internal/native/`) acts as a bridge between Golang and native (C/C++) code. 4 | It manages spawning and communicating with a native process via sockets (gRPC and Unix stream). 5 | 6 | For performance-critical operations such as video frame, **a dedicated Unix socket should be used** to avoid the overhead of gRPC and ensure low-latency communication. 7 | 8 | ## Debugging 9 | 10 | To enable debug mode, create a file called `.native-debug-mode` in the `/userdata/jetkvm` directory. 11 | 12 | ```bash 13 | touch /userdata/jetkvm/.native-debug-mode 14 | ``` 15 | 16 | This will cause the native process to listen for SIGHUP signal and crash the process. 17 | 18 | ```bash 19 | pgrep native | xargs kill -SIGHUP 20 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.classFunctions": [ 3 | "cva", 4 | "cx" 5 | ], 6 | "gopls": { 7 | "build.buildFlags": [ 8 | "-tags", 9 | "synctrace" 10 | ] 11 | }, 12 | "git.ignoreLimitWarning": true, 13 | "cmake.sourceDirectory": "${workspaceFolder}/internal/native/cgo", 14 | "cmake.ignoreCMakeListsMissing": true, 15 | "C_Cpp.inlayHints.autoDeclarationTypes.enabled": true, 16 | "C_Cpp.inlayHints.parameterNames.enabled": true, 17 | "C_Cpp.inlayHints.referenceOperator.enabled": true, 18 | "TARGET_IP": "192.168.0.199", 19 | "DEBUG_PORT": "2345", 20 | "json.schemas": [ 21 | { 22 | "fileMatch": [ 23 | "/internal/ota/testdata/ota/*.json" 24 | ], 25 | "url": "./internal/ota/testdata/ota.schema.json" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /block_device_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package kvm 4 | 5 | import ( 6 | "github.com/pojntfx/go-nbd/pkg/client" 7 | ) 8 | 9 | func (d *NBDDevice) runClientConn() { 10 | err := client.Connect(d.clientConn, d.dev, &client.Options{ 11 | ExportName: "jetkvm", 12 | BlockSize: uint32(4 * 1024), 13 | }) 14 | d.l.Info().Err(err).Msg("nbd client exited") 15 | } 16 | 17 | func (d *NBDDevice) Close() { 18 | if d.dev != nil { 19 | err := client.Disconnect(d.dev) 20 | if err != nil { 21 | d.l.Warn().Err(err).Msg("error disconnecting nbd client") 22 | } 23 | _ = d.dev.Close() 24 | } 25 | if d.listener != nil { 26 | _ = d.listener.Close() 27 | } 28 | if d.clientConn != nil { 29 | _ = d.clientConn.Close() 30 | } 31 | if d.serverConn != nil { 32 | _ = d.serverConn.Close() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/native/cgo/lvgl_defconfig: -------------------------------------------------------------------------------- 1 | CONFIG_LV_OS_PTHREAD=y 2 | CONFIG_LV_USE_OBJ_ID=y 3 | CONFIG_LV_USE_OBJ_NAME=y 4 | CONFIG_LV_USE_OBJ_ID_BUILTIN=y 5 | CONFIG_LV_USE_OBJ_PROPERTY=y 6 | CONFIG_LV_USE_OBJ_PROPERTY_NAME=y 7 | CONFIG_LV_USE_PRIVATE_API=y 8 | # CONFIG_LV_USE_CALENDAR is not set 9 | # CONFIG_LV_USE_CHART is not set 10 | # CONFIG_LV_USE_CHECKBOX is not set 11 | # CONFIG_LV_USE_MSGBOX is not set 12 | # CONFIG_LV_USE_ROLLER is not set 13 | # CONFIG_LV_USE_SCALE is not set 14 | # CONFIG_LV_USE_SLIDER is not set 15 | # CONFIG_LV_USE_TABLE is not set 16 | # CONFIG_LV_USE_TABVIEW is not set 17 | # CONFIG_LV_USE_TILEVIEW is not set 18 | CONFIG_LV_USE_QRCODE=y 19 | CONFIG_LV_USE_LINUX_FBDEV=y 20 | CONFIG_LV_USE_EVDEV=y 21 | CONFIG_LV_USE_ST7789=y 22 | CONFIG_LV_BUILD_EXAMPLES=n 23 | CONFIG_LV_BUILD_DEMOS=n 24 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/ui.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_GUI_H 2 | #define EEZ_LVGL_UI_GUI_H 3 | 4 | #include 5 | 6 | typedef void (jetkvm_rpc_handler_t)(const char *method, const char *params); 7 | 8 | void ui_set_rpc_handler(jetkvm_rpc_handler_t *handler); 9 | void ui_call_rpc_handler(const char *method, const char *params); 10 | 11 | 12 | 13 | 14 | 15 | #if defined(EEZ_FOR_LVGL) 16 | #include 17 | #endif 18 | 19 | #if !defined(EEZ_FOR_LVGL) 20 | #include "screens.h" 21 | #endif 22 | 23 | #ifdef __cplusplus 24 | extern "C" { 25 | #endif 26 | 27 | 28 | 29 | void ui_init(); 30 | void ui_tick(); 31 | 32 | #if !defined(EEZ_FOR_LVGL) 33 | void loadScreen(enum ScreensEnum screenId); 34 | #endif 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | 40 | #endif // EEZ_LVGL_UI_GUI_H -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // coding styles 4 | "chrislajoie.vscode-modelines", 5 | "editorconfig.editorconfig", 6 | // GitHub 7 | "GitHub.vscode-pull-request-github", 8 | "github.vscode-github-actions", 9 | // Golang 10 | "golang.go", 11 | // C / C++ 12 | "ms-vscode.cpptools", 13 | "ms-vscode.cpptools-extension-pack", 14 | // CMake / Makefile 15 | "ms-vscode.makefile-tools", 16 | "ms-vscode.cmake-tools", 17 | // Frontend 18 | "esbenp.prettier-vscode", 19 | "dbaeumer.vscode-eslint", 20 | "bradlc.vscode-tailwindcss", 21 | "codeandstuff.package-json-upgrade", 22 | // Localization 23 | "inlang.vs-code-extension" 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/workflows/ui-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ui-lint 3 | on: 4 | push: 5 | paths: 6 | - "ui/**" 7 | - "package.json" 8 | - "package-lock.json" 9 | - ".github/workflows/ui-lint.yml" 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | ui-lint: 16 | name: UI Lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v5 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v6 23 | with: 24 | node-version: "22" 25 | cache: "npm" 26 | cache-dependency-path: "**/package-lock.json" 27 | - name: Install dependencies 28 | run: | 29 | cd ui 30 | npm ci 31 | - name: Lint UI 32 | run: | 33 | cd ui 34 | npm run lint 35 | -------------------------------------------------------------------------------- /ui/src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | 3 | export default function LoadingSpinner({ 4 | className, 5 | }: { 6 | className: string | undefined; 7 | }) { 8 | return ( 9 | 15 | 23 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /internal/native/cgo/log_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_HANDLER_H 2 | #define LOG_HANDLER_H 3 | 4 | typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, const int line, const char *message); 5 | 6 | /** 7 | * @brief Log a message 8 | * 9 | * @param level The level of the message 10 | * @param filename The filename of the message 11 | * @param funcname The function name of the message 12 | * @param line The line number of the message 13 | * @param message The message to log 14 | * @return void 15 | */ 16 | void log_message(int level, const char *filename, const char *funcname, const int line, const char *message); 17 | 18 | /** 19 | * @brief Set the log handler 20 | * 21 | * @param handler The handler to set 22 | * @return void 23 | */ 24 | void log_set_handler(jetkvm_log_handler_t *handler); 25 | 26 | #endif -------------------------------------------------------------------------------- /ui/dev_device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check if an IP address was provided as an argument 4 | if [ -z "$1" ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | ip_address="$1" 10 | 11 | # Print header 12 | echo "┌──────────────────────────────────────┐" 13 | echo "│ JetKVM Development Setup │" 14 | echo "└──────────────────────────────────────┘" 15 | 16 | # Set the environment variable and run Vite 17 | echo "Starting development server with JetKVM device at: $ip_address" 18 | 19 | # Check if pwd is the current directory of the script 20 | if [ "$(pwd)" != "$(dirname "$0")" ]; then 21 | pushd "$(dirname "$0")" > /dev/null 22 | echo "Changed directory to: $(pwd)" 23 | fi 24 | 25 | sleep 1 26 | 27 | JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device 28 | 29 | popd > /dev/null 30 | -------------------------------------------------------------------------------- /resource/dev_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | JSON_OUTPUT=false 3 | GET_COMMANDS=false 4 | if [ "$1" = "-json" ]; then 5 | JSON_OUTPUT=true 6 | shift 7 | fi 8 | ADDITIONAL_ARGS=$@ 9 | EXIT_CODE=0 10 | 11 | runTest() { 12 | PKG_ARGS="" 13 | if [ "$2" != "" ]; then 14 | PKG_ARGS="-p $2" 15 | fi 16 | if [ "$JSON_OUTPUT" = true ]; then 17 | ./test2json $PKG_ARGS -t $1 -test.v $ADDITIONAL_ARGS | tee $1.result.json 18 | if [ $? -ne 0 ]; then 19 | EXIT_CODE=1 20 | fi 21 | else 22 | $@ 23 | if [ $? -ne 0 ]; then 24 | EXIT_CODE=1 25 | fi 26 | fi 27 | } 28 | 29 | function exit_with_code() { 30 | if [ $EXIT_CODE -ne 0 ]; then 31 | printf "\e[0;31m❌ Test failed\e[0m\n" 32 | fi 33 | 34 | exit $EXIT_CODE 35 | } 36 | 37 | trap exit_with_code EXIT 38 | -------------------------------------------------------------------------------- /ui/src/components/FeatureFlag.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import { useFeatureFlag } from "@hooks/useFeatureFlag"; 4 | 5 | export function FeatureFlag({ 6 | minAppVersion, 7 | name = "unnamed", 8 | fallback = null, 9 | children, 10 | }: { 11 | minAppVersion: string; 12 | name?: string; 13 | fallback?: React.ReactNode; 14 | children: React.ReactNode; 15 | }) { 16 | const { isEnabled, appVersion } = useFeatureFlag(minAppVersion); 17 | 18 | useEffect(() => { 19 | if (!appVersion) return; 20 | console.log( 21 | `Feature '${name}' ${isEnabled ? "ENABLED" : "DISABLED"}: ` + 22 | `Current version: ${appVersion}, ` + 23 | `Required min version: ${minAppVersion || "N/A"}`, 24 | ); 25 | }, [isEnabled, name, minAppVersion, appVersion]); 26 | 27 | return isEnabled ? children : fallback; 28 | } 29 | -------------------------------------------------------------------------------- /scripts/ci_helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") 4 | source ${SCRIPT_PATH}/build_utils.sh 5 | 6 | set -e 7 | 8 | # check if GITHUB_ENV is set 9 | if [ -z "$GITHUB_ENV" ]; then 10 | echo "GITHUB_ENV is not set" 11 | exit 1 12 | fi 13 | 14 | if [ "$1" = "prepare" ]; then 15 | prepare_docker_build_context 16 | echo "DOCKER_BUILD_CONTEXT_DIR=$DOCKER_BUILD_CONTEXT_DIR" >> $GITHUB_ENV 17 | echo "DOCKER_BUILD_TAG=$DOCKER_BUILD_TAG" >> $GITHUB_ENV 18 | elif [ "$1" = "make" ]; then 19 | BUILD_IN_DOCKER=true 20 | # check if GO is available 21 | if ! command -v go &> /dev/null; then 22 | msg_info "Go is not available, will using default cache directory" 23 | else 24 | DOCKER_GO_CACHE_DIR=$(go env GOCACHE) 25 | fi 26 | do_make "${@:2}" 27 | fi 28 | 29 | -------------------------------------------------------------------------------- /ui/src/components/Fieldset.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigation } from "react-router"; 3 | import type { FetcherWithComponents } from "react-router"; 4 | import clsx from "clsx"; 5 | 6 | export default function Fieldset({ 7 | children, 8 | fetcher, 9 | className, 10 | disabled, 11 | }: { 12 | children: React.ReactNode; 13 | fetcher?: FetcherWithComponents; 14 | className?: string; 15 | disabled?: boolean; 16 | }) { 17 | const navigation = useNavigation(); 18 | const loader = fetcher ? fetcher : navigation; 19 | return ( 20 |
28 | {children} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /internal/network/types/resolvconf.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "net" 4 | 5 | // InterfaceResolvConf represents the DNS configuration for a network interface 6 | type InterfaceResolvConf struct { 7 | NameServers []net.IP `json:"nameservers"` 8 | SearchList []string `json:"search_list"` 9 | Domain string `json:"domain,omitempty"` // TODO: remove this once we have a better way to handle the domain 10 | Source string `json:"source,omitempty"` 11 | } 12 | 13 | // InterfaceResolvConfMap .. 14 | type InterfaceResolvConfMap map[string]InterfaceResolvConf 15 | 16 | // ResolvConf represents the DNS configuration for the system 17 | type ResolvConf struct { 18 | ConfigIPv4 InterfaceResolvConfMap `json:"config_ipv4"` 19 | ConfigIPv6 InterfaceResolvConfMap `json:"config_ipv6"` 20 | Domain string `json:"domain"` 21 | HostName string `json:"host_name"` 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/api.ts: -------------------------------------------------------------------------------- 1 | function api(url: string, options: RequestInit): Promise { 2 | const baseOptions: RequestInit = { 3 | mode: "cors", 4 | credentials: "include", 5 | headers: { 6 | "Content-Type": "application/json", 7 | }, 8 | ...options, 9 | }; 10 | 11 | return fetch(url, baseOptions); 12 | } 13 | 14 | export default Object.assign(api, { 15 | GET: (url: string, options?: RequestInit) => api(url, { method: "GET", ...options }), 16 | POST: (url: string, body?: object, options?: RequestInit) => 17 | api(url, { method: "POST", body: JSON.stringify(body), ...options }), 18 | PUT: (url: string, body?: object, options?: RequestInit) => 19 | api(url, { method: "PUT", body: JSON.stringify(body), ...options }), 20 | DELETE: (url: string, body?: object, options?: RequestInit) => 21 | api(url, { method: "DELETE", body: JSON.stringify(body), ...options }), 22 | }); 23 | -------------------------------------------------------------------------------- /internal/usbgadget/mass_storage.go: -------------------------------------------------------------------------------- 1 | package usbgadget 2 | 3 | var massStorageBaseConfig = gadgetConfigItem{ 4 | order: 3000, 5 | device: "mass_storage.usb0", 6 | path: []string{"functions", "mass_storage.usb0"}, 7 | configPath: []string{"mass_storage.usb0"}, 8 | attrs: gadgetAttributes{ 9 | "stall": "1", 10 | }, 11 | } 12 | 13 | var massStorageLun0Config = gadgetConfigItem{ 14 | order: 3001, 15 | path: []string{"functions", "mass_storage.usb0", "lun.0"}, 16 | attrs: gadgetAttributes{ 17 | "cdrom": "1", 18 | "ro": "1", 19 | "removable": "1", 20 | "file": "\n", 21 | // the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string 22 | // https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556 23 | // Vendor (8 chars), product (16 chars) 24 | "inquiry_string": "JetKVM Virtual Media", 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/components/FailSafeModeBanner.tsx: -------------------------------------------------------------------------------- 1 | import { LuTriangleAlert } from "react-icons/lu"; 2 | 3 | import Card from "@components/Card"; 4 | 5 | interface FailsafeModeBannerProps { 6 | reason: string; 7 | } 8 | 9 | export function FailsafeModeBanner({ reason }: FailsafeModeBannerProps) { 10 | const getReasonMessage = () => { 11 | switch (reason) { 12 | case "video": 13 | return "Failsafe Mode Active: Video-related settings are currently unavailable"; 14 | default: 15 | return "Failsafe Mode Active: Some settings may be unavailable"; 16 | } 17 | }; 18 | 19 | return ( 20 | 21 |
22 | 23 |

24 | {getReasonMessage()} 25 |

26 |
27 |
28 | ); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /internal/native/cgo/edid.h: -------------------------------------------------------------------------------- 1 | #ifndef EDID_H 2 | #define EDID_H 3 | 4 | #include 5 | 6 | 7 | #include 8 | 9 | /** 10 | * @brief Read the EDID from the display 11 | * 12 | * @param edid Buffer to store the EDID data 13 | * @param max_size Maximum size of the buffer (should be 128 or 256) 14 | * @return int Number of bytes read on success, -1 on failure 15 | */ 16 | int get_edid(uint8_t *edid, size_t max_size); 17 | 18 | /** 19 | * @brief Set the EDID of the display 20 | * 21 | * @param edid The EDID to set, it can be modified 22 | * @param size The size of the EDID (should be 128 or 256) 23 | * @return int 0 on success, -1 on failure 24 | */ 25 | int set_edid(uint8_t *edid, size_t size); 26 | 27 | /** 28 | * @brief Get the status of the videocontroller, aka v4l2-ctl --log-status. 29 | * User should free the returned string 30 | * 31 | * @return const char* The status of the videocontroller 32 | */ 33 | const char* videoc_log_status(); 34 | 35 | #endif // EDID_H 36 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "deploy", 8 | "isBackground": true, 9 | "type": "shell", 10 | "command": "bash", 11 | "args": [ 12 | "dev_deploy.sh", 13 | "-r", 14 | "${config:TARGET_IP}", 15 | "--gdb-port", 16 | "${config:DEBUG_PORT}", 17 | "--native-binary", 18 | "--disable-docker" 19 | ], 20 | "problemMatcher": { 21 | "base": "$gcc", 22 | "background": { 23 | "activeOnStart": true, 24 | "beginsPattern": "${config:BINARY}", 25 | "endsPattern": "Listening on port [0-9]{4}" 26 | } 27 | } 28 | }, 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/components/AutoHeight.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect } from "react"; 2 | import AnimateHeight, { Height } from "react-animate-height"; 3 | 4 | const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => { 5 | const [height, setHeight] = useState("auto"); 6 | const contentDiv = useRef(null); 7 | 8 | useEffect(() => { 9 | const element = contentDiv.current as HTMLDivElement; 10 | 11 | const resizeObserver = new ResizeObserver(() => { 12 | setHeight(element.clientHeight); 13 | }); 14 | 15 | resizeObserver.observe(element); 16 | 17 | return () => resizeObserver.disconnect(); 18 | }, []); 19 | 20 | return ( 21 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | export default AutoHeight; 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "GDB Debug - Native (binary)", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "internal/native/cgo/build/jknative-bin", 9 | "args": [], 10 | "stopAtEntry": true, 11 | "cwd": "${workspaceFolder}", 12 | "environment": [], 13 | "MIMode": "gdb", 14 | "miDebuggerPath": "/usr/bin/gdb-multiarch", 15 | "miDebuggerServerAddress": "${config:TARGET_IP}:${config:DEBUG_PORT}", 16 | "targetArchitecture": "arm", 17 | "preLaunchTask": "deploy", 18 | "setupCommands": [ 19 | { 20 | "description": "Pretty-printing for gdb", 21 | "text": "-enable-pretty-printing", 22 | "ignoreFailures": true 23 | } 24 | ], 25 | "externalConsole": true 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /internal/native/proto/README.md: -------------------------------------------------------------------------------- 1 | # Proto Files 2 | 3 | This directory contains the Protocol Buffer definitions for the native service. 4 | 5 | ## Generating Code 6 | 7 | To generate the Go code from the proto files, run: 8 | 9 | ```bash 10 | ./scripts/generate_proto.sh 11 | ``` 12 | 13 | Or manually: 14 | 15 | ```bash 16 | protoc \ 17 | --go_out=. \ 18 | --go_opt=paths=source_relative \ 19 | --go-grpc_out=. \ 20 | --go-grpc_opt=paths=source_relative \ 21 | internal/native/proto/native.proto 22 | ``` 23 | 24 | ## Prerequisites 25 | 26 | - `protoc` - Protocol Buffer compiler 27 | - `protoc-gen-go` - Go plugin for protoc (install with: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`) 28 | - `protoc-gen-go-grpc` - gRPC Go plugin for protoc (install with: `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest`) 29 | 30 | ## Note 31 | 32 | The current `native.pb.go` and `native_grpc.pb.go` files are placeholder/stub files. They should be regenerated from `native.proto` using the commands above. 33 | 34 | -------------------------------------------------------------------------------- /ui/src/components/SimpleNavbar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router"; 3 | 4 | import LogoBlueIcon from "@assets/logo-blue.png"; 5 | import LogoWhiteIcon from "@assets/logo-white.svg"; 6 | import Container from "@components/Container"; 7 | 8 | interface Props { logoHref?: string; actionElement?: React.ReactNode } 9 | 10 | export default function SimpleNavbar({ logoHref, actionElement }: Props) { 11 | return ( 12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 |
{actionElement}
21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/components/CustomTooltip.tsx: -------------------------------------------------------------------------------- 1 | import Card from "@components/Card"; 2 | 3 | export interface CustomTooltipProps { 4 | payload: { payload: { date: number; metric: number }; unit: string }[]; 5 | } 6 | 7 | export default function CustomTooltip({ payload }: CustomTooltipProps) { 8 | if (payload?.length) { 9 | const toolTipData = payload[0]; 10 | const { date, metric } = toolTipData.payload; 11 | 12 | return ( 13 | 14 |
15 |
16 | {new Date(date * 1000).toLocaleTimeString()} 17 |
18 |
19 |
20 |
21 | 22 | {metric} {toolTipData?.unit} 23 | 24 |
25 |
26 |
27 | 28 | ); 29 | } 30 | 31 | return null; 32 | } 33 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/vars.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_VARS_H 2 | #define EEZ_LVGL_UI_VARS_H 3 | 4 | #include 5 | #include 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | // enum declarations 12 | 13 | 14 | 15 | // Flow global variables 16 | 17 | enum FlowGlobalVariables { 18 | FLOW_GLOBAL_VARIABLE_APP_VERSION = 0, 19 | FLOW_GLOBAL_VARIABLE_SYSTEM_VERSION = 1, 20 | FLOW_GLOBAL_VARIABLE_LVGL_VERSION = 2, 21 | FLOW_GLOBAL_VARIABLE_MAIN_SCREEN = 3 22 | }; 23 | 24 | // Native global variables 25 | 26 | extern const char *get_var_app_version(); 27 | extern void set_var_app_version(const char *value); 28 | extern const char *get_var_system_version(); 29 | extern void set_var_system_version(const char *value); 30 | extern const char *get_var_lvgl_version(); 31 | extern void set_var_lvgl_version(const char *value); 32 | extern const char *get_var_main_screen(); 33 | extern void set_var_main_screen(const char *value); 34 | 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | 40 | #endif /*EEZ_LVGL_UI_VARS_H*/ -------------------------------------------------------------------------------- /ui/src/keyboardLayouts/fr_CH.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardLayout, KeyCombo } from "../keyboardLayouts" 2 | 3 | import { de_CH } from "./de_CH" 4 | 5 | const name = "Français de Suisse"; 6 | const isoCode = "fr-CH"; 7 | 8 | const chars = { 9 | ...de_CH.chars, 10 | "è": { key: "BracketLeft" }, 11 | "ü": { key: "BracketLeft", shift: true }, 12 | "é": { key: "Semicolon" }, 13 | "ö": { key: "Semicolon", shift: true }, 14 | "à": { key: "Quote" }, 15 | "ä": { key: "Quote", shift: true }, 16 | } as Record; 17 | 18 | const keyDisplayMap = { 19 | ...de_CH.keyDisplayMap, 20 | "BracketLeft": "è", 21 | "BracketLeftShift": "ü", 22 | "Semicolon": "é", 23 | "SemicolonShift": "ö", 24 | "Quote": "à", 25 | "QuoteShift": "ä", 26 | } as Record; 27 | 28 | export const fr_CH: KeyboardLayout = { 29 | isoCode: isoCode, 30 | name: name, 31 | chars: chars, 32 | keyDisplayMap: keyDisplayMap, 33 | // TODO need to localize these maps and layouts 34 | modifierDisplayMap: de_CH.modifierDisplayMap, 35 | virtualKeyboard: de_CH.virtualKeyboard 36 | }; 37 | -------------------------------------------------------------------------------- /internal/native/cgo/main.h: -------------------------------------------------------------------------------- 1 | #ifndef JETKVM_NATIVE_MAIN_H 2 | #define JETKVM_NATIVE_MAIN_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "ctrl.h" 12 | 13 | void jetkvm_c_log_handler(int level, const char *filename, const char *funcname, int line, const char *message); 14 | void jetkvm_video_handler(const uint8_t *frame, ssize_t len); 15 | void jetkvm_video_state_handler(jetkvm_video_state_t *state); 16 | void jetkvm_indev_handler(int code); 17 | void jetkvm_rpc_handler(const char *method, const char *params); 18 | 19 | 20 | // typedef void (jetkvm_video_state_handler_t)(jetkvm_video_state_t *state); 21 | // typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, int line, const char *message); 22 | // typedef void (jetkvm_rpc_handler_t)(const char *method, const char *params); 23 | // typedef void (jetkvm_video_handler_t)(const uint8_t *frame, ssize_t len); 24 | // typedef void (jetkvm_indev_handler_t)(int code); 25 | 26 | #endif -------------------------------------------------------------------------------- /pkg/nmlite/jetdhcpc/utils.go: -------------------------------------------------------------------------------- 1 | package jetdhcpc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/vishvananda/netlink" 9 | ) 10 | 11 | type waitForCondition func(l netlink.Link, logger *zerolog.Logger) (ready bool, err error) 12 | 13 | func (c *Client) waitFor( 14 | link netlink.Link, 15 | timeout <-chan time.Time, 16 | condition waitForCondition, 17 | timeoutError error, 18 | ) error { 19 | return waitFor(c.ctx, link, c.l, timeout, condition, timeoutError) 20 | } 21 | 22 | func waitFor( 23 | ctx context.Context, 24 | link netlink.Link, 25 | logger *zerolog.Logger, 26 | timeout <-chan time.Time, 27 | condition waitForCondition, 28 | timeoutError error, 29 | ) error { 30 | for { 31 | if ready, err := condition(link, logger); err != nil { 32 | return err 33 | } else if ready { 34 | break 35 | } 36 | 37 | select { 38 | case <-time.After(100 * time.Millisecond): 39 | continue 40 | case <-timeout: 41 | return timeoutError 42 | case <-ctx.Done(): 43 | return timeoutError 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/no_components.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Upgrade System & App (no components given)", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false" 7 | }, 8 | "code": 200, 9 | "data": { 10 | "appVersion": "0.4.7", 11 | "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", 12 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 13 | "systemVersion": "0.2.5", 14 | "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", 15 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 16 | } 17 | } 18 | ], 19 | "localMetadata": { 20 | "systemVersion": "0.2.2", 21 | "appVersion": "0.4.2" 22 | }, 23 | "updateParams": { 24 | "includePreRelease": false, 25 | "components": {} 26 | }, 27 | "expected": { 28 | "system": true, 29 | "app": true 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: golangci-lint 3 | on: 4 | push: 5 | paths: 6 | - "go.sum" 7 | - "go.mod" 8 | - "**.go" 9 | - ".github/workflows/golangci-lint.yml" 10 | - ".golangci.yml" 11 | pull_request: 12 | 13 | permissions: # added using https://github.com/step-security/secure-repo 14 | contents: read 15 | 16 | jobs: 17 | golangci: 18 | permissions: 19 | contents: read # for actions/checkout to fetch code 20 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 21 | name: lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v5 26 | - name: Install Go 27 | uses: actions/setup-go@v6 28 | with: 29 | go-version: oldstable 30 | - name: Create empty resource directory 31 | run: | 32 | mkdir -p static && touch static/.gitkeep 33 | - name: Lint 34 | uses: golangci/golangci-lint-action@v8 35 | with: 36 | args: --verbose 37 | version: v2.1 38 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/app_only_upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Upgrade App Only", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false" 7 | }, 8 | "code": 200, 9 | "data": { 10 | "appVersion": "0.4.7", 11 | "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", 12 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 13 | "systemVersion": "0.2.5", 14 | "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", 15 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 16 | } 17 | } 18 | ], 19 | "localMetadata": { 20 | "systemVersion": "0.2.2", 21 | "appVersion": "0.4.5" 22 | }, 23 | "updateParams": { 24 | "includePreRelease": false, 25 | "components": { 26 | "app": "" 27 | } 28 | }, 29 | "expected": { 30 | "system": false, 31 | "app": true 32 | } 33 | } -------------------------------------------------------------------------------- /ui/localization/jetKVM.UI.inlang/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/project-settings", 3 | "baseLocale": "en", 4 | "sourceLanguageTag": "en", 5 | "locales": [ 6 | "en", 7 | "da", 8 | "de", 9 | "es", 10 | "fr", 11 | "it", 12 | "nb", 13 | "sv", 14 | "zh" 15 | ], 16 | "languageTags": [ 17 | "en", 18 | "da", 19 | "de", 20 | "es", 21 | "fr", 22 | "it", 23 | "nb", 24 | "sv", 25 | "zh" 26 | ], 27 | "modules": [ 28 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js", 29 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js" 30 | ], 31 | "plugin.inlang.messageFormat": { 32 | "pathPattern": "./messages/{locale}.json" 33 | }, 34 | "plugin.inlang.mFunctionMatcher": { 35 | "matchers": [ 36 | { 37 | "type": "m-function", 38 | "function": "plural", 39 | "parameter": "count" 40 | } 41 | ] 42 | }, 43 | "strategy": [ 44 | "cookie", 45 | "preferredLanguage", 46 | "baseLocale" 47 | ] 48 | } -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - forbidigo 5 | - misspell 6 | - whitespace 7 | - gochecknoinits 8 | settings: 9 | forbidigo: 10 | forbid: 11 | - pattern: ^fmt\.Print.*$ 12 | msg: Do not commit print statements. Use logger package. 13 | - pattern: ^log\.(Fatal|Panic|Print)(f|ln)?.*$ 14 | msg: Do not commit log statements. Use logger package. 15 | exclusions: 16 | generated: lax 17 | presets: 18 | - comments 19 | - common-false-positives 20 | - legacy 21 | - std-error-handling 22 | rules: 23 | - linters: 24 | - errcheck 25 | path: _test.go 26 | - linters: 27 | - forbidigo 28 | path: cmd/main.go 29 | - linters: 30 | - gochecknoinits 31 | path: internal/logging/sse.go 32 | paths: 33 | - third_party$ 34 | - builtin$ 35 | - examples$ 36 | formatters: 37 | enable: 38 | - goimports 39 | exclusions: 40 | generated: lax 41 | paths: 42 | - third_party$ 43 | - builtin$ 44 | - examples$ 45 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/system_only_upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Upgrade System Only", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false" 7 | }, 8 | "code": 200, 9 | "data": { 10 | "appVersion": "0.4.7", 11 | "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", 12 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 13 | "systemVersion": "0.2.6", 14 | "systemUrl": "https://update.jetkvm.com/system/0.2.6/system.tar", 15 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 16 | } 17 | } 18 | ], 19 | "localMetadata": { 20 | "systemVersion": "0.2.5", 21 | "appVersion": "0.4.5" 22 | }, 23 | "updateParams": { 24 | "includePreRelease": false, 25 | "components": { 26 | "system": "" 27 | } 28 | }, 29 | "expected": { 30 | "system": true, 31 | "app": false 32 | } 33 | } -------------------------------------------------------------------------------- /ui/src/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useSearchParams } from "react-router"; 2 | 3 | import { m } from "@localizations/messages.js"; 4 | import AuthLayout from "@components/AuthLayout"; 5 | 6 | export default function LoginRoute() { 7 | const [sq] = useSearchParams(); 8 | const location = useLocation(); 9 | const deviceId = sq.get("deviceId") || location.state?.deviceId; 10 | 11 | if (deviceId) { 12 | return ( 13 | 22 | ); 23 | } 24 | 25 | return ( 26 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/images.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_IMAGES_H 2 | #define EEZ_LVGL_UI_IMAGES_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | extern const lv_img_dsc_t img_logo; 11 | extern const lv_img_dsc_t img_boot_logo_2; 12 | extern const lv_img_dsc_t img_arrow_icon; 13 | extern const lv_img_dsc_t img_back_caret; 14 | extern const lv_img_dsc_t img_back_icon; 15 | extern const lv_img_dsc_t img_check_icon; 16 | extern const lv_img_dsc_t img_cloud_disconnected; 17 | extern const lv_img_dsc_t img_cloud; 18 | extern const lv_img_dsc_t img_d2; 19 | extern const lv_img_dsc_t img_ethernet; 20 | extern const lv_img_dsc_t img_hdmi; 21 | extern const lv_img_dsc_t img_jetkvm; 22 | extern const lv_img_dsc_t img_usb; 23 | extern const lv_img_dsc_t img_x_icon; 24 | 25 | #ifndef EXT_IMG_DESC_T 26 | #define EXT_IMG_DESC_T 27 | typedef struct _ext_img_desc_t { 28 | const char *name; 29 | const lv_img_dsc_t *img_dsc; 30 | } ext_img_desc_t; 31 | #endif 32 | 33 | extern const ext_img_desc_t images[14]; 34 | 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | 40 | #endif /*EEZ_LVGL_UI_IMAGES_H*/ -------------------------------------------------------------------------------- /internal/native/cgo/ui_index.h: -------------------------------------------------------------------------------- 1 | #ifndef UI_INDEX_H 2 | #define UI_INDEX_H 3 | 4 | #include "ui/ui.h" 5 | #include "ui/screens.h" 6 | #include "ui/styles.h" 7 | #include "ui/images.h" 8 | #include "ui/vars.h" 9 | 10 | typedef struct { 11 | const char *name; 12 | lv_obj_t **obj; // Pointer to the object pointer, as the object pointer is only populated after the ui is initialized 13 | } ui_obj_map; 14 | 15 | extern ui_obj_map ui_objects[]; 16 | extern const int ui_objects_size; 17 | 18 | typedef struct { 19 | const char *name; 20 | lv_style_t *(*getter)(); 21 | } ui_style_map; 22 | 23 | extern ui_style_map ui_styles[]; 24 | extern const int ui_styles_size; 25 | 26 | typedef struct { 27 | const char *name; 28 | const lv_img_dsc_t *img; // Pointer to the image descriptor const 29 | } ui_img_map; 30 | 31 | extern ui_img_map ui_images[]; 32 | extern const int ui_images_size; 33 | 34 | typedef struct { 35 | const char *name; 36 | const char *(*getter)(); 37 | void (*setter)(const char *value); 38 | } ui_var_map; 39 | 40 | extern ui_var_map ui_vars[]; 41 | extern const int ui_vars_size; 42 | 43 | #endif // UI_INDEX_H 44 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/both_upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Upgrade System & App (components given)", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false" 7 | }, 8 | "code": 200, 9 | "data": { 10 | "appVersion": "0.4.7", 11 | "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", 12 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 13 | "systemVersion": "0.2.5", 14 | "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", 15 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 16 | } 17 | } 18 | ], 19 | "localMetadata": { 20 | "systemVersion": "0.2.2", 21 | "appVersion": "0.4.5" 22 | }, 23 | "updateParams": { 24 | "includePreRelease": false, 25 | "components": { 26 | "system": "", 27 | "app": "" 28 | } 29 | }, 30 | "expected": { 31 | "system": true, 32 | "app": true 33 | } 34 | } -------------------------------------------------------------------------------- /internal/ota/testdata/ota/app_only_downgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Downgrade App Only", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false", 7 | "appVersion": "0.4.6" 8 | }, 9 | "code": 200, 10 | "data": { 11 | "appVersion": "0.4.6", 12 | "appUrl": "https://update.jetkvm.com/app/0.4.6/jetkvm_app", 13 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 14 | "systemVersion": "0.2.5", 15 | "systemUrl": "https://update.jetkvm.com/system/0.2.5/system.tar", 16 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 17 | } 18 | } 19 | ], 20 | "localMetadata": { 21 | "systemVersion": "0.2.2", 22 | "appVersion": "0.4.5" 23 | }, 24 | "updateParams": { 25 | "includePreRelease": false, 26 | "components": { 27 | "app": "0.4.6" 28 | } 29 | }, 30 | "expected": { 31 | "system": false, 32 | "app": true 33 | } 34 | } -------------------------------------------------------------------------------- /ui/src/routes/signup.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useSearchParams } from "react-router"; 2 | 3 | import AuthLayout from "@components/AuthLayout"; 4 | import { m } from "@localizations/messages.js"; 5 | 6 | export default function SignupRoute() { 7 | const [sq] = useSearchParams(); 8 | const location = useLocation(); 9 | const deviceId = sq.get("deviceId") || location.state?.deviceId; 10 | 11 | if (deviceId) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | return ( 25 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/system_only_downgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Downgrade System Only", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false", 7 | "systemVersion": "0.2.2" 8 | }, 9 | "code": 200, 10 | "data": { 11 | "appVersion": "0.4.7", 12 | "appUrl": "https://update.jetkvm.com/app/0.4.7/jetkvm_app", 13 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 14 | "systemVersion": "0.2.2", 15 | "systemUrl": "https://update.jetkvm.com/system/0.2.2/system.tar", 16 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 17 | } 18 | } 19 | ], 20 | "localMetadata": { 21 | "systemVersion": "0.2.5", 22 | "appVersion": "0.4.5" 23 | }, 24 | "updateParams": { 25 | "includePreRelease": false, 26 | "components": { 27 | "system": "0.2.2" 28 | } 29 | }, 30 | "expected": { 31 | "system": true, 32 | "app": false 33 | } 34 | } -------------------------------------------------------------------------------- /.devcontainer/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SUDO_PATH=$(which sudo) 4 | function sudo() { 5 | if [ "$UID" -eq 0 ]; then 6 | "$@" 7 | else 8 | ${SUDO_PATH} "$@" 9 | fi 10 | } 11 | 12 | set -ex 13 | 14 | export DEBIAN_FRONTEND=noninteractive 15 | sudo apt-get update && \ 16 | sudo apt-get install -y --no-install-recommends \ 17 | iputils-ping \ 18 | build-essential \ 19 | device-tree-compiler \ 20 | gperf g++-multilib gcc-multilib \ 21 | gdb-multiarch \ 22 | libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \ 23 | bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \ 24 | wget zstd \ 25 | python3-venv python3-kconfiglib \ 26 | && sudo rm -rf /var/lib/apt/lists/* 27 | 28 | # Install buildkit 29 | BUILDKIT_VERSION="v0.2.5" 30 | BUILDKIT_TMPDIR="$(mktemp -d)" 31 | pushd "${BUILDKIT_TMPDIR}" > /dev/null 32 | 33 | wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSION}/buildkit.tar.zst && \ 34 | sudo mkdir -p /opt/jetkvm-native-buildkit && \ 35 | sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ 36 | rm buildkit.tar.zst 37 | popd 38 | rm -rf "${BUILDKIT_TMPDIR}" 39 | -------------------------------------------------------------------------------- /internal/ota/testdata/ota/both_downgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Downgrade System & App", 3 | "remoteMetadata": [ 4 | { 5 | "params": { 6 | "prerelease": "false", 7 | "systemVersion": "0.2.2", 8 | "appVersion": "0.4.6" 9 | }, 10 | "code": 200, 11 | "data": { 12 | "appVersion": "0.4.6", 13 | "appUrl": "https://update.jetkvm.com/app/0.4.6/jetkvm_app", 14 | "appHash": "714f33432f17035e38d238bf376e98f3073e6cc2845d269ff617503d12d92bdd", 15 | "systemVersion": "0.2.2", 16 | "systemUrl": "https://update.jetkvm.com/system/0.2.2/system.tar", 17 | "systemHash": "2323463ea8652be767d94514e548f90dd61b1ebcc0fb1834d700fac5b3d88a35" 18 | } 19 | } 20 | ], 21 | "localMetadata": { 22 | "systemVersion": "0.2.5", 23 | "appVersion": "0.4.5" 24 | }, 25 | "updateParams": { 26 | "includePreRelease": false, 27 | "components": { 28 | "system": "0.2.2", 29 | "app": "0.4.6" 30 | } 31 | }, 32 | "expected": { 33 | "system": true, 34 | "app": true 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /ui/public/jetkvm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /publish_source.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Check if a commit message was provided 4 | if [ -z "$1" ]; then 5 | echo "Usage: $0 \"Your commit message here\"" 6 | exit 1 7 | fi 8 | 9 | COMMIT_MESSAGE="$1" 10 | 11 | # Ensure you're on the main branch 12 | git checkout main 13 | 14 | # Add 'public' remote if it doesn't exist 15 | if ! git remote | grep -q '^public$'; then 16 | git remote add public https://github.com/jetkvm/kvm.git 17 | fi 18 | 19 | # Fetch the latest from the public repository 20 | git fetch public || true 21 | 22 | # Create a temporary branch for the release 23 | git checkout -b release-temp 24 | 25 | # If public/main exists, reset to it; else, use the root commit 26 | if git ls-remote --heads public main | grep -q 'refs/heads/main'; then 27 | git reset --soft public/main 28 | else 29 | git reset --soft "$(git rev-list --max-parents=0 HEAD)" 30 | fi 31 | 32 | # Merge changes from main 33 | git merge --squash main 34 | 35 | # Commit all changes as a single release commit 36 | git commit -m "$COMMIT_MESSAGE" 37 | 38 | # Force push the squashed commit to the public repository 39 | git push --force public release-temp:main 40 | 41 | # Switch back to main and delete the temporary branch 42 | git checkout main 43 | git branch -D release-temp 44 | 45 | # Remove the public remote 46 | git remote remove public 47 | -------------------------------------------------------------------------------- /ui/src/providers/FeatureFlagProvider.tsx: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | 3 | import { FeatureFlagContext } from "./FeatureFlagContext"; 4 | 5 | export interface FeatureFlagContextType { 6 | appVersion: string | null; 7 | isFeatureEnabled: (minVersion: string) => boolean; 8 | } 9 | 10 | // Provider component that fetches version and provides context 11 | export const FeatureFlagProvider = ({ 12 | children, 13 | appVersion, 14 | }: { 15 | children: React.ReactNode; 16 | appVersion: string | null; 17 | }) => { 18 | const isFeatureEnabled = (minAppVersion: string) => { 19 | // If no version is set, feature is disabled. 20 | // The feature flag component can decide what to display as a fallback - either omit the component or like a "please upgrade to enable". 21 | if (!appVersion) return false; 22 | 23 | // Extract the base versions without prerelease identifier 24 | const baseCurrentVersion = semver.coerce(appVersion)?.version; 25 | const baseMinVersion = semver.coerce(minAppVersion)?.version; 26 | 27 | if (!baseCurrentVersion || !baseMinVersion) return false; 28 | 29 | return semver.gte(baseCurrentVersion, baseMinVersion); 30 | }; 31 | 32 | const value = { appVersion, isFeatureEnabled }; 33 | 34 | return ( 35 | {children} 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /.devcontainer/docker/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JetKVM docker devcontainer", 3 | "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": { 6 | // Should match what is defined in ui/package.json 7 | "version": "22.20.0" 8 | } 9 | }, 10 | "mounts": [ 11 | "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" 12 | ], 13 | "onCreateCommand": ".devcontainer/install-deps.sh", 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | // coding styles 18 | "chrislajoie.vscode-modelines", 19 | "editorconfig.editorconfig", 20 | // GitHub 21 | "GitHub.vscode-pull-request-github", 22 | "github.vscode-github-actions", 23 | // Golang 24 | "golang.go", 25 | // C / C++ 26 | "ms-vscode.cpptools", 27 | "ms-vscode.cpptools-extension-pack", 28 | // CMake / Makefile 29 | "ms-vscode.makefile-tools", 30 | "ms-vscode.cmake-tools", 31 | // Frontend 32 | "esbenp.prettier-vscode", 33 | "dbaeumer.vscode-eslint", 34 | "bradlc.vscode-tailwindcss", 35 | "codeandstuff.package-json-upgrade", 36 | // Localization 37 | "inlang.vs-code-extension" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/native/cgo/video.h: -------------------------------------------------------------------------------- 1 | #ifndef VIDEO_DAEMON_VIDEO_H 2 | #define VIDEO_DAEMON_VIDEO_H 3 | 4 | /** 5 | * @brief Initialize the video subsystem 6 | * 7 | * @return int 0 on success, -1 on failure 8 | */ 9 | int video_init(float quality_factor); 10 | 11 | /** 12 | * @brief Shutdown the video subsystem 13 | */ 14 | void video_shutdown(); 15 | 16 | /** 17 | * @brief Run the detect format thread 18 | * 19 | * @param arg The argument to pass to the thread 20 | * @return void* The result of the thread 21 | */ 22 | void *run_detect_format(void *arg); 23 | 24 | /** 25 | * @brief Start the video streaming 26 | */ 27 | void video_start_streaming(); 28 | 29 | /** 30 | * @brief Stop the video streaming 31 | */ 32 | void video_stop_streaming(); 33 | 34 | /** 35 | * @brief Get the streaming status of the video 36 | * 37 | * @return uint8_t 1 if the video streaming is active, 2 if the video streaming is stopping, 0 otherwise 38 | */ 39 | uint8_t video_get_streaming_status(); 40 | 41 | /** 42 | * @brief Set the quality factor of the video 43 | * 44 | * @param factor The quality factor to set 45 | */ 46 | void video_set_quality_factor(float factor); 47 | 48 | /** 49 | * @brief Get the quality factor of the video 50 | * 51 | * @return float The quality factor of the video 52 | */ 53 | float video_get_quality_factor(); 54 | 55 | #endif //VIDEO_DAEMON_VIDEO_H 56 | -------------------------------------------------------------------------------- /internal/ota/app.go: -------------------------------------------------------------------------------- 1 | package ota 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | const ( 9 | appUpdatePath = "/userdata/jetkvm/jetkvm_app.update" 10 | ) 11 | 12 | // DO NOT call it directly, it's not thread safe 13 | // Mutex is currently held by the caller, e.g. doUpdate 14 | func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) error { 15 | l := s.l.With().Str("path", appUpdatePath).Logger() 16 | 17 | if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil { 18 | return s.componentUpdateError("Error downloading app update", err, &l) 19 | } 20 | 21 | downloadFinished := time.Now() 22 | appUpdate.downloadFinishedAt = downloadFinished 23 | appUpdate.downloadProgress = 1 24 | s.triggerComponentUpdateState("app", appUpdate) 25 | 26 | if err := s.verifyFile( 27 | appUpdatePath, 28 | appUpdate.hash, 29 | &appUpdate.verificationProgress, 30 | ); err != nil { 31 | return s.componentUpdateError("Error verifying app update hash", err, &l) 32 | } 33 | verifyFinished := time.Now() 34 | appUpdate.verifiedAt = verifyFinished 35 | appUpdate.verificationProgress = 1 36 | appUpdate.updatedAt = verifyFinished 37 | appUpdate.updateProgress = 1 38 | s.triggerComponentUpdateState("app", appUpdate) 39 | 40 | l.Info().Msg("App update downloaded") 41 | 42 | s.rebootNeeded = true 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/actions.h: -------------------------------------------------------------------------------- 1 | #ifndef EEZ_LVGL_UI_EVENTS_H 2 | #define EEZ_LVGL_UI_EVENTS_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | extern int handle_gesture_screen_switch(lv_event_t *e, lv_dir_t direction, int screenId); 11 | 12 | extern void action_switch_to_menu(lv_event_t * e); 13 | extern void action_switch_to_advanced_menu(lv_event_t * e); 14 | extern void action_switch_to_reset_config(lv_event_t * e); 15 | extern void action_switch_to_about(lv_event_t * e); 16 | extern void action_menu_screen_gesture(lv_event_t * e); 17 | extern void action_home_screen_gesture(lv_event_t * e); 18 | extern void action_menu_advanced_screen_gesture(lv_event_t * e); 19 | extern void action_reset_config_screen_gesture(lv_event_t * e); 20 | extern void action_about_screen_gesture(lv_event_t * e); 21 | extern void action_switch_to_status(lv_event_t * e); 22 | extern void action_common_click_event(lv_event_t * e); 23 | extern void action_handle_common_press_event(lv_event_t * e); 24 | extern void action_reset_config(lv_event_t * e); 25 | extern void action_reboot(lv_event_t * e); 26 | extern void action_switch_to_reboot(lv_event_t * e); 27 | extern void action_dhcpc(lv_event_t * e); 28 | extern void action_switch_to_dhcpc(lv_event_t * e); 29 | 30 | 31 | #ifdef __cplusplus 32 | } 33 | #endif 34 | 35 | #endif /*EEZ_LVGL_UI_EVENTS_H*/ -------------------------------------------------------------------------------- /internal/sync/mutex.go: -------------------------------------------------------------------------------- 1 | //go:build synctrace 2 | 3 | package sync 4 | 5 | import ( 6 | gosync "sync" 7 | ) 8 | 9 | // Mutex is a wrapper around the sync.Mutex 10 | type Mutex struct { 11 | mu gosync.Mutex 12 | } 13 | 14 | // Lock locks the mutex 15 | func (m *Mutex) Lock() { 16 | logLock(m) 17 | m.mu.Lock() 18 | } 19 | 20 | // Unlock unlocks the mutex 21 | func (m *Mutex) Unlock() { 22 | logUnlock(m) 23 | m.mu.Unlock() 24 | } 25 | 26 | // TryLock tries to lock the mutex 27 | func (m *Mutex) TryLock() bool { 28 | logTryLock(m) 29 | l := m.mu.TryLock() 30 | logTryLockResult(m, l) 31 | return l 32 | } 33 | 34 | // RWMutex is a wrapper around the sync.RWMutex 35 | type RWMutex struct { 36 | mu gosync.RWMutex 37 | } 38 | 39 | // Lock locks the mutex 40 | func (m *RWMutex) Lock() { 41 | logLock(m) 42 | m.mu.Lock() 43 | } 44 | 45 | // Unlock unlocks the mutex 46 | func (m *RWMutex) Unlock() { 47 | logUnlock(m) 48 | m.mu.Unlock() 49 | } 50 | 51 | // RLock locks the mutex for reading 52 | func (m *RWMutex) RLock() { 53 | logRLock(m) 54 | m.mu.RLock() 55 | } 56 | 57 | // RUnlock unlocks the mutex for reading 58 | func (m *RWMutex) RUnlock() { 59 | logRUnlock(m) 60 | m.mu.RUnlock() 61 | } 62 | 63 | // TryRLock tries to lock the mutex for reading 64 | func (m *RWMutex) TryRLock() bool { 65 | logTryRLock(m) 66 | l := m.mu.TryRLock() 67 | logTryRLockResult(m, l) 68 | return l 69 | } 70 | -------------------------------------------------------------------------------- /ui/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | 3 | import { cx } from "@/cva.config"; 4 | 5 | interface CardPropsType { 6 | children: React.ReactNode; 7 | className?: string; 8 | } 9 | 10 | export const GridCard = ({ 11 | children, 12 | cardClassName, 13 | }: { 14 | children: React.ReactNode; 15 | cardClassName?: string; 16 | }) => { 17 | return ( 18 | 19 |
20 |
21 |
22 |
{children}
23 |
24 | 25 | ); 26 | }; 27 | 28 | const Card = forwardRef(({ children, className }, ref) => { 29 | return ( 30 |
37 | {children} 38 |
39 | ); 40 | }); 41 | 42 | Card.displayName = "Card"; 43 | 44 | export default Card; 45 | -------------------------------------------------------------------------------- /ui/src/components/GridBackground.tsx: -------------------------------------------------------------------------------- 1 | export default function GridBackground() { 2 | return ( 3 |
4 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /scripts/generate_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generate gRPC code from proto files 3 | 4 | set -e 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" 8 | 9 | cd "$PROJECT_ROOT" 10 | 11 | # Check if protoc is installed 12 | if ! command -v protoc &> /dev/null; then 13 | echo "Error: protoc is not installed" 14 | echo "Install it with:" 15 | echo " apt-get install protobuf-compiler # Debian/Ubuntu" 16 | echo " brew install protobuf # macOS" 17 | exit 1 18 | fi 19 | 20 | # Check if protoc-gen-go is installed 21 | if ! command -v protoc-gen-go &> /dev/null; then 22 | echo "Error: protoc-gen-go is not installed" 23 | echo "Install it with: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest" 24 | exit 1 25 | fi 26 | 27 | # Check if protoc-gen-go-grpc is installed 28 | if ! command -v protoc-gen-go-grpc &> /dev/null; then 29 | echo "Error: protoc-gen-go-grpc is not installed" 30 | echo "Install it with: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest" 31 | exit 1 32 | fi 33 | 34 | # Generate code 35 | echo "Generating gRPC code from proto files..." 36 | protoc \ 37 | --go_out=. \ 38 | --go_opt=paths=source_relative \ 39 | --go-grpc_out=. \ 40 | --go-grpc_opt=paths=source_relative \ 41 | internal/native/proto/native.proto 42 | 43 | echo "Done!" 44 | 45 | -------------------------------------------------------------------------------- /ui/src/components/EmptyCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { GridCard } from "@components/Card"; 4 | import { cx } from "@/cva.config"; 5 | 6 | interface Props { 7 | IconElm?: React.FC<{ className: string | undefined }>; 8 | headline: string; 9 | description?: string | React.ReactNode; 10 | BtnElm?: React.ReactNode; 11 | className?: string; 12 | } 13 | 14 | export default function EmptyCard({ 15 | IconElm, 16 | headline, 17 | description, 18 | BtnElm, 19 | className, 20 | }: Props) { 21 | return ( 22 | 23 |
29 |
30 |
31 | {IconElm && ( 32 | 33 | )} 34 |

35 | {headline} 36 |

37 |
38 |

39 | {description} 40 |

41 |
42 | {BtnElm} 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/routes/adopt.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "react-router"; 2 | import type { LoaderFunction, LoaderFunctionArgs } from "react-router"; 3 | 4 | import { DEVICE_API } from "@/ui.config"; 5 | import api from "@/api"; 6 | 7 | export interface CloudState { 8 | connected: boolean; 9 | url: string; 10 | appUrl: string; 11 | } 12 | 13 | const loader: LoaderFunction = async ({ request }: LoaderFunctionArgs) => { 14 | const url = new URL(request.url); 15 | const searchParams = url.searchParams; 16 | 17 | const tempToken = searchParams.get("tempToken"); 18 | const deviceId = searchParams.get("deviceId"); 19 | const oidcGoogle = searchParams.get("oidcGoogle"); 20 | const clientId = searchParams.get("clientId"); 21 | 22 | const [cloudStateResponse, registerResponse] = await Promise.all([ 23 | api.GET(`${DEVICE_API}/cloud/state`), 24 | api.POST(`${DEVICE_API}/cloud/register`, { 25 | token: tempToken, 26 | oidcGoogle, 27 | clientId, 28 | }), 29 | ]); 30 | 31 | if (!cloudStateResponse.ok) throw new Error("Failed to get cloud state"); 32 | const cloudState = (await cloudStateResponse.json()) as CloudState; 33 | 34 | if (!registerResponse.ok) throw new Error("Failed to register device"); 35 | 36 | return redirect(cloudState.appUrl + `/devices/${deviceId}/setup`); 37 | }; 38 | 39 | export default function AdoptRoute() { 40 | return (<>); 41 | } 42 | 43 | AdoptRoute.loader = loader; 44 | -------------------------------------------------------------------------------- /ui/src/components/StatusCards.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cx } from "@/cva.config"; 4 | 5 | interface Props { 6 | title: string; 7 | status: string; 8 | icon?: React.FC<{ className: string | undefined }>; 9 | iconClassName?: string; 10 | statusIndicatorClassName?: string; 11 | } 12 | 13 | export default function StatusCard({ 14 | title, 15 | status, 16 | icon: Icon, 17 | iconClassName, 18 | statusIndicatorClassName, 19 | }: Props) { 20 | return ( 21 |
22 | {Icon ? ( 23 | 24 | 25 | 26 | ) : null} 27 | 28 |
29 |
30 | {title} 31 |
32 |
33 |
34 |
40 | {status} 41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /scripts/build_cgo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") 5 | source ${SCRIPT_PATH}/build_utils.sh 6 | 7 | CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} 8 | 9 | CGO_PATH=$(realpath "${SCRIPT_PATH}/../internal/native/cgo") 10 | BUILD_DIR=${CGO_PATH}/build 11 | 12 | CMAKE_TOOLCHAIN_FILE=/opt/jetkvm-native-buildkit/rv1106-jetkvm-v2.cmake 13 | CLEAN_ALL=${CLEAN_ALL:-0} 14 | 15 | if [ "$CLEAN_ALL" -eq 1 ]; then 16 | rm -rf "${BUILD_DIR}" 17 | fi 18 | 19 | TMP_DIR=$(mktemp -d) 20 | pushd "${CGO_PATH}" > /dev/null 21 | 22 | msg_info "▶ Generating UI index" 23 | ./ui_index.gen.sh 24 | 25 | msg_info "▶ Building native library" 26 | VERBOSE=1 cmake -B "${BUILD_DIR}" \ 27 | -DCMAKE_SYSTEM_PROCESSOR=armv7l \ 28 | -DCMAKE_SYSTEM_NAME=Linux \ 29 | -DCMAKE_CROSSCOMPILING=1 \ 30 | -DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE \ 31 | -DLV_BUILD_USE_KCONFIG=ON \ 32 | -DLV_BUILD_DEFCONFIG_PATH=${CGO_PATH}/lvgl_defconfig \ 33 | -DCONFIG_LV_BUILD_EXAMPLES=OFF \ 34 | -DCONFIG_LV_BUILD_DEMOS=OFF \ 35 | -DSKIP_GLIBC_NAMES=ON \ 36 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \ 37 | -DCMAKE_INSTALL_PREFIX="${TMP_DIR}" 38 | 39 | msg_info "▶ Copying built library and header files" 40 | cmake --build "${BUILD_DIR}" --target install 41 | cp -r "${TMP_DIR}/include" "${CGO_PATH}" 42 | cp -r "${TMP_DIR}/lib" "${CGO_PATH}" 43 | rm -rf "${TMP_DIR}" 44 | 45 | popd > /dev/null 46 | -------------------------------------------------------------------------------- /ui/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/native/cgo/screen.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_H 2 | #define SCREEN_H 3 | 4 | #include 5 | 6 | typedef void (indev_handler_t)(lv_event_code_t code); 7 | 8 | void lvgl_set_indev_handler(indev_handler_t *handler); 9 | 10 | void lvgl_init(u_int16_t rotation); 11 | void lvgl_tick(void); 12 | 13 | void lvgl_set_rotation(lv_display_t *disp, u_int16_t rotation); 14 | 15 | /** 16 | * @brief Set the text of an object 17 | * 18 | * @param name The name of the object 19 | * @param text The text to set 20 | * @return void 21 | */ 22 | void ui_set_text(const char *name, const char *text); 23 | 24 | /** 25 | * @brief Get the object with the given name 26 | * 27 | * @param name The name of the object 28 | * @return lv_obj_t* The object with the given name 29 | */ 30 | lv_obj_t *ui_get_obj(const char *name); 31 | 32 | /** 33 | * @brief Get the style with the given name 34 | * 35 | * @param name The name of the style 36 | * @return lv_style_t* The style with the given name 37 | */ 38 | lv_style_t *ui_get_style(const char *name); 39 | 40 | /** 41 | * @brief Get the image with the given name 42 | * 43 | * @param name The name of the image 44 | * @return const lv_img_dsc_t* The image with the given name 45 | */ 46 | const lv_img_dsc_t *ui_get_image(const char *name); 47 | 48 | /** 49 | * @brief Get the current screen name 50 | * 51 | * @return const char* The name of the current screen 52 | */ 53 | const char *ui_get_current_screen(); 54 | 55 | #endif // SCREEN_H 56 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "github.com/jetkvm/kvm/internal/logging" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error { 9 | return logging.ErrorfL(l, format, err, args...) 10 | } 11 | 12 | var ( 13 | logger = logging.GetSubsystemLogger("jetkvm") 14 | failsafeLogger = logging.GetSubsystemLogger("failsafe") 15 | networkLogger = logging.GetSubsystemLogger("network") 16 | cloudLogger = logging.GetSubsystemLogger("cloud") 17 | websocketLogger = logging.GetSubsystemLogger("websocket") 18 | webrtcLogger = logging.GetSubsystemLogger("webrtc") 19 | nativeLogger = logging.GetSubsystemLogger("native") 20 | nbdLogger = logging.GetSubsystemLogger("nbd") 21 | timesyncLogger = logging.GetSubsystemLogger("timesync") 22 | jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc") 23 | hidRPCLogger = logging.GetSubsystemLogger("hidrpc") 24 | watchdogLogger = logging.GetSubsystemLogger("watchdog") 25 | websecureLogger = logging.GetSubsystemLogger("websecure") 26 | otaLogger = logging.GetSubsystemLogger("ota") 27 | serialLogger = logging.GetSubsystemLogger("serial") 28 | terminalLogger = logging.GetSubsystemLogger("terminal") 29 | displayLogger = logging.GetSubsystemLogger("display") 30 | wolLogger = logging.GetSubsystemLogger("wol") 31 | usbLogger = logging.GetSubsystemLogger("usb") 32 | // external components 33 | ginLogger = logging.GetSubsystemLogger("gin") 34 | ) 35 | -------------------------------------------------------------------------------- /ui/src/components/useCopyToClipBoard.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | export function useCopyToClipboard(resetInterval = 2000) { 4 | const [isCopied, setIsCopied] = useState(false); 5 | 6 | const copy = useCallback(async (text: string) => { 7 | if (!text) return false; 8 | 9 | let success = false; 10 | 11 | if (navigator.clipboard && window.isSecureContext) { 12 | try { 13 | await navigator.clipboard.writeText(text); 14 | success = true; 15 | } catch (err) { 16 | console.warn("Clipboard API failed:", err); 17 | } 18 | } 19 | 20 | // Fallback for insecure contexts 21 | if (!success) { 22 | const textarea = document.createElement("textarea"); 23 | textarea.value = text; 24 | textarea.style.position = "fixed"; 25 | textarea.style.opacity = "0"; 26 | document.body.appendChild(textarea); 27 | textarea.focus(); 28 | textarea.select(); 29 | 30 | try { 31 | success = document.execCommand("copy"); 32 | } catch (err) { 33 | console.error("Fallback copy failed:", err); 34 | success = false; 35 | } finally { 36 | document.body.removeChild(textarea); 37 | } 38 | } 39 | 40 | setIsCopied(success); 41 | if (success && resetInterval > 0) { 42 | setTimeout(() => setIsCopied(false), resetInterval); 43 | } 44 | 45 | return success; 46 | }, [resetInterval]); 47 | 48 | return { copy, isCopied }; 49 | } 50 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "html/template" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/jetkvm/kvm/internal/native" 11 | "github.com/prometheus/common/version" 12 | ) 13 | 14 | var versionInfoTmpl = ` 15 | JetKVM Application, version {{.version}} (branch: {{.branch}}, revision: {{.revision}}) 16 | build date: {{.buildDate}} 17 | go version: {{.goVersion}} 18 | platform: {{.platform}} 19 | 20 | {{if .lvglVersion}} 21 | LVGL version {{.lvglVersion}} 22 | {{end}} 23 | ` 24 | 25 | func GetVersionData(isJson bool) ([]byte, error) { 26 | version.Version = GetBuiltAppVersion() 27 | 28 | m := map[string]string{ 29 | "version": version.Version, 30 | "revision": version.GetRevision(), 31 | "branch": version.Branch, 32 | "buildDate": version.BuildDate, 33 | "goVersion": version.GoVersion, 34 | "platform": runtime.GOOS + "/" + runtime.GOARCH, 35 | } 36 | 37 | lvglVersion := native.GetLVGLVersion() 38 | if lvglVersion != "" { 39 | m["lvglVersion"] = lvglVersion 40 | } 41 | 42 | if isJson { 43 | jsonData, err := json.Marshal(m) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return jsonData, nil 48 | } 49 | 50 | t := template.Must( 51 | template.New("version").Parse( 52 | strings.TrimSpace(versionInfoTmpl), 53 | ), 54 | ) 55 | 56 | var buf bytes.Buffer 57 | if err := t.ExecuteTemplate(&buf, "version", m); err != nil { 58 | return nil, err 59 | } 60 | 61 | return buf.Bytes(), nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/native/cgo/ui_index.gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat << EOF > ui_index.c 4 | // This file was generated by ui_index.gen.sh, do not edit it manually 5 | #include "ui_index.h" 6 | 7 | ui_obj_map ui_objects[] = { 8 | $(grep -h "lv_obj_t \*" ui/screens.h | sed 's/lv_obj_t \*//g' | sed 's/;//g' | while read -r line; do 9 | echo " {\"$line\", &(objects.$line)}," 10 | done) 11 | }; 12 | 13 | const int ui_objects_size = sizeof(ui_objects) / sizeof(ui_objects[0]); 14 | 15 | ui_style_map ui_styles[] = { 16 | $(grep 'lv_style_t \*get_style_' ui/styles.h | sed 's/lv_style_t \*get_style_//g' | sed 's/_MAIN_DEFAULT();//g' | sed 's/\r//' | while read -r line; do 17 | echo " {\"$line\", &get_style_${line}_MAIN_DEFAULT}," 18 | done) 19 | }; 20 | 21 | const int ui_styles_size = sizeof(ui_styles) / sizeof(ui_styles[0]); 22 | 23 | ui_img_map ui_images[] = { 24 | $(grep "extern const lv_img_dsc_t " ui/images.h | sed 's/extern const lv_img_dsc_t //g' | sed 's/;//g' | while read -r line; do 25 | echo " {\"$line\", &$line}," 26 | done) 27 | }; 28 | 29 | const int ui_images_size = sizeof(ui_images) / sizeof(ui_images[0]); 30 | 31 | ui_var_map ui_vars[] = { 32 | $(grep 'extern const char \*get_var_' ui/vars.h | sed 's/extern const char \*get_var_//g' | sed 's/();//g' | sed 's/\r//' | while read -r line; do 33 | echo " {\"$line\", &get_var_$line, &set_var_$line}," 34 | done) 35 | }; 36 | 37 | const int ui_vars_size = sizeof(ui_vars) / sizeof(ui_vars[0]); 38 | EOF 39 | 40 | echo "ui_index.c has been generated successfully." 41 | -------------------------------------------------------------------------------- /scripts/configure_vscode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | 5 | DEFAULT_C_INTELLISENSE_SETTINGS = { 6 | "configurations": [ 7 | { 8 | "name": "Linux", 9 | "includePath": [ 10 | "${workspaceFolder}/**" 11 | ], 12 | "defines": [], 13 | # "compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc", 14 | "cStandard": "c17", 15 | "cppStandard": "gnu++17", 16 | "intelliSenseMode": "linux-gcc-arm", 17 | "configurationProvider": "ms-vscode.cmake-tools" 18 | } 19 | ], 20 | "version": 4 21 | } 22 | 23 | def configure_c_intellisense(): 24 | settings_path = os.path.join('.vscode', 'c_cpp_properties.json') 25 | settings = DEFAULT_C_INTELLISENSE_SETTINGS.copy() 26 | 27 | # open existing settings if they exist 28 | if os.path.exists(settings_path): 29 | with open(settings_path, 'r') as f: 30 | settings = json.load(f) 31 | 32 | # update compiler path 33 | settings['configurations'][0]['compilerPath'] = "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc" 34 | settings['configurations'][0]['configurationProvider'] = "ms-vscode.cmake-tools" 35 | 36 | with open(settings_path, 'w') as f: 37 | json.dump(settings, f, indent=4) 38 | 39 | print("C/C++ IntelliSense configuration updated.") 40 | 41 | 42 | if __name__ == "__main__": 43 | configure_c_intellisense() -------------------------------------------------------------------------------- /ui/src/components/FieldLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cx } from "@/cva.config"; 4 | 5 | interface Props { 6 | label: string | React.ReactNode; 7 | id?: string; 8 | as?: "label" | "span"; 9 | description?: string | React.ReactNode | null; 10 | disabled?: boolean; 11 | } 12 | export default function FieldLabel({ 13 | label, 14 | id, 15 | as = "label", 16 | description, 17 | disabled, 18 | }: Props) { 19 | if (as === "label") { 20 | return ( 21 | 35 | ); 36 | } else if (as === "span") { 37 | return ( 38 |
39 | 40 | {label} 41 | 42 | {description && ( 43 | 44 | {description} 45 | 46 | )} 47 |
48 | ); 49 | } else { 50 | return <>; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dc_metrics.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | var ( 10 | dcCurrentGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 11 | Name: "jetkvm_dc_current_amperes", 12 | Help: "Current DC power consumption in amperes", 13 | }) 14 | 15 | dcPowerGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 16 | Name: "jetkvm_dc_power_watts", 17 | Help: "DC power consumption in watts", 18 | }) 19 | 20 | dcVoltageGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 21 | Name: "jetkvm_dc_voltage_volts", 22 | Help: "DC voltage in volts", 23 | }) 24 | 25 | dcStateGauge = prometheus.NewGauge(prometheus.GaugeOpts{ 26 | Name: "jetkvm_dc_power_state", 27 | Help: "DC power state (1 = on, 0 = off)", 28 | }) 29 | 30 | dcMetricsRegistered sync.Once 31 | ) 32 | 33 | // registerDCMetrics registers the DC power metrics with Prometheus (called once when DC control is mounted) 34 | func registerDCMetrics() { 35 | dcMetricsRegistered.Do(func() { 36 | prometheus.MustRegister(dcCurrentGauge) 37 | prometheus.MustRegister(dcPowerGauge) 38 | prometheus.MustRegister(dcVoltageGauge) 39 | prometheus.MustRegister(dcStateGauge) 40 | }) 41 | } 42 | 43 | // updateDCMetrics updates the Prometheus metrics with current DC power state values 44 | func updateDCMetrics(state DCPowerState) { 45 | dcCurrentGauge.Set(state.Current) 46 | dcPowerGauge.Set(state.Power) 47 | dcVoltageGauge.Set(state.Voltage) 48 | if state.IsOn { 49 | dcStateGauge.Set(1) 50 | } else { 51 | dcStateGauge.Set(0) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "lib": [ 7 | "ES2021", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "module": "ESNext", 12 | "skipLibCheck": true, 13 | "allowJs": true, 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": false, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "erasableSyntaxOnly": true, 27 | "noUncheckedSideEffectImports": true, 28 | "types": [ 29 | "vite/client" 30 | ], 31 | /* Import Aliases */ 32 | "paths": { 33 | "@components/*": [ 34 | "./src/components/*" 35 | ], 36 | "@routes/*": [ 37 | "./src/routes/*" 38 | ], 39 | "@hooks/*": [ 40 | "./src/hooks/*" 41 | ], 42 | "@providers/*": [ 43 | "./src/providers/*" 44 | ], 45 | "@assets/*": [ 46 | "./src/assets/*" 47 | ], 48 | "@localizations/*": [ 49 | "./localization/paraglide/*" 50 | ], 51 | "@/*": [ 52 | "./src/*" 53 | ] 54 | } 55 | }, 56 | "include": [ 57 | "src" 58 | ], 59 | "references": [ 60 | { 61 | "path": "./tsconfig.node.json" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /ui/src/keyboardLayouts.ts: -------------------------------------------------------------------------------- 1 | export interface KeyStroke { modifier: number; keys: number[]; } 2 | export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean } 3 | export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo } 4 | export interface KeyboardLayout { 5 | isoCode: string; 6 | name: string; 7 | chars: Record; 8 | modifierDisplayMap: Record; 9 | keyDisplayMap: Record; 10 | virtualKeyboard: { 11 | main: { default: string[], shift: string[] }, 12 | control?: { default: string[], shift?: string[] }, 13 | arrows?: { default: string[] } 14 | }; 15 | } 16 | 17 | // To add a new layout, create a file like the above and add it to the list 18 | import { cs_CZ } from "@/keyboardLayouts/cs_CZ" 19 | import { de_CH } from "@/keyboardLayouts/de_CH" 20 | import { de_DE } from "@/keyboardLayouts/de_DE" 21 | import { en_US } from "@/keyboardLayouts/en_US" 22 | import { en_UK } from "@/keyboardLayouts/en_UK" 23 | import { es_ES } from "@/keyboardLayouts/es_ES" 24 | import { fr_BE } from "@/keyboardLayouts/fr_BE" 25 | import { fr_CH } from "@/keyboardLayouts/fr_CH" 26 | import { fr_FR } from "@/keyboardLayouts/fr_FR" 27 | import { it_IT } from "@/keyboardLayouts/it_IT" 28 | import { nb_NO } from "@/keyboardLayouts/nb_NO" 29 | import { sv_SE } from "@/keyboardLayouts/sv_SE" 30 | import { da_DK } from "@/keyboardLayouts/da_DK" 31 | 32 | export const keyboards: KeyboardLayout[] = [cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE, da_DK]; 33 | -------------------------------------------------------------------------------- /ui/tools/resort_messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import json 4 | from pathlib import Path 5 | 6 | def main(): 7 | p = argparse.ArgumentParser( 8 | description="Sort translations keys in message *.json files" 9 | ) 10 | p.add_argument( 11 | "--path", default="./localization/messages/", help="path to messages *.json" 12 | ) 13 | args = p.parse_args() 14 | 15 | messages_path = Path(args.path) 16 | if not messages_path.is_dir(): 17 | print(f"message path is not a directory: {messages_path}") 18 | raise SystemExit(2) 19 | 20 | files = list(messages_path.glob("*.json")) 21 | if len(files) == 0: 22 | print(f"no message files (*.json) found in: {messages_path}") 23 | raise SystemExit(3) 24 | 25 | for f in files: 26 | print(f"Processing {f.name} ...") 27 | data = json.loads(f.read_text(encoding="utf-8")) 28 | 29 | # Keep $schema first if present 30 | schema = None 31 | if "$schema" in data: 32 | schema = data.pop("$schema") 33 | 34 | sorted_items = dict(sorted(data.items())) 35 | 36 | if schema is not None: 37 | out = {"$schema": schema} 38 | out.update(sorted_items) 39 | else: 40 | out = sorted_items 41 | 42 | f.write_text( 43 | json.dumps(out, ensure_ascii=False, indent=4) + "\n", encoding="utf-8" 44 | ) 45 | 46 | print(f"Processed {len(files)} files in {messages_path}") 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /ui/src/webrtc.d.ts: -------------------------------------------------------------------------------- 1 | interface RTCIceCandidateStats { 2 | address?: string; // The address of the candidate. Could be IPv4, IPv6, or a fully-qualified domain name. 3 | candidateType: "host" | "srflx" | "prflx" | "relay"; // The type of candidate (host, srflx, prflx, relay). 4 | foundation: string; // A unique identifier for this candidate, used for network performance optimization. 5 | id: string; // A unique identifier for this object. 6 | port?: number; // The network port used by the candidate. 7 | priority?: number; // The priority of the candidate. 8 | protocol?: string; // The protocol used by the candidate (tcp or udp). 9 | relatedAddress?: string; // The related address of the candidate. 10 | relatedPort?: number; // The related port of the candidate. 11 | sdpMid?: string; // The media stream identification for the candidate. 12 | sdpMLineIndex?: number; // The index of the media line for the candidate. 13 | tcpType?: string; // The type of TCP candidate (active, passive, or so). 14 | type: "local-candidate" | "remote-candidate"; // The type of the statistics object. 15 | usernameFragment: string; // The username fragment used for message authentication. 16 | timestamp: number; // The timestamp at which the sample was taken. 17 | } 18 | 19 | interface RTCDataChannelStats { 20 | bytesReceived: number; 21 | bytesSent: number; 22 | dataChannelIdentifier: number; 23 | id: string; 24 | label: string; 25 | messagesReceived: number; 26 | messagesSent: number; 27 | protocol: string; 28 | state: string; 29 | timestamp: number; 30 | type: string; 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/components/MacroBar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { LuCommand } from "react-icons/lu"; 3 | 4 | import { useMacrosStore } from "@hooks/stores"; 5 | import useKeyboard from "@hooks/useKeyboard"; 6 | import { useJsonRpc } from "@hooks/useJsonRpc"; 7 | import { Button } from "@components/Button"; 8 | import Container from "@components/Container"; 9 | 10 | export default function MacroBar() { 11 | const { macros, initialized, loadMacros, setSendFn } = useMacrosStore(); 12 | const { executeMacro } = useKeyboard(); 13 | const { send } = useJsonRpc(); 14 | 15 | useEffect(() => { 16 | setSendFn(send); 17 | 18 | if (!initialized) { 19 | loadMacros(); 20 | } 21 | }, [initialized, loadMacros, setSendFn, send]); 22 | 23 | if (macros.length === 0) { 24 | return null; 25 | } 26 | 27 | return ( 28 | 29 |
30 |
31 | 32 |
33 |
34 | {macros.map(macro => ( 35 |
45 |
46 |
47 | ); 48 | } -------------------------------------------------------------------------------- /.devcontainer/podman/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JetKVM podman devcontainer", 3 | "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": { 6 | // Should match what is defined in ui/package.json 7 | "version": "22.20.0" 8 | } 9 | }, 10 | "runArgs": [ 11 | "--userns=keep-id", 12 | "--security-opt=label=disable", 13 | "--security-opt=label=nested" 14 | ], 15 | "containerUser": "vscode", 16 | "containerEnv": { 17 | "HOME": "/home/vscode" 18 | }, 19 | "mounts": [ 20 | "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" 21 | ], 22 | "onCreateCommand": ".devcontainer/install-deps.sh", 23 | "customizations": { 24 | "vscode": { 25 | "extensions": [ 26 | // coding styles 27 | "chrislajoie.vscode-modelines", 28 | "editorconfig.editorconfig", 29 | // GitHub 30 | "GitHub.vscode-pull-request-github", 31 | "github.vscode-github-actions", 32 | // Golang 33 | "golang.go", 34 | // C / C++ 35 | "ms-vscode.cpptools", 36 | "ms-vscode.cpptools-extension-pack", 37 | // CMake / Makefile 38 | "ms-vscode.makefile-tools", 39 | "ms-vscode.cmake-tools", 40 | // Frontend 41 | "esbenp.prettier-vscode", 42 | "dbaeumer.vscode-eslint", 43 | "bradlc.vscode-tailwindcss", 44 | "codeandstuff.package-json-upgrade", 45 | // Localization 46 | "inlang.vs-code-extension" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/native/eez/src/ui/vars.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "vars.h" 5 | 6 | char app_version[100] = { 0 }; 7 | char system_version[100] = { 0 }; 8 | char lvgl_version[32] = { 0 }; 9 | char main_screen[32] = "home_screen"; 10 | 11 | const char *get_var_app_version() { 12 | return app_version; 13 | } 14 | 15 | const char *get_var_system_version() { 16 | return system_version; 17 | } 18 | 19 | const char *get_var_lvgl_version() { 20 | if (lvgl_version[0] == '\0') { 21 | char buf[32]; 22 | sprintf(buf, "%d.%d.%d", LVGL_VERSION_MAJOR, LVGL_VERSION_MINOR, LVGL_VERSION_PATCH); 23 | 24 | 25 | strncpy(lvgl_version, buf, sizeof(lvgl_version) / sizeof(char)); 26 | app_version[sizeof(lvgl_version) / sizeof(char) - 1] = 0; 27 | } 28 | return lvgl_version; 29 | } 30 | 31 | void set_var_app_version(const char *value) { 32 | strncpy(app_version, value, sizeof(app_version) / sizeof(char)); 33 | app_version[sizeof(app_version) / sizeof(char) - 1] = 0; 34 | } 35 | 36 | void set_var_system_version(const char *value) { 37 | strncpy(system_version, value, sizeof(system_version) / sizeof(char)); 38 | system_version[sizeof(system_version) / sizeof(char) - 1] = 0; 39 | } 40 | 41 | void set_var_lvgl_version(const char *value) {} 42 | 43 | void set_var_main_screen(const char *value) { 44 | strncpy(main_screen, value, sizeof(main_screen) / sizeof(char)); 45 | main_screen[sizeof(main_screen) / sizeof(char) - 1] = 0; 46 | } 47 | 48 | const char *get_var_main_screen() { 49 | return main_screen; 50 | } -------------------------------------------------------------------------------- /internal/native/eez/src/ui/ui.c: -------------------------------------------------------------------------------- 1 | #if defined(EEZ_FOR_LVGL) 2 | #include 3 | #endif 4 | 5 | #include "ui.h" 6 | #include "screens.h" 7 | #include "images.h" 8 | #include "actions.h" 9 | #include "vars.h" 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | jetkvm_rpc_handler_t *ui_rpc_handler = NULL; 19 | 20 | void ui_set_rpc_handler(jetkvm_rpc_handler_t *handler) { 21 | ui_rpc_handler = handler; 22 | } 23 | 24 | void ui_call_rpc_handler(const char *method, const char *params) { 25 | if (ui_rpc_handler != NULL) { 26 | (*ui_rpc_handler)(method, params); 27 | } 28 | } 29 | 30 | #if defined(EEZ_FOR_LVGL) 31 | 32 | void ui_init() { 33 | eez_flow_init(assets, sizeof(assets), (lv_obj_t **)&objects, sizeof(objects), images, sizeof(images), actions); 34 | } 35 | 36 | void ui_tick() { 37 | eez_flow_tick(); 38 | tick_screen(g_currentScreen); 39 | } 40 | 41 | #else 42 | 43 | #include 44 | 45 | static int16_t currentScreen = -1; 46 | 47 | static lv_obj_t *getLvglObjectFromIndex(int32_t index) { 48 | if (index == -1) { 49 | return 0; 50 | } 51 | return ((lv_obj_t **)&objects)[index]; 52 | } 53 | 54 | void loadScreen(enum ScreensEnum screenId) { 55 | currentScreen = screenId - 1; 56 | lv_obj_t *screen = getLvglObjectFromIndex(currentScreen); 57 | lv_scr_load(screen); 58 | // lv_scr_load_anim(screen, LV_SCR_LOAD_ANIM_FADE_IN, 200, 0, false); 59 | } 60 | 61 | void ui_init() { 62 | create_screens(); 63 | loadScreen(SCREEN_ID_BOOT_SCREEN); 64 | 65 | } 66 | 67 | void ui_tick() { 68 | tick_screen(currentScreen); 69 | } 70 | 71 | #endif 72 | -------------------------------------------------------------------------------- /ui/src/routes/devices.already-adopted.tsx: -------------------------------------------------------------------------------- 1 | import { LinkButton } from "@/components/Button"; 2 | import SimpleNavbar from "@/components/SimpleNavbar"; 3 | import Container from "@/components/Container"; 4 | import GridBackground from "@components/GridBackground"; 5 | import { m } from "@localizations/messages.js"; 6 | 7 | export default function DevicesAlreadyAdopted() { 8 | return ( 9 | <> 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 |
18 |

{m.already_adopted_title()}

19 |

20 | {m.already_adopted_other_user()} 21 |

22 |

23 | {m.already_adopted_new_owner()} 24 |

25 |
26 | 27 |
28 | 34 |
35 |
36 |
37 |
38 |
39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /timesync.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/jetkvm/kvm/internal/timesync" 8 | ) 9 | 10 | var ( 11 | timeSync *timesync.TimeSync 12 | builtTimestamp string 13 | ) 14 | 15 | func isTimeSyncNeeded() bool { 16 | if builtTimestamp == "" { 17 | timesyncLogger.Warn().Msg("built timestamp is not set, time sync is needed") 18 | return true 19 | } 20 | 21 | ts, err := strconv.Atoi(builtTimestamp) 22 | if err != nil { 23 | timesyncLogger.Warn().Str("error", err.Error()).Msg("failed to parse built timestamp") 24 | return true 25 | } 26 | 27 | // builtTimestamp is UNIX timestamp in seconds 28 | builtTime := time.Unix(int64(ts), 0) 29 | now := time.Now() 30 | 31 | if now.Sub(builtTime) < 0 { 32 | timesyncLogger.Warn(). 33 | Str("built_time", builtTime.Format(time.RFC3339)). 34 | Str("now", now.Format(time.RFC3339)). 35 | Msg("system time is behind the built time, time sync is needed") 36 | return true 37 | } 38 | 39 | return false 40 | } 41 | 42 | func initTimeSync() { 43 | timeSync = timesync.NewTimeSync(×ync.TimeSyncOptions{ 44 | Logger: timesyncLogger, 45 | NetworkConfig: config.NetworkConfig, 46 | PreCheckIPv4: func() (bool, error) { 47 | if !networkManager.IPv4Ready() { 48 | return false, nil 49 | } 50 | return true, nil 51 | }, 52 | PreCheckIPv6: func() (bool, error) { 53 | if !networkManager.IPv6Ready() { 54 | return false, nil 55 | } 56 | return true, nil 57 | }, 58 | PreCheckFunc: func() (bool, error) { 59 | if !networkManager.IsOnline() { 60 | return false, nil 61 | } 62 | return true, nil 63 | }, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /internal/native/interface.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | // NativeInterface defines the interface that both Native and NativeProxy implement 4 | type NativeInterface interface { 5 | Start() error 6 | VideoSetSleepMode(enabled bool) error 7 | VideoGetSleepMode() (bool, error) 8 | VideoSleepModeSupported() bool 9 | VideoSetQualityFactor(factor float64) error 10 | VideoGetQualityFactor() (float64, error) 11 | VideoSetEDID(edid string) error 12 | VideoGetEDID() (string, error) 13 | VideoLogStatus() (string, error) 14 | VideoStop() error 15 | VideoStart() error 16 | GetLVGLVersion() (string, error) 17 | UIObjHide(objName string) (bool, error) 18 | UIObjShow(objName string) (bool, error) 19 | UISetVar(name string, value string) 20 | UIGetVar(name string) string 21 | UIObjAddState(objName string, state string) (bool, error) 22 | UIObjClearState(objName string, state string) (bool, error) 23 | UIObjAddFlag(objName string, flag string) (bool, error) 24 | UIObjClearFlag(objName string, flag string) (bool, error) 25 | UIObjSetOpacity(objName string, opacity int) (bool, error) 26 | UIObjFadeIn(objName string, duration uint32) (bool, error) 27 | UIObjFadeOut(objName string, duration uint32) (bool, error) 28 | UIObjSetLabelText(objName string, text string) (bool, error) 29 | UIObjSetImageSrc(objName string, image string) (bool, error) 30 | DisplaySetRotation(rotation uint16) (bool, error) 31 | UpdateLabelIfChanged(objName string, newText string) 32 | UpdateLabelAndChangeVisibility(objName string, newText string) 33 | SwitchToScreenIf(screenName string, shouldSwitch []string) 34 | SwitchToScreenIfDifferent(screenName string) 35 | DoNotUseThisIsForCrashTestingOnly() 36 | } 37 | -------------------------------------------------------------------------------- /internal/tzdata/gen.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "archive/zip" 7 | "bytes" 8 | "flag" 9 | "fmt" 10 | "os" 11 | "text/template" 12 | ) 13 | 14 | var tmpl = `// Code generated by "go run gen.go". DO NOT EDIT. 15 | //go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go 16 | package tzdata 17 | var TimeZones = []string{ 18 | {{- range . }} 19 | "{{.}}", 20 | {{- end }} 21 | } 22 | ` 23 | 24 | var filename = flag.String("output", "tzdata.go", "output file name") 25 | 26 | func main() { 27 | flag.Parse() 28 | 29 | path := os.Getenv("ZONEINFO") 30 | if path == "" { 31 | fmt.Println("ZONEINFO is not set") 32 | os.Exit(1) 33 | } 34 | 35 | if _, err := os.Stat(path); os.IsNotExist(err) { 36 | fmt.Printf("ZONEINFO %s does not exist\n", path) 37 | os.Exit(1) 38 | } 39 | 40 | zipfile, err := zip.OpenReader(path) 41 | if err != nil { 42 | fmt.Printf("Error opening ZONEINFO %s: %v\n", path, err) 43 | os.Exit(1) 44 | } 45 | defer zipfile.Close() 46 | 47 | timezones := []string{} 48 | 49 | for _, file := range zipfile.File { 50 | timezones = append(timezones, file.Name) 51 | } 52 | 53 | var buf bytes.Buffer 54 | 55 | tmpl, err := template.New("tzdata").Parse(tmpl) 56 | if err != nil { 57 | fmt.Printf("Error parsing template: %v\n", err) 58 | os.Exit(1) 59 | } 60 | 61 | err = tmpl.Execute(&buf, timezones) 62 | if err != nil { 63 | fmt.Printf("Error executing template: %v\n", err) 64 | os.Exit(1) 65 | } 66 | 67 | err = os.WriteFile(*filename, buf.Bytes(), 0644) 68 | if err != nil { 69 | fmt.Printf("Error writing file %s: %v\n", *filename, err) 70 | os.Exit(1) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/websecure/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package websecure 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | fixtureEd25519Certificate = `-----BEGIN CERTIFICATE----- 10 | MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG 11 | A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1 12 | MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV 13 | BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev 14 | bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy 15 | r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U 16 | C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I 17 | -----END CERTIFICATE-----` 18 | 19 | fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY----- 20 | MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB 21 | -----END PRIVATE KEY-----` 22 | 23 | certStore *CertStore 24 | certSigner *SelfSigner 25 | ) 26 | 27 | func TestMain(m *testing.M) { 28 | tlsStorePath, err := os.MkdirTemp("", "jktls.*") 29 | if err != nil { 30 | defaultLogger.Fatal().Err(err).Msg("failed to create temp directory") 31 | } 32 | 33 | certStore = NewCertStore(tlsStorePath, nil) 34 | certStore.LoadCertificates() 35 | 36 | certSigner = NewSelfSigner( 37 | certStore, 38 | nil, 39 | "ci.jetkvm.com", 40 | "JetKVM", 41 | "JetKVM", 42 | "JetKVM", 43 | ) 44 | 45 | m.Run() 46 | 47 | os.RemoveAll(tlsStorePath) 48 | } 49 | 50 | func TestSaveEd25519Certificate(t *testing.T) { 51 | err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true) 52 | if err != nil { 53 | t.Fatalf("failed to save certificate: %v", err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/nmlite/link/sysctl.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func (nm *NetlinkManager) setSysctlValues(ifaceName string, values map[string]int) error { 12 | for name, value := range values { 13 | name = fmt.Sprintf(name, ifaceName) 14 | name = strings.ReplaceAll(name, ".", "/") 15 | 16 | if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil { 17 | return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err) 18 | } 19 | } 20 | return nil 21 | } 22 | 23 | // EnableIPv6 enables IPv6 on the interface 24 | func (nm *NetlinkManager) EnableIPv6(ifaceName string) error { 25 | return nm.setSysctlValues(ifaceName, map[string]int{ 26 | "net.ipv6.conf.%s.disable_ipv6": 0, 27 | "net.ipv6.conf.%s.accept_ra": 2, 28 | }) 29 | } 30 | 31 | // DisableIPv6 disables IPv6 on the interface 32 | func (nm *NetlinkManager) DisableIPv6(ifaceName string) error { 33 | return nm.setSysctlValues(ifaceName, map[string]int{ 34 | "net.ipv6.conf.%s.disable_ipv6": 1, 35 | }) 36 | } 37 | 38 | // EnableIPv6SLAAC enables IPv6 SLAAC on the interface 39 | func (nm *NetlinkManager) EnableIPv6SLAAC(ifaceName string) error { 40 | return nm.setSysctlValues(ifaceName, map[string]int{ 41 | "net.ipv6.conf.%s.disable_ipv6": 0, 42 | "net.ipv6.conf.%s.accept_ra": 2, 43 | }) 44 | } 45 | 46 | // EnableIPv6LinkLocal enables IPv6 link-local only on the interface 47 | func (nm *NetlinkManager) EnableIPv6LinkLocal(ifaceName string) error { 48 | return nm.setSysctlValues(ifaceName, map[string]int{ 49 | "net.ipv6.conf.%s.disable_ipv6": 0, 50 | "net.ipv6.conf.%s.accept_ra": 0, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/hooks/useKeyboardLayout.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | import { useSettingsStore } from "@/hooks/stores"; 4 | import { keyboards } from "@/keyboardLayouts"; 5 | 6 | export default function useKeyboardLayout() { 7 | const { keyboardLayout } = useSettingsStore(); 8 | 9 | const keyboardOptions = useMemo(() => { 10 | return keyboards.map((keyboard) => { 11 | return { label: keyboard.name, value: keyboard.isoCode } 12 | }); 13 | }, []); 14 | 15 | const isoCode = useMemo(() => { 16 | // If we don't have a specific layout, default to "en-US" because that was the original layout 17 | // developed so it is a good fallback. Additionally, we replace "en_US" with "en-US" because 18 | // the original server-side code used "en_US" as the default value, but that's not the correct 19 | // ISO code for English/United State. To ensure we remain backward compatible with devices that 20 | // have not had their Keyboard Layout selected by the user, we want to treat "en_US" as if it was 21 | // "en-US" to match the ISO standard codes now used in the keyboardLayouts. 22 | console.debug("Current keyboard layout from store:", keyboardLayout); 23 | if (keyboardLayout && keyboardLayout.length > 0) 24 | return keyboardLayout.replace("en_US", "en-US"); 25 | return "en-US"; 26 | }, [keyboardLayout]); 27 | 28 | const selectedKeyboard = useMemo(() => { 29 | // fallback to original behaviour of en-US if no isoCode given or matching layout not found 30 | return keyboards.find(keyboard => keyboard.isoCode === isoCode) 31 | ?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!; 32 | }, [isoCode]); 33 | 34 | return { keyboardOptions, isoCode, selectedKeyboard }; 35 | } -------------------------------------------------------------------------------- /internal/utils/env_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/Masterminds/semver/v3" 8 | ) 9 | 10 | type nativeOptions struct { 11 | Disable bool `env:"JETKVM_NATIVE_DISABLE"` 12 | SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"` 13 | AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"` 14 | DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"` 15 | DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"` 16 | } 17 | 18 | func TestMarshalEnv(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | instance interface{} 22 | want []string 23 | wantErr bool 24 | }{ 25 | { 26 | name: "basic struct", 27 | instance: nativeOptions{ 28 | Disable: false, 29 | SystemVersion: semver.MustParse("1.1.0"), 30 | AppVersion: semver.MustParse("1111.0.0"), 31 | DisplayRotation: 1, 32 | DefaultQualityFactor: 1.0, 33 | }, 34 | want: []string{ 35 | "JETKVM_NATIVE_DISABLE=false", 36 | "JETKVM_NATIVE_SYSTEM_VERSION=1.1.0", 37 | "JETKVM_NATIVE_APP_VERSION=1111.0.0", 38 | "JETKVM_NATIVE_DISPLAY_ROTATION=1", 39 | "JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR=1", 40 | }, 41 | wantErr: false, 42 | }, 43 | } 44 | 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | got, err := MarshalEnv(tt.instance) 48 | if (err != nil) != tt.wantErr { 49 | t.Errorf("MarshalEnv() error = %v, wantErr %v", err, tt.wantErr) 50 | return 51 | } 52 | if !reflect.DeepEqual(got, tt.want) { 53 | t.Errorf("MarshalEnv() = %v, want %v", got, tt.want) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | type: 'Feature' 3 | description: 🚀 Request a new feature. 4 | labels: 5 | - 'type: feature' 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: A note for the community 10 | value: | 11 | > [!NOTE] 12 | > Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | attributes: 17 | label: Disclaimer 18 | description: | 19 | Before requesting a feature, check it does not already exist in the [documentation](https://jetkvm.com/docs) or our [roadmap](https://jetkvm.com/roadmap). 20 | You are quite welcome opening a feature request before spending time to implement it yourself. 21 | options: 22 | - label: I have read and understood the disclaimer. 23 | required: true 24 | - label: I plan to implement the feature myself. 25 | - type: dropdown 26 | attributes: 27 | label: Subsystem 28 | description: Provide the subsystem of the feature you request, you can choose multiple if you think it fits in multiple areas. 29 | options: 30 | - Hardware 31 | - Device Compatibility 32 | - Keyboard 33 | - Mouse 34 | - Power 35 | - UI: Screen 36 | - UI: Application 37 | - UI: Cloud 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: Feature description 43 | description: | 44 | Provide a description of the feature you request. 45 | validations: 46 | required: true -------------------------------------------------------------------------------- /ui/src/components/UpdateInProgressStatusCard.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from "@/cva.config"; 2 | import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; 3 | import { Button } from "@components/Button"; 4 | import { GridCard } from "@components/Card"; 5 | import LoadingSpinner from "@components/LoadingSpinner"; 6 | import { m } from "@localizations/messages.js"; 7 | 8 | export default function UpdateInProgressStatusCard() { 9 | const { navigateTo } = useDeviceUiNavigation(); 10 | 11 | return ( 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | {m.update_in_progress()} 20 |
21 |
22 |
23 | 24 | {m.updating_leave_device_on()} 25 | 26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/components/SettingsItem.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from "@/cva.config"; 2 | import LoadingSpinner from "@components/LoadingSpinner"; 3 | 4 | interface SettingsItemProps { 5 | readonly title: string; 6 | readonly description: string | React.ReactNode; 7 | readonly badge?: string; 8 | readonly badgeTheme?: keyof typeof badgeTheme; 9 | readonly className?: string; 10 | readonly loading?: boolean; 11 | readonly children?: React.ReactNode; 12 | } 13 | 14 | const badgeTheme = { 15 | info: "bg-blue-500 text-white", 16 | success: "bg-green-500 text-white", 17 | warning: "bg-yellow-500 text-white", 18 | danger: "bg-red-500 text-white", 19 | }; 20 | 21 | export function SettingsItem(props: SettingsItemProps) { 22 | const { title, description, badge, badgeTheme: badgeThemeProp = "danger", children, className, loading } = props; 23 | const badgeThemeClass = badgeTheme[badgeThemeProp]; 24 | 25 | return ( 26 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /internal/logging/pion.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/pion/logging" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type pionLogger struct { 9 | logger *zerolog.Logger 10 | } 11 | 12 | // Print all messages except trace. 13 | func (c pionLogger) Trace(msg string) { 14 | c.logger.Trace().Msg(msg) 15 | } 16 | func (c pionLogger) Tracef(format string, args ...any) { 17 | c.logger.Trace().Msgf(format, args...) 18 | } 19 | 20 | func (c pionLogger) Debug(msg string) { 21 | c.logger.Debug().Msg(msg) 22 | } 23 | func (c pionLogger) Debugf(format string, args ...any) { 24 | c.logger.Debug().Msgf(format, args...) 25 | } 26 | func (c pionLogger) Info(msg string) { 27 | c.logger.Info().Msg(msg) 28 | } 29 | func (c pionLogger) Infof(format string, args ...any) { 30 | c.logger.Info().Msgf(format, args...) 31 | } 32 | func (c pionLogger) Warn(msg string) { 33 | c.logger.Warn().Msg(msg) 34 | } 35 | func (c pionLogger) Warnf(format string, args ...any) { 36 | c.logger.Warn().Msgf(format, args...) 37 | } 38 | func (c pionLogger) Error(msg string) { 39 | c.logger.Error().Msg(msg) 40 | } 41 | func (c pionLogger) Errorf(format string, args ...any) { 42 | c.logger.Error().Msgf(format, args...) 43 | } 44 | 45 | // customLoggerFactory satisfies the interface logging.LoggerFactory 46 | // This allows us to create different loggers per subsystem. So we can 47 | // add custom behavior. 48 | type pionLoggerFactory struct{} 49 | 50 | func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { 51 | logger := rootLogger.getLogger(subsystem).With(). 52 | Str("scope", "pion"). 53 | Str("component", subsystem). 54 | Logger() 55 | 56 | return pionLogger{logger: &logger} 57 | } 58 | 59 | var defaultLoggerFactory = &pionLoggerFactory{} 60 | 61 | func GetPionDefaultLoggerFactory() logging.LoggerFactory { 62 | return defaultLoggerFactory 63 | } 64 | -------------------------------------------------------------------------------- /pkg/nmlite/utils.go: -------------------------------------------------------------------------------- 1 | package nmlite 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/jetkvm/kvm/internal/network/types" 8 | ) 9 | 10 | func lifetimeToTime(lifetime int) *time.Time { 11 | if lifetime == 0 { 12 | return nil 13 | } 14 | 15 | // Check for infinite lifetime (0xFFFFFFFF = 4294967295) 16 | // This is used for static/permanent addresses 17 | // Use uint32 to avoid int overflow on 32-bit systems 18 | const infiniteLifetime uint32 = 0xFFFFFFFF 19 | if uint32(lifetime) == infiniteLifetime || lifetime < 0 { 20 | return nil // Infinite lifetime - no expiration 21 | } 22 | 23 | // For finite lifetimes (SLAAC addresses) 24 | t := time.Now().Add(time.Duration(lifetime) * time.Second) 25 | return &t 26 | } 27 | 28 | func sortAndCompareStringSlices(a, b []string) bool { 29 | if len(a) != len(b) { 30 | return false 31 | } 32 | 33 | sort.Strings(a) 34 | sort.Strings(b) 35 | 36 | for i := range a { 37 | if a[i] != b[i] { 38 | return false 39 | } 40 | } 41 | 42 | return true 43 | } 44 | 45 | func sortIPv6AddressSlicesStable(a []types.IPv6Address) { 46 | sort.SliceStable(a, func(i, j int) bool { 47 | return a[i].Address.String() < a[j].Address.String() 48 | }) 49 | } 50 | 51 | func sortAndCompareIPv6AddressSlices(a, b []types.IPv6Address) bool { 52 | if len(a) != len(b) { 53 | return false 54 | } 55 | 56 | sortIPv6AddressSlicesStable(a) 57 | sortIPv6AddressSlicesStable(b) 58 | 59 | for i := range a { 60 | if a[i].Address.String() != b[i].Address.String() { 61 | return false 62 | } 63 | 64 | if a[i].Prefix.String() != b[i].Prefix.String() { 65 | return false 66 | } 67 | 68 | if a[i].Flags != b[i].Flags { 69 | return false 70 | } 71 | 72 | // we don't compare the lifetimes because they are not always same 73 | if a[i].Scope != b[i].Scope { 74 | return false 75 | } 76 | } 77 | return true 78 | } 79 | -------------------------------------------------------------------------------- /ui/src/components/UpdatingStatusCard.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { CheckCircleIcon } from "@heroicons/react/24/solid"; // adjust import if you use a different icon set 3 | 4 | import LoadingSpinner from "@components/LoadingSpinner"; // adjust import path if needed 5 | import { m } from "@localizations/messages.js"; 6 | 7 | export interface UpdatePart { 8 | pending: boolean; 9 | status: string; 10 | progress: number; 11 | complete: boolean; 12 | } 13 | 14 | export default function UpdatingStatusCard({ 15 | label, 16 | part, 17 | }: { 18 | label: string; 19 | part: UpdatePart; 20 | }) { 21 | return ( 22 |
23 |
24 |

{label}

25 | {part.progress < 100 ? ( 26 | 27 | ) : ( 28 | 29 | )} 30 |
31 |
39 |
43 |
44 |
45 | {part.status} 46 | {part.progress < 100 ? {`${Math.round(part.progress)}%`} : null} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /internal/utils/ssh.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | // ValidSSHKeyTypes is a list of valid SSH key types 12 | // 13 | // Please make sure that all the types in this list are supported by dropbear 14 | // https://github.com/mkj/dropbear/blob/003c5fcaabc114430d5d14142e95ffdbbd2d19b6/src/signkey.c#L37 15 | // 16 | // ssh-dss is not allowed here as it's insecure 17 | var ValidSSHKeyTypes = []string{ 18 | ssh.KeyAlgoRSA, 19 | ssh.KeyAlgoED25519, 20 | ssh.KeyAlgoECDSA256, 21 | ssh.KeyAlgoECDSA384, 22 | ssh.KeyAlgoECDSA521, 23 | ssh.KeyAlgoSKED25519, 24 | ssh.KeyAlgoSKECDSA256, 25 | } 26 | 27 | // ValidateSSHKey validates authorized_keys file content 28 | func ValidateSSHKey(sshKey string) error { 29 | // validate SSH key 30 | var ( 31 | hasValidPublicKey = false 32 | lastError = fmt.Errorf("no valid SSH key found") 33 | ) 34 | for _, key := range strings.Split(sshKey, "\n") { 35 | key = strings.TrimSpace(key) 36 | 37 | // skip empty lines and comments 38 | if key == "" || strings.HasPrefix(key, "#") { 39 | continue 40 | } 41 | 42 | parsedPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 43 | if err != nil { 44 | lastError = err 45 | continue 46 | } 47 | 48 | if parsedPublicKey == nil { 49 | continue 50 | } 51 | 52 | parsedType := parsedPublicKey.Type() 53 | textType := strings.Fields(key)[0] 54 | 55 | if parsedType != textType { 56 | lastError = fmt.Errorf("parsed SSH key type %s does not match type in text %s", parsedType, textType) 57 | continue 58 | } 59 | 60 | if !slices.Contains(ValidSSHKeyTypes, parsedType) { 61 | lastError = fmt.Errorf("invalid SSH key type: %s", parsedType) 62 | continue 63 | } 64 | 65 | hasValidPublicKey = true 66 | } 67 | 68 | if !hasValidPublicKey { 69 | return lastError 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /wol.go: -------------------------------------------------------------------------------- 1 | package kvm 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "net" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promauto" 10 | ) 11 | 12 | var ( 13 | wolPackets = promauto.NewCounter( 14 | prometheus.CounterOpts{ 15 | Name: "jetkvm_wol_sent_packets_total", 16 | Help: "Total number of Wake-on-LAN magic packets sent.", 17 | }, 18 | ) 19 | wolErrors = promauto.NewCounter( 20 | prometheus.CounterOpts{ 21 | Name: "jetkvm_wol_sent_packet_errors_total", 22 | Help: "Total number of Wake-on-LAN magic packets errors.", 23 | }, 24 | ) 25 | ) 26 | 27 | // SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address 28 | func rpcSendWOLMagicPacket(macAddress string) error { 29 | // Parse the MAC address 30 | mac, err := net.ParseMAC(macAddress) 31 | if err != nil { 32 | wolErrors.Inc() 33 | return ErrorfL(wolLogger, "invalid MAC address", err) 34 | } 35 | 36 | // Create the magic packet 37 | packet := createMagicPacket(mac) 38 | 39 | // Set up UDP connection 40 | conn, err := net.Dial("udp", "255.255.255.255:9") 41 | if err != nil { 42 | wolErrors.Inc() 43 | return ErrorfL(wolLogger, "failed to establish UDP connection", err) 44 | } 45 | defer conn.Close() 46 | 47 | // Send the packet 48 | _, err = conn.Write(packet) 49 | if err != nil { 50 | wolErrors.Inc() 51 | return ErrorfL(wolLogger, "failed to send WOL packet", err) 52 | } 53 | 54 | wolLogger.Info().Str("mac", macAddress).Msg("WOL packet sent") 55 | wolPackets.Inc() 56 | 57 | return nil 58 | } 59 | 60 | // createMagicPacket creates a Wake-on-LAN magic packet 61 | func createMagicPacket(mac net.HardwareAddr) []byte { 62 | var buf bytes.Buffer 63 | 64 | // Write 6 bytes of 0xFF 65 | buf.Write(bytes.Repeat([]byte{0xFF}, 6)) 66 | 67 | // Write the target MAC address 16 times 68 | for range 16 { 69 | _ = binary.Write(&buf, binary.BigEndian, mac) 70 | } 71 | 72 | return buf.Bytes() 73 | } 74 | -------------------------------------------------------------------------------- /ui/src/routes/devices.$id.other-session.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useOutletContext } from "react-router"; 2 | 3 | import { Button } from "@components/Button"; 4 | import { GridCard } from "@components/Card"; 5 | import LogoBlue from "@assets/logo-blue.svg"; 6 | import LogoWhite from "@assets/logo-white.svg"; 7 | import { m } from "@localizations/messages"; 8 | 9 | interface ContextType { 10 | setupPeerConnection: () => Promise; 11 | } 12 | /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ 13 | 14 | export default function OtherSessionRoute() { 15 | const outletContext = useOutletContext(); 16 | const navigate = useNavigate(); 17 | 18 | // Function to handle closing the modal 19 | const handleClose = () => { 20 | outletContext?.setupPeerConnection().then(() => navigate("..")); 21 | }; 22 | 23 | return ( 24 | 25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |

34 | {m.other_session_detected()} 35 |

36 |

37 | {m.other_session_take_over()} 38 |

39 |
40 |
42 |
43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /internal/sync/release.go: -------------------------------------------------------------------------------- 1 | //go:build !synctrace 2 | 3 | package sync 4 | 5 | import ( 6 | gosync "sync" 7 | ) 8 | 9 | // Mutex is a wrapper around the sync.Mutex 10 | type Mutex struct { 11 | mu gosync.Mutex 12 | } 13 | 14 | // Lock locks the mutex 15 | func (m *Mutex) Lock() { 16 | m.mu.Lock() 17 | } 18 | 19 | // Unlock unlocks the mutex 20 | func (m *Mutex) Unlock() { 21 | m.mu.Unlock() 22 | } 23 | 24 | // TryLock tries to lock the mutex 25 | func (m *Mutex) TryLock() bool { 26 | return m.mu.TryLock() 27 | } 28 | 29 | // RWMutex is a wrapper around the sync.RWMutex 30 | type RWMutex struct { 31 | mu gosync.RWMutex 32 | } 33 | 34 | // Lock locks the mutex 35 | func (m *RWMutex) Lock() { 36 | m.mu.Lock() 37 | } 38 | 39 | // Unlock unlocks the mutex 40 | func (m *RWMutex) Unlock() { 41 | m.mu.Unlock() 42 | } 43 | 44 | // RLock locks the mutex for reading 45 | func (m *RWMutex) RLock() { 46 | m.mu.RLock() 47 | } 48 | 49 | // RUnlock unlocks the mutex for reading 50 | func (m *RWMutex) RUnlock() { 51 | m.mu.RUnlock() 52 | } 53 | 54 | // TryRLock tries to lock the mutex for reading 55 | func (m *RWMutex) TryRLock() bool { 56 | return m.mu.TryRLock() 57 | } 58 | 59 | // TryLock tries to lock the mutex 60 | func (m *RWMutex) TryLock() bool { 61 | return m.mu.TryLock() 62 | } 63 | 64 | // WaitGroup is a wrapper around the sync.WaitGroup 65 | type WaitGroup struct { 66 | wg gosync.WaitGroup 67 | } 68 | 69 | // Add adds a function to the wait group 70 | func (w *WaitGroup) Add(delta int) { 71 | w.wg.Add(delta) 72 | } 73 | 74 | // Done decrements the wait group counter 75 | func (w *WaitGroup) Done() { 76 | w.wg.Done() 77 | } 78 | 79 | // Wait waits for the wait group to finish 80 | func (w *WaitGroup) Wait() { 81 | w.wg.Wait() 82 | } 83 | 84 | // Once is a wrapper around the sync.Once 85 | type Once struct { 86 | mu gosync.Once 87 | } 88 | 89 | // Do calls the function f if and only if Do has not been called before for this instance of Once. 90 | func (o *Once) Do(f func()) { 91 | o.mu.Do(f) 92 | } 93 | -------------------------------------------------------------------------------- /ui/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react"; 3 | 4 | import { cx } from "@/cva.config"; 5 | 6 | const Modal = React.memo(function Modal({ 7 | children, 8 | className, 9 | open, 10 | onClose, 11 | }: { 12 | children: React.ReactNode; 13 | className?: string; 14 | open: boolean; 15 | onClose: () => void; 16 | }) { 17 | return ( 18 | 19 | 23 |
26 | {/* TODO: This doesn't work well with other-sessions */} 27 |
28 | 36 |
37 |
38 |
e.stopPropagation()} 41 | > 42 | {children} 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ); 51 | }); 52 | 53 | export default Modal; 54 | -------------------------------------------------------------------------------- /internal/native/chan.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | var ( 10 | videoFrameChan chan []byte = make(chan []byte) 11 | videoStateChan chan VideoState = make(chan VideoState) 12 | logChan chan nativeLogMessage = make(chan nativeLogMessage) 13 | indevEventChan chan int = make(chan int) 14 | rpcEventChan chan string = make(chan string) 15 | ) 16 | 17 | func (n *Native) handleVideoFrameChan() { 18 | lastFrame := time.Now() 19 | for { 20 | frame := <-videoFrameChan 21 | now := time.Now() 22 | sinceLastFrame := now.Sub(lastFrame) 23 | lastFrame = now 24 | n.onVideoFrameReceived(frame, sinceLastFrame) 25 | } 26 | } 27 | 28 | func (n *Native) handleVideoStateChan() { 29 | for { 30 | state := <-videoStateChan 31 | 32 | n.onVideoStateChange(state) 33 | } 34 | } 35 | 36 | func (n *Native) handleLogChan() { 37 | for { 38 | entry := <-logChan 39 | l := n.l.With(). 40 | Str("file", entry.File). 41 | Str("func", entry.FuncName). 42 | Int("line", entry.Line). 43 | Logger() 44 | 45 | switch entry.Level { 46 | case zerolog.DebugLevel: 47 | l.Debug().Msg(entry.Message) 48 | case zerolog.InfoLevel: 49 | l.Info().Msg(entry.Message) 50 | case zerolog.WarnLevel: 51 | l.Warn().Msg(entry.Message) 52 | case zerolog.ErrorLevel: 53 | l.Error().Msg(entry.Message) 54 | case zerolog.PanicLevel: 55 | l.Panic().Msg(entry.Message) 56 | case zerolog.FatalLevel: 57 | l.Fatal().Msg(entry.Message) 58 | case zerolog.TraceLevel: 59 | l.Trace().Msg(entry.Message) 60 | case zerolog.NoLevel: 61 | l.Info().Msg(entry.Message) 62 | default: 63 | l.Info().Msg(entry.Message) 64 | } 65 | } 66 | } 67 | 68 | func (n *Native) handleIndevEventChan() { 69 | for { 70 | event := <-indevEventChan 71 | name := uiEventCodeToName(event) 72 | n.onIndevEvent(name) 73 | } 74 | } 75 | 76 | func (n *Native) handleRpcEventChan() { 77 | for { 78 | event := <-rpcEventChan 79 | n.onRpcEvent(event) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /ui/src/routes/devices.$id.settings.appearance.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | import { SelectMenuBasic } from "@components/SelectMenuBasic"; 4 | import { SettingsItem } from "@components/SettingsItem"; 5 | import { SettingsPageHeader } from "@components/SettingsPageheader"; 6 | import { m } from "@localizations/messages.js"; 7 | 8 | export default function SettingsAppearanceRoute() { 9 | const [currentTheme, setCurrentTheme] = useState(() => { 10 | return localStorage.theme || "system"; 11 | }); 12 | 13 | const handleThemeChange = useCallback((value: string) => { 14 | const root = document.documentElement; 15 | 16 | if (value === "system") { 17 | localStorage.removeItem("theme"); 18 | // Check system preference 19 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches 20 | ? "dark" 21 | : "light"; 22 | root.classList.remove("light", "dark"); 23 | root.classList.add(systemTheme); 24 | } else { 25 | localStorage.theme = value; 26 | root.classList.remove("light", "dark"); 27 | root.classList.add(value); 28 | } 29 | }, []); 30 | 31 | const themeOptions = [ 32 | { value: "system", label: m.appearance_theme_system() }, 33 | { value: "light", label: m.appearance_theme_light() }, 34 | { value: "dark", label: m.appearance_theme_dark() }, 35 | ]; 36 | 37 | return ( 38 |
39 | 43 | 44 | { 50 | setCurrentTheme(e.target.value); 51 | handleThemeChange(e.target.value); 52 | }} 53 | /> 54 | 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /pkg/nmlite/udhcpc/parser_test.go: -------------------------------------------------------------------------------- 1 | package udhcpc 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestUnmarshalDHCPCLease(t *testing.T) { 9 | lease := &Lease{} 10 | err := UnmarshalDHCPCLease(lease, ` 11 | # generated @ Mon Jan 4 19:31:53 UTC 2021 12 | # 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04 13 | # the date might be inaccurate if the clock is not set 14 | ip=192.168.0.240 15 | siaddr=192.168.0.1 16 | sname= 17 | boot_file= 18 | subnet=255.255.255.0 19 | timezone= 20 | router=192.168.0.1 21 | timesvr= 22 | namesvr= 23 | dns=172.19.53.2 24 | logsvr= 25 | cookiesvr= 26 | lprsvr= 27 | hostname= 28 | bootsize= 29 | domain= 30 | swapsvr= 31 | rootpath= 32 | ipttl= 33 | mtu= 34 | broadcast= 35 | ntpsrv=162.159.200.123 36 | wins= 37 | lease=172800 38 | dhcptype= 39 | serverid=192.168.0.1 40 | message= 41 | tftp= 42 | bootfile= 43 | `) 44 | if lease.IPAddress.String() != "192.168.0.240" { 45 | t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String()) 46 | } 47 | if lease.Netmask.String() != "255.255.255.0" { 48 | t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String()) 49 | } 50 | if len(lease.Routers) != 1 { 51 | t.Fatalf("expected 1 router, got %d", len(lease.Routers)) 52 | } 53 | if lease.Routers[0].String() != "192.168.0.1" { 54 | t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String()) 55 | } 56 | if len(lease.NTPServers) != 1 { 57 | t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers)) 58 | } 59 | if lease.NTPServers[0].String() != "162.159.200.123" { 60 | t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String()) 61 | } 62 | if len(lease.DNS) != 1 { 63 | t.Fatalf("expected 1 dns, got %d", len(lease.DNS)) 64 | } 65 | if lease.DNS[0].String() != "172.19.53.2" { 66 | t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String()) 67 | } 68 | if lease.LeaseTime != 172800*time.Second { 69 | t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime) 70 | } 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/nmlite/jetdhcpc/logging.go: -------------------------------------------------------------------------------- 1 | package jetdhcpc 2 | 3 | import ( 4 | "github.com/insomniacslk/dhcp/dhcpv4" 5 | "github.com/insomniacslk/dhcp/dhcpv4/nclient4" 6 | "github.com/insomniacslk/dhcp/dhcpv6/nclient6" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | type dhcpLogger struct { 11 | // Printfer is used for actual output of the logger 12 | nclient4.Printfer 13 | 14 | l *zerolog.Logger 15 | } 16 | 17 | // Printf prints a log message as-is via predefined Printfer 18 | func (s dhcpLogger) Printf(format string, v ...interface{}) { 19 | s.l.Info().Msgf(format, v...) 20 | } 21 | 22 | // PrintMessage prints a DHCP message in the short format via predefined Printfer 23 | func (s dhcpLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { 24 | s.l.Info().Msgf("%s: %s", prefix, message.String()) 25 | } 26 | 27 | func summaryStructured(d *dhcpv4.DHCPv4, l *zerolog.Logger) *zerolog.Logger { 28 | logger := l.With(). 29 | Str("opCode", d.OpCode.String()). 30 | Str("hwType", d.HWType.String()). 31 | Int("hopCount", int(d.HopCount)). 32 | Str("transactionID", d.TransactionID.String()). 33 | Int("numSeconds", int(d.NumSeconds)). 34 | Str("flagsString", d.FlagsToString()). 35 | Int("flags", int(d.Flags)). 36 | Str("clientIP", d.ClientIPAddr.String()). 37 | Str("yourIP", d.YourIPAddr.String()). 38 | Str("serverIP", d.ServerIPAddr.String()). 39 | Str("gatewayIP", d.GatewayIPAddr.String()). 40 | Str("clientMAC", d.ClientHWAddr.String()). 41 | Str("serverHostname", d.ServerHostName). 42 | Str("bootFileName", d.BootFileName). 43 | Str("options", d.Options.Summary(nil)). 44 | Logger() 45 | return &logger 46 | } 47 | 48 | func (c *Client) getDHCP4Logger(ifname string) nclient4.ClientOpt { 49 | logger := c.l.With(). 50 | Str("interface", ifname). 51 | Str("source", "dhcp4"). 52 | Logger() 53 | 54 | return nclient4.WithLogger(dhcpLogger{ 55 | l: &logger, 56 | }) 57 | } 58 | 59 | // TODO: nclient6 doesn't implement the WithLogger option, 60 | // we might need to open a PR to add it 61 | 62 | func (c *Client) getDHCP6Logger() nclient6.ClientOpt { 63 | return nclient6.WithSummaryLogger() 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/components/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { JSX } from "react"; 2 | import clsx from "clsx"; 3 | 4 | import FieldLabel from "@components/FieldLabel"; 5 | import { FieldError } from "@components/InputField"; 6 | import Card from "@components/Card"; 7 | import { cx } from "@/cva.config"; 8 | 9 | type TextAreaProps = JSX.IntrinsicElements["textarea"] & { 10 | error?: string | null; 11 | }; 12 | 13 | const TextArea = React.forwardRef( 14 | function TextArea(props, ref) { 15 | return ( 16 | 23 |