+
{title}
@@ -111,4 +111,4 @@ export function ConfirmDialog({
);
-}
+}
\ No newline at end of file
diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx
index 927178e..d1dab68 100644
--- a/ui/src/routes/devices.$id.settings.advanced.tsx
+++ b/ui/src/routes/devices.$id.settings.advanced.tsx
@@ -1,17 +1,16 @@
-
-import { useCallback, useState, useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
import { GridCard } from "@components/Card";
-import { SettingsPageHeader } from "../components/SettingsPageheader";
-import Checkbox from "../components/Checkbox";
-import { useJsonRpc } from "../hooks/useJsonRpc";
-import notifications from "../notifications";
-import { TextAreaWithLabel } from "../components/TextArea";
-import { isOnDevice } from "../main";
import { Button } from "../components/Button";
+import Checkbox from "../components/Checkbox";
+import { ConfirmDialog } from "../components/ConfirmDialog";
+import { SettingsPageHeader } from "../components/SettingsPageheader";
+import { TextAreaWithLabel } from "../components/TextArea";
import { useSettingsStore } from "../hooks/stores";
-
+import { useJsonRpc } from "../hooks/useJsonRpc";
+import { isOnDevice } from "../main";
+import notifications from "../notifications";
import { SettingsItem } from "./devices.$id.settings";
@@ -22,6 +21,8 @@ export default function SettingsAdvancedRoute() {
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
const [devChannel, setDevChannel] = useState(false);
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
+ const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
+ const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
const settings = useSettingsStore();
@@ -46,6 +47,11 @@ export default function SettingsAdvancedRoute() {
if ("error" in resp) return;
setDevChannel(resp.result as boolean);
});
+
+ send("getLocalLoopbackOnly", {}, resp => {
+ if ("error" in resp) return;
+ setLocalLoopbackOnly(resp.result as boolean);
+ });
}, [send, setDeveloperMode]);
const getUsbEmulationState = useCallback(() => {
@@ -110,17 +116,62 @@ export default function SettingsAdvancedRoute() {
[send, setDeveloperMode],
);
- const handleDevChannelChange = (enabled: boolean) => {
- send("setDevChannelState", { enabled }, resp => {
- if ("error" in resp) {
- notifications.error(
- `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
- );
- return;
+ const handleDevChannelChange = useCallback(
+ (enabled: boolean) => {
+ send("setDevChannelState", { enabled }, resp => {
+ if ("error" in resp) {
+ notifications.error(
+ `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
+ );
+ return;
+ }
+ setDevChannel(enabled);
+ });
+ },
+ [send, setDevChannel],
+ );
+
+ const applyLoopbackOnlyMode = useCallback(
+ (enabled: boolean) => {
+ send("setLocalLoopbackOnly", { enabled }, resp => {
+ if ("error" in resp) {
+ notifications.error(
+ `Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
+ );
+ return;
+ }
+ setLocalLoopbackOnly(enabled);
+ if (enabled) {
+ notifications.success(
+ "Loopback-only mode enabled. Restart your device to apply.",
+ );
+ } else {
+ notifications.success(
+ "Loopback-only mode disabled. Restart your device to apply.",
+ );
+ }
+ });
+ },
+ [send, setLocalLoopbackOnly],
+ );
+
+ const handleLoopbackOnlyModeChange = useCallback(
+ (enabled: boolean) => {
+ // If trying to enable loopback-only mode, show warning first
+ if (enabled) {
+ setShowLoopbackWarning(true);
+ } else {
+ // If disabling, just proceed
+ applyLoopbackOnlyMode(false);
}
- setDevChannel(enabled);
- });
- };
+ },
+ [applyLoopbackOnlyMode, setShowLoopbackWarning],
+ );
+
+ const confirmLoopbackModeEnable = useCallback(() => {
+ applyLoopbackOnlyMode(true);
+ setShowLoopbackWarning(false);
+ }, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
return (
@@ -153,7 +204,7 @@ export default function SettingsAdvancedRoute() {
{settings.developerMode && (
-
+
);
}
diff --git a/web.go b/web.go
index 766eaf5..059915c 100644
--- a/web.go
+++ b/web.go
@@ -52,8 +52,9 @@ type ChangePasswordRequest struct {
}
type LocalDevice struct {
- AuthMode *string `json:"authMode"`
- DeviceID string `json:"deviceId"`
+ AuthMode *string `json:"authMode"`
+ DeviceID string `json:"deviceId"`
+ LoopbackOnly bool `json:"loopbackOnly"`
}
type DeviceStatus struct {
@@ -532,7 +533,15 @@ func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc {
func RunWebServer() {
r := setupRouter()
- err := r.Run(":80")
+
+ // Determine the binding address based on the config
+ bindAddress := ":80" // Default to all interfaces
+ if config.LocalLoopbackOnly {
+ bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
+ }
+
+ logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).Msg("Starting web server")
+ err := r.Run(bindAddress)
if err != nil {
panic(err)
}
@@ -540,8 +549,9 @@ func RunWebServer() {
func handleDevice(c *gin.Context) {
response := LocalDevice{
- AuthMode: &config.LocalAuthMode,
- DeviceID: GetDeviceID(),
+ AuthMode: &config.LocalAuthMode,
+ DeviceID: GetDeviceID(),
+ LoopbackOnly: config.LocalLoopbackOnly,
}
c.JSON(http.StatusOK, response)