|
|
@@ -1,5 +1,6 @@
|
|
|
|
from __future__ import annotations
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import shutil
|
|
|
|
import shutil
|
|
|
|
import threading
|
|
|
|
import threading
|
|
|
@@ -22,9 +23,56 @@ class AgentDashboard:
|
|
|
|
self.install_dir = Path(install_dir).resolve()
|
|
|
|
self.install_dir = Path(install_dir).resolve()
|
|
|
|
self.server: ThreadingHTTPServer | None = None
|
|
|
|
self.server: ThreadingHTTPServer | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
def _sha256(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 _embedded_tdl(self) -> Path:
|
|
|
|
|
|
|
|
preferred = self.install_dir / "erp_local_agent" / "mirror_runtime" / "ERP_Accounting_Mirror_v3_0_1.tdl"
|
|
|
|
|
|
|
|
if preferred.is_file():
|
|
|
|
|
|
|
|
return preferred
|
|
|
|
|
|
|
|
compatibility = self.install_dir / "erp_local_agent" / "mirror_runtime" / "ERP_Accounting_Mirror_v3_1.tdl"
|
|
|
|
|
|
|
|
if compatibility.is_file():
|
|
|
|
|
|
|
|
return compatibility
|
|
|
|
|
|
|
|
raise RuntimeError("ERP Accounting Mirror TDL is missing from the Local Agent runtime.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _managed_tdl(self) -> Path:
|
|
|
|
|
|
|
|
return self.install_dir / "tally" / "ERP_Accounting_Mirror.tdl"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_managed_tdl(self, force: bool = False) -> dict[str, Any]:
|
|
|
|
|
|
|
|
source = self._embedded_tdl()
|
|
|
|
|
|
|
|
target = self._managed_tdl()
|
|
|
|
|
|
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
source_hash = self._sha256(source)
|
|
|
|
|
|
|
|
target_hash = self._sha256(target) if target.is_file() else ""
|
|
|
|
|
|
|
|
repaired = False
|
|
|
|
|
|
|
|
if force or source_hash != target_hash:
|
|
|
|
|
|
|
|
temp = target.with_suffix(".tdl.new")
|
|
|
|
|
|
|
|
shutil.copy2(source, temp)
|
|
|
|
|
|
|
|
temp.replace(target)
|
|
|
|
|
|
|
|
target_hash = self._sha256(target)
|
|
|
|
|
|
|
|
repaired = True
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
"version": "3.0.1",
|
|
|
|
|
|
|
|
"path": str(target),
|
|
|
|
|
|
|
|
"url": f"http://{self.config.dashboard_host}:{self.config.dashboard_port}/tally/ERP_Accounting_Mirror.tdl",
|
|
|
|
|
|
|
|
"sha256": target_hash,
|
|
|
|
|
|
|
|
"source_path": str(source),
|
|
|
|
|
|
|
|
"repaired": repaired,
|
|
|
|
|
|
|
|
"exists": target.is_file(),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def start(self) -> None:
|
|
|
|
def start(self) -> None:
|
|
|
|
if not self.config.dashboard_enabled:
|
|
|
|
if not self.config.dashboard_enabled:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
self._ensure_managed_tdl()
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
|
|
self.logger.warning("Could not prepare managed Tally TDL: %s", exc)
|
|
|
|
dashboard = self
|
|
|
|
dashboard = self
|
|
|
|
|
|
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
|
@@ -58,6 +106,17 @@ class AgentDashboard:
|
|
|
|
return self._json(dashboard.history())
|
|
|
|
return self._json(dashboard.history())
|
|
|
|
if self.path.startswith("/api/cash-progress"):
|
|
|
|
if self.path.startswith("/api/cash-progress"):
|
|
|
|
return self._json(dashboard.cash_progress())
|
|
|
|
return self._json(dashboard.cash_progress())
|
|
|
|
|
|
|
|
if self.path.split("?", 1)[0] == "/tally/ERP_Accounting_Mirror.tdl":
|
|
|
|
|
|
|
|
info = dashboard._ensure_managed_tdl()
|
|
|
|
|
|
|
|
payload = Path(info["path"]).read_bytes()
|
|
|
|
|
|
|
|
self.send_response(HTTPStatus.OK)
|
|
|
|
|
|
|
|
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
|
|
|
|
|
|
self.send_header("Cache-Control", "no-store")
|
|
|
|
|
|
|
|
self.send_header("Content-Disposition", 'inline; filename="ERP_Accounting_Mirror.tdl"')
|
|
|
|
|
|
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
|
|
|
|
|
|
self.end_headers()
|
|
|
|
|
|
|
|
self.wfile.write(payload)
|
|
|
|
|
|
|
|
return
|
|
|
|
self.send_error(404)
|
|
|
|
self.send_error(404)
|
|
|
|
|
|
|
|
|
|
|
|
def do_POST(self):
|
|
|
|
def do_POST(self):
|
|
|
@@ -76,6 +135,10 @@ class AgentDashboard:
|
|
|
|
status = TallyLiveConnector().status()
|
|
|
|
status = TallyLiveConnector().status()
|
|
|
|
dashboard.db.record_tally_status(status)
|
|
|
|
dashboard.db.record_tally_status(status)
|
|
|
|
return self._json({"ok": True, "tally": status})
|
|
|
|
return self._json({"ok": True, "tally": status})
|
|
|
|
|
|
|
|
if self.path == "/api/tally/tdl/repair":
|
|
|
|
|
|
|
|
info = dashboard._ensure_managed_tdl(force=True)
|
|
|
|
|
|
|
|
dashboard.db.record_event("INFO", "tally_tdl_repaired", f"Managed TDL refreshed: {info['path']}")
|
|
|
|
|
|
|
|
return self._json({"ok": True, "tdl": info})
|
|
|
|
self.send_error(404)
|
|
|
|
self.send_error(404)
|
|
|
|
except Exception as exc:
|
|
|
|
except Exception as exc:
|
|
|
|
dashboard.logger.exception("Dashboard action failed: %s", exc)
|
|
|
|
dashboard.logger.exception("Dashboard action failed: %s", exc)
|
|
|
@@ -120,6 +183,7 @@ class AgentDashboard:
|
|
|
|
"free_bytes": free,
|
|
|
|
"free_bytes": free,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"tally": tally,
|
|
|
|
"tally": tally,
|
|
|
|
|
|
|
|
"tdl": self._ensure_managed_tdl(),
|
|
|
|
"update": self.updater.status(),
|
|
|
|
"update": self.updater.status(),
|
|
|
|
"dashboard": {"host": self.config.dashboard_host, "port": self.config.dashboard_port},
|
|
|
|
"dashboard": {"host": self.config.dashboard_host, "port": self.config.dashboard_port},
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -149,7 +213,7 @@ class AgentDashboard:
|
|
|
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
<title>ERP Local Agent</title>
|
|
|
|
<title>ERP Local Agent</title>
|
|
|
|
<style>
|
|
|
|
<style>
|
|
|
|
body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;margin:0}.wrap{max-width:1100px;margin:28px auto;padding:0 18px}h1{margin:0 0 4px}.muted{color:#6b7280}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin:22px 0}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04)}.label{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6b7280}.value{font-size:22px;font-weight:650;margin-top:8px}.ok{color:#087f5b}.bad{color:#c92a2a}.warn{color:#b26a00}button{border:0;border-radius:8px;padding:10px 14px;margin:4px 6px 4px 0;font-weight:600;cursor:pointer;background:#111827;color:#fff}button.secondary{background:#e5e7eb;color:#111827}button:disabled{opacity:.5;cursor:not-allowed}.section{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin:14px 0}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:8px;border-bottom:1px solid #eee;vertical-align:top}#message{margin-top:10px;padding:10px;border-radius:8px;display:none}.msgok{display:block!important;background:#e6fcf5;color:#087f5b}.msgbad{display:block!important;background:#fff5f5;color:#c92a2a}code{background:#f3f4f6;padding:2px 5px;border-radius:4px}.progressbox{margin-top:14px;padding:14px;border:1px solid #dbe3ea;border-radius:10px;background:#f8fafc}.progresshead{display:flex;justify-content:space-between;gap:12px;align-items:center}.progressbar{height:12px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin-top:10px}.progressfill{height:100%;width:0%;background:#111827;transition:width .45s ease}.progressmsg{margin-top:8px;font-size:13px;color:#4b5563}.pulse{animation:pulse 1.2s ease-in-out infinite}.cashsteps{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px;margin-top:12px}.cashstep{padding:9px 10px;border-radius:8px;background:#f3f4f6;font-size:12px}.cashstep.done{background:#e6fcf5;color:#087f5b}.cashstep.run{background:#e7f5ff;color:#1864ab;font-weight:700}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:12px}.metric{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px}.metric b{display:block;font-size:18px;margin-top:3px}@keyframes pulse{0%,100%{opacity:.65}50%{opacity:1}}
|
|
|
|
body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;margin:0}.wrap{max-width:1100px;margin:28px auto;padding:0 18px}h1{margin:0 0 4px}.muted{color:#6b7280}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin:22px 0}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04)}.label{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6b7280}.value{font-size:22px;font-weight:650;margin-top:8px}.ok{color:#087f5b}.bad{color:#c92a2a}.warn{color:#b26a00}button{border:0;border-radius:8px;padding:10px 14px;margin:4px 6px 4px 0;font-weight:600;cursor:pointer;background:#111827;color:#fff}button.secondary{background:#e5e7eb;color:#111827}.copyrow{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;margin-top:8px}.copybox{width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:8px;padding:10px;background:#f8fafc;font-family:Consolas,monospace;font-size:12px;color:#111827}button:disabled{opacity:.5;cursor:not-allowed}.section{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin:14px 0}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:8px;border-bottom:1px solid #eee;vertical-align:top}#message{margin-top:10px;padding:10px;border-radius:8px;display:none}.msgok{display:block!important;background:#e6fcf5;color:#087f5b}.msgbad{display:block!important;background:#fff5f5;color:#c92a2a}code{background:#f3f4f6;padding:2px 5px;border-radius:4px}.progressbox{margin-top:14px;padding:14px;border:1px solid #dbe3ea;border-radius:10px;background:#f8fafc}.progresshead{display:flex;justify-content:space-between;gap:12px;align-items:center}.progressbar{height:12px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin-top:10px}.progressfill{height:100%;width:0%;background:#111827;transition:width .45s ease}.progressmsg{margin-top:8px;font-size:13px;color:#4b5563}.pulse{animation:pulse 1.2s ease-in-out infinite}.cashsteps{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px;margin-top:12px}.cashstep{padding:9px 10px;border-radius:8px;background:#f3f4f6;font-size:12px}.cashstep.done{background:#e6fcf5;color:#087f5b}.cashstep.run{background:#e7f5ff;color:#1864ab;font-weight:700}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:12px}.metric{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px}.metric b{display:block;font-size:18px;margin-top:3px}@keyframes pulse{0%,100%{opacity:.65}50%{opacity:1}}
|
|
|
|
</style></head>
|
|
|
|
</style></head>
|
|
|
|
<body><div class="wrap"><h1>ERP Local Agent</h1><div class="muted">Local dashboard — available only on this computer</div>
|
|
|
|
<body><div class="wrap"><h1>ERP Local Agent</h1><div class="muted">Local dashboard — available only on this computer</div>
|
|
|
|
<div class="grid">
|
|
|
|
<div class="grid">
|
|
|
@@ -162,6 +226,7 @@ body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;marg
|
|
|
|
<button id="check" onclick="checkUpdate()">Check for Update</button><button class="secondary" id="download" onclick="downloadUpdate()">Download Update</button><button id="install" onclick="installUpdate()">Install & Restart Safely</button><div id="message"></div>
|
|
|
|
<button id="check" onclick="checkUpdate()">Check for Update</button><button class="secondary" id="download" onclick="downloadUpdate()">Download Update</button><button id="install" onclick="installUpdate()">Install & Restart Safely</button><div id="message"></div>
|
|
|
|
<div id="progressBox" class="progressbox" style="display:none"><div class="progresshead"><strong id="progressTitle">Update</strong><span id="progressPct">0%</span></div><div class="progressbar"><div id="progressFill" class="progressfill"></div></div><div id="progressMsg" class="progressmsg">Waiting…</div></div></div>
|
|
|
|
<div id="progressBox" class="progressbox" style="display:none"><div class="progresshead"><strong id="progressTitle">Update</strong><span id="progressPct">0%</span></div><div class="progressbar"><div id="progressFill" class="progressfill"></div></div><div id="progressMsg" class="progressmsg">Waiting…</div></div></div>
|
|
|
|
<div class="section"><h2>Tally</h2><button onclick="action('/api/tally/check')">Test Tally Connection</button><p class="muted">Tally is checked locally at <code>127.0.0.1:9000</code>. Port 9000 is not exposed to the internet.</p></div>
|
|
|
|
<div class="section"><h2>Tally</h2><button onclick="action('/api/tally/check')">Test Tally Connection</button><p class="muted">Tally is checked locally at <code>127.0.0.1:9000</code>. Port 9000 is not exposed to the internet.</p></div>
|
|
|
|
|
|
|
|
<div class="section"><h2>Tally Accounting Mirror TDL</h2><p class="muted">Link Tally once to the stable local path below. Local Agent updates keep this same file path and refresh its contents, so future TDL repairs do not require a new Tally link.</p><div class="label">Stable Local TDL Path</div><div class="copyrow"><input id="tdlPath" class="copybox" readonly value="Loading…"><button onclick="copyText('tdlPath')">Copy Path</button></div><div class="label" style="margin-top:12px">Local TDL URL</div><div class="copyrow"><input id="tdlUrl" class="copybox" readonly value="Loading…"><button onclick="copyText('tdlUrl')">Copy URL</button></div><div id="tdlMeta" class="muted" style="margin-top:8px"></div><button class="secondary" onclick="repairTdl()">Repair / Refresh TDL</button><p class="muted">For Tally's Local TDL setting, use the stable file path if your TallyPrime build does not accept an HTTP URL. The URL is also kept stable for tools that support URL loading.</p></div>
|
|
|
|
<div class="section" id="cashJobSection" style="display:none"><div class="progresshead"><div><h2 style="margin:0">Cash Payment Compliance</h2><div id="cashJobSub" class="muted">Background Tally job</div></div><strong id="cashJobPct">0%</strong></div><div class="progressbar"><div id="cashJobFill" class="progressfill"></div></div><div id="cashJobStage" class="progressmsg">Waiting…</div><div class="metricgrid"><div class="metric"><span class="label">Days</span><b id="cashDays">0 / 0</b></div><div class="metric"><span class="label">Cash vouchers</span><b id="cashVouchers">0</b></div><div class="metric"><span class="label">Tally requests</span><b id="cashRequests">0</b></div><div class="metric"><span class="label">Current date</span><b id="cashDate">—</b></div></div><div class="cashsteps"><div id="cs1" class="cashstep">1. Connect to Tally</div><div id="cs2" class="cashstep">2. Confirm Cash ledger</div><div id="cs3" class="cashstep">3. Extract transactions</div><div id="cs4" class="cashstep">4. Analyse threshold</div><div id="cs5" class="cashstep">5. Complete</div></div><div id="cashError" class="msgbad" style="margin-top:10px"></div></div>
|
|
|
|
<div class="section" id="cashJobSection" style="display:none"><div class="progresshead"><div><h2 style="margin:0">Cash Payment Compliance</h2><div id="cashJobSub" class="muted">Background Tally job</div></div><strong id="cashJobPct">0%</strong></div><div class="progressbar"><div id="cashJobFill" class="progressfill"></div></div><div id="cashJobStage" class="progressmsg">Waiting…</div><div class="metricgrid"><div class="metric"><span class="label">Days</span><b id="cashDays">0 / 0</b></div><div class="metric"><span class="label">Cash vouchers</span><b id="cashVouchers">0</b></div><div class="metric"><span class="label">Tally requests</span><b id="cashRequests">0</b></div><div class="metric"><span class="label">Current date</span><b id="cashDate">—</b></div></div><div class="cashsteps"><div id="cs1" class="cashstep">1. Connect to Tally</div><div id="cs2" class="cashstep">2. Confirm Cash ledger</div><div id="cs3" class="cashstep">3. Extract transactions</div><div id="cs4" class="cashstep">4. Analyse threshold</div><div id="cs5" class="cashstep">5. Complete</div></div><div id="cashError" class="msgbad" style="margin-top:10px"></div></div>
|
|
|
|
<div class="section"><h2>Recent History</h2><div id="history" class="muted">Loading…</div></div>
|
|
|
|
<div class="section"><h2>Recent History</h2><div id="history" class="muted">Loading…</div></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
@@ -171,13 +236,14 @@ function gb(n){return (n/1073741824).toFixed(1)+' GB'}
|
|
|
|
function setProgress(title,pct,text,active=true){let b=document.getElementById('progressBox');b.style.display='block';document.getElementById('progressTitle').textContent=title;let p=Math.max(0,Math.min(100,Number(pct)||0));lastServerPercent=Math.max(lastServerPercent,p);document.getElementById('progressPct').textContent=Math.round(p)+'%';document.getElementById('progressFill').style.width=p+'%';document.getElementById('progressMsg').textContent=text||'';document.getElementById('progressFill').className='progressfill'+(active&&p<100?' pulse':'')}
|
|
|
|
function setProgress(title,pct,text,active=true){let b=document.getElementById('progressBox');b.style.display='block';document.getElementById('progressTitle').textContent=title;let p=Math.max(0,Math.min(100,Number(pct)||0));lastServerPercent=Math.max(lastServerPercent,p);document.getElementById('progressPct').textContent=Math.round(p)+'%';document.getElementById('progressFill').style.width=p+'%';document.getElementById('progressMsg').textContent=text||'';document.getElementById('progressFill').className='progressfill'+(active&&p<100?' pulse':'')}
|
|
|
|
function applyProgress(u){let p=(u&&u.progress)||{};if(!p.operation)return;let title=p.operation==='check'?'Checking for Updates':p.operation==='download'?'Downloading Update':'Applying Update';setProgress(title,p.percent||0,p.message||p.phase||'',!['updated','downloaded','current','available','error'].includes(p.status));if(p.operation==='install'&&p.target_version){installTarget=p.target_version}}
|
|
|
|
function applyProgress(u){let p=(u&&u.progress)||{};if(!p.operation)return;let title=p.operation==='check'?'Checking for Updates':p.operation==='download'?'Downloading Update':'Applying Update';setProgress(title,p.percent||0,p.message||p.phase||'',!['updated','downloaded','current','available','error'].includes(p.status));if(p.operation==='install'&&p.target_version){installTarget=p.target_version}}
|
|
|
|
function setCashStep(id,state){let e=document.getElementById(id);e.className='cashstep'+(state?' '+state:'')}
|
|
|
|
function setCashStep(id,state){let e=document.getElementById(id);e.className='cashstep'+(state?' '+state:'')}
|
|
|
|
async function refreshCashProgress(){try{const r=await fetch('/api/cash-progress',{cache:'no-store'});const d=await r.json();const j=d.job||{};if(!j.job_id){document.getElementById('cashJobSection').style.display='none';return}document.getElementById('cashJobSection').style.display='block';let p=Math.max(0,Math.min(100,Number(j.percent)||0));document.getElementById('cashJobPct').textContent=Math.round(p)+'%';document.getElementById('cashJobFill').style.width=p+'%';document.getElementById('cashJobStage').textContent=j.stage||j.status||'Working…';let kind=String(j.job_kind||'');let isLedger=(kind==='ledger_master'||kind==='cash_ledger_discovery');document.querySelector('#cashJobSection h2').textContent=isLedger?'Cash Payment — Tally Ledger Sync':'Cash Payment Compliance';document.getElementById('cashJobSub').textContent=(j.company_name||'Tally company')+(j.cash_ledgers&&j.cash_ledgers.length?' • Cash ledger: '+j.cash_ledgers.join(', '):'');if(isLedger){document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Ledgers';document.getElementById('cashDays').textContent=String(j.ledger_count||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Status';document.getElementById('cashVouchers').textContent=j.status||'working';document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Progress';document.getElementById('cashRequests').textContent=Math.round(p)+'%';document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Source';document.getElementById('cashDate').textContent=(kind==='cash_ledger_discovery'?'Cash-in-Hand':'Tally ledger master');}else{document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Days';document.getElementById('cashDays').textContent=String(j.completed_days||0)+' / '+String(j.total_days||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Cash vouchers';document.getElementById('cashVouchers').textContent=String(j.vouchers_cached||0);document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Tally requests';document.getElementById('cashRequests').textContent=String(j.tally_requests||0);document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Current date';document.getElementById('cashDate').textContent=j.current_date||'—';}let st=String(j.stage||'').toLowerCase(),status=String(j.status||'').toLowerCase();if(isLedger){setCashStep('cs1',(p>=15?'done':(p>0?'run':'')));setCashStep('cs2','');setCashStep('cs3','');setCashStep('cs4','');setCashStep('cs5','');}else{setCashStep('cs1',(p>=10?'done':(p>0?'run':'')));setCashStep('cs2',(p>=20?'done':(st.includes('ledger')?'run':'')));setCashStep('cs3',(p>=93?'done':(p>=20?'run':'')));setCashStep('cs4',(status==='completed'?'done':(st.includes('analys')?'run':'')));setCashStep('cs5',(status==='completed'?'done':''));}let er=document.getElementById('cashError');if(j.error){er.style.display='block';er.textContent=j.error}else{er.style.display='none'}}catch(e){}}
|
|
|
|
async function refreshCashProgress(){try{const r=await fetch('/api/cash-progress',{cache:'no-store'});const d=await r.json();const j=d.job||{};if(!j.job_id){document.getElementById('cashJobSection').style.display='none';return}document.getElementById('cashJobSection').style.display='block';let p=Math.max(0,Math.min(100,Number(j.percent)||0));document.getElementById('cashJobPct').textContent=Math.round(p)+'%';document.getElementById('cashJobFill').style.width=p+'%';document.getElementById('cashJobStage').textContent=j.stage||j.status||'Working…';document.getElementById('cashJobSub').textContent=(j.company_name||'Tally company')+(j.cash_ledgers&&j.cash_ledgers.length?' • Cash ledger: '+j.cash_ledgers.join(', '):'');document.getElementById('cashDays').textContent=String(j.completed_days||0)+' / '+String(j.total_days||0);document.getElementById('cashVouchers').textContent=String(j.vouchers_cached||0);document.getElementById('cashRequests').textContent=String(j.tally_requests||0);document.getElementById('cashDate').textContent=j.current_date||'—';let st=String(j.stage||'').toLowerCase(),status=String(j.status||'').toLowerCase();setCashStep('cs1',(p>=10?'done':(p>0?'run':'')));setCashStep('cs2',(p>=20?'done':(st.includes('ledger')?'run':'')));setCashStep('cs3',(p>=93?'done':(p>=20?'run':'')));setCashStep('cs4',(status==='completed'?'done':(st.includes('analys')?'run':'')));setCashStep('cs5',(status==='completed'?'done':''));let er=document.getElementById('cashError');if(j.error){er.style.display='block';er.textContent=j.error}else{er.style.display='none'}}catch(e){}}
|
|
|
|
async function refresh(){try{const r=await fetch('/api/status',{cache:'no-store'});if(!r.ok)throw new Error('status unavailable');const s=await r.json();offlineTicks=0;
|
|
|
|
async function refresh(){try{const r=await fetch('/api/status',{cache:'no-store'});if(!r.ok)throw new Error('status unavailable');const s=await r.json();offlineTicks=0;
|
|
|
|
let c=s.agent.connection_state||'unknown';let ce=document.getElementById('connection');ce.textContent=c.charAt(0).toUpperCase()+c.slice(1);ce.className='value '+(c==='connected'?'ok':'bad');
|
|
|
|
let c=s.agent.connection_state||'unknown';let ce=document.getElementById('connection');ce.textContent=c.charAt(0).toUpperCase()+c.slice(1);ce.className='value '+(c==='connected'?'ok':'bad');
|
|
|
|
document.getElementById('heartbeat').textContent=s.agent.last_heartbeat_utc?'Last heartbeat: '+s.agent.last_heartbeat_utc:'No heartbeat recorded';
|
|
|
|
document.getElementById('heartbeat').textContent=s.agent.last_heartbeat_utc?'Last heartbeat: '+s.agent.last_heartbeat_utc:'No heartbeat recorded';
|
|
|
|
document.getElementById('version').textContent=s.agent.version;document.getElementById('latest').textContent='Latest: '+s.update.latest_version+(s.update.update_available?' • update available':' • current');
|
|
|
|
document.getElementById('version').textContent=s.agent.version;document.getElementById('latest').textContent='Latest: '+s.update.latest_version+(s.update.update_available?' • update available':' • current');
|
|
|
|
let te=document.getElementById('tally');let known=s.tally.connected!==null;te.textContent=!known?'Not checked':(s.tally.connected?'Connected':'Offline');te.className='value '+(!known?'warn':(s.tally.connected?'ok':'bad'));document.getElementById('companies').textContent=!known?'Use Test Tally Connection':(s.tally.connected?(s.tally.company_count+' loaded compan'+(s.tally.company_count===1?'y':'ies')):(s.tally.error||''));
|
|
|
|
let te=document.getElementById('tally');let known=s.tally.connected!==null;te.textContent=!known?'Not checked':(s.tally.connected?'Connected':'Offline');te.className='value '+(!known?'warn':(s.tally.connected?'ok':'bad'));document.getElementById('companies').textContent=!known?'Use Test Tally Connection':(s.tally.connected?(s.tally.company_count+' loaded compan'+(s.tally.company_count===1?'y':'ies')):(s.tally.error||''));
|
|
|
|
document.getElementById('storage').textContent=gb(s.storage.free_bytes);document.getElementById('storageRoot').textContent=s.storage.root;
|
|
|
|
document.getElementById('storage').textContent=gb(s.storage.free_bytes);document.getElementById('storageRoot').textContent=s.storage.root;
|
|
|
|
|
|
|
|
if(s.tdl){document.getElementById('tdlPath').value=s.tdl.path||'';document.getElementById('tdlUrl').value=s.tdl.url||'';document.getElementById('tdlMeta').textContent='Mirror '+(s.tdl.version||'')+' • SHA256 '+String(s.tdl.sha256||'').slice(0,16)+'…'+(s.tdl.repaired?' • refreshed now':'');}
|
|
|
|
document.getElementById('check').disabled=!!s.update.busy;document.getElementById('download').disabled=!s.update.update_available||!!s.update.busy;document.getElementById('install').disabled=!s.update.downloaded||!!s.update.busy;applyProgress(s.update);
|
|
|
|
document.getElementById('check').disabled=!!s.update.busy;document.getElementById('download').disabled=!s.update.update_available||!!s.update.busy;document.getElementById('install').disabled=!s.update.downloaded||!!s.update.busy;applyProgress(s.update);
|
|
|
|
if(installWatching&&installTarget&&s.agent.version===installTarget){setProgress('Update Complete',100,'ERP Local Agent '+installTarget+' is running. Updated dashboard loaded.',false);msg('Update completed successfully.',true);installWatching=false;setTimeout(()=>location.reload(),1200)}
|
|
|
|
if(installWatching&&installTarget&&s.agent.version===installTarget){setProgress('Update Complete',100,'ERP Local Agent '+installTarget+' is running. Updated dashboard loaded.',false);msg('Update completed successfully.',true);installWatching=false;setTimeout(()=>location.reload(),1200)}
|
|
|
|
await refreshHistory();await refreshCashProgress();}catch(e){if(installWatching){offlineTicks++;let estimated=Math.min(92,Math.max(lastServerPercent,35+offlineTicks*3));setProgress('Applying Update',estimated,'Local Agent is restarting. Waiting for the updated dashboard to come back online…',true)}else{console.log(e)}}}
|
|
|
|
await refreshHistory();await refreshCashProgress();}catch(e){if(installWatching){offlineTicks++;let estimated=Math.min(92,Math.max(lastServerPercent,35+offlineTicks*3));setProgress('Applying Update',estimated,'Local Agent is restarting. Waiting for the updated dashboard to come back online…',true)}else{console.log(e)}}}
|
|
|
@@ -187,6 +253,8 @@ async function checkUpdate(){setProgress('Checking for Updates',10,'Checking ERP
|
|
|
|
async function downloadUpdate(){setProgress('Downloading Update',5,'Starting download…',true);msg('Downloading update…',true);try{const r=await fetch('/api/update/download',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Download failed');msg('Download started. You can keep this dashboard open.',true);await refresh()}catch(e){setProgress('Download Update',100,e.message,false);msg(e.message,false)}}
|
|
|
|
async function downloadUpdate(){setProgress('Downloading Update',5,'Starting download…',true);msg('Downloading update…',true);try{const r=await fetch('/api/update/download',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Download failed');msg('Download started. You can keep this dashboard open.',true);await refresh()}catch(e){setProgress('Download Update',100,e.message,false);msg(e.message,false)}}
|
|
|
|
async function action(url){msg('Working…',true);try{const r=await fetch(url,{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Operation failed');msg('Completed successfully.',true);await refresh()}catch(e){msg(e.message,false)}}
|
|
|
|
async function action(url){msg('Working…',true);try{const r=await fetch(url,{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Operation failed');msg('Completed successfully.',true);await refresh()}catch(e){msg(e.message,false)}}
|
|
|
|
async function installUpdate(){if(!confirm('Install the downloaded update now? The Local Agent worker will restart safely and this dashboard will show progress until the updated dashboard returns.'))return;let latest=document.getElementById('latest').textContent.replace('Latest: ','').split(' •')[0];installTarget=latest;installWatching=true;offlineTicks=0;lastServerPercent=5;setProgress('Applying Update',5,'Queuing update with the Local Agent supervisor…',true);msg('Starting safe update installation…',true);try{const r=await fetch('/api/update/install',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Install failed');if(j.target_version)installTarget=j.target_version;msg(j.message||'Update queued. Keep this dashboard open.',true);setProgress('Applying Update',8,'Update queued. Waiting for supervisor…',true)}catch(e){installWatching=false;setProgress('Apply Update',100,e.message,false);msg(e.message,false)}}
|
|
|
|
async function installUpdate(){if(!confirm('Install the downloaded update now? The Local Agent worker will restart safely and this dashboard will show progress until the updated dashboard returns.'))return;let latest=document.getElementById('latest').textContent.replace('Latest: ','').split(' •')[0];installTarget=latest;installWatching=true;offlineTicks=0;lastServerPercent=5;setProgress('Applying Update',5,'Queuing update with the Local Agent supervisor…',true);msg('Starting safe update installation…',true);try{const r=await fetch('/api/update/install',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Install failed');if(j.target_version)installTarget=j.target_version;msg(j.message||'Update queued. Keep this dashboard open.',true);setProgress('Applying Update',8,'Update queued. Waiting for supervisor…',true)}catch(e){installWatching=false;setProgress('Apply Update',100,e.message,false);msg(e.message,false)}}
|
|
|
|
|
|
|
|
async function copyText(id){let e=document.getElementById(id);let v=e.value||'';try{await navigator.clipboard.writeText(v);msg('Copied to clipboard.',true)}catch(err){e.focus();e.select();document.execCommand('copy');msg('Copied to clipboard.',true)}}
|
|
|
|
|
|
|
|
async function repairTdl(){msg('Refreshing the managed TDL…',true);try{const r=await fetch('/api/tally/tdl/repair',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'TDL repair failed');msg('TDL refreshed successfully. Restart/reload TDL in TallyPrime if it is already open.',true);await refresh()}catch(e){msg(e.message,false)}}
|
|
|
|
function msg(t,ok){let e=document.getElementById('message');e.textContent=t;e.className=ok?'msgok':'msgbad'}
|
|
|
|
function msg(t,ok){let e=document.getElementById('message');e.textContent=t;e.className=ok?'msgok':'msgbad'}
|
|
|
|
refresh();setInterval(refresh,1000);
|
|
|
|
refresh();setInterval(refresh,1000);
|
|
|
|
</script></body></html>'''
|
|
|
|
</script></body></html>'''
|
|
|
|