added autostart for windows and autostart for devices
This commit is contained in:
parent
e2b853840d
commit
486cf6d239
BIN
bin/usb-client
BIN
bin/usb-client
Binary file not shown.
Binary file not shown.
BIN
bin/usb-relay
BIN
bin/usb-relay
Binary file not shown.
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export PATH="$PATH:/usr/local/go/bin"
|
||||||
|
|
||||||
|
OUT="bin"
|
||||||
|
LDFLAGS="-s -w"
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
echo "=== Building Linux Client ==="
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-client" ./cmd/usb-client/
|
||||||
|
echo " -> $OUT/usb-client"
|
||||||
|
|
||||||
|
echo "=== Building Windows Client ==="
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-client.exe" ./cmd/usb-client/
|
||||||
|
echo " -> $OUT/usb-client.exe"
|
||||||
|
|
||||||
|
echo "=== Building Relay ==="
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-relay" ./cmd/usb-relay/
|
||||||
|
echo " -> $OUT/usb-relay"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. Binaries in $OUT/"
|
||||||
|
ls -lh "$OUT/"
|
||||||
|
|
@ -209,7 +209,7 @@ func cmdRun(mode string) {
|
||||||
}()
|
}()
|
||||||
go sm.Run()
|
go sm.Run()
|
||||||
} else {
|
} else {
|
||||||
um := client.NewUseManager(c)
|
um := client.NewUseManager(c, cfg, cfgPath)
|
||||||
webHandler.GetDevices = func() interface{} {
|
webHandler.GetDevices = func() interface{} {
|
||||||
available := um.GetAvailableDevices()
|
available := um.GetAvailableDevices()
|
||||||
attached := um.GetAttachedDevices()
|
attached := um.GetAttachedDevices()
|
||||||
|
|
@ -232,10 +232,14 @@ func cmdRun(mode string) {
|
||||||
for _, d := range attached {
|
for _, d := range attached {
|
||||||
attachList = append(attachList, map[string]interface{}{
|
attachList = append(attachList, map[string]interface{}{
|
||||||
"bus_id": d.BusID,
|
"bus_id": d.BusID,
|
||||||
|
"vendor_id": d.VendorID,
|
||||||
|
"product_id": d.ProductID,
|
||||||
|
"name": d.Name,
|
||||||
"client_id": d.ClientID,
|
"client_id": d.ClientID,
|
||||||
"client_name": d.ClientName,
|
"client_name": d.ClientName,
|
||||||
"tunnel_id": d.TunnelID,
|
"tunnel_id": d.TunnelID,
|
||||||
"vhci_port": d.VHCIPort,
|
"vhci_port": d.VHCIPort,
|
||||||
|
"auto_connect": um.IsAutoConnect(d.VendorID, d.ProductID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -247,6 +251,8 @@ func cmdRun(mode string) {
|
||||||
}
|
}
|
||||||
webHandler.AttachDevice = um.AttachDevice
|
webHandler.AttachDevice = um.AttachDevice
|
||||||
webHandler.DetachDevice = um.DetachDevice
|
webHandler.DetachDevice = um.DetachDevice
|
||||||
|
webHandler.SetAutoConnect = um.SetAutoConnect
|
||||||
|
webHandler.IsAutoConnect = um.IsAutoConnect
|
||||||
webHandler.GetStatus = func() map[string]interface{} {
|
webHandler.GetStatus = func() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"connected": true,
|
"connected": true,
|
||||||
|
|
@ -278,7 +284,7 @@ func cmdRun(mode string) {
|
||||||
sm := client.NewShareManager(c)
|
sm := client.NewShareManager(c)
|
||||||
go sm.Run()
|
go sm.Run()
|
||||||
} else {
|
} else {
|
||||||
client.NewUseManager(c)
|
client.NewUseManager(c, cfg, cfgPath)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := c.Run(); err != nil {
|
if err := c.Run(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/duffy/usb-server/internal/config"
|
||||||
"github.com/duffy/usb-server/internal/protocol"
|
"github.com/duffy/usb-server/internal/protocol"
|
||||||
"github.com/duffy/usb-server/internal/usbip"
|
"github.com/duffy/usb-server/internal/usbip"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
@ -28,6 +30,8 @@ type AttachedDevice struct {
|
||||||
// UseManager handles receiving/using remote USB devices
|
// UseManager handles receiving/using remote USB devices
|
||||||
type UseManager struct {
|
type UseManager struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
cfg *config.Config
|
||||||
|
cfgPath string
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
available map[string][]RemoteDevice // clientID -> devices
|
available map[string][]RemoteDevice // clientID -> devices
|
||||||
attached map[string]*AttachedDevice // busID@clientID -> attached info
|
attached map[string]*AttachedDevice // busID@clientID -> attached info
|
||||||
|
|
@ -44,9 +48,11 @@ type useTunnel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUseManager creates a use manager
|
// NewUseManager creates a use manager
|
||||||
func NewUseManager(client *Client) *UseManager {
|
func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManager {
|
||||||
um := &UseManager{
|
um := &UseManager{
|
||||||
client: client,
|
client: client,
|
||||||
|
cfg: cfg,
|
||||||
|
cfgPath: cfgPath,
|
||||||
available: make(map[string][]RemoteDevice),
|
available: make(map[string][]RemoteDevice),
|
||||||
attached: make(map[string]*AttachedDevice),
|
attached: make(map[string]*AttachedDevice),
|
||||||
tunnels: make(map[string]*useTunnel),
|
tunnels: make(map[string]*useTunnel),
|
||||||
|
|
@ -210,13 +216,18 @@ func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
key := busID + "@" + clientID
|
key := busID + "@" + clientID
|
||||||
|
remDev := RemoteDevice{
|
||||||
|
USBDevice: protocol.USBDevice{BusID: busID},
|
||||||
|
ClientID: clientID,
|
||||||
|
}
|
||||||
|
if devInfo != nil {
|
||||||
|
remDev = *devInfo
|
||||||
|
}
|
||||||
|
|
||||||
um.mu.Lock()
|
um.mu.Lock()
|
||||||
um.tunnels[granted.TunnelID] = tunnel
|
um.tunnels[granted.TunnelID] = tunnel
|
||||||
um.attached[key] = &AttachedDevice{
|
um.attached[key] = &AttachedDevice{
|
||||||
RemoteDevice: RemoteDevice{
|
RemoteDevice: remDev,
|
||||||
USBDevice: protocol.USBDevice{BusID: busID},
|
|
||||||
ClientID: clientID,
|
|
||||||
},
|
|
||||||
TunnelID: granted.TunnelID,
|
TunnelID: granted.TunnelID,
|
||||||
VHCIPort: vhciPort,
|
VHCIPort: vhciPort,
|
||||||
}
|
}
|
||||||
|
|
@ -273,10 +284,102 @@ func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
um.available[msg.ClientID] = remoteDevs
|
um.available[msg.ClientID] = remoteDevs
|
||||||
|
|
||||||
|
// Collect devices to auto-connect (while holding the lock to check attached map)
|
||||||
|
var toAutoConnect []RemoteDevice
|
||||||
|
for _, dev := range remoteDevs {
|
||||||
|
if dev.Status != protocol.StatusAvailable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := dev.BusID + "@" + msg.ClientID
|
||||||
|
if _, attached := um.attached[key]; attached {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if um.matchesAutoConnect(dev) {
|
||||||
|
toAutoConnect = append(toAutoConnect, dev)
|
||||||
|
}
|
||||||
|
}
|
||||||
um.mu.Unlock()
|
um.mu.Unlock()
|
||||||
|
|
||||||
log.Printf("[use] received device list from %s (%s): %d devices",
|
log.Printf("[use] received device list from %s (%s): %d devices",
|
||||||
msg.ClientName, msg.ClientID[:8], len(msg.Devices))
|
msg.ClientName, msg.ClientID[:8], len(msg.Devices))
|
||||||
|
|
||||||
|
// Auto-connect matching devices (outside lock, each in its own goroutine)
|
||||||
|
for _, dev := range toAutoConnect {
|
||||||
|
log.Printf("[use] auto-connecting %s (%s:%s) from %s",
|
||||||
|
dev.Name, dev.VendorID, dev.ProductID, msg.ClientName)
|
||||||
|
go um.AttachDevice(msg.ClientID, dev.BusID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesAutoConnect checks if a device matches any auto-connect rule.
|
||||||
|
// Must be called with um.mu held (at least RLock).
|
||||||
|
func (um *UseManager) matchesAutoConnect(dev RemoteDevice) bool {
|
||||||
|
for _, rule := range um.cfg.AutoConnect {
|
||||||
|
if rule.VendorID != "" && !strings.EqualFold(rule.VendorID, dev.VendorID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.ProductID != "" && !strings.EqualFold(rule.ProductID, dev.ProductID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.BusID != "" && rule.BusID != dev.BusID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.ClientName != "" && rule.ClientName != dev.ClientName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true // all specified fields match
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoConnect adds or removes an auto-connect rule for a VendorID:ProductID pair.
|
||||||
|
func (um *UseManager) SetAutoConnect(vendorID, productID string, enabled bool) error {
|
||||||
|
um.mu.Lock()
|
||||||
|
defer um.mu.Unlock()
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
// Check if rule already exists
|
||||||
|
for _, rule := range um.cfg.AutoConnect {
|
||||||
|
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||||
|
return nil // already exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
um.cfg.AutoConnect = append(um.cfg.AutoConnect, config.AutoConnectRule{
|
||||||
|
VendorID: vendorID,
|
||||||
|
ProductID: productID,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Remove matching rule
|
||||||
|
filtered := um.cfg.AutoConnect[:0]
|
||||||
|
for _, rule := range um.cfg.AutoConnect {
|
||||||
|
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, rule)
|
||||||
|
}
|
||||||
|
um.cfg.AutoConnect = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := um.cfg.Save(um.cfgPath); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[use] auto-connect %s:%s = %v", vendorID, productID, enabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAutoConnect checks if there is an auto-connect rule for this VendorID:ProductID.
|
||||||
|
func (um *UseManager) IsAutoConnect(vendorID, productID string) bool {
|
||||||
|
um.mu.RLock()
|
||||||
|
defer um.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, rule := range um.cfg.AutoConnect {
|
||||||
|
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,81 @@
|
||||||
|
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const taskName = "USB-Client"
|
||||||
|
|
||||||
|
// Install creates a Windows Scheduled Task that runs at system startup
|
||||||
func Install(mode, configPath string) error {
|
func Install(mode, configPath string) error {
|
||||||
return fmt.Errorf("Windows service installation not yet implemented")
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding executable: %w", err)
|
||||||
|
}
|
||||||
|
exePath, err = filepath.Abs(exePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
absConfigPath, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove previous task if it exists
|
||||||
|
exec.Command("schtasks", "/Delete", "/TN", taskName, "/F").Run()
|
||||||
|
|
||||||
|
// Create task: run at system startup, as SYSTEM, with highest privileges
|
||||||
|
taskCmd := fmt.Sprintf(`"%s" %s --config "%s" --no-gui`, exePath, mode, absConfigPath)
|
||||||
|
cmd := exec.Command("schtasks", "/Create",
|
||||||
|
"/SC", "ONSTART",
|
||||||
|
"/TN", taskName,
|
||||||
|
"/TR", taskCmd,
|
||||||
|
"/RU", "SYSTEM",
|
||||||
|
"/RL", "HIGHEST",
|
||||||
|
"/F",
|
||||||
|
)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating scheduled task: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the task immediately
|
||||||
|
startCmd := exec.Command("schtasks", "/Run", "/TN", taskName)
|
||||||
|
startOutput, err := startCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting task: %w (output: %s)", err, strings.TrimSpace(string(startOutput)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uninstall stops and removes the Scheduled Task
|
||||||
func Uninstall() error {
|
func Uninstall() error {
|
||||||
return fmt.Errorf("Windows service uninstallation not yet implemented")
|
// Stop the running task
|
||||||
|
exec.Command("schtasks", "/End", "/TN", taskName).Run()
|
||||||
|
|
||||||
|
// Delete the task
|
||||||
|
cmd := exec.Command("schtasks", "/Delete", "/TN", taskName, "/F")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("deleting scheduled task: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status returns the current status of the Scheduled Task
|
||||||
func Status() (string, error) {
|
func Status() (string, error) {
|
||||||
return "not implemented on Windows", nil
|
cmd := exec.Command("schtasks", "/Query", "/TN", taskName, "/FO", "LIST")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "Task nicht gefunden", nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ type Handler struct {
|
||||||
GetDevices func() interface{}
|
GetDevices func() interface{}
|
||||||
AttachDevice func(clientID, busID string) error
|
AttachDevice func(clientID, busID string) error
|
||||||
DetachDevice func(clientID, busID string) error
|
DetachDevice func(clientID, busID string) error
|
||||||
|
SetAutoConnect func(vendorID, productID string, enabled bool) error
|
||||||
|
IsAutoConnect func(vendorID, productID string) bool
|
||||||
InstallService func() error
|
InstallService func() error
|
||||||
UninstallService func() error
|
UninstallService func() error
|
||||||
GetStatus func() map[string]interface{}
|
GetStatus func() map[string]interface{}
|
||||||
|
|
@ -54,6 +56,7 @@ func (h *Handler) setupRoutes() {
|
||||||
h.mux.HandleFunc("/api/config", h.handleConfig)
|
h.mux.HandleFunc("/api/config", h.handleConfig)
|
||||||
h.mux.HandleFunc("/api/generate-token", h.handleGenerateToken)
|
h.mux.HandleFunc("/api/generate-token", h.handleGenerateToken)
|
||||||
h.mux.HandleFunc("/api/apply-tokens", h.handleApplyTokens)
|
h.mux.HandleFunc("/api/apply-tokens", h.handleApplyTokens)
|
||||||
|
h.mux.HandleFunc("/api/auto-connect", h.handleAutoConnect)
|
||||||
h.mux.HandleFunc("/api/service/install", h.handleServiceInstall)
|
h.mux.HandleFunc("/api/service/install", h.handleServiceInstall)
|
||||||
h.mux.HandleFunc("/api/service/uninstall", h.handleServiceUninstall)
|
h.mux.HandleFunc("/api/service/uninstall", h.handleServiceUninstall)
|
||||||
}
|
}
|
||||||
|
|
@ -252,6 +255,35 @@ func (h *Handler) handleApplyTokens(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, map[string]interface{}{"ok": true, "hash": hash})
|
writeJSON(w, map[string]interface{}{"ok": true, "hash": hash})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleAutoConnect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
VendorID string `json:"vendor_id"`
|
||||||
|
ProductID string `json:"product_id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.SetAutoConnect == nil {
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.SetAutoConnect(req.VendorID, req.ProductID, req.Enabled); err != nil {
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) handleServiceInstall(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleServiceInstall(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
http.Error(w, "Method not allowed", 405)
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
|
|
||||||
|
|
@ -94,13 +94,19 @@ function renderUseDevices(container, available, attached) {
|
||||||
html += attached.map(dev => `
|
html += attached.map(dev => `
|
||||||
<div class="device-card">
|
<div class="device-card">
|
||||||
<div class="device-info">
|
<div class="device-info">
|
||||||
<div class="device-name">${escapeHtml(dev.bus_id)}</div>
|
<div class="device-name">${escapeHtml(dev.name || dev.bus_id)}</div>
|
||||||
<div class="device-details">
|
<div class="device-details">
|
||||||
<span>Von: ${escapeHtml(dev.client_name || dev.client_id)}</span>
|
<span>Von: ${escapeHtml(dev.client_name || dev.client_id)}</span>
|
||||||
|
${dev.vendor_id ? `<span>VID:PID: ${dev.vendor_id}:${dev.product_id}</span>` : ''}
|
||||||
<span>VHCI Port: ${dev.vhci_port}</span>
|
<span>VHCI Port: ${dev.vhci_port}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-status">
|
<div class="device-status">
|
||||||
|
<label class="auto-connect-label" title="Beim Start automatisch verbinden">
|
||||||
|
<input type="checkbox" ${dev.auto_connect ? 'checked' : ''}
|
||||||
|
onchange="toggleAutoConnect('${dev.vendor_id}', '${dev.product_id}', this.checked)">
|
||||||
|
Autostart
|
||||||
|
</label>
|
||||||
<span class="badge attached">Verbunden</span>
|
<span class="badge attached">Verbunden</span>
|
||||||
<button class="btn small danger" onclick="detachDevice('${dev.client_id}', '${dev.bus_id}')">Trennen</button>
|
<button class="btn small danger" onclick="detachDevice('${dev.client_id}', '${dev.bus_id}')">Trennen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -191,6 +197,25 @@ async function detachDevice(clientId, busId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-connect toggle
|
||||||
|
async function toggleAutoConnect(vendorId, productId, enabled) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(API_BASE + '/api/auto-connect', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ vendor_id: vendorId, product_id: productId, enabled: enabled })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!data.ok) {
|
||||||
|
alert('Fehler: ' + (data.error || 'Unbekannt'));
|
||||||
|
updateDevices();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Fehler: ' + e.message);
|
||||||
|
updateDevices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -279,3 +279,18 @@ nav {
|
||||||
color: #666;
|
color: #666;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-connect-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-connect-label input[type="checkbox"] {
|
||||||
|
accent-color: #00d4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue