Add ERP Local Agent secure self update framework
This commit is contained in:
@@ -1,484 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import io
|
import io
|
||||||
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
# Embedded LSA4 tunnel-enabled Task Scheduler local storage agent package.
|
ERP_LOCAL_AGENT_VERSION = "1.0.0"
|
||||||
# Uses Windows Task Scheduler instead of pywin32 Windows Service.
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
# This is used by the ERP Branch Storage Dashboard to generate branch-specific
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
# pre-configured agent ZIP files with .env already filled.
|
|
||||||
LSA4_ZIP_B64 = """
|
|
||||||
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
|
|
||||||
zy8Tas8vE2p1eAsAAQQAAAAABAAAAABAZWNobyBvZmYKY2QgL2QgJX5kcDAKY2FsbCBydW5fYWdlbnQuYmF0CnBhdXNlClBLAwQUAAAACACqiLhcc9tLXl4A
|
|
||||||
AABmAAAADAAcAHJ1bl9vbmNlLmJhdFVUCQADzy8Tas8vE2p1eAsAAQQAAAAABAAAAAANyTEOgCAMAMC9r+jiKPoE/+BKYmqpSiKUSCVx8e063HST8KGo2wYc
|
|
||||||
cAjYvaGMwHSe6Jrk5me+YrHqiS02MnErGZTHDs3YJ6Q7RFuq6UW7LL9sLlH8r9fMAoXuKvABUEsDBAoAAAAAAKqIuFzlgx33OgAAADoAAAAYABwAc3RhcnRf
|
|
||||||
dGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAvUnVuIC9UTiAiQXVkaXRGaXJtU3RvcmFn
|
|
||||||
ZUFnZW50IgpwYXVzZQpQSwMECgAAAAAAqoi4XDqIHG1IAAAASAAAABkAHABzdGF0dXNfdGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAA
|
|
||||||
AAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAvUXVlcnkgL1ROICJBdWRpdEZpcm1TdG9yYWdlQWdlbnQiIC9WIC9GTyBMSVNUCnBhdXNlClBLAwQKAAAAAACq
|
|
||||||
iLhcQl08MToAAAA6AAAAFwAcAHN0b3BfdGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAv
|
|
||||||
RW5kIC9UTiAiQXVkaXRGaXJtU3RvcmFnZUFnZW50IgpwYXVzZQpQSwMEFAAAAAgAqoi4XLi315BRAAAAdQAAABwAHAB1bmluc3RhbGxfdGFza19zY2hlZHVs
|
|
||||||
ZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAHNITc7IV8hPS+MqTs4oSSzOLlbQd81LUdAP8VNQcixNySxxyyzKDS7JL0pMT3VMT80rUVKwyyvN
|
|
||||||
UTACkUiaXFJzUktS8erTd+MqSCwtTuUCAFBLAQIeAxQAAAAIAKqIuFw8Po1A0QAAAD4BAAAMABgAAAAAAAEAAACkgQAAAAAuZW52LmV4YW1wbGVVVAUAA9Av
|
|
||||||
E2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcUizhRVgCAAA6BAAACgAYAAAAAAABAAAApIEXAQAAUkVBRE1FLnR4dFVUBQAD0C8TanV4CwABBAAA
|
|
||||||
AAAEAAAAAFBLAQIeAxQAAAAIAKqIuFyR0y7TkwMAADkHAAAjABgAAAAAAAEAAACkgbMDAABSRUFETUVfTFNBMV9MT0NBTF9TVE9SQUdFX0FHRU5ULnR4dFVU
|
|
||||||
BQADzy8TanV4CwABBAAAAAAEAAAAAFBLAQIeAwoAAAAAAKuIuFwAAAAAAAAAAAAAAAAUABgAAAAAAAAAEADtQaMHAABhdWRpdF9zdG9yYWdlX2FnZW50L1VU
|
|
||||||
BQAD0S8TanV4CwABBAAAAAAEAAAAAFBLAQIeAwoAAAAAAKqIuFxraGOJJQAAACUAAAAfABgAAAAAAAEAAACkgfEHAABhdWRpdF9zdG9yYWdlX2FnZW50L19f
|
|
||||||
aW5pdF9fLnB5VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XGFbMtTgAgAA8AsAAB0AGAAAAAAAAQAAAKSBbwgAAGF1ZGl0X3N0b3Jh
|
|
||||||
Z2VfYWdlbnQvY2xpZW50LnB5VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XGb0X0DJBAAA5g0AAB0AGAAAAAAAAQAAAKSBpgsAAGF1
|
|
||||||
ZGl0X3N0b3JhZ2VfYWdlbnQvY29uZmlnLnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XIrppAA2AgAAQgkAABkAGAAAAAAAAQAA
|
|
||||||
AKSBxhAAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvZGIucHlVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcMiXO1IcBAAA/AwAAHQAYAAAA
|
|
||||||
AAABAAAApIFPEwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9sb2dnZXIucHlVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhc605uolECAAB9
|
|
||||||
BQAAGwAYAAAAAAABAAAApIEtFQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9tYWluLnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XFPu
|
|
||||||
e8JJAwAA5AcAAB4AGAAAAAAAAQAAAKSB0xcAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvc3RvcmFnZS5weVVUBQADzy8TanV4CwABBAAAAAAEAAAAAFBLAQIeAxQA
|
|
||||||
AAAIAKqIuFx7PTmzXwUAANMOAAAnABgAAAAAAAEAAACkgXQbAABhdWRpdF9zdG9yYWdlX2FnZW50L3N0b3JhZ2VfaWRlbnRpdHkucHlVVAUAA88vE2p1eAsA
|
|
||||||
AQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcnDkQJcEGAAB6GgAAGwAYAAAAAAABAAAApIE0IQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9zeW5jLnB5VVQFAAPQ
|
|
||||||
LxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XDByUh2HAwAA9QoAAB0AGAAAAAAAAQAAAKSBSigAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvdHVubmVs
|
|
||||||
LnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DCgAAAAAAq4i4XAAAAAAAAAAAAAAAACAAGAAAAAAAAAAQAO1BKCwAAGF1ZGl0X3N0b3JhZ2VfYWdl
|
|
||||||
bnQvX19weWNhY2hlX18vVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XAW4epmaAAAAvgAAADgAGAAAAAAAAAAAAKSBgiwAAGF1ZGl0
|
|
||||||
X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vX19pbml0X18uY3B5dGhvbi0zMTMucHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4
|
|
||||||
XNpcCab4CAAANBUAADYAGAAAAAAAAAAAAKSBji0AAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vY2xpZW50LmNweXRob24tMzEzLnB5Y1VUBQAD
|
|
||||||
0S8TanV4CwABBAAAAAAEAAAAAFBLAQIeAxQAAAAIAKuIuFz3aglpvAsAAGsWAAA2ABgAAAAAAAAAAACkgfY2AABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHlj
|
|
||||||
YWNoZV9fL2NvbmZpZy5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACriLhc5UFNhAoGAABmDwAAMgAYAAAAAAAA
|
|
||||||
AAAApIEiQwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9kYi5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMU
|
|
||||||
AAAACACriLhcLV0uZJQDAAAOBgAANgAYAAAAAAAAAAAApIGYSQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9sb2dnZXIuY3B5dGhvbi0zMTMu
|
|
||||||
cHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XG4hMUfhBQAAvwkAADQAGAAAAAAAAAAAAKSBnE0AAGF1ZGl0X3N0b3JhZ2VfYWdl
|
|
||||||
bnQvX19weWNhY2hlX18vbWFpbi5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACriLhcWJ9BapoIAACeDwAANwAY
|
|
||||||
AAAAAAAAAAAApIHrUwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9zdG9yYWdlLmNweXRob24tMzEzLnB5Y1VUBQAD0S8TanV4CwABBAAAAAAE
|
|
||||||
AAAAAFBLAQIeAxQAAAAIAKuIuFwpZ4K0XgsAAGoVAABAABgAAAAAAAAAAACkgfZcAABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHljYWNoZV9fL3N0b3JhZ2Vf
|
|
||||||
aWRlbnRpdHkuY3B5dGhvbi0zMTMucHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XHr6MhnMEgAA6SoAADQAGAAAAAAAAAAAAKSB
|
|
||||||
zmgAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vc3luYy5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAA
|
|
||||||
CACriLhcHRn93QsLAAAuFQAANgAYAAAAAAAAAAAApIEIfAAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy90dW5uZWwuY3B5dGhvbi0zMTMucHlj
|
|
||||||
VVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XApngiOWAAAA2QAAABAAGAAAAAAAAQAAAKSBg4cAAGNoZWNrX2NvbmZpZy5iYXRVVAUA
|
|
||||||
A88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcjA5piscBAAArBAAADwAYAAAAAAABAAAApIFjiAAAY2hlY2tfY29uZmlnLnB5VVQFAAPPLxNq
|
|
||||||
dXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XPoxAmEiAQAA6AEAAA8AGAAAAAAAAQAAAKSBc4oAAGluc3RhbGxfZGV2LnBzMVVUBQADzy8TanV4CwAB
|
|
||||||
BAAAAAAEAAAAAFBLAQIeAxQAAAAIAKqIuFxDbXcK+gEAAJwDAAAaABgAAAAAAAEAAACkgd6LAABpbnN0YWxsX3Rhc2tfc2NoZWR1bGVyLmJhdFVUBQADzy8T
|
|
||||||
anV4CwABBAAAAAAEAAAAAFBLAQIeAxQAAAAIAKqIuFxOD3XONQAAADcAAAAQABgAAAAAAAEAAACkgSyOAAByZXF1aXJlbWVudHMudHh0VVQFAAPQLxNqdXgL
|
|
||||||
AAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XHw7z7v8AAAAlAEAAA0AGAAAAAAAAQAAAKSBq44AAHJ1bl9hZ2VudC5iYXRVVAUAA88vE2p1eAsAAQQAAAAA
|
|
||||||
BAAAAABQSwECHgMKAAAAAACqiLhcraKIbC8AAAAvAAAAEwAYAAAAAAABAAAApIHujwAAcnVuX2FnZW50X2RlYnVnLmJhdFVUBQADzy8TanV4CwABBAAAAAAE
|
|
||||||
AAAAAFBLAQIeAxQAAAAIAKqIuFxz20teXgAAAGYAAAAMABgAAAAAAAEAAACkgWqQAABydW5fb25jZS5iYXRVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwEC
|
|
||||||
HgMKAAAAAACqiLhc5YMd9zoAAAA6AAAAGAAYAAAAAAABAAAApIEOkQAAc3RhcnRfdGFza19zY2hlZHVsZXIuYmF0VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAA
|
|
||||||
UEsBAh4DCgAAAAAAqoi4XDqIHG1IAAAASAAAABkAGAAAAAAAAQAAAKSBmpEAAHN0YXR1c190YXNrX3NjaGVkdWxlci5iYXRVVAUAA88vE2p1eAsAAQQAAAAA
|
|
||||||
BAAAAABQSwECHgMKAAAAAACqiLhcQl08MToAAAA6AAAAFwAYAAAAAAABAAAApIE1kgAAc3RvcF90YXNrX3NjaGVkdWxlci5iYXRVVAUAA88vE2p1eAsAAQQA
|
|
||||||
AAAABAAAAABQSwECHgMUAAAACACqiLhcuLfXkFEAAAB1AAAAHAAYAAAAAAABAAAApIHAkgAAdW5pbnN0YWxsX3Rhc2tfc2NoZWR1bGVyLmJhdFVUBQADzy8T
|
|
||||||
anV4CwABBAAAAAAEAAAAAFBLBQYAAAAAJQAlALsOAABnkwAAAAA=
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, storage_root: str, tenant_id: int | str | None = None, branch_id: int | str | None = None, sync_interval_seconds: int = 30, request_timeout_seconds: int = 60, tunnel_enabled: bool = True, tunnel_reconnect_seconds: int = 10) -> str:
|
def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, storage_root: str, tenant_id=None, branch_id=None, sync_interval_seconds: int = 30, request_timeout_seconds: int = 60, tunnel_enabled: bool = True, tunnel_reconnect_seconds: int = 10) -> str:
|
||||||
erp_base_url = (erp_base_url or "").strip().rstrip("/")
|
erp_base_url = (erp_base_url or "").strip().rstrip("/")
|
||||||
# Coolify/Traefik may forward the application request internally as HTTP even
|
|
||||||
# when the public ERP URL is HTTPS. Public agent packages must use HTTPS so
|
|
||||||
# tunnel.py derives a valid wss:// endpoint. Preserve explicit local test URLs.
|
|
||||||
if erp_base_url.startswith("http://"):
|
if erp_base_url.startswith("http://"):
|
||||||
host = erp_base_url[7:].split("/", 1)[0].split(":", 1)[0].lower()
|
host = erp_base_url[7:].split("/", 1)[0].split(":", 1)[0].lower()
|
||||||
if host not in {"localhost", "127.0.0.1", "::1"}:
|
if host not in {"localhost", "127.0.0.1", "::1"}:
|
||||||
@@ -497,93 +29,32 @@ def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, stor
|
|||||||
f"REQUEST_TIMEOUT_SECONDS={int(request_timeout_seconds or 60)}\n"
|
f"REQUEST_TIMEOUT_SECONDS={int(request_timeout_seconds or 60)}\n"
|
||||||
f"TUNNEL_ENABLED={str(bool(tunnel_enabled)).lower()}\n"
|
f"TUNNEL_ENABLED={str(bool(tunnel_enabled)).lower()}\n"
|
||||||
f"TUNNEL_RECONNECT_SECONDS={int(tunnel_reconnect_seconds or 10)}\n"
|
f"TUNNEL_RECONNECT_SECONDS={int(tunnel_reconnect_seconds or 10)}\n"
|
||||||
|
"AUTO_UPDATE=true\n"
|
||||||
|
"UPDATE_CHECK_INTERVAL_SECONDS=300\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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}")
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as dst:
|
||||||
|
for path in sorted(RUNTIME_ROOT.rglob("*")):
|
||||||
|
if not path.is_file() or "__pycache__" in path.parts:
|
||||||
|
continue
|
||||||
|
name = path.relative_to(RUNTIME_ROOT).as_posix()
|
||||||
|
dst.writestr(name, path.read_bytes())
|
||||||
|
if include_env and env_text is not None:
|
||||||
|
dst.writestr(".env", env_text)
|
||||||
|
if include_admin_readme:
|
||||||
|
dst.writestr("README_ERP_LOCAL_AGENT.txt", f"ERP Local Agent {ERP_LOCAL_AGENT_VERSION}\nStorage behaviour and D:\\AuditFirmStorage are preserved. Future compatible releases self-update securely from ERP.\n")
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def build_preconfigured_agent_zip(*, env_text: str, include_admin_readme: bool = False) -> bytes:
|
def build_preconfigured_agent_zip(*, env_text: str, include_admin_readme: bool = False) -> bytes:
|
||||||
"""Build branch-specific LSA4 tunnel-enabled ZIP.
|
return _build_zip(env_text=env_text, include_env=True, include_admin_readme=include_admin_readme)
|
||||||
|
|
||||||
Partner/Branch Admin packages intentionally hide roadmap/readme text files and
|
|
||||||
.env.example. The package still contains requirements.txt because pip needs it.
|
def build_agent_update_zip() -> bytes:
|
||||||
"""
|
# Update package deliberately excludes .env, .venv, data, logs and client files.
|
||||||
base = base64.b64decode(LSA4_ZIP_B64)
|
return _build_zip(env_text=None, include_env=False, include_admin_readme=False)
|
||||||
src = zipfile.ZipFile(io.BytesIO(base), "r")
|
|
||||||
buffer = io.BytesIO()
|
|
||||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as dst:
|
|
||||||
replacements = {
|
|
||||||
"run_agent.bat": (
|
|
||||||
"@echo off\r\n"
|
|
||||||
"setlocal\r\n"
|
|
||||||
"cd /d \"%~dp0\"\r\n"
|
|
||||||
"if not exist \".env\" (\r\n"
|
|
||||||
" echo ERROR: .env file is missing. Generate/download the configured agent package from ERP.\r\n"
|
|
||||||
" exit /b 1\r\n"
|
|
||||||
")\r\n"
|
|
||||||
"if not exist \".venv\\Scripts\\python.exe\" (\r\n"
|
|
||||||
" echo ERROR: Python virtual environment is missing. Run install_task_scheduler.bat first.\r\n"
|
|
||||||
" exit /b 1\r\n"
|
|
||||||
")\r\n"
|
|
||||||
"\".venv\\Scripts\\python.exe\" -m audit_storage_agent.main\r\n"
|
|
||||||
).encode("utf-8"),
|
|
||||||
"install_task_scheduler.bat": (
|
|
||||||
"@echo off\r\n"
|
|
||||||
"setlocal\r\n"
|
|
||||||
"cd /d \"%~dp0\"\r\n"
|
|
||||||
"if not exist \".env\" (\r\n"
|
|
||||||
" echo ERROR: .env file is missing. Generate/download the configured agent package from ERP.\r\n"
|
|
||||||
" pause\r\n"
|
|
||||||
" exit /b 1\r\n"
|
|
||||||
")\r\n"
|
|
||||||
"if not exist \".venv\\Scripts\\python.exe\" (\r\n"
|
|
||||||
" echo Creating virtual environment...\r\n"
|
|
||||||
" py -3 -m venv .venv\r\n"
|
|
||||||
" if errorlevel 1 (\r\n"
|
|
||||||
" echo ERROR: Unable to create the Python virtual environment.\r\n"
|
|
||||||
" pause\r\n"
|
|
||||||
" exit /b 1\r\n"
|
|
||||||
" )\r\n"
|
|
||||||
")\r\n"
|
|
||||||
"\".venv\\Scripts\\python.exe\" -m pip install --upgrade pip\r\n"
|
|
||||||
"if errorlevel 1 goto :install_failed\r\n"
|
|
||||||
"\".venv\\Scripts\\python.exe\" -m pip install -r requirements.txt\r\n"
|
|
||||||
"if errorlevel 1 goto :install_failed\r\n"
|
|
||||||
"set \"AGENT_DIR=%~dp0\"\r\n"
|
|
||||||
"set \"AGENT_PYTHON=%~dp0.venv\\Scripts\\python.exe\"\r\n"
|
|
||||||
"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; $taskName='AuditFirmStorageAgent'; $agentDir=$env:AGENT_DIR.TrimEnd('\\'); $python=$env:AGENT_PYTHON; $action=New-ScheduledTaskAction -Execute $python -Argument '-m audit_storage_agent.main' -WorkingDirectory $agentDir; $trigger=New-ScheduledTaskTrigger -AtStartup; $principal=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest; $settings=New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null; Start-ScheduledTask -TaskName $taskName\"\r\n"
|
|
||||||
"if errorlevel 1 (\r\n"
|
|
||||||
" echo ERROR: Task Scheduler installation failed. Run this file as Administrator.\r\n"
|
|
||||||
" pause\r\n"
|
|
||||||
" exit /b 1\r\n"
|
|
||||||
")\r\n"
|
|
||||||
"echo Audit Firm Storage Agent scheduled task installed and started successfully.\r\n"
|
|
||||||
"echo The agent now runs in the background; no command window needs to remain open.\r\n"
|
|
||||||
"pause\r\n"
|
|
||||||
"exit /b 0\r\n"
|
|
||||||
":install_failed\r\n"
|
|
||||||
"echo ERROR: Python dependency installation failed.\r\n"
|
|
||||||
"pause\r\n"
|
|
||||||
"exit /b 1\r\n"
|
|
||||||
).encode("utf-8"),
|
|
||||||
}
|
|
||||||
for info in src.infolist():
|
|
||||||
name = info.filename.replace("\\", "/")
|
|
||||||
lower = name.lower()
|
|
||||||
if name == ".env" or lower.endswith(".env.example"):
|
|
||||||
continue
|
|
||||||
if (not include_admin_readme) and lower.endswith(".txt") and lower != "requirements.txt":
|
|
||||||
continue
|
|
||||||
dst.writestr(info, replacements.get(name, src.read(info.filename)))
|
|
||||||
dst.writestr(".env", env_text)
|
|
||||||
if include_admin_readme:
|
|
||||||
dst.writestr(
|
|
||||||
"README_AUTO_SETUP.txt",
|
|
||||||
"This LSA4 tunnel-enabled Task Scheduler agent package was generated from Audit ERP Branch Storage Auto Setup.\n"
|
|
||||||
"The .env file is pre-filled for this branch storage node.\n\n"
|
|
||||||
"Recommended setup on branch storage computer:\n"
|
|
||||||
"1. Extract this ZIP to C:\\AuditFirmStorageAgent or any fixed folder.\n"
|
|
||||||
"2. Right-click Command Prompt / PowerShell and Run as Administrator.\n"
|
|
||||||
"3. Run check_config.bat.\n"
|
|
||||||
"4. Run install_task_scheduler.bat.\n"
|
|
||||||
"5. Check Documents -> Branch Storage Dashboard in ERP for Online/Tunnel status.\n"
|
|
||||||
)
|
|
||||||
return buffer.getvalue()
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d %~dp0
|
||||||
|
if not exist .venv\Scripts\python.exe (
|
||||||
|
py -3 -m venv .venv
|
||||||
|
)
|
||||||
|
call .venv\Scripts\activate.bat
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python check_config.py
|
||||||
|
pause
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from audit_storage_agent.config import load_config
|
||||||
|
from audit_storage_agent.client import ERPClient
|
||||||
|
from audit_storage_agent.logger import setup_logger
|
||||||
|
from audit_storage_agent.storage_identity import ensure_storage_identity
|
||||||
|
|
||||||
|
base_dir = Path(__file__).resolve().parent
|
||||||
|
logger = setup_logger(base_dir)
|
||||||
|
config = load_config(str(base_dir / ".env"))
|
||||||
|
logger.info("Configuration loaded successfully")
|
||||||
|
logger.info("ERP_BASE_URL=%s", config.erp_base_url)
|
||||||
|
logger.info("NODE_CODE=%s", config.node_code)
|
||||||
|
logger.info("STORAGE_ROOT=%s", config.storage_root)
|
||||||
|
logger.info("TENANT_ID=%s", config.tenant_id or "-")
|
||||||
|
logger.info("BRANCH_ID=%s", config.branch_id or "-")
|
||||||
|
identity_path = ensure_storage_identity(config)
|
||||||
|
logger.info("Storage identity file verified: %s", identity_path)
|
||||||
|
client = ERPClient(config)
|
||||||
|
response = client.heartbeat({"status": "CONFIG_TEST", "storage_root": str(config.storage_root), "agent_version": "0.2.0"})
|
||||||
|
logger.info("ERP heartbeat test response: %s", response)
|
||||||
|
print("Configuration and ERP connection test completed successfully.")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__version__ = "1.0.0"
|
||||||
|
AGENT_NAME = "ERP Local Agent"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .config import AgentConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ERPClient:
|
||||||
|
def __init__(self, config: AgentConfig):
|
||||||
|
self.config = config
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update(config.headers)
|
||||||
|
|
||||||
|
def _url(self, path: str) -> str:
|
||||||
|
return f"{self.config.erp_base_url}{path}"
|
||||||
|
|
||||||
|
def heartbeat(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
response = self.session.post(
|
||||||
|
self._url("/documents/storage-agent/heartbeat"),
|
||||||
|
json=payload,
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json() if response.content else {"status": "ok"}
|
||||||
|
|
||||||
|
def pending_storage_jobs(self) -> list[dict[str, Any]]:
|
||||||
|
response = self.session.get(
|
||||||
|
self._url("/documents/storage-agent/jobs/pending"),
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
return data.get("jobs", [])
|
||||||
|
|
||||||
|
def download_storage_job(self, job_id: int | str):
|
||||||
|
response = self.session.get(
|
||||||
|
self._url(f"/documents/storage-agent/jobs/{job_id}/download"),
|
||||||
|
stream=True,
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response
|
||||||
|
|
||||||
|
def acknowledge_storage_job(self, job_id: int | str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
response = self.session.post(
|
||||||
|
self._url(f"/documents/storage-agent/jobs/{job_id}/ack"),
|
||||||
|
json=payload,
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json() if response.content else {"status": "ok"}
|
||||||
|
|
||||||
|
def pending_download_requests(self) -> list[dict[str, Any]]:
|
||||||
|
response = self.session.get(
|
||||||
|
self._url("/documents/storage-agent/download-requests/pending"),
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
return data.get("requests", [])
|
||||||
|
|
||||||
|
def upload_download_request_file(self, request_id: int | str, file_path: Path, extra: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
with file_path.open("rb") as handle:
|
||||||
|
files = {"file": (file_path.name, handle, "application/octet-stream")}
|
||||||
|
data = {key: str(value) for key, value in extra.items() if value is not None}
|
||||||
|
response = self.session.post(
|
||||||
|
self._url(f"/documents/storage-agent/download-requests/{request_id}/upload"),
|
||||||
|
files=files,
|
||||||
|
data=data,
|
||||||
|
timeout=max(self.config.request_timeout_seconds, 300),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json() if response.content else {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def update_manifest(self) -> dict[str, Any]:
|
||||||
|
response = self.session.get(
|
||||||
|
self._url("/documents/erp-local-agent/update-manifest"),
|
||||||
|
timeout=self.config.request_timeout_seconds,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def download_update_package(self) -> bytes:
|
||||||
|
response = self.session.get(
|
||||||
|
self._url("/documents/erp-local-agent/update-package"),
|
||||||
|
timeout=max(self.config.request_timeout_seconds, 300),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.content
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AgentConfig:
|
||||||
|
erp_base_url: str
|
||||||
|
node_code: str
|
||||||
|
node_secret: str
|
||||||
|
storage_root: Path
|
||||||
|
tenant_id: str | None = None
|
||||||
|
branch_id: str | None = None
|
||||||
|
poll_interval_seconds: int = 30
|
||||||
|
download_request_interval_seconds: int = 30
|
||||||
|
heartbeat_interval_seconds: int = 60
|
||||||
|
request_timeout_seconds: int = 60
|
||||||
|
max_retries: int = 3
|
||||||
|
tunnel_enabled: bool = True
|
||||||
|
tunnel_reconnect_seconds: int = 10
|
||||||
|
auto_update: bool = True
|
||||||
|
update_check_interval_seconds: int = 300
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"X-Node-Code": self.node_code,
|
||||||
|
"X-Node-Secret": self.node_secret,
|
||||||
|
"User-Agent": f"ERPLocalAgent/{__version__}",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tunnel_url(self) -> str:
|
||||||
|
base = self.erp_base_url.rstrip("/")
|
||||||
|
if base.startswith("https://"):
|
||||||
|
ws_base = "wss://" + base[len("https://"):]
|
||||||
|
elif base.startswith("http://"):
|
||||||
|
ws_base = "ws://" + base[len("http://"):]
|
||||||
|
else:
|
||||||
|
ws_base = base
|
||||||
|
query = urlencode({"node_code": self.node_code, "node_secret": self.node_secret})
|
||||||
|
return f"{ws_base}/documents/storage-agent/tunnel?{query}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bool(name: str, default: bool) -> bool:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None or str(raw).strip() == "":
|
||||||
|
return default
|
||||||
|
return str(raw).strip().lower() in {"1", "true", "yes", "y", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_int(name: str, default: int) -> int:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None or str(raw).strip() == "":
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise RuntimeError(f"Invalid integer for {name}: {raw!r}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(env_file: str | None = None) -> AgentConfig:
|
||||||
|
if env_file:
|
||||||
|
load_dotenv(env_file)
|
||||||
|
else:
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
erp_base_url = os.getenv("ERP_BASE_URL", "").rstrip("/")
|
||||||
|
node_code = os.getenv("NODE_CODE", "").strip()
|
||||||
|
node_secret = os.getenv("NODE_SECRET", "").strip()
|
||||||
|
storage_root_raw = os.getenv("STORAGE_ROOT", "").strip()
|
||||||
|
tenant_id = os.getenv("TENANT_ID", os.getenv("AUDIT_FIRM_ID", "")).strip() or None
|
||||||
|
branch_id = os.getenv("BRANCH_ID", "").strip() or None
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
if not erp_base_url:
|
||||||
|
missing.append("ERP_BASE_URL")
|
||||||
|
if not node_code:
|
||||||
|
missing.append("NODE_CODE")
|
||||||
|
if not node_secret:
|
||||||
|
missing.append("NODE_SECRET")
|
||||||
|
if not storage_root_raw:
|
||||||
|
missing.append("STORAGE_ROOT")
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("Missing required configuration: " + ", ".join(missing))
|
||||||
|
|
||||||
|
storage_root = Path(storage_root_raw).expanduser().resolve()
|
||||||
|
storage_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return AgentConfig(
|
||||||
|
erp_base_url=erp_base_url,
|
||||||
|
node_code=node_code,
|
||||||
|
node_secret=node_secret,
|
||||||
|
storage_root=storage_root,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
branch_id=branch_id,
|
||||||
|
poll_interval_seconds=_get_int("POLL_INTERVAL_SECONDS", 30),
|
||||||
|
download_request_interval_seconds=_get_int("DOWNLOAD_REQUEST_INTERVAL_SECONDS", 30),
|
||||||
|
heartbeat_interval_seconds=_get_int("HEARTBEAT_INTERVAL_SECONDS", 60),
|
||||||
|
request_timeout_seconds=_get_int("REQUEST_TIMEOUT_SECONDS", 60),
|
||||||
|
max_retries=_get_int("MAX_RETRIES", 3),
|
||||||
|
tunnel_enabled=_get_bool("TUNNEL_ENABLED", True),
|
||||||
|
tunnel_reconnect_seconds=_get_int("TUNNEL_RECONNECT_SECONDS", 10),
|
||||||
|
auto_update=_get_bool("AUTO_UPDATE", True),
|
||||||
|
update_check_interval_seconds=max(60, _get_int("UPDATE_CHECK_INTERVAL_SECONDS", 300)),
|
||||||
|
)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
class LocalDB:
|
||||||
|
def __init__(self, db_path: Path):
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _init(self) -> None:
|
||||||
|
with self.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS processed_storage_jobs (
|
||||||
|
job_id TEXT PRIMARY KEY,
|
||||||
|
local_path TEXT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
file_size INTEGER NOT NULL,
|
||||||
|
processed_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS processed_download_requests (
|
||||||
|
request_id TEXT PRIMARY KEY,
|
||||||
|
local_path TEXT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
file_size INTEGER NOT NULL,
|
||||||
|
processed_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def record_storage_job(self, job_id: str, local_path: str, sha256: str, file_size: int) -> None:
|
||||||
|
with self.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO processed_storage_jobs
|
||||||
|
(job_id, local_path, sha256, file_size, processed_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(str(job_id), local_path, sha256, int(file_size), datetime.now(timezone.utc).isoformat()),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def record_download_request(self, request_id: str, local_path: str, sha256: str, file_size: int) -> None:
|
||||||
|
with self.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO processed_download_requests
|
||||||
|
(request_id, local_path, sha256, file_size, processed_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(str(request_id), local_path, sha256, int(file_size), datetime.now(timezone.utc).isoformat()),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger(base_dir: Path | None = None) -> logging.Logger:
|
||||||
|
root = base_dir or Path.cwd()
|
||||||
|
logs_dir = root / "logs"
|
||||||
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
logger = logging.getLogger("audit_storage_agent")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
||||||
|
|
||||||
|
console = logging.StreamHandler()
|
||||||
|
console.setFormatter(formatter)
|
||||||
|
logger.addHandler(console)
|
||||||
|
|
||||||
|
file_handler = RotatingFileHandler(logs_dir / "agent.log", maxBytes=2_000_000, backupCount=5, encoding="utf-8")
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .client import ERPClient
|
||||||
|
from .config import load_config
|
||||||
|
from .db import LocalDB
|
||||||
|
from .logger import setup_logger
|
||||||
|
from .sync import StorageAgent
|
||||||
|
from .tunnel import StorageAgentTunnel
|
||||||
|
from .updater import AgentUpdater
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="ERP Local Agent - storage and local ERP tool runtime")
|
||||||
|
parser.add_argument("--env", default=None)
|
||||||
|
parser.add_argument("--once", action="store_true")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = build_parser().parse_args()
|
||||||
|
root = Path.cwd()
|
||||||
|
logger = setup_logger(root)
|
||||||
|
try:
|
||||||
|
config = load_config(args.env)
|
||||||
|
db = LocalDB(root / "data" / "agent.db")
|
||||||
|
client = ERPClient(config)
|
||||||
|
agent = StorageAgent(config, client, db, logger)
|
||||||
|
updater = AgentUpdater(config, client, logger, root)
|
||||||
|
if args.once:
|
||||||
|
updater.maybe_update(force=True)
|
||||||
|
agent.run_once()
|
||||||
|
return 0
|
||||||
|
def update_worker():
|
||||||
|
while True:
|
||||||
|
updater.maybe_update()
|
||||||
|
time.sleep(max(60, config.update_check_interval_seconds))
|
||||||
|
threading.Thread(target=update_worker, name="erp-local-agent-updater", daemon=True).start()
|
||||||
|
if config.tunnel_enabled:
|
||||||
|
asyncio.run(StorageAgentTunnel(agent).run_forever())
|
||||||
|
else:
|
||||||
|
agent.run_forever()
|
||||||
|
return 0
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("ERP Local Agent stopped by user")
|
||||||
|
return 0
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("ERP Local Agent failed: %s", exc)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
_SAFE_SEGMENT = re.compile(r"[^A-Za-z0-9._ -]+")
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_segment(value: str, fallback: str = "item") -> str:
|
||||||
|
value = (value or "").strip().replace("/", "_").replace("\\", "_")
|
||||||
|
value = _SAFE_SEGMENT.sub("_", value)
|
||||||
|
value = value.strip(" ._")
|
||||||
|
return value or fallback
|
||||||
|
|
||||||
|
|
||||||
|
def safe_relative_path(raw_path: str | None, fallback_filename: str) -> Path:
|
||||||
|
"""Return a safe relative path, preventing path traversal outside storage root."""
|
||||||
|
if raw_path:
|
||||||
|
parts = []
|
||||||
|
for part in Path(str(raw_path).replace("\\", "/")).parts:
|
||||||
|
if part in ("", ".", ".."):
|
||||||
|
continue
|
||||||
|
parts.append(sanitize_segment(part))
|
||||||
|
if parts:
|
||||||
|
return Path(*parts)
|
||||||
|
return Path(sanitize_segment(fallback_filename, "document.bin"))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_under_root(storage_root: Path, relative_path: Path) -> Path:
|
||||||
|
target = (storage_root / relative_path).resolve()
|
||||||
|
root = storage_root.resolve()
|
||||||
|
if os.path.commonpath([str(root), str(target)]) != str(root):
|
||||||
|
raise RuntimeError("Refusing to write/read outside storage root")
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_stream_to_file(stream: BinaryIO, destination: Path) -> tuple[int, str]:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
total = 0
|
||||||
|
tmp = destination.with_suffix(destination.suffix + ".part")
|
||||||
|
with tmp.open("wb") as out:
|
||||||
|
while True:
|
||||||
|
chunk = stream.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
out.write(chunk)
|
||||||
|
digest.update(chunk)
|
||||||
|
total += len(chunk)
|
||||||
|
tmp.replace(destination)
|
||||||
|
return total, digest.hexdigest()
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
IDENTITY_FILENAME = ".audit_storage_node.json"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class StorageIdentity:
|
||||||
|
erp_base_url: str
|
||||||
|
node_code: str
|
||||||
|
tenant_id: str | None
|
||||||
|
branch_id: str | None
|
||||||
|
storage_root: str
|
||||||
|
agent_identity_version: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise(value: Any) -> str:
|
||||||
|
return str(value or "").strip().rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_optional(value: Any) -> str | None:
|
||||||
|
value = str(value or "").strip()
|
||||||
|
return value or None
|
||||||
|
|
||||||
|
|
||||||
|
def identity_path(storage_root: Path) -> Path:
|
||||||
|
return storage_root / IDENTITY_FILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def build_identity(config) -> StorageIdentity:
|
||||||
|
return StorageIdentity(
|
||||||
|
erp_base_url=_normalise(config.erp_base_url),
|
||||||
|
node_code=_normalise(config.node_code),
|
||||||
|
tenant_id=_normalise_optional(getattr(config, "tenant_id", None)),
|
||||||
|
branch_id=_normalise_optional(getattr(config, "branch_id", None)),
|
||||||
|
storage_root=str(config.storage_root.resolve()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_storage_identity(config, *, allow_initialise: bool = True) -> Path:
|
||||||
|
"""Create/validate one identity file in the selected storage root.
|
||||||
|
|
||||||
|
This protects the branch storage folder from accidentally being reused by a
|
||||||
|
different ERP node/branch, and gives the branch one stable storage root.
|
||||||
|
It does not scan the whole machine; ERP still remains the final authority
|
||||||
|
for enforcing one active node and one configured root per branch.
|
||||||
|
"""
|
||||||
|
root = config.storage_root.resolve()
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = identity_path(root)
|
||||||
|
expected = build_identity(config)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
if not allow_initialise:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Storage identity file is missing at {path}. Initialise the branch storage root first."
|
||||||
|
)
|
||||||
|
path.write_text(json.dumps(asdict(expected), indent=2), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"Storage identity file is unreadable/corrupt: {path}") from exc
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
"erp_base_url": expected.erp_base_url,
|
||||||
|
"node_code": expected.node_code,
|
||||||
|
"tenant_id": expected.tenant_id,
|
||||||
|
"branch_id": expected.branch_id,
|
||||||
|
}
|
||||||
|
conflicts: list[str] = []
|
||||||
|
for key, expected_value in checks.items():
|
||||||
|
existing_value = existing.get(key)
|
||||||
|
# Older packages may not have tenant_id/branch_id. Allow blank existing
|
||||||
|
# values to be upgraded below, but never allow contradictory values.
|
||||||
|
if key in {"tenant_id", "branch_id"} and not existing_value:
|
||||||
|
continue
|
||||||
|
if str(existing_value or "").strip().rstrip("/") != str(expected_value or "").strip().rstrip("/"):
|
||||||
|
conflicts.append(f"{key}: existing={existing_value!r}, expected={expected_value!r}")
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
raise RuntimeError(
|
||||||
|
"This storage folder is already linked to a different ERP storage node/branch. "
|
||||||
|
"Use the existing branch storage package or reset/change the storage root from ERP. "
|
||||||
|
+ "; ".join(conflicts)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backfill missing tenant/branch fields and refresh resolved root path.
|
||||||
|
changed = False
|
||||||
|
updated = dict(existing)
|
||||||
|
for key, value in asdict(expected).items():
|
||||||
|
if updated.get(key) in (None, "") and value not in (None, ""):
|
||||||
|
updated[key] = value
|
||||||
|
changed = True
|
||||||
|
if updated.get("storage_root") != expected.storage_root:
|
||||||
|
updated["storage_root"] = expected.storage_root
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
path.write_text(json.dumps(updated, indent=2), encoding="utf-8")
|
||||||
|
return path
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .client import ERPClient
|
||||||
|
from .config import AgentConfig
|
||||||
|
from .db import LocalDB
|
||||||
|
from .storage import resolve_under_root, safe_relative_path, sha256_file, sanitize_segment, write_stream_to_file
|
||||||
|
from .storage_identity import ensure_storage_identity
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class StorageAgent:
|
||||||
|
def __init__(self, config: AgentConfig, client: ERPClient, db: LocalDB, logger):
|
||||||
|
self.config = config
|
||||||
|
self.client = client
|
||||||
|
self.db = db
|
||||||
|
self.logger = logger
|
||||||
|
self.last_heartbeat = 0.0
|
||||||
|
self.identity_file = ensure_storage_identity(config)
|
||||||
|
self.logger.info("Storage root identity verified: %s", self.identity_file)
|
||||||
|
|
||||||
|
def run_forever(self) -> None:
|
||||||
|
self.logger.info("Starting ERP Local Agent for node=%s", self.config.node_code)
|
||||||
|
while True:
|
||||||
|
self.run_once()
|
||||||
|
time.sleep(max(5, self.config.poll_interval_seconds))
|
||||||
|
|
||||||
|
def run_once(self) -> None:
|
||||||
|
self._maybe_heartbeat()
|
||||||
|
self.process_storage_jobs()
|
||||||
|
self.process_download_requests()
|
||||||
|
|
||||||
|
def _maybe_heartbeat(self, force: bool = False) -> None:
|
||||||
|
now = time.time()
|
||||||
|
if not force and now - self.last_heartbeat < self.config.heartbeat_interval_seconds:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
total, used, free = shutil.disk_usage(self.config.storage_root)
|
||||||
|
payload = {
|
||||||
|
"status": "ONLINE",
|
||||||
|
"storage_root": str(self.config.storage_root),
|
||||||
|
"total_bytes": total,
|
||||||
|
"used_bytes": used,
|
||||||
|
"free_bytes": free,
|
||||||
|
"agent_version": __version__,
|
||||||
|
"agent_name": "ERP Local Agent",
|
||||||
|
"capabilities": ["storage"],
|
||||||
|
"agent_time_utc": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
self.client.heartbeat(payload)
|
||||||
|
self.last_heartbeat = now
|
||||||
|
self.logger.info("Heartbeat sent. free_gb=%.2f", free / (1024 ** 3))
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Heartbeat failed: %s", exc)
|
||||||
|
|
||||||
|
def process_storage_jobs(self) -> None:
|
||||||
|
try:
|
||||||
|
jobs = self.client.pending_storage_jobs()
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Could not fetch pending storage jobs: %s", exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.process_storage_jobs_from_payload(jobs)
|
||||||
|
|
||||||
|
def process_storage_jobs_from_payload(self, jobs: list[dict[str, Any]]) -> None:
|
||||||
|
if not jobs:
|
||||||
|
return
|
||||||
|
self.logger.info("Found %s pending storage job(s)", len(jobs))
|
||||||
|
for job in jobs:
|
||||||
|
try:
|
||||||
|
self._process_storage_job(job)
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Storage job failed job=%s error=%s", job.get("id") or job.get("job_id"), exc)
|
||||||
|
|
||||||
|
def _process_storage_job(self, job: dict[str, Any]) -> None:
|
||||||
|
job_id = job.get("id") or job.get("job_id")
|
||||||
|
if job_id is None:
|
||||||
|
raise RuntimeError(f"Storage job missing id: {job}")
|
||||||
|
|
||||||
|
original_filename = job.get("original_filename") or job.get("filename") or f"job_{job_id}.bin"
|
||||||
|
relative_path_raw = job.get("local_relative_path") or job.get("relative_path")
|
||||||
|
relative_path = safe_relative_path(relative_path_raw, original_filename)
|
||||||
|
destination = resolve_under_root(self.config.storage_root, relative_path)
|
||||||
|
|
||||||
|
self.logger.info("Downloading storage job=%s to %s", job_id, destination)
|
||||||
|
response = self.client.download_storage_job(job_id)
|
||||||
|
file_size, digest = write_stream_to_file(response.raw, destination)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"status": "COMPLETED",
|
||||||
|
"sha256_hash": digest,
|
||||||
|
"file_hash": digest,
|
||||||
|
"file_size": file_size,
|
||||||
|
"local_relative_path": str(relative_path).replace("\\", "/"),
|
||||||
|
"stored_at_utc": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
self.client.acknowledge_storage_job(job_id, payload)
|
||||||
|
self.db.record_storage_job(str(job_id), str(relative_path).replace("\\", "/"), digest, file_size)
|
||||||
|
self.logger.info("Storage job completed job=%s size=%s sha256=%s", job_id, file_size, digest)
|
||||||
|
|
||||||
|
def process_download_requests(self) -> None:
|
||||||
|
try:
|
||||||
|
requests_ = self.client.pending_download_requests()
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Could not fetch pending download requests: %s", exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.process_download_requests_from_payload(requests_)
|
||||||
|
|
||||||
|
def process_download_requests_from_payload(self, requests_: list[dict[str, Any]]) -> None:
|
||||||
|
if not requests_:
|
||||||
|
return
|
||||||
|
self.logger.info("Found %s pending download request(s)", len(requests_))
|
||||||
|
for request in requests_:
|
||||||
|
try:
|
||||||
|
self._process_download_request(request)
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Download request failed request=%s error=%s", request.get("id") or request.get("request_id"), exc)
|
||||||
|
|
||||||
|
def _process_download_request(self, request: dict[str, Any]) -> None:
|
||||||
|
request_id = request.get("id") or request.get("request_id")
|
||||||
|
if request_id is None:
|
||||||
|
raise RuntimeError(f"Download request missing id: {request}")
|
||||||
|
|
||||||
|
relative_path_raw = request.get("local_relative_path") or request.get("relative_path")
|
||||||
|
filename = request.get("original_filename") or request.get("filename") or f"download_request_{request_id}.bin"
|
||||||
|
relative_path = safe_relative_path(relative_path_raw, filename)
|
||||||
|
local_file = resolve_under_root(self.config.storage_root, relative_path)
|
||||||
|
if not local_file.exists() or not local_file.is_file():
|
||||||
|
raise FileNotFoundError(f"Requested file is not found locally: {local_file}")
|
||||||
|
|
||||||
|
file_size = local_file.stat().st_size
|
||||||
|
digest = sha256_file(local_file)
|
||||||
|
expected_hash = request.get("sha256_hash") or request.get("file_hash")
|
||||||
|
if expected_hash and str(expected_hash).lower() != digest.lower():
|
||||||
|
raise RuntimeError(f"SHA256 mismatch before upload. expected={expected_hash} actual={digest}")
|
||||||
|
|
||||||
|
self.logger.info("Uploading file for download request=%s path=%s", request_id, local_file)
|
||||||
|
extra = {
|
||||||
|
"sha256_hash": digest,
|
||||||
|
"file_hash": digest,
|
||||||
|
"file_size": file_size,
|
||||||
|
"local_relative_path": str(relative_path).replace("\\", "/"),
|
||||||
|
}
|
||||||
|
self.client.upload_download_request_file(request_id, local_file, extra)
|
||||||
|
self.db.record_download_request(str(request_id), str(relative_path).replace("\\", "/"), digest, file_size)
|
||||||
|
self.logger.info("Download request fulfilled request=%s size=%s sha256=%s", request_id, file_size, digest)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
from .sync import StorageAgent
|
||||||
|
|
||||||
|
|
||||||
|
class StorageAgentTunnel:
|
||||||
|
def __init__(self, agent: StorageAgent):
|
||||||
|
self.agent = agent
|
||||||
|
self.config = agent.config
|
||||||
|
self.logger = agent.logger
|
||||||
|
|
||||||
|
async def run_forever(self) -> None:
|
||||||
|
self.logger.info("Starting tunnel mode for node=%s", self.config.node_code)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self._connect_once()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("Tunnel disconnected/error: %s", exc)
|
||||||
|
await asyncio.sleep(max(5, self.config.tunnel_reconnect_seconds))
|
||||||
|
|
||||||
|
async def _connect_once(self) -> None:
|
||||||
|
async with websockets.connect(
|
||||||
|
self.config.tunnel_url,
|
||||||
|
ping_interval=30,
|
||||||
|
ping_timeout=30,
|
||||||
|
close_timeout=10,
|
||||||
|
max_size=1024 * 1024,
|
||||||
|
) as websocket:
|
||||||
|
await websocket.send(self._json_text({"type": "ready", "agent_time_utc": self._now()}))
|
||||||
|
self.agent._maybe_heartbeat(force=True)
|
||||||
|
async for raw in websocket:
|
||||||
|
message = self._parse_json(raw)
|
||||||
|
if not isinstance(message, dict):
|
||||||
|
continue
|
||||||
|
msg_type = message.get("type")
|
||||||
|
if msg_type == "connected":
|
||||||
|
self.logger.info("Tunnel connected: %s", message.get("node_code"))
|
||||||
|
continue
|
||||||
|
if msg_type == "sync":
|
||||||
|
jobs = message.get("jobs") or []
|
||||||
|
requests_ = message.get("download_requests") or message.get("requests") or []
|
||||||
|
await asyncio.to_thread(self.agent.process_storage_jobs_from_payload, jobs)
|
||||||
|
await asyncio.to_thread(self.agent.process_download_requests_from_payload, requests_)
|
||||||
|
await websocket.send(self._json_text({
|
||||||
|
"type": "agent_status",
|
||||||
|
"jobs_seen": len(jobs),
|
||||||
|
"requests_seen": len(requests_),
|
||||||
|
"agent_time_utc": self._now(),
|
||||||
|
}))
|
||||||
|
continue
|
||||||
|
if msg_type == "error":
|
||||||
|
self.logger.error("Tunnel server error: %s", message.get("error"))
|
||||||
|
|
||||||
|
def _now(self) -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
def _json_text(self, payload: dict[str, Any]) -> str:
|
||||||
|
import json
|
||||||
|
return json.dumps(payload, separators=(",", ":"))
|
||||||
|
|
||||||
|
def _parse_json(self, raw: str) -> Any:
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class AgentUpdater:
|
||||||
|
def __init__(self, config, client, logger, install_dir: Path):
|
||||||
|
self.config = config
|
||||||
|
self.client = client
|
||||||
|
self.logger = logger
|
||||||
|
self.install_dir = install_dir.resolve()
|
||||||
|
self.last_check = 0.0
|
||||||
|
self.busy = False
|
||||||
|
|
||||||
|
def maybe_update(self, force: bool = False) -> bool:
|
||||||
|
if not self.config.auto_update or self.busy:
|
||||||
|
return False
|
||||||
|
now = time.time()
|
||||||
|
if not force and now - self.last_check < self.config.update_check_interval_seconds:
|
||||||
|
return False
|
||||||
|
self.last_check = now
|
||||||
|
try:
|
||||||
|
manifest = self.client.update_manifest()
|
||||||
|
latest = str(manifest.get("latest_version") or "").strip()
|
||||||
|
if not latest or latest == __version__:
|
||||||
|
return False
|
||||||
|
expected = str(manifest.get("sha256") or "").lower().strip()
|
||||||
|
package = self.client.download_update_package()
|
||||||
|
actual = hashlib.sha256(package).hexdigest().lower()
|
||||||
|
if len(expected) != 64 or actual != expected:
|
||||||
|
raise RuntimeError("ERP Local Agent update package SHA256 verification failed.")
|
||||||
|
self._stage_and_restart(latest, package)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.exception("ERP Local Agent update check failed: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _stage_and_restart(self, latest: str, package: bytes) -> None:
|
||||||
|
self.busy = True
|
||||||
|
updates = self.install_dir / "updates"
|
||||||
|
updates.mkdir(parents=True, exist_ok=True)
|
||||||
|
safe = "".join(ch for ch in latest if ch.isalnum() or ch in ".-_") or "update"
|
||||||
|
package_path = updates / f"ERP_Local_Agent_{safe}.zip"
|
||||||
|
package_path.write_bytes(package)
|
||||||
|
staged = updates / f"staged_{safe}"
|
||||||
|
shutil.rmtree(staged, ignore_errors=True)
|
||||||
|
staged.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(package_path, "r") as archive:
|
||||||
|
archive.extractall(staged)
|
||||||
|
if not (staged / "erp_local_agent" / "__init__.py").exists():
|
||||||
|
raise RuntimeError("ERP Local Agent update package is incomplete.")
|
||||||
|
script_path = updates / f"apply_{safe}.ps1"
|
||||||
|
script_path.write_text(self._powershell_update_script(staged), encoding="utf-8")
|
||||||
|
flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0)
|
||||||
|
subprocess.Popen(
|
||||||
|
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script_path), "-ParentPid", str(os.getpid())],
|
||||||
|
cwd=str(self.install_dir), creationflags=flags, close_fds=True,
|
||||||
|
)
|
||||||
|
self.logger.warning("ERP Local Agent update %s staged; restarting.", latest)
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def _powershell_update_script(self, staged: Path) -> str:
|
||||||
|
install = str(self.install_dir).replace("'", "''")
|
||||||
|
stage = str(staged.resolve()).replace("'", "''")
|
||||||
|
lines = [
|
||||||
|
"param([int]$ParentPid)",
|
||||||
|
'$ErrorActionPreference = "Stop"',
|
||||||
|
"$InstallDir = '" + install + "'",
|
||||||
|
"$StagedDir = '" + stage + "'",
|
||||||
|
"$TaskName = 'ERP Local Agent'",
|
||||||
|
"$BackupDir = Join-Path $InstallDir ('updates\\backup_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))",
|
||||||
|
"try {",
|
||||||
|
" if ($ParentPid -gt 0) { try { Wait-Process -Id $ParentPid -Timeout 120 -ErrorAction SilentlyContinue } catch {} }",
|
||||||
|
" try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch {}",
|
||||||
|
" New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null",
|
||||||
|
" if (Test-Path (Join-Path $InstallDir 'erp_local_agent')) { Copy-Item (Join-Path $InstallDir 'erp_local_agent') -Destination $BackupDir -Recurse -Force }",
|
||||||
|
" Remove-Item (Join-Path $InstallDir 'erp_local_agent') -Recurse -Force -ErrorAction SilentlyContinue",
|
||||||
|
" Copy-Item (Join-Path $StagedDir 'erp_local_agent') -Destination $InstallDir -Recurse -Force",
|
||||||
|
" foreach ($Item in @('requirements.txt','run_agent.bat','run_once.bat','check_config.py','check_config.bat','start_task_scheduler.bat','stop_task_scheduler.bat','status_task_scheduler.bat','install_task_scheduler.bat','uninstall_task_scheduler.bat')) { $Source=Join-Path $StagedDir $Item; if (Test-Path $Source) { Copy-Item $Source -Destination (Join-Path $InstallDir $Item) -Force } }",
|
||||||
|
" $Python = Join-Path $InstallDir '.venv\\Scripts\\python.exe'",
|
||||||
|
" if (-not (Test-Path $Python)) { throw 'ERP Local Agent virtual environment is missing.' }",
|
||||||
|
" & $Python -m pip install -r (Join-Path $InstallDir 'requirements.txt')",
|
||||||
|
" if ($LASTEXITCODE -ne 0) { throw 'Dependency update failed.' }",
|
||||||
|
" Start-ScheduledTask -TaskName $TaskName",
|
||||||
|
"} catch {",
|
||||||
|
" try { if (Test-Path (Join-Path $BackupDir 'erp_local_agent')) { Remove-Item (Join-Path $InstallDir 'erp_local_agent') -Recurse -Force -ErrorAction SilentlyContinue; Copy-Item (Join-Path $BackupDir 'erp_local_agent') -Destination $InstallDir -Recurse -Force }; Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch {}",
|
||||||
|
" New-Item -ItemType Directory -Force -Path (Join-Path $InstallDir 'logs') | Out-Null",
|
||||||
|
" Add-Content -Path (Join-Path $InstallDir 'logs\\update_error.log') -Value ((Get-Date).ToString('s') + ' ' + $_.Exception.Message)",
|
||||||
|
" exit 1",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
return "\r\n".join(lines) + "\r\n"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
if not exist ".env" ( echo ERROR: .env file is missing. Download the configured ERP Local Agent package from ERP. & pause & exit /b 1 )
|
||||||
|
if not exist ".venv\Scripts\python.exe" ( py -3 -m venv .venv & if errorlevel 1 ( echo ERROR: Unable to create Python virtual environment. & pause & exit /b 1 ) )
|
||||||
|
".venv\Scripts\python.exe" -m pip install --upgrade pip
|
||||||
|
if errorlevel 1 goto :failed
|
||||||
|
".venv\Scripts\python.exe" -m pip install -r requirements.txt
|
||||||
|
if errorlevel 1 goto :failed
|
||||||
|
set "AGENT_DIR=%~dp0"
|
||||||
|
set "AGENT_PYTHON=%~dp0.venv\Scripts\python.exe"
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; try { Stop-ScheduledTask -TaskName 'AuditFirmStorageAgent' -ErrorAction SilentlyContinue } catch {}; try { Unregister-ScheduledTask -TaskName 'AuditFirmStorageAgent' -Confirm:$false -ErrorAction SilentlyContinue } catch {}; $d=$env:AGENT_DIR.TrimEnd('\'); $p=$env:AGENT_PYTHON; $a=New-ScheduledTaskAction -Execute $p -Argument '-m erp_local_agent.main' -WorkingDirectory $d; $t=New-ScheduledTaskTrigger -AtStartup; $pr=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest; $s=New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries; Register-ScheduledTask -TaskName 'ERP Local Agent' -Action $a -Trigger $t -Principal $pr -Settings $s -Force | Out-Null; Start-ScheduledTask -TaskName 'ERP Local Agent'"
|
||||||
|
if errorlevel 1 goto :failed
|
||||||
|
echo ERP Local Agent installed and started successfully.
|
||||||
|
echo Future compatible releases will update automatically from ERP.
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
|
:failed
|
||||||
|
echo ERROR: ERP Local Agent installation failed. Run this file as Administrator.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
requests==2.32.3
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
websockets==12.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
".venv\Scripts\python.exe" -m erp_local_agent.main
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
".venv\Scripts\python.exe" -m erp_local_agent.main --once
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-ScheduledTask -TaskName 'ERP Local Agent'"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Get-ScheduledTask -TaskName 'ERP Local Agent' | Get-ScheduledTaskInfo | Format-List *"
|
||||||
|
pause
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Stop-ScheduledTask -TaskName 'ERP Local Agent'"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Unregister-ScheduledTask -TaskName 'ERP Local Agent' -Confirm:$false -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName 'AuditFirmStorageAgent' -Confirm:$false -ErrorAction SilentlyContinue"
|
||||||
|
pause
|
||||||
@@ -3,9 +3,10 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-slate-900">Branch Local Storage</h1>
|
<h1 class="text-2xl font-bold text-slate-900">ERP Local Agent</h1>
|
||||||
<p class="text-sm text-slate-500">Partner/branch controlled storage setup for engagement document streaming.</p>
|
<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>
|
||||||
<p class="text-xs text-slate-400 mt-1">Scope: {{ storage_scope_title or '-' }}</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>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Back to Documents</a>
|
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Back to Documents</a>
|
||||||
@@ -41,13 +42,13 @@
|
|||||||
|
|
||||||
{% if request.query_params.get('created') == '1' %}
|
{% if request.query_params.get('created') == '1' %}
|
||||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||||
Storage node created. Use the <strong>Download Agent Package</strong> button in the node row to generate the branch package. Secret and standalone .env are not displayed.
|
Storage node created. Use the <strong>Download ERP Local Agent</strong> button in the node row to generate the branch package. Secret and standalone .env are not displayed.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if request.query_params.get('error') == 'env_download_disabled' %}
|
{% if request.query_params.get('error') == 'env_download_disabled' %}
|
||||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||||
Standalone .env download is disabled for security. Download the pre-configured agent ZIP instead.
|
Standalone .env download is disabled for security. Download the pre-configured ERP Local Agent ZIP instead.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" value="D:\AuditFirmStorage"><p class="text-xs text-amber-700 mt-1">For a new branch setup only. Existing branch nodes keep their already configured root.</p></div>
|
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" value="D:\AuditFirmStorage"><p class="text-xs text-amber-700 mt-1">For a new branch setup only. Existing branch nodes keep their already configured root.</p></div>
|
||||||
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
|
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
|
||||||
<button class="w-full rounded-xl bg-emerald-700 text-white py-2 font-semibold">Download / Regenerate Branch Agent Package</button>
|
<button class="w-full rounded-xl bg-emerald-700 text-white py-2 font-semibold">Download / Regenerate ERP Local Agent</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" action="/documents/storage-nodes" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
|
<form method="post" action="/documents/storage-nodes" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
|
||||||
@@ -123,7 +124,7 @@
|
|||||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||||
<div class="p-4 border-b flex items-center justify-between">
|
<div class="p-4 border-b flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold text-slate-900">Configured storage nodes</h2>
|
<h2 class="font-bold text-slate-900">Configured ERP Local 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
|
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
|
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
|
||||||
<button class="text-sm text-emerald-700 font-semibold">Download Agent Package</button>
|
<button class="text-sm text-emerald-700 font-semibold">Download ERP Local Agent</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/toggle">
|
<form method="post" action="/documents/storage-nodes/{{ node.id }}/toggle">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -80,7 +80,7 @@ from app.modules.documents.services import (
|
|||||||
from app.modules.services.models import ClientServiceSubscription
|
from app.modules.services.models import ClientServiceSubscription
|
||||||
from app.modules.core.tenancy.models import Branch, Tenant
|
from app.modules.core.tenancy.models import Branch, Tenant
|
||||||
from app.modules.core.tenancy.year_control import is_row_financial_year_locked
|
from app.modules.core.tenancy.year_control import is_row_financial_year_locked
|
||||||
from app.modules.documents.agent_package import build_agent_env, build_preconfigured_agent_zip
|
from app.modules.documents.agent_package import ERP_LOCAL_AGENT_VERSION, build_agent_env, build_preconfigured_agent_zip, build_agent_update_zip
|
||||||
from app.modules.documents.models import BranchStorageNode
|
from app.modules.documents.models import BranchStorageNode
|
||||||
|
|
||||||
from app.modules.services.task_documents import (
|
from app.modules.services.task_documents import (
|
||||||
@@ -1073,7 +1073,7 @@ def _default_node_name(db, tenant_id: int, branch_id: int | None) -> str:
|
|||||||
|
|
||||||
def _agent_download_filename(node_code: str, suffix: str) -> str:
|
def _agent_download_filename(node_code: str, suffix: str) -> str:
|
||||||
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", node_code or "storage_node")
|
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", node_code or "storage_node")
|
||||||
return f"AuditFirmStorageAgent_{safe}{suffix}"
|
return f"ERPLocalAgent_{safe}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def _find_existing_storage_node(db, tenant_id: int, branch_id: int | None) -> BranchStorageNode | None:
|
def _find_existing_storage_node(db, tenant_id: int, branch_id: int | None) -> BranchStorageNode | None:
|
||||||
@@ -1226,7 +1226,7 @@ def _branch_name_map(db, branches=None):
|
|||||||
|
|
||||||
def _storage_scope_title(user, scope) -> str:
|
def _storage_scope_title(user, scope) -> str:
|
||||||
if scope.is_system_admin:
|
if scope.is_system_admin:
|
||||||
return "All audit firms — monitoring only"
|
return "All audit firms — monitoring only"
|
||||||
if scope.is_firm_admin:
|
if scope.is_firm_admin:
|
||||||
return "All branches of your audit firm"
|
return "All branches of your audit firm"
|
||||||
if _is_partner_branch_storage_scope(scope):
|
if _is_partner_branch_storage_scope(scope):
|
||||||
@@ -1282,6 +1282,7 @@ def storage_nodes(request: Request):
|
|||||||
recent_download_requests=requests,
|
recent_download_requests=requests,
|
||||||
generated_secret=None,
|
generated_secret=None,
|
||||||
storage_scope_title=_storage_scope_title(user, scope),
|
storage_scope_title=_storage_scope_title(user, scope),
|
||||||
|
erp_local_agent_version=ERP_LOCAL_AGENT_VERSION,
|
||||||
can_manage_branch_storage=_can_manage_branch_storage(scope),
|
can_manage_branch_storage=_can_manage_branch_storage(scope),
|
||||||
forced_branch_id=branch_filter,
|
forced_branch_id=branch_filter,
|
||||||
)
|
)
|
||||||
@@ -1575,6 +1576,33 @@ def _agent_auth(db, request: Request, x_node_code: str | None, x_node_secret: st
|
|||||||
return node, None
|
return node, None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/erp-local-agent/update-manifest")
|
||||||
|
def erp_local_agent_update_manifest(request: Request, x_node_code: str | None = Header(None), x_node_secret: str | None = Header(None)):
|
||||||
|
import hashlib
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
node, error = _agent_auth(db, request, x_node_code, x_node_secret)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
package = build_agent_update_zip()
|
||||||
|
db.commit()
|
||||||
|
return {"ok": True, "agent_name": "ERP Local Agent", "latest_version": ERP_LOCAL_AGENT_VERSION, "sha256": hashlib.sha256(package).hexdigest(), "size_bytes": len(package), "download_url": "/documents/erp-local-agent/update-package", "channel": "stable"}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@router.get("/erp-local-agent/update-package")
|
||||||
|
def erp_local_agent_update_package(request: Request, x_node_code: str | None = Header(None), x_node_secret: str | None = Header(None)):
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
node, error = _agent_auth(db, request, x_node_code, x_node_secret)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
package = build_agent_update_zip()
|
||||||
|
db.commit()
|
||||||
|
filename = f"ERP_Local_Agent_{ERP_LOCAL_AGENT_VERSION}.zip"
|
||||||
|
return Response(package, media_type="application/zip", headers={"Content-Disposition": f'attachment; filename="{filename}"'})
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@router.post("/storage-agent/heartbeat")
|
@router.post("/storage-agent/heartbeat")
|
||||||
async def storage_agent_heartbeat(request: Request, x_node_code: str | None = Header(None), x_node_secret: str | None = Header(None)):
|
async def storage_agent_heartbeat(request: Request, x_node_code: str | None = Header(None), x_node_secret: str | None = Header(None)):
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
@@ -1891,3 +1919,4 @@ def delete_document(request: Request, document_id: int, csrf_token: str = Form(.
|
|||||||
return RedirectResponse(url=f"/documents/engagements/{document.engagement_id}?deleted=1", status_code=303)
|
return RedirectResponse(url=f"/documents/engagements/{document.engagement_id}?deleted=1", status_code=303)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
{% set is_system_admin_user = full_auth and ("System Admin" in ui_roles) %}
|
{% set is_system_admin_user = full_auth and ("System Admin" in ui_roles) %}
|
||||||
{% set can_manage_local_storage_agent = full_auth and can_view_documents(current_user, ui_perms, ui_roles) and can_upload_documents(current_user, ui_perms, ui_roles) and (ui_roles|select("in", ["Firm Admin", "Partner", "Branch Manager"])|list|length > 0) %}
|
{% set can_manage_local_storage_agent = full_auth and can_view_documents(current_user, ui_perms, ui_roles) and can_upload_documents(current_user, ui_perms, ui_roles) and (ui_roles|select("in", ["Firm Admin", "Partner", "Branch Manager"])|list|length > 0) %}
|
||||||
|
|
||||||
{# UI/UX V2 Phase 1G — Global Role Navigation Host.
|
{# UI/UX V2 Phase 1G — Global Role Navigation Host.
|
||||||
Role navigation is rendered once, immediately below the shared header, so it
|
Role navigation is rendered once, immediately below the shared header, so it
|
||||||
remains available on dashboards, shared modules, forms, detail pages and
|
remains available on dashboards, shared modules, forms, detail pages and
|
||||||
submenu pages. Existing page-level navigation includes are suppressed by the
|
submenu pages. Existing page-level navigation includes are suppressed by the
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
{% if firm_branding.logo_url %}<img src="{{ firm_branding.logo_url }}" alt="{{ current_firm_name }} logo" class="h-11 w-11 rounded-2xl bg-white object-contain p-1 shadow-soft" />{% else %}<div class="flex h-11 w-11 items-center justify-center rounded-2xl bg-brand-500 font-bold text-white shadow-soft">{{ (current_firm_name[:2] if current_firm_name else 'AF')|upper }}</div>{% endif %}
|
{% if firm_branding.logo_url %}<img src="{{ firm_branding.logo_url }}" alt="{{ current_firm_name }} logo" class="h-11 w-11 rounded-2xl bg-white object-contain p-1 shadow-soft" />{% else %}<div class="flex h-11 w-11 items-center justify-center rounded-2xl bg-brand-500 font-bold text-white shadow-soft">{{ (current_firm_name[:2] if current_firm_name else 'AF')|upper }}</div>{% endif %}
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="truncate text-sm font-semibold tracking-wide">{{ current_firm_name }}</div>
|
<div class="truncate text-sm font-semibold tracking-wide">{{ current_firm_name }}</div>
|
||||||
<div class="truncate text-xs text-slate-400">{% if full_auth %}{{ current_branch_name }}{% if current_branch_name and current_branch_name != "-" %} Branch{% endif %} • Workspace{% elif domain_context.is_resolved %}{{ current_branch_name or 'Domain Workspace' }}{% else %}Secure Practice Workspace{% endif %}</div>
|
<div class="truncate text-xs text-slate-400">{% if full_auth %}{{ current_branch_name }}{% if current_branch_name and current_branch_name != "-" %} Branch{% endif %} • Workspace{% elif domain_context.is_resolved %}{{ current_branch_name or 'Domain Workspace' }}{% else %}Secure Practice Workspace{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/employee') or current_path.startswith('/alerts') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/employee') or current_path.startswith('/alerts') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>My Workspace</span>
|
<span>My Workspace</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
{% if can_view_employee_portal(current_user, ui_perms, ui_roles) %}
|
{% if can_view_employee_portal(current_user, ui_perms, ui_roles) %}
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/partner') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/partner') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Partner Workspace</span>
|
<span>Partner Workspace</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
<a href="/partner/dashboard" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/partner/dashboard' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Overview</a>
|
<a href="/partner/dashboard" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/partner/dashboard' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Overview</a>
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/manager') or current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path.startswith('/employees/leave') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/manager') or current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path.startswith('/employees/leave') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Team Workspace</span>
|
<span>Team Workspace</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
{% if can_manage_employee_work(current_user, ui_perms, ui_roles) %}
|
{% if can_manage_employee_work(current_user, ui_perms, ui_roles) %}
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/billing') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/billing') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Billing</span>
|
<span>Billing</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
<a href="/billing" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/billing' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Invoices</a>
|
<a href="/billing" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path == '/billing' %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Invoices</a>
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/employees') and not (current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path == '/employees/leave') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/employees') and not (current_path.startswith('/employees/work') or current_path.startswith('/employees/progress') or current_path.startswith('/employees/attendance') or current_path == '/employees/leave') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Team Administration</span>
|
<span>Team Administration</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
{% if can_view_employee_dashboard(current_user, ui_perms, ui_roles) %}<a href="/employees/dashboard" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">HR Dashboard</a>{% endif %}
|
{% if can_view_employee_dashboard(current_user, ui_perms, ui_roles) %}<a href="/employees/dashboard" class="block rounded-lg px-3 py-1.5 text-sm text-slate-300 transition hover:bg-slate-800 hover:text-white">HR Dashboard</a>{% endif %}
|
||||||
@@ -339,7 +339,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/system-settings/users') or current_path.startswith('/system-settings/rbac') or current_path.startswith('/services') or current_path.startswith('/clients') or current_path.startswith('/consultants') or current_path.startswith('/email') or current_path.startswith('/notice-cases') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/system-settings/users') or current_path.startswith('/system-settings/rbac') or current_path.startswith('/services') or current_path.startswith('/clients') or current_path.startswith('/consultants') or current_path.startswith('/email') or current_path.startswith('/notice-cases') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Firm Administration</span>
|
<span>Firm Administration</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
{% if "Firm Admin" in ui_roles or is_system_admin_user %}
|
{% if "Firm Admin" in ui_roles or is_system_admin_user %}
|
||||||
@@ -387,7 +387,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_manage_local_storage_agent %}
|
{% if can_manage_local_storage_agent %}
|
||||||
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Local Storage</div>
|
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Local Storage</div>
|
||||||
<a href="/documents/storage-nodes" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/documents/storage-nodes') or current_path.startswith('/documents/branch-storage-dashboard') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">Local Storage Agent</a>
|
<a href="/documents/storage-nodes" class="block rounded-lg px-3 py-1.5 text-sm transition {% if current_path.startswith('/documents/storage-nodes') or current_path.startswith('/documents/branch-storage-dashboard') %}bg-brand-600 text-white{% else %}text-slate-300 hover:bg-slate-800 hover:text-white{% endif %}">ERP Local Agent</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -397,7 +397,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/domains') or current_path.startswith('/platform-billing') or current_path.startswith('/marketplace') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/domains') or current_path.startswith('/platform-billing') or current_path.startswith('/marketplace') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Platform</span>
|
<span>Platform</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Domain Configuration</div>
|
<div class="pt-3 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-500">Domain Configuration</div>
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
<details class="group rounded-xl" {% if current_path.startswith('/system-settings') %}open{% endif %}>
|
<details class="group rounded-xl" {% if current_path.startswith('/system-settings') %}open{% endif %}>
|
||||||
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
<summary class="flex cursor-pointer list-none items-center justify-between rounded-xl px-3 py-2 text-sm font-semibold transition hover:bg-slate-800">
|
||||||
<span>Core Setup</span>
|
<span>Core Setup</span>
|
||||||
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
<span class="text-xs text-slate-400 transition group-open:rotate-90">›</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
<div class="mt-1 space-y-1 border-l border-slate-700/70 pl-3">
|
||||||
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
|
{% if can_view_settings(current_user, ui_perms, ui_roles) %}
|
||||||
@@ -490,11 +490,11 @@
|
|||||||
<div class="font-medium text-slate-800">{{ current_user.full_name or current_user.email }}</div>
|
<div class="font-medium text-slate-800">{{ current_user.full_name or current_user.email }}</div>
|
||||||
<div class="text-slate-500">{{ current_user.email }}</div>
|
<div class="text-slate-500">{{ current_user.email }}</div>
|
||||||
{% if current_user.qualification or current_user.designation %}
|
{% if current_user.qualification or current_user.designation %}
|
||||||
<div class="text-xs text-slate-500">{{ current_user.qualification or '' }}{% if current_user.qualification and current_user.designation %} • {% endif %}{{ current_user.designation or '' }}</div>
|
<div class="text-xs text-slate-500">{{ current_user.qualification or '' }}{% if current_user.qualification and current_user.designation %} • {% endif %}{{ current_user.designation or '' }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="text-xs text-slate-400">
|
<div class="text-xs text-slate-400">
|
||||||
Your Firm: {{ current_firm_name }}
|
Your Firm: {{ current_firm_name }}
|
||||||
• Branch: {{ current_branch_name }}{% if active_financial_year %} • FY: {{ active_financial_year }}{% endif %}
|
• Branch: {{ current_branch_name }}{% if active_financial_year %} • FY: {{ active_financial_year }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% include "ui/templates/components/workspace_switcher.html" %}
|
{% include "ui/templates/components/workspace_switcher.html" %}
|
||||||
<div class="mt-2 flex justify-end gap-2">
|
<div class="mt-2 flex justify-end gap-2">
|
||||||
@@ -517,7 +517,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if domain_context.is_resolved %}
|
{% if domain_context.is_resolved %}
|
||||||
<div class="mb-2 text-xs text-slate-500">{{ current_firm_name }}{% if current_branch_name %} • {{ current_branch_name }}{% endif %}</div>
|
<div class="mb-2 text-xs text-slate-500">{{ current_firm_name }}{% if current_branch_name %} • {{ current_branch_name }}{% endif %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="inline-flex rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700" href="/login">Login</a>
|
<a class="inline-flex rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700" href="/login">Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -566,7 +566,7 @@
|
|||||||
<div class="text-xs text-slate-500">
|
<div class="text-xs text-slate-500">
|
||||||
Active Scope:
|
Active Scope:
|
||||||
{{ current_firm_name }}
|
{{ current_firm_name }}
|
||||||
• {{ current_branch_name if active_branch_id else "All Branches" }}{% if active_financial_year %} • FY {{ active_financial_year }}{% endif %}
|
• {{ current_branch_name if active_branch_id else "All Branches" }}{% if active_financial_year %} • FY {{ active_financial_year }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -650,7 +650,7 @@
|
|||||||
wrapper.className = "overflow-hidden rounded-2xl border shadow-soft " + priorityClasses(alert.priority);
|
wrapper.className = "overflow-hidden rounded-2xl border shadow-soft " + priorityClasses(alert.priority);
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="flex items-start gap-3 p-4">
|
<div class="flex items-start gap-3 p-4">
|
||||||
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/80 text-base">🔔</div>
|
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/80 text-base">🔔</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wide opacity-75">New Alert</div>
|
<div class="text-xs font-semibold uppercase tracking-wide opacity-75">New Alert</div>
|
||||||
<div class="mt-0.5 line-clamp-2 text-sm font-semibold"></div>
|
<div class="mt-0.5 line-clamp-2 text-sm font-semibold"></div>
|
||||||
@@ -771,3 +771,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user