Stabilize Local Agent and workstation management v1.26.23
This commit is contained in:
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.26.22"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.26.23"
|
||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||
@@ -39,9 +39,30 @@ def _write_zip_bytes(dst: zipfile.ZipFile, name: str, payload: bytes) -> None:
|
||||
dst.writestr(_zip_info(name), payload)
|
||||
|
||||
|
||||
def _validate_runtime_source() -> None:
|
||||
init_path = RUNTIME_ROOT / "erp_local_agent" / "__init__.py"
|
||||
dashboard_path = RUNTIME_ROOT / "erp_local_agent" / "dashboard.py"
|
||||
if not init_path.is_file() or not dashboard_path.is_file():
|
||||
raise RuntimeError("ERP Local Agent runtime is incomplete.")
|
||||
namespace: dict[str, object] = {}
|
||||
exec(compile(init_path.read_text(encoding="utf-8"), str(init_path), "exec"), namespace)
|
||||
runtime_version = str(namespace.get("__version__") or "").strip()
|
||||
agent_name = str(namespace.get("AGENT_NAME") or "").strip()
|
||||
if runtime_version != ERP_LOCAL_AGENT_VERSION:
|
||||
raise RuntimeError(
|
||||
f"ERP Local Agent package version mismatch: package={ERP_LOCAL_AGENT_VERSION}, runtime={runtime_version or 'missing'}."
|
||||
)
|
||||
if agent_name != ERP_LOCAL_AGENT_NAME:
|
||||
raise RuntimeError("ERP Local Agent runtime is missing the canonical AGENT_NAME constant.")
|
||||
dashboard_text = dashboard_path.read_text(encoding="utf-8")
|
||||
if "from . import AGENT_NAME, __version__" not in dashboard_text:
|
||||
raise RuntimeError("ERP Local Agent dashboard/runtime identity contract is inconsistent.")
|
||||
|
||||
|
||||
def _build_zip(*, env_text: str | None, include_env: bool, include_admin_readme: bool) -> bytes:
|
||||
if not RUNTIME_ROOT.exists():
|
||||
raise RuntimeError(f"ERP Local Agent runtime is missing: {RUNTIME_ROOT}")
|
||||
_validate_runtime_source()
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as dst:
|
||||
for path in sorted(RUNTIME_ROOT.rglob("*"), key=lambda item: item.as_posix()):
|
||||
|
||||
@@ -252,14 +252,50 @@ class Supervisor:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _validate_staged(staged: Path) -> None:
|
||||
def _validate_staged(staged: Path, expected_version: str = "") -> None:
|
||||
if not staged.exists() or not staged.is_dir():
|
||||
raise RuntimeError(f"Staged update directory not found: {staged}")
|
||||
if not (staged / "erp_local_agent" / "__init__.py").is_file():
|
||||
package_dir = staged / "erp_local_agent"
|
||||
init_path = package_dir / "__init__.py"
|
||||
dashboard_path = package_dir / "dashboard.py"
|
||||
main_path = package_dir / "main.py"
|
||||
if not init_path.is_file():
|
||||
raise RuntimeError("Staged update does not contain erp_local_agent/__init__.py.")
|
||||
if not dashboard_path.is_file() or not main_path.is_file():
|
||||
raise RuntimeError("Staged update is missing dashboard.py or main.py.")
|
||||
if not (staged / "requirements.txt").is_file():
|
||||
raise RuntimeError("Staged update does not contain requirements.txt.")
|
||||
|
||||
# Validate the candidate in-place BEFORE the working worker is stopped.
|
||||
# This specifically protects the dashboard identity contract that caused
|
||||
# older staged builds to fail on: from . import AGENT_NAME, __version__.
|
||||
py = _python(console=True)
|
||||
code = (
|
||||
"import sys;"
|
||||
f"sys.path.insert(0, {str(staged)!r});"
|
||||
"import erp_local_agent;"
|
||||
"assert getattr(erp_local_agent, 'AGENT_NAME', '') == 'ERP Local Agent', 'AGENT_NAME missing/invalid';"
|
||||
"assert getattr(erp_local_agent, '__version__', ''), '__version__ missing';"
|
||||
"from erp_local_agent import dashboard,main,commands,updater;"
|
||||
"print(erp_local_agent.__version__)"
|
||||
)
|
||||
result = subprocess.run(
|
||||
[str(py), "-c", code],
|
||||
cwd=str(staged),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
creationflags=_create_no_window(),
|
||||
timeout=90,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError("Staged update preflight import validation failed:\n" + result.stdout[-4000:])
|
||||
staged_version = (result.stdout or "").strip().splitlines()[-1].strip()
|
||||
if expected_version and staged_version != expected_version:
|
||||
raise RuntimeError(
|
||||
f"Staged update version mismatch: requested={expected_version}, package={staged_version or 'missing'}."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _backup_runtime(version: str) -> Path:
|
||||
backup = UPDATES_DIR / f"supervisor_backup_{time.strftime('%Y%m%d_%H%M%S')}_{version}"
|
||||
@@ -345,7 +381,8 @@ class Supervisor:
|
||||
raise RuntimeError("Dependency installation failed:\n" + result.stdout[-4000:])
|
||||
code = (
|
||||
"import erp_local_agent;"
|
||||
"from erp_local_agent import accounting_store,commands,tally,updater;"
|
||||
"assert getattr(erp_local_agent, 'AGENT_NAME', '') == 'ERP Local Agent', 'AGENT_NAME missing/invalid';"
|
||||
"from erp_local_agent import accounting_store,commands,tally,updater,dashboard,main;"
|
||||
"print(erp_local_agent.__version__)"
|
||||
)
|
||||
result = subprocess.run(
|
||||
@@ -379,7 +416,7 @@ class Supervisor:
|
||||
if not version or not staged_text:
|
||||
raise RuntimeError("Supervisor update request is missing version/staged_dir.")
|
||||
staged = Path(staged_text).resolve()
|
||||
self._validate_staged(staged)
|
||||
self._validate_staged(staged, expected_version=version)
|
||||
|
||||
_update_progress("preparing", 8, f"Preparing ERP Local Agent {version} update.", version=version)
|
||||
_state("installing", f"Installing ERP Local Agent {version}.", target_version=version, progress_pct=8)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
__version__ = "1.26.22"
|
||||
__version__ = "1.26.23"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">ERP Local Agent</h1>
|
||||
<p class="text-sm text-slate-500">Branch-local ERP runtime. Existing storage remains unchanged; future Tally/GST/local tools will use the same agent.</p>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Office Server & Workstation Agents</h1>
|
||||
<p class="text-sm text-slate-500">Office Server/Storage Agent manages branch storage; Workstation Agents provide per-PC Tally, GST and desktop integration. Existing storage and workstation features remain unchanged.</p>
|
||||
<p class="text-xs text-slate-400 mt-1">Scope: {{ storage_scope_title or '-' }}</p>
|
||||
<p class="text-xs text-emerald-700 mt-1">ERP Local Agent release: {{ erp_local_agent_version or '-' }} · automatic updates enabled</p>
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="font-bold text-slate-900">Configured ERP Local Agents</h2>
|
||||
<h2 class="font-bold text-slate-900">Office Server / Storage Agents</h2>
|
||||
<p class="text-xs text-slate-500">Partners/Branch Managers see only their managed branch. One branch should have one active node and one fixed storage root. Existing node secrets are not shown again.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +152,7 @@
|
||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-new-workstation">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
|
||||
<button class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white">+ New Workstation</button>
|
||||
<button class="rounded-lg bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white">Install New Workstation</button>
|
||||
</form>
|
||||
<div class="max-w-48 text-right text-[11px] text-slate-500">Installs on another office PC without changing credentials used by existing workstations.</div>
|
||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
|
||||
@@ -178,8 +178,8 @@
|
||||
|
||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b">
|
||||
<h2 class="font-bold text-slate-900">Registered Workstations</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">Each PC keeps a persistent agent identity. Use <strong>+ New Workstation</strong> on the branch node above for another office PC; it receives its own workstation credential and existing agents keep working.</p>
|
||||
<h2 class="font-bold text-slate-900">Workstation Agents</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">Each PC keeps a persistent agent identity. Use <strong>Install New Workstation</strong> on the branch node above for another office PC; it receives its own workstation credential and existing agents keep working.</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
|
||||
Reference in New Issue
Block a user