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" %}