Fix VPS async cash ledger loading
This commit is contained in:
@@ -269,6 +269,7 @@ async def cash_payment_ledgers(request: Request):
|
|||||||
if not selected_client:
|
if not selected_client:
|
||||||
return JSONResponse({"ok": False, "error": "Select a valid client."}, status_code=400)
|
return JSONResponse({"ok": False, "error": "Select a valid client."}, status_code=400)
|
||||||
tally_guid = str(form.get("tally_guid") or "").strip()
|
tally_guid = str(form.get("tally_guid") or "").strip()
|
||||||
|
company_name = str(form.get("company_name") or "").strip()
|
||||||
date_from = str(form.get("date_from") or date.today().isoformat()).strip()
|
date_from = str(form.get("date_from") or date.today().isoformat()).strip()
|
||||||
force_refresh = str(form.get("force_refresh") or "").strip().lower() in {"1","true","yes","on"}
|
force_refresh = str(form.get("force_refresh") or "").strip().lower() in {"1","true","yes","on"}
|
||||||
if not tally_guid:
|
if not tally_guid:
|
||||||
@@ -284,6 +285,7 @@ async def cash_payment_ledgers(request: Request):
|
|||||||
{
|
{
|
||||||
**_accounting_storage_payload(selected_client, fy),
|
**_accounting_storage_payload(selected_client, fy),
|
||||||
"tally_guid": tally_guid,
|
"tally_guid": tally_guid,
|
||||||
|
"company_name": company_name,
|
||||||
"force_refresh": force_refresh,
|
"force_refresh": force_refresh,
|
||||||
},
|
},
|
||||||
timeout_seconds=20,
|
timeout_seconds=20,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<option value="">Select open/mapped company</option>
|
<option value="">Select open/mapped company</option>
|
||||||
{% if live_result and live_result.accounting %}
|
{% if live_result and live_result.accounting %}
|
||||||
{% for mapping in live_result.accounting.mappings or [] %}
|
{% for mapping in live_result.accounting.mappings or [] %}
|
||||||
<option value="{{ mapping.tally_guid }}" {% if selected_tally_guid == mapping.tally_guid %}selected{% endif %}>{{ mapping.company_name or mapping.tally_company_name or mapping.tally_guid }}</option>
|
<option value="{{ mapping.tally_guid }}" data-company-name="{{ mapping.company_name or mapping.tally_company_name or '' }}" {% if selected_tally_guid == mapping.tally_guid %}selected{% endif %}>{{ mapping.company_name or mapping.tally_company_name or mapping.tally_guid }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
@@ -259,6 +259,10 @@
|
|||||||
const fd = new FormData(form);
|
const fd = new FormData(form);
|
||||||
if (!fd.get('client_id') || !fd.get('tally_guid')) { ledgerHelp.textContent = 'Select a client and Tally company first.'; return; }
|
if (!fd.get('client_id') || !fd.get('tally_guid')) { ledgerHelp.textContent = 'Select a client and Tally company first.'; return; }
|
||||||
fd.set('csrf_token', document.getElementById('cash-csrf-token').value);
|
fd.set('csrf_token', document.getElementById('cash-csrf-token').value);
|
||||||
|
const companySelect = form.querySelector('select[name="tally_guid"]');
|
||||||
|
const companyOption = companySelect && companySelect.options[companySelect.selectedIndex];
|
||||||
|
const companyName = companyOption ? String(companyOption.dataset.companyName || companyOption.textContent || '').trim() : '';
|
||||||
|
if (companyName) fd.set('company_name', companyName);
|
||||||
if(forceRefresh) fd.set('force_refresh','1');
|
if(forceRefresh) fd.set('force_refresh','1');
|
||||||
ledgerButton.disabled = true; if(ledgerRefreshButton) ledgerRefreshButton.disabled=true;
|
ledgerButton.disabled = true; if(ledgerRefreshButton) ledgerRefreshButton.disabled=true;
|
||||||
ledgerHelp.textContent = forceRefresh ? 'Starting background refresh from Tally…' : 'Loading cached ledgers or starting background Tally sync…';
|
ledgerHelp.textContent = forceRefresh ? 'Starting background refresh from Tally…' : 'Loading cached ledgers or starting background Tally sync…';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.22.15"
|
ERP_LOCAL_AGENT_VERSION = "1.22.16"
|
||||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "1.22.15"
|
__version__ = "1.22.16"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -794,17 +794,20 @@ class AgentCommandProcessor:
|
|||||||
def _cash_payment_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _cash_payment_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Backward-compatible non-blocking ledger request.
|
"""Backward-compatible non-blocking ledger request.
|
||||||
|
|
||||||
If a cached ledger master exists it is returned immediately. Otherwise a
|
This path must never perform a live Tally status call before acknowledging
|
||||||
background master-sync job is started and the caller receives its job id.
|
the ERP request. A cached master is returned immediately; otherwise the
|
||||||
|
background start path is used.
|
||||||
"""
|
"""
|
||||||
client_id = int(payload.get("client_id"))
|
client_id = int(payload.get("client_id"))
|
||||||
company, company_name = self._resolve_open_company(payload)
|
tally_guid = str(payload.get("tally_guid") or "").strip()
|
||||||
tally_guid = str(company.get("guid") or payload.get("tally_guid") or "").strip()
|
if not tally_guid:
|
||||||
|
raise ValueError("Mapped Tally company GUID is required.")
|
||||||
|
company_name = str(payload.get("company_name") or "").strip()
|
||||||
cached = self._cash_payment_cached_ledgers(client_id, tally_guid)
|
cached = self._cash_payment_cached_ledgers(client_id, tally_guid)
|
||||||
if cached:
|
if cached:
|
||||||
return {
|
return {
|
||||||
"status": "cached",
|
"status": "cached",
|
||||||
"company": company,
|
"company": {"guid": tally_guid, "name": company_name},
|
||||||
"company_name": company_name,
|
"company_name": company_name,
|
||||||
"ledgers": cached,
|
"ledgers": cached,
|
||||||
"cash_candidates": [x for x in cached if x["is_cash_candidate"]],
|
"cash_candidates": [x for x in cached if x["is_cash_candidate"]],
|
||||||
@@ -815,18 +818,36 @@ class AgentCommandProcessor:
|
|||||||
return self._cash_payment_ledgers_start(payload)
|
return self._cash_payment_ledgers_start(payload)
|
||||||
|
|
||||||
def _cash_payment_ledgers_start(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _cash_payment_ledgers_start(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Create/return a ledger-master job without blocking on Tally.
|
||||||
|
|
||||||
|
Live company validation and the ledger export happen only inside the
|
||||||
|
background worker. This guarantees the VPS receives an immediate job ACK
|
||||||
|
even when Tally needs a minute or more to enumerate a large ledger master.
|
||||||
|
"""
|
||||||
client_id = int(payload.get("client_id"))
|
client_id = int(payload.get("client_id"))
|
||||||
company, company_name = self._resolve_open_company(payload)
|
tally_guid = str(payload.get("tally_guid") or "").strip()
|
||||||
tally_guid = str(company.get("guid") or payload.get("tally_guid") or "").strip()
|
if not tally_guid:
|
||||||
|
raise ValueError("Mapped Tally company GUID is required.")
|
||||||
|
expected_company_name = str(payload.get("company_name") or "").strip()
|
||||||
self._ensure_cash_ledger_schema(client_id)
|
self._ensure_cash_ledger_schema(client_id)
|
||||||
|
|
||||||
cached = self._cash_payment_cached_ledgers(client_id, tally_guid)
|
cached = self._cash_payment_cached_ledgers(client_id, tally_guid)
|
||||||
force_refresh = bool(payload.get("force_refresh"))
|
force_refresh = bool(payload.get("force_refresh"))
|
||||||
if cached and not force_refresh:
|
if cached and not force_refresh:
|
||||||
|
cached_company = expected_company_name
|
||||||
|
if not cached_company:
|
||||||
|
with self.store.connect(client_id) as db:
|
||||||
|
row = db.execute(
|
||||||
|
"""SELECT company_name FROM cash_payment_ledger_cache
|
||||||
|
WHERE client_id=? AND tally_guid=? AND company_name<>''
|
||||||
|
ORDER BY synced_at_utc DESC LIMIT 1""",
|
||||||
|
(client_id, tally_guid),
|
||||||
|
).fetchone()
|
||||||
|
cached_company = str(row[0] or "") if row else ""
|
||||||
return {
|
return {
|
||||||
"status": "cached",
|
"status": "cached",
|
||||||
"company": company,
|
"company": {"guid": tally_guid, "name": cached_company},
|
||||||
"company_name": company_name,
|
"company_name": cached_company,
|
||||||
"ledgers": cached,
|
"ledgers": cached,
|
||||||
"cash_candidates": [x for x in cached if x["is_cash_candidate"]],
|
"cash_candidates": [x for x in cached if x["is_cash_candidate"]],
|
||||||
"ledger_count": len(cached),
|
"ledger_count": len(cached),
|
||||||
@@ -850,7 +871,7 @@ class AgentCommandProcessor:
|
|||||||
"""INSERT INTO cash_payment_ledger_jobs
|
"""INSERT INTO cash_payment_ledger_jobs
|
||||||
(job_id, client_id, tally_guid, company_name, status, stage, percent, ledger_count, error, started_at_utc, finished_at_utc)
|
(job_id, client_id, tally_guid, company_name, status, stage, percent, ledger_count, error, started_at_utc, finished_at_utc)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
(job_id, client_id, tally_guid, company_name, "queued", "Queued for Tally ledger master sync", 2, 0, "", now, ""),
|
(job_id, client_id, tally_guid, expected_company_name, "queued", "Queued for Tally ledger master sync", 2, 0, "", now, ""),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -860,16 +881,33 @@ class AgentCommandProcessor:
|
|||||||
"status": "queued",
|
"status": "queued",
|
||||||
"stage": "Queued for Tally ledger master sync",
|
"stage": "Queued for Tally ledger master sync",
|
||||||
"percent": 2,
|
"percent": 2,
|
||||||
"company_name": company_name,
|
"company_name": expected_company_name,
|
||||||
"ledger_count": 0,
|
"ledger_count": 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
|
company_name = expected_company_name
|
||||||
try:
|
try:
|
||||||
with self.store.connect(client_id) as db:
|
with self.store.connect(client_id) as db:
|
||||||
db.execute(
|
db.execute(
|
||||||
"UPDATE cash_payment_ledger_jobs SET status='running',stage=?,percent=? WHERE job_id=?",
|
"UPDATE cash_payment_ledger_jobs SET status='running',stage=?,percent=? WHERE job_id=?",
|
||||||
("Connected to Tally; reading ledger master", 15, job_id),
|
("Validating mapped company with Tally", 8, job_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
_cash_progress_snapshot({
|
||||||
|
"job_id": job_id, "job_kind": "ledger_master", "status": "running",
|
||||||
|
"stage": "Validating mapped company with Tally", "percent": 8,
|
||||||
|
"company_name": company_name, "ledger_count": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Potentially slow Tally status/preflight is deliberately inside the
|
||||||
|
# worker so it can never delay the ERP command acknowledgement.
|
||||||
|
company, live_company_name = self._resolve_open_company(payload)
|
||||||
|
company_name = live_company_name
|
||||||
|
with self.store.connect(client_id) as db:
|
||||||
|
db.execute(
|
||||||
|
"UPDATE cash_payment_ledger_jobs SET company_name=?,stage=?,percent=? WHERE job_id=?",
|
||||||
|
(company_name, "Connected to Tally; reading ledger master", 15, job_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
_cash_progress_snapshot({
|
_cash_progress_snapshot({
|
||||||
@@ -877,8 +915,7 @@ class AgentCommandProcessor:
|
|||||||
"stage": "Connected to Tally; reading ledger master", "percent": 15,
|
"stage": "Connected to Tally; reading ledger master", "percent": 15,
|
||||||
"company_name": company_name, "ledger_count": 0,
|
"company_name": company_name, "ledger_count": 0,
|
||||||
})
|
})
|
||||||
# The Tally export is intentionally isolated in this background thread
|
|
||||||
# so websocket/tunnel keepalives and the HTTP command ACK remain responsive.
|
|
||||||
rows = self.tally.export_master_collection(company_name, "ledgers") or []
|
rows = self.tally.export_master_collection(company_name, "ledgers") or []
|
||||||
ledgers = self._cash_ledger_rows(rows)
|
ledgers = self._cash_ledger_rows(rows)
|
||||||
_cash_progress_snapshot({
|
_cash_progress_snapshot({
|
||||||
@@ -904,9 +941,9 @@ class AgentCommandProcessor:
|
|||||||
)
|
)
|
||||||
db.execute(
|
db.execute(
|
||||||
"""UPDATE cash_payment_ledger_jobs
|
"""UPDATE cash_payment_ledger_jobs
|
||||||
SET status='completed',stage=?,percent=100,ledger_count=?,finished_at_utc=?
|
SET status='completed',company_name=?,stage=?,percent=100,ledger_count=?,finished_at_utc=?
|
||||||
WHERE job_id=?""",
|
WHERE job_id=?""",
|
||||||
(f"{len(ledgers)} ledgers cached and ready", len(ledgers), synced, job_id),
|
(company_name, f"{len(ledgers)} ledgers cached and ready", len(ledgers), synced, job_id),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
@@ -945,6 +982,9 @@ class AgentCommandProcessor:
|
|||||||
with _CASH_LEDGER_THREADS_LOCK:
|
with _CASH_LEDGER_THREADS_LOCK:
|
||||||
_CASH_LEDGER_THREADS[job_id] = thread
|
_CASH_LEDGER_THREADS[job_id] = thread
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
# Important: do not call Tally or wait for the worker here. Return the job
|
||||||
|
# record immediately so the VPS can poll progress independently.
|
||||||
return self._cash_payment_ledgers_status({**payload, "job_id": job_id})
|
return self._cash_payment_ledgers_status({**payload, "job_id": job_id})
|
||||||
|
|
||||||
def _cash_payment_ledgers_status(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _cash_payment_ledgers_status(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ 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 isLedger=String(j.job_kind||'')==='ledger_master';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='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',(status==='completed'?'done':'run'));setCashStep('cs3','');setCashStep('cs4','');setCashStep('cs5',(status==='completed'?'done':''));}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…';let isLedger=String(j.job_kind||'')==='ledger_master';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='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 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';
|
||||||
|
|||||||
Reference in New Issue
Block a user