Add Phase 1 distributed lean workstation agent foundation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -81,7 +82,7 @@ from app.modules.services.models import ClientServiceSubscription
|
||||
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.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, ERPWorkstationAgent
|
||||
from app.modules.accounting.agent_bridge import list_pending_agent_commands, record_agent_command_result
|
||||
|
||||
from app.modules.services.task_documents import (
|
||||
@@ -1254,6 +1255,53 @@ def _selected_or_forced_branch_id(branch_id: str | None, user, scope) -> int | N
|
||||
return int(forced_branch_id)
|
||||
return int(branch_id) if branch_id and str(branch_id).isdigit() else None
|
||||
|
||||
|
||||
def _record_workstation_status(db, node: BranchStorageNode, payload: dict | None, client_ip: str | None = None) -> None:
|
||||
if not isinstance(payload, dict):
|
||||
return
|
||||
instance_id = str(payload.get("agent_instance_id") or "").strip()
|
||||
fingerprint = str(payload.get("machine_fingerprint") or "").strip()
|
||||
machine_name = str(payload.get("machine_name") or "").strip()
|
||||
if not instance_id or not fingerprint or not machine_name:
|
||||
return
|
||||
row = db.execute(
|
||||
select(ERPWorkstationAgent).where(
|
||||
ERPWorkstationAgent.storage_node_id == node.id,
|
||||
ERPWorkstationAgent.agent_instance_id == instance_id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if row is None:
|
||||
row = ERPWorkstationAgent(
|
||||
tenant_id=node.tenant_id, branch_id=node.branch_id, storage_node_id=node.id,
|
||||
agent_instance_id=instance_id, machine_fingerprint=fingerprint, machine_name=machine_name,
|
||||
)
|
||||
db.add(row)
|
||||
tally = payload.get("tally") if isinstance(payload.get("tally"), dict) else {}
|
||||
capabilities = payload.get("capabilities") if isinstance(payload.get("capabilities"), list) else []
|
||||
row.tenant_id = node.tenant_id
|
||||
row.branch_id = node.branch_id
|
||||
row.machine_fingerprint = fingerprint
|
||||
row.machine_name = machine_name[:200]
|
||||
row.platform_name = str(payload.get("platform_name") or "")[:120] or None
|
||||
row.agent_version = str(payload.get("agent_version") or "")[:40] or None
|
||||
row.capabilities_json = json.dumps(capabilities, ensure_ascii=False, separators=(",", ":"))
|
||||
row.tally_connected = bool(tally.get("connected"))
|
||||
row.tally_company_count = int(tally.get("company_count") or 0)
|
||||
row.tally_companies_json = json.dumps(list(tally.get("companies") or []), ensure_ascii=False, separators=(",", ":"))
|
||||
row.status = "online"
|
||||
row.is_active = True
|
||||
row.last_seen_at_utc = datetime.now(timezone.utc)
|
||||
row.last_seen_ip = (client_ip or "")[:80] or None
|
||||
|
||||
|
||||
def _visible_workstation_agents(db, tenant_id: int | None, branch_id: int | None) -> list[ERPWorkstationAgent]:
|
||||
stmt = select(ERPWorkstationAgent).order_by(ERPWorkstationAgent.last_seen_at_utc.desc())
|
||||
if tenant_id is not None:
|
||||
stmt = stmt.where(ERPWorkstationAgent.tenant_id == tenant_id)
|
||||
if branch_id is not None:
|
||||
stmt = stmt.where(ERPWorkstationAgent.branch_id == branch_id)
|
||||
return list(db.execute(stmt).scalars().all())
|
||||
|
||||
@router.get("/storage-nodes")
|
||||
def storage_nodes(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
@@ -1270,6 +1318,7 @@ def storage_nodes(request: Request):
|
||||
branches = _visible_branches(db, user, scope)
|
||||
jobs = list_storage_jobs(db, tenant_id=tenant_filter, branch_id=branch_filter, limit=10)
|
||||
requests = list_download_requests(db, tenant_id=tenant_filter, branch_id=branch_filter, limit=10)
|
||||
workstations = _visible_workstation_agents(db, tenant_filter, branch_filter)
|
||||
return _render(
|
||||
request,
|
||||
"modules/documents/templates/documents/storage_nodes.html",
|
||||
@@ -1281,6 +1330,7 @@ def storage_nodes(request: Request):
|
||||
branch_map=_branch_name_map(db, branches),
|
||||
recent_jobs=jobs,
|
||||
recent_download_requests=requests,
|
||||
workstation_agents=workstations,
|
||||
generated_secret=None,
|
||||
storage_scope_title=_storage_scope_title(user, scope),
|
||||
erp_local_agent_version=ERP_LOCAL_AGENT_VERSION,
|
||||
@@ -1535,7 +1585,7 @@ def toggle_storage_node(request: Request, node_id: int, csrf_token: str = Form(.
|
||||
scope = build_document_scope(request, db, user)
|
||||
if not _can_manage_branch_storage(scope):
|
||||
return _redirect_denied()
|
||||
from app.modules.documents.models import BranchStorageNode
|
||||
from app.modules.documents.models import BranchStorageNode, ERPWorkstationAgent
|
||||
node = db.get(BranchStorageNode, node_id)
|
||||
if not _node_allowed_for_storage_scope(node, user, scope):
|
||||
return _redirect_denied()
|
||||
@@ -1839,6 +1889,9 @@ async def storage_agent_tunnel(websocket: WebSocket):
|
||||
await websocket.send_json({"ok": False, "type": "error", "error": "node_deactivated_or_invalid"})
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
if message and isinstance(message.get("workstation"), dict):
|
||||
client_ip = websocket.client.host if websocket.client else None
|
||||
_record_workstation_status(db, node, message.get("workstation"), client_ip)
|
||||
if message and message.get("type") == "command_result":
|
||||
record_agent_command_result(node.node_code, message)
|
||||
if message and message.get("type") in {"heartbeat", "agent_status", "command_result"}:
|
||||
|
||||
Reference in New Issue
Block a user