This commit is contained in:
2026-01-22 02:45:08 -08:00
parent 3d571b007b
commit ba23c0606f
18 changed files with 75 additions and 5355 deletions

View File

@@ -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