diff --git a/kvmapp/system/init.d/S95nanokvm b/kvmapp/system/init.d/S95nanokvm index f314261..38d2197 100755 --- a/kvmapp/system/init.d/S95nanokvm +++ b/kvmapp/system/init.d/S95nanokvm @@ -1,6 +1,78 @@ #!/bin/sh # nanokvm Rev3.1 +wait_for_exit() { + process="$1" + timeout="$2" + elapsed=0 + + while pidof "$process" >/dev/null 2>&1; do + if [ "$elapsed" -ge "$timeout" ]; then + return 1 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + return 0 +} + +stop_process() { + process="$1" + + if ! pidof "$process" >/dev/null 2>&1; then + return + fi + + killall -INT "$process" 2>/dev/null || true + if wait_for_exit "$process" 20; then + return + fi + + killall -TERM "$process" 2>/dev/null || true + if wait_for_exit "$process" 5; then + return + fi + + killall -KILL "$process" 2>/dev/null || true + wait_for_exit "$process" 2 || true +} + +stop_services() { + # The server owns VI/VPSS. Let it release MMF before stopping kvm_system. + stop_process NanoKVM-Server + stop_process kvm_system + rm -rf /tmp/kvm_system /tmp/server +} + +start_services() { + cp -r /kvmapp/kvm_system /tmp/ + if [ -f /kvmapp/kvm_new_app ]; then + touch /tmp/.nanokvm_migrating + /tmp/kvm_system/kvm_system & + + # The legacy migration invokes this init script recursively and starts + # the server itself. Wait for that single migration path to finish. + migration_wait=0 + while [ "$migration_wait" -lt 30 ]; do + if [ ! -f /kvmapp/kvm_new_app ] && pidof NanoKVM-Server >/dev/null 2>&1; then + rm -f /tmp/.nanokvm_migrating + return + fi + sleep 1 + migration_wait=$((migration_wait + 1)) + done + rm -f /tmp/.nanokvm_migrating + else + /tmp/kvm_system/kvm_system & + fi + + if ! pidof NanoKVM-Server >/dev/null 2>&1; then + rm -rf /tmp/server + cp -r /kvmapp/server /tmp/ + /tmp/server/NanoKVM-Server & + fi +} + case "$1" in start) echo -n kvm > /boot/hostname.prefix @@ -45,30 +117,20 @@ case "$1" in fi # Start services - cp -r /kvmapp/kvm_system /tmp/ - /tmp/kvm_system/kvm_system & - - cp -r /kvmapp/server /tmp/ - /tmp/server/NanoKVM-Server & + start_services ;; stop) - killall kvm_system - killall NanoKVM-Server - rm -r /tmp/kvm_system /tmp/server + stop_services echo "OK" ;; restart) - killall kvm_system - killall NanoKVM-Server - rm -r /tmp/kvm_system /tmp/server - - cp -r /kvmapp/kvm_system /tmp/ - /tmp/kvm_system/kvm_system & - - cp -r /kvmapp/server /tmp/ - /tmp/server/NanoKVM-Server & + if [ -f /tmp/.nanokvm_migrating ]; then + exit 0 + fi + stop_services + start_services sync echo "OK" @@ -78,4 +140,4 @@ case "$1" in echo "Usage: $0 {start|stop|restart}" exit 1 ;; -esac \ No newline at end of file +esac diff --git a/server/common/kvm_vision.go b/server/common/kvm_vision.go index 99d3a95..03a85b8 100644 --- a/server/common/kvm_vision.go +++ b/server/common/kvm_vision.go @@ -18,7 +18,10 @@ var ( kvmVisionOnce sync.Once ) -type KvmVision struct{} +type KvmVision struct { + mutex sync.RWMutex + closed bool +} func GetKvmVision() *KvmVision { kvmVisionOnce.Do(func() { @@ -33,6 +36,12 @@ func GetKvmVision() *KvmVision { } func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data []byte, result int) { + k.mutex.RLock() + defer k.mutex.RUnlock() + if k.closed { + return nil, -1 + } + var ( kvmData *C.uint8_t dataSize C.uint32_t @@ -57,6 +66,12 @@ func (k *KvmVision) ReadMjpeg(width uint16, height uint16, quality uint16) (data } func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data []byte, result int) { + k.mutex.RLock() + defer k.mutex.RUnlock() + if k.closed { + return nil, -1 + } + var ( kvmData *C.uint8_t dataSize C.uint32_t @@ -81,6 +96,12 @@ func (k *KvmVision) ReadH264(width uint16, height uint16, bitRate uint16) (data } func (k *KvmVision) SetHDMI(enable bool) int { + k.mutex.RLock() + defer k.mutex.RUnlock() + if k.closed { + return -1 + } + hdmiEnable := C.uint8_t(0) if enable { hdmiEnable = C.uint8_t(1) @@ -96,16 +117,35 @@ func (k *KvmVision) SetHDMI(enable bool) int { } func (k *KvmVision) SetGop(gop uint8) { + k.mutex.RLock() + defer k.mutex.RUnlock() + if k.closed { + return + } + _gop := C.uint8_t(gop) C.set_h264_gop(_gop) } func (k *KvmVision) SetFrameDetect(frame uint8) { + k.mutex.RLock() + defer k.mutex.RUnlock() + if k.closed { + return + } + _frame := C.uint8_t(frame) C.set_frame_detact(_frame) } func (k *KvmVision) Close() { + k.mutex.Lock() + defer k.mutex.Unlock() + if k.closed { + return + } + + k.closed = true C.kvmv_deinit() log.Debugf("stop kvm vision...") } diff --git a/server/proto/application.go b/server/proto/application.go index c808202..f5a58f2 100644 --- a/server/proto/application.go +++ b/server/proto/application.go @@ -12,3 +12,13 @@ type GetPreviewRsp struct { type SetPreviewReq struct { Enable bool `validate:"omitempty"` } + +type GetUpdateServerRsp struct { + Enabled bool `json:"enabled"` + URL string `json:"url"` +} + +type SetUpdateServerReq struct { + Enabled *bool `json:"enabled" form:"enabled" validate:"required"` + URL string `json:"url" form:"url"` +} diff --git a/server/router/application.go b/server/router/application.go index 9ff7ace..23dfe3a 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -17,4 +17,7 @@ func applicationRouter(r *gin.Engine) { api.GET("/application/preview", service.GetPreview) // get preview updates state api.POST("/application/preview", service.SetPreview) // set preview updates state + + api.GET("/application/update-server", service.GetUpdateServer) // get custom update server + api.POST("/application/update-server", service.SetUpdateServer) // set custom update server } diff --git a/server/service/application/update.go b/server/service/application/update.go index 065649c..90330cb 100644 --- a/server/service/application/update.go +++ b/server/service/application/update.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "time" "github.com/gin-gonic/gin" @@ -41,7 +42,7 @@ func (s *Service) Update(c *gin.Context) { // Sleep for a second before restarting the device time.Sleep(1 * time.Second) - _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() + _ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run() } func update() error { @@ -58,7 +59,7 @@ func update() error { } // download - target := fmt.Sprintf("%s/%s", CacheDir, latest.Name) + target := filepath.Join(CacheDir, latest.Name) if err := download(latest.Url, target); err != nil { log.Errorf("download app failed: %s", err) return err diff --git a/server/service/application/update_offline.go b/server/service/application/update_offline.go index f8aa803..f2aa8b6 100644 --- a/server/service/application/update_offline.go +++ b/server/service/application/update_offline.go @@ -36,7 +36,7 @@ func (s *Service) OfflineUpdate(c *gin.Context) { log.Debugf("offline update application success") time.Sleep(1 * time.Second) - _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() + _ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run() } func offlineUpdate(c *gin.Context) error { diff --git a/server/service/application/update_server.go b/server/service/application/update_server.go new file mode 100644 index 0000000..decf0e7 --- /dev/null +++ b/server/service/application/update_server.go @@ -0,0 +1,212 @@ +package application + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + + "NanoKVM-Server/proto" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const ( + UpdateServerConfigFile = "/etc/kvm/application-update.json" + maxUpdateServerURLSize = 2048 +) + +var ( + updateServerConfigMu sync.Mutex + updateServerConfigPath = UpdateServerConfigFile +) + +type UpdateServerConfig struct { + Enabled bool `json:"enabled"` + URL string `json:"url"` +} + +func defaultUpdateServerConfig() UpdateServerConfig { + return UpdateServerConfig{URL: StableURL} +} + +func (s *Service) GetUpdateServer(c *gin.Context) { + var rsp proto.Response + + cfg, err := loadUpdateServerConfig() + if err != nil { + log.Errorf("failed to load update server config: %s", err) + rsp.ErrRsp(c, -1, "failed to load update server config") + return + } + + rsp.OkRspWithData(c, &proto.GetUpdateServerRsp{ + Enabled: cfg.Enabled, + URL: cfg.URL, + }) +} + +func (s *Service) SetUpdateServer(c *gin.Context) { + var req proto.SetUpdateServerReq + var rsp proto.Response + + // Avoid the shared request logger because the URL may contain credentials. + if err := c.ShouldBind(&req); err != nil || req.Enabled == nil { + rsp.ErrRsp(c, -1, "invalid arguments") + return + } + + normalizedURL, err := normalizeUpdateServerURL(req.URL) + if err != nil { + rsp.ErrRsp(c, -2, err.Error()) + return + } + if *req.Enabled && normalizedURL == "" { + rsp.ErrRsp(c, -2, "update server URL is required") + return + } + if normalizedURL == "" { + normalizedURL = StableURL + } + + cfg := UpdateServerConfig{Enabled: *req.Enabled, URL: normalizedURL} + if err := saveUpdateServerConfig(cfg); err != nil { + log.Errorf("failed to save update server config: %s", err) + rsp.ErrRsp(c, -3, "failed to save update server config") + return + } + + rsp.OkRspWithData(c, &proto.GetUpdateServerRsp{ + Enabled: cfg.Enabled, + URL: cfg.URL, + }) +} + +func resolveUpdateBaseURL() (string, error) { + cfg, err := loadUpdateServerConfig() + if err != nil { + return "", err + } + if cfg.Enabled { + return cfg.URL, nil + } + if isPreviewEnabled() { + return PreviewURL, nil + } + return StableURL, nil +} + +func normalizeUpdateServerURL(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return "", nil + } + if len(raw) > maxUpdateServerURLSize { + return "", errors.New("update server URL is too long") + } + + parsed, err := url.Parse(raw) + if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { + return "", errors.New("invalid update server URL") + } + if parsed.RawQuery != "" || parsed.Fragment != "" { + return "", errors.New("update server URL must not contain a query or fragment") + } + if strings.HasSuffix(strings.TrimRight(parsed.Path, "/"), "/latest.json") { + return "", errors.New("enter the update server directory, not latest.json") + } + + parsed.Path = strings.TrimRight(parsed.Path, "/") + return parsed.String(), nil +} + +func loadUpdateServerConfig() (UpdateServerConfig, error) { + updateServerConfigMu.Lock() + defer updateServerConfigMu.Unlock() + + return loadUpdateServerConfigFromPath(updateServerConfigPath) +} + +func loadUpdateServerConfigFromPath(path string) (UpdateServerConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return defaultUpdateServerConfig(), nil + } + return UpdateServerConfig{}, err + } + + var cfg UpdateServerConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return UpdateServerConfig{}, fmt.Errorf("decode update server config: %w", err) + } + + normalizedURL, err := normalizeUpdateServerURL(cfg.URL) + if err != nil { + return UpdateServerConfig{}, err + } + if normalizedURL == "" { + normalizedURL = StableURL + } + cfg.URL = normalizedURL + return cfg, nil +} + +func saveUpdateServerConfig(cfg UpdateServerConfig) error { + updateServerConfigMu.Lock() + defer updateServerConfigMu.Unlock() + + return saveUpdateServerConfigToPath(updateServerConfigPath, cfg) +} + +func saveUpdateServerConfigToPath(path string, cfg UpdateServerConfig) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create update server config directory: %w", err) + } + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("encode update server config: %w", err) + } + data = append(data, '\n') + + tmp, err := os.CreateTemp(dir, ".application-update.json.*") + if err != nil { + return fmt.Errorf("create temporary update server config: %w", err) + } + tmpPath := tmp.Name() + defer func() { _ = os.Remove(tmpPath) }() + + if err := tmp.Chmod(0o600); err != nil { + _ = tmp.Close() + return fmt.Errorf("set update server config permissions: %w", err) + } + if _, err := tmp.Write(data); err != nil { + _ = tmp.Close() + return fmt.Errorf("write update server config: %w", err) + } + if err := tmp.Sync(); err != nil { + _ = tmp.Close() + return fmt.Errorf("sync update server config: %w", err) + } + if err := tmp.Close(); err != nil { + return fmt.Errorf("close update server config: %w", err) + } + if err := os.Rename(tmpPath, path); err != nil { + return fmt.Errorf("replace update server config: %w", err) + } + + directory, err := os.Open(dir) + if err == nil { + _ = directory.Sync() + _ = directory.Close() + } + + return nil +} diff --git a/server/service/application/version.go b/server/service/application/version.go index 0efdd99..b0ddb1b 100644 --- a/server/service/application/version.go +++ b/server/service/application/version.go @@ -1,11 +1,15 @@ package application import ( + "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net/http" + "net/url" "os" + "regexp" "strings" "time" @@ -19,10 +23,20 @@ type Latest struct { Version string `json:"version"` Name string `json:"name"` Sha512 string `json:"sha512"` - Size uint `json:"size"` - Url string `json:"url"` + Size uint64 `json:"size"` + Url string `json:"-"` } +const ( + maxLatestJSONSize = 64 * 1024 +) + +var ( + latestClient = &http.Client{Timeout: 15 * time.Second} + packageNamePattern = regexp.MustCompile(`^nanokvm_[0-9]+\.[0-9]+\.[0-9]+\.tar\.gz$`) + versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`) +) + func (s *Service) GetVersion(c *gin.Context) { var rsp proto.Response @@ -39,9 +53,12 @@ func (s *Service) GetVersion(c *gin.Context) { // latest version latestVersion := "" latest, err := getLatest() - if err == nil && latest != nil { - latestVersion = latest.Version + if err != nil { + log.Errorf("failed to get latest version: %s", err) + rsp.ErrRsp(c, -1, "failed to query latest version") + return } + latestVersion = latest.Version rsp.OkRspWithData(c, &proto.GetVersionRsp{ Current: currentVersion, @@ -50,23 +67,33 @@ func (s *Service) GetVersion(c *gin.Context) { } func getLatest() (*Latest, error) { - baseURL := StableURL - if isPreviewEnabled() { - baseURL = PreviewURL + baseURL, err := resolveUpdateBaseURL() + if err != nil { + return nil, err } - url := fmt.Sprintf("%s/latest.json?now=%d", baseURL, time.Now().Unix()) - - resp, err := http.Get(url) + manifestURL, err := joinUpdateURL(baseURL, "latest.json") if err != nil { - log.Debugf("failed to request version: %v", err) return nil, err } + parsedManifestURL, err := url.Parse(manifestURL) + if err != nil { + return nil, err + } + query := parsedManifestURL.Query() + query.Set("now", fmt.Sprintf("%d", time.Now().Unix())) + parsedManifestURL.RawQuery = query.Encode() + + resp, err := latestClient.Get(parsedManifestURL.String()) + if err != nil { + log.Debugf("failed to request version from %s", parsedManifestURL.Redacted()) + return nil, errors.New("update server is inaccessible") + } defer func() { _ = resp.Body.Close() }() - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxLatestJSONSize+1)) if err != nil { log.Errorf("failed to read response: %v", err) return nil, err @@ -76,15 +103,49 @@ func getLatest() (*Latest, error) { log.Errorf("server responded with status code: %d", resp.StatusCode) return nil, fmt.Errorf("status code %d", resp.StatusCode) } + if len(body) > maxLatestJSONSize { + return nil, fmt.Errorf("latest manifest exceeds %d bytes", maxLatestJSONSize) + } var latest Latest if err := json.Unmarshal(body, &latest); err != nil { log.Errorf("failed to unmarshal response: %s", err) return nil, err } + if err := validateLatest(&latest); err != nil { + return nil, err + } - latest.Url = fmt.Sprintf("%s/%s", baseURL, latest.Name) + latest.Url, err = joinUpdateURL(baseURL, latest.Name) + if err != nil { + return nil, err + } log.Debugf("get application latest version: %s", latest.Version) return &latest, nil } + +func joinUpdateURL(baseURL string, element string) (string, error) { + joined, err := url.JoinPath(baseURL, element) + if err != nil { + return "", fmt.Errorf("join update URL: %w", err) + } + return joined, nil +} + +func validateLatest(latest *Latest) error { + if !versionPattern.MatchString(latest.Version) { + return errors.New("invalid latest version") + } + if !packageNamePattern.MatchString(latest.Name) { + return errors.New("invalid update package name") + } + digest, err := base64.StdEncoding.DecodeString(latest.Sha512) + if err != nil || len(digest) != 64 { + return errors.New("invalid update package sha512") + } + if latest.Size == 0 { + return errors.New("invalid update package size") + } + return nil +} diff --git a/server/utils/http.go b/server/utils/http.go index 8f77fc7..f297685 100644 --- a/server/utils/http.go +++ b/server/utils/http.go @@ -2,16 +2,22 @@ package utils import ( "errors" + "fmt" "io" "net/http" "os" "path/filepath" + "time" log "github.com/sirupsen/logrus" ) +const maxDownloadSize = int64(1024 * 1024 * 1024) + +var downloadClient = &http.Client{Timeout: 15 * time.Minute} + func Download(req *http.Request, target string) error { - log.Debugf("downloading %s to %s", req.URL.String(), target) + log.Debugf("downloading %s to %s", req.URL.Redacted(), target) err := os.MkdirAll(filepath.Dir(target), 0o755) if err != nil { log.Errorf("create dir %s err: %s", filepath.Dir(target), err) @@ -26,10 +32,10 @@ func Download(req *http.Request, target string) error { _ = out.Close() }() - resp, err := (&http.Client{}).Do(req) + resp, err := downloadClient.Do(req) if err != nil { - log.Errorf("request error: %s", err) - return err + log.Errorf("request to %s failed", req.URL.Redacted()) + return errors.New("update website is inaccessible right now") } defer func() { _ = resp.Body.Close() @@ -46,11 +52,14 @@ func Download(req *http.Request, target string) error { return errors.New("unsupported content type") } - _, err = io.Copy(out, resp.Body) + written, err := io.Copy(out, io.LimitReader(resp.Body, maxDownloadSize+1)) if err != nil { log.Errorf("download file to %s err: %s", target, err) return err } + if written > maxDownloadSize { + return fmt.Errorf("download exceeds %d bytes", maxDownloadSize) + } return nil } diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 594896f..8c7f4e3 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -1,6 +1,11 @@ import { http } from '@/lib/http.ts'; import { getBaseUrl } from '@/lib/service.ts'; +export type UpdateServerConfig = { + enabled: boolean; + url: string; +}; + // get application version export function getVersion() { return http.get('/api/application/version'); @@ -37,3 +42,13 @@ export function setPreviewUpdates(enable: boolean) { export function getPreviewUpdates() { return http.get('/api/application/preview'); } + +// get custom update server configuration +export function getUpdateServer() { + return http.get('/api/application/update-server'); +} + +// enable/disable custom update server +export function setUpdateServer(config: UpdateServerConfig) { + return http.post('/api/application/update-server', config); +} diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index 54d046b..b3e4b90 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -392,7 +392,8 @@ const ca = { hdmi: { description: 'Activa la sortida HDMI', idleTimeoutTitle: "Temps d'espera d'inactivitat de captura", - idleTimeoutDescription: "Atura la captura HDMI després de no detectar espectadors actius durant", + idleTimeoutDescription: + 'Atura la captura HDMI després de no detectar espectadors actius durant', minutes: 'min' }, autostart: { @@ -517,6 +518,22 @@ const ca = { preview: 'Versió de prova', previewDesc: 'Prova noves funcions abans que ningú', previewTip: 'Compte: aquestes versions poden tenir errors o funcions inacabades!', + customServer: { + title: 'Servidor d’actualitzacions personalitzat', + desc: 'Cerca i baixa actualitzacions en línia des d’un servidor especificat', + invalidUrl: + 'Introduïu un directori de servidor HTTP o HTTPS vàlid, sense paràmetres de consulta, fragments ni latest.json.', + loadFailed: 'No s’ha pogut carregar la configuració del servidor d’actualitzacions.', + saveFailed: 'No s’ha pogut desar la configuració del servidor d’actualitzacions.', + saved: 'S’ha desat la configuració del servidor d’actualitzacions.', + save: 'Desa', + confirmTitle: 'Voleu utilitzar un servidor d’actualitzacions personalitzat?', + confirmDesc: + 'SHA-512 només comprova que el paquet coincideixi amb el manifest proporcionat per aquest servidor. Això no demostra que el paquet sigui una versió oficial de NanoKVM. Un servidor defectuós o maliciós pot deixar el dispositiu inutilitzable, provocar la pèrdua de dades o comprometre el sistema.', + confirm: 'Utilitza’l igualment', + previewDisabled: + 'Les actualitzacions de previsualització no estan disponibles mentre hi hagi activat un servidor d’actualitzacions personalitzat.' + }, offline: { title: 'Actualitzacions fora de línia', desc: "Actualització mitjançant el paquet d'instal·lació local", @@ -570,8 +587,10 @@ const ca = { ready: "Temps d'execució a punt", stopped: "El temps d'execució s'ha aturat", blockedByMCP: 'El control MCP extern està actiu', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: "Temps d'execució no disponible", configError: 'Error de configuració' }, @@ -602,7 +621,8 @@ const ca = { mcp: 'Control del dispositiu: MCP extern', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Control del dispositiu: desactivat', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Concedeix control', @@ -610,7 +630,8 @@ const ca = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Control de PicoClaw concedit', released: 'Control de PicoClaw alliberat', grantFailed: "No s'ha pogut concedir el control a PicoClaw", diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 6e43002..1864278 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -522,6 +522,22 @@ const cz = { previewDesc: 'Získejte včasný přístup k novým funkcím a vylepšením', previewTip: 'Uvědomte si prosím, že předběžné verze mohou obsahovat chyby nebo neúplné funkce!', + customServer: { + title: 'Vlastní aktualizační server', + desc: 'Vyhledávejte a stahujte online aktualizace ze zadaného serveru', + invalidUrl: + 'Zadejte platnou adresu adresáře serveru HTTP nebo HTTPS bez parametrů, fragmentu nebo souboru latest.json.', + loadFailed: 'Konfiguraci aktualizačního serveru se nepodařilo načíst.', + saveFailed: 'Konfiguraci aktualizačního serveru se nepodařilo uložit.', + saved: 'Konfigurace aktualizačního serveru byla uložena.', + save: 'Uložit', + confirmTitle: 'Použít vlastní aktualizační server?', + confirmDesc: + 'SHA-512 pouze ověřuje, že balíček odpovídá manifestu poskytnutému tímto serverem. Neprokazuje, že je balíček oficiálním vydáním NanoKVM. Vadný nebo škodlivý server může způsobit nefunkčnost zařízení, ztrátu dat nebo narušení zabezpečení systému.', + confirm: 'Přesto použít', + previewDisabled: + 'Testovací aktualizace nejsou při použití vlastního aktualizačního serveru dostupné.' + }, offline: { title: 'Offline aktualizace', desc: 'Aktualizace prostřednictvím místního instalačního balíčku', @@ -575,8 +591,10 @@ const cz = { ready: 'Běhové prostředí připraveno', stopped: 'Běhové prostředí zastaveno', blockedByMCP: 'Externí ovládání MCP je aktivní', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Běhové prostředí není k dispozici', configError: 'Chyba konfigurace' }, @@ -607,7 +625,8 @@ const cz = { mcp: 'Ovládání zařízení: externí MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Ovládání zařízení: vypnuto', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Předat ovládání', @@ -615,7 +634,8 @@ const cz = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Ovládání PicoClaw povoleno', released: 'Ovládání PicoClaw uvolněno', grantFailed: 'Nepodařilo se předat ovládání PicoClaw', diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index 49e4f14..a013deb 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -520,6 +520,22 @@ const da = { previewDesc: 'Få tidlig adgang til nye funktioner og forbedringer', previewTip: 'Vær opmærksom på, at forhåndsvisningsudgivelser kan indeholde fejl eller ufuldstændig funktionalitet!', + customServer: { + title: 'Brugerdefineret opdateringsserver', + desc: 'Søg efter og download onlineopdateringer fra en angivet server', + invalidUrl: + 'Indtast en gyldig HTTP- eller HTTPS-servermappe uden forespørgsel, fragment eller latest.json.', + loadFailed: 'Konfigurationen af opdateringsserveren kunne ikke indlæses.', + saveFailed: 'Konfigurationen af opdateringsserveren kunne ikke gemmes.', + saved: 'Konfigurationen af opdateringsserveren er gemt.', + save: 'Gem', + confirmTitle: 'Vil du bruge en brugerdefineret opdateringsserver?', + confirmDesc: + 'SHA-512 kontrollerer kun, at pakken stemmer overens med manifestet fra denne server. Det beviser ikke, at pakken er en officiel NanoKVM-udgivelse. En fejlbehæftet eller ondsindet server kan gøre enheden ubrugelig, medføre tab af data eller kompromittere systemet.', + confirm: 'Brug alligevel', + previewDisabled: + 'Forhåndsvisningsopdateringer er ikke tilgængelige, mens en brugerdefineret opdateringsserver er aktiveret.' + }, offline: { title: 'Offline opdateringer', desc: 'Opdatering via lokal installationspakke', @@ -573,8 +589,10 @@ const da = { ready: 'Runtime klar', stopped: 'Runtime stoppet', blockedByMCP: 'Ekstern MCP-styring er aktiv', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime utilgængelig', configError: 'Konfigurationsfejl' }, @@ -605,7 +623,8 @@ const da = { mcp: 'Enhedsstyring: ekstern MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Enhedsstyring: fra', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Giv styring', @@ -613,7 +632,8 @@ const da = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-styring givet', released: 'PicoClaw-styring frigivet', grantFailed: 'Kunne ikke give PicoClaw styring', diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index ee93c3c..dd940a8 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -398,7 +398,8 @@ const de = { hdmi: { description: 'HDMI/Monitor-Ausgabe aktivieren', idleTimeoutTitle: 'Zeitlimit für inaktive Aufnahme', - idleTimeoutDescription: 'HDMI-Aufnahme stoppen, wenn keine aktiven Zuschauer vorhanden sind für', + idleTimeoutDescription: + 'HDMI-Aufnahme stoppen, wenn keine aktiven Zuschauer vorhanden sind für', minutes: 'Min.' }, autostart: { @@ -526,6 +527,22 @@ const de = { previewDesc: 'Erhalten Sie vorab Zugriff auf neue Funktionen und Verbesserungen', previewTip: 'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!', + customServer: { + title: 'Benutzerdefinierter Update-Server', + desc: 'Online-Updates von einem angegebenen Server suchen und herunterladen', + invalidUrl: + 'Geben Sie ein gültiges HTTP- oder HTTPS-Serververzeichnis ohne Abfrageparameter, Fragment oder latest.json ein.', + loadFailed: 'Die Konfiguration des Update-Servers konnte nicht geladen werden.', + saveFailed: 'Die Konfiguration des Update-Servers konnte nicht gespeichert werden.', + saved: 'Die Konfiguration des Update-Servers wurde gespeichert.', + save: 'Speichern', + confirmTitle: 'Benutzerdefinierten Update-Server verwenden?', + confirmDesc: + 'SHA-512 bestätigt lediglich, dass das Paket mit dem von diesem Server bereitgestellten Manifest übereinstimmt. Es beweist nicht, dass das Paket eine offizielle NanoKVM-Version ist. Ein fehlerhafter oder bösartiger Server kann das Gerät unbrauchbar machen, Datenverlust verursachen oder das System kompromittieren.', + confirm: 'Trotzdem verwenden', + previewDisabled: + 'Vorschau-Updates sind nicht verfügbar, solange ein benutzerdefinierter Update-Server aktiviert ist.' + }, offline: { title: 'Offline Aktualisierung', desc: 'Über lokales Installationspaket aktualisieren', @@ -580,8 +597,10 @@ const de = { ready: 'Runtime bereit', stopped: 'Runtime gestoppt', blockedByMCP: 'Externe MCP-Steuerung ist aktiv', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime nicht verfügbar', configError: 'Konfigurationsfehler' }, @@ -612,7 +631,8 @@ const de = { mcp: 'Gerätesteuerung: externes MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Gerätesteuerung: aus', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Steuerung erteilen', @@ -620,7 +640,8 @@ const de = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-Steuerung erteilt', released: 'PicoClaw-Steuerung freigegeben', grantFailed: 'PicoClaw-Steuerung konnte nicht erteilt werden', diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 4d0a835..215f957 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -517,6 +517,21 @@ const en = { previewDesc: 'Get early access to new features and improvements', previewTip: 'Please be aware that preview releases may contain bugs or incomplete functionality!', + customServer: { + title: 'Custom Update Server', + desc: 'Check for and download online updates from a specified server', + invalidUrl: + 'Enter a valid HTTP or HTTPS server directory without a query, fragment, or latest.json.', + loadFailed: 'Failed to load the update server configuration.', + saveFailed: 'Failed to save the update server configuration.', + saved: 'Update server configuration saved.', + save: 'Save', + confirmTitle: 'Use a custom update server?', + confirmDesc: + 'SHA-512 only checks that the package matches the manifest supplied by this server. It does not prove that the package is an official NanoKVM release. A faulty or malicious server may make the device unusable, cause data loss, or compromise the system.', + confirm: 'Use Anyway', + previewDisabled: 'Preview Updates are unavailable while a custom update server is enabled' + }, offline: { title: 'Offline Updates', desc: 'Update through local installation package', @@ -570,8 +585,10 @@ const en = { ready: 'Runtime ready', stopped: 'Runtime stopped', blockedByMCP: 'External MCP control is active', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime unavailable', configError: 'Configuration error' }, diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 93afbbd..0325ef9 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -395,7 +395,8 @@ const es = { hdmi: { description: 'Habilitar salida HDMI/monitor', idleTimeoutTitle: 'Tiempo de espera de captura inactiva', - idleTimeoutDescription: 'Detener la captura HDMI después de no haber espectadores activos durante', + idleTimeoutDescription: + 'Detener la captura HDMI después de no haber espectadores activos durante', minutes: 'min' }, autostart: { @@ -524,6 +525,22 @@ const es = { previewDesc: 'Accede anticipadamente a nuevas funciones y mejoras', previewTip: 'Ten en cuenta que las versiones de vista previa pueden contener errores o funcionalidades incompletas', + customServer: { + title: 'Servidor de actualizaciones personalizado', + desc: 'Buscar y descargar actualizaciones en línea desde un servidor especificado', + invalidUrl: + 'Introduce un directorio de servidor HTTP o HTTPS válido, sin parámetros de consulta, fragmentos ni latest.json.', + loadFailed: 'No se pudo cargar la configuración del servidor de actualizaciones.', + saveFailed: 'No se pudo guardar la configuración del servidor de actualizaciones.', + saved: 'Se ha guardado la configuración del servidor de actualizaciones.', + save: 'Guardar', + confirmTitle: '¿Usar un servidor de actualizaciones personalizado?', + confirmDesc: + 'SHA-512 solo comprueba que el paquete coincide con el manifiesto proporcionado por este servidor. No demuestra que el paquete sea una versión oficial de NanoKVM. Un servidor defectuoso o malicioso puede inutilizar el dispositivo, provocar la pérdida de datos o comprometer el sistema.', + confirm: 'Usar de todos modos', + previewDisabled: + 'Las actualizaciones preliminares no están disponibles mientras esté activado un servidor de actualizaciones personalizado.' + }, offline: { title: 'Actualizaciones sin conexión', desc: 'Actualización a través del paquete de instalación local', @@ -578,8 +595,10 @@ const es = { ready: 'Tiempo de ejecución listo', stopped: 'Tiempo de ejecución detenido', blockedByMCP: 'El control MCP externo está activo', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Tiempo de ejecución no disponible', configError: 'Error de configuración' }, @@ -610,7 +629,8 @@ const es = { mcp: 'Control del dispositivo: MCP externo', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Control del dispositivo: desactivado', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Conceder control', @@ -618,7 +638,8 @@ const es = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Control de PicoClaw concedido', released: 'Control de PicoClaw liberado', grantFailed: 'No se pudo conceder el control de PicoClaw', @@ -700,7 +721,8 @@ const es = { enableConfirmOk: 'Iniciar PicoClaw', enableConfirmCancel: 'Cancelar', title: 'Iniciar PicoClaw', - description: 'Inicia el tiempo de ejecución para comenzar a utilizar el asistente PicoClaw.', + description: + 'Inicia el tiempo de ejecución para comenzar a utilizar el asistente PicoClaw.', switchFromMCP: 'Switch to PicoClaw and start', takeoverAndStart: 'Take over and start' } diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index 3598ba1..7179124 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -397,7 +397,8 @@ const fr = { hdmi: { description: 'Activer HDMI/sortie moniteur', idleTimeoutTitle: "Délai d'inactivité de la capture", - idleTimeoutDescription: "Arrêter la capture HDMI lorsqu'il n'y a aucun spectateur actif pendant", + idleTimeoutDescription: + "Arrêter la capture HDMI lorsqu'il n'y a aucun spectateur actif pendant", minutes: 'min' }, autostart: { @@ -526,6 +527,22 @@ const fr = { "Bénéficiez d'un accès anticipé aux nouvelles fonctionnalités et améliorations", previewTip: 'Veuillez noter que les versions préliminaires peuvent contenir des bugs ou des fonctionnalités incomplètes!', + customServer: { + title: 'Serveur de mise à jour personnalisé', + desc: 'Rechercher et télécharger les mises à jour en ligne depuis un serveur spécifié', + invalidUrl: + 'Saisissez un répertoire de serveur HTTP ou HTTPS valide, sans paramètres de requête, fragment ni latest.json.', + loadFailed: 'Impossible de charger la configuration du serveur de mise à jour.', + saveFailed: 'Impossible d’enregistrer la configuration du serveur de mise à jour.', + saved: 'Configuration du serveur de mise à jour enregistrée.', + save: 'Enregistrer', + confirmTitle: 'Utiliser un serveur de mise à jour personnalisé ?', + confirmDesc: + 'SHA-512 vérifie uniquement que le paquet correspond au manifeste fourni par ce serveur. Cela ne prouve pas que le paquet est une version officielle de NanoKVM. Un serveur défectueux ou malveillant peut rendre l’appareil inutilisable, entraîner une perte de données ou compromettre le système.', + confirm: 'Utiliser quand même', + previewDisabled: + 'Les mises à jour en préversion ne sont pas disponibles lorsqu’un serveur de mise à jour personnalisé est activé.' + }, offline: { title: 'Mises à jour hors ligne', desc: "Mise à jour via le package d'installation local", @@ -580,8 +597,10 @@ const fr = { ready: 'Runtime prêt', stopped: 'Runtime arrêté', blockedByMCP: 'Le contrôle MCP externe est actif', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime indisponible', configError: 'Erreur de configuration' }, @@ -612,7 +631,8 @@ const fr = { mcp: "Contrôle de l'appareil : MCP externe", mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: "Contrôle de l'appareil : désactivé", - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Accorder le contrôle', @@ -620,7 +640,8 @@ const fr = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Contrôle PicoClaw accordé', released: 'Contrôle PicoClaw libéré', grantFailed: "Échec de l'octroi du contrôle PicoClaw", diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index e0b51ba..bc0679b 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -524,6 +524,22 @@ const hu = { previewDesc: 'Korai hozzáférést kap az új funkciókhoz és fejlesztésekhez', previewTip: 'Kérjük, vegye figyelembe, hogy az előzetes verziók hibákat vagy hiányos funkciókat tartalmazhatnak!', + customServer: { + title: 'Egyéni frissítési kiszolgáló', + desc: 'Online frissítések keresése és letöltése a megadott kiszolgálóról', + invalidUrl: + 'Adjon meg egy érvényes HTTP- vagy HTTPS-kiszolgálókönyvtárat lekérdezés, töredékazonosító és latest.json nélkül.', + loadFailed: 'Nem sikerült betölteni a frissítési kiszolgáló beállításait.', + saveFailed: 'Nem sikerült menteni a frissítési kiszolgáló beállításait.', + saved: 'A frissítési kiszolgáló beállításai mentve.', + save: 'Mentés', + confirmTitle: 'Egyéni frissítési kiszolgálót használ?', + confirmDesc: + 'Az SHA-512 csak azt ellenőrzi, hogy a csomag megfelel-e a kiszolgáló által biztosított jegyzéknek. Nem igazolja, hogy a csomag hivatalos NanoKVM-kiadás. Egy hibás vagy rosszindulatú kiszolgáló használhatatlanná teheti az eszközt, adatvesztést okozhat, vagy veszélyeztetheti a rendszert.', + confirm: 'Használat mindenképpen', + previewDisabled: + 'Az előzetes frissítések nem érhetők el, amíg egyéni frissítési kiszolgáló van engedélyezve.' + }, offline: { title: 'Offline frissítések', desc: 'Frissítés helyi telepítőcsomaggal', @@ -577,8 +593,10 @@ const hu = { ready: 'Runtime kész', stopped: 'Runtime leállt', blockedByMCP: 'A külső MCP-vezérlés aktív', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime nem érhető el', configError: 'Konfigurációs hiba' }, @@ -609,7 +627,8 @@ const hu = { mcp: 'Eszközvezérlés: külső MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Eszközvezérlés: kikapcsolva', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Vezérlés átadása', @@ -617,7 +636,8 @@ const hu = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-vezérlés megadva', released: 'PicoClaw-vezérlés feloldva', grantFailed: 'Nem sikerült megadni a PicoClaw-vezérlést', diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index d8cd105..46fa573 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -521,6 +521,22 @@ const id = { previewDesc: 'Dapatkan akses awal ke fitur dan peningkatan baru', previewTip: 'Perlu diketahui bahwa rilis pratinjau mungkin mengandung bug atau fungsi yang tidak lengkap!', + customServer: { + title: 'Server Pembaruan Kustom', + desc: 'Periksa dan unduh pembaruan daring dari server yang ditentukan', + invalidUrl: + 'Masukkan direktori server HTTP atau HTTPS yang valid tanpa kueri, fragmen, atau latest.json.', + loadFailed: 'Gagal memuat konfigurasi server pembaruan.', + saveFailed: 'Gagal menyimpan konfigurasi server pembaruan.', + saved: 'Konfigurasi server pembaruan telah disimpan.', + save: 'Simpan', + confirmTitle: 'Gunakan server pembaruan kustom?', + confirmDesc: + 'SHA-512 hanya memeriksa bahwa paket cocok dengan manifes yang disediakan oleh server ini. Pemeriksaan ini tidak membuktikan bahwa paket tersebut merupakan rilis resmi NanoKVM. Server yang bermasalah atau berbahaya dapat membuat perangkat tidak dapat digunakan, menyebabkan kehilangan data, atau membahayakan sistem.', + confirm: 'Tetap Gunakan', + previewDisabled: + 'Pembaruan Pratinjau tidak tersedia saat server pembaruan kustom diaktifkan.' + }, offline: { title: 'Pembaruan Offline', desc: 'Perbarui melalui paket instalasi lokal', @@ -574,8 +590,10 @@ const id = { ready: 'Runtime siap', stopped: 'Runtime dihentikan', blockedByMCP: 'Kontrol MCP eksternal sedang aktif', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime tidak tersedia', configError: 'Kesalahan konfigurasi' }, @@ -606,7 +624,8 @@ const id = { mcp: 'Kontrol perangkat: MCP eksternal', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Kontrol perangkat: nonaktif', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Berikan kontrol', @@ -614,7 +633,8 @@ const id = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Kontrol PicoClaw diberikan', released: 'Kontrol PicoClaw dilepaskan', grantFailed: 'Gagal memberikan kontrol PicoClaw', diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 67e4956..0ca9388 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -396,7 +396,8 @@ const it = { hdmi: { description: 'Abilita HDMI/monitora uscita', idleTimeoutTitle: 'Timeout cattura inattiva', - idleTimeoutDescription: 'Interrompi la cattura HDMI dopo che non ci sono visualizzatori attivi per', + idleTimeoutDescription: + 'Interrompi la cattura HDMI dopo che non ci sono visualizzatori attivi per', minutes: 'min' }, autostart: { @@ -525,6 +526,22 @@ const it = { previewDesc: "Ottieni l'accesso anticipato a nuove funzionalità e miglioramenti", previewTip: 'Tieni presente che le versioni di anteprima possono contenere bug o funzionalità incomplete!', + customServer: { + title: 'Server di aggiornamento personalizzato', + desc: 'Cerca e scarica gli aggiornamenti online da un server specificato', + invalidUrl: + 'Inserisci una directory server HTTP o HTTPS valida, senza parametri di query, frammenti o latest.json.', + loadFailed: 'Impossibile caricare la configurazione del server di aggiornamento.', + saveFailed: 'Impossibile salvare la configurazione del server di aggiornamento.', + saved: 'Configurazione del server di aggiornamento salvata.', + save: 'Salva', + confirmTitle: 'Utilizzare un server di aggiornamento personalizzato?', + confirmDesc: + 'SHA-512 verifica soltanto che il pacchetto corrisponda al manifesto fornito da questo server. Non garantisce che il pacchetto sia una versione ufficiale di NanoKVM. Un server difettoso o dannoso può rendere inutilizzabile il dispositivo, causare la perdita di dati o compromettere il sistema.', + confirm: 'Utilizza comunque', + previewDisabled: + 'Gli aggiornamenti in anteprima non sono disponibili quando è attivo un server di aggiornamento personalizzato.' + }, offline: { title: 'Aggiornamenti offline', desc: 'Aggiornamento tramite pacchetto di installazione locale', @@ -578,8 +595,10 @@ const it = { ready: 'Runtime pronto', stopped: 'Runtime interrotto', blockedByMCP: 'Il controllo MCP esterno è attivo', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime non disponibile', configError: 'Errore di configurazione' }, @@ -610,7 +629,8 @@ const it = { mcp: 'Controllo dispositivo: MCP esterno', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Controllo dispositivo: disattivato', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Concedi controllo', @@ -618,7 +638,8 @@ const it = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Controllo PicoClaw concesso', released: 'Controllo PicoClaw rilasciato', grantFailed: 'Impossibile concedere il controllo PicoClaw', diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index e622d35..2f35f9b 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -395,7 +395,8 @@ const ja = { hdmi: { description: 'HDMI/モニター 出力機能を有効にする', idleTimeoutTitle: 'キャプチャのアイドルタイムアウト', - idleTimeoutDescription: 'アクティブな閲覧者がいない状態が次の時間続いたら HDMI キャプチャを停止', + idleTimeoutDescription: + 'アクティブな閲覧者がいない状態が次の時間続いたら HDMI キャプチャを停止', minutes: '分' }, autostart: { @@ -523,6 +524,22 @@ const ja = { previewDesc: '新機能や改善をいち早く体験する', previewTip: 'プレビューアップデートには不安定な部分や不完全な機能が含まれる場合があります!', + customServer: { + title: 'カスタム更新サーバー', + desc: '指定したサーバーでオンラインアップデートを確認し、ダウンロードします', + invalidUrl: + 'クエリ、フラグメント、latest.json を含まない、有効な HTTP または HTTPS のサーバーディレクトリを入力してください。', + loadFailed: '更新サーバーの設定を読み込めませんでした。', + saveFailed: '更新サーバーの設定を保存できませんでした。', + saved: '更新サーバーの設定を保存しました。', + save: '保存', + confirmTitle: 'カスタム更新サーバーを使用しますか?', + confirmDesc: + 'SHA-512 で確認できるのは、パッケージがこのサーバーから提供されたマニフェストと一致することだけです。そのパッケージが NanoKVM の公式リリースであることは保証されません。不具合のあるサーバーや悪意のあるサーバーを使用すると、デバイスが使用不能になったり、データが失われたり、システムが侵害されたりする可能性があります。', + confirm: 'そのまま使用', + previewDisabled: + 'カスタム更新サーバーが有効な間は、プレビュー版アップデートを利用できません。' + }, offline: { title: 'オフラインアップデート', desc: 'ローカルインストールパッケージでアップデートする', @@ -577,8 +594,10 @@ const ja = { ready: 'ランタイムの準備が完了しました', stopped: 'ランタイムが停止しました', blockedByMCP: '外部 MCP 制御が有効です', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'ランタイムが使用できません', configError: '構成エラー' }, @@ -609,7 +628,8 @@ const ja = { mcp: 'デバイス制御: 外部 MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'デバイス制御: オフ', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: '制御を付与', @@ -617,7 +637,8 @@ const ja = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw 制御を付与しました', released: 'PicoClaw 制御を解除しました', grantFailed: 'PicoClaw 制御の付与に失敗しました', diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index 03219b8..384a6fb 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -389,7 +389,8 @@ const ko = { hdmi: { description: 'HDMI/모니터 출력 활성화', idleTimeoutTitle: '캡처 유휴 시간 제한', - idleTimeoutDescription: '활성 시청자가 없는 상태가 다음 시간 동안 지속되면 HDMI 캡처 중지', + idleTimeoutDescription: + '활성 시청자가 없는 상태가 다음 시간 동안 지속되면 HDMI 캡처 중지', minutes: '분' }, autostart: { @@ -514,6 +515,22 @@ const ko = { preview: '미리보기 업데이트', previewDesc: '새로운 기능과 개선 사항에 미리 접근하세요', previewTip: '미리보기 버전에는 버그나 완성되지 않은 기능이 포함될 수 있으니 주의하세요!', + customServer: { + title: '사용자 지정 업데이트 서버', + desc: '지정한 서버에서 온라인 업데이트를 확인하고 다운로드합니다', + invalidUrl: + '쿼리, 프래그먼트 또는 latest.json이 포함되지 않은 올바른 HTTP 또는 HTTPS 서버 디렉터리를 입력하세요.', + loadFailed: '업데이트 서버 구성을 불러오지 못했습니다.', + saveFailed: '업데이트 서버 구성을 저장하지 못했습니다.', + saved: '업데이트 서버 구성을 저장했습니다.', + save: '저장', + confirmTitle: '사용자 지정 업데이트 서버를 사용하시겠습니까?', + confirmDesc: + 'SHA-512는 패키지가 이 서버에서 제공한 매니페스트와 일치하는지만 확인합니다. 해당 패키지가 공식 NanoKVM 릴리스임을 보장하지는 않습니다. 결함이 있거나 악의적인 서버를 사용하면 장치를 사용할 수 없게 되거나, 데이터가 손실되거나, 시스템이 침해될 수 있습니다.', + confirm: '그래도 사용', + previewDisabled: + '사용자 지정 업데이트 서버가 활성화되어 있는 동안에는 미리 보기 업데이트를 사용할 수 없습니다.' + }, offline: { title: '오프라인 업데이트', desc: '로컬 설치 패키지를 통한 업데이트', @@ -567,8 +584,10 @@ const ko = { ready: '런타임 준비됨', stopped: '런타임 중지됨', blockedByMCP: '외부 MCP 제어가 활성화되어 있습니다', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: '런타임 사용 불가', configError: '구성 에러' }, @@ -599,7 +618,8 @@ const ko = { mcp: '장치 제어: 외부 MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: '장치 제어: 꺼짐', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: '제어 권한 부여', @@ -607,7 +627,8 @@ const ko = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw 제어 권한이 부여되었습니다', released: 'PicoClaw 제어가 해제되었습니다', grantFailed: 'PicoClaw 제어 권한 부여에 실패했습니다', diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index 2bf1de9..788b139 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -520,6 +520,22 @@ const nb = { previewDesc: 'Få tidlig tilgang til nye funksjoner og forbedringer', previewTip: 'Vær oppmerksom på at forhåndsvisningsutgivelser kan inneholde feil eller ufullstendig funksjonalitet!', + customServer: { + title: 'Egendefinert oppdateringsserver', + desc: 'Se etter og last ned nettbaserte oppdateringer fra en angitt server', + invalidUrl: + 'Angi en gyldig HTTP- eller HTTPS-servermappe uten spørring, fragment eller latest.json.', + loadFailed: 'Kunne ikke laste inn konfigurasjonen for oppdateringsserveren.', + saveFailed: 'Kunne ikke lagre konfigurasjonen for oppdateringsserveren.', + saved: 'Konfigurasjonen for oppdateringsserveren er lagret.', + save: 'Lagre', + confirmTitle: 'Vil du bruke en egendefinert oppdateringsserver?', + confirmDesc: + 'SHA-512 kontrollerer bare at pakken samsvarer med manifestet fra denne serveren. Det beviser ikke at pakken er en offisiell NanoKVM-utgivelse. En feilkonfigurert eller ondsinnet server kan gjøre enheten ubrukelig, føre til tap av data eller kompromittere systemet.', + confirm: 'Bruk likevel', + previewDisabled: + 'Forhåndsvisningsoppdateringer er ikke tilgjengelige mens en egendefinert oppdateringsserver er aktivert.' + }, offline: { title: 'Offline oppdateringer', desc: 'Oppdater gjennom lokal installasjonspakke', @@ -573,8 +589,10 @@ const nb = { ready: 'Runtime klar', stopped: 'Runtime stoppet', blockedByMCP: 'Ekstern MCP-styring er aktiv', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime utilgjengelig', configError: 'Konfigurasjonsfeil' }, @@ -605,7 +623,8 @@ const nb = { mcp: 'Enhetsstyring: ekstern MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Enhetsstyring: av', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Gi styring', @@ -613,7 +632,8 @@ const nb = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-styring gitt', released: 'PicoClaw-styring frigitt', grantFailed: 'Kunne ikke gi PicoClaw styring', diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index a23b659..fd074c7 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -396,7 +396,8 @@ const nl = { hdmi: { description: 'Schakel HDMI/monitoruitgang in', idleTimeoutTitle: 'Time-out voor inactieve opname', - idleTimeoutDescription: 'HDMI-opname stoppen nadat er gedurende deze tijd geen actieve kijkers zijn:', + idleTimeoutDescription: + 'HDMI-opname stoppen nadat er gedurende deze tijd geen actieve kijkers zijn:', minutes: 'min' }, autostart: { @@ -525,6 +526,22 @@ const nl = { previewDesc: 'Krijg vroegtijdig toegang tot nieuwe functies en verbeteringen', previewTip: 'Houd er rekening mee dat preview-releases bugs of onvolledige functionaliteit kunnen bevatten!', + customServer: { + title: 'Aangepaste updateserver', + desc: 'Online-updates zoeken en downloaden vanaf een opgegeven server', + invalidUrl: + 'Voer een geldige HTTP- of HTTPS-servermap in zonder queryparameters, fragment of latest.json.', + loadFailed: 'De configuratie van de updateserver kon niet worden geladen.', + saveFailed: 'De configuratie van de updateserver kon niet worden opgeslagen.', + saved: 'De configuratie van de updateserver is opgeslagen.', + save: 'Opslaan', + confirmTitle: 'Een aangepaste updateserver gebruiken?', + confirmDesc: + 'SHA-512 controleert alleen of het pakket overeenkomt met het manifest dat door deze server wordt verstrekt. Het bewijst niet dat het pakket een officiële NanoKVM-release is. Een defecte of kwaadwillende server kan het apparaat onbruikbaar maken, gegevensverlies veroorzaken of het systeem compromitteren.', + confirm: 'Toch gebruiken', + previewDisabled: + 'Preview-updates zijn niet beschikbaar zolang een aangepaste updateserver is ingeschakeld.' + }, offline: { title: 'Offline-updates', desc: 'Update via lokaal installatiepakket', @@ -578,8 +595,10 @@ const nl = { ready: 'Runtime gereed', stopped: 'Runtime gestopt', blockedByMCP: 'Externe MCP-bediening is actief', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime niet beschikbaar', configError: 'Configuratiefout' }, @@ -610,7 +629,8 @@ const nl = { mcp: 'Apparaatbediening: externe MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Apparaatbediening: uit', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Bediening geven', @@ -618,7 +638,8 @@ const nl = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-bediening gegeven', released: 'PicoClaw-bediening vrijgegeven', grantFailed: 'Kan PicoClaw-bediening niet geven', diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index a489d98..8701567 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -524,6 +524,22 @@ const pl = { previewDesc: 'Uzyskaj wcześniejszy dostęp do nowych funkcji i ulepszeń', previewTip: 'Należy pamiętać, że wersje poglądowe mogą zawierać błędy lub niekompletną funkcjonalność!', + customServer: { + title: 'Niestandardowy serwer aktualizacji', + desc: 'Sprawdzaj dostępność aktualizacji online i pobieraj je ze wskazanego serwera', + invalidUrl: + 'Wprowadź prawidłowy adres katalogu serwera HTTP lub HTTPS, bez zapytania, fragmentu ani pliku latest.json.', + loadFailed: 'Nie udało się wczytać konfiguracji serwera aktualizacji.', + saveFailed: 'Nie udało się zapisać konfiguracji serwera aktualizacji.', + saved: 'Konfiguracja serwera aktualizacji została zapisana.', + save: 'Zapisz', + confirmTitle: 'Użyć niestandardowego serwera aktualizacji?', + confirmDesc: + 'SHA-512 sprawdza jedynie, czy pakiet jest zgodny z manifestem dostarczonym przez ten serwer. Nie potwierdza, że pakiet jest oficjalnym wydaniem NanoKVM. Wadliwy lub złośliwy serwer może unieruchomić urządzenie, spowodować utratę danych lub naruszyć bezpieczeństwo systemu.', + confirm: 'Użyj mimo to', + previewDisabled: + 'Aktualizacje w wersji testowej są niedostępne, gdy włączony jest niestandardowy serwer aktualizacji.' + }, offline: { title: 'Aktualizacje offline', desc: 'Aktualizacja poprzez lokalny pakiet instalacyjny', @@ -577,8 +593,10 @@ const pl = { ready: 'Runtime gotowy', stopped: 'Runtime zatrzymany', blockedByMCP: 'Zewnętrzne sterowanie MCP jest aktywne', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime niedostępny', configError: 'Błąd konfiguracji' }, @@ -609,7 +627,8 @@ const pl = { mcp: 'Sterowanie urządzeniem: zewnętrzny MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Sterowanie urządzeniem: wyłączone', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Przekaż sterowanie', @@ -617,7 +636,8 @@ const pl = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Sterowanie PicoClaw przyznane', released: 'Sterowanie PicoClaw zwolnione', grantFailed: 'Nie udało się przyznać sterowania PicoClaw', diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index 78cdc7c..435ed2b 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -522,6 +522,22 @@ const pt_br = { previewDesc: 'Tenha acesso antecipado a novos recursos e melhorias', previewTip: 'Esteja ciente de que as versões de prévia podem conter bugs ou funcionalidade incompleta!', + customServer: { + title: 'Servidor de atualização personalizado', + desc: 'Verifique e baixe atualizações online de um servidor especificado', + invalidUrl: + 'Insira um diretório de servidor HTTP ou HTTPS válido, sem parâmetros de consulta, fragmentos ou latest.json.', + loadFailed: 'Não foi possível carregar a configuração do servidor de atualização.', + saveFailed: 'Não foi possível salvar a configuração do servidor de atualização.', + saved: 'Configuração do servidor de atualização salva.', + save: 'Salvar', + confirmTitle: 'Usar um servidor de atualização personalizado?', + confirmDesc: + 'O SHA-512 apenas verifica se o pacote corresponde ao manifesto fornecido por este servidor. Ele não comprova que o pacote seja uma versão oficial do NanoKVM. Um servidor com falha ou mal-intencionado pode inutilizar o dispositivo, causar perda de dados ou comprometer o sistema.', + confirm: 'Usar mesmo assim', + previewDisabled: + 'As atualizações de prévia ficam indisponíveis enquanto um servidor de atualização personalizado estiver ativado.' + }, offline: { title: 'Atualizações off-line', desc: 'Atualização através do pacote de instalação local', @@ -575,8 +591,10 @@ const pt_br = { ready: 'Runtime pronto', stopped: 'Runtime interrompido', blockedByMCP: 'O controle MCP externo está ativo', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime indisponível', configError: 'Erro de configuração' }, @@ -607,7 +625,8 @@ const pt_br = { mcp: 'Controle do dispositivo: MCP externo', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Controle do dispositivo: desativado', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Conceder controle', @@ -615,7 +634,8 @@ const pt_br = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Controle do PicoClaw concedido', released: 'Controle do PicoClaw liberado', grantFailed: 'Falha ao conceder controle ao PicoClaw', diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 88d5040..6c5ed5f 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -523,6 +523,22 @@ const ru = { previewDesc: 'Получайте ранний доступ к новым функциям и улучшениям', previewTip: 'Обратите внимание: в ранних версиях могут быть ошибки или незавершённый функционал!', + customServer: { + title: 'Пользовательский сервер обновлений', + desc: 'Проверяйте наличие обновлений и загружайте их с указанного сервера', + invalidUrl: + 'Введите корректный адрес каталога HTTP- или HTTPS-сервера без строки запроса, фрагмента или файла latest.json.', + loadFailed: 'Не удалось загрузить конфигурацию сервера обновлений.', + saveFailed: 'Не удалось сохранить конфигурацию сервера обновлений.', + saved: 'Конфигурация сервера обновлений сохранена.', + save: 'Сохранить', + confirmTitle: 'Использовать пользовательский сервер обновлений?', + confirmDesc: + 'SHA-512 проверяет только соответствие пакета манифесту, предоставленному этим сервером. Это не подтверждает, что пакет является официальным выпуском NanoKVM. Неисправный или вредоносный сервер может вывести устройство из строя, привести к потере данных или поставить под угрозу безопасность системы.', + confirm: 'Всё равно использовать', + previewDisabled: + 'Предварительные обновления недоступны, пока включён пользовательский сервер обновлений.' + }, offline: { title: 'Автономные обновления', desc: 'Обновление через локальный установочный пакет', @@ -576,8 +592,10 @@ const ru = { ready: 'Runtime готов', stopped: 'Runtime остановлен', blockedByMCP: 'Внешнее управление MCP активно', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime недоступен', configError: 'Ошибка конфигурации' }, @@ -608,7 +626,8 @@ const ru = { mcp: 'Управление устройством: внешний MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Управление устройством: выкл.', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Передать управление', @@ -616,7 +635,8 @@ const ru = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Управление PicoClaw предоставлено', released: 'Управление PicoClaw освобождено', grantFailed: 'Не удалось предоставить управление PicoClaw', diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index fa1adc1..ec84bca 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -391,7 +391,8 @@ const se = { hdmi: { description: 'Aktivera HDMI/monitorutgång', idleTimeoutTitle: 'Tidsgräns för inaktiv inspelning', - idleTimeoutDescription: 'Stoppa HDMI-inspelning efter att det inte har funnits aktiva tittare i', + idleTimeoutDescription: + 'Stoppa HDMI-inspelning efter att det inte har funnits aktiva tittare i', minutes: 'min' }, autostart: { @@ -517,6 +518,22 @@ const se = { previewDesc: 'Få tidig tillgång till nya funktioner och förbättringar', previewTip: 'Observera att förhandsversioner kan innehålla buggar eller ofullständig funktionalitet!', + customServer: { + title: 'Anpassad uppdateringsserver', + desc: 'Sök efter och hämta onlineuppdateringar från en angiven server', + invalidUrl: + 'Ange en giltig HTTP- eller HTTPS-serverkatalog utan frågesträng, fragment eller latest.json.', + loadFailed: 'Det gick inte att läsa in uppdateringsserverns konfiguration.', + saveFailed: 'Det gick inte att spara uppdateringsserverns konfiguration.', + saved: 'Uppdateringsserverns konfiguration har sparats.', + save: 'Spara', + confirmTitle: 'Vill du använda en anpassad uppdateringsserver?', + confirmDesc: + 'SHA-512 kontrollerar endast att paketet överensstämmer med manifestet från den här servern. Det bevisar inte att paketet är en officiell NanoKVM-utgåva. En felaktig eller skadlig server kan göra enheten obrukbar, orsaka dataförlust eller äventyra systemets säkerhet.', + confirm: 'Använd ändå', + previewDisabled: + 'Förhandsuppdateringar är inte tillgängliga när en anpassad uppdateringsserver är aktiverad.' + }, offline: { title: 'Offlineuppdateringar', desc: 'Uppdatera genom lokalt installationspaket', @@ -570,8 +587,10 @@ const se = { ready: 'Runtime klar', stopped: 'Runtime stoppad', blockedByMCP: 'Extern MCP-styrning är aktiv', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime inte tillgänglig', configError: 'Konfigurationsfel' }, @@ -602,7 +621,8 @@ const se = { mcp: 'Enhetsstyrning: extern MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Enhetsstyrning: av', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Ge styrning', @@ -610,7 +630,8 @@ const se = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw-styrning beviljad', released: 'PicoClaw-styrning släppt', grantFailed: 'Det gick inte att ge PicoClaw styrning', diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index 682b424..847870d 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -513,6 +513,22 @@ const th = { previewDesc: 'เข้าถึงฟีเจอร์และการปรับปรุงใหม่ก่อนใคร', previewTip: 'โปรดทราบว่าการเผยแพร่ตัวอย่างอาจมีข้อบกพร่องหรือฟังก์ชันการทำงานที่ไม่สมบูรณ์!', + customServer: { + title: 'เซิร์ฟเวอร์อัปเดตแบบกำหนดเอง', + desc: 'ตรวจสอบและดาวน์โหลดการอัปเดตออนไลน์จากเซิร์ฟเวอร์ที่ระบุ', + invalidUrl: + 'ป้อนไดเรกทอรีเซิร์ฟเวอร์ HTTP หรือ HTTPS ที่ถูกต้อง โดยไม่มีคิวรี แฟรกเมนต์ หรือ latest.json', + loadFailed: 'โหลดการกำหนดค่าเซิร์ฟเวอร์อัปเดตไม่สำเร็จ', + saveFailed: 'บันทึกการกำหนดค่าเซิร์ฟเวอร์อัปเดตไม่สำเร็จ', + saved: 'บันทึกการกำหนดค่าเซิร์ฟเวอร์อัปเดตแล้ว', + save: 'บันทึก', + confirmTitle: 'ใช้เซิร์ฟเวอร์อัปเดตแบบกำหนดเองหรือไม่', + confirmDesc: + 'SHA-512 ตรวจสอบเพียงว่าแพ็กเกจตรงกับไฟล์ Manifest ที่เซิร์ฟเวอร์นี้จัดเตรียมไว้เท่านั้น ไม่ได้ยืนยันว่าแพ็กเกจดังกล่าวเป็นรุ่นอย่างเป็นทางการของ NanoKVM เซิร์ฟเวอร์ที่มีข้อผิดพลาดหรือเป็นอันตรายอาจทำให้อุปกรณ์ใช้งานไม่ได้ ทำให้ข้อมูลสูญหาย หรือทำให้ระบบถูกบุกรุก', + confirm: 'ใช้ต่อไป', + previewDisabled: + 'การอัปเดตเวอร์ชันตัวอย่างจะไม่พร้อมใช้งานขณะที่เปิดใช้เซิร์ฟเวอร์อัปเดตแบบกำหนดเอง' + }, offline: { title: 'อัปเดตออฟไลน์', desc: 'อัปเดตผ่านแพ็คเกจการติดตั้งในเครื่อง', @@ -566,8 +582,10 @@ const th = { ready: 'Runtime พร้อมใช้งาน', stopped: 'หยุด Runtime แล้ว', blockedByMCP: 'การควบคุม MCP ภายนอกกำลังทำงาน', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime ไม่พร้อมใช้งาน', configError: 'ข้อผิดพลาดในการกำหนดค่า' }, @@ -598,7 +616,8 @@ const th = { mcp: 'การควบคุมอุปกรณ์: MCP ภายนอก', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'การควบคุมอุปกรณ์: ปิด', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'มอบการควบคุม', @@ -606,7 +625,8 @@ const th = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'มอบการควบคุม PicoClaw แล้ว', released: 'ปล่อยการควบคุม PicoClaw แล้ว', grantFailed: 'ไม่สามารถมอบการควบคุม PicoClaw ได้', diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index d0c8f01..d5e457f 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -393,7 +393,8 @@ const tr = { hdmi: { description: 'HDMI/Momitör çıktısını aktifleştir', idleTimeoutTitle: 'Etkin olmayan yakalama zaman aşımı', - idleTimeoutDescription: 'Etkin görüntüleyici olmadığında HDMI yakalamayı şu süre sonunda durdur:', + idleTimeoutDescription: + 'Etkin görüntüleyici olmadığında HDMI yakalamayı şu süre sonunda durdur:', minutes: 'dk' }, autostart: { @@ -521,6 +522,22 @@ const tr = { previewDesc: 'En son geliştirmelere ve özelliklere erken erişin', previewTip: 'Ön izleme güncellemelerinin tamamlanmamış olduğunu ve sorunlara sebep olabileceğini unutmayın!', + customServer: { + title: 'Özel güncelleme sunucusu', + desc: 'Belirtilen sunucudaki çevrimiçi güncellemeleri denetleyin ve indirin', + invalidUrl: + 'Sorgu, parça tanımlayıcısı veya latest.json içermeyen geçerli bir HTTP ya da HTTPS sunucu dizini girin.', + loadFailed: 'Güncelleme sunucusu yapılandırması yüklenemedi.', + saveFailed: 'Güncelleme sunucusu yapılandırması kaydedilemedi.', + saved: 'Güncelleme sunucusu yapılandırması kaydedildi.', + save: 'Kaydet', + confirmTitle: 'Özel bir güncelleme sunucusu kullanılsın mı?', + confirmDesc: + 'SHA-512 yalnızca paketin bu sunucunun sağladığı bildirimle eşleştiğini doğrular. Paketin resmi bir NanoKVM sürümü olduğunu kanıtlamaz. Hatalı veya kötü amaçlı bir sunucu cihazı kullanılamaz hâle getirebilir, veri kaybına yol açabilir ya da sistem güvenliğini tehlikeye atabilir.', + confirm: 'Yine de kullan', + previewDisabled: + 'Özel bir güncelleme sunucusu etkinken önizleme güncellemeleri kullanılamaz.' + }, offline: { title: 'Çevrimdışı Güncellemeler', desc: 'Yerel kurulum paketi aracılığıyla güncelleme', @@ -574,8 +591,10 @@ const tr = { ready: 'Runtime hazır', stopped: 'Runtime durduruldu', blockedByMCP: 'Harici MCP kontrolü etkin', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime mevcut değil', configError: 'Yapılandırma hatası' }, @@ -606,7 +625,8 @@ const tr = { mcp: 'Cihaz kontrolü: harici MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Cihaz kontrolü: kapalı', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Kontrol ver', @@ -614,7 +634,8 @@ const tr = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'PicoClaw kontrolü verildi', released: 'PicoClaw kontrolü bırakıldı', grantFailed: 'PicoClaw kontrolü verilemedi', diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index b638ea3..4f23446 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -521,6 +521,22 @@ const uk = { previewDesc: 'Отримайте ранній доступ до нових функцій та вдосконалень', previewTip: 'Будь ласка, майте на увазі, що випуски бета релізів можуть містити помилки або неповну функціональність!', + customServer: { + title: 'Користувацький сервер оновлень', + desc: 'Перевіряйте наявність оновлень і завантажуйте їх із зазначеного сервера', + invalidUrl: + 'Введіть коректну адресу каталогу HTTP- або HTTPS-сервера без рядка запиту, фрагмента чи файлу latest.json.', + loadFailed: 'Не вдалося завантажити конфігурацію сервера оновлень.', + saveFailed: 'Не вдалося зберегти конфігурацію сервера оновлень.', + saved: 'Конфігурацію сервера оновлень збережено.', + save: 'Зберегти', + confirmTitle: 'Використовувати користувацький сервер оновлень?', + confirmDesc: + 'SHA-512 перевіряє лише відповідність пакета маніфесту, наданому цим сервером. Це не підтверджує, що пакет є офіційним випуском NanoKVM. Несправний або зловмисний сервер може вивести пристрій із ладу, спричинити втрату даних або поставити під загрозу безпеку системи.', + confirm: 'Усе одно використовувати', + previewDisabled: + 'Попередні оновлення недоступні, доки ввімкнено користувацький сервер оновлень.' + }, offline: { title: 'Оновлення в автономному режимі', desc: 'Оновлення через локальний інсталяційний пакет', @@ -574,8 +590,10 @@ const uk = { ready: 'Runtime готовий', stopped: 'Runtime зупинено', blockedByMCP: 'Зовнішнє керування MCP активне', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime недоступний', configError: 'Помилка конфігурації' }, @@ -606,7 +624,8 @@ const uk = { mcp: 'Керування пристроєм: зовнішній MCP', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Керування пристроєм: вимкнено', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Надати керування', @@ -614,7 +633,8 @@ const uk = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Керування PicoClaw надано', released: 'Керування PicoClaw звільнено', grantFailed: 'Не вдалося надати керування PicoClaw', diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index e988efd..ae2b79d 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -391,7 +391,8 @@ const vi = { hdmi: { description: 'Kích hoạt HDMI/đầu ra màn hình', idleTimeoutTitle: 'Thời gian chờ khi không hoạt động', - idleTimeoutDescription: 'Dừng việc ghi hình HDMI sau khi không có người xem hoạt động trong', + idleTimeoutDescription: + 'Dừng việc ghi hình HDMI sau khi không có người xem hoạt động trong', minutes: 'phút' }, autostart: { @@ -518,6 +519,22 @@ const vi = { previewDesc: 'Nhận quyền truy cập sớm vào các tính năng và cải tiến mới', previewTip: 'Xin lưu ý rằng các bản phát hành xem trước có thể có lỗi hoặc chức năng chưa hoàn chỉnh!', + customServer: { + title: 'Máy chủ cập nhật tùy chỉnh', + desc: 'Kiểm tra và tải xuống các bản cập nhật trực tuyến từ máy chủ được chỉ định', + invalidUrl: + 'Nhập thư mục máy chủ HTTP hoặc HTTPS hợp lệ, không chứa truy vấn, phân đoạn hoặc latest.json.', + loadFailed: 'Không thể tải cấu hình máy chủ cập nhật.', + saveFailed: 'Không thể lưu cấu hình máy chủ cập nhật.', + saved: 'Đã lưu cấu hình máy chủ cập nhật.', + save: 'Lưu', + confirmTitle: 'Sử dụng máy chủ cập nhật tùy chỉnh?', + confirmDesc: + 'SHA-512 chỉ kiểm tra xem gói có khớp với tệp kê khai do máy chủ này cung cấp hay không. Điều này không chứng minh rằng gói đó là bản phát hành NanoKVM chính thức. Máy chủ bị lỗi hoặc độc hại có thể khiến thiết bị không thể sử dụng, gây mất dữ liệu hoặc xâm phạm hệ thống.', + confirm: 'Vẫn sử dụng', + previewDisabled: + 'Không thể sử dụng Bản cập nhật xem trước khi máy chủ cập nhật tùy chỉnh đang được bật.' + }, offline: { title: 'Cập nhật ngoại tuyến', desc: 'Cập nhật thông qua gói cài đặt cục bộ', @@ -572,8 +589,10 @@ const vi = { ready: 'Runtime đã sẵn sàng', stopped: 'Đã dừng runtime', blockedByMCP: 'Điều khiển MCP bên ngoài đang hoạt động', - readyBlockedByMCP: 'The runtime is running, but external MCP currently controls device input.', - readyWithoutControl: 'The runtime is running. Grant PicoClaw device control before reconnecting.', + readyBlockedByMCP: + 'The runtime is running, but external MCP currently controls device input.', + readyWithoutControl: + 'The runtime is running. Grant PicoClaw device control before reconnecting.', unavailable: 'Runtime không khả dụng', configError: 'Lỗi cấu hình' }, @@ -604,7 +623,8 @@ const vi = { mcp: 'Điều khiển thiết bị: MCP bên ngoài', mcpDescription: 'External MCP can write to the device. PicoClaw will not take over input.', off: 'Điều khiển thiết bị: tắt', - offDescription: 'AI will not write keyboard or mouse input. Manual control remains available.', + offDescription: + 'AI will not write keyboard or mouse input. Manual control remains available.', transitioning: 'Device control: switching', transitioningDescription: 'Device control is syncing. Please wait.', grant: 'Cấp quyền điều khiển', @@ -612,7 +632,8 @@ const vi = { releasing: 'Releasing...', switching: 'Switching...', releasingLabel: 'Device control: releasing', - releasingDescription: 'Device control is being returned. PicoClaw has stopped current writes.', + releasingDescription: + 'Device control is being returned. PicoClaw has stopped current writes.', granted: 'Đã cấp quyền điều khiển PicoClaw', released: 'Đã nhả quyền điều khiển PicoClaw', grantFailed: 'Không thể cấp quyền điều khiển PicoClaw', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index f51f31e..95c64ae 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -506,6 +506,21 @@ const zh = { preview: '预览更新', previewDesc: '率先体验即将推出的新功能和优化', previewTip: '预览版更新可能包含一些不稳定因素或未完善的功能!', + customServer: { + title: '自定义更新服务器', + desc: '从指定服务器检查并下载在线更新', + invalidUrl: + '请输入有效的 HTTP 或 HTTPS 服务器目录,不能包含查询参数、片段或 latest.json。', + loadFailed: '读取更新服务器配置失败。', + saveFailed: '保存更新服务器配置失败。', + saved: '更新服务器配置已保存。', + save: '保存', + confirmTitle: '使用自定义更新服务器?', + confirmDesc: + 'SHA-512 只能验证安装包与该服务器提供的清单一致,不能证明安装包来自 NanoKVM 官方。错误或恶意的服务器可能导致设备不可用、数据丢失或系统被接管。', + confirm: '仍然使用', + previewDisabled: '启用自定义更新服务器时,预览更新不可用' + }, offline: { title: '离线更新', desc: '通过本地安装包进行更新', diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index ccc3501..2276c3b 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -506,6 +506,21 @@ const zh_tw = { preview: '預覽更新', previewDesc: '預覽版本,搶先體驗新功能和改進', previewTip: '請注意,預覽版本可能包含一些不穩定因素或未完善的功能!', + customServer: { + title: '自訂更新伺服器', + desc: '從指定伺服器檢查並下載線上更新', + invalidUrl: + '請輸入有效的 HTTP 或 HTTPS 伺服器目錄,不可包含查詢參數、片段或 latest.json。', + loadFailed: '讀取更新伺服器設定失敗。', + saveFailed: '儲存更新伺服器設定失敗。', + saved: '更新伺服器設定已儲存。', + save: '儲存', + confirmTitle: '使用自訂更新伺服器?', + confirmDesc: + 'SHA-512 只能驗證安裝套件與該伺服器提供的清單一致,不能證明安裝套件來自 NanoKVM 官方。錯誤或惡意的伺服器可能導致裝置無法使用、資料遺失或系統遭到接管。', + confirm: '仍然使用', + previewDisabled: '啟用自訂更新伺服器時,預覽更新無法使用' + }, offline: { title: '離線更新', desc: '透過本地安裝包進行更新', diff --git a/web/src/pages/desktop/menu/settings/update/custom-server.tsx b/web/src/pages/desktop/menu/settings/update/custom-server.tsx new file mode 100644 index 0000000..c7fd967 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/update/custom-server.tsx @@ -0,0 +1,214 @@ +import { useEffect, useState } from 'react'; +import { Button, Input, message, Modal, Switch } from 'antd'; +import { TriangleAlertIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +import * as api from '@/api/application.ts'; + +const OFFICIAL_UPDATE_SERVER = 'https://cdn.sipeed.com/nanokvm'; + +interface CustomServerProps { + checkForUpdates: () => void; + onEnabledChange: (enabled: boolean) => void; +} + +export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerProps) => { + const { t } = useTranslation(); + + const [savedConfig, setSavedConfig] = useState({ + enabled: false, + url: OFFICIAL_UPDATE_SERVER + }); + const [enabled, setEnabled] = useState(false); + const [url, setUrl] = useState(OFFICIAL_UPDATE_SERVER); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + api + .getUpdateServer() + .then((rsp) => { + if (rsp.code !== 0 || !rsp.data) { + message.error(t('settings.update.customServer.loadFailed')); + return; + } + + const config = rsp.data as api.UpdateServerConfig; + setSavedConfig(config); + setEnabled(config.enabled); + setUrl(config.url || OFFICIAL_UPDATE_SERVER); + onEnabledChange(config.enabled); + }) + .catch(() => message.error(t('settings.update.customServer.loadFailed'))) + .finally(() => setIsLoading(false)); + }, [onEnabledChange, t]); + + function validateURL(value: string) { + const trimmed = value.trim(); + if (!trimmed) return t('settings.update.customServer.invalidUrl'); + + try { + const parsed = new URL(trimmed); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return t('settings.update.customServer.invalidUrl'); + } + if ( + parsed.search || + parsed.hash || + parsed.pathname.replace(/\/+$/, '').endsWith('/latest.json') + ) { + return t('settings.update.customServer.invalidUrl'); + } + } catch { + return t('settings.update.customServer.invalidUrl'); + } + + return ''; + } + + function requestEnable(nextEnabled: boolean) { + if (isLoading || isSaving) return; + + if (nextEnabled) { + setIsConfirmOpen(true); + return; + } + + if (!savedConfig.enabled) { + setEnabled(false); + setError(''); + onEnabledChange(false); + return; + } + + saveConfig({ enabled: false, url: savedConfig.url }); + } + + function requestSave() { + if (isSaving) return; + + const validationError = validateURL(url); + setError(validationError); + if (validationError) return; + + saveConfig({ enabled: true, url: url.trim() }); + } + + function confirmRisk() { + setEnabled(true); + setError(''); + onEnabledChange(true); + setIsConfirmOpen(false); + } + + function saveConfig(config: api.UpdateServerConfig) { + setIsSaving(true); + let savedSuccessfully = false; + api + .setUpdateServer(config) + .then((rsp) => { + if (rsp.code !== 0 || !rsp.data) { + message.error(rsp.msg || t('settings.update.customServer.saveFailed')); + return; + } + + const saved = rsp.data as api.UpdateServerConfig; + savedSuccessfully = true; + setSavedConfig(saved); + setEnabled(saved.enabled); + setUrl(saved.url); + setError(''); + onEnabledChange(saved.enabled); + message.success(t('settings.update.customServer.saved')); + checkForUpdates(); + }) + .catch(() => message.error(t('settings.update.customServer.saveFailed'))) + .finally(() => { + if (!savedSuccessfully) { + setEnabled(savedConfig.enabled); + onEnabledChange(savedConfig.enabled); + } + setIsSaving(false); + setIsConfirmOpen(false); + }); + } + + const hasChanges = enabled !== savedConfig.enabled || url.trim() !== savedConfig.url; + const modalTitle = ( +
+ + {t('settings.update.customServer.confirmTitle')} +
+ ); + + return ( + <> +
+
+
+
{t('settings.update.customServer.title')}
+
{t('settings.update.customServer.desc')}
+
+ +
+ + {enabled && ( +
+
+
+ { + setUrl(event.target.value); + setError(''); + }} + onPressEnter={requestSave} + /> + {error &&
{error}
} +
+ +
+
+ )} +
+ + setIsConfirmOpen(false)} + > +
+

{t('settings.update.customServer.confirmDesc')}

+
+ {url || OFFICIAL_UPDATE_SERVER} +
+
+
+ + ); +}; diff --git a/web/src/pages/desktop/menu/settings/update/index.tsx b/web/src/pages/desktop/menu/settings/update/index.tsx index fdbc116..4726fce 100644 --- a/web/src/pages/desktop/menu/settings/update/index.tsx +++ b/web/src/pages/desktop/menu/settings/update/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { LoadingOutlined, RocketOutlined, SmileOutlined } from '@ant-design/icons'; import { Button, Divider, Result, Spin } from 'antd'; import { useTranslation } from 'react-i18next'; @@ -6,6 +6,7 @@ import semver from 'semver'; import * as api from '@/api/application.ts'; +import { CustomServer } from './custom-server.tsx'; import { Offline } from './offline.tsx'; import { Preview } from './preview.tsx'; @@ -20,18 +21,21 @@ export const Update = ({ setIsLocked }: UpdateProps) => { const [currentVersion, setCurrentVersion] = useState(''); const [latestVersion, setLatestVersion] = useState(''); const [errMsg, setErrMsg] = useState(''); + const [isCustomServerEnabled, setIsCustomServerEnabled] = useState(false); + const versionRequestRef = useRef(0); useEffect(() => { checkForUpdates(); }, []); function checkForUpdates() { - if (status === 'loading') return; + const requestId = ++versionRequestRef.current; setStatus('loading'); api .getVersion() .then((rsp: any) => { + if (requestId !== versionRequestRef.current) return; if (rsp.code !== 0 || !rsp.data) { setStatus('failed'); setErrMsg(t('settings.update.queryFailed')); @@ -49,6 +53,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => { } }) .catch(() => { + if (requestId !== versionRequestRef.current) return; setStatus('failed'); setErrMsg(t('settings.update.queryFailed')); }); @@ -83,7 +88,8 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
{t('settings.update.title')}
- + + void; + disabled?: boolean; } -export const Preview = ({ checkForUpdates }: PreviewProps) => { +export const Preview = ({ checkForUpdates, disabled = false }: PreviewProps) => { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); @@ -75,10 +76,19 @@ export const Preview = ({ checkForUpdates }: PreviewProps) => { - {t('settings.update.previewDesc')} + + {disabled + ? t('settings.update.customServer.previewDisabled') + : t('settings.update.previewDesc')} + - + ); };