added other dsconnect settings
This commit is contained in:
parent
019c60689e
commit
09af99946e
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.
|
|
@ -184,7 +184,7 @@ func cmdRun(mode string) {
|
||||||
webHandler := web.NewHandler(cfg, cfgPath)
|
webHandler := web.NewHandler(cfg, cfgPath)
|
||||||
|
|
||||||
if mode == "share" {
|
if mode == "share" {
|
||||||
sm := client.NewShareManager(c)
|
sm := client.NewShareManager(c, cfg)
|
||||||
webHandler.GetDevices = func() interface{} {
|
webHandler.GetDevices = func() interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"mode": "share",
|
"mode": "share",
|
||||||
|
|
@ -217,14 +217,15 @@ func cmdRun(mode string) {
|
||||||
var availList []map[string]interface{}
|
var availList []map[string]interface{}
|
||||||
for _, d := range available {
|
for _, d := range available {
|
||||||
availList = append(availList, map[string]interface{}{
|
availList = append(availList, map[string]interface{}{
|
||||||
"bus_id": d.BusID,
|
"bus_id": d.BusID,
|
||||||
"vendor_id": d.VendorID,
|
"vendor_id": d.VendorID,
|
||||||
"product_id": d.ProductID,
|
"product_id": d.ProductID,
|
||||||
"name": d.Name,
|
"name": d.Name,
|
||||||
"status": d.Status,
|
"status": d.Status,
|
||||||
"speed": d.Speed,
|
"speed": d.Speed,
|
||||||
"client_id": d.ClientID,
|
"client_id": d.ClientID,
|
||||||
"client_name": d.ClientName,
|
"client_name": d.ClientName,
|
||||||
|
"allow_force_detach": um.IsForceDetachable(d.ClientID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,6 +252,7 @@ func cmdRun(mode string) {
|
||||||
}
|
}
|
||||||
webHandler.AttachDevice = um.AttachDevice
|
webHandler.AttachDevice = um.AttachDevice
|
||||||
webHandler.DetachDevice = um.DetachDevice
|
webHandler.DetachDevice = um.DetachDevice
|
||||||
|
webHandler.ForceDetachDevice = um.ForceDetachDevice
|
||||||
webHandler.SetAutoConnect = um.SetAutoConnect
|
webHandler.SetAutoConnect = um.SetAutoConnect
|
||||||
webHandler.IsAutoConnect = um.IsAutoConnect
|
webHandler.IsAutoConnect = um.IsAutoConnect
|
||||||
webHandler.GetStatus = func() map[string]interface{} {
|
webHandler.GetStatus = func() map[string]interface{} {
|
||||||
|
|
@ -281,7 +283,7 @@ func cmdRun(mode string) {
|
||||||
} else {
|
} else {
|
||||||
// No GUI mode
|
// No GUI mode
|
||||||
if mode == "share" {
|
if mode == "share" {
|
||||||
sm := client.NewShareManager(c)
|
sm := client.NewShareManager(c, cfg)
|
||||||
go sm.Run()
|
go sm.Run()
|
||||||
} else {
|
} else {
|
||||||
client.NewUseManager(c, cfg, cfgPath)
|
client.NewUseManager(c, cfg, cfgPath)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type Client struct {
|
||||||
OnClientLeft func(msg *protocol.ClientLeft)
|
OnClientLeft func(msg *protocol.ClientLeft)
|
||||||
OnRequestDevice func(targetClient, fromClient, busID, requestID string)
|
OnRequestDevice func(targetClient, fromClient, busID, requestID string)
|
||||||
OnReleaseDevice func(busID, fromClient string)
|
OnReleaseDevice func(busID, fromClient string)
|
||||||
|
OnForceRelease func(targetClient, fromClient, busID string)
|
||||||
OnTunnelData func(tunnelID string, data []byte)
|
OnTunnelData func(tunnelID string, data []byte)
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
@ -255,6 +256,18 @@ func (c *Client) handleTextMessage(data []byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case protocol.MsgForceRelease:
|
||||||
|
if c.OnForceRelease != nil {
|
||||||
|
var msg struct {
|
||||||
|
TargetClient string `json:"target_client"`
|
||||||
|
FromClient string `json:"from_client"`
|
||||||
|
BusID string `json:"bus_id"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(data, &msg) == nil {
|
||||||
|
c.OnForceRelease(msg.TargetClient, msg.FromClient, msg.BusID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case protocol.MsgReleaseDevice:
|
case protocol.MsgReleaseDevice:
|
||||||
if c.OnReleaseDevice != nil {
|
if c.OnReleaseDevice != nil {
|
||||||
var msg struct {
|
var msg struct {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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/usb"
|
"github.com/duffy/usb-server/internal/usb"
|
||||||
"github.com/duffy/usb-server/internal/usbip"
|
"github.com/duffy/usb-server/internal/usbip"
|
||||||
|
|
@ -16,6 +17,7 @@ import (
|
||||||
// ShareManager handles sharing USB devices
|
// ShareManager handles sharing USB devices
|
||||||
type ShareManager struct {
|
type ShareManager struct {
|
||||||
client *Client
|
client *Client
|
||||||
|
cfg *config.Config
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
devices []usb.Device
|
devices []usb.Device
|
||||||
active map[string]*activeShare // busID -> active share
|
active map[string]*activeShare // busID -> active share
|
||||||
|
|
@ -38,9 +40,10 @@ type shareTunnel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShareManager creates a share manager
|
// NewShareManager creates a share manager
|
||||||
func NewShareManager(client *Client) *ShareManager {
|
func NewShareManager(client *Client, cfg *config.Config) *ShareManager {
|
||||||
sm := &ShareManager{
|
sm := &ShareManager{
|
||||||
client: client,
|
client: client,
|
||||||
|
cfg: cfg,
|
||||||
active: make(map[string]*activeShare),
|
active: make(map[string]*activeShare),
|
||||||
tunnels: make(map[string]*shareTunnel),
|
tunnels: make(map[string]*shareTunnel),
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +53,7 @@ func NewShareManager(client *Client) *ShareManager {
|
||||||
client.OnReleaseDevice = sm.handleReleaseDevice
|
client.OnReleaseDevice = sm.handleReleaseDevice
|
||||||
client.OnTunnelData = sm.handleTunnelData
|
client.OnTunnelData = sm.handleTunnelData
|
||||||
client.OnClientLeft = sm.handleClientLeft
|
client.OnClientLeft = sm.handleClientLeft
|
||||||
|
client.OnForceRelease = sm.handleForceRelease
|
||||||
|
|
||||||
return sm
|
return sm
|
||||||
}
|
}
|
||||||
|
|
@ -129,10 +133,11 @@ func (sm *ShareManager) broadcastDeviceList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &protocol.DeviceList{
|
msg := &protocol.DeviceList{
|
||||||
Type: protocol.MsgDeviceList,
|
Type: protocol.MsgDeviceList,
|
||||||
ClientID: sm.client.ID(),
|
ClientID: sm.client.ID(),
|
||||||
ClientName: sm.client.Config().Name,
|
ClientName: sm.client.Config().Name,
|
||||||
Devices: protoDevices,
|
Devices: protoDevices,
|
||||||
|
AllowForceDetach: sm.cfg.AllowForceDetach,
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.client.SendJSON(msg)
|
sm.client.SendJSON(msg)
|
||||||
|
|
@ -312,6 +317,24 @@ func (sm *ShareManager) handleReleaseDevice(busID, fromClient string) {
|
||||||
sm.broadcastDeviceList()
|
sm.broadcastDeviceList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sm *ShareManager) handleForceRelease(targetClient, fromClient, busID string) {
|
||||||
|
if !sm.cfg.AllowForceDetach {
|
||||||
|
log.Printf("[share] force-release denied for %s (not allowed by config)", busID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.mu.RLock()
|
||||||
|
share, exists := sm.active[busID]
|
||||||
|
sm.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[share] force-releasing %s (requested by %s, was used by %s)", busID, fromClient[:8], share.usedBy[:8])
|
||||||
|
sm.handleReleaseDevice(busID, share.usedBy)
|
||||||
|
}
|
||||||
|
|
||||||
func (sm *ShareManager) handleClientLeft(msg *protocol.ClientLeft) {
|
func (sm *ShareManager) handleClientLeft(msg *protocol.ClientLeft) {
|
||||||
sm.mu.RLock()
|
sm.mu.RLock()
|
||||||
var toRelease []string
|
var toRelease []string
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,11 @@ type UseManager struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
cfgPath string
|
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
|
||||||
tunnels map[string]*useTunnel // tunnelID -> tunnel
|
tunnels map[string]*useTunnel // tunnelID -> tunnel
|
||||||
pending map[string]chan *protocol.DeviceGranted // requestID -> response channel
|
pending map[string]chan *protocol.DeviceGranted // requestID -> response channel
|
||||||
|
forceDetachable map[string]bool // clientID -> allow_force_detach
|
||||||
}
|
}
|
||||||
|
|
||||||
type useTunnel struct {
|
type useTunnel struct {
|
||||||
|
|
@ -53,10 +54,11 @@ func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManag
|
||||||
client: client,
|
client: client,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cfgPath: cfgPath,
|
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),
|
||||||
pending: make(map[string]chan *protocol.DeviceGranted),
|
pending: make(map[string]chan *protocol.DeviceGranted),
|
||||||
|
forceDetachable: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
client.OnDeviceList = um.handleDeviceList
|
client.OnDeviceList = um.handleDeviceList
|
||||||
|
|
@ -284,6 +286,7 @@ func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
um.available[msg.ClientID] = remoteDevs
|
um.available[msg.ClientID] = remoteDevs
|
||||||
|
um.forceDetachable[msg.ClientID] = msg.AllowForceDetach
|
||||||
|
|
||||||
// Collect devices to auto-connect (while holding the lock to check attached map)
|
// Collect devices to auto-connect (while holding the lock to check attached map)
|
||||||
var toAutoConnect []RemoteDevice
|
var toAutoConnect []RemoteDevice
|
||||||
|
|
@ -382,6 +385,22 @@ func (um *UseManager) IsAutoConnect(vendorID, productID string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsForceDetachable checks if a share client allows force-detach
|
||||||
|
func (um *UseManager) IsForceDetachable(clientID string) bool {
|
||||||
|
um.mu.RLock()
|
||||||
|
defer um.mu.RUnlock()
|
||||||
|
return um.forceDetachable[clientID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceDetachDevice sends a force-release request to the share client
|
||||||
|
func (um *UseManager) ForceDetachDevice(clientID, busID string) error {
|
||||||
|
return um.client.SendJSON(&protocol.ForceRelease{
|
||||||
|
Type: protocol.MsgForceRelease,
|
||||||
|
TargetClient: clientID,
|
||||||
|
BusID: busID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
||||||
um.mu.RLock()
|
um.mu.RLock()
|
||||||
ch, exists := um.pending[msg.RequestID]
|
ch, exists := um.pending[msg.RequestID]
|
||||||
|
|
@ -430,6 +449,7 @@ func (um *UseManager) handleTunnelData(tunnelID string, data []byte) {
|
||||||
func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) {
|
func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) {
|
||||||
um.mu.Lock()
|
um.mu.Lock()
|
||||||
delete(um.available, msg.ClientID)
|
delete(um.available, msg.ClientID)
|
||||||
|
delete(um.forceDetachable, msg.ClientID)
|
||||||
|
|
||||||
// Detach any devices from this client
|
// Detach any devices from this client
|
||||||
for key, dev := range um.attached {
|
for key, dev := range um.attached {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ type Config struct {
|
||||||
|
|
||||||
// Auto-connect rules (use mode only)
|
// Auto-connect rules (use mode only)
|
||||||
AutoConnect []AutoConnectRule `json:"auto_connect,omitempty"`
|
AutoConnect []AutoConnectRule `json:"auto_connect,omitempty"`
|
||||||
|
|
||||||
|
// Share mode: allow other clients to force-detach devices in use
|
||||||
|
AllowForceDetach bool `json:"allow_force_detach,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a config with sensible defaults
|
// DefaultConfig returns a config with sensible defaults
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const (
|
||||||
MsgDeviceReleased = "device_released"
|
MsgDeviceReleased = "device_released"
|
||||||
MsgClientJoined = "client_joined"
|
MsgClientJoined = "client_joined"
|
||||||
MsgClientLeft = "client_left"
|
MsgClientLeft = "client_left"
|
||||||
|
MsgForceRelease = "force_release"
|
||||||
MsgPing = "ping"
|
MsgPing = "ping"
|
||||||
MsgPong = "pong"
|
MsgPong = "pong"
|
||||||
MsgError = "error"
|
MsgError = "error"
|
||||||
|
|
@ -63,10 +64,11 @@ type USBDevice struct {
|
||||||
|
|
||||||
// DeviceList is sent by share clients to announce available devices
|
// DeviceList is sent by share clients to announce available devices
|
||||||
type DeviceList struct {
|
type DeviceList struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
ClientName string `json:"client_name"`
|
ClientName string `json:"client_name"`
|
||||||
Devices []USBDevice `json:"devices"`
|
Devices []USBDevice `json:"devices"`
|
||||||
|
AllowForceDetach bool `json:"allow_force_detach,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestDevice is sent by use clients to request a specific device
|
// RequestDevice is sent by use clients to request a specific device
|
||||||
|
|
@ -108,6 +110,13 @@ type DeviceReleased struct {
|
||||||
BusID string `json:"bus_id"`
|
BusID string `json:"bus_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForceRelease is sent by use clients to force-release a device from another user
|
||||||
|
type ForceRelease struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
TargetClient string `json:"target_client"`
|
||||||
|
BusID string `json:"bus_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// ClientJoined is broadcast when a new client joins the group
|
// ClientJoined is broadcast when a new client joins the group
|
||||||
type ClientJoined struct {
|
type ClientJoined struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ func (h *Hub) HandleTextMessage(sender *Client, data []byte) {
|
||||||
h.handleDeviceGranted(sender, data)
|
h.handleDeviceGranted(sender, data)
|
||||||
case protocol.MsgDeviceDenied:
|
case protocol.MsgDeviceDenied:
|
||||||
h.handleDeviceDenied(sender, data)
|
h.handleDeviceDenied(sender, data)
|
||||||
|
case protocol.MsgForceRelease:
|
||||||
|
h.handleForceRelease(sender, data)
|
||||||
case protocol.MsgReleaseDevice:
|
case protocol.MsgReleaseDevice:
|
||||||
h.handleReleaseDevice(sender, data)
|
h.handleReleaseDevice(sender, data)
|
||||||
case protocol.MsgDeviceReleased:
|
case protocol.MsgDeviceReleased:
|
||||||
|
|
@ -311,6 +313,41 @@ func (h *Hub) handleReleaseDevice(sender *Client, data []byte) {
|
||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleForceRelease forwards a force-release request to the target share client
|
||||||
|
func (h *Hub) handleForceRelease(sender *Client, data []byte) {
|
||||||
|
var msg protocol.ForceRelease
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up tunnel for this device (by BusID, regardless of who owns it)
|
||||||
|
h.mu.Lock()
|
||||||
|
for tid, tunnel := range h.tunnels {
|
||||||
|
if tunnel.BusID == msg.BusID && tunnel.ShareClient == msg.TargetClient {
|
||||||
|
delete(h.tunnels, tid)
|
||||||
|
log.Printf("[hub] tunnel force-closed: %s", tid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
// Forward to share client
|
||||||
|
h.mu.RLock()
|
||||||
|
group := h.groups[sender.Hash]
|
||||||
|
if group != nil {
|
||||||
|
if target := group[msg.TargetClient]; target != nil && target.Mode == protocol.ModeShare {
|
||||||
|
enriched := map[string]interface{}{
|
||||||
|
"type": protocol.MsgForceRelease,
|
||||||
|
"target_client": msg.TargetClient,
|
||||||
|
"bus_id": msg.BusID,
|
||||||
|
"from_client": sender.ID,
|
||||||
|
}
|
||||||
|
target.WriteJSON(enriched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
// handleDeviceReleased broadcasts device released notification
|
// handleDeviceReleased broadcasts device released notification
|
||||||
func (h *Hub) handleDeviceReleased(sender *Client, data []byte) {
|
func (h *Hub) handleDeviceReleased(sender *Client, data []byte) {
|
||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@ type Handler struct {
|
||||||
DetachDevice func(clientID, busID string) error
|
DetachDevice func(clientID, busID string) error
|
||||||
SetAutoConnect func(vendorID, productID string, enabled bool) error
|
SetAutoConnect func(vendorID, productID string, enabled bool) error
|
||||||
IsAutoConnect func(vendorID, productID string) bool
|
IsAutoConnect func(vendorID, productID string) bool
|
||||||
InstallService func() error
|
ForceDetachDevice func(clientID, busID string) error
|
||||||
UninstallService func() error
|
InstallService func() error
|
||||||
GetStatus func() map[string]interface{}
|
UninstallService func() error
|
||||||
|
GetStatus func() map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new web handler
|
// NewHandler creates a new web handler
|
||||||
|
|
@ -57,6 +58,7 @@ func (h *Handler) setupRoutes() {
|
||||||
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/auto-connect", h.handleAutoConnect)
|
||||||
|
h.mux.HandleFunc("/api/force-detach", h.handleForceDetach)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +166,11 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
var updates struct {
|
var updates struct {
|
||||||
RelayAddr string `json:"relay_addr"`
|
RelayAddr string `json:"relay_addr"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
WebPort int `json:"web_port"`
|
WebPort int `json:"web_port"`
|
||||||
|
AllowForceDetach *bool `json:"allow_force_detach,omitempty"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||||
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
|
||||||
|
|
@ -186,6 +189,9 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if updates.WebPort > 0 {
|
if updates.WebPort > 0 {
|
||||||
h.cfg.WebPort = updates.WebPort
|
h.cfg.WebPort = updates.WebPort
|
||||||
}
|
}
|
||||||
|
if updates.AllowForceDetach != nil {
|
||||||
|
h.cfg.AllowForceDetach = *updates.AllowForceDetach
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.cfg.Save(h.cfgPath); err != nil {
|
if err := h.cfg.Save(h.cfgPath); err != nil {
|
||||||
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
|
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
|
||||||
|
|
@ -284,6 +290,34 @@ func (h *Handler) handleAutoConnect(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, map[string]interface{}{"ok": true})
|
writeJSON(w, map[string]interface{}{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleForceDetach(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "Method not allowed", 405)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
BusID string `json:"bus_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.ForceDetachDevice == nil {
|
||||||
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.ForceDetachDevice(req.ClientID, req.BusID); 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)
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,8 @@ function renderUseDevices(container, available, attached) {
|
||||||
</div>
|
</div>
|
||||||
<div class="device-status">
|
<div class="device-status">
|
||||||
${dev.status === 'in_use'
|
${dev.status === 'in_use'
|
||||||
? '<span class="badge in-use">In Benutzung</span>'
|
? `<span class="badge in-use">In Benutzung</span>
|
||||||
|
${dev.allow_force_detach ? `<button class="btn small danger" onclick="forceDetach('${clientId}', '${dev.bus_id}')">Trennen</button>` : ''}`
|
||||||
: isAttached
|
: isAttached
|
||||||
? '<span class="badge attached">Verbunden</span>'
|
? '<span class="badge attached">Verbunden</span>'
|
||||||
: `<span class="badge available">Verfuegbar</span>
|
: `<span class="badge available">Verfuegbar</span>
|
||||||
|
|
@ -197,6 +198,24 @@ async function detachDevice(clientId, busId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force detach
|
||||||
|
async function forceDetach(clientId, busId) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(API_BASE + '/api/force-detach', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ client_id: clientId, bus_id: busId })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!data.ok) {
|
||||||
|
alert('Fehler: ' + (data.error || 'Unbekannt'));
|
||||||
|
}
|
||||||
|
updateDevices();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Verbindungsfehler: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-connect toggle
|
// Auto-connect toggle
|
||||||
async function toggleAutoConnect(vendorId, productId, enabled) {
|
async function toggleAutoConnect(vendorId, productId, enabled) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -226,6 +245,7 @@ async function loadSettings() {
|
||||||
document.getElementById('mode').value = cfg.mode || 'use';
|
document.getElementById('mode').value = cfg.mode || 'use';
|
||||||
document.getElementById('client-name').value = cfg.name || '';
|
document.getElementById('client-name').value = cfg.name || '';
|
||||||
document.getElementById('web-port').value = cfg.web_port || 8080;
|
document.getElementById('web-port').value = cfg.web_port || 8080;
|
||||||
|
document.getElementById('allow-force-detach').checked = cfg.allow_force_detach || false;
|
||||||
document.getElementById('token1').value = cfg.token1 || '';
|
document.getElementById('token1').value = cfg.token1 || '';
|
||||||
document.getElementById('token2').value = cfg.token2 || '';
|
document.getElementById('token2').value = cfg.token2 || '';
|
||||||
document.getElementById('token3').value = cfg.token3 || '';
|
document.getElementById('token3').value = cfg.token3 || '';
|
||||||
|
|
@ -248,6 +268,7 @@ document.getElementById('settings-form').addEventListener('submit', async (e) =>
|
||||||
mode: document.getElementById('mode').value,
|
mode: document.getElementById('mode').value,
|
||||||
name: document.getElementById('client-name').value,
|
name: document.getElementById('client-name').value,
|
||||||
web_port: parseInt(document.getElementById('web-port').value),
|
web_port: parseInt(document.getElementById('web-port').value),
|
||||||
|
allow_force_detach: document.getElementById('allow-force-detach').checked,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,13 @@
|
||||||
<label for="web-port">Web-UI Port</label>
|
<label for="web-port">Web-UI Port</label>
|
||||||
<input type="number" id="web-port" value="8080">
|
<input type="number" id="web-port" value="8080">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="auto-connect-label">
|
||||||
|
<input type="checkbox" id="allow-force-detach">
|
||||||
|
Andere Clients duerfen Geraete trennen (Share-Modus)
|
||||||
|
</label>
|
||||||
|
<small>Erlaubt Use-Clients, Geraete die von anderen benutzt werden zu trennen</small>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn primary">Speichern</button>
|
<button type="submit" class="btn primary">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue