Make every live edit reach the device, and warn when it doesn't

In live mode the device is now written first and the document only takes
what the device accepted. Every live_hooks failure records a reason, and
warn_failure() shows it in a dialog and the status bar instead of failing
silently.

- Move, properties, OX page editor: rejected values are rolled back and listed
- Add/remove link: no longer edits the DOM alone when an end can't be resolved
- Add/delete component: report why the device refused
- Tidy Layout: sends each new position to the device, reverts refusals,
  marks the document dirty, and pushes no undo step in live mode
- User Manager: add (one Sox add carrying cred/perm/prov), edit and delete
  now go to the device
- Binary Buf slots (e.g. User.cred) are sent as Buf bytes, not as Str text
- Bump version to 0.0.0.136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 20:16:00 +03:00
co-authored by Claude Opus 5.5
parent f15fc42363
commit a2d4be6ea8
8 changed files with 312 additions and 114 deletions
+57 -3
View File
@@ -17,6 +17,8 @@ from tkinter import font as tkfont, messagebox, ttk
import xml.etree.ElementTree as ET
import app_state
import live_hooks
import sax_file
import undo
from sax_dom import set_prop_value
@@ -425,9 +427,32 @@ class _UserDialog:
def _apply_add(self, username, password):
elem = ET.Element("comp", name=username, type="sys::User")
cred = _hash_credentials(username, password)
ET.SubElement(elem, "prop", name="cred", val=cred)
ET.SubElement(elem, "prop", name="perm", val=str(self._perm_val))
ET.SubElement(elem, "prop", name="prov", val=str(self._prov_val))
values = {"cred": cred, "perm": str(self._perm_val), "prov": str(self._prov_val)}
if app_state.live_mode:
# One Sox add carrying every config value, so the user never exists on the
# device without its password and rights.
try:
config = sax_file.build_config_values("sys::User", values)
except (ValueError, TypeError) as e:
live_hooks.warn_failure("Add User", f"User '{username}' was not added to the device",
f"A value does not fit its slot: {e}")
return
new_id = live_hooks.live_add_component(
self._service_comp, "sys", "User", username, config)
if new_id is None:
live_hooks.warn_failure("Add User", f"User '{username}' was not added to the device")
return
elem.set("id", str(new_id))
for name, val in values.items():
ET.SubElement(elem, "prop", name=name, val=val)
if app_state.live_mode:
self._service_comp.append(elem)
app_state.mark_dirty()
app_state.set_status(f"User '{username}' added to device.")
return
index = len(list(self._service_comp))
cmd = undo.AddComponentCommand(self._service_comp, index, elem, label="add user")
@@ -458,6 +483,25 @@ class _UserDialog:
if not changes:
return
if app_state.live_mode:
failures = []
for elem, prop, _old, new in changes:
if live_hooks.live_set_prop_value(elem, prop, new):
set_prop_value(elem, prop, new)
app_state.mark_dirty()
else:
failures.append(f"{prop}: {live_hooks.last_error}")
if failures:
live_hooks.warn_failure(
"Edit User",
f"{len(failures)} of {len(changes)} change(s) to user '{username}' did "
"not reach the device and were not applied",
"\n".join(f" • {failure}" for failure in failures),
)
else:
app_state.set_status(f"User '{username}' updated on device.")
return
cmd = undo.SlotValueCommand(changes, label="edit user")
cmd.redo()
undo.push(cmd)
@@ -593,6 +637,16 @@ class UserServicePanel:
parent=self.frame.winfo_toplevel()):
return
if app_state.live_mode:
if not live_hooks.live_delete_component(user_elem):
live_hooks.warn_failure("Delete User", f"User '{name}' was not deleted from the device")
return
self.comp.remove(user_elem)
app_state.mark_dirty()
app_state.set_status(f"User '{name}' deleted from device.")
self._after_change()
return
index = list(self.comp).index(user_elem)
cmd = undo.AddComponentCommand(self.comp, index, user_elem, label="delete user")
cmd.undo()