From 9838c6c615bef6e030e0978735ccf6bb28f7b82a Mon Sep 17 00:00:00 2001 From: def Date: Sat, 18 Apr 2026 01:44:59 +0000 Subject: [PATCH] bump v0.6.1 - add service templates system --- PROJECT_STATE.md | 12 + README.md | 13 + VERSION | 2 +- backend/app.py | 213 +++- backend/routes/portal_service_launch.py | 57 + logs/crypto_reconciliation_worker.log | 657 +++++++++++ logs/invoice_reminder_worker.log | 14 + patch.sh | 1332 +++++++++++++++++++++-- patch1.sh | 115 +- patch2.sh | 102 -- patch3.sh | 174 --- patch4.sh | 143 --- patch5.sh | 272 ----- patch6.sh | 78 -- patch7.sh | 157 --- patch8.sh | 125 --- static/css/brand.css | 27 + static/css/style.css | 166 +++ templatepatch.sh | 11 + templates/service_templates/edit.html | 98 ++ templates/service_templates/list.html | 48 + templates/service_templates/new.html | 98 ++ templates/services/edit.html | 51 +- templates/services/list.html | 7 +- templates/services/new.html | 52 +- 25 files changed, 2842 insertions(+), 1182 deletions(-) create mode 100644 backend/routes/portal_service_launch.py delete mode 100755 patch2.sh delete mode 100755 patch3.sh delete mode 100755 patch4.sh delete mode 100755 patch5.sh delete mode 100755 patch6.sh delete mode 100755 patch7.sh delete mode 100755 patch8.sh create mode 100644 templatepatch.sh create mode 100644 templates/service_templates/edit.html create mode 100644 templates/service_templates/list.html create mode 100644 templates/service_templates/new.html diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index d089cef..fe14a18 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,3 +1,15 @@ +## v0.6.1 - Service Templates Phase 1 + +- Added service_templates table +- Implemented admin CRUD routes in app.py +- Added templates UI pages +- Integrated template selection into services/new and services/edit +- Auto-fill JS implemented for template selection + +Status: FUNCTIONAL +Next: link templates to services + invoice integration + +--- # Project State Update - v0.6.0 Updated: 2026-04-11 01:49:22 UTC diff --git a/README.md b/README.md index 997b297..64bd35a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +## v0.6.1 - 2026-04-18 + +### Added +- Service Templates system (standalone pricing catalog) +- Admin UI for managing reusable service pricing +- Template selector on service create/edit pages (auto-fill fields) + +### Notes +- Templates are not yet linked to services via template_id (planned) +- Setup amount stored in templates for future invoice integration +- Maintains compatibility with existing services table + +--- ## v0.6.0 - 2026-04-11 01:49:22 ### Highlights diff --git a/VERSION b/VERSION index 60f6343..1490961 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.6.0 +v0.6.1 diff --git a/backend/app.py b/backend/app.py index cbbc1bd..afc2fbf 100644 --- a/backend/app.py +++ b/backend/app.py @@ -12,6 +12,7 @@ from pathlib import Path from email.message import EmailMessage from dateutil.relativedelta import relativedelta from routes.portal_services import portal_services_bp +from routes.portal_service_launch import portal_service_launch_bp from io import BytesIO, StringIO import csv @@ -42,6 +43,7 @@ app = Flask( static_folder="../static", ) app.register_blueprint(portal_services_bp) +app.register_blueprint(portal_service_launch_bp) app.config["OTB_HEALTH_DB_CONNECTOR"] = get_db_connection TERMS_VERSION = "v1.0" @@ -2774,8 +2776,15 @@ def new_service(): cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") clients = cursor.fetchall() + cursor.execute(""" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + """) + templates = cursor.fetchall() conn.close() - return render_template("services/new.html", clients=clients) + return render_template("services/new.html", clients=clients, templates=templates) @app.route("/services/edit/", methods=["GET", "POST"]) def edit_service(service_id): @@ -2832,9 +2841,16 @@ def edit_service(service_id): cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") clients = cursor.fetchall() + cursor.execute(""" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + """) + templates = cursor.fetchall() conn.close() - return render_template("services/edit.html", service=service, clients=clients, errors=errors) + return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=errors) update_cursor = conn.cursor() update_cursor.execute(""" @@ -2875,13 +2891,204 @@ def edit_service(service_id): cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") clients = cursor.fetchall() + cursor.execute(""" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + """) + templates = cursor.fetchall() conn.close() if not service: return "Service not found", 404 - return render_template("services/edit.html", service=service, clients=clients, errors=[]) + return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[]) + + +@app.route("/service-templates") +def service_templates(): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(""" + SELECT * + FROM service_templates + ORDER BY id DESC + """) + templates = cursor.fetchall() + conn.close() + + return render_template("service_templates/list.html", templates=templates) + + +@app.route("/service-templates/new", methods=["GET", "POST"]) +def new_service_template(): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + errors = [] + + if request.method == "POST": + template_name = request.form.get("template_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + setup_amount = request.form.get("setup_amount", "").strip() + description = request.form.get("description", "").strip() + is_active = 1 if request.form.get("is_active") == "1" else 0 + + if not template_name: + errors.append("Template name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if recurring_amount == "": + errors.append("Recurring amount is required.") + if setup_amount == "": + errors.append("Setup amount is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + setup_amount_value = float(setup_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + if setup_amount_value < 0: + errors.append("Setup amount cannot be negative.") + except ValueError: + errors.append("Amounts must be valid numbers.") + + if not errors: + insert_cursor = conn.cursor() + insert_cursor.execute(""" + INSERT INTO service_templates + ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description, + is_active + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """, ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description or None, + is_active + )) + conn.commit() + conn.close() + return redirect("/service-templates") + + conn.close() + return render_template("service_templates/new.html", errors=errors) + + +@app.route("/service-templates/edit/", methods=["GET", "POST"]) +def edit_service_template(template_id): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + template_name = request.form.get("template_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + setup_amount = request.form.get("setup_amount", "").strip() + description = request.form.get("description", "").strip() + is_active = 1 if request.form.get("is_active") == "1" else 0 + + errors = [] + + if not template_name: + errors.append("Template name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if recurring_amount == "": + errors.append("Recurring amount is required.") + if setup_amount == "": + errors.append("Setup amount is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + setup_amount_value = float(setup_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + if setup_amount_value < 0: + errors.append("Setup amount cannot be negative.") + except ValueError: + errors.append("Amounts must be valid numbers.") + + if errors: + cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,)) + template = cursor.fetchone() + conn.close() + return render_template("service_templates/edit.html", template=template, errors=errors) + + update_cursor = conn.cursor() + update_cursor.execute(""" + UPDATE service_templates + SET template_name = %s, + service_type = %s, + billing_cycle = %s, + currency_code = %s, + recurring_amount = %s, + setup_amount = %s, + description = %s, + is_active = %s + WHERE id = %s + """, ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description or None, + is_active, + template_id + )) + conn.commit() + conn.close() + return redirect("/service-templates") + + cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,)) + template = cursor.fetchone() + conn.close() + + if not template: + return "Service template not found", 404 + return render_template("service_templates/edit.html", template=template, errors=[]) diff --git a/backend/routes/portal_service_launch.py b/backend/routes/portal_service_launch.py new file mode 100644 index 0000000..f4fffb8 --- /dev/null +++ b/backend/routes/portal_service_launch.py @@ -0,0 +1,57 @@ +import os +import json +import time +import uuid +import hmac +import base64 +import hashlib +from urllib.parse import quote + +from flask import Blueprint, session, redirect, url_for, flash + +portal_service_launch_bp = Blueprint("portal_service_launch", __name__) + +def _b64url_encode(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8") + +def _sign_payload(payload: dict, secret: str) -> str: + payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8") + payload_b64 = _b64url_encode(payload_json) + sig = hmac.new(secret.encode("utf-8"), payload_b64.encode("utf-8"), hashlib.sha256).digest() + sig_b64 = _b64url_encode(sig) + return f"{payload_b64}.{sig_b64}" + +@portal_service_launch_bp.route("/portal/services/follow-me-launch") +def portal_follow_me_launch(): + portal_client_id = session.get("portal_client_id") + portal_email = session.get("portal_email") + + if not portal_client_id or not portal_email: + flash("Please sign in to launch Follow-me.", "warning") + return redirect(url_for("portal_login")) + + secret = os.getenv("FMV2_HANDOFF_SECRET", "").strip() + base_url = os.getenv("FMV2_BASE_URL", "https://follow-me.outsidethebox.top").strip().rstrip("/") + ttl_seconds = int(os.getenv("FMV2_HANDOFF_TTL_SECONDS", "300")) + + if not secret: + flash("Follow-me launch is not configured. Missing FMV2_HANDOFF_SECRET.", "danger") + return redirect(url_for("portal_services.portal_services_home")) + + now = int(time.time()) + payload = { + "iss": "otb-billing", + "aud": "fmv2", + "jti": str(uuid.uuid4()), + "iat": now, + "exp": now + ttl_seconds, + "portal_client_id": int(portal_client_id), + "portal_email": portal_email, + "portal_contact_name": session.get("portal_contact_name", ""), + "portal_company_name": session.get("portal_company_name", ""), + "return_to": "/dashboard", + } + + token = _sign_payload(payload, secret) + target = f"{base_url}/auth/portal-handoff?token={quote(token)}" + return redirect(target) diff --git a/logs/crypto_reconciliation_worker.log b/logs/crypto_reconciliation_worker.log index cf0f1d0..458ccbc 100644 --- a/logs/crypto_reconciliation_worker.log +++ b/logs/crypto_reconciliation_worker.log @@ -1935,3 +1935,660 @@ crypto reconciliation complete mode=interval scanned=0 resolved=0 flagged=0 [2026-04-11T01:03:40.531627+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 [2026-04-11T01:19:30.117863+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 [2026-04-11T01:34:40.057937+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T01:49:53.278229+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T02:05:40.141029+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T02:20:40.511784+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T02:36:40.092185+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T02:52:30.100577+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T03:07:40.051218+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T03:22:40.464994+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T03:38:40.067342+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T03:54:40.065044+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T04:00:16.366684+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-11T04:09:40.514148+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T04:25:30.086771+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T04:40:40.066913+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T04:56:40.162680+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T05:12:40.067304+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T05:28:30.119535+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T05:43:40.086247+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T05:58:40.506573+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T06:14:39.952502+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T06:30:40.130552+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T06:46:29.988648+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T07:01:40.156235+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T07:16:40.363390+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T07:32:40.192332+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T07:47:40.438257+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T08:02:45.124915+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T08:18:40.080213+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T08:34:30.107423+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T08:49:39.947765+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T09:04:40.560552+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T09:20:40.097301+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T09:36:40.057695+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T09:51:40.513234+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T10:07:30.131923+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T10:22:39.984910+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T10:37:40.647453+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T10:53:40.060926+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T11:08:40.507498+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T11:24:40.043846+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T11:39:40.619284+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T11:55:30.000581+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T12:10:40.126529+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T12:26:40.120968+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T12:42:40.086522+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T12:58:30.060397+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T13:13:40.115147+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T13:28:40.452990+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T13:44:40.090979+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T14:00:40.096525+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T14:16:04.314597+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T14:31:30.439188+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T14:46:40.069365+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T15:02:39.983170+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T15:17:40.501338+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T15:33:40.111376+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T15:48:40.507935+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T16:04:30.086809+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T16:19:40.016152+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T16:34:40.424593+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T16:50:40.280789+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T17:06:40.776224+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T17:21:40.554451+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T17:37:30.029263+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T17:52:40.072909+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T18:08:40.024084+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T18:24:40.218531+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T18:40:30.163810+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T18:55:40.137094+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T19:10:40.523993+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T19:26:40.179418+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T19:41:40.485329+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T19:57:40.231886+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T20:12:40.567083+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T20:28:30.470593+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T20:43:40.063669+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T20:58:40.589750+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T21:13:47.418613+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T21:28:49.192899+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T21:44:17.661196+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T21:59:20.653478+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T22:14:40.005300+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T22:29:40.879564+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T22:45:40.050664+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T23:00:40.665970+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T23:16:04.535194+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T23:31:30.137037+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-11T23:46:40.094731+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T00:02:30.578525+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T00:17:40.037746+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T00:32:40.545409+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T00:48:59.925769+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T01:04:30.253092+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T01:19:40.209754+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T01:34:40.534930+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T01:50:40.076983+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T02:06:40.104425+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T02:22:29.942937+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T02:37:40.144848+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T02:52:40.452520+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T03:08:40.123605+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T03:24:40.028986+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T03:40:30.173497+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T03:55:41.914541+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T04:00:21.795042+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-12T04:10:40.562699+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T04:26:40.429861+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T04:42:40.106792+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T04:58:30.017161+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T05:13:40.100404+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T05:28:40.454529+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T05:44:40.149869+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T06:00:40.110709+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T06:15:43.488132+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T06:31:30.075767+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T06:46:40.079531+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T07:02:39.897594+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T07:18:40.164837+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T07:33:40.464008+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T07:49:30.051976+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T08:04:39.960982+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T08:19:40.551864+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T08:34:58.262670+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T08:50:40.011084+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T09:06:40.010273+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T09:21:40.690376+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T09:37:30.029649+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T09:52:40.195534+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T10:08:40.033584+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T10:24:40.117275+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T10:39:40.422761+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T10:55:30.058239+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T11:10:30.378718+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T11:25:40.041984+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T11:40:40.471124+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T11:56:40.127899+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T12:11:40.370214+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T12:27:40.042869+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T12:42:40.411754+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T12:58:30.120712+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T13:13:40.021405+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T13:28:40.576070+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T13:44:06.436865+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T13:59:40.043153+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T14:14:40.429140+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T14:30:40.084291+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T14:46:29.961774+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T15:01:47.078756+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T15:16:40.505394+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T15:32:40.328812+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T15:48:40.368641+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T16:04:30.068324+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T16:19:40.116236+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T16:34:40.603246+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T16:50:40.134258+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T17:05:40.633867+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T17:21:40.283607+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T17:36:40.639503+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T17:52:30.132461+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T18:07:40.172052+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T18:22:40.696651+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T18:38:40.314289+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T18:54:40.205575+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T19:09:40.715936+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T19:25:30.261212+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T19:40:40.326370+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T19:55:40.518647+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T20:11:40.181355+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T20:26:40.546811+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T20:42:40.155997+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T20:57:46.487871+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T21:13:30.233855+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T21:28:40.263986+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T21:44:40.111985+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T22:00:40.056307+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T22:16:04.220906+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T22:31:30.146504+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T22:46:40.116999+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T23:02:40.067266+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T23:17:40.574968+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T23:33:40.391102+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-12T23:48:40.653404+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T00:04:30.114333+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T00:19:40.227423+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T00:34:40.863398+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T00:50:40.253081+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T01:05:40.619985+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T01:21:34.960211+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T01:36:40.217628+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T01:52:30.153935+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T02:07:40.107998+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T02:22:40.627306+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T02:38:40.084510+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T02:54:40.132440+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T03:10:30.493987+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T03:25:40.164450+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T03:40:40.617606+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T03:56:40.232397+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T04:00:16.376539+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-13T04:12:40.081352+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T04:28:30.205327+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T04:43:40.087733+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T04:58:40.581018+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T05:13:58.669260+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T05:29:40.086671+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T05:44:40.538480+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T06:00:40.165632+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T06:16:04.411910+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T06:31:30.180258+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T06:46:40.153858+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T07:02:40.145503+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T07:18:40.117573+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T07:34:30.219782+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T07:49:40.116982+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T08:04:40.536908+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T08:20:40.113003+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T08:36:40.212387+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T08:52:30.194906+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T09:07:40.167468+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T09:22:40.509196+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T09:38:40.147965+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T09:54:40.151269+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T10:09:40.637206+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T10:25:30.178376+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T10:40:40.283555+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T10:55:40.572205+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T11:11:40.151968+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T11:26:40.625718+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T11:42:40.159666+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T11:58:30.218765+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T12:13:40.249932+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T12:28:40.640427+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T12:45:01.025976+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T13:00:40.140187+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T13:15:40.665763+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T13:31:30.142440+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T13:46:40.204780+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T14:02:40.187097+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T14:18:40.246361+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T14:33:40.547954+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T14:49:30.231058+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T15:04:40.184076+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T15:19:40.641289+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T15:35:40.154715+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T15:50:40.614451+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T16:06:40.064727+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T16:22:30.093148+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T16:37:40.177133+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T16:52:40.731632+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T17:08:40.121628+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T17:24:40.121342+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T17:40:30.473598+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T17:55:40.162708+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T18:10:40.571705+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T18:26:40.159328+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T18:42:40.077560+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T18:57:40.627430+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T19:13:30.045751+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T19:28:40.162135+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T19:44:40.179203+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T20:00:40.594751+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T20:16:04.820002+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T20:31:30.211455+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T20:46:40.072757+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T21:01:40.628012+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T21:17:40.104155+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T21:32:40.608171+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T21:48:40.053725+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T22:04:30.044580+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T22:19:40.065314+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T22:34:40.568803+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T22:50:42.589389+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T23:06:39.990952+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T23:22:30.058945+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T23:37:40.171840+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-13T23:52:40.473641+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T00:08:40.146525+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T00:24:40.129412+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T00:40:30.104410+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T00:55:41.198391+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T01:10:40.602018+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T01:26:40.339871+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T01:41:40.662939+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T01:57:40.051325+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T02:12:40.513395+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T02:28:30.130054+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T02:43:40.125822+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T02:58:40.460979+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T03:14:40.069739+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T03:30:39.995071+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T03:45:40.576758+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T04:00:16.322016+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-14T04:01:08.189627+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T04:16:40.092801+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T04:32:40.038027+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T04:47:40.587003+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T05:03:40.137769+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T05:18:40.501566+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T05:34:30.038507+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T05:49:40.101287+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T06:04:40.489839+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T06:20:40.124456+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T06:36:40.073983+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T06:52:30.137896+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T07:07:40.134850+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T07:22:40.461562+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T07:38:40.153526+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T07:54:40.136863+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T08:09:40.607440+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T08:25:30.142733+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T08:40:40.085705+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T08:56:40.154320+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T09:11:40.890995+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T09:27:40.131570+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T09:42:40.385000+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T09:58:30.208141+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T10:13:40.024667+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T10:28:40.671432+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T10:44:40.080357+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T11:00:40.072741+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T11:16:04.134124+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T11:31:30.074047+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T11:46:40.131034+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T12:01:40.525845+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T12:17:39.995881+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T12:32:40.564288+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T12:48:40.052419+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T13:03:40.512932+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T13:19:29.988599+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T13:34:40.128212+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T13:49:40.588474+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T14:05:40.216612+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T14:20:40.448654+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T14:36:40.146717+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T14:52:30.003241+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T15:07:40.103542+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T15:22:40.454089+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T15:38:40.085868+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T15:53:40.545534+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T16:09:40.202305+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T16:24:40.508234+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T16:40:30.365942+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T16:55:40.110415+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T17:10:40.558907+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T17:26:40.111463+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T17:42:40.110612+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T17:58:29.942655+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T18:13:40.110569+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T18:28:40.454377+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T18:44:40.098088+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T19:00:40.172773+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T19:16:04.340096+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T19:31:30.176615+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T19:46:40.087879+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T20:02:39.959994+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T20:18:40.115070+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T20:34:30.016393+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T20:49:40.479728+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T21:04:40.466614+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T21:20:40.100538+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T21:36:40.141281+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T21:52:30.172831+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T22:07:40.105911+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T22:22:40.581717+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T22:38:16.499157+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T22:53:40.050041+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T23:08:40.542188+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T23:24:40.088437+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T23:39:40.537225+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-14T23:55:30.246617+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T00:10:40.041905+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T00:26:40.082559+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T00:41:40.525983+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T00:57:40.182121+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T01:12:40.451341+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T01:28:30.750018+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T01:43:40.067359+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T01:58:40.533974+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T02:14:40.036841+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T02:29:40.533772+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T02:45:40.088207+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T03:00:40.453470+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T03:16:04.527976+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T03:31:30.073369+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T03:46:40.034207+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T04:00:16.329498+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-15T04:02:30.532256+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T04:17:40.146127+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T04:32:40.582885+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T04:48:40.103885+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T05:04:30.152491+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T05:19:40.118147+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T05:34:40.515393+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T05:50:40.124500+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T06:06:40.115104+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T06:21:40.558161+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T06:37:30.084955+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T06:52:40.181313+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T07:08:40.251011+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T07:24:40.158739+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T07:40:30.237790+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T07:55:40.163564+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T08:10:40.667102+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T08:26:40.086813+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T08:41:40.564912+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T08:57:40.159326+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T09:12:40.492842+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T09:28:30.126958+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T09:43:40.126157+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T09:58:40.513637+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T10:14:40.249478+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T10:29:40.593862+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T10:45:40.049018+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T11:00:40.603942+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T11:16:04.790250+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T11:31:30.565720+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T11:46:40.167248+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T12:02:40.148348+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T12:17:40.602346+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T12:33:40.037647+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T12:48:40.540027+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T13:04:30.139847+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T13:19:40.229418+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T13:34:40.620420+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T13:50:40.127021+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T14:05:41.570919+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T14:21:40.096975+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T14:36:40.628309+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T14:52:30.193542+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T15:07:40.103992+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T15:22:40.519320+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T15:38:40.160700+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T15:54:40.168891+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T16:09:40.408635+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T16:25:30.163678+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T16:40:40.090405+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T16:55:40.489425+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T17:11:40.521318+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T17:26:40.595504+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T17:42:39.986413+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T17:58:30.186083+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T18:13:39.951306+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T18:28:40.533715+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T18:44:40.005624+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T19:00:40.196511+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T19:16:04.971426+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T19:31:30.325850+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T19:46:40.198759+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T20:02:40.243123+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T20:18:40.331580+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T20:34:30.132675+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T20:49:40.159038+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T21:04:40.603999+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T21:20:41.142916+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T21:36:40.209587+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T21:51:40.793016+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T22:07:30.289250+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T22:22:40.047317+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T22:38:40.108840+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T22:54:40.041707+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T23:10:30.105723+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T23:25:40.104900+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T23:40:40.594261+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-15T23:56:40.916082+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T00:12:40.077182+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T00:28:30.077395+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T00:43:40.228776+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T00:58:40.388740+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T01:14:40.159402+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T01:30:40.083836+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T01:46:30.159641+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T02:01:40.169410+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T02:16:40.575877+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T02:32:41.217708+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T02:47:40.658041+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T03:03:40.102230+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T03:18:41.143861+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T03:34:30.225126+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T03:49:40.188070+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T04:00:17.188815+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-16T04:04:41.838617+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T04:20:40.558289+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T04:36:41.006094+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T04:52:30.483668+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T05:07:40.312775+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T05:22:40.912025+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T05:38:40.289117+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T05:54:40.358820+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T06:13:47.466428+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T06:30:48.158996+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T06:40:40.667394+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T06:56:41.373109+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T07:12:40.337260+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T07:27:40.757140+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T07:43:30.318422+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T07:58:40.343998+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T08:14:40.323222+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T08:29:40.795881+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T08:45:40.321295+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T09:00:40.477596+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T09:16:04.327347+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T09:31:30.039179+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T09:46:40.085981+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T10:02:40.078941+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T10:17:40.629778+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T10:33:40.298899+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T10:48:40.482877+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T11:04:30.077799+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T11:19:40.181971+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T11:34:40.433806+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T11:50:40.183245+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T12:06:40.162793+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T12:21:40.683272+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T12:37:31.142792+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T12:52:41.439136+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T13:08:40.019640+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T13:24:40.211334+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T13:39:40.552158+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T13:55:30.046046+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T14:10:40.078872+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T14:26:40.106177+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T14:42:40.106651+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T14:58:30.193991+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T15:13:40.081950+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T15:28:40.537708+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T15:44:40.027718+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T15:59:40.613297+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T16:15:40.041673+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T16:30:40.645807+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T16:46:16.444296+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T17:01:30.154170+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T17:16:40.026347+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T17:32:40.048222+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T17:48:40.198617+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T18:03:40.562926+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T18:19:30.163839+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T18:34:40.175649+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T18:50:40.048942+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T19:06:40.101085+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T19:21:40.605054+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T19:37:30.185144+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T19:52:42.234639+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T20:08:40.185105+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T20:23:41.537646+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T20:39:40.154305+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T20:54:40.544021+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T21:10:30.179139+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T21:25:40.183454+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T21:40:40.533922+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T21:56:40.248117+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T22:12:40.129323+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T22:28:30.187039+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T22:43:40.118537+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T22:58:40.568764+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T23:14:02.515764+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T23:29:40.124931+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-16T23:44:40.554848+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T00:00:16.661776+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T00:15:40.075457+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T00:30:40.624456+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T00:46:30.203746+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T01:01:40.187141+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T01:16:40.545189+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T01:32:40.177048+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T01:47:40.627561+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T02:03:40.194047+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T02:18:40.559143+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T02:34:12.128162+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T02:49:30.774712+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T03:04:40.282699+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T03:20:40.304075+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T03:36:40.106180+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T03:52:19.006682+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T04:00:16.380527+00:00] crypto reconciliation complete mode=daily scanned=13 resolved=0 flagged=0 +[2026-04-17T04:07:30.122713+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T04:22:40.065421+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T04:38:40.073477+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T04:54:40.111120+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T05:10:30.021762+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T05:25:40.069102+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T05:40:40.481276+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T05:56:40.223278+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T06:11:40.468922+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T06:27:40.078534+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T06:42:40.535859+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T06:58:30.122045+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T07:13:40.199346+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T07:28:40.540270+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T07:44:40.097781+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T08:00:40.150693+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T08:16:04.863391+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T08:31:30.173016+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T08:46:40.203737+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T09:02:30.983235+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T09:17:40.138825+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T09:32:40.655583+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T09:48:41.117605+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T10:03:40.629448+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T10:19:30.104491+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T10:34:41.084177+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T10:50:40.120053+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T11:06:41.644493+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T11:22:30.171427+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T11:37:40.343789+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T11:52:40.549893+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T12:08:40.129540+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T12:24:40.167578+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T12:40:30.136398+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T12:55:40.099260+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T13:10:40.640777+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T13:26:40.150723+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T13:42:40.790928+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T13:58:30.403132+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T14:13:41.084522+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T14:28:40.513331+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T14:44:40.128032+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T15:00:40.198992+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T15:16:04.885444+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T15:31:30.084090+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T15:46:40.200551+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T16:01:40.606746+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T16:17:40.099968+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T16:32:41.253624+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T16:48:40.185046+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T17:04:30.142643+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T17:19:40.173649+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T17:34:40.474625+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T17:50:40.159707+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T18:06:40.022551+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T18:22:30.165062+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T18:37:40.048612+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T18:52:40.503816+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T19:08:40.143531+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T19:24:40.089949+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T19:40:30.244522+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T19:55:40.170838+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T20:10:40.599761+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T20:26:40.151328+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T20:42:40.038579+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T20:58:30.194446+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T21:13:40.050821+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T21:28:41.083581+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T21:44:40.630331+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T21:59:40.903358+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T22:15:40.219106+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T22:30:40.924894+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T22:46:30.397096+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T23:01:40.507226+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T23:16:40.901377+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T23:32:40.491545+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-17T23:48:40.339345+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T00:03:43.119979+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T00:18:50.218164+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T00:34:30.348400+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T00:49:40.481461+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T01:04:41.085178+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T01:19:51.265124+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 +[2026-04-18T01:35:40.601272+00:00] crypto reconciliation complete mode=interval scanned=13 resolved=0 flagged=0 diff --git a/logs/invoice_reminder_worker.log b/logs/invoice_reminder_worker.log index d23ad9d..0c04370 100644 --- a/logs/invoice_reminder_worker.log +++ b/logs/invoice_reminder_worker.log @@ -89,3 +89,17 @@ TypeError: 'NoneType' object is not subscriptable [2026-04-09T09:00:14.906306] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 [2026-04-10T09:00:14.318241] invoice_reminder_worker starting [2026-04-10T09:00:14.322748] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-11T09:00:14.565546] invoice_reminder_worker starting +[2026-04-11T09:00:14.570056] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-12T09:00:14.474926] invoice_reminder_worker starting +[2026-04-12T09:00:14.479514] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-13T09:00:14.394240] invoice_reminder_worker starting +[2026-04-13T09:00:14.398664] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-14T09:00:14.661985] invoice_reminder_worker starting +[2026-04-14T09:00:14.666496] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-15T09:00:14.627345] invoice_reminder_worker starting +[2026-04-15T09:00:14.631650] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-16T09:00:14.632830] invoice_reminder_worker starting +[2026-04-16T09:00:14.638602] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 +[2026-04-17T09:00:14.603365] invoice_reminder_worker starting +[2026-04-17T09:00:14.607816] checked=0 reminders_sent=0 overdue_sent=0 skipped=0 diff --git a/patch.sh b/patch.sh index 05219db..6d84714 100755 --- a/patch.sh +++ b/patch.sh @@ -1,80 +1,1278 @@ cd /home/def/otb_billing || exit 1 -set -e +cat > /tmp/otb_billing_service_templates_patch.sh <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +APP_ROOT="/home/def/otb_billing" +BACKUP_DIR="/home/def/backuphere" STAMP="$(date +%Y%m%d-%H%M%S)" +mkdir -p "$BACKUP_DIR" + +echo "===== sanity =====" +test -f "$APP_ROOT/backend/app.py" +test -f "$APP_ROOT/templates/services/list.html" +test -f "$APP_ROOT/templates/services/new.html" +test -f "$APP_ROOT/templates/services/edit.html" + echo "===== backups =====" -cp templates/includes/site_nav.html "templates/includes/site_nav.html.bak.${STAMP}" -cp templates/portal_login.html "templates/portal_login.html.bak.${STAMP}" -cp templates/portal_dashboard.html "templates/portal_dashboard.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_set_password.html "templates/portal_set_password.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_terms.html "templates/portal_terms.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_invoice_detail.html "templates/portal_invoice_detail.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_forgot_password.html "templates/portal_forgot_password.html.bak.${STAMP}" 2>/dev/null || true - -echo "===== replace shared nav include from otb-shared-brand =====" -cp /opt/otb/otb-shared-brand/fragments/header.html templates/includes/site_nav.html - -echo "===== create shared portal statusbar include =====" -cat > templates/includes/otb_statusbar.html <<'EOF' -
-
- All billing is calculated in 🇨🇦 CAD - - Crypto conversions use the OTB Oracle - - - Methods: - Credit Card (via Square), - e-Transfer, - and enabled crypto assets - -
-
-EOF +cp "$APP_ROOT/backend/app.py" "$BACKUP_DIR/app.py.service-templates.${STAMP}.bak" +cp "$APP_ROOT/templates/services/list.html" "$BACKUP_DIR/services-list.html.service-templates.${STAMP}.bak" +cp "$APP_ROOT/templates/services/new.html" "$BACKUP_DIR/services-new.html.service-templates.${STAMP}.bak" +cp "$APP_ROOT/templates/services/edit.html" "$BACKUP_DIR/services-edit.html.service-templates.${STAMP}.bak" +cp "$APP_ROOT/templates/includes/site_nav.html" "$BACKUP_DIR/site_nav.html.service-templates.${STAMP}.bak" || true + +mkdir -p "$APP_ROOT/templates/service_templates" -echo "===== replace inline portal statusbars with include =====" +echo "===== create database table if missing =====" +sudo mysql -D otb_billing <<'SQL' +CREATE TABLE IF NOT EXISTS service_templates ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + template_name VARCHAR(255) NOT NULL, + service_type ENUM('hosting','rpc','explorer','node','ipfs','consulting','crypto_infra','other') NOT NULL DEFAULT 'other', + billing_cycle ENUM('one_time','monthly','quarterly','yearly','manual') NOT NULL DEFAULT 'monthly', + currency_code VARCHAR(16) NOT NULL DEFAULT 'CAD', + recurring_amount DECIMAL(18,8) NOT NULL DEFAULT 0.00000000, + setup_amount DECIMAL(18,8) NOT NULL DEFAULT 0.00000000, + description TEXT DEFAULT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp(), + updated_at TIMESTAMP NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + KEY idx_service_templates_active (is_active), + KEY idx_service_templates_name (template_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +SQL + +echo "===== patch backend/app.py =====" python3 <<'PY' from pathlib import Path -import re - -files = [ - Path("templates/portal_login.html"), - Path("templates/portal_dashboard.html"), - Path("templates/portal_set_password.html"), - Path("templates/portal_terms.html"), - Path("templates/portal_invoice_detail.html"), - Path("templates/portal_forgot_password.html"), -] - -replacement = '{% include "includes/otb_statusbar.html" %}' - -for p in files: - if not p.exists(): - continue - text = p.read_text() - - new_text = re.sub( - r'
.*?
\s*', - replacement, - text, - count=1, - flags=re.S - ) - - if new_text != text: - p.write_text(new_text) - print(f"updated: {p}") - else: - print(f"no inline statusbar found: {p}") + +path = Path("/home/def/otb_billing/backend/app.py") +text = path.read_text() + +old_new_block = """@app.route("/services/new", methods=["GET", "POST"]) +def new_service(): + gate = admin_required() + if gate: + return gate + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + client_id = request.form["client_id"] + service_name = request.form["service_name"] + service_type = request.form["service_type"] + billing_cycle = request.form["billing_cycle"] + currency_code = request.form["currency_code"] + recurring_amount = request.form["recurring_amount"] + status = request.form["status"] + start_date = request.form["start_date"] or None + description = request.form["description"] + + cursor.execute("SELECT MAX(id) AS last_id FROM services") + result = cursor.fetchone() + last_number = result["last_id"] if result["last_id"] else 0 + service_code = generate_service_code(service_name, last_number) + + insert_cursor = conn.cursor() + insert_cursor.execute( + \"\"\" + INSERT INTO services + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + \"\"\", + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + ) + conn.commit() + conn.close() + + return redirect("/services") + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + conn.close() + return render_template("services/new.html", clients=clients) +""" + +new_new_block = """@app.route("/services/new", methods=["GET", "POST"]) +def new_service(): + gate = admin_required() + if gate: + return gate + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + client_id = request.form["client_id"] + service_name = request.form["service_name"] + service_type = request.form["service_type"] + billing_cycle = request.form["billing_cycle"] + currency_code = request.form["currency_code"] + recurring_amount = request.form["recurring_amount"] + status = request.form["status"] + start_date = request.form["start_date"] or None + description = request.form["description"] + + cursor.execute("SELECT MAX(id) AS last_id FROM services") + result = cursor.fetchone() + last_number = result["last_id"] if result["last_id"] else 0 + service_code = generate_service_code(service_name, last_number) + + insert_cursor = conn.cursor() + insert_cursor.execute( + \"\"\" + INSERT INTO services + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + \"\"\", + ( + client_id, + service_code, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date, + description + ) + ) + conn.commit() + conn.close() + + return redirect("/services") + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + cursor.execute(\"\"\" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + \"\"\") + templates = cursor.fetchall() + conn.close() + return render_template("services/new.html", clients=clients, templates=templates) +""" + +if old_new_block not in text: + raise SystemExit("Could not find /services/new block to replace.") +text = text.replace(old_new_block, new_new_block) + +old_edit_block = """@app.route("/services/edit/", methods=["GET", "POST"]) +def edit_service(service_id): + gate = admin_required() + if gate: + return gate + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + client_id = request.form.get("client_id", "").strip() + service_name = request.form.get("service_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + status = request.form.get("status", "").strip() + start_date = request.form.get("start_date", "").strip() + description = request.form.get("description", "").strip() + + errors = [] + + if not client_id: + errors.append("Client is required.") + if not service_name: + errors.append("Service name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if not recurring_amount: + errors.append("Recurring amount is required.") + if not status: + errors.append("Status is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + except ValueError: + errors.append("Recurring amount must be a valid number.") + + if errors: + cursor.execute(\"\"\" + SELECT s.*, c.company_name + FROM services s + LEFT JOIN clients c ON s.client_id = c.id + WHERE s.id = %s + \"\"\", (service_id,)) + service = cursor.fetchone() + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + + conn.close() + return render_template("services/edit.html", service=service, clients=clients, errors=errors) + + update_cursor = conn.cursor() + update_cursor.execute(\"\"\" + UPDATE services + SET client_id = %s, + service_name = %s, + service_type = %s, + billing_cycle = %s, + status = %s, + currency_code = %s, + recurring_amount = %s, + start_date = %s, + description = %s + WHERE id = %s + \"\"\", ( + client_id, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date or None, + description or None, + service_id + )) + conn.commit() + conn.close() + return redirect("/services") + + cursor.execute(\"\"\" + SELECT s.*, c.company_name + FROM services s + LEFT JOIN clients c ON s.client_id = c.id + WHERE s.id = %s + \"\"\", (service_id,)) + service = cursor.fetchone() + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + conn.close() + + if not service: + return "Service not found", 404 + + return render_template("services/edit.html", service=service, clients=clients, errors=[]) +""" + +new_edit_block = """@app.route("/services/edit/", methods=["GET", "POST"]) +def edit_service(service_id): + gate = admin_required() + if gate: + return gate + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + client_id = request.form.get("client_id", "").strip() + service_name = request.form.get("service_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + status = request.form.get("status", "").strip() + start_date = request.form.get("start_date", "").strip() + description = request.form.get("description", "").strip() + + errors = [] + + if not client_id: + errors.append("Client is required.") + if not service_name: + errors.append("Service name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if not recurring_amount: + errors.append("Recurring amount is required.") + if not status: + errors.append("Status is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + except ValueError: + errors.append("Recurring amount must be a valid number.") + + if errors: + cursor.execute(\"\"\" + SELECT s.*, c.company_name + FROM services s + LEFT JOIN clients c ON s.client_id = c.id + WHERE s.id = %s + \"\"\", (service_id,)) + service = cursor.fetchone() + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + cursor.execute(\"\"\" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + \"\"\") + templates = cursor.fetchall() + + conn.close() + return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=errors) + + update_cursor = conn.cursor() + update_cursor.execute(\"\"\" + UPDATE services + SET client_id = %s, + service_name = %s, + service_type = %s, + billing_cycle = %s, + status = %s, + currency_code = %s, + recurring_amount = %s, + start_date = %s, + description = %s + WHERE id = %s + \"\"\", ( + client_id, + service_name, + service_type, + billing_cycle, + status, + currency_code, + recurring_amount, + start_date or None, + description or None, + service_id + )) + conn.commit() + conn.close() + return redirect("/services") + + cursor.execute(\"\"\" + SELECT s.*, c.company_name + FROM services s + LEFT JOIN clients c ON s.client_id = c.id + WHERE s.id = %s + \"\"\", (service_id,)) + service = cursor.fetchone() + + cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name ASC") + clients = cursor.fetchall() + cursor.execute(\"\"\" + SELECT id, template_name, service_type, billing_cycle, currency_code, recurring_amount, setup_amount, description + FROM service_templates + WHERE is_active = 1 + ORDER BY template_name ASC + \"\"\") + templates = cursor.fetchall() + conn.close() + + if not service: + return "Service not found", 404 + + return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[]) +""" + +if old_edit_block not in text: + raise SystemExit("Could not find /services/edit block to replace.") +text = text.replace(old_edit_block, new_edit_block) + +insert_after = """ return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[]) + + + + + + +@app.route("/invoices/export.csv") +""" +new_routes = """ return render_template("services/edit.html", service=service, clients=clients, templates=templates, errors=[]) + + +@app.route("/service-templates") +def service_templates(): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute(\"\"\" + SELECT * + FROM service_templates + ORDER BY id DESC + \"\"\") + templates = cursor.fetchall() + conn.close() + + return render_template("service_templates/list.html", templates=templates) + + +@app.route("/service-templates/new", methods=["GET", "POST"]) +def new_service_template(): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + errors = [] + + if request.method == "POST": + template_name = request.form.get("template_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + setup_amount = request.form.get("setup_amount", "").strip() + description = request.form.get("description", "").strip() + is_active = 1 if request.form.get("is_active") == "1" else 0 + + if not template_name: + errors.append("Template name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if recurring_amount == "": + errors.append("Recurring amount is required.") + if setup_amount == "": + errors.append("Setup amount is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + setup_amount_value = float(setup_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + if setup_amount_value < 0: + errors.append("Setup amount cannot be negative.") + except ValueError: + errors.append("Amounts must be valid numbers.") + + if not errors: + insert_cursor = conn.cursor() + insert_cursor.execute(\"\"\" + INSERT INTO service_templates + ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description, + is_active + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + \"\"\", ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description or None, + is_active + )) + conn.commit() + conn.close() + return redirect("/service-templates") + + conn.close() + return render_template("service_templates/new.html", errors=errors) + + +@app.route("/service-templates/edit/", methods=["GET", "POST"]) +def edit_service_template(template_id): + gate = admin_required() + if gate: + return gate + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + if request.method == "POST": + template_name = request.form.get("template_name", "").strip() + service_type = request.form.get("service_type", "").strip() + billing_cycle = request.form.get("billing_cycle", "").strip() + currency_code = request.form.get("currency_code", "").strip() + recurring_amount = request.form.get("recurring_amount", "").strip() + setup_amount = request.form.get("setup_amount", "").strip() + description = request.form.get("description", "").strip() + is_active = 1 if request.form.get("is_active") == "1" else 0 + + errors = [] + + if not template_name: + errors.append("Template name is required.") + if not service_type: + errors.append("Service type is required.") + if not billing_cycle: + errors.append("Billing cycle is required.") + if not currency_code: + errors.append("Currency code is required.") + if recurring_amount == "": + errors.append("Recurring amount is required.") + if setup_amount == "": + errors.append("Setup amount is required.") + + if not errors: + try: + recurring_amount_value = float(recurring_amount) + setup_amount_value = float(setup_amount) + if recurring_amount_value < 0: + errors.append("Recurring amount cannot be negative.") + if setup_amount_value < 0: + errors.append("Setup amount cannot be negative.") + except ValueError: + errors.append("Amounts must be valid numbers.") + + if errors: + cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,)) + template = cursor.fetchone() + conn.close() + return render_template("service_templates/edit.html", template=template, errors=errors) + + update_cursor = conn.cursor() + update_cursor.execute(\"\"\" + UPDATE service_templates + SET template_name = %s, + service_type = %s, + billing_cycle = %s, + currency_code = %s, + recurring_amount = %s, + setup_amount = %s, + description = %s, + is_active = %s + WHERE id = %s + \"\"\", ( + template_name, + service_type, + billing_cycle, + currency_code, + recurring_amount, + setup_amount, + description or None, + is_active, + template_id + )) + conn.commit() + conn.close() + return redirect("/service-templates") + + cursor.execute("SELECT * FROM service_templates WHERE id = %s", (template_id,)) + template = cursor.fetchone() + conn.close() + + if not template: + return "Service template not found", 404 + + return render_template("service_templates/edit.html", template=template, errors=[]) + + + + + +@app.route("/invoices/export.csv") +""" +if insert_after not in text: + raise SystemExit("Could not find insertion point before /invoices/export.csv.") +text = text.replace(insert_after, new_routes) + +path.write_text(text) PY -echo "===== verify includes =====" -grep -n "video.outsidethebox.top" templates/includes/site_nav.html -grep -Rni 'includes/otb_statusbar.html' templates/portal_*.html | sed -n '1,50p' +echo "===== rewrite templates/services/list.html =====" +cat > "$APP_ROOT/templates/services/list.html" <<'HTML' + + + +Services + + + + +

Services

+ +

Home

+

+ Add Service | + Service Templates +

+ + + + + + + + + + + + + + + + +{% for s in services %} + + + + + + + + + + + + + +{% endfor %} + +
IDService CodeClientService NameTypeCycleCurrencyAmountStatusStart DateActions
{{ s.id }}{{ s.service_code }}{{ s.client_code }} - {{ s.company_name }}{{ s.service_name }}{{ s.service_type }}{{ s.billing_cycle }}{{ s.currency_code }}{{ s.recurring_amount|money(s.currency_code) }}{{ s.status }}{{ s.start_date }}Edit
+ +{% include "footer.html" %} + + +HTML + +echo "===== rewrite templates/services/new.html =====" +cat > "$APP_ROOT/templates/services/new.html" <<'HTML' + + + +New Service + + + + +

Add Service

+ +

Home

+

+ Back to Services | + Service Templates +

+ +
+ +

+Client
+ +

+ +

+Load from Template
+ +

+ +

+Service Name
+ +

+ +

+Service Type
+ +

+ +

+Billing Cycle
+ +

+ +

+Currency Code
+ +

+ +

+Recurring Amount
+ +

+ +

+Status
+ +

+ +

+Start Date
+ +

+ +

+Description
+ +

+ +

+ +

+ +
+ + + +{% include "footer.html" %} + + +HTML + +echo "===== rewrite templates/services/edit.html =====" +cat > "$APP_ROOT/templates/services/edit.html" <<'HTML' + + + +Edit Service + + + + +

Edit Service

+ +

Home

+

+ Back to Services | + Service Templates +

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Service Code
+ +

+ +

+Client *
+ +

+ +

+Load from Template
+ +

+ +

+Service Name *
+ +

+ +

+Service Type *
+ +

+ +

+Billing Cycle *
+ +

+ +

+Currency Code *
+ +

+ +

+Recurring Amount *
+ +

+ +

+Status *
+ +

+ +

+Start Date
+ +

+ +

+Description
+ +

+ +

+ +

+ +
+ + + +{% include "footer.html" %} + + +HTML + +echo "===== create templates/service_templates/list.html =====" +cat > "$APP_ROOT/templates/service_templates/list.html" <<'HTML' + + + +Service Templates + + + + +

Service Templates

+ +

Home

+

+ Back to Services | + Add Service Template +

+ + + + + + + + + + + + + + +{% for t in templates %} + + + + + + + + + + + +{% endfor %} + +
IDTemplate NameTypeCycleCurrencyRecurringSetupActiveActions
{{ t.id }}{{ t.template_name }}{{ t.service_type }}{{ t.billing_cycle }}{{ t.currency_code }}{{ t.recurring_amount|money(t.currency_code) }}{{ t.setup_amount|money(t.currency_code) }}{% if t.is_active %}yes{% else %}no{% endif %}Edit
+ +{% include "footer.html" %} + + +HTML + +echo "===== create templates/service_templates/new.html =====" +cat > "$APP_ROOT/templates/service_templates/new.html" <<'HTML' + + + +New Service Template + + + + +

Add Service Template

+ +

Home

+

Back to Service Templates

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Template Name *
+ +

+ +

+Service Type *
+ +

+ +

+Billing Cycle *
+ +

+ +

+Currency Code *
+ +

+ +

+Recurring Amount *
+ +

+ +

+Setup Amount *
+ +

+ +

+Description
+ +

+ +

+Active
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + +HTML + +echo "===== create templates/service_templates/edit.html =====" +cat > "$APP_ROOT/templates/service_templates/edit.html" <<'HTML' + + + +Edit Service Template + + + + +

Edit Service Template

+ +

Home

+

Back to Service Templates

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Template Name *
+ +

+ +

+Service Type *
+ +

+ +

+Billing Cycle *
+ +

+ +

+Currency Code *
+ +

+ +

+Recurring Amount *
+ +

+ +

+Setup Amount *
+ +

+ +

+Description
+ +

+ +

+Active
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + +HTML + +echo "===== verify syntax =====" +python3 -m py_compile "$APP_ROOT/backend/app.py" echo "===== restart service =====" -sudo systemctl restart otb-billing +sudo systemctl restart otb_billing.service +sleep 2 +sudo systemctl --no-pager --full status otb_billing.service | sed -n '1,35p' + +echo "===== verify routes present =====" +grep -nE '@app.route\("/service-templates"|def service_templates|def new_service_template|def edit_service_template' "$APP_ROOT/backend/app.py" + +echo "===== verify template table =====" +sudo mysql -D otb_billing -e "SHOW TABLES LIKE 'service_templates';" +sudo mysql -D otb_billing -e "DESCRIBE service_templates;" + +echo "===== completed =====" +echo "Backups saved under: $BACKUP_DIR" +EOF -echo "===== quick verify from template side =====" -sed -n '1,40p' templates/includes/site_nav.html +chmod +x /tmp/otb_billing_service_templates_patch.sh +/tmp/otb_billing_service_templates_patch.sh diff --git a/patch1.sh b/patch1.sh index 8a4bb42..a9c5bfa 100755 --- a/patch1.sh +++ b/patch1.sh @@ -1,57 +1,70 @@ cd /home/def/otb_billing || exit 1 -set -e -STAMP="$(date +%Y%m%d-%H%M%S)" +STAMP=$(date +%Y%m%d-%H%M%S) +NEWVER="v0.6.1" -cp templates/includes/otb_statusbar.html "templates/includes/otb_statusbar.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_login.html "templates/portal_login.html.bak.footer.${STAMP}" -cp templates/portal_dashboard.html "templates/portal_dashboard.html.bak.footer.${STAMP}" 2>/dev/null || true -cp templates/portal_set_password.html "templates/portal_set_password.html.bak.footer.${STAMP}" 2>/dev/null || true -cp templates/portal_terms.html "templates/portal_terms.html.bak.footer.${STAMP}" 2>/dev/null || true -cp templates/portal_invoice_detail.html "templates/portal_invoice_detail.html.bak.footer.${STAMP}" 2>/dev/null || true -cp templates/portal_forgot_password.html "templates/portal_forgot_password.html.bak.footer.${STAMP}" 2>/dev/null || true +echo "===== backup full project =====" +mkdir -p /home/def/backuphere +cd /home/def +tar -czf /home/def/backuphere/otb_billing-${NEWVER}-${STAMP}.tar.gz otb_billing -cat > templates/includes/otb_footer.html <<'EOF' -{% include "includes/otb_statusbar.html" %} - +echo "===== update VERSION =====" +cd /home/def/otb_billing || exit 1 +echo "${NEWVER}" > VERSION + +echo "===== update README.md =====" +cp README.md /home/def/backuphere/README.md.${STAMP}.bak + +cat > /tmp/readme_entry.txt <', - '{% include "includes/otb_footer.html" %}', - text, - count=1, - flags=re.S - ) - - text = re.sub( - r'\{\%\s*include\s+"includes/otb_statusbar\.html"\s*\%\}', - '{% include "includes/otb_footer.html" %}', - text, - count=1, - flags=re.S - ) - - p.write_text(text) - print(f"updated: {p}") -PY - -sudo systemctl restart otb_billing +cat /tmp/readme_entry.txt README.md > README.md.new +mv README.md.new README.md + +echo "===== update PROJECT_STATE.md =====" +cp PROJECT_STATE.md /home/def/backuphere/PROJECT_STATE.md.${STAMP}.bak + +cat > /tmp/state_entry.txt < PROJECT_STATE.md.new +mv PROJECT_STATE.md.new PROJECT_STATE.md + +echo "===== git status =====" +git status + +echo "===== commit =====" +git add . +git commit -m "bump ${NEWVER} - add service templates system" + +echo "===== push =====" +git push + +echo "===== done =====" +echo "Backup saved at:" +ls -lh /home/def/backuphere/otb_billing-${NEWVER}-${STAMP}.tar.gz diff --git a/patch2.sh b/patch2.sh deleted file mode 100755 index 702f769..0000000 --- a/patch2.sh +++ /dev/null @@ -1,102 +0,0 @@ -cd /home/def/otb_billing || exit 1 -set -e - -STAMP="$(date +%Y%m%d-%H%M%S)" - -echo "===== backups =====" -cp templates/includes/site_nav.html "templates/includes/site_nav.html.bak.${STAMP}" -cp templates/includes/otb_statusbar.html "templates/includes/otb_statusbar.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_login.html "templates/portal_login.html.bak.${STAMP}" -cp templates/portal_dashboard.html "templates/portal_dashboard.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_set_password.html "templates/portal_set_password.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_terms.html "templates/portal_terms.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_invoice_detail.html "templates/portal_invoice_detail.html.bak.${STAMP}" 2>/dev/null || true -cp templates/portal_forgot_password.html "templates/portal_forgot_password.html.bak.${STAMP}" 2>/dev/null || true -cp static/css/style.css "static/css/style.css.bak.${STAMP}" 2>/dev/null || true -cp static/brand.js "static/brand.js.bak.${STAMP}" 2>/dev/null || true - -echo "===== sync shared nav/footer assets from mintme-backed shared brand =====" -cp /opt/otb/otb-shared-brand/fragments/header.html templates/includes/site_nav.html - -cat > templates/includes/otb_footer.html <<'EOF' -
-
- All billing is calculated in 🇨🇦 CAD - - Crypto conversions use the OTB Oracle - - - Methods: - Credit Card (via Square), - e-Transfer, - and enabled crypto assets - -
-
- -EOF - -echo "===== sync shared css/js =====" -cp /var/www/outsidethebox.top/assets/brand.js static/brand.js - -python3 <<'PY' -from pathlib import Path -src = Path("/var/www/outsidethebox.top/assets/style.css") -dst = Path("/home/def/otb_billing/static/css/style.css") -text = src.read_text() - -# keep the portal's own css file as the shared base for now -dst.write_text(text) -print("synced style.css from shared public shell") -PY - -echo "===== replace portal footer/statusbar blocks with shared footer include =====" -python3 <<'PY' -from pathlib import Path -import re - -files = [ - Path("templates/portal_login.html"), - Path("templates/portal_dashboard.html"), - Path("templates/portal_set_password.html"), - Path("templates/portal_terms.html"), - Path("templates/portal_invoice_detail.html"), - Path("templates/portal_forgot_password.html"), -] - -for p in files: - if not p.exists(): - continue - text = p.read_text() - - text = re.sub( - r'
.*?
\s*\s*', - '{% include "includes/otb_footer.html" %}', - text, - count=1, - flags=re.S - ) - - text = re.sub( - r'\{\%\s*include\s+"includes/otb_statusbar\.html"\s*\%\}\s*', - '{% include "includes/otb_footer.html" %}', - text, - count=1, - flags=re.S - ) - - text = re.sub( - r'\{\%\s*include\s+"includes/otb_statusbar\.html"\s*\%\}', - '{% include "includes/otb_footer.html" %}', - text, - count=1, - flags=re.S - ) - - p.write_text(text) - print(f"updated: {p}") -PY - -sudo systemctl restart otb_billing - -echo "===== done =====" diff --git a/patch3.sh b/patch3.sh deleted file mode 100755 index d4e6a65..0000000 --- a/patch3.sh +++ /dev/null @@ -1,174 +0,0 @@ -cd /opt/otb_tracker || exit 1 -cat > /tmp/otb_tracker_shared_brand_patch.sh <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -APP_ROOT="/opt/otb_tracker" -SHARED_ROOT="/opt/otb/otb-shared-brand" -STAMP="$(date +%Y%m%d-%H%M%S)" - -cd "$APP_ROOT" || exit 1 - -echo "===== detect template/static roots =====" -TPL_ROOT="" -STATIC_ROOT="" - -if [ -d "$APP_ROOT/templates" ]; then - TPL_ROOT="$APP_ROOT/templates" -elif [ -d "$APP_ROOT/backend/templates" ]; then - TPL_ROOT="$APP_ROOT/backend/templates" -else - echo "ERROR: no templates dir found" - exit 1 -fi - -if [ -d "$APP_ROOT/static" ]; then - STATIC_ROOT="$APP_ROOT/static" -elif [ -d "$APP_ROOT/backend/static" ]; then - STATIC_ROOT="$APP_ROOT/backend/static" -else - echo "ERROR: no static dir found" - exit 1 -fi - -echo "Templates: $TPL_ROOT" -echo "Static: $STATIC_ROOT" - -echo "===== ensure shared brand exists and is current =====" -cd "$SHARED_ROOT" || exit 1 -git pull origin main -cd "$APP_ROOT" || exit 1 - -echo "===== backup =====" -mkdir -p "$APP_ROOT/backups/shared-brand-$STAMP" -cp -a "$TPL_ROOT" "$APP_ROOT/backups/shared-brand-$STAMP/templates" -cp -a "$STATIC_ROOT" "$APP_ROOT/backups/shared-brand-$STAMP/static" - -echo "===== create includes dir =====" -mkdir -p "$TPL_ROOT/includes" -mkdir -p "$STATIC_ROOT/css" - -echo "===== sync shared assets =====" -cp "$SHARED_ROOT/brand.css" "$STATIC_ROOT/css/brand.css" -cp "$SHARED_ROOT/brand.js" "$STATIC_ROOT/brand.js" - -echo "===== sync shared header include =====" -cp "$SHARED_ROOT/fragments/header.html" "$TPL_ROOT/includes/site_nav.html" - -echo "===== write canonical shared footer includes =====" -cat > "$TPL_ROOT/includes/otb_statusbar.html" <<'EOT' -
-
- All billing is calculated in 🇨🇦 CAD - - Crypto conversions use the OTB Oracle - - - Methods: - Credit Card (via Square), - e-Transfer, - and enabled crypto assets - -
-
-EOT - -cat > "$TPL_ROOT/includes/otb_footer.html" <<'EOT' -{% include "includes/otb_statusbar.html" %} - -EOT - -echo "===== patch html templates =====" -python3 <)', - r'\\1\n ', - text, - count=1 - ) - text = re.sub( - r'()', - r'\\1\n ', - text, - count=1 - ) - - # normalize brand css/js cache versions - text = text.replace('/static/css/brand.css?v=0.3.5', '/static/css/brand.css?v=0.3.6') - text = text.replace('/static/css/brand.css?v=0.3.6?v=0.3.6', '/static/css/brand.css?v=0.3.6') - text = text.replace('/static/brand.js?v=0.3.5', '/static/brand.js?v=0.3.6') - text = text.replace('/static/brand.js?v=0.3.6?v=0.3.6', '/static/brand.js?v=0.3.6') - - # replace inline shared header block if present - text = re.sub( - r'', - '{% include "includes/site_nav.html" %}', - text, - count=1, - flags=re.S - ) - - # replace older nav block if present - text = re.sub( - r'
\s*\s*', - '{% include "includes/site_nav.html" %}\n', - text, - count=1, - flags=re.S - ) - - # if no shared nav include and no obvious header/nav remains, insert after - if '{% include "includes/site_nav.html" %}' not in text: - if '' in text: - text = text.replace('', '\n {% include "includes/site_nav.html" %}', 1) - - # replace inline statusbar/footer block with shared footer include - text = re.sub( - r'
.*?
\s*
\s*(\s*', - '\n', - text, - flags=re.S - ) - - # if footer include absent, append before - if '{% include "includes/otb_footer.html" %}' not in text: - text = text.replace('', ' {% include "includes/otb_footer.html" %}\n', 1) - - p.write_text(text, encoding="utf-8") - print(f"patched: {p}") -PY - -echo "===== restart service =====" -sudo systemctl restart otb-tracker.service - -echo "===== verify =====" -grep -Rni 'includes/site_nav.html\|includes/otb_footer.html\|/static/css/brand.css\|video.outsidethebox.top' "$TPL_ROOT" | sed -n '1,200p' -ls -l "$STATIC_ROOT/css/brand.css" "$STATIC_ROOT/brand.js" - -echo "===== done =====" -EOF - -chmod +x /tmp/otb_tracker_shared_brand_patch.sh -/tmp/otb_tracker_shared_brand_patch.sh diff --git a/patch4.sh b/patch4.sh deleted file mode 100755 index 52ac120..0000000 --- a/patch4.sh +++ /dev/null @@ -1,143 +0,0 @@ -cd /home/def/otb_billing || exit 1 - -cp templates/portal/services_here.html templates/portal/services_here.html.bak.$(date +%Y%m%d-%H%M%S) - -cat > templates/portal/services_here.html <<'EOF' - - - - - - Services - OutsideTheBox - - - - - - - -{% include "includes/site_nav.html" %} - -
-
- -
-
-

Services

-

{{ client_name }}

-

Launch available OTB services from one place.

-
- -
- - -
-
Logged in as: {{ client_name }}
-
-
-
- -
- {% for service in services %} -
-
-
-

{{ service.name }}

-

{{ service.summary }}

-
- -
- {% if service.status == 'beta' %} - Beta - {% elif service.status == 'coming_soon' %} - Coming Soon - {% else %} - Available - {% endif %} -
-
- -
- {% if service.enabled %} - {{ service.button_text }} - {% else %} - - {% endif %} -
-
- {% endfor %} -
- -
-
- - - -{% include "includes/otb_footer.html" %} - - - -EOF - -sudo systemctl restart otb_billing.service diff --git a/patch5.sh b/patch5.sh deleted file mode 100755 index 0204a77..0000000 --- a/patch5.sh +++ /dev/null @@ -1,272 +0,0 @@ -cd /home/def/otb_billing || exit 1 -set -e - -STAMP="$(date +%Y%m%d-%H%M%S)" - -cp templates/portal_dashboard.html "templates/portal_dashboard.html.bak.${STAMP}" -cp templates/portal/services_here.html "templates/portal/services_here.html.bak.${STAMP}" - -cat > templates/portal_base.html <<'EOF' - - - - - - {% block title %}Portal - OutsideTheBox{% endblock %} - - - - {% block head_extra %}{% endblock %} - - - {% include "includes/site_nav.html" %} - -
-
- {% block portal_content %}{% endblock %} -
-
- - {% block scripts %}{% endblock %} - - {% include "includes/otb_footer.html" %} - - -EOF - -cat > templates/portal_dashboard.html <<'EOF' -{% extends "portal_base.html" %} - -{% block title %}Client Dashboard - OutsideTheBox{% endblock %} - -{% block portal_content %} -
-
-

Client Dashboard

-

{{ client.company_name or client.contact_name or client.email }}

-

Invoices, balances, and account activity in one place.

-
- -
- -
-
Logged in as: {{ client.contact_name or client.company_name or client.email }}
- {% if client_credit_balance and client_credit_balance != "0.00" %} -
- - 🏦 Credit: ${{ client_credit_balance }} - -
- {% endif %} -
-
-
- -
-
-

Total Invoices

-
{{ invoice_count }}
-
Invoices currently visible in your portal
-
- -
-

Total Outstanding

-
{{ total_outstanding }}
-
Current unpaid balance
-
- -
-

Total Paid

-
{{ total_paid }}
-
Payments already applied
-
-
- -

Invoices

- -
- - - - - - - - - - - - - {% for row in invoices %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
InvoiceStatusCreatedTotalPaidOutstanding
- - {{ row.invoice_number or ("INV-" ~ row.id) }} - - - {% set s = (row.status or "")|lower %} - {% if s == "paid" %} - {{ row.status }} - {% if row.payment_method_label %} -
- {{ row.payment_method_label }} -
- {% endif %} - {% elif s == "pending" %} - {{ row.status }} - {% elif s == "overdue" %} - {{ row.status }} - {% else %} - {{ row.status }} - {% endif %} -
{{ row.created_at }}{{ row.total_amount }}{{ row.amount_paid }}{{ row.outstanding }}
No invoices available.
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} -EOF - -cat > templates/portal/services_here.html <<'EOF' -{% extends "portal_base.html" %} - -{% block title %}Services - OutsideTheBox{% endblock %} - -{% block portal_content %} -
-
-

Services

-

{{ client_name }}

-

Launch available OTB services from one place.

-
- -
- - -
-
Logged in as: {{ client_name }}
-
-
-
- -
- {% for service in services %} -
-
-
-

{{ service.name }}

-

{{ service.summary }}

-
- -
- {% if service.status == 'beta' %} - Beta - {% elif service.status == 'coming_soon' %} - Coming Soon - {% else %} - Available - {% endif %} -
-
- -
- {% if service.enabled %} - {{ service.button_text }} - {% else %} - - {% endif %} -
-
- {% endfor %} -
- - -{% endblock %} -EOF - -sudo systemctl restart otb_billing.service -sudo systemctl status otb_billing.service --no-pager -l | sed -n '1,30p' diff --git a/patch6.sh b/patch6.sh deleted file mode 100755 index 6163bcb..0000000 --- a/patch6.sh +++ /dev/null @@ -1,78 +0,0 @@ -cd /home/def/otb_billing || exit 1 - -cp backend/routes/portal_services.py backend/routes/portal_services.py.bak.$(date +%Y%m%d-%H%M%S) - -cat > backend/routes/portal_services.py <<'EOF' -from flask import Blueprint, render_template, session, redirect, url_for, flash - -portal_services_bp = Blueprint("portal_services", __name__) - -def _portal_user_is_logged_in() -> bool: - return bool( - session.get("portal_user_id") - or session.get("client_user_id") - or session.get("portal_client_id") - or session.get("client_id") - or session.get("user_id") - ) - -@portal_services_bp.route("/portal/services") -def portal_services_home(): - if not _portal_user_is_logged_in(): - flash("Please sign in to access services.", "warning") - return redirect(url_for("portal_login")) - - client = { - "contact_name": session.get("portal_contact_name"), - "company_name": session.get("portal_company_name"), - "email": session.get("portal_email"), - } - - client_name = ( - client.get("contact_name") - or client.get("company_name") - or client.get("email") - or "Client" - ) - - services = [ - { - "key": "follow_me", - "name": "Follow-me Tracker", - "summary": "Create and manage your GPS tracking network. Free for up to 2 users.", - "status": "beta", - "enabled": True, - "href": "/follow-me", - "button_text": "Open Follow-me", - }, - { - "key": "video_render", - "name": "Video Rendering / Streaming", - "summary": "Submit video rendering, conversion, and hosted streaming jobs.", - "status": "coming_soon", - "enabled": False, - "href": "#", - "button_text": "Coming Soon", - }, - { - "key": "miner_rentals", - "name": "Miner Rentals", - "summary": "Rent available OTB hashpower by time or package.", - "status": "coming_soon", - "enabled": False, - "href": "#", - "button_text": "Coming Soon", - }, - ] - - return render_template( - "portal/services_here.html", - client=client, - client_name=client_name, - services=services, - ) -EOF - -python3 -m py_compile backend/routes/portal_services.py -sudo systemctl restart otb_billing.service -sudo systemctl status otb_billing.service --no-pager -l | sed -n '1,30p' diff --git a/patch7.sh b/patch7.sh deleted file mode 100755 index 37937e1..0000000 --- a/patch7.sh +++ /dev/null @@ -1,157 +0,0 @@ -cd /home/def/otb_billing || exit 1 -set -e - -STAMP="$(date +%Y%m%d-%H%M%S)" -NEW_VERSION="v0.6.0" -ZIP_NAME="otb_billing-${NEW_VERSION}.zip" - -echo "===== backups =====" -cp VERSION "VERSION.bak.${STAMP}" -cp README.md "README.md.bak.${STAMP}" -cp PROJECT_STATE.md "PROJECT_STATE.md.bak.${STAMP}" - -echo "===== version bump =====" -printf '%s\n' "${NEW_VERSION}" > VERSION - -echo "===== update README + PROJECT_STATE =====" -python3 <<'PY' -from pathlib import Path -from datetime import datetime, timezone - -root = Path("/home/def/otb_billing") -version = "v0.6.0" -stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") -utc_stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - -readme = root / "README.md" -project_state = root / "PROJECT_STATE.md" - -readme_text = readme.read_text() if readme.exists() else "" -project_text = project_state.read_text() if project_state.exists() else "" - -readme_entry = f"""## {version} - {stamp} - -### Highlights -- Added authenticated `/portal/services` page as a new service hub inside the billing portal. -- Added modular route file `backend/routes/portal_services.py` instead of expanding the main app with another large inline route block. -- Added shared `templates/portal_base.html` layout using Jinja `{% extends %}` / block pattern. -- Converted portal pages to the shared base-template structure for cleaner reuse and consistent branding. -- Added branded service cards for: - - Follow-me Tracker - - Video Rendering / Streaming - - Miner Rentals -- Fixed portal services layout so it uses the same nav, footer, styling, and light/dark toggle behavior as the working portal/dashboard pages. -- Corrected the Follow-me service link to use the real external service domain. -- Updated service launch buttons so external services open in a new tab. -- Improved portal services identity display to prepare for showing real logged-in client identity instead of a generic placeholder. - -### Notes -- This is the first successful rollout of the reusable `portal_base.html` structure and it should be used for future portal pages and future project builds. -- This release establishes the billing portal as the launch point for OTB services. - -""" - -project_entry = f"""# Project State Update - {version} -Updated: {utc_stamp} - -## Current Version -{version} - -## Current Status -OTB Billing has been advanced from a billing-only portal toward a unified service-launch platform. - -## Completed This Session -- Created `/portal/services` authenticated service hub. -- Added modular route file: `backend/routes/portal_services.py` -- Added shared Jinja portal base template: `templates/portal_base.html` -- Converted: - - `templates/portal_dashboard.html` - - `templates/portal/services_here.html` - to the new shared base-template architecture. -- Restored consistent OTB branding, nav, footer, spacing, and dark/light toggle behavior on the services page. -- Added service cards for: - - Follow-me Tracker (active/beta) - - Video Rendering / Streaming (coming soon) - - Miner Rentals (coming soon) -- Fixed service routing so Follow-me points to the real service domain. -- Updated external service launch links to open in a new tab. -- Confirmed the new shared template architecture is successful and should be reused in future pages/projects. - -## Architectural Direction -The portal now has a proven reusable pattern: -- `templates/portal_base.html` -- child templates using `{{% extends "portal_base.html" %}}` -- focused page content blocks instead of duplicating full HTML shell on each page - -This is now the preferred portal/page structure going forward. - -## Next Logical Steps -- Unify logged-in client identity handling across portal routes. -- Add real service-aware handoff / account linkage for Follow-me. -- Add future service cards/pages without duplicating layout shell. -- Move page-specific inline service-card CSS into shared stylesheet when ready. - -""" - -def prepend_once(existing: str, new_block: str, marker: str) -> str: - if marker in existing: - return existing - if existing.startswith("# "): - return existing.rstrip() + "\n\n" + new_block - return new_block + existing - -readme.write_text(prepend_once(readme_text, readme_entry, f"## {version} - ")) -project_state.write_text(prepend_once(project_text, project_entry, f"# Project State Update - {version}")) -PY - -echo "===== build full zip snapshot =====" -rm -f "releases/${ZIP_NAME}" -mkdir -p releases - -python3 <<'PY' -from pathlib import Path -import zipfile - -root = Path("/home/def/otb_billing") -zip_path = root / "releases" / "otb_billing-v0.6.0.zip" - -exclude_parts = { - ".git", - "__pycache__", - ".pytest_cache", -} - -exclude_suffixes = { - ".pyc", - ".pyo", -} - -with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: - for path in root.rglob("*"): - rel = path.relative_to(root) - if any(part in exclude_parts for part in rel.parts): - continue - if path.suffix in exclude_suffixes: - continue - if path == zip_path: - continue - zf.write(path, arcname=str(Path("otb_billing-v0.6.0") / rel)) -print(zip_path) -PY - -echo "===== quick verify =====" -echo -echo "--- VERSION ---" -cat VERSION -echo -echo "--- README.md top 40 ---" -sed -n '1,40p' README.md -echo -echo "--- PROJECT_STATE.md top 60 ---" -sed -n '1,60p' PROJECT_STATE.md -echo -echo "--- ZIP snapshot ---" -ls -lh "releases/${ZIP_NAME}" -echo -echo "===== optional git review =====" -git status --short diff --git a/patch8.sh b/patch8.sh deleted file mode 100755 index 041cd89..0000000 --- a/patch8.sh +++ /dev/null @@ -1,125 +0,0 @@ -cd /home/def/otb_billing || exit 1 -set -e - -STAMP="$(date +%Y%m%d-%H%M%S)" -NEW_VERSION="v0.6.0" -ZIP_NAME="otb_billing-${NEW_VERSION}.zip" - -echo "===== backups =====" -cp VERSION "VERSION.bak.${STAMP}" -cp README.md "README.md.bak.${STAMP}" -cp PROJECT_STATE.md "PROJECT_STATE.md.bak.${STAMP}" - -echo "===== version bump =====" -printf '%s\n' "${NEW_VERSION}" > VERSION - -echo "===== update README + PROJECT_STATE =====" -python3 <<'PY' -from pathlib import Path -from datetime import datetime, timezone - -root = Path("/home/def/otb_billing") -version = "v0.6.0" -stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") -utc_stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - -readme = root / "README.md" -project_state = root / "PROJECT_STATE.md" - -readme_text = readme.read_text() if readme.exists() else "" -project_text = project_state.read_text() if project_state.exists() else "" - -readme_entry = """## v0.6.0 - {stamp} - -### Highlights -- Added authenticated /portal/services page as a service hub -- Introduced modular route backend/routes/portal_services.py -- Created shared templates/portal_base.html layout -- Converted portal pages to base-template architecture -- Added service cards (Follow-me, Video, Miner Rentals) -- Fixed branding, nav, footer, and toggle consistency -- Corrected Follow-me external link -- External services now open in new tabs -- Improved identity display for logged-in user - -### Notes -- portal_base.html is now the standard structure for future pages and projects -- Billing portal is now the launch point for all OTB services - -""".replace("{stamp}", stamp) - -project_entry = """# Project State Update - v0.6.0 -Updated: {utc_stamp} - -## Current Version -v0.6.0 - -## Current Status -OTB Billing is now a service-launch platform, not just billing. - -## Completed This Session -- Added /portal/services page -- Added portal_services.py route module -- Created portal_base.html shared template -- Converted dashboard + services page to shared layout -- Restored consistent branding, nav, footer, toggle -- Added service cards (Follow-me, Video, Miner) -- Fixed external service routing -- Enabled new-tab launch for services - -## Architecture -Using shared base template: -templates/portal_base.html - -All pages now: -{% extends "portal_base.html" %} - -## Next Steps -- Unify client identity across all routes -- Add Follow-me provisioning + billing linkage -- Move inline CSS into shared styles later - -""".replace("{utc_stamp}", utc_stamp) - -def prepend(existing, new_block, marker): - if marker in existing: - return existing - if existing.startswith("# "): - return existing.rstrip() + "\n\n" + new_block - return new_block + existing - -readme.write_text(prepend(readme_text, readme_entry, "## v0.6.0")) -project_state.write_text(prepend(project_text, project_entry, "# Project State Update - v0.6.0")) -PY - -echo "===== build full zip snapshot =====" -rm -f "releases/${ZIP_NAME}" -mkdir -p releases - -python3 <<'PY' -from pathlib import Path -import zipfile - -root = Path("/home/def/otb_billing") -zip_path = root / "releases" / "otb_billing-v0.6.0.zip" - -exclude_parts = {".git", "__pycache__", ".pytest_cache"} -exclude_suffixes = {".pyc", ".pyo"} - -with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: - for path in root.rglob("*"): - rel = path.relative_to(root) - if any(part in exclude_parts for part in rel.parts): - continue - if path.suffix in exclude_suffixes: - continue - if path == zip_path: - continue - zf.write(path, arcname=str(Path("otb_billing-v0.6.0") / rel)) - -print(zip_path) -PY - -echo "===== verify =====" -cat VERSION -ls -lh "releases/${ZIP_NAME}" diff --git a/static/css/brand.css b/static/css/brand.css index 95e5365..2d1a46e 100644 --- a/static/css/brand.css +++ b/static/css/brand.css @@ -547,3 +547,30 @@ html[data-theme="light"] .otb-dot{ margin-top:8px !important; } } + +/* light theme button consistency fix */ +body.light-theme .portal-btn, +body.light-theme a.portal-btn, +body.light-theme button.portal-btn { + background: rgba(255,255,255,0.75); + color: var(--text, #10233f); + border: 1px solid rgba(40,70,120,0.14); + opacity: 1; +} + +body.light-theme .portal-btn.primary, +body.light-theme a.portal-btn.primary, +body.light-theme button.portal-btn.primary { + color: #081528; +} + +body.light-theme .portal-toolbar .portal-btn, +body.light-theme .portal-actions .portal-btn { + opacity: 1; +} + +body.light-theme .portal-btn[disabled], +body.light-theme button.portal-btn[disabled], +body.light-theme .portal-btn.disabled { + opacity: 0.55; +} diff --git a/static/css/style.css b/static/css/style.css index 57fa5f2..9f05200 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1108,3 +1108,169 @@ body{ background: #2563eb; color: #ffffff; } + +/* ===== final portal light-theme button normalization ===== */ + +body.light-theme .portal-btn, +body.light-theme a.portal-btn, +body.light-theme button.portal-btn, +body.light-theme .btn, +body.light-theme a.btn, +body.light-theme button.btn { + background: rgba(255,255,255,0.88); + color: #10233f; + border: 1px solid rgba(40,70,120,0.14); + box-shadow: none; + opacity: 1 !important; +} + +body.light-theme .portal-btn:hover, +body.light-theme a.portal-btn:hover, +body.light-theme button.portal-btn:hover, +body.light-theme .btn:hover, +body.light-theme a.btn:hover, +body.light-theme button.btn:hover { + border-color: rgba(122,162,255,.45); + box-shadow: 0 0 0 4px rgba(122,162,255,.10); + text-decoration: none; +} + +body.light-theme .portal-btn.primary, +body.light-theme .btn.primary { + background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85)); + border-color: transparent; + color: #071017; +} + +body.light-theme .portal-btn[disabled], +body.light-theme button.portal-btn[disabled], +body.light-theme .portal-btn.disabled, +body.light-theme .btn[disabled], +body.light-theme button.btn[disabled], +body.light-theme .btn.disabled { + opacity: 0.5 !important; +} + +/* ===== portal button fix for actual OTB light theme selectors ===== */ + +html[data-theme="light"] .portal-btn, +html[data-theme="light"] a.portal-btn, +html[data-theme="light"] button.portal-btn, +body.otb-light .portal-btn, +body.otb-light a.portal-btn, +body.otb-light button.portal-btn, +html[data-theme="light"] .btn, +html[data-theme="light"] a.btn, +html[data-theme="light"] button.btn, +body.otb-light .btn, +body.otb-light a.btn, +body.otb-light button.btn { + background: rgba(255,255,255,0.88) !important; + color: #10233f !important; + border: 1px solid rgba(40,70,120,0.14) !important; + box-shadow: none !important; + opacity: 1 !important; +} + +html[data-theme="light"] .portal-btn:hover, +html[data-theme="light"] a.portal-btn:hover, +html[data-theme="light"] button.portal-btn:hover, +body.otb-light .portal-btn:hover, +body.otb-light a.portal-btn:hover, +body.otb-light button.portal-btn:hover, +html[data-theme="light"] .btn:hover, +html[data-theme="light"] a.btn:hover, +html[data-theme="light"] button.btn:hover, +body.otb-light .btn:hover, +body.otb-light a.btn:hover, +body.otb-light button.btn:hover { + border-color: rgba(122,162,255,.45) !important; + box-shadow: 0 0 0 4px rgba(122,162,255,.10) !important; + text-decoration: none !important; +} + +html[data-theme="light"] .portal-btn.primary, +html[data-theme="light"] .btn.primary, +body.otb-light .portal-btn.primary, +body.otb-light .btn.primary { + background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85)) !important; + border-color: transparent !important; + color: #071017 !important; +} + +html[data-theme="light"] .portal-btn[disabled], +html[data-theme="light"] button.portal-btn[disabled], +html[data-theme="light"] .portal-btn.disabled, +html[data-theme="light"] .btn[disabled], +html[data-theme="light"] button.btn[disabled], +html[data-theme="light"] .btn.disabled, +body.otb-light .portal-btn[disabled], +body.otb-light button.portal-btn[disabled], +body.otb-light .portal-btn.disabled, +body.otb-light .btn[disabled], +body.otb-light button.btn[disabled], +body.otb-light .btn.disabled { + opacity: 0.5 !important; +} + +/* ===== portal button fix for actual OTB light theme selectors ===== */ + +html[data-theme="light"] .portal-btn, +html[data-theme="light"] a.portal-btn, +html[data-theme="light"] button.portal-btn, +body.otb-light .portal-btn, +body.otb-light a.portal-btn, +body.otb-light button.portal-btn, +html[data-theme="light"] .btn, +html[data-theme="light"] a.btn, +html[data-theme="light"] button.btn, +body.otb-light .btn, +body.otb-light a.btn, +body.otb-light button.btn { + background: rgba(255,255,255,0.88) !important; + color: #10233f !important; + border: 1px solid rgba(40,70,120,0.14) !important; + box-shadow: none !important; + opacity: 1 !important; +} + +html[data-theme="light"] .portal-btn:hover, +html[data-theme="light"] a.portal-btn:hover, +html[data-theme="light"] button.portal-btn:hover, +body.otb-light .portal-btn:hover, +body.otb-light a.portal-btn:hover, +body.otb-light button.portal-btn:hover, +html[data-theme="light"] .btn:hover, +html[data-theme="light"] a.btn:hover, +html[data-theme="light"] button.btn:hover, +body.otb-light .btn:hover, +body.otb-light a.btn:hover, +body.otb-light button.btn:hover { + border-color: rgba(122,162,255,.45) !important; + box-shadow: 0 0 0 4px rgba(122,162,255,.10) !important; + text-decoration: none !important; +} + +html[data-theme="light"] .portal-btn.primary, +html[data-theme="light"] .btn.primary, +body.otb-light .portal-btn.primary, +body.otb-light .btn.primary { + background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85)) !important; + border-color: transparent !important; + color: #071017 !important; +} + +html[data-theme="light"] .portal-btn[disabled], +html[data-theme="light"] button.portal-btn[disabled], +html[data-theme="light"] .portal-btn.disabled, +html[data-theme="light"] .btn[disabled], +html[data-theme="light"] button.btn[disabled], +html[data-theme="light"] .btn.disabled, +body.otb-light .portal-btn[disabled], +body.otb-light button.portal-btn[disabled], +body.otb-light .portal-btn.disabled, +body.otb-light .btn[disabled], +body.otb-light button.btn[disabled], +body.otb-light .btn.disabled { + opacity: 0.5 !important; +} diff --git a/templatepatch.sh b/templatepatch.sh new file mode 100644 index 0000000..1777af5 --- /dev/null +++ b/templatepatch.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e + +echo "===== starting patch =====" + +# commands directly here (no nested EOF) +# example: +echo "doing work..." +sleep 1 + +echo "===== patch completed successfully =====" diff --git a/templates/service_templates/edit.html b/templates/service_templates/edit.html new file mode 100644 index 0000000..f7c0d7b --- /dev/null +++ b/templates/service_templates/edit.html @@ -0,0 +1,98 @@ + + + +Edit Service Template + + + + +

Edit Service Template

+ +

Home

+

Back to Service Templates

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Template Name *
+ +

+ +

+Service Type *
+ +

+ +

+Billing Cycle *
+ +

+ +

+Currency Code *
+ +

+ +

+Recurring Amount *
+ +

+ +

+Setup Amount *
+ +

+ +

+Description
+ +

+ +

+Active
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + diff --git a/templates/service_templates/list.html b/templates/service_templates/list.html new file mode 100644 index 0000000..3a5d028 --- /dev/null +++ b/templates/service_templates/list.html @@ -0,0 +1,48 @@ + + + +Service Templates + + + + +

Service Templates

+ +

Home

+

+ Back to Services | + Add Service Template +

+ + + + + + + + + + + + + + +{% for t in templates %} + + + + + + + + + + + +{% endfor %} + +
IDTemplate NameTypeCycleCurrencyRecurringSetupActiveActions
{{ t.id }}{{ t.template_name }}{{ t.service_type }}{{ t.billing_cycle }}{{ t.currency_code }}{{ t.recurring_amount|money(t.currency_code) }}{{ t.setup_amount|money(t.currency_code) }}{% if t.is_active %}yes{% else %}no{% endif %}Edit
+ +{% include "footer.html" %} + + diff --git a/templates/service_templates/new.html b/templates/service_templates/new.html new file mode 100644 index 0000000..2468967 --- /dev/null +++ b/templates/service_templates/new.html @@ -0,0 +1,98 @@ + + + +New Service Template + + + + +

Add Service Template

+ +

Home

+

Back to Service Templates

+ +{% if errors %} +
+ Please fix the following: +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+{% endif %} + +
+ +

+Template Name *
+ +

+ +

+Service Type *
+ +

+ +

+Billing Cycle *
+ +

+ +

+Currency Code *
+ +

+ +

+Recurring Amount *
+ +

+ +

+Setup Amount *
+ +

+ +

+Description
+ +

+ +

+Active
+ +

+ +

+ +

+ +
+ +{% include "footer.html" %} + + diff --git a/templates/services/edit.html b/templates/services/edit.html index 062261c..9aca2df 100644 --- a/templates/services/edit.html +++ b/templates/services/edit.html @@ -2,14 +2,17 @@ Edit Service - +

Edit Service

Home

-

Back to Services

+

+ Back to Services | + Service Templates +

{% if errors %}
@@ -41,6 +44,27 @@ Client *

+

+Load from Template
+ +

+

Service Name *
@@ -55,8 +79,8 @@ Service Type *
- - + +

@@ -112,6 +136,25 @@ Description
+ + {% include "footer.html" %} diff --git a/templates/services/list.html b/templates/services/list.html index 3151601..8bb3748 100644 --- a/templates/services/list.html +++ b/templates/services/list.html @@ -2,14 +2,17 @@ Services - +

Services

Home

-

Add Service

+

+ Add Service | + Service Templates +

diff --git a/templates/services/new.html b/templates/services/new.html index 425cfd5..065243c 100644 --- a/templates/services/new.html +++ b/templates/services/new.html @@ -2,12 +2,18 @@ New Service - +

Add Service

+

Home

+

+ Back to Services | + Service Templates +

+

@@ -20,6 +26,27 @@ Client

+

+Load from Template
+ +

+

Service Name
@@ -35,7 +62,7 @@ Service Type
- +

@@ -91,6 +118,25 @@ Description
+ + +{% include "footer.html" %} -{% include "footer.html" %}