f
This commit is contained in:
@@ -128,7 +128,6 @@ class ConfigModal(ModalScreen):
|
||||
yield Label("Categories", classes="config-label")
|
||||
with ListView(id="category-list"):
|
||||
yield ListItem(Label("Global Settings"), id="cat-globals")
|
||||
yield ListItem(Label("Connectors"), id="cat-networking")
|
||||
yield ListItem(Label("Stores"), id="cat-stores")
|
||||
yield ListItem(Label("Providers"), id="cat-providers")
|
||||
|
||||
@@ -138,44 +137,62 @@ class ConfigModal(ModalScreen):
|
||||
yield Button("Save", variant="success", id="save-btn")
|
||||
yield Button("Add Store", variant="primary", id="add-store-btn")
|
||||
yield Button("Add Provider", variant="primary", id="add-provider-btn")
|
||||
yield Button("Add Net", variant="primary", id="add-net-btn")
|
||||
yield Button("Back", id="back-btn")
|
||||
yield Button("Close", variant="error", id="cancel-btn")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.query_one("#add-store-btn", Button).display = False
|
||||
self.query_one("#add-provider-btn", Button).display = False
|
||||
self.query_one("#add-net-btn", Button).display = False
|
||||
self.refresh_view()
|
||||
|
||||
def refresh_view(self) -> None:
|
||||
container = self.query_one("#fields-container", ScrollableContainer)
|
||||
"""
|
||||
Refresh the content area. We debounce this call and use a render_id
|
||||
to avoid race conditions with Textual's async widget mounting.
|
||||
"""
|
||||
self._render_id = getattr(self, "_render_id", 0) + 1
|
||||
|
||||
if hasattr(self, "_refresh_timer"):
|
||||
self._refresh_timer.stop()
|
||||
self._refresh_timer = self.set_timer(0.02, self._actual_refresh)
|
||||
|
||||
def _actual_refresh(self) -> None:
|
||||
try:
|
||||
container = self.query_one("#fields-container", ScrollableContainer)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
self._button_id_map.clear()
|
||||
self._input_id_map.clear()
|
||||
|
||||
# Clear existing synchronously
|
||||
for child in list(container.children):
|
||||
child.remove()
|
||||
|
||||
# Clear existing
|
||||
container.query("*").remove()
|
||||
|
||||
# Update visibility of buttons
|
||||
try:
|
||||
self.query_one("#add-store-btn", Button).display = (self.current_category == "stores" and self.editing_item_name is None)
|
||||
self.query_one("#add-provider-btn", Button).display = (self.current_category == "providers" and self.editing_item_name is None)
|
||||
self.query_one("#add-net-btn", Button).display = (self.current_category == "networking" and self.editing_item_name is None)
|
||||
self.query_one("#back-btn", Button).display = (self.editing_item_name is not None)
|
||||
self.query_one("#save-btn", Button).display = (self.editing_item_name is not None or self.current_category == "globals")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# We mount using call_after_refresh to ensure the removals are processed by Textual
|
||||
# before we try to mount new widgets with potentially duplicate IDs.
|
||||
render_id = self._render_id
|
||||
|
||||
def do_mount():
|
||||
# If a new refresh was started, ignore this old mount request
|
||||
if getattr(self, "_render_id", 0) != render_id:
|
||||
return
|
||||
|
||||
# Final check that container is empty. remove() is async.
|
||||
if container.children:
|
||||
for child in list(container.children):
|
||||
child.remove()
|
||||
|
||||
if self.editing_item_name:
|
||||
self.render_item_editor(container)
|
||||
elif self.current_category == "globals":
|
||||
self.render_globals(container)
|
||||
elif self.current_category == "networking":
|
||||
self.render_networking(container)
|
||||
elif self.current_category == "stores":
|
||||
self.render_stores(container)
|
||||
elif self.current_category == "providers":
|
||||
@@ -241,73 +258,6 @@ class ConfigModal(ModalScreen):
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
idx += 1
|
||||
|
||||
def render_networking(self, container: ScrollableContainer) -> None:
|
||||
container.mount(Label("ZeroTier Networks (local)", classes="config-label"))
|
||||
|
||||
from API import zerotier as zt
|
||||
# Show whether we have an explicit authtoken available and its source
|
||||
try:
|
||||
token_src = zt._get_token_path()
|
||||
except Exception:
|
||||
token_src = None
|
||||
|
||||
if token_src == "env":
|
||||
container.mount(Static("Auth: authtoken provided via env var (ZEROTIER_AUTH_TOKEN) — no admin required", classes="config-note"))
|
||||
elif token_src:
|
||||
container.mount(Static(f"Auth: authtoken file found: {token_src} — no admin required", classes="config-note"))
|
||||
else:
|
||||
container.mount(Static("Auth: authtoken not found in workspace; TUI may need admin to join networks", classes="config-warning"))
|
||||
|
||||
try:
|
||||
local_nets = zt.list_networks()
|
||||
if not local_nets:
|
||||
container.mount(Static("No active ZeroTier networks found on this machine."))
|
||||
else:
|
||||
for n in local_nets:
|
||||
row = Horizontal(
|
||||
Static(f"{n.name} [{n.id}] - {n.status}", classes="item-label"),
|
||||
Button("Leave", variant="error", id=f"zt-leave-{n.id}"),
|
||||
classes="item-row"
|
||||
)
|
||||
container.mount(row)
|
||||
except Exception as exc:
|
||||
container.mount(Static(f"Error listing ZeroTier networks: {exc}"))
|
||||
|
||||
container.mount(Rule())
|
||||
container.mount(Label("Connectors", classes="config-label"))
|
||||
net = self.config_data.get("networking", {})
|
||||
if not net:
|
||||
container.mount(Static("No connectors configured."))
|
||||
else:
|
||||
idx = 0
|
||||
for ntype, conf in net.items():
|
||||
edit_id = f"edit-net-{idx}"
|
||||
del_id = f"del-net-{idx}"
|
||||
self._button_id_map[edit_id] = ("edit", "networking", ntype)
|
||||
self._button_id_map[del_id] = ("del", "networking", ntype)
|
||||
idx += 1
|
||||
|
||||
label = ntype
|
||||
if ntype == "zerotier":
|
||||
serve = conf.get("serve", "Unknown")
|
||||
net_id = conf.get("network_id", "Unknown")
|
||||
net_name = net_id
|
||||
try:
|
||||
for ln in local_nets:
|
||||
if ln.id == net_id:
|
||||
net_name = ln.name
|
||||
break
|
||||
except Exception: pass
|
||||
label = f"{serve} ---> {net_name}"
|
||||
|
||||
row = Horizontal(
|
||||
Static(label, classes="item-label"),
|
||||
Button("Edit", id=edit_id),
|
||||
Button("Delete", variant="error", id=del_id),
|
||||
classes="item-row"
|
||||
)
|
||||
container.mount(row)
|
||||
|
||||
def render_stores(self, container: ScrollableContainer) -> None:
|
||||
container.mount(Label("Configured Stores", classes="config-label"))
|
||||
stores = self.config_data.get("store", {})
|
||||
@@ -402,30 +352,6 @@ class ConfigModal(ModalScreen):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fetch Networking schema
|
||||
if item_type == "networking":
|
||||
if item_name == "zerotier":
|
||||
from API import zerotier as zt
|
||||
local_net_choices = []
|
||||
try:
|
||||
for n in zt.list_networks():
|
||||
local_net_choices.append((f"{n.name} ({n.id})", n.id))
|
||||
except Exception: pass
|
||||
|
||||
local_store_choices = []
|
||||
for s_type, s_data in self.config_data.get("store", {}).items():
|
||||
for s_name in s_data.keys():
|
||||
local_store_choices.append(s_name)
|
||||
|
||||
schema = [
|
||||
{"key": "network_id", "label": "Network to Share on", "choices": local_net_choices},
|
||||
{"key": "serve", "label": "Local Store to Share", "choices": local_store_choices},
|
||||
{"key": "port", "label": "Port", "default": "999"},
|
||||
{"key": "api_key", "label": "Access Key (API Key)", "default": "", "secret": True},
|
||||
]
|
||||
for f in schema:
|
||||
provider_schema_map[f["key"].upper()] = f
|
||||
|
||||
# Use columns for better layout of inputs with paste buttons
|
||||
container.mount(Label("Edit Settings"))
|
||||
# render_item_editor will handle the inputs for us if we set these
|
||||
@@ -583,8 +509,6 @@ class ConfigModal(ModalScreen):
|
||||
if not event.item: return
|
||||
if event.item.id == "cat-globals":
|
||||
self.current_category = "globals"
|
||||
elif event.item.id == "cat-networking":
|
||||
self.current_category = "networking"
|
||||
elif event.item.id == "cat-stores":
|
||||
self.current_category = "stores"
|
||||
elif event.item.id == "cat-providers":
|
||||
@@ -608,24 +532,7 @@ class ConfigModal(ModalScreen):
|
||||
if not self.validate_current_editor():
|
||||
return
|
||||
try:
|
||||
# If we are editing networking.zerotier, check if network_id changed and join it
|
||||
if self.editing_item_type == "networking" and self.editing_item_name == "zerotier":
|
||||
old_id = str(self.config_data.get("networking", {}).get("zerotier", {}).get("network_id") or "").strip()
|
||||
self.save_all()
|
||||
new_id = str(self.config_data.get("networking", {}).get("zerotier", {}).get("network_id") or "").strip()
|
||||
|
||||
if new_id and new_id != old_id:
|
||||
from API import zerotier as zt
|
||||
try:
|
||||
if zt.join_network(new_id):
|
||||
self.notify(f"Joined ZeroTier network {new_id}")
|
||||
else:
|
||||
self.notify(f"Config saved, but failed to join network {new_id}", severity="warning")
|
||||
except Exception as exc:
|
||||
self.notify(f"Join error: {exc}", severity="error")
|
||||
else:
|
||||
self.save_all()
|
||||
|
||||
self.save_all()
|
||||
self.notify("Configuration saved!")
|
||||
# Return to the main list view within the current category
|
||||
self.editing_item_name = None
|
||||
@@ -633,15 +540,6 @@ class ConfigModal(ModalScreen):
|
||||
self.refresh_view()
|
||||
except Exception as exc:
|
||||
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
||||
elif bid.startswith("zt-leave-"):
|
||||
nid = bid.replace("zt-leave-", "")
|
||||
from API import zerotier as zt
|
||||
try:
|
||||
zt.leave_network(nid)
|
||||
self.notify(f"Left ZeroTier network {nid}")
|
||||
self.refresh_view()
|
||||
except Exception as exc:
|
||||
self.notify(f"Failed to leave: {exc}", severity="error")
|
||||
elif bid in self._button_id_map:
|
||||
action, itype, name = self._button_id_map[bid]
|
||||
if action == "edit":
|
||||
@@ -657,9 +555,6 @@ class ConfigModal(ModalScreen):
|
||||
elif itype == "provider":
|
||||
if "provider" in self.config_data and name in self.config_data["provider"]:
|
||||
del self.config_data["provider"][name]
|
||||
elif itype == "networking":
|
||||
if "networking" in self.config_data and name in self.config_data["networking"]:
|
||||
del self.config_data["networking"][name]
|
||||
self.refresh_view()
|
||||
elif bid == "add-store-btn":
|
||||
all_classes = _discover_store_classes()
|
||||
@@ -685,9 +580,6 @@ class ConfigModal(ModalScreen):
|
||||
except Exception:
|
||||
pass
|
||||
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
|
||||
elif bid == "add-net-btn":
|
||||
options = ["zerotier"]
|
||||
self.app.push_screen(SelectionModal("Select Networking Service", options), callback=self.on_net_type_selected)
|
||||
elif bid.startswith("paste-"):
|
||||
# Programmatic paste button
|
||||
target_id = bid.replace("paste-", "")
|
||||
@@ -725,124 +617,6 @@ class ConfigModal(ModalScreen):
|
||||
def on_store_type_selected(self, stype: str) -> None:
|
||||
if not stype: return
|
||||
|
||||
if stype == "zerotier":
|
||||
# Push a discovery wizard
|
||||
from TUI.modalscreen.selection_modal import SelectionModal
|
||||
from API import zerotier as zt
|
||||
|
||||
# 1. Choose Network
|
||||
joined = zt.list_networks()
|
||||
if not joined:
|
||||
self.notify("Error: Join a ZeroTier network first in 'Connectors'", severity="error")
|
||||
return
|
||||
|
||||
net_options = [f"{n.name or 'Network'} ({n.id})" for n in joined]
|
||||
|
||||
def on_net_selected(net_choice: str):
|
||||
if not net_choice: return
|
||||
net_id = net_choice.split("(")[-1].rstrip(")")
|
||||
|
||||
# 2. Host or Connect?
|
||||
def on_mode_selected(mode: str):
|
||||
if not mode: return
|
||||
|
||||
if mode == "Host (Share a local store)":
|
||||
# 3a. Select Local Store to Share
|
||||
local_stores = []
|
||||
for s_type, s_data in self.config_data.get("store", {}).items():
|
||||
if s_type == "zerotier": continue
|
||||
for s_name in s_data.keys():
|
||||
local_stores.append(f"{s_name} ({s_type})")
|
||||
|
||||
if not local_stores:
|
||||
self.notify("No local stores available to share.", severity="error")
|
||||
return
|
||||
|
||||
def on_share_selected(share_choice: str):
|
||||
if not share_choice: return
|
||||
share_name = share_choice.split(" (")[0]
|
||||
|
||||
# Update networking config
|
||||
if "networking" not in self.config_data: self.config_data["networking"] = {}
|
||||
zt_net = self.config_data["networking"].setdefault("zerotier", {})
|
||||
zt_net["serve"] = share_name
|
||||
zt_net["network_id"] = net_id
|
||||
if not zt_net.get("port"):
|
||||
zt_net["port"] = "999"
|
||||
|
||||
try:
|
||||
self.save_all()
|
||||
from SYS.background_services import ensure_zerotier_server_running
|
||||
ensure_zerotier_server_running()
|
||||
self.notify(f"ZeroTier auto-saved: Sharing '{share_name}' on network {net_id}")
|
||||
except Exception as e:
|
||||
self.notify(f"Auto-save failed: {e}", severity="error")
|
||||
|
||||
self.refresh_view()
|
||||
|
||||
self.app.push_screen(SelectionModal("Select Local Store to Share", local_stores), callback=on_share_selected)
|
||||
|
||||
else:
|
||||
# 3b. Connect to Remote Peer - Background Discovery
|
||||
@work
|
||||
async def run_discovery(node):
|
||||
self.notify(f"Discovery: Scanning {net_id} for peers...", timeout=5)
|
||||
central_token = self.config_data.get("networking", {}).get("zerotier", {}).get("api_key")
|
||||
try:
|
||||
import asyncio
|
||||
from functools import partial
|
||||
loop = asyncio.get_event_loop()
|
||||
probes = await loop.run_in_executor(None, partial(
|
||||
zt.discover_services_on_network, net_id, ports=[999, 45869], api_token=central_token
|
||||
))
|
||||
except Exception as e:
|
||||
self.notify(f"Discovery error: {e}", severity="error")
|
||||
return
|
||||
|
||||
if not probes:
|
||||
self.notify("No peers found. Check firewall or server status.", severity="warning")
|
||||
return
|
||||
|
||||
peer_options = []
|
||||
for p in probes:
|
||||
label = "Remote"
|
||||
if isinstance(p.payload, dict):
|
||||
label = p.payload.get("name") or p.payload.get("peer_id") or label
|
||||
status = " [Locked]" if p.status_code == 401 else ""
|
||||
peer_options.append(f"{p.address} ({label}){status}")
|
||||
|
||||
def on_peer_selected(choice: str):
|
||||
if not choice: return
|
||||
addr = choice.split(" ")[0]
|
||||
match = next((p for p in probes if p.address == addr), None)
|
||||
if match:
|
||||
save_connected_store(match)
|
||||
|
||||
self.app.push_screen(SelectionModal("Select Peer to Connect", peer_options), callback=on_peer_selected)
|
||||
|
||||
def save_connected_store(p: zt.ZeroTierServiceProbe):
|
||||
new_name = f"zt_{p.address.replace('.', '_')}"
|
||||
if "store" not in self.config_data: self.config_data["store"] = {}
|
||||
store_cfg = self.config_data["store"].setdefault("zerotier", {})
|
||||
|
||||
store_cfg[new_name] = {
|
||||
"NAME": new_name,
|
||||
"NETWORK_ID": net_id,
|
||||
"HOST": p.address,
|
||||
"PORT": str(p.port),
|
||||
"SERVICE": p.service_hint or "remote"
|
||||
}
|
||||
self.save_all()
|
||||
self.notify(f"Connected to {p.address}")
|
||||
self.refresh_view()
|
||||
|
||||
run_discovery(self)
|
||||
|
||||
self.app.push_screen(SelectionModal("ZeroTier Mode", ["Host (Share a local store)", "Connect (Use a remote store)"]), callback=on_mode_selected)
|
||||
|
||||
self.app.push_screen(SelectionModal("Select ZeroTier Network", net_options), callback=on_net_selected)
|
||||
return
|
||||
|
||||
new_name = f"new_{stype}"
|
||||
if "store" not in self.config_data:
|
||||
self.config_data["store"] = {}
|
||||
@@ -907,18 +681,6 @@ class ConfigModal(ModalScreen):
|
||||
self.editing_item_name = ptype
|
||||
self.refresh_view()
|
||||
|
||||
def on_net_type_selected(self, ntype: str) -> None:
|
||||
if not ntype: return
|
||||
self.editing_item_type = "networking"
|
||||
self.editing_item_name = ntype
|
||||
|
||||
# Ensure it exists in config_data
|
||||
net = self.config_data.setdefault("networking", {})
|
||||
if ntype not in net:
|
||||
net[ntype] = {}
|
||||
|
||||
self.refresh_view()
|
||||
|
||||
def _update_config_value(self, widget_id: str, value: Any) -> None:
|
||||
if widget_id not in self._input_id_map:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user