From a3ef2170d688e704c37667ea2dfd9343322c551c Mon Sep 17 00:00:00 2001
From: def
Date: Mon, 18 May 2026 01:59:51 +0000
Subject: [PATCH] Bump OTB Billing to v2.0.0 with last reconcile run
---
PROJECT_STATE.md | 34 ++++++++++++++++++++++++++++++++++
README.md | 24 ++++++++++++++++++++++++
VERSION | 2 +-
backend/health.py | 36 ++++++++++++++++++++++++++++++++++--
templates/health.html | 2 ++
5 files changed, 95 insertions(+), 3 deletions(-)
diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md
index fa14042..14a9805 100644
--- a/PROJECT_STATE.md
+++ b/PROJECT_STATE.md
@@ -1,5 +1,39 @@
# PROJECT_STATE - OTB Billing
+## v2.0.0 - 2026-05-18
+
+Current state:
+- OTB Billing is running on outsidethedb.
+- Service: otb_billing.service.
+- App port: 5050.
+- Project path: /home/def/otb_billing.
+- Current version: v2.0.0.
+- /health renders the standard health grid with Status, Database, Uptime, Load Average, Memory, Disk, Operations Bal, Treasury Bal, and Crypto Reconcile.
+- Operations Bal wallet: 0x44f6c44C42e6ae0392E7289F032384C0d37F56D5.
+- Treasury Bal wallet: 0xbe1fdc8c69f712d62cfcd3bf23f636de1dbd213f.
+- Wallet coin names are clickable explorer links:
+ - USDC uses Arbiscan
+ - ETH uses Etherscan
+ - ETHO uses explorer.ethoprotocol.com
+ - EGAZ and ETI use explorer.etica-stats.org
+- Crypto Reconcile card shows:
+ - timer status
+ - service status
+ - last run timestamp
+ - last result
+ - pending payment count
+ - confirmed today count
+ - stale pending count
+ - Reconcile Now button
+- Crypto reconciliation worker:
+ - Timer: otb-billing-crypto-reconcile.timer
+ - Service: otb-billing-crypto-reconcile.service
+ - Script: /home/def/otb_billing/scripts/crypto_reconciliation_worker.py
+ - Timer cadence: every 15 minutes
+- The reconcile service is one-shot; inactive between runs is normal when the timer is active.
+
+# PROJECT_STATE - OTB Billing
+
## v1.4.0 - 2026-05-18
Current state:
diff --git a/README.md b/README.md
index c9b9a5c..ac38801 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,27 @@
+# OTB Billing - v2.0.0
+
+Build date: 2026-05-18
+
+## v2.0.0 changes
+
+- Promoted OTB Billing to v2.0.0.
+- Added Last Run and Last Result fields to the Crypto Reconcile health card.
+- Last Run is read from systemd one-shot service timestamps for otb-billing-crypto-reconcile.service.
+- Confirmed Crypto Reconcile remains a normal card in the main /health grid.
+- Existing health cards remain:
+ - Status
+ - Database
+ - Uptime
+ - Load Average
+ - Memory
+ - Disk
+ - Operations Bal
+ - Treasury Bal
+ - Crypto Reconcile
+- Existing Operations Bal and Treasury Bal cards retain clickable explorer links for USDC, ETH, ETHO, EGAZ, and ETI.
+- Reconcile Now button remains available on /health to manually start the crypto reconciliation worker.
+- Footer/app version bumped from v1.4.0 to v2.0.0.
+
# OTB Billing - v1.4.0
Build date: 2026-05-18
diff --git a/VERSION b/VERSION
index 0d0c52f..46b105a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v1.4.0
+v2.0.0
diff --git a/backend/health.py b/backend/health.py
index 176a6c5..87cf477 100644
--- a/backend/health.py
+++ b/backend/health.py
@@ -490,6 +490,18 @@ def _try_payment_stats_from_db(app):
pass
+def _parse_systemctl_show(stdout):
+ parsed = {}
+
+ for line in str(stdout or "").splitlines():
+ if "=" not in line:
+ continue
+ key, value = line.split("=", 1)
+ parsed[key.strip()] = value.strip()
+
+ return parsed
+
+
def _crypto_reconcile_status(app):
service_name = "otb-billing-crypto-reconcile.service"
timer_name = "otb-billing-crypto-reconcile.timer"
@@ -507,16 +519,33 @@ def _crypto_reconcile_status(app):
timer_name,
])
- last_logs = _systemctl_value([
+ service_show = _systemctl_value([
"show",
service_name,
"--property=ActiveEnterTimestamp",
"--property=InactiveEnterTimestamp",
"--property=ExecMainStatus",
"--property=Result",
+ "--property=NRestarts",
"--no-pager",
])
+ parsed = _parse_systemctl_show(service_show["stdout"])
+
+ active_entered = parsed.get("ActiveEnterTimestamp") or ""
+ inactive_entered = parsed.get("InactiveEnterTimestamp") or ""
+ result = parsed.get("Result") or "unknown"
+ exec_status = parsed.get("ExecMainStatus") or "unknown"
+
+ # For a oneshot timer service, InactiveEnterTimestamp is usually the useful
+ # "last completed" timestamp. Fall back to ActiveEnterTimestamp if needed.
+ last_run = inactive_entered
+ if not last_run or last_run.lower() in ("n/a", "never"):
+ last_run = active_entered
+
+ if not last_run:
+ last_run = "unknown"
+
return {
"service_name": service_name,
"timer_name": timer_name,
@@ -525,7 +554,10 @@ def _crypto_reconcile_status(app):
"service_active": service_active["stdout"] or "unknown",
"service_enabled": service_enabled["stdout"] or "unknown",
"timer_line": timer_list["stdout"],
- "service_details": last_logs["stdout"],
+ "service_details": service_show["stdout"],
+ "last_run": last_run,
+ "last_result": result,
+ "last_exit_status": exec_status,
"payment_stats": _try_payment_stats_from_db(app),
}
diff --git a/templates/health.html b/templates/health.html
index 7f25195..755278c 100644
--- a/templates/health.html
+++ b/templates/health.html
@@ -222,6 +222,8 @@
{% endif %}
Service: {{ health.crypto_reconcile.service_active }}
+ Last Run: {{ health.crypto_reconcile.last_run }}
+ Last Result: {{ health.crypto_reconcile.last_result }}
{% if health.crypto_reconcile.payment_stats and health.crypto_reconcile.payment_stats.available %}
Pending: {{ health.crypto_reconcile.payment_stats.pending }}