Browse Source

release: v0.6.0 - portal services hub + base template system

main
def 4 weeks ago
parent
commit
b8f99c4f12
  1. 31
      PROJECT_STATE.md
  2. 17
      README.md
  3. 2
      VERSION
  4. 14
      backend/app.py
  5. 3075
      backend/app.py.fix_indent_backup
  6. 68
      backend/routes/portal_services.py
  7. 142
      backend/templates/portal/services_here.html
  8. 21
      backups/css_fix_20260313-024601/base.html
  9. 69
      backups/css_fix_20260313-024601/dashboard.html
  10. 376
      backups/css_fix_20260313-024601/footer.html
  11. 139
      backups/css_fix_20260313-024601/health.html
  12. 151
      backups/css_fix_20260313-024601/portal_dashboard.html
  13. 81
      backups/css_fix_20260313-024601/portal_forgot_password.html
  14. 166
      backups/css_fix_20260313-024601/portal_invoice_detail.html
  15. 95
      backups/css_fix_20260313-024601/portal_login.html
  16. 75
      backups/css_fix_20260313-024601/portal_set_password.html
  17. 202
      backups/css_fix_20260313-024601/settings.html
  18. 125
      backups/footer-flag-20260322-045513/portal_dashboard.html
  19. 21
      backups/footer-flag-20260322-045513/portal_forgot_password.html
  20. 31
      backups/footer-flag-20260322-045513/portal_invoice_detail.html
  21. 64
      backups/footer-flag-20260322-045513/portal_login.html
  22. 21
      backups/footer-flag-20260322-045513/portal_set_password.html
  23. 346
      backups/footer-toggle-cleanup-20260322-221548/footer.html
  24. 376
      backups/footer-toggle-removal-20260322-220351/footer.html
  25. 6344
      backups/payment-method-fix-20260322-040838/app.py.before_fix
  26. 107
      backups/payment-method-fix-20260322-040838/portal_dashboard.html.before_fix
  27. 6344
      backups/payment-method-fix2-20260322-041036/app.py.before_fix
  28. 107
      backups/payment-method-fix2-20260322-041036/portal_dashboard.html.before_fix
  29. 110
      backups/portal-bottom-bar-20260322-044046/portal_dashboard.html
  30. 9
      backups/portal-bottom-bar-20260322-044046/portal_forgot_password.html
  31. 19
      backups/portal-bottom-bar-20260322-044046/portal_invoice_detail.html
  32. 52
      backups/portal-bottom-bar-20260322-044046/portal_login.html
  33. 9
      backups/portal-bottom-bar-20260322-044046/portal_set_password.html
  34. 587
      backups/portal-bottom-bar-20260322-044046/style.css
  35. 170
      backups/portal-cleanup-20260322-032010/portal_dashboard.html
  36. 90
      backups/portal-cleanup-20260322-032010/portal_forgot_password.html
  37. 735
      backups/portal-cleanup-20260322-032010/portal_invoice_detail.html
  38. 104
      backups/portal-cleanup-20260322-032010/portal_login.html
  39. 84
      backups/portal-cleanup-20260322-032010/portal_set_password.html
  40. 170
      backups/portal-cleanup2-20260322-032316/portal_dashboard.html
  41. 90
      backups/portal-cleanup2-20260322-032316/portal_forgot_password.html
  42. 735
      backups/portal-cleanup2-20260322-032316/portal_invoice_detail.html
  43. 104
      backups/portal-cleanup2-20260322-032316/portal_login.html
  44. 84
      backups/portal-cleanup2-20260322-032316/portal_set_password.html
  45. 21
      backups/portal-dropdown-fix-20260322-182243/site_nav.html
  46. 699
      backups/portal-dropdown-fix-20260322-182243/style.css
  47. 28
      backups/portal-dropdown-fix-20260322-182803/site_nav.html
  48. 775
      backups/portal-dropdown-fix-20260322-182803/style.css
  49. 168
      backups/portal-links-20260322-033024/portal_dashboard.html
  50. 88
      backups/portal-links-20260322-033024/portal_forgot_password.html
  51. 733
      backups/portal-links-20260322-033024/portal_invoice_detail.html
  52. 102
      backups/portal-links-20260322-033024/portal_login.html
  53. 82
      backups/portal-links-20260322-033024/portal_set_password.html
  54. 162
      backups/portal-nav-20260322-030029/portal_dashboard.html
  55. 82
      backups/portal-nav-20260322-030029/portal_forgot_password.html
  56. 727
      backups/portal-nav-20260322-030029/portal_invoice_detail.html
  57. 96
      backups/portal-nav-20260322-030029/portal_login.html
  58. 76
      backups/portal-nav-20260322-030029/portal_set_password.html
  59. 168
      backups/portal-navbar-20260322-031219/portal_dashboard.html
  60. 88
      backups/portal-navbar-20260322-031219/portal_forgot_password.html
  61. 733
      backups/portal-navbar-20260322-031219/portal_invoice_detail.html
  62. 102
      backups/portal-navbar-20260322-031219/portal_login.html
  63. 82
      backups/portal-navbar-20260322-031219/portal_set_password.html
  64. 6392
      backups/portal-theme-cleanup-20260322-215133/app.py
  65. 125
      backups/portal-theme-cleanup-20260322-215133/portal_dashboard.html
  66. 21
      backups/portal-theme-cleanup-20260322-215133/portal_forgot_password.html
  67. 31
      backups/portal-theme-cleanup-20260322-215133/portal_invoice_detail.html
  68. 64
      backups/portal-theme-cleanup-20260322-215133/portal_login.html
  69. 21
      backups/portal-theme-cleanup-20260322-215133/portal_set_password.html
  70. 125
      backups/portal-toggle-cleanup-20260322-213704/portal_dashboard.html
  71. 21
      backups/portal-toggle-cleanup-20260322-213704/portal_forgot_password.html
  72. 31
      backups/portal-toggle-cleanup-20260322-213704/portal_invoice_detail.html
  73. 64
      backups/portal-toggle-cleanup-20260322-213704/portal_login.html
  74. 21
      backups/portal-toggle-cleanup-20260322-213704/portal_set_password.html
  75. 12
      backups/saas-panel-20260322-034651/portal_dashboard.html
  76. 9
      backups/saas-panel-20260322-034651/portal_login.html
  77. 134
      backups/saas-panel-20260322-034651/style.css
  78. 125
      backups/shared-brand-20260322-212608/portal_dashboard.html
  79. 21
      backups/shared-brand-20260322-212608/portal_forgot_password.html
  80. 31
      backups/shared-brand-20260322-212608/portal_invoice_detail.html
  81. 64
      backups/shared-brand-20260322-212608/portal_login.html
  82. 21
      backups/shared-brand-20260322-212608/portal_set_password.html
  83. 28
      backups/shared-brand-20260322-212608/site_nav.html
  84. 794
      backups/shared-brand-20260322-212608/style.css
  85. 126
      backups/shared-brand-20260322-221933/portal_dashboard.html
  86. 22
      backups/shared-brand-20260322-221933/portal_forgot_password.html
  87. 32
      backups/shared-brand-20260322-221933/portal_invoice_detail.html
  88. 65
      backups/shared-brand-20260322-221933/portal_login.html
  89. 22
      backups/shared-brand-20260322-221933/portal_set_password.html
  90. 35
      backups/shared-brand-20260322-221933/site_nav.html
  91. 1096
      backups/shared-brand-20260322-221933/style.css
  92. 129
      backups/shared-brand-20260322-222018/portal_dashboard.html
  93. 25
      backups/shared-brand-20260322-222018/portal_forgot_password.html
  94. 35
      backups/shared-brand-20260322-222018/portal_invoice_detail.html
  95. 68
      backups/shared-brand-20260322-222018/portal_login.html
  96. 25
      backups/shared-brand-20260322-222018/portal_set_password.html
  97. 35
      backups/shared-brand-20260322-222018/site_nav.html
  98. 1096
      backups/shared-brand-20260322-222018/style.css
  99. 1937
      logs/crypto_reconciliation_worker.log
  100. 91
      logs/invoice_reminder_worker.log
  101. Some files were not shown because too many files have changed in this diff Show More

31
PROJECT_STATE.md

@ -1,3 +1,34 @@
# Project State Update - v0.6.0
Updated: 2026-04-11 01:49:22 UTC
## 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
### v0.5.3 - 2026-03-27 21:25:28 ### v0.5.3 - 2026-03-27 21:25:28
- OTB Billing crypto payment flow is now stable end-to-end. - OTB Billing crypto payment flow is now stable end-to-end.

17
README.md

@ -1,3 +1,20 @@
## v0.6.0 - 2026-04-11 01:49:22
### 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
## v0.5.3 - 2026-03-27 21:25:11 ## v0.5.3 - 2026-03-27 21:25:11
- Fixed stale pending crypto payment lock issue so abandoned wallet attempts no longer trap the invoice - Fixed stale pending crypto payment lock issue so abandoned wallet attempts no longer trap the invoice

2
VERSION

@ -1 +1 @@
v0.5.3 v0.6.0

14
backend/app.py

@ -11,6 +11,7 @@ from decimal import Decimal, InvalidOperation
from pathlib import Path from pathlib import Path
from email.message import EmailMessage from email.message import EmailMessage
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from routes.portal_services import portal_services_bp
from io import BytesIO, StringIO from io import BytesIO, StringIO
import csv import csv
@ -40,6 +41,7 @@ app = Flask(
template_folder="../templates", template_folder="../templates",
static_folder="../static", static_folder="../static",
) )
app.register_blueprint(portal_services_bp)
app.config["OTB_HEALTH_DB_CONNECTOR"] = get_db_connection app.config["OTB_HEALTH_DB_CONNECTOR"] = get_db_connection
TERMS_VERSION = "v1.0" TERMS_VERSION = "v1.0"
@ -2425,7 +2427,17 @@ def admin_login():
username = (request.form.get("username") or "").strip() username = (request.form.get("username") or "").strip()
password = (request.form.get("password") or "").strip() password = (request.form.get("password") or "").strip()
if username == OTB_ADMIN_USER and password == OTB_ADMIN_PASS: admin_ok = (
(username == OTB_ADMIN_USER and password == OTB_ADMIN_PASS) or
(
username == os.getenv("OTB_ADMIN_USER_2", "").strip() and
password == os.getenv("OTB_ADMIN_PASS_2", "").strip() and
os.getenv("OTB_ADMIN_USER_2", "").strip() != "" and
os.getenv("OTB_ADMIN_PASS_2", "").strip() != ""
)
)
if admin_ok:
session["admin_authenticated"] = True session["admin_authenticated"] = True
session["admin_user"] = username session["admin_user"] = username
return redirect(session.pop("admin_next", "/")) return redirect(session.pop("admin_next", "/"))

3075
backend/app.py.fix_indent_backup

File diff suppressed because it is too large Load Diff

68
backend/routes/portal_services.py

@ -0,0 +1,68 @@
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": "https://follow-me.outsidethebox.top",
"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,
)

142
backend/templates/portal/services_here.html

@ -0,0 +1,142 @@
{% include "includes/site_nav.html" %}
<div class="portal-wrap">
<h1>Services Here</h1>
<p>Launch available OTB services.</p>
<p>Logged in as: {{ client_name }}</p>
{% block title %}Services Here{% endblock %}
{% block content %}
<section class="portal-hero">
<div class="portal-hero-copy">
<h1>Services Here</h1>
<p class="portal-subtitle">
Launch available OTB services from one place.
</p>
<p class="portal-muted">
Logged in as: {{ client_name }}
</p>
</div>
<div class="portal-hero-actions">
<a class="btn btn-secondary" href="{{ url_for('client_portal_dashboard') }}">Back to Dashboard</a>
</div>
</section>
<section class="services-grid">
{% for service in services %}
<article class="service-card status-{{ service.status }}">
<div class="service-card-header">
<div>
<h2>{{ service.name }}</h2>
<p>{{ service.summary }}</p>
</div>
<div class="service-badge-wrap">
{% if service.status == 'beta' %}
<span class="service-badge service-badge-beta">Beta</span>
{% elif service.status == 'coming_soon' %}
<span class="service-badge service-badge-soon">Coming Soon</span>
{% else %}
<span class="service-badge">Available</span>
{% endif %}
</div>
</div>
<div class="service-card-actions">
{% if service.enabled %}
<a class="btn btn-primary" href="{{ service.href }}">{{ service.button_text }}</a>
{% else %}
<button class="btn btn-disabled" type="button" disabled>{{ service.button_text }}</button>
{% endif %}
</div>
</article>
{% endfor %}
</section>
<style>
.portal-hero {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
margin-bottom: 28px;
flex-wrap: wrap;
}
.portal-hero h1 {
margin: 0 0 8px 0;
}
.portal-subtitle {
margin: 0 0 8px 0;
opacity: 0.95;
}
.portal-muted {
margin: 0;
opacity: 0.75;
font-size: 0.95rem;
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 20px;
}
.service-card {
border-radius: 18px;
padding: 22px;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(8, 18, 37, 0.72);
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
}
.service-card-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
}
.service-card h2 {
margin: 0 0 10px 0;
font-size: 1.3rem;
}
.service-card p {
margin: 0;
line-height: 1.5;
opacity: 0.92;
}
.service-card-actions {
margin-top: 22px;
}
.service-badge {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
white-space: nowrap;
background: rgba(255,255,255,0.10);
}
.service-badge-beta {
background: rgba(73, 192, 255, 0.18);
}
.service-badge-soon {
background: rgba(255, 196, 73, 0.18);
}
.btn-disabled {
opacity: 0.65;
cursor: not-allowed;
}
</style>
{% endblock %}
</div>

21
backups/css_fix_20260313-024601/base.html

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<h1>OTB Billing</h1>
</header>
<main>
<p>{{ content }}</p>
</main>
</body>
</html>
{% include "footer.html" %}

69
backups/css_fix_20260313-024601/dashboard.html

@ -0,0 +1,69 @@
<!doctype html>
<html>
<head>
<title>OTB Billing Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="otb-page">
{% if app_settings.business_logo_url %}
<div style="margin-bottom:15px;">
<img src="{{ app_settings.business_logo_url }}" style="height:60px;" alt="Logo">
</div>
{% endif %}
<h1 class="otb-section-title">{{ app_settings.business_name or 'OTB Billing' }} Dashboard</h1>
{% if request.args.get('pkg_email') == '1' %}
<div class="otb-alert otb-alert-success">
Accounting package emailed successfully.
</div>
{% endif %}
{% if request.args.get('pkg_email_failed') == '1' %}
<div class="otb-alert otb-alert-error">
Accounting package email failed. Check SMTP settings or server log.
</div>
{% endif %}
<div class="otb-nav">
<a href="/clients">Clients</a>
<a href="/services">Services</a>
<a href="/invoices">Invoices</a>
<a href="/payments">Payments</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/reports/revenue">Revenue Report</a>
<a href="/reports/aging">Aging Report</a>
<a href="/reports/accounting-package.zip">Monthly Accounting Package</a>
<a href="/settings">Settings / Config</a>
<a href="/health">Health</a>
</div>
<form method="post" action="/reports/accounting-package/email" style="margin:0 0 16px 0;">
<button type="submit">Email Accounting Package</button>
</form>
<table class="otb-table otb-summary-table">
<tr>
<th>Total Clients</th>
<th>Active Services</th>
<th>Outstanding Invoices</th>
<th>Outstanding Balance (CAD)</th>
<th>Revenue Received (CAD)</th>
</tr>
<tr>
<td>{{ total_clients }}</td>
<td>{{ active_services }}</td>
<td>{{ outstanding_invoices }}</td>
<td>{{ outstanding_balance|money('CAD') }}</td>
<td>{{ revenue_received|money('CAD') }}</td>
</tr>
</table>
<p>Displayed times are shown in Eastern Time (Toronto).</p>
</div>
{% include "footer.html" %}
</body>
</html>

376
backups/css_fix_20260313-024601/footer.html

@ -0,0 +1,376 @@
<style>
:root {
--otb-bg: #f5f7fb;
--otb-text: #1e293b;
--otb-card: #ffffff;
--otb-border: #cbd5e1;
--otb-link: #1d4ed8;
--otb-muted: #64748b;
--otb-shadow: 0 2px 8px rgba(15,23,42,0.08);
--otb-hover: #eef4ff;
--otb-success-bg: #dcfce7;
--otb-success-text: #166534;
--otb-warning-bg: #fef3c7;
--otb-warning-text: #92400e;
--otb-danger-bg: #fee2e2;
--otb-danger-text: #991b1b;
--otb-info-bg: #dbeafe;
--otb-info-text: #1d4ed8;
--otb-neutral-bg: #e5e7eb;
--otb-neutral-text: #374151;
}
body.otb-dark {
--otb-bg: #0f172a;
--otb-text: #e5e7eb;
--otb-card: #111827;
--otb-border: #334155;
--otb-link: #93c5fd;
--otb-muted: #94a3b8;
--otb-shadow: 0 2px 10px rgba(0,0,0,0.35);
--otb-hover: #172554;
--otb-success-bg: #052e16;
--otb-success-text: #86efac;
--otb-warning-bg: #451a03;
--otb-warning-text: #fcd34d;
--otb-danger-bg: #450a0a;
--otb-danger-text: #fca5a5;
--otb-info-bg: #172554;
--otb-info-text: #93c5fd;
--otb-neutral-bg: #1f2937;
--otb-neutral-text: #d1d5db;
}
body {
background: var(--otb-bg);
color: var(--otb-text);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 24px;
transition: background 0.2s ease, color 0.2s ease;
}
a {
color: var(--otb-link);
}
img {
max-width: 100%;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
.otb-page {
max-width: 1240px;
margin: 0 auto;
background: var(--otb-card);
border: 1px solid var(--otb-border);
box-shadow: var(--otb-shadow);
padding: 20px;
}
.otb-table,
table {
border-collapse: collapse;
width: 100%;
margin-top: 14px;
background: var(--otb-card);
}
.otb-table th,
.otb-table td,
table th,
table td {
border: 1px solid var(--otb-border);
padding: 8px 10px;
vertical-align: top;
}
.otb-table th,
table th {
text-align: left;
}
.otb-table tbody tr:hover,
table tbody tr:hover,
table tr:hover td {
background: var(--otb-hover);
}
.otb-money {
text-align: right;
white-space: nowrap;
}
.otb-alert {
padding: 10px;
margin-bottom: 15px;
max-width: 950px;
border: 1px solid;
}
.otb-alert-success {
background: var(--otb-success-bg);
border-color: var(--otb-success-text);
color: var(--otb-success-text);
}
.otb-alert-error {
background: var(--otb-danger-bg);
border-color: var(--otb-danger-text);
color: var(--otb-danger-text);
}
.otb-footer {
margin: 18px auto 0 auto;
max-width: 1240px;
font-size: 12px;
color: var(--otb-muted);
}
.otb-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 18px 0 20px 0;
}
.otb-nav a,
.otb-nav button {
display: inline-block;
text-decoration: none;
padding: 9px 14px;
border: 1px solid var(--otb-border);
background: var(--otb-card);
color: var(--otb-text);
border-radius: 10px;
box-shadow: var(--otb-shadow);
}
.otb-nav a:hover,
.otb-nav button:hover {
background: var(--otb-hover);
}
.otb-section-title {
margin-top: 0;
}
.otb-status {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.02em;
white-space: nowrap;
}
.otb-status-paid,
.otb-status-active,
.otb-status-confirmed,
.otb-status-sent {
background: var(--otb-success-bg);
color: var(--otb-success-text);
}
.otb-status-pending,
.otb-status-partial,
.otb-status-warning,
.otb-status-paused {
background: var(--otb-warning-bg);
color: var(--otb-warning-text);
}
.otb-status-overdue,
.otb-status-cancelled,
.otb-status-reversed,
.otb-status-failed {
background: var(--otb-danger-bg);
color: var(--otb-danger-text);
}
.otb-status-draft,
.otb-status-current,
.otb-status-info {
background: var(--otb-info-bg);
color: var(--otb-info-text);
}
.otb-status-neutral {
background: var(--otb-neutral-bg);
color: var(--otb-neutral-text);
}
/* Toggle switch */
.otb-theme-toggle-wrap {
position: fixed;
top: 12px;
right: 12px;
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
background: var(--otb-card);
color: var(--otb-text);
border: 1px solid var(--otb-border);
box-shadow: var(--otb-shadow);
border-radius: 999px;
padding: 8px 12px;
}
.otb-theme-toggle-wrap .label {
font-size: 12px;
color: var(--otb-muted);
}
.otb-switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.otb-switch input {
opacity: 0;
width: 0;
height: 0;
}
.otb-slider {
position: absolute;
inset: 0;
background: #cbd5e1;
border-radius: 999px;
transition: 0.2s ease;
}
.otb-slider:before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.2s ease;
}
.otb-switch input:checked + .otb-slider {
background: #2563eb;
}
.otb-switch input:checked + .otb-slider:before {
transform: translateX(22px);
}
</style>
<div class="otb-theme-toggle-wrap">
<span class="label">Theme</span>
<label class="otb-switch">
<input type="checkbox" id="otbThemeToggle">
<span class="otb-slider"></span>
</label>
</div>
<hr>
<div class="otb-footer">
OTB Billing v{{ app_version }}
</div>
<script>
(function () {
const themeKey = "otb_theme";
const toggle = document.getElementById("otbThemeToggle");
function applyTheme(theme) {
if (theme === "dark") {
document.body.classList.add("otb-dark");
if (toggle) toggle.checked = true;
} else {
document.body.classList.remove("otb-dark");
if (toggle) toggle.checked = false;
}
}
const savedTheme = localStorage.getItem(themeKey) || "light";
applyTheme(savedTheme);
if (toggle) {
toggle.addEventListener("change", function () {
const nextTheme = toggle.checked ? "dark" : "light";
localStorage.setItem(themeKey, nextTheme);
applyTheme(nextTheme);
});
}
function normalizeStatusText(text) {
return (text || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "-");
}
function statusClassFor(value) {
const normalized = normalizeStatusText(value);
if (["paid", "active", "confirmed", "sent"].includes(normalized)) {
return "otb-status otb-status-paid";
}
if (["pending", "partial", "paused"].includes(normalized)) {
return "otb-status otb-status-pending";
}
if (["overdue", "cancelled", "reversed", "failed"].includes(normalized)) {
return "otb-status otb-status-overdue";
}
if (["draft", "current"].includes(normalized)) {
return "otb-status otb-status-draft";
}
return "";
}
function enhanceStatusCells() {
const cells = document.querySelectorAll("td, span, div");
cells.forEach(function (el) {
if (el.dataset.otbStatusDone === "1") return;
if (el.children.length > 0) return;
const text = (el.textContent || "").trim();
const klass = statusClassFor(text);
if (!klass) return;
if (el.tagName.toLowerCase() === "span") {
el.className = (el.className ? el.className + " " : "") + klass;
} else if (el.tagName.toLowerCase() === "td" || el.tagName.toLowerCase() === "div") {
el.innerHTML = '<span class="' + klass + '">' + text + '</span>';
}
el.dataset.otbStatusDone = "1";
});
}
enhanceStatusCells();
})();
</script>

139
backups/css_fix_20260313-024601/health.html

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Health - OTB Billing</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.page-wrap {
padding: 1.5rem;
}
.page-header h1 {
margin-bottom: 0.35rem;
}
.page-header p {
margin-top: 0;
opacity: 0.9;
}
.health-links {
margin: 1rem 0 1.25rem 0;
}
.health-links a {
margin-right: 1rem;
text-decoration: underline;
}
.health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.health-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem 1.1rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 6px 18px rgba(0,0,0,0.16);
}
.health-card h3 {
margin-top: 0;
margin-bottom: 0.85rem;
}
.health-card p {
margin: 0.45rem 0;
}
.status-good {
color: #63d471;
font-weight: 700;
}
.status-bad {
color: #ff7b7b;
font-weight: 700;
}
.monoish {
word-break: break-word;
}
</style>
</head>
<body>
<div class="page-wrap">
<div class="page-header">
<h1>System Health</h1>
<p>Application and server status for OTB Billing.</p>
</div>
<div class="health-links">
<a href="/">Home</a>
<a href="/health.json">Raw JSON</a>
</div>
<div class="health-grid">
<div class="health-card">
<h3>Status</h3>
{% if health.status == "ok" %}
<p class="status-good">Healthy</p>
{% else %}
<p class="status-bad">Degraded</p>
{% endif %}
<p><strong>App:</strong> {{ health.app_name }}</p>
<p><strong>Host:</strong> {{ health.hostname }}</p>
<p class="monoish"><strong>Toronto Time:</strong> {{ health.server_time_toronto }}</p>
<p class="monoish"><strong>UTC Time:</strong> {{ health.server_time_utc }}</p>
</div>
<div class="health-card">
<h3>Database</h3>
{% if health.database.ok %}
<p class="status-good">Connected</p>
{% else %}
<p class="status-bad">Connection Error</p>
{% endif %}
<p class="monoish"><strong>Error:</strong> {{ health.database.error or "None" }}</p>
</div>
<div class="health-card">
<h3>Uptime</h3>
<p><strong>Application:</strong> {{ health.app_uptime_human }}</p>
<p><strong>Server:</strong> {{ health.server_uptime_human }}</p>
</div>
<div class="health-card">
<h3>Load Average</h3>
<p><strong>1 min:</strong> {{ health.load_average["1m"] }}</p>
<p><strong>5 min:</strong> {{ health.load_average["5m"] }}</p>
<p><strong>15 min:</strong> {{ health.load_average["15m"] }}</p>
</div>
<div class="health-card">
<h3>Memory</h3>
<p><strong>Total:</strong> {{ health.memory.total_mb }} MB</p>
<p><strong>Available:</strong> {{ health.memory.available_mb }} MB</p>
<p><strong>Used:</strong> {{ health.memory.used_mb }} MB</p>
<p><strong>Used %:</strong> {{ health.memory.used_percent }}%</p>
</div>
<div class="health-card">
<h3>Disk /</h3>
<p><strong>Total:</strong> {{ health.disk_root.total_gb }} GB</p>
<p><strong>Used:</strong> {{ health.disk_root.used_gb }} GB</p>
<p><strong>Free:</strong> {{ health.disk_root.free_gb }} GB</p>
<p><strong>Used %:</strong> {{ health.disk_root.used_percent }}%</p>
</div>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

151
backups/css_fix_20260313-024601/portal_dashboard.html

@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "footer.html" %}
</body>
</html>

81
backups/css_fix_20260313-024601/portal_forgot_password.html

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

166
backups/css_fix_20260313-024601/portal_invoice_detail.html

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.detail-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.detail-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.detail-card h3 {
margin-top: 0;
margin-bottom: 0.4rem;
}
table.portal-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-actions {
margin-top: 1rem;
}
.invoice-actions a {
margin-right: 1rem;
text-decoration: underline;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="detail-grid">
<div class="detail-card">
<h3>Invoice</h3>
<div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div>
</div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}
<span class="status-badge status-other">{{ invoice.status }}</span>
{% endif %}
</div>
<div class="detail-card">
<h3>Created</h3>
<div>{{ invoice.created_at }}</div>
</div>
<div class="detail-card">
<h3>Total</h3>
<div>{{ invoice.total_amount }}</div>
</div>
<div class="detail-card">
<h3>Paid</h3>
<div>{{ invoice.amount_paid }}</div>
</div>
<div class="detail-card">
<h3>Outstanding</h3>
<div>{{ invoice.outstanding }}</div>
</div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Line Total</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unit_price }}</td>
<td>{{ item.line_total }}</td>
</tr>
{% else %}
<tr>
<td colspan="4">No invoice line items found.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pdf_url %}
<div class="invoice-actions">
<a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a>
</div>
{% endif %}
</div>
{% include "footer.html" %}
</body>
</html>

95
backups/css_fix_20260313-024601/portal_login.html

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

75
backups/css_fix_20260313-024601/portal_set_password.html

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

202
backups/css_fix_20260313-024601/settings.html

@ -0,0 +1,202 @@
<!doctype html>
<html>
<head>
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
max-width: 1100px;
}
.card {
border: 1px solid #ccc;
padding: 16px;
}
.card h2 {
margin-top: 0;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
box-sizing: border-box;
margin-top: 4px;
margin-bottom: 12px;
padding: 8px;
}
textarea { min-height: 90px; }
.checkbox-row {
margin: 8px 0 14px 0;
}
.save-row {
margin-top: 18px;
}
.logo-preview {
margin: 10px 0 14px 0;
}
.logo-preview img {
max-height: 70px;
max-width: 220px;
border: 1px solid #ccc;
padding: 6px;
background: #fff;
}
small {
color: #444;
}
</style>
</head>
<body>
<h1>Settings / Config</h1>
<p><a href="/">Home</a></p>
<form method="post">
<div class="form-grid">
<div class="card">
<h2>Business Identity</h2>
Business Name<br>
<input type="text" name="business_name" value="{{ settings.business_name }}"><br>
Business Logo URL<br>
<input type="text" name="business_logo_url" value="{{ settings.business_logo_url }}"><br>
<small>Example: /static/favicon.png or https://site.com/logo.png</small><br>
{% if settings.business_logo_url %}
<div class="logo-preview">
<img src="{{ settings.business_logo_url }}" alt="Business Logo Preview">
</div>
{% endif %}
Slogan / Tagline<br>
<input type="text" name="business_tagline" value="{{ settings.business_tagline }}"><br>
Business Email<br>
<input type="email" name="business_email" value="{{ settings.business_email }}"><br>
Business Phone<br>
<input type="text" name="business_phone" value="{{ settings.business_phone }}"><br>
Business Address<br>
<textarea name="business_address">{{ settings.business_address }}</textarea><br>
Website<br>
<input type="text" name="business_website" value="{{ settings.business_website }}"><br>
Business Number / Registration Number<br>
<input type="text" name="business_number" value="{{ settings.business_number }}"><br>
Default Currency<br>
<select name="default_currency">
<option value="CAD" {% if settings.default_currency == 'CAD' %}selected{% endif %}>CAD</option>
<option value="USD" {% if settings.default_currency == 'USD' %}selected{% endif %}>USD</option>
<option value="ETHO" {% if settings.default_currency == 'ETHO' %}selected{% endif %}>ETHO</option>
<option value="EGAZ" {% if settings.default_currency == 'EGAZ' %}selected{% endif %}>EGAZ</option>
<option value="ALT" {% if settings.default_currency == 'ALT' %}selected{% endif %}>ALT</option>
</select>
Report Frequency<br>
<select name="report_frequency">
<option value="monthly" {% if settings.report_frequency == 'monthly' %}selected{% endif %}>monthly</option>
<option value="quarterly" {% if settings.report_frequency == 'quarterly' %}selected{% endif %}>quarterly</option>
<option value="yearly" {% if settings.report_frequency == 'yearly' %}selected{% endif %}>yearly</option>
</select>
</div>
<div class="card">
<h2>Tax Settings</h2>
Local Country<br>
<input type="text" name="local_country" value="{{ settings.local_country }}"><br>
Tax Label<br>
<input type="text" name="tax_label" value="{{ settings.tax_label }}"><br>
Tax Rate (%)<br>
<input type="number" step="0.01" name="tax_rate" value="{{ settings.tax_rate }}"><br>
Tax Number<br>
<input type="text" name="tax_number" value="{{ settings.tax_number }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="apply_local_tax_only" value="1" {% if settings.apply_local_tax_only == '1' %}checked{% endif %}>
Apply tax only to local clients
</label>
</div>
Payment Terms<br>
<textarea name="payment_terms">{{ settings.payment_terms }}</textarea><br>
Invoice Footer<br>
<textarea name="invoice_footer">{{ settings.invoice_footer }}</textarea><br>
</div>
<div class="card">
<h2>Advanced / Email / SMTP</h2>
SMTP Host<br>
<input type="text" name="smtp_host" value="{{ settings.smtp_host }}"><br>
SMTP Port<br>
<input type="number" name="smtp_port" value="{{ settings.smtp_port }}"><br>
SMTP Username<br>
<input type="text" name="smtp_user" value="{{ settings.smtp_user }}"><br>
SMTP Password<br>
<input type="password" name="smtp_pass" value="{{ settings.smtp_pass }}"><br>
From Email<br>
<input type="email" name="smtp_from_email" value="{{ settings.smtp_from_email }}"><br>
From Name<br>
<input type="text" name="smtp_from_name" value="{{ settings.smtp_from_name }}"><br>
Report / Accounting Delivery Email<br>
<input type="email" name="report_delivery_email" value="{{ settings.report_delivery_email }}"><br>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_tls" value="1" {% if settings.smtp_use_tls == '1' %}checked{% endif %}>
Use TLS
</label>
</div>
<div class="checkbox-row">
<label>
<input type="checkbox" name="smtp_use_ssl" value="1" {% if settings.smtp_use_ssl == '1' %}checked{% endif %}>
Use SSL
</label>
</div>
</div>
<div class="card">
<h2>Notes</h2>
<p>
Branding, tax identity, and SMTP values are stored here for this installation.
</p>
<p>
Logo can be a local static path like <strong>/static/favicon.png</strong> or a full external/IPFS URL.
</p>
<p>
Email sending is not wired yet, but these SMTP settings are stored now so the next step can use them.
</p>
</div>
</div>
<div class="save-row">
<button type="submit">Save Settings</button>
</div>
</form>
{% include "footer.html" %}
</body>
</html>

125
backups/footer-flag-20260322-045513/portal_dashboard.html

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Square, e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/footer-flag-20260322-045513/portal_forgot_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Square, e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

31
backups/footer-flag-20260322-045513/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

64
backups/footer-flag-20260322-045513/portal_login.html

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Square, e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/footer-flag-20260322-045513/portal_set_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Square, e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

346
backups/footer-toggle-cleanup-20260322-221548/footer.html

@ -0,0 +1,346 @@
<style>
:root {
--otb-bg: #f5f7fb;
--otb-text: #1e293b;
--otb-card: #ffffff;
--otb-border: #cbd5e1;
--otb-link: #1d4ed8;
--otb-muted: #64748b;
--otb-shadow: 0 2px 8px rgba(15,23,42,0.08);
--otb-hover: #eef4ff;
--otb-success-bg: #dcfce7;
--otb-success-text: #166534;
--otb-warning-bg: #fef3c7;
--otb-warning-text: #92400e;
--otb-danger-bg: #fee2e2;
--otb-danger-text: #991b1b;
--otb-info-bg: #dbeafe;
--otb-info-text: #1d4ed8;
--otb-neutral-bg: #e5e7eb;
--otb-neutral-text: #374151;
}
body.otb-dark {
--otb-bg: #0f172a;
--otb-text: #e5e7eb;
--otb-card: #111827;
--otb-border: #334155;
--otb-link: #93c5fd;
--otb-muted: #94a3b8;
--otb-shadow: 0 2px 10px rgba(0,0,0,0.35);
--otb-hover: #172554;
--otb-success-bg: #052e16;
--otb-success-text: #86efac;
--otb-warning-bg: #451a03;
--otb-warning-text: #fcd34d;
--otb-danger-bg: #450a0a;
--otb-danger-text: #fca5a5;
--otb-info-bg: #172554;
--otb-info-text: #93c5fd;
--otb-neutral-bg: #1f2937;
--otb-neutral-text: #d1d5db;
}
body {
background: var(--otb-bg);
color: var(--otb-text);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 24px;
transition: background 0.2s ease, color 0.2s ease;
}
a {
color: var(--otb-link);
}
img {
max-width: 100%;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
.otb-page {
max-width: 1240px;
margin: 0 auto;
background: var(--otb-card);
border: 1px solid var(--otb-border);
box-shadow: var(--otb-shadow);
padding: 20px;
}
.otb-table,
table {
border-collapse: collapse;
width: 100%;
margin-top: 14px;
background: var(--otb-card);
}
.otb-table th,
.otb-table td,
table th,
table td {
border: 1px solid var(--otb-border);
padding: 8px 10px;
vertical-align: top;
}
.otb-table th,
table th {
text-align: left;
}
.otb-table tbody tr:hover,
table tbody tr:hover,
table tr:hover td {
background: var(--otb-hover);
}
.otb-money {
text-align: right;
white-space: nowrap;
}
.otb-alert {
padding: 10px;
margin-bottom: 15px;
max-width: 950px;
border: 1px solid;
}
.otb-alert-success {
background: var(--otb-success-bg);
border-color: var(--otb-success-text);
color: var(--otb-success-text);
}
.otb-alert-error {
background: var(--otb-danger-bg);
border-color: var(--otb-danger-text);
color: var(--otb-danger-text);
}
.otb-footer {
margin: 18px auto 0 auto;
max-width: 1240px;
font-size: 12px;
color: var(--otb-muted);
}
.otb-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 18px 0 20px 0;
}
.otb-nav a,
.otb-nav button {
display: inline-block;
text-decoration: none;
padding: 9px 14px;
border: 1px solid var(--otb-border);
background: var(--otb-card);
color: var(--otb-text);
border-radius: 10px;
box-shadow: var(--otb-shadow);
}
.otb-nav a:hover,
.otb-nav button:hover {
background: var(--otb-hover);
}
.otb-section-title {
margin-top: 0;
}
.otb-status {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.02em;
white-space: nowrap;
}
.otb-status-paid,
.otb-status-active,
.otb-status-confirmed,
.otb-status-sent {
background: var(--otb-success-bg);
color: var(--otb-success-text);
}
.otb-status-pending,
.otb-status-partial,
.otb-status-warning,
.otb-status-paused {
background: var(--otb-warning-bg);
color: var(--otb-warning-text);
}
.otb-status-overdue,
.otb-status-cancelled,
.otb-status-reversed,
.otb-status-failed {
background: var(--otb-danger-bg);
color: var(--otb-danger-text);
}
.otb-status-draft,
.otb-status-current,
.otb-status-info {
background: var(--otb-info-bg);
color: var(--otb-info-text);
}
.otb-status-neutral {
background: var(--otb-neutral-bg);
color: var(--otb-neutral-text);
}
/* Toggle switch */
.otb-switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.otb-switch input {
opacity: 0;
width: 0;
height: 0;
}
.otb-slider {
position: absolute;
inset: 0;
background: #cbd5e1;
border-radius: 999px;
transition: 0.2s ease;
}
.otb-slider:before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.2s ease;
}
.otb-switch input:checked + .otb-slider {
background: #2563eb;
}
.otb-switch input:checked + .otb-slider:before {
transform: translateX(22px);
}
</style>
<hr>
<div class="otb-footer">
OTB Billing v{{ app_version }}
</div>
<script>
(function () {
const themeKey = "otb_theme";
const toggle = document.getElementById("otbThemeToggle");
function applyTheme(theme) {
if (theme === "dark") {
document.body.classList.add("otb-dark");
if (toggle) toggle.checked = true;
} else {
document.body.classList.remove("otb-dark");
if (toggle) toggle.checked = false;
}
}
const savedTheme = localStorage.getItem(themeKey) || "light";
applyTheme(savedTheme);
if (toggle) {
toggle.addEventListener("change", function () {
const nextTheme = toggle.checked ? "dark" : "light";
localStorage.setItem(themeKey, nextTheme);
applyTheme(nextTheme);
});
}
function normalizeStatusText(text) {
return (text || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "-");
}
function statusClassFor(value) {
const normalized = normalizeStatusText(value);
if (["paid", "active", "confirmed", "sent"].includes(normalized)) {
return "otb-status otb-status-paid";
}
if (["pending", "partial", "paused"].includes(normalized)) {
return "otb-status otb-status-pending";
}
if (["overdue", "cancelled", "reversed", "failed"].includes(normalized)) {
return "otb-status otb-status-overdue";
}
if (["draft", "current"].includes(normalized)) {
return "otb-status otb-status-draft";
}
return "";
}
function enhanceStatusCells() {
const cells = document.querySelectorAll("td, span, div");
cells.forEach(function (el) {
if (el.dataset.otbStatusDone === "1") return;
if (el.children.length > 0) return;
const text = (el.textContent || "").trim();
const klass = statusClassFor(text);
if (!klass) return;
if (el.tagName.toLowerCase() === "span") {
el.className = (el.className ? el.className + " " : "") + klass;
} else if (el.tagName.toLowerCase() === "td" || el.tagName.toLowerCase() === "div") {
el.innerHTML = '<span class="' + klass + '">' + text + '</span>';
}
el.dataset.otbStatusDone = "1";
});
}
enhanceStatusCells();
})();
</script>

376
backups/footer-toggle-removal-20260322-220351/footer.html

@ -0,0 +1,376 @@
<style>
:root {
--otb-bg: #f5f7fb;
--otb-text: #1e293b;
--otb-card: #ffffff;
--otb-border: #cbd5e1;
--otb-link: #1d4ed8;
--otb-muted: #64748b;
--otb-shadow: 0 2px 8px rgba(15,23,42,0.08);
--otb-hover: #eef4ff;
--otb-success-bg: #dcfce7;
--otb-success-text: #166534;
--otb-warning-bg: #fef3c7;
--otb-warning-text: #92400e;
--otb-danger-bg: #fee2e2;
--otb-danger-text: #991b1b;
--otb-info-bg: #dbeafe;
--otb-info-text: #1d4ed8;
--otb-neutral-bg: #e5e7eb;
--otb-neutral-text: #374151;
}
body.otb-dark {
--otb-bg: #0f172a;
--otb-text: #e5e7eb;
--otb-card: #111827;
--otb-border: #334155;
--otb-link: #93c5fd;
--otb-muted: #94a3b8;
--otb-shadow: 0 2px 10px rgba(0,0,0,0.35);
--otb-hover: #172554;
--otb-success-bg: #052e16;
--otb-success-text: #86efac;
--otb-warning-bg: #451a03;
--otb-warning-text: #fcd34d;
--otb-danger-bg: #450a0a;
--otb-danger-text: #fca5a5;
--otb-info-bg: #172554;
--otb-info-text: #93c5fd;
--otb-neutral-bg: #1f2937;
--otb-neutral-text: #d1d5db;
}
body {
background: var(--otb-bg);
color: var(--otb-text);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 24px;
transition: background 0.2s ease, color 0.2s ease;
}
a {
color: var(--otb-link);
}
img {
max-width: 100%;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
cursor: pointer;
}
.otb-page {
max-width: 1240px;
margin: 0 auto;
background: var(--otb-card);
border: 1px solid var(--otb-border);
box-shadow: var(--otb-shadow);
padding: 20px;
}
.otb-table,
table {
border-collapse: collapse;
width: 100%;
margin-top: 14px;
background: var(--otb-card);
}
.otb-table th,
.otb-table td,
table th,
table td {
border: 1px solid var(--otb-border);
padding: 8px 10px;
vertical-align: top;
}
.otb-table th,
table th {
text-align: left;
}
.otb-table tbody tr:hover,
table tbody tr:hover,
table tr:hover td {
background: var(--otb-hover);
}
.otb-money {
text-align: right;
white-space: nowrap;
}
.otb-alert {
padding: 10px;
margin-bottom: 15px;
max-width: 950px;
border: 1px solid;
}
.otb-alert-success {
background: var(--otb-success-bg);
border-color: var(--otb-success-text);
color: var(--otb-success-text);
}
.otb-alert-error {
background: var(--otb-danger-bg);
border-color: var(--otb-danger-text);
color: var(--otb-danger-text);
}
.otb-footer {
margin: 18px auto 0 auto;
max-width: 1240px;
font-size: 12px;
color: var(--otb-muted);
}
.otb-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 18px 0 20px 0;
}
.otb-nav a,
.otb-nav button {
display: inline-block;
text-decoration: none;
padding: 9px 14px;
border: 1px solid var(--otb-border);
background: var(--otb-card);
color: var(--otb-text);
border-radius: 10px;
box-shadow: var(--otb-shadow);
}
.otb-nav a:hover,
.otb-nav button:hover {
background: var(--otb-hover);
}
.otb-section-title {
margin-top: 0;
}
.otb-status {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.02em;
white-space: nowrap;
}
.otb-status-paid,
.otb-status-active,
.otb-status-confirmed,
.otb-status-sent {
background: var(--otb-success-bg);
color: var(--otb-success-text);
}
.otb-status-pending,
.otb-status-partial,
.otb-status-warning,
.otb-status-paused {
background: var(--otb-warning-bg);
color: var(--otb-warning-text);
}
.otb-status-overdue,
.otb-status-cancelled,
.otb-status-reversed,
.otb-status-failed {
background: var(--otb-danger-bg);
color: var(--otb-danger-text);
}
.otb-status-draft,
.otb-status-current,
.otb-status-info {
background: var(--otb-info-bg);
color: var(--otb-info-text);
}
.otb-status-neutral {
background: var(--otb-neutral-bg);
color: var(--otb-neutral-text);
}
/* Toggle switch */
.otb-theme-toggle-wrap {
position: fixed;
top: 12px;
right: 12px;
z-index: 9999;
display: flex;
align-items: center;
gap: 8px;
background: var(--otb-card);
color: var(--otb-text);
border: 1px solid var(--otb-border);
box-shadow: var(--otb-shadow);
border-radius: 999px;
padding: 8px 12px;
}
.otb-theme-toggle-wrap .label {
font-size: 12px;
color: var(--otb-muted);
}
.otb-switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.otb-switch input {
opacity: 0;
width: 0;
height: 0;
}
.otb-slider {
position: absolute;
inset: 0;
background: #cbd5e1;
border-radius: 999px;
transition: 0.2s ease;
}
.otb-slider:before {
content: "";
position: absolute;
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.2s ease;
}
.otb-switch input:checked + .otb-slider {
background: #2563eb;
}
.otb-switch input:checked + .otb-slider:before {
transform: translateX(22px);
}
</style>
<div class="otb-theme-toggle-wrap">
<span class="label">Theme</span>
<label class="otb-switch">
<input type="checkbox" id="otbThemeToggle">
<span class="otb-slider"></span>
</label>
</div>
<hr>
<div class="otb-footer">
OTB Billing v{{ app_version }}
</div>
<script>
(function () {
const themeKey = "otb_theme";
const toggle = document.getElementById("otbThemeToggle");
function applyTheme(theme) {
if (theme === "dark") {
document.body.classList.add("otb-dark");
if (toggle) toggle.checked = true;
} else {
document.body.classList.remove("otb-dark");
if (toggle) toggle.checked = false;
}
}
const savedTheme = localStorage.getItem(themeKey) || "light";
applyTheme(savedTheme);
if (toggle) {
toggle.addEventListener("change", function () {
const nextTheme = toggle.checked ? "dark" : "light";
localStorage.setItem(themeKey, nextTheme);
applyTheme(nextTheme);
});
}
function normalizeStatusText(text) {
return (text || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "-");
}
function statusClassFor(value) {
const normalized = normalizeStatusText(value);
if (["paid", "active", "confirmed", "sent"].includes(normalized)) {
return "otb-status otb-status-paid";
}
if (["pending", "partial", "paused"].includes(normalized)) {
return "otb-status otb-status-pending";
}
if (["overdue", "cancelled", "reversed", "failed"].includes(normalized)) {
return "otb-status otb-status-overdue";
}
if (["draft", "current"].includes(normalized)) {
return "otb-status otb-status-draft";
}
return "";
}
function enhanceStatusCells() {
const cells = document.querySelectorAll("td, span, div");
cells.forEach(function (el) {
if (el.dataset.otbStatusDone === "1") return;
if (el.children.length > 0) return;
const text = (el.textContent || "").trim();
const klass = statusClassFor(text);
if (!klass) return;
if (el.tagName.toLowerCase() === "span") {
el.className = (el.className ? el.className + " " : "") + klass;
} else if (el.tagName.toLowerCase() === "td" || el.tagName.toLowerCase() === "div") {
el.innerHTML = '<span class="' + klass + '">' + text + '</span>';
}
el.dataset.otbStatusDone = "1";
});
}
enhanceStatusCells();
})();
</script>

6344
backups/payment-method-fix-20260322-040838/app.py.before_fix

File diff suppressed because it is too large Load Diff

107
backups/payment-method-fix-20260322-040838/portal_dashboard.html.before_fix

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

6344
backups/payment-method-fix2-20260322-041036/app.py.before_fix

File diff suppressed because it is too large Load Diff

107
backups/payment-method-fix2-20260322-041036/portal_dashboard.html.before_fix

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

110
backups/portal-bottom-bar-20260322-044046/portal_dashboard.html

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="summary-sub" style="margin-top:6px;">via {{ row.payment_method_label }}</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

9
backups/portal-bottom-bar-20260322-044046/portal_forgot_password.html

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div> {% include "footer.html" %}
</body>
</html>

19
backups/portal-bottom-bar-20260322-044046/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

52
backups/portal-bottom-bar-20260322-044046/portal_login.html

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

9
backups/portal-bottom-bar-20260322-044046/portal_set_password.html

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div> {% include "footer.html" %}
</body>
</html>

587
backups/portal-bottom-bar-20260322-044046/style.css

@ -0,0 +1,587 @@
:root{
--bg:#0b0f14;
--card:#121825;
--card-soft:rgba(18,24,37,.78);
--text:#e8eefc;
--muted:#aab6d6;
--line:#24304a;
--accent:#7aa2ff;
--accent2:#62e6b7;
--success:#4ade80;
--warn:#fbbf24;
--danger:#f87171;
--radius:16px;
--shadow:0 16px 40px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji";
background:
radial-gradient(1200px 600px at 20% 0%, rgba(122,162,255,.22), transparent 60%),
radial-gradient(900px 500px at 90% 20%, rgba(98,230,183,.15), transparent 60%),
linear-gradient(180deg, #081225 0%, #09172d 100%);
color:var(--text);
line-height:1.45;
}
a{color:inherit}
/* ===== Shared container ===== */
.container{
max-width:1100px;
margin:0 auto;
padding:20px 18px;
}
/* ===== Header / branded nav ===== */
.header{width:100%}
.nav{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:12px 0 22px 0;
}
.brand{
display:flex;
align-items:center;
gap:14px;
text-decoration:none;
}
.brand img{
height:60px;
width:auto;
display:block;
object-fit:contain;
background: rgba(255,255,255,0.92);
padding: 6px 12px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.title{
display:flex;
flex-direction:column;
line-height:1.1;
}
.title strong{letter-spacing:.2px}
.title span{
color:var(--muted);
font-size:13px;
margin-top:2px;
}
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
}
.navlinks a{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color:var(--muted);
border:1px solid transparent;
}
.navlinks a:hover{
color:var(--text);
border-color:rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.navlinks a.active{
color:var(--text);
border-color:rgba(255,255,255,.10);
background:rgba(255,255,255,.04);
}
/* ===== Generic portal shell ===== */
.portal-shell{
max-width:1100px;
margin:16px auto 28px auto;
padding:0 18px 20px 18px;
}
.portal-card,
.detail-card,
.summary-card,
.pay-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.portal-card{
max-width:760px;
margin:24px auto 12px auto;
padding:22px;
}
.portal-page-header{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap:16px;
flex-wrap:wrap;
margin:6px 0 18px 0;
}
.portal-page-title{
margin:0;
font-size:24px;
line-height:1.1;
}
.portal-page-subtitle{
margin:8px 0 0 0;
color:var(--muted);
}
.portal-client-name{
margin:8px 0 0 0;
color:var(--text);
font-size:15px;
}
.portal-toolbar{
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:center;
}
.portal-btn,
.btn,
.pay-btn,
.quote-pick-btn{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:42px;
padding:10px 14px;
border-radius:12px;
text-decoration:none;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.05);
color:var(--text);
font-weight:600;
cursor:pointer;
}
.portal-btn:hover,
.btn:hover,
.pay-btn:hover,
.quote-pick-btn:hover{
border-color:rgba(122,162,255,.45);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
text-decoration:none;
}
.portal-btn.primary,
.btn.primary{
background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85));
border-color: transparent;
color:#071017;
font-weight:700;
}
.portal-btn.primary:hover,
.btn.primary:hover{
box-shadow:0 0 0 4px rgba(98,230,183,.18);
}
/* ===== Login / forms ===== */
.portal-sub{
color:var(--muted);
margin:0 0 16px 0;
}
.portal-form{
display:grid;
gap:14px;
}
.portal-form label{
display:block;
font-weight:600;
margin-bottom:6px;
}
.portal-form input,
.portal-form select,
.pay-selector{
width:100%;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color:var(--text);
box-sizing:border-box;
outline:none;
}
.portal-form input:focus,
.portal-form select:focus,
.pay-selector:focus{
border-color:rgba(122,162,255,.65);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
}
.portal-actions{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:4px;
}
.portal-note{
margin-top:16px;
color:var(--muted);
font-size:14px;
line-height:1.5;
}
.portal-msg,
.error-box,
.success-box{
margin-bottom:16px;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.04);
}
.error-box{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
color: #fecaca;
}
.success-box{
border-color: rgba(34, 197, 94, 0.55);
background: rgba(22, 101, 52, 0.18);
color: #dcfce7;
}
/* ===== Dashboard ===== */
.portal-wrap{
max-width:1100px;
margin:0 auto;
padding:0;
}
.summary-grid,
.detail-grid{
display:grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap:14px;
margin: 0 0 18px 0;
}
.summary-card,
.detail-card{
padding:18px;
}
.summary-card h3,
.detail-card h3{
margin:0 0 8px 0;
font-size:14px;
color:var(--muted);
}
.summary-card .summary-value,
.detail-card .detail-value{
font-size:28px;
font-weight:800;
line-height:1.1;
color:var(--text);
}
.summary-card .summary-sub{
margin-top:6px;
font-size:12px;
color:var(--muted);
}
.section-title{
margin:18px 0 10px 0;
font-size:22px;
}
.table-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
/* ===== Tables ===== */
table,
.portal-table,
.quote-table{
width:100%;
border-collapse: collapse;
}
table.portal-table th,
table.portal-table td,
.quote-table th,
.quote-table td{
padding: 0.9rem 0.85rem;
border-bottom: 1px solid rgba(255,255,255,0.10);
text-align: left;
vertical-align: middle;
}
table.portal-table th,
.quote-table th{
background: rgba(255,255,255,.06);
color: var(--text);
font-size:13px;
letter-spacing:.02em;
}
table.portal-table tr:hover td,
.quote-table tr:hover td{
background: rgba(255,255,255,.02);
}
.invoice-link{
color: var(--text);
text-decoration: none;
font-weight: 700;
}
.invoice-link:hover{
color: var(--accent);
text-decoration: underline;
}
/* ===== Badges ===== */
.status-badge,
.quote-badge{
display: inline-block;
padding: 0.22rem 0.62rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.status-paid{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.status-pending{ background: rgba(245, 158, 11, 0.20); color: var(--warn); }
.status-overdue{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
.status-other{ background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.quote-live{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.quote-stale{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
/* ===== Payments / invoice detail ===== */
.pay-card{
padding:18px;
margin-top: 1.25rem;
}
.pay-selector-row{
display:flex;
gap:0.75rem;
align-items:center;
flex-wrap:wrap;
margin-top:0.75rem;
}
.pay-panel{
margin-top: 1rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: rgba(255,255,255,0.02);
}
.pay-panel.hidden{ display:none; }
.pay-btn-square { background:#16a34a; border-color:transparent; color:#fff; }
.pay-btn-wallet { background:#2563eb; border-color:transparent; color:#fff; }
.pay-btn-mobile { background:#7c3aed; border-color:transparent; color:#fff; }
.pay-btn-copy { background:#374151; border-color:transparent; color:#fff; }
.snapshot-wrap{
position: relative;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.02);
}
.snapshot-header{
display:flex;
justify-content:space-between;
gap:1rem;
align-items:flex-start;
}
.snapshot-meta{
flex: 1 1 auto;
min-width: 0;
line-height: 1.65;
}
.snapshot-timer-box{
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value{
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
}
.snapshot-timer-label{
margin-top: 0.55rem;
font-size: 0.95rem;
opacity: 0.95;
}
.snapshot-timer-expired{ color: var(--danger); }
.lock-box{
margin-top: 1rem;
border: 1px solid rgba(34, 197, 94, 0.28);
background: rgba(22, 101, 52, 0.16);
border-radius: 12px;
padding: 1rem;
}
.lock-box.expired{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.lock-grid{
display:grid;
grid-template-columns: 1fr 220px;
gap:1rem;
align-items:start;
}
.lock-code{
display:block;
margin-top:0.35rem;
padding:0.65rem 0.8rem;
background: rgba(0,0,0,0.22);
border-radius: 8px;
overflow-wrap:anywhere;
}
.wallet-actions{
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help{
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4{
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p{ margin: 0.35rem 0; }
.wallet-note{ opacity:0.9; margin-top:0.65rem; }
.mono{ font-family: monospace; }
.copy-row{
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target{
flex: 1 1 420px;
min-width: 220px;
}
.copy-status{
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
/* ===== Footer ===== */
footer{
margin:28px auto 20px auto;
max-width:1100px;
padding:0 18px;
color:var(--muted);
font-size:13px;
}
/* ===== Responsive ===== */
@media (max-width: 900px){
.summary-grid,
.detail-grid{
grid-template-columns:1fr;
}
.nav{
align-items:flex-start;
flex-direction:column;
}
.navlinks{
justify-content:flex-start;
}
.brand img{
height:54px;
}
}
@media (max-width: 820px){
.snapshot-header,
.lock-grid{
grid-template-columns: 1fr;
display:block;
}
.snapshot-timer-box{
width: 100%;
margin-top: 1rem;
min-height: 110px;
}
}

170
backups/portal-cleanup-20260322-032010/portal_dashboard.html

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function() {
setTimeout(function() {
window.location.reload();
}, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

90
backups/portal-cleanup-20260322-032010/portal_forgot_password.html

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

735
backups/portal-cleanup-20260322-032010/portal_invoice_detail.html

@ -0,0 +1,735 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; }
.portal-actions a { margin-left: 0.75rem; text-decoration: underline; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; }
.detail-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-bottom: 1rem; }
.detail-card h3 { margin-top: 0; margin-bottom: 0.4rem; }
table.portal-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; }
table.portal-table th { background: #e9eef7; color: #10203f; }
.status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; }
.status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; }
.status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.pay-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-top: 1.25rem; }
.pay-selector-row { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; margin-top:0.75rem; }
.pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; }
.pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); }
.pay-panel.hidden { display: none; }
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square { background:#16a34a; }
.pay-btn-wallet { background:#2563eb; }
.pay-btn-mobile { background:#7c3aed; }
.pay-btn-copy { background:#374151; }
.error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); }
.snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }
.snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; }
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; }
.snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; }
.snapshot-timer-expired { color: #f87171; }
.quote-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.quote-table th, .quote-table td { padding: 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; vertical-align: top; }
.quote-table th { background: #e9eef7; color: #10203f; }
.quote-badge { display: inline-block; padding: 0.14rem 0.48rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; margin-left: 0.4rem; }
.quote-live { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.quote-stale { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.quote-pick-btn { padding: 8px 12px; border-radius: 8px; border: none; background: #2563eb; color: #fff; font-weight: 700; cursor: pointer; }
.quote-pick-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.lock-box { margin-top: 1rem; border: 1px solid rgba(34, 197, 94, 0.28); background: rgba(22, 101, 52, 0.16); border-radius: 12px; padding: 1rem; }
.lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); }
.lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; }
.lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; }
.wallet-actions {
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help {
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4 {
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p {
margin: 0.35rem 0;
}
.wallet-note { opacity:0.9; margin-top:0.65rem; }
.mono { font-family: monospace; }
.copy-row {
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target {
flex: 1 1 420px;
min-width: 220px;
}
.copy-status {
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
@media (max-width: 820px) {
.snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; }
.snapshot-timer-box { width: 100%; margin-top: 1rem; min-height: 110px; }
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
{% if (invoice.status or "")|lower == "paid" %}
<div class="success-box">✓ This invoice has been paid. Thank you!</div>
{% endif %}
{% if crypto_error %}
<div class="error-box">{{ crypto_error }}</div>
{% endif %}
<div class="detail-grid">
<div class="detail-card"><h3>Invoice</h3><div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div></div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %}
<span class="status-badge status-pending">processing</span>
{% elif s == "paid" %}<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}<span class="status-badge status-other">{{ invoice.status }}</span>{% endif %}
</div>
<div class="detail-card"><h3>Created</h3><div>{{ invoice.created_at }}</div></div>
<div class="detail-card"><h3>Total</h3><div>{{ invoice.total_amount }}</div></div>
<div class="detail-card"><h3>Paid</h3><div>{{ invoice.amount_paid }}</div></div>
<div class="detail-card"><h3>Outstanding</h3><div>{{ invoice.outstanding }}</div></div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Line Total</th></tr></thead>
<tbody>
{% for item in items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.unit_price }}</td><td>{{ item.line_total }}</td></tr>
{% else %}
<tr><td colspan="4">No invoice line items found.</td></tr>
{% endfor %}
</tbody>
</table>
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
<div class="pay-card">
<h3>Pay Now</h3>
<div class="pay-selector-row">
<label for="payMethodSelect"><strong>Choose payment method:</strong></label>
<select id="payMethodSelect" class="pay-selector">
<option value="" {% if not pay_mode %}selected{% endif %}>Select…</option>
<option value="etransfer" {% if pay_mode == "etransfer" %}selected{% endif %}>e-Transfer</option>
<option value="square" {% if pay_mode == "square" %}selected{% endif %}>Credit Card</option>
<option value="crypto" {% if pay_mode == "crypto" %}selected{% endif %}>Crypto</option>
</select>
</div>
<div id="panel-etransfer" class="pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}">
<p><strong>Interac e-Transfer</strong><br>Send payment to:<br>payment@outsidethebox.top<br>Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</p>
</div>
<div id="panel-square" class="pay-panel{% if pay_mode != 'square' %} hidden{% endif %}">
<p><strong>Credit Card (Square)</strong></p>
<a href="/portal/invoice/{{ invoice.id }}/pay-square" target="_blank" rel="noopener noreferrer" class="pay-btn pay-btn-square">Pay with Credit Card</a>
</div>
<div id="panel-crypto" class="pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}">
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
<div class="snapshot-wrap">
<div class="snapshot-header">
<div class="snapshot-meta">
<h3 style="margin-top:0;">Crypto Quote Snapshot</h3>
<div><strong>Quoted At:</strong> {{ invoice.oracle_quote.quoted_at or "—" }}</div>
<div><strong>Source Status:</strong> {{ invoice.oracle_quote.source_status or "—" }}</div>
<div><strong>Frozen Amount:</strong> {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}</div>
{% if pending_crypto_payment %}
<div style="margin-top:0.75rem;"><strong>Your quote is protected after acceptance.</strong></div>
{% else %}
<div style="margin-top:0.75rem;"><strong>Select a crypto asset to accept the quote.</strong></div>
{% endif %}
</div>
{% if pending_crypto_payment and pending_crypto_payment.txid %}
<div class="snapshot-timer-box">
<div id="processingTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.processing_expires_at_iso }}">--:--</div>
<div id="processingTimerLabel" class="snapshot-timer-label">Watching transaction / waiting for confirmation</div>
</div>
{% elif pending_crypto_payment %}
<div class="snapshot-timer-box">
<div id="lockTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.lock_expires_at_iso }}">--:--</div>
<div id="lockTimerLabel" class="snapshot-timer-label">Quote protected while you open wallet</div>
</div>
{% else %}
<div class="snapshot-timer-box">
<div id="quoteTimerValue" class="snapshot-timer-value" data-expiry="{{ crypto_quote_window_expires_iso }}">--:--</div>
<div id="quoteTimerLabel" class="snapshot-timer-label">This price times out:</div>
</div>
{% endif %}
</div>
{% if pending_crypto_payment and selected_crypto_option %}
<div id="lockBox" class="lock-box{% if pending_crypto_payment.lock_expired or pending_crypto_payment.processing_expired %} expired{% endif %}">
<div class="lock-grid">
<div>
<h3 style="margin-top:0;">{{ selected_crypto_option.label }} Payment Instructions</h3>
<div><strong>Send exactly:</strong> {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}</div>
<div style="margin-top:0.65rem;"><strong>Destination wallet:</strong></div>
<code id="walletAddressText" class="lock-code copy-target">{{ pending_crypto_payment.wallet_address }}</code>
<div style="margin-top:0.65rem;"><strong>Reference / Invoice:</strong></div>
<code id="invoiceRefText" class="lock-code copy-target">{{ pending_crypto_payment.reference }}</code>
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
<div class="wallet-actions">
<button
type="button"
id="walletPayButton"
class="pay-btn pay-btn-wallet"
data-invoice-id="{{ invoice.id }}"
data-payment-id="{{ pending_crypto_payment.id }}"
data-asset="{{ selected_crypto_option.symbol }}"
data-chain-id="{{ selected_crypto_option.chain_id }}"
data-asset-type="{{ selected_crypto_option.asset_type }}"
data-to="{{ selected_crypto_option.wallet_address }}"
data-amount="{{ pending_crypto_payment.payment_amount }}"
data-decimals="{{ selected_crypto_option.decimals }}"
data-token-contract="{{ selected_crypto_option.token_contract or '' }}"
data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}'
>
Open MetaMask / Rabby
</button>
<a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
</a>
<button type="button" id="copyDetailsButton" class="pay-btn pay-btn-copy">
Copy Payment Details
</button>
</div>
<div class="wallet-help">
<h4>Fastest way to pay</h4>
<p>1. Click <strong>Open MetaMask / Rabby</strong> if your wallet is installed in this browser.</p>
<p>2. If that does not open your wallet, click <strong>Open in MetaMask Mobile</strong>.</p>
<p>3. If needed, use <strong>Copy Payment Details</strong> and send manually.</p>
</div>
<div class="wallet-note">
You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
</div>
<div class="copy-row">
<span id="walletStatusText"></span>
<span id="copyStatusText" class="copy-status"></span>
</div>
{% elif pending_crypto_payment.txid %}
<div style="margin-top:0.9rem;"><strong>Transaction Hash:</strong></div>
<code class="lock-code mono">{{ pending_crypto_payment.txid }}</code>
<div style="margin-top:0.75rem;">Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.</div>
{% elif pending_crypto_payment.lock_expired %}
<div style="margin-top:0.75rem;">price has expired - please refresh your quote to update</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<form id="cryptoPickForm" method="post" action="/portal/invoice/{{ invoice.id }}/pay-crypto">
<table class="quote-table">
<thead><tr><th>Asset</th><th>Quoted Amount</th><th>CAD Price</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
{% for q in crypto_options %}
<tr>
<td>
{{ q.label }}
{% if q.recommended %}<span class="quote-badge quote-live">recommended</span>{% endif %}
{% if q.wallet_capable %}<span class="quote-badge quote-live">wallet</span>{% endif %}
</td>
<td>{{ q.display_amount or "—" }}</td>
<td>{% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}</td>
<td>{% if q.available %}<span class="quote-badge quote-live">live</span>{% else %}<span class="quote-badge quote-stale">{{ q.reason or "unavailable" }}</span>{% endif %}</td>
<td><button type="submit" name="asset" value="{{ q.symbol }}" class="quote-pick-btn" {% if not q.available %}disabled{% endif %}>Accept {{ q.symbol }}</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
</div>
{% else %}
<p>No crypto quote snapshot is available for this invoice yet.</p>
{% endif %}
</div>
</div>
{% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %}
</div>
<script>
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage, disableSelector) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff <= 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
if (disableSelector) {
document.querySelectorAll(disableSelector).forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) lockBox.classList.add("expired");
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer && quoteTimer.dataset.expiry) {
bindCountdown("quoteTimerValue", "quoteTimerLabel", quoteTimer.dataset.expiry, "price has expired - please refresh your view to update", "#cryptoPickForm button");
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer && lockTimer.dataset.expiry) {
bindCountdown("lockTimerValue", "lockTimerLabel", lockTimer.dataset.expiry, "price has expired - please refresh your quote to update", "#walletPayButton");
}
const processingTimer = document.getElementById("processingTimerValue");
if (processingTimer && processingTimer.dataset.expiry) {
bindCountdown("processingTimerValue", "processingTimerLabel", processingTimer.dataset.expiry, "price has expired - please refresh your quote to update", null);
}
function toHexBigIntFromDecimal(amountText, decimals) {
const text = String(amountText || "0");
const parts = text.split(".");
const whole = parts[0] || "0";
const frac = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
const combined = (whole + frac).replace(/^0+/, "") || "0";
return "0x" + BigInt(combined).toString(16);
}
function erc20TransferData(to, amountText, decimals) {
const method = "a9059cbb";
const addr = String(to || "").toLowerCase().replace(/^0x/, "").padStart(64, "0");
const amtHex = BigInt(toHexBigIntFromDecimal(amountText, decimals)).toString(16).padStart(64, "0");
return "0x" + method + addr + amtHex;
}
async function switchChain(chainId, chainAddParams) {
const hexChainId = "0x" + Number(chainId).toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
} catch (err) {
const code = err && (err.code ?? err?.data?.originalError?.code);
if ((code === 4902 || String(err).includes("4902")) && chainAddParams) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainAddParams]
});
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
}
throw err;
}
}
function buildMetaMaskMobileLink() {
const currentUrl = window.location.href;
return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, "");
}
const mmLink = document.getElementById("metamaskMobileLink");
if (mmLink) {
mmLink.href = buildMetaMaskMobileLink();
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
const copyBtn = document.getElementById("copyDetailsButton");
if (copyBtn) {
copyBtn.addEventListener("click", async function() {
const copyStatus = document.getElementById("copyStatusText");
const walletAddress = document.getElementById("walletAddressText")?.textContent || "";
const invoiceRef = document.getElementById("invoiceRefText")?.textContent || "";
const amount = document.getElementById("walletPayButton")?.dataset.amount || "";
const asset = document.getElementById("walletPayButton")?.dataset.asset || "";
const payload =
`Asset: ${asset}
Amount: ${amount} ${asset}
Wallet: ${walletAddress}
Reference: ${invoiceRef}`;
try {
await copyText(payload);
if (copyStatus) copyStatus.textContent = "Payment details copied.";
} catch (err) {
if (copyStatus) copyStatus.textContent = "Copy failed.";
}
});
}
const walletButton = document.getElementById("walletPayButton");
function pendingTxStorageKey(invoiceId, paymentId) {
return `otb_pending_tx_${invoiceId}_${paymentId}`;
}
async function submitTxHash(invoiceId, paymentId, asset, txHash) {
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
payment_id: paymentId,
asset: asset,
tx_hash: txHash
})
});
let data = {};
try {
data = await res.json();
} catch (err) {
data = { ok: false, error: "invalid_json_response" };
}
if (!res.ok || !data.ok) {
throw new Error(data.error || `submit_failed_http_${res.status}`);
}
return data;
}
async function tryRecoverPendingTxFromStorage() {
const invoiceId = "{{ invoice.id }}";
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
if (!invoiceId || !paymentId || !asset) return;
{% if pending_crypto_payment and pending_crypto_payment.txid %}
return;
{% endif %}
const key = pendingTxStorageKey(invoiceId, paymentId);
const savedTx = localStorage.getItem(key);
if (!savedTx || !savedTx.startsWith("0x")) return;
const walletStatus = document.getElementById("walletStatusText");
try {
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
await submitTxHash(invoiceId, paymentId, asset, savedTx);
localStorage.removeItem(key);
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
}
}
if (walletButton) {
walletButton.addEventListener("click", async function() {
const walletStatus = document.getElementById("walletStatusText");
const invoiceId = this.dataset.invoiceId;
const paymentId = this.dataset.paymentId;
const asset = this.dataset.asset;
const chainId = this.dataset.chainId;
const assetType = this.dataset.assetType;
const to = this.dataset.to;
const amount = this.dataset.amount;
const decimals = Number(this.dataset.decimals || "18");
const tokenContract = this.dataset.tokenContract || "";
let chainAddParams = null;
try {
chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null;
} catch (err) {
chainAddParams = null;
}
const setStatus = (msg) => {
if (walletStatus) walletStatus.textContent = msg;
};
if (!window.ethereum || !window.ethereum.request) {
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
return;
}
try {
this.disabled = true;
setStatus("Opening wallet...");
await window.ethereum.request({ method: "eth_requestAccounts" });
if (chainId && chainId !== "None" && chainId !== "") {
try {
await switchChain(Number(chainId), chainAddParams);
} catch (err) {
setStatus(`Chain switch failed: ${err.message || err}`);
this.disabled = false;
return;
}
}
let txParams;
if (assetType === "token" && tokenContract) {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: tokenContract,
data: erc20TransferData(to, amount, decimals),
value: "0x0"
};
} else {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: to,
value: toHexBigIntFromDecimal(amount, decimals)
};
}
setStatus("Waiting for wallet confirmation...");
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [txParams]
});
if (!txHash || !String(txHash).startsWith("0x")) {
throw new Error("wallet did not return a tx hash");
}
const storageKey = pendingTxStorageKey(invoiceId, paymentId);
localStorage.setItem(storageKey, String(txHash));
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
await submitTxHash(invoiceId, paymentId, asset, txHash);
localStorage.removeItem(storageKey);
setStatus("Transaction submitted. Reloading into processing view...");
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
setStatus(`Wallet submit failed: ${err.message || err}`);
this.disabled = false;
}
});
}
tryRecoverPendingTxFromStorage();
})();
</script>
<script>
(function() {
const processingAutoRefreshEnabled = {{ 'true' if pending_crypto_payment and pending_crypto_payment.txid and (invoice.status or '')|lower != 'paid' else 'false' }};
if (processingAutoRefreshEnabled) {
setTimeout(function() {
window.location.reload();
}, 10000);
}
})();
</script>
{% include "footer.html" %}
</body>
</html>

104
backups/portal-cleanup-20260322-032010/portal_login.html

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

84
backups/portal-cleanup-20260322-032010/portal_set_password.html

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

170
backups/portal-cleanup2-20260322-032316/portal_dashboard.html

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function() {
setTimeout(function() {
window.location.reload();
}, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

90
backups/portal-cleanup2-20260322-032316/portal_forgot_password.html

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

735
backups/portal-cleanup2-20260322-032316/portal_invoice_detail.html

@ -0,0 +1,735 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; }
.portal-actions a { margin-left: 0.75rem; text-decoration: underline; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; }
.detail-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-bottom: 1rem; }
.detail-card h3 { margin-top: 0; margin-bottom: 0.4rem; }
table.portal-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; }
table.portal-table th { background: #e9eef7; color: #10203f; }
.status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; }
.status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; }
.status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.pay-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-top: 1.25rem; }
.pay-selector-row { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; margin-top:0.75rem; }
.pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; }
.pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); }
.pay-panel.hidden { display: none; }
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square { background:#16a34a; }
.pay-btn-wallet { background:#2563eb; }
.pay-btn-mobile { background:#7c3aed; }
.pay-btn-copy { background:#374151; }
.error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); }
.snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }
.snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; }
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; }
.snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; }
.snapshot-timer-expired { color: #f87171; }
.quote-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.quote-table th, .quote-table td { padding: 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; vertical-align: top; }
.quote-table th { background: #e9eef7; color: #10203f; }
.quote-badge { display: inline-block; padding: 0.14rem 0.48rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; margin-left: 0.4rem; }
.quote-live { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.quote-stale { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.quote-pick-btn { padding: 8px 12px; border-radius: 8px; border: none; background: #2563eb; color: #fff; font-weight: 700; cursor: pointer; }
.quote-pick-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.lock-box { margin-top: 1rem; border: 1px solid rgba(34, 197, 94, 0.28); background: rgba(22, 101, 52, 0.16); border-radius: 12px; padding: 1rem; }
.lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); }
.lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; }
.lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; }
.wallet-actions {
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help {
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4 {
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p {
margin: 0.35rem 0;
}
.wallet-note { opacity:0.9; margin-top:0.65rem; }
.mono { font-family: monospace; }
.copy-row {
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target {
flex: 1 1 420px;
min-width: 220px;
}
.copy-status {
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
@media (max-width: 820px) {
.snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; }
.snapshot-timer-box { width: 100%; margin-top: 1rem; min-height: 110px; }
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
{% if (invoice.status or "")|lower == "paid" %}
<div class="success-box">✓ This invoice has been paid. Thank you!</div>
{% endif %}
{% if crypto_error %}
<div class="error-box">{{ crypto_error }}</div>
{% endif %}
<div class="detail-grid">
<div class="detail-card"><h3>Invoice</h3><div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div></div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %}
<span class="status-badge status-pending">processing</span>
{% elif s == "paid" %}<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}<span class="status-badge status-other">{{ invoice.status }}</span>{% endif %}
</div>
<div class="detail-card"><h3>Created</h3><div>{{ invoice.created_at }}</div></div>
<div class="detail-card"><h3>Total</h3><div>{{ invoice.total_amount }}</div></div>
<div class="detail-card"><h3>Paid</h3><div>{{ invoice.amount_paid }}</div></div>
<div class="detail-card"><h3>Outstanding</h3><div>{{ invoice.outstanding }}</div></div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Line Total</th></tr></thead>
<tbody>
{% for item in items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.unit_price }}</td><td>{{ item.line_total }}</td></tr>
{% else %}
<tr><td colspan="4">No invoice line items found.</td></tr>
{% endfor %}
</tbody>
</table>
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
<div class="pay-card">
<h3>Pay Now</h3>
<div class="pay-selector-row">
<label for="payMethodSelect"><strong>Choose payment method:</strong></label>
<select id="payMethodSelect" class="pay-selector">
<option value="" {% if not pay_mode %}selected{% endif %}>Select…</option>
<option value="etransfer" {% if pay_mode == "etransfer" %}selected{% endif %}>e-Transfer</option>
<option value="square" {% if pay_mode == "square" %}selected{% endif %}>Credit Card</option>
<option value="crypto" {% if pay_mode == "crypto" %}selected{% endif %}>Crypto</option>
</select>
</div>
<div id="panel-etransfer" class="pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}">
<p><strong>Interac e-Transfer</strong><br>Send payment to:<br>payment@outsidethebox.top<br>Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</p>
</div>
<div id="panel-square" class="pay-panel{% if pay_mode != 'square' %} hidden{% endif %}">
<p><strong>Credit Card (Square)</strong></p>
<a href="/portal/invoice/{{ invoice.id }}/pay-square" target="_blank" rel="noopener noreferrer" class="pay-btn pay-btn-square">Pay with Credit Card</a>
</div>
<div id="panel-crypto" class="pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}">
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
<div class="snapshot-wrap">
<div class="snapshot-header">
<div class="snapshot-meta">
<h3 style="margin-top:0;">Crypto Quote Snapshot</h3>
<div><strong>Quoted At:</strong> {{ invoice.oracle_quote.quoted_at or "—" }}</div>
<div><strong>Source Status:</strong> {{ invoice.oracle_quote.source_status or "—" }}</div>
<div><strong>Frozen Amount:</strong> {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}</div>
{% if pending_crypto_payment %}
<div style="margin-top:0.75rem;"><strong>Your quote is protected after acceptance.</strong></div>
{% else %}
<div style="margin-top:0.75rem;"><strong>Select a crypto asset to accept the quote.</strong></div>
{% endif %}
</div>
{% if pending_crypto_payment and pending_crypto_payment.txid %}
<div class="snapshot-timer-box">
<div id="processingTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.processing_expires_at_iso }}">--:--</div>
<div id="processingTimerLabel" class="snapshot-timer-label">Watching transaction / waiting for confirmation</div>
</div>
{% elif pending_crypto_payment %}
<div class="snapshot-timer-box">
<div id="lockTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.lock_expires_at_iso }}">--:--</div>
<div id="lockTimerLabel" class="snapshot-timer-label">Quote protected while you open wallet</div>
</div>
{% else %}
<div class="snapshot-timer-box">
<div id="quoteTimerValue" class="snapshot-timer-value" data-expiry="{{ crypto_quote_window_expires_iso }}">--:--</div>
<div id="quoteTimerLabel" class="snapshot-timer-label">This price times out:</div>
</div>
{% endif %}
</div>
{% if pending_crypto_payment and selected_crypto_option %}
<div id="lockBox" class="lock-box{% if pending_crypto_payment.lock_expired or pending_crypto_payment.processing_expired %} expired{% endif %}">
<div class="lock-grid">
<div>
<h3 style="margin-top:0;">{{ selected_crypto_option.label }} Payment Instructions</h3>
<div><strong>Send exactly:</strong> {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}</div>
<div style="margin-top:0.65rem;"><strong>Destination wallet:</strong></div>
<code id="walletAddressText" class="lock-code copy-target">{{ pending_crypto_payment.wallet_address }}</code>
<div style="margin-top:0.65rem;"><strong>Reference / Invoice:</strong></div>
<code id="invoiceRefText" class="lock-code copy-target">{{ pending_crypto_payment.reference }}</code>
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
<div class="wallet-actions">
<button
type="button"
id="walletPayButton"
class="pay-btn pay-btn-wallet"
data-invoice-id="{{ invoice.id }}"
data-payment-id="{{ pending_crypto_payment.id }}"
data-asset="{{ selected_crypto_option.symbol }}"
data-chain-id="{{ selected_crypto_option.chain_id }}"
data-asset-type="{{ selected_crypto_option.asset_type }}"
data-to="{{ selected_crypto_option.wallet_address }}"
data-amount="{{ pending_crypto_payment.payment_amount }}"
data-decimals="{{ selected_crypto_option.decimals }}"
data-token-contract="{{ selected_crypto_option.token_contract or '' }}"
data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}'
>
Open MetaMask / Rabby
</button>
<a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
</a>
<button type="button" id="copyDetailsButton" class="pay-btn pay-btn-copy">
Copy Payment Details
</button>
</div>
<div class="wallet-help">
<h4>Fastest way to pay</h4>
<p>1. Click <strong>Open MetaMask / Rabby</strong> if your wallet is installed in this browser.</p>
<p>2. If that does not open your wallet, click <strong>Open in MetaMask Mobile</strong>.</p>
<p>3. If needed, use <strong>Copy Payment Details</strong> and send manually.</p>
</div>
<div class="wallet-note">
You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
</div>
<div class="copy-row">
<span id="walletStatusText"></span>
<span id="copyStatusText" class="copy-status"></span>
</div>
{% elif pending_crypto_payment.txid %}
<div style="margin-top:0.9rem;"><strong>Transaction Hash:</strong></div>
<code class="lock-code mono">{{ pending_crypto_payment.txid }}</code>
<div style="margin-top:0.75rem;">Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.</div>
{% elif pending_crypto_payment.lock_expired %}
<div style="margin-top:0.75rem;">price has expired - please refresh your quote to update</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<form id="cryptoPickForm" method="post" action="/portal/invoice/{{ invoice.id }}/pay-crypto">
<table class="quote-table">
<thead><tr><th>Asset</th><th>Quoted Amount</th><th>CAD Price</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
{% for q in crypto_options %}
<tr>
<td>
{{ q.label }}
{% if q.recommended %}<span class="quote-badge quote-live">recommended</span>{% endif %}
{% if q.wallet_capable %}<span class="quote-badge quote-live">wallet</span>{% endif %}
</td>
<td>{{ q.display_amount or "—" }}</td>
<td>{% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}</td>
<td>{% if q.available %}<span class="quote-badge quote-live">live</span>{% else %}<span class="quote-badge quote-stale">{{ q.reason or "unavailable" }}</span>{% endif %}</td>
<td><button type="submit" name="asset" value="{{ q.symbol }}" class="quote-pick-btn" {% if not q.available %}disabled{% endif %}>Accept {{ q.symbol }}</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
</div>
{% else %}
<p>No crypto quote snapshot is available for this invoice yet.</p>
{% endif %}
</div>
</div>
{% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %}
</div>
<script>
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage, disableSelector) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff <= 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
if (disableSelector) {
document.querySelectorAll(disableSelector).forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) lockBox.classList.add("expired");
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer && quoteTimer.dataset.expiry) {
bindCountdown("quoteTimerValue", "quoteTimerLabel", quoteTimer.dataset.expiry, "price has expired - please refresh your view to update", "#cryptoPickForm button");
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer && lockTimer.dataset.expiry) {
bindCountdown("lockTimerValue", "lockTimerLabel", lockTimer.dataset.expiry, "price has expired - please refresh your quote to update", "#walletPayButton");
}
const processingTimer = document.getElementById("processingTimerValue");
if (processingTimer && processingTimer.dataset.expiry) {
bindCountdown("processingTimerValue", "processingTimerLabel", processingTimer.dataset.expiry, "price has expired - please refresh your quote to update", null);
}
function toHexBigIntFromDecimal(amountText, decimals) {
const text = String(amountText || "0");
const parts = text.split(".");
const whole = parts[0] || "0";
const frac = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
const combined = (whole + frac).replace(/^0+/, "") || "0";
return "0x" + BigInt(combined).toString(16);
}
function erc20TransferData(to, amountText, decimals) {
const method = "a9059cbb";
const addr = String(to || "").toLowerCase().replace(/^0x/, "").padStart(64, "0");
const amtHex = BigInt(toHexBigIntFromDecimal(amountText, decimals)).toString(16).padStart(64, "0");
return "0x" + method + addr + amtHex;
}
async function switchChain(chainId, chainAddParams) {
const hexChainId = "0x" + Number(chainId).toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
} catch (err) {
const code = err && (err.code ?? err?.data?.originalError?.code);
if ((code === 4902 || String(err).includes("4902")) && chainAddParams) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainAddParams]
});
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
}
throw err;
}
}
function buildMetaMaskMobileLink() {
const currentUrl = window.location.href;
return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, "");
}
const mmLink = document.getElementById("metamaskMobileLink");
if (mmLink) {
mmLink.href = buildMetaMaskMobileLink();
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
const copyBtn = document.getElementById("copyDetailsButton");
if (copyBtn) {
copyBtn.addEventListener("click", async function() {
const copyStatus = document.getElementById("copyStatusText");
const walletAddress = document.getElementById("walletAddressText")?.textContent || "";
const invoiceRef = document.getElementById("invoiceRefText")?.textContent || "";
const amount = document.getElementById("walletPayButton")?.dataset.amount || "";
const asset = document.getElementById("walletPayButton")?.dataset.asset || "";
const payload =
`Asset: ${asset}
Amount: ${amount} ${asset}
Wallet: ${walletAddress}
Reference: ${invoiceRef}`;
try {
await copyText(payload);
if (copyStatus) copyStatus.textContent = "Payment details copied.";
} catch (err) {
if (copyStatus) copyStatus.textContent = "Copy failed.";
}
});
}
const walletButton = document.getElementById("walletPayButton");
function pendingTxStorageKey(invoiceId, paymentId) {
return `otb_pending_tx_${invoiceId}_${paymentId}`;
}
async function submitTxHash(invoiceId, paymentId, asset, txHash) {
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
payment_id: paymentId,
asset: asset,
tx_hash: txHash
})
});
let data = {};
try {
data = await res.json();
} catch (err) {
data = { ok: false, error: "invalid_json_response" };
}
if (!res.ok || !data.ok) {
throw new Error(data.error || `submit_failed_http_${res.status}`);
}
return data;
}
async function tryRecoverPendingTxFromStorage() {
const invoiceId = "{{ invoice.id }}";
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
if (!invoiceId || !paymentId || !asset) return;
{% if pending_crypto_payment and pending_crypto_payment.txid %}
return;
{% endif %}
const key = pendingTxStorageKey(invoiceId, paymentId);
const savedTx = localStorage.getItem(key);
if (!savedTx || !savedTx.startsWith("0x")) return;
const walletStatus = document.getElementById("walletStatusText");
try {
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
await submitTxHash(invoiceId, paymentId, asset, savedTx);
localStorage.removeItem(key);
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
}
}
if (walletButton) {
walletButton.addEventListener("click", async function() {
const walletStatus = document.getElementById("walletStatusText");
const invoiceId = this.dataset.invoiceId;
const paymentId = this.dataset.paymentId;
const asset = this.dataset.asset;
const chainId = this.dataset.chainId;
const assetType = this.dataset.assetType;
const to = this.dataset.to;
const amount = this.dataset.amount;
const decimals = Number(this.dataset.decimals || "18");
const tokenContract = this.dataset.tokenContract || "";
let chainAddParams = null;
try {
chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null;
} catch (err) {
chainAddParams = null;
}
const setStatus = (msg) => {
if (walletStatus) walletStatus.textContent = msg;
};
if (!window.ethereum || !window.ethereum.request) {
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
return;
}
try {
this.disabled = true;
setStatus("Opening wallet...");
await window.ethereum.request({ method: "eth_requestAccounts" });
if (chainId && chainId !== "None" && chainId !== "") {
try {
await switchChain(Number(chainId), chainAddParams);
} catch (err) {
setStatus(`Chain switch failed: ${err.message || err}`);
this.disabled = false;
return;
}
}
let txParams;
if (assetType === "token" && tokenContract) {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: tokenContract,
data: erc20TransferData(to, amount, decimals),
value: "0x0"
};
} else {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: to,
value: toHexBigIntFromDecimal(amount, decimals)
};
}
setStatus("Waiting for wallet confirmation...");
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [txParams]
});
if (!txHash || !String(txHash).startsWith("0x")) {
throw new Error("wallet did not return a tx hash");
}
const storageKey = pendingTxStorageKey(invoiceId, paymentId);
localStorage.setItem(storageKey, String(txHash));
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
await submitTxHash(invoiceId, paymentId, asset, txHash);
localStorage.removeItem(storageKey);
setStatus("Transaction submitted. Reloading into processing view...");
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
setStatus(`Wallet submit failed: ${err.message || err}`);
this.disabled = false;
}
});
}
tryRecoverPendingTxFromStorage();
})();
</script>
<script>
(function() {
const processingAutoRefreshEnabled = {{ 'true' if pending_crypto_payment and pending_crypto_payment.txid and (invoice.status or '')|lower != 'paid' else 'false' }};
if (processingAutoRefreshEnabled) {
setTimeout(function() {
window.location.reload();
}, 10000);
}
})();
</script>
{% include "footer.html" %}
</body>
</html>

104
backups/portal-cleanup2-20260322-032316/portal_login.html

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

84
backups/portal-cleanup2-20260322-032316/portal_set_password.html

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/portal-dropdown-fix-20260322-182243/site_nav.html

@ -0,0 +1,21 @@
<header class="header">
<div class="container">
<div class="nav">
<a class="brand" href="https://outsidethebox.top">
<img src="/static/favicon.png" alt="outsidethebox.top logo" />
<div class="title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<nav class="navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<a href="/portal">Portal</a>
</nav>
</div>
</div>
</header>

699
backups/portal-dropdown-fix-20260322-182243/style.css

@ -0,0 +1,699 @@
:root{
--bg:#0b0f14;
--card:#121825;
--card-soft:rgba(18,24,37,.78);
--text:#e8eefc;
--muted:#aab6d6;
--line:#24304a;
--accent:#7aa2ff;
--accent2:#62e6b7;
--success:#4ade80;
--warn:#fbbf24;
--danger:#f87171;
--radius:16px;
--shadow:0 16px 40px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji";
background:
radial-gradient(1200px 600px at 20% 0%, rgba(122,162,255,.22), transparent 60%),
radial-gradient(900px 500px at 90% 20%, rgba(98,230,183,.15), transparent 60%),
linear-gradient(180deg, #081225 0%, #09172d 100%);
color:var(--text);
line-height:1.45;
}
a{color:inherit}
/* ===== Shared container ===== */
.container{
max-width:1100px;
margin:0 auto;
padding:20px 18px;
}
/* ===== Header / branded nav ===== */
.header{width:100%}
.nav{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:12px 0 22px 0;
}
.brand{
display:flex;
align-items:center;
gap:14px;
text-decoration:none;
}
.brand img{
height:60px;
width:auto;
display:block;
object-fit:contain;
background: rgba(255,255,255,0.92);
padding: 6px 12px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.title{
display:flex;
flex-direction:column;
line-height:1.1;
}
.title strong{letter-spacing:.2px}
.title span{
color:var(--muted);
font-size:13px;
margin-top:2px;
}
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
}
.navlinks a{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color:var(--muted);
border:1px solid transparent;
}
.navlinks a:hover{
color:var(--text);
border-color:rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.navlinks a.active{
color:var(--text);
border-color:rgba(255,255,255,.10);
background:rgba(255,255,255,.04);
}
/* ===== Generic portal shell ===== */
.portal-shell{
max-width:1100px;
margin:16px auto 28px auto;
padding:0 18px 20px 18px;
}
.portal-card,
.detail-card,
.summary-card,
.pay-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.portal-card{
max-width:760px;
margin:24px auto 12px auto;
padding:22px;
}
.portal-page-header{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap:16px;
flex-wrap:wrap;
margin:6px 0 18px 0;
}
.portal-page-title{
margin:0;
font-size:24px;
line-height:1.1;
}
.portal-page-subtitle{
margin:8px 0 0 0;
color:var(--muted);
}
.portal-client-name{
margin:8px 0 0 0;
color:var(--text);
font-size:15px;
}
.portal-toolbar{
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:center;
}
.portal-btn,
.btn,
.pay-btn,
.quote-pick-btn{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:42px;
padding:10px 14px;
border-radius:12px;
text-decoration:none;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.05);
color:var(--text);
font-weight:600;
cursor:pointer;
}
.portal-btn:hover,
.btn:hover,
.pay-btn:hover,
.quote-pick-btn:hover{
border-color:rgba(122,162,255,.45);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
text-decoration:none;
}
.portal-btn.primary,
.btn.primary{
background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85));
border-color: transparent;
color:#071017;
font-weight:700;
}
.portal-btn.primary:hover,
.btn.primary:hover{
box-shadow:0 0 0 4px rgba(98,230,183,.18);
}
/* ===== Login / forms ===== */
.portal-sub{
color:var(--muted);
margin:0 0 16px 0;
}
.portal-form{
display:grid;
gap:14px;
}
.portal-form label{
display:block;
font-weight:600;
margin-bottom:6px;
}
.portal-form input,
.portal-form select,
.pay-selector{
width:100%;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color:var(--text);
box-sizing:border-box;
outline:none;
}
.portal-form input:focus,
.portal-form select:focus,
.pay-selector:focus{
border-color:rgba(122,162,255,.65);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
}
.portal-actions{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:4px;
}
.portal-note{
margin-top:16px;
color:var(--muted);
font-size:14px;
line-height:1.5;
}
.portal-msg,
.error-box,
.success-box{
margin-bottom:16px;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.04);
}
.error-box{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
color: #fecaca;
}
.success-box{
border-color: rgba(34, 197, 94, 0.55);
background: rgba(22, 101, 52, 0.18);
color: #dcfce7;
}
/* ===== Dashboard ===== */
.portal-wrap{
max-width:1100px;
margin:0 auto;
padding:0;
}
.summary-grid,
.detail-grid{
display:grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap:14px;
margin: 0 0 18px 0;
}
.summary-card,
.detail-card{
padding:18px;
}
.summary-card h3,
.detail-card h3{
margin:0 0 8px 0;
font-size:14px;
color:var(--muted);
}
.summary-card .summary-value,
.detail-card .detail-value{
font-size:28px;
font-weight:800;
line-height:1.1;
color:var(--text);
}
.summary-card .summary-sub{
margin-top:6px;
font-size:12px;
color:var(--muted);
}
.section-title{
margin:18px 0 10px 0;
font-size:22px;
}
.table-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
/* ===== Tables ===== */
table,
.portal-table,
.quote-table{
width:100%;
border-collapse: collapse;
}
table.portal-table th,
table.portal-table td,
.quote-table th,
.quote-table td{
padding: 0.9rem 0.85rem;
border-bottom: 1px solid rgba(255,255,255,0.10);
text-align: left;
vertical-align: middle;
}
table.portal-table th,
.quote-table th{
background: rgba(255,255,255,.06);
color: var(--text);
font-size:13px;
letter-spacing:.02em;
}
table.portal-table tr:hover td,
.quote-table tr:hover td{
background: rgba(255,255,255,.02);
}
.invoice-link{
color: var(--text);
text-decoration: none;
font-weight: 700;
}
.invoice-link:hover{
color: var(--accent);
text-decoration: underline;
}
/* ===== Badges ===== */
.status-badge,
.quote-badge{
display: inline-block;
padding: 0.22rem 0.62rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.status-paid{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.status-pending{ background: rgba(245, 158, 11, 0.20); color: var(--warn); }
.status-overdue{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
.status-other{ background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.quote-live{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.quote-stale{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
/* ===== Payments / invoice detail ===== */
.pay-card{
padding:18px;
margin-top: 1.25rem;
}
.pay-selector-row{
display:flex;
gap:0.75rem;
align-items:center;
flex-wrap:wrap;
margin-top:0.75rem;
}
.pay-panel{
margin-top: 1rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: rgba(255,255,255,0.02);
}
.pay-panel.hidden{ display:none; }
.pay-btn-square { background:#16a34a; border-color:transparent; color:#fff; }
.pay-btn-wallet { background:#2563eb; border-color:transparent; color:#fff; }
.pay-btn-mobile { background:#7c3aed; border-color:transparent; color:#fff; }
.pay-btn-copy { background:#374151; border-color:transparent; color:#fff; }
.snapshot-wrap{
position: relative;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.02);
}
.snapshot-header{
display:flex;
justify-content:space-between;
gap:1rem;
align-items:flex-start;
}
.snapshot-meta{
flex: 1 1 auto;
min-width: 0;
line-height: 1.65;
}
.snapshot-timer-box{
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value{
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
}
.snapshot-timer-label{
margin-top: 0.55rem;
font-size: 0.95rem;
opacity: 0.95;
}
.snapshot-timer-expired{ color: var(--danger); }
.lock-box{
margin-top: 1rem;
border: 1px solid rgba(34, 197, 94, 0.28);
background: rgba(22, 101, 52, 0.16);
border-radius: 12px;
padding: 1rem;
}
.lock-box.expired{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.lock-grid{
display:grid;
grid-template-columns: 1fr 220px;
gap:1rem;
align-items:start;
}
.lock-code{
display:block;
margin-top:0.35rem;
padding:0.65rem 0.8rem;
background: rgba(0,0,0,0.22);
border-radius: 8px;
overflow-wrap:anywhere;
}
.wallet-actions{
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help{
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4{
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p{ margin: 0.35rem 0; }
.wallet-note{ opacity:0.9; margin-top:0.65rem; }
.mono{ font-family: monospace; }
.copy-row{
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target{
flex: 1 1 420px;
min-width: 220px;
}
.copy-status{
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
/* ===== Footer ===== */
footer{
margin:28px auto 20px auto;
max-width:1100px;
padding:0 18px;
color:var(--muted);
font-size:13px;
}
/* ===== Responsive ===== */
@media (max-width: 900px){
.summary-grid,
.detail-grid{
grid-template-columns:1fr;
}
.nav{
align-items:flex-start;
flex-direction:column;
}
.navlinks{
justify-content:flex-start;
}
.brand img{
height:54px;
}
}
@media (max-width: 820px){
.snapshot-header,
.lock-grid{
grid-template-columns: 1fr;
display:block;
}
.snapshot-timer-box{
width: 100%;
margin-top: 1rem;
min-height: 110px;
}
}
/* ===== Fixed CAD / Oracle status bar ===== */
body{
padding-bottom: 56px;
}
.otb-statusbar{
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 8px 14px;
background: rgba(8, 16, 32, 0.94);
border-top: 1px solid rgba(255,255,255,.10);
backdrop-filter: blur(8px);
box-shadow: 0 -8px 24px rgba(0,0,0,.28);
}
.otb-statusbar-inner{
width: 100%;
max-width: 1100px;
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
text-align: center;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.otb-statusbar strong{
color: var(--text);
font-weight: 700;
}
.otb-statusbar a{
color: var(--accent2);
text-decoration: none;
font-weight: 600;
}
.otb-statusbar a:hover{
text-decoration: underline;
}
.otb-dot{
width: 6px;
height: 6px;
border-radius: 999px;
display: inline-block;
background: rgba(255,255,255,.25);
flex: 0 0 auto;
}
/* ===== Payment method chips ===== */
.payment-method{
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 7px;
padding: 4px 9px;
border-radius: 999px;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.08);
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .01em;
}
.payment-method::before{
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
background: #7aa2ff;
box-shadow: 0 0 0 3px rgba(255,255,255,.04);
}
.payment-square::before{ background: #7dd3fc; }
.payment-etransfer::before{ background: #86efac; }
.payment-etho::before{ background: #b084ff; }
.payment-etica::before{ background: #4fd1c5; }
.payment-alt::before{ background: #fbbf24; }
.payment-cad::before{ background: #cbd5e1; }
/* ===== Slightly stronger row hover ===== */
table.portal-table tbody tr:hover td,
.quote-table tbody tr:hover td{
background: rgba(255,255,255,.035);
transition: background .14s ease;
}
@media (max-width: 700px){
body{
padding-bottom: 72px;
}
.otb-statusbar-inner{
font-size: 11px;
line-height: 1.25;
}
}

28
backups/portal-dropdown-fix-20260322-182803/site_nav.html

@ -0,0 +1,28 @@
<header class="header">
<div class="container">
<div class="nav">
<a class="brand" href="https://outsidethebox.top">
<img src="/static/favicon.png" alt="outsidethebox.top logo" />
<div class="title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<nav class="navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<div class="dropdown">
<a href="#" class="dropdown-toggle">Services</a>
<div class="dropdown-menu">
<a href="https://follow-me.outsidethebox.top">Follow-me Tracker</a>
<a href="https://monitor.outsidethebox.top">Oracle</a>
</div>
</div>
<a href="/portal">Portal</a>
</nav>
</div>
</div>
</header>

775
backups/portal-dropdown-fix-20260322-182803/style.css

@ -0,0 +1,775 @@
:root{
--bg:#0b0f14;
--card:#121825;
--card-soft:rgba(18,24,37,.78);
--text:#e8eefc;
--muted:#aab6d6;
--line:#24304a;
--accent:#7aa2ff;
--accent2:#62e6b7;
--success:#4ade80;
--warn:#fbbf24;
--danger:#f87171;
--radius:16px;
--shadow:0 16px 40px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji";
background:
radial-gradient(1200px 600px at 20% 0%, rgba(122,162,255,.22), transparent 60%),
radial-gradient(900px 500px at 90% 20%, rgba(98,230,183,.15), transparent 60%),
linear-gradient(180deg, #081225 0%, #09172d 100%);
color:var(--text);
line-height:1.45;
}
a{color:inherit}
/* ===== Shared container ===== */
.container{
max-width:1100px;
margin:0 auto;
padding:20px 18px;
}
/* ===== Header / branded nav ===== */
.header{width:100%}
.nav{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:12px 0 22px 0;
}
.brand{
display:flex;
align-items:center;
gap:14px;
text-decoration:none;
}
.brand img{
height:60px;
width:auto;
display:block;
object-fit:contain;
background: rgba(255,255,255,0.92);
padding: 6px 12px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.title{
display:flex;
flex-direction:column;
line-height:1.1;
}
.title strong{letter-spacing:.2px}
.title span{
color:var(--muted);
font-size:13px;
margin-top:2px;
}
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
}
.navlinks a{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color:var(--muted);
border:1px solid transparent;
}
.navlinks a:hover{
color:var(--text);
border-color:rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.navlinks a.active{
color:var(--text);
border-color:rgba(255,255,255,.10);
background:rgba(255,255,255,.04);
}
/* ===== Generic portal shell ===== */
.portal-shell{
max-width:1100px;
margin:16px auto 28px auto;
padding:0 18px 20px 18px;
}
.portal-card,
.detail-card,
.summary-card,
.pay-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.portal-card{
max-width:760px;
margin:24px auto 12px auto;
padding:22px;
}
.portal-page-header{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap:16px;
flex-wrap:wrap;
margin:6px 0 18px 0;
}
.portal-page-title{
margin:0;
font-size:24px;
line-height:1.1;
}
.portal-page-subtitle{
margin:8px 0 0 0;
color:var(--muted);
}
.portal-client-name{
margin:8px 0 0 0;
color:var(--text);
font-size:15px;
}
.portal-toolbar{
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:center;
}
.portal-btn,
.btn,
.pay-btn,
.quote-pick-btn{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:42px;
padding:10px 14px;
border-radius:12px;
text-decoration:none;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.05);
color:var(--text);
font-weight:600;
cursor:pointer;
}
.portal-btn:hover,
.btn:hover,
.pay-btn:hover,
.quote-pick-btn:hover{
border-color:rgba(122,162,255,.45);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
text-decoration:none;
}
.portal-btn.primary,
.btn.primary{
background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85));
border-color: transparent;
color:#071017;
font-weight:700;
}
.portal-btn.primary:hover,
.btn.primary:hover{
box-shadow:0 0 0 4px rgba(98,230,183,.18);
}
/* ===== Login / forms ===== */
.portal-sub{
color:var(--muted);
margin:0 0 16px 0;
}
.portal-form{
display:grid;
gap:14px;
}
.portal-form label{
display:block;
font-weight:600;
margin-bottom:6px;
}
.portal-form input,
.portal-form select,
.pay-selector{
width:100%;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color:var(--text);
box-sizing:border-box;
outline:none;
}
.portal-form input:focus,
.portal-form select:focus,
.pay-selector:focus{
border-color:rgba(122,162,255,.65);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
}
.portal-actions{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:4px;
}
.portal-note{
margin-top:16px;
color:var(--muted);
font-size:14px;
line-height:1.5;
}
.portal-msg,
.error-box,
.success-box{
margin-bottom:16px;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.04);
}
.error-box{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
color: #fecaca;
}
.success-box{
border-color: rgba(34, 197, 94, 0.55);
background: rgba(22, 101, 52, 0.18);
color: #dcfce7;
}
/* ===== Dashboard ===== */
.portal-wrap{
max-width:1100px;
margin:0 auto;
padding:0;
}
.summary-grid,
.detail-grid{
display:grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap:14px;
margin: 0 0 18px 0;
}
.summary-card,
.detail-card{
padding:18px;
}
.summary-card h3,
.detail-card h3{
margin:0 0 8px 0;
font-size:14px;
color:var(--muted);
}
.summary-card .summary-value,
.detail-card .detail-value{
font-size:28px;
font-weight:800;
line-height:1.1;
color:var(--text);
}
.summary-card .summary-sub{
margin-top:6px;
font-size:12px;
color:var(--muted);
}
.section-title{
margin:18px 0 10px 0;
font-size:22px;
}
.table-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
/* ===== Tables ===== */
table,
.portal-table,
.quote-table{
width:100%;
border-collapse: collapse;
}
table.portal-table th,
table.portal-table td,
.quote-table th,
.quote-table td{
padding: 0.9rem 0.85rem;
border-bottom: 1px solid rgba(255,255,255,0.10);
text-align: left;
vertical-align: middle;
}
table.portal-table th,
.quote-table th{
background: rgba(255,255,255,.06);
color: var(--text);
font-size:13px;
letter-spacing:.02em;
}
table.portal-table tr:hover td,
.quote-table tr:hover td{
background: rgba(255,255,255,.02);
}
.invoice-link{
color: var(--text);
text-decoration: none;
font-weight: 700;
}
.invoice-link:hover{
color: var(--accent);
text-decoration: underline;
}
/* ===== Badges ===== */
.status-badge,
.quote-badge{
display: inline-block;
padding: 0.22rem 0.62rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.status-paid{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.status-pending{ background: rgba(245, 158, 11, 0.20); color: var(--warn); }
.status-overdue{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
.status-other{ background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.quote-live{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.quote-stale{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
/* ===== Payments / invoice detail ===== */
.pay-card{
padding:18px;
margin-top: 1.25rem;
}
.pay-selector-row{
display:flex;
gap:0.75rem;
align-items:center;
flex-wrap:wrap;
margin-top:0.75rem;
}
.pay-panel{
margin-top: 1rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: rgba(255,255,255,0.02);
}
.pay-panel.hidden{ display:none; }
.pay-btn-square { background:#16a34a; border-color:transparent; color:#fff; }
.pay-btn-wallet { background:#2563eb; border-color:transparent; color:#fff; }
.pay-btn-mobile { background:#7c3aed; border-color:transparent; color:#fff; }
.pay-btn-copy { background:#374151; border-color:transparent; color:#fff; }
.snapshot-wrap{
position: relative;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.02);
}
.snapshot-header{
display:flex;
justify-content:space-between;
gap:1rem;
align-items:flex-start;
}
.snapshot-meta{
flex: 1 1 auto;
min-width: 0;
line-height: 1.65;
}
.snapshot-timer-box{
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value{
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
}
.snapshot-timer-label{
margin-top: 0.55rem;
font-size: 0.95rem;
opacity: 0.95;
}
.snapshot-timer-expired{ color: var(--danger); }
.lock-box{
margin-top: 1rem;
border: 1px solid rgba(34, 197, 94, 0.28);
background: rgba(22, 101, 52, 0.16);
border-radius: 12px;
padding: 1rem;
}
.lock-box.expired{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.lock-grid{
display:grid;
grid-template-columns: 1fr 220px;
gap:1rem;
align-items:start;
}
.lock-code{
display:block;
margin-top:0.35rem;
padding:0.65rem 0.8rem;
background: rgba(0,0,0,0.22);
border-radius: 8px;
overflow-wrap:anywhere;
}
.wallet-actions{
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help{
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4{
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p{ margin: 0.35rem 0; }
.wallet-note{ opacity:0.9; margin-top:0.65rem; }
.mono{ font-family: monospace; }
.copy-row{
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target{
flex: 1 1 420px;
min-width: 220px;
}
.copy-status{
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
/* ===== Footer ===== */
footer{
margin:28px auto 20px auto;
max-width:1100px;
padding:0 18px;
color:var(--muted);
font-size:13px;
}
/* ===== Responsive ===== */
@media (max-width: 900px){
.summary-grid,
.detail-grid{
grid-template-columns:1fr;
}
.nav{
align-items:flex-start;
flex-direction:column;
}
.navlinks{
justify-content:flex-start;
}
.brand img{
height:54px;
}
}
@media (max-width: 820px){
.snapshot-header,
.lock-grid{
grid-template-columns: 1fr;
display:block;
}
.snapshot-timer-box{
width: 100%;
margin-top: 1rem;
min-height: 110px;
}
}
/* ===== Fixed CAD / Oracle status bar ===== */
body{
padding-bottom: 56px;
}
.otb-statusbar{
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 8px 14px;
background: rgba(8, 16, 32, 0.94);
border-top: 1px solid rgba(255,255,255,.10);
backdrop-filter: blur(8px);
box-shadow: 0 -8px 24px rgba(0,0,0,.28);
}
.otb-statusbar-inner{
width: 100%;
max-width: 1100px;
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
text-align: center;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.otb-statusbar strong{
color: var(--text);
font-weight: 700;
}
.otb-statusbar a{
color: var(--accent2);
text-decoration: none;
font-weight: 600;
}
.otb-statusbar a:hover{
text-decoration: underline;
}
.otb-dot{
width: 6px;
height: 6px;
border-radius: 999px;
display: inline-block;
background: rgba(255,255,255,.25);
flex: 0 0 auto;
}
/* ===== Payment method chips ===== */
.payment-method{
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 7px;
padding: 4px 9px;
border-radius: 999px;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.08);
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .01em;
}
.payment-method::before{
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
background: #7aa2ff;
box-shadow: 0 0 0 3px rgba(255,255,255,.04);
}
.payment-square::before{ background: #7dd3fc; }
.payment-etransfer::before{ background: #86efac; }
.payment-etho::before{ background: #b084ff; }
.payment-etica::before{ background: #4fd1c5; }
.payment-alt::before{ background: #fbbf24; }
.payment-cad::before{ background: #cbd5e1; }
/* ===== Slightly stronger row hover ===== */
table.portal-table tbody tr:hover td,
.quote-table tbody tr:hover td{
background: rgba(255,255,255,.035);
transition: background .14s ease;
}
@media (max-width: 700px){
body{
padding-bottom: 72px;
}
.otb-statusbar-inner{
font-size: 11px;
line-height: 1.25;
}
}
/* ===== Shared services dropdown ===== */
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
align-items:center;
}
.dropdown{
position:relative;
display:inline-block;
}
.dropdown-toggle{
display:inline-block;
cursor:pointer;
}
.dropdown-menu{
position:absolute;
top:calc(100% + 8px);
right:0;
min-width:220px;
display:none;
padding:10px;
border-radius:14px;
background:rgba(18,24,37,.98);
border:1px solid rgba(255,255,255,.08);
box-shadow:0 16px 40px rgba(0,0,0,.35);
z-index:9999;
}
.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu{
display:block;
}
.dropdown-menu a{
display:block;
padding:9px 10px;
border-radius:10px;
color:var(--muted);
text-decoration:none;
white-space:nowrap;
margin:0;
}
.dropdown-menu a + a{
margin-top:4px;
}
.dropdown-menu a:hover{
color:var(--text);
background:rgba(255,255,255,.04);
}
@media (max-width: 900px){
.dropdown{
width:100%;
}
.dropdown-toggle{
width:100%;
}
.dropdown-menu{
position:static;
right:auto;
top:auto;
min-width:100%;
margin-top:6px;
}
}

168
backups/portal-links-20260322-033024/portal_dashboard.html

@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function() {
setTimeout(function() {
window.location.reload();
}, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

88
backups/portal-links-20260322-033024/portal_forgot_password.html

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

733
backups/portal-links-20260322-033024/portal_invoice_detail.html

@ -0,0 +1,733 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; }
.portal-actions a { margin-left: 0.75rem; text-decoration: underline; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; }
.detail-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-bottom: 1rem; }
.detail-card h3 { margin-top: 0; margin-bottom: 0.4rem; }
table.portal-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; }
table.portal-table th { background: #e9eef7; color: #10203f; }
.status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; }
.status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; }
.status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.pay-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-top: 1.25rem; }
.pay-selector-row { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; margin-top:0.75rem; }
.pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; }
.pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); }
.pay-panel.hidden { display: none; }
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square { background:#16a34a; }
.pay-btn-wallet { background:#2563eb; }
.pay-btn-mobile { background:#7c3aed; }
.pay-btn-copy { background:#374151; }
.error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); }
.snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }
.snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; }
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; }
.snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; }
.snapshot-timer-expired { color: #f87171; }
.quote-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.quote-table th, .quote-table td { padding: 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; vertical-align: top; }
.quote-table th { background: #e9eef7; color: #10203f; }
.quote-badge { display: inline-block; padding: 0.14rem 0.48rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; margin-left: 0.4rem; }
.quote-live { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.quote-stale { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.quote-pick-btn { padding: 8px 12px; border-radius: 8px; border: none; background: #2563eb; color: #fff; font-weight: 700; cursor: pointer; }
.quote-pick-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.lock-box { margin-top: 1rem; border: 1px solid rgba(34, 197, 94, 0.28); background: rgba(22, 101, 52, 0.16); border-radius: 12px; padding: 1rem; }
.lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); }
.lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; }
.lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; }
.wallet-actions {
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help {
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4 {
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p {
margin: 0.35rem 0;
}
.wallet-note { opacity:0.9; margin-top:0.65rem; }
.mono { font-family: monospace; }
.copy-row {
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target {
flex: 1 1 420px;
min-width: 220px;
}
.copy-status {
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
@media (max-width: 820px) {
.snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; }
.snapshot-timer-box { width: 100%; margin-top: 1rem; min-height: 110px; }
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
{% if (invoice.status or "")|lower == "paid" %}
<div class="success-box">✓ This invoice has been paid. Thank you!</div>
{% endif %}
{% if crypto_error %}
<div class="error-box">{{ crypto_error }}</div>
{% endif %}
<div class="detail-grid">
<div class="detail-card"><h3>Invoice</h3><div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div></div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %}
<span class="status-badge status-pending">processing</span>
{% elif s == "paid" %}<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}<span class="status-badge status-other">{{ invoice.status }}</span>{% endif %}
</div>
<div class="detail-card"><h3>Created</h3><div>{{ invoice.created_at }}</div></div>
<div class="detail-card"><h3>Total</h3><div>{{ invoice.total_amount }}</div></div>
<div class="detail-card"><h3>Paid</h3><div>{{ invoice.amount_paid }}</div></div>
<div class="detail-card"><h3>Outstanding</h3><div>{{ invoice.outstanding }}</div></div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Line Total</th></tr></thead>
<tbody>
{% for item in items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.unit_price }}</td><td>{{ item.line_total }}</td></tr>
{% else %}
<tr><td colspan="4">No invoice line items found.</td></tr>
{% endfor %}
</tbody>
</table>
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
<div class="pay-card">
<h3>Pay Now</h3>
<div class="pay-selector-row">
<label for="payMethodSelect"><strong>Choose payment method:</strong></label>
<select id="payMethodSelect" class="pay-selector">
<option value="" {% if not pay_mode %}selected{% endif %}>Select…</option>
<option value="etransfer" {% if pay_mode == "etransfer" %}selected{% endif %}>e-Transfer</option>
<option value="square" {% if pay_mode == "square" %}selected{% endif %}>Credit Card</option>
<option value="crypto" {% if pay_mode == "crypto" %}selected{% endif %}>Crypto</option>
</select>
</div>
<div id="panel-etransfer" class="pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}">
<p><strong>Interac e-Transfer</strong><br>Send payment to:<br>payment@outsidethebox.top<br>Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</p>
</div>
<div id="panel-square" class="pay-panel{% if pay_mode != 'square' %} hidden{% endif %}">
<p><strong>Credit Card (Square)</strong></p>
<a href="/portal/invoice/{{ invoice.id }}/pay-square" target="_blank" rel="noopener noreferrer" class="pay-btn pay-btn-square">Pay with Credit Card</a>
</div>
<div id="panel-crypto" class="pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}">
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
<div class="snapshot-wrap">
<div class="snapshot-header">
<div class="snapshot-meta">
<h3 style="margin-top:0;">Crypto Quote Snapshot</h3>
<div><strong>Quoted At:</strong> {{ invoice.oracle_quote.quoted_at or "—" }}</div>
<div><strong>Source Status:</strong> {{ invoice.oracle_quote.source_status or "—" }}</div>
<div><strong>Frozen Amount:</strong> {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}</div>
{% if pending_crypto_payment %}
<div style="margin-top:0.75rem;"><strong>Your quote is protected after acceptance.</strong></div>
{% else %}
<div style="margin-top:0.75rem;"><strong>Select a crypto asset to accept the quote.</strong></div>
{% endif %}
</div>
{% if pending_crypto_payment and pending_crypto_payment.txid %}
<div class="snapshot-timer-box">
<div id="processingTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.processing_expires_at_iso }}">--:--</div>
<div id="processingTimerLabel" class="snapshot-timer-label">Watching transaction / waiting for confirmation</div>
</div>
{% elif pending_crypto_payment %}
<div class="snapshot-timer-box">
<div id="lockTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.lock_expires_at_iso }}">--:--</div>
<div id="lockTimerLabel" class="snapshot-timer-label">Quote protected while you open wallet</div>
</div>
{% else %}
<div class="snapshot-timer-box">
<div id="quoteTimerValue" class="snapshot-timer-value" data-expiry="{{ crypto_quote_window_expires_iso }}">--:--</div>
<div id="quoteTimerLabel" class="snapshot-timer-label">This price times out:</div>
</div>
{% endif %}
</div>
{% if pending_crypto_payment and selected_crypto_option %}
<div id="lockBox" class="lock-box{% if pending_crypto_payment.lock_expired or pending_crypto_payment.processing_expired %} expired{% endif %}">
<div class="lock-grid">
<div>
<h3 style="margin-top:0;">{{ selected_crypto_option.label }} Payment Instructions</h3>
<div><strong>Send exactly:</strong> {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}</div>
<div style="margin-top:0.65rem;"><strong>Destination wallet:</strong></div>
<code id="walletAddressText" class="lock-code copy-target">{{ pending_crypto_payment.wallet_address }}</code>
<div style="margin-top:0.65rem;"><strong>Reference / Invoice:</strong></div>
<code id="invoiceRefText" class="lock-code copy-target">{{ pending_crypto_payment.reference }}</code>
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
<div class="wallet-actions">
<button
type="button"
id="walletPayButton"
class="pay-btn pay-btn-wallet"
data-invoice-id="{{ invoice.id }}"
data-payment-id="{{ pending_crypto_payment.id }}"
data-asset="{{ selected_crypto_option.symbol }}"
data-chain-id="{{ selected_crypto_option.chain_id }}"
data-asset-type="{{ selected_crypto_option.asset_type }}"
data-to="{{ selected_crypto_option.wallet_address }}"
data-amount="{{ pending_crypto_payment.payment_amount }}"
data-decimals="{{ selected_crypto_option.decimals }}"
data-token-contract="{{ selected_crypto_option.token_contract or '' }}"
data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}'
>
Open MetaMask / Rabby
</button>
<a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
</a>
<button type="button" id="copyDetailsButton" class="pay-btn pay-btn-copy">
Copy Payment Details
</button>
</div>
<div class="wallet-help">
<h4>Fastest way to pay</h4>
<p>1. Click <strong>Open MetaMask / Rabby</strong> if your wallet is installed in this browser.</p>
<p>2. If that does not open your wallet, click <strong>Open in MetaMask Mobile</strong>.</p>
<p>3. If needed, use <strong>Copy Payment Details</strong> and send manually.</p>
</div>
<div class="wallet-note">
You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
</div>
<div class="copy-row">
<span id="walletStatusText"></span>
<span id="copyStatusText" class="copy-status"></span>
</div>
{% elif pending_crypto_payment.txid %}
<div style="margin-top:0.9rem;"><strong>Transaction Hash:</strong></div>
<code class="lock-code mono">{{ pending_crypto_payment.txid }}</code>
<div style="margin-top:0.75rem;">Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.</div>
{% elif pending_crypto_payment.lock_expired %}
<div style="margin-top:0.75rem;">price has expired - please refresh your quote to update</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<form id="cryptoPickForm" method="post" action="/portal/invoice/{{ invoice.id }}/pay-crypto">
<table class="quote-table">
<thead><tr><th>Asset</th><th>Quoted Amount</th><th>CAD Price</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
{% for q in crypto_options %}
<tr>
<td>
{{ q.label }}
{% if q.recommended %}<span class="quote-badge quote-live">recommended</span>{% endif %}
{% if q.wallet_capable %}<span class="quote-badge quote-live">wallet</span>{% endif %}
</td>
<td>{{ q.display_amount or "—" }}</td>
<td>{% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}</td>
<td>{% if q.available %}<span class="quote-badge quote-live">live</span>{% else %}<span class="quote-badge quote-stale">{{ q.reason or "unavailable" }}</span>{% endif %}</td>
<td><button type="submit" name="asset" value="{{ q.symbol }}" class="quote-pick-btn" {% if not q.available %}disabled{% endif %}>Accept {{ q.symbol }}</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
</div>
{% else %}
<p>No crypto quote snapshot is available for this invoice yet.</p>
{% endif %}
</div>
</div>
{% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %}
</div>
<script>
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage, disableSelector) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff <= 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
if (disableSelector) {
document.querySelectorAll(disableSelector).forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) lockBox.classList.add("expired");
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer && quoteTimer.dataset.expiry) {
bindCountdown("quoteTimerValue", "quoteTimerLabel", quoteTimer.dataset.expiry, "price has expired - please refresh your view to update", "#cryptoPickForm button");
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer && lockTimer.dataset.expiry) {
bindCountdown("lockTimerValue", "lockTimerLabel", lockTimer.dataset.expiry, "price has expired - please refresh your quote to update", "#walletPayButton");
}
const processingTimer = document.getElementById("processingTimerValue");
if (processingTimer && processingTimer.dataset.expiry) {
bindCountdown("processingTimerValue", "processingTimerLabel", processingTimer.dataset.expiry, "price has expired - please refresh your quote to update", null);
}
function toHexBigIntFromDecimal(amountText, decimals) {
const text = String(amountText || "0");
const parts = text.split(".");
const whole = parts[0] || "0";
const frac = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
const combined = (whole + frac).replace(/^0+/, "") || "0";
return "0x" + BigInt(combined).toString(16);
}
function erc20TransferData(to, amountText, decimals) {
const method = "a9059cbb";
const addr = String(to || "").toLowerCase().replace(/^0x/, "").padStart(64, "0");
const amtHex = BigInt(toHexBigIntFromDecimal(amountText, decimals)).toString(16).padStart(64, "0");
return "0x" + method + addr + amtHex;
}
async function switchChain(chainId, chainAddParams) {
const hexChainId = "0x" + Number(chainId).toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
} catch (err) {
const code = err && (err.code ?? err?.data?.originalError?.code);
if ((code === 4902 || String(err).includes("4902")) && chainAddParams) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainAddParams]
});
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
}
throw err;
}
}
function buildMetaMaskMobileLink() {
const currentUrl = window.location.href;
return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, "");
}
const mmLink = document.getElementById("metamaskMobileLink");
if (mmLink) {
mmLink.href = buildMetaMaskMobileLink();
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
const copyBtn = document.getElementById("copyDetailsButton");
if (copyBtn) {
copyBtn.addEventListener("click", async function() {
const copyStatus = document.getElementById("copyStatusText");
const walletAddress = document.getElementById("walletAddressText")?.textContent || "";
const invoiceRef = document.getElementById("invoiceRefText")?.textContent || "";
const amount = document.getElementById("walletPayButton")?.dataset.amount || "";
const asset = document.getElementById("walletPayButton")?.dataset.asset || "";
const payload =
`Asset: ${asset}
Amount: ${amount} ${asset}
Wallet: ${walletAddress}
Reference: ${invoiceRef}`;
try {
await copyText(payload);
if (copyStatus) copyStatus.textContent = "Payment details copied.";
} catch (err) {
if (copyStatus) copyStatus.textContent = "Copy failed.";
}
});
}
const walletButton = document.getElementById("walletPayButton");
function pendingTxStorageKey(invoiceId, paymentId) {
return `otb_pending_tx_${invoiceId}_${paymentId}`;
}
async function submitTxHash(invoiceId, paymentId, asset, txHash) {
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
payment_id: paymentId,
asset: asset,
tx_hash: txHash
})
});
let data = {};
try {
data = await res.json();
} catch (err) {
data = { ok: false, error: "invalid_json_response" };
}
if (!res.ok || !data.ok) {
throw new Error(data.error || `submit_failed_http_${res.status}`);
}
return data;
}
async function tryRecoverPendingTxFromStorage() {
const invoiceId = "{{ invoice.id }}";
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
if (!invoiceId || !paymentId || !asset) return;
{% if pending_crypto_payment and pending_crypto_payment.txid %}
return;
{% endif %}
const key = pendingTxStorageKey(invoiceId, paymentId);
const savedTx = localStorage.getItem(key);
if (!savedTx || !savedTx.startsWith("0x")) return;
const walletStatus = document.getElementById("walletStatusText");
try {
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
await submitTxHash(invoiceId, paymentId, asset, savedTx);
localStorage.removeItem(key);
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
}
}
if (walletButton) {
walletButton.addEventListener("click", async function() {
const walletStatus = document.getElementById("walletStatusText");
const invoiceId = this.dataset.invoiceId;
const paymentId = this.dataset.paymentId;
const asset = this.dataset.asset;
const chainId = this.dataset.chainId;
const assetType = this.dataset.assetType;
const to = this.dataset.to;
const amount = this.dataset.amount;
const decimals = Number(this.dataset.decimals || "18");
const tokenContract = this.dataset.tokenContract || "";
let chainAddParams = null;
try {
chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null;
} catch (err) {
chainAddParams = null;
}
const setStatus = (msg) => {
if (walletStatus) walletStatus.textContent = msg;
};
if (!window.ethereum || !window.ethereum.request) {
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
return;
}
try {
this.disabled = true;
setStatus("Opening wallet...");
await window.ethereum.request({ method: "eth_requestAccounts" });
if (chainId && chainId !== "None" && chainId !== "") {
try {
await switchChain(Number(chainId), chainAddParams);
} catch (err) {
setStatus(`Chain switch failed: ${err.message || err}`);
this.disabled = false;
return;
}
}
let txParams;
if (assetType === "token" && tokenContract) {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: tokenContract,
data: erc20TransferData(to, amount, decimals),
value: "0x0"
};
} else {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: to,
value: toHexBigIntFromDecimal(amount, decimals)
};
}
setStatus("Waiting for wallet confirmation...");
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [txParams]
});
if (!txHash || !String(txHash).startsWith("0x")) {
throw new Error("wallet did not return a tx hash");
}
const storageKey = pendingTxStorageKey(invoiceId, paymentId);
localStorage.setItem(storageKey, String(txHash));
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
await submitTxHash(invoiceId, paymentId, asset, txHash);
localStorage.removeItem(storageKey);
setStatus("Transaction submitted. Reloading into processing view...");
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
setStatus(`Wallet submit failed: ${err.message || err}`);
this.disabled = false;
}
});
}
tryRecoverPendingTxFromStorage();
})();
</script>
<script>
(function() {
const processingAutoRefreshEnabled = {{ 'true' if pending_crypto_payment and pending_crypto_payment.txid and (invoice.status or '')|lower != 'paid' else 'false' }};
if (processingAutoRefreshEnabled) {
setTimeout(function() {
window.location.reload();
}, 10000);
}
})();
</script>
{% include "footer.html" %}
</body>
</html>

102
backups/portal-links-20260322-033024/portal_login.html

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

82
backups/portal-links-20260322-033024/portal_set_password.html

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div style="background:#111827;padding:10px 20px;">
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

162
backups/portal-nav-20260322-030029/portal_dashboard.html

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function() {
setTimeout(function() {
window.location.reload();
}, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

82
backups/portal-nav-20260322-030029/portal_forgot_password.html

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

727
backups/portal-nav-20260322-030029/portal_invoice_detail.html

@ -0,0 +1,727 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; }
.portal-actions a { margin-left: 0.75rem; text-decoration: underline; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; }
.detail-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-bottom: 1rem; }
.detail-card h3 { margin-top: 0; margin-bottom: 0.4rem; }
table.portal-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; }
table.portal-table th { background: #e9eef7; color: #10203f; }
.status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; }
.status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; }
.status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.pay-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-top: 1.25rem; }
.pay-selector-row { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; margin-top:0.75rem; }
.pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; }
.pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); }
.pay-panel.hidden { display: none; }
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square { background:#16a34a; }
.pay-btn-wallet { background:#2563eb; }
.pay-btn-mobile { background:#7c3aed; }
.pay-btn-copy { background:#374151; }
.error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); }
.snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }
.snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; }
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; }
.snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; }
.snapshot-timer-expired { color: #f87171; }
.quote-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.quote-table th, .quote-table td { padding: 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; vertical-align: top; }
.quote-table th { background: #e9eef7; color: #10203f; }
.quote-badge { display: inline-block; padding: 0.14rem 0.48rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; margin-left: 0.4rem; }
.quote-live { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.quote-stale { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.quote-pick-btn { padding: 8px 12px; border-radius: 8px; border: none; background: #2563eb; color: #fff; font-weight: 700; cursor: pointer; }
.quote-pick-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.lock-box { margin-top: 1rem; border: 1px solid rgba(34, 197, 94, 0.28); background: rgba(22, 101, 52, 0.16); border-radius: 12px; padding: 1rem; }
.lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); }
.lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; }
.lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; }
.wallet-actions {
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help {
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4 {
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p {
margin: 0.35rem 0;
}
.wallet-note { opacity:0.9; margin-top:0.65rem; }
.mono { font-family: monospace; }
.copy-row {
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target {
flex: 1 1 420px;
min-width: 220px;
}
.copy-status {
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
@media (max-width: 820px) {
.snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; }
.snapshot-timer-box { width: 100%; margin-top: 1rem; min-height: 110px; }
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
{% if (invoice.status or "")|lower == "paid" %}
<div class="success-box">✓ This invoice has been paid. Thank you!</div>
{% endif %}
{% if crypto_error %}
<div class="error-box">{{ crypto_error }}</div>
{% endif %}
<div class="detail-grid">
<div class="detail-card"><h3>Invoice</h3><div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div></div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %}
<span class="status-badge status-pending">processing</span>
{% elif s == "paid" %}<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}<span class="status-badge status-other">{{ invoice.status }}</span>{% endif %}
</div>
<div class="detail-card"><h3>Created</h3><div>{{ invoice.created_at }}</div></div>
<div class="detail-card"><h3>Total</h3><div>{{ invoice.total_amount }}</div></div>
<div class="detail-card"><h3>Paid</h3><div>{{ invoice.amount_paid }}</div></div>
<div class="detail-card"><h3>Outstanding</h3><div>{{ invoice.outstanding }}</div></div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Line Total</th></tr></thead>
<tbody>
{% for item in items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.unit_price }}</td><td>{{ item.line_total }}</td></tr>
{% else %}
<tr><td colspan="4">No invoice line items found.</td></tr>
{% endfor %}
</tbody>
</table>
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
<div class="pay-card">
<h3>Pay Now</h3>
<div class="pay-selector-row">
<label for="payMethodSelect"><strong>Choose payment method:</strong></label>
<select id="payMethodSelect" class="pay-selector">
<option value="" {% if not pay_mode %}selected{% endif %}>Select…</option>
<option value="etransfer" {% if pay_mode == "etransfer" %}selected{% endif %}>e-Transfer</option>
<option value="square" {% if pay_mode == "square" %}selected{% endif %}>Credit Card</option>
<option value="crypto" {% if pay_mode == "crypto" %}selected{% endif %}>Crypto</option>
</select>
</div>
<div id="panel-etransfer" class="pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}">
<p><strong>Interac e-Transfer</strong><br>Send payment to:<br>payment@outsidethebox.top<br>Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</p>
</div>
<div id="panel-square" class="pay-panel{% if pay_mode != 'square' %} hidden{% endif %}">
<p><strong>Credit Card (Square)</strong></p>
<a href="/portal/invoice/{{ invoice.id }}/pay-square" target="_blank" rel="noopener noreferrer" class="pay-btn pay-btn-square">Pay with Credit Card</a>
</div>
<div id="panel-crypto" class="pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}">
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
<div class="snapshot-wrap">
<div class="snapshot-header">
<div class="snapshot-meta">
<h3 style="margin-top:0;">Crypto Quote Snapshot</h3>
<div><strong>Quoted At:</strong> {{ invoice.oracle_quote.quoted_at or "—" }}</div>
<div><strong>Source Status:</strong> {{ invoice.oracle_quote.source_status or "—" }}</div>
<div><strong>Frozen Amount:</strong> {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}</div>
{% if pending_crypto_payment %}
<div style="margin-top:0.75rem;"><strong>Your quote is protected after acceptance.</strong></div>
{% else %}
<div style="margin-top:0.75rem;"><strong>Select a crypto asset to accept the quote.</strong></div>
{% endif %}
</div>
{% if pending_crypto_payment and pending_crypto_payment.txid %}
<div class="snapshot-timer-box">
<div id="processingTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.processing_expires_at_iso }}">--:--</div>
<div id="processingTimerLabel" class="snapshot-timer-label">Watching transaction / waiting for confirmation</div>
</div>
{% elif pending_crypto_payment %}
<div class="snapshot-timer-box">
<div id="lockTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.lock_expires_at_iso }}">--:--</div>
<div id="lockTimerLabel" class="snapshot-timer-label">Quote protected while you open wallet</div>
</div>
{% else %}
<div class="snapshot-timer-box">
<div id="quoteTimerValue" class="snapshot-timer-value" data-expiry="{{ crypto_quote_window_expires_iso }}">--:--</div>
<div id="quoteTimerLabel" class="snapshot-timer-label">This price times out:</div>
</div>
{% endif %}
</div>
{% if pending_crypto_payment and selected_crypto_option %}
<div id="lockBox" class="lock-box{% if pending_crypto_payment.lock_expired or pending_crypto_payment.processing_expired %} expired{% endif %}">
<div class="lock-grid">
<div>
<h3 style="margin-top:0;">{{ selected_crypto_option.label }} Payment Instructions</h3>
<div><strong>Send exactly:</strong> {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}</div>
<div style="margin-top:0.65rem;"><strong>Destination wallet:</strong></div>
<code id="walletAddressText" class="lock-code copy-target">{{ pending_crypto_payment.wallet_address }}</code>
<div style="margin-top:0.65rem;"><strong>Reference / Invoice:</strong></div>
<code id="invoiceRefText" class="lock-code copy-target">{{ pending_crypto_payment.reference }}</code>
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
<div class="wallet-actions">
<button
type="button"
id="walletPayButton"
class="pay-btn pay-btn-wallet"
data-invoice-id="{{ invoice.id }}"
data-payment-id="{{ pending_crypto_payment.id }}"
data-asset="{{ selected_crypto_option.symbol }}"
data-chain-id="{{ selected_crypto_option.chain_id }}"
data-asset-type="{{ selected_crypto_option.asset_type }}"
data-to="{{ selected_crypto_option.wallet_address }}"
data-amount="{{ pending_crypto_payment.payment_amount }}"
data-decimals="{{ selected_crypto_option.decimals }}"
data-token-contract="{{ selected_crypto_option.token_contract or '' }}"
data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}'
>
Open MetaMask / Rabby
</button>
<a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
</a>
<button type="button" id="copyDetailsButton" class="pay-btn pay-btn-copy">
Copy Payment Details
</button>
</div>
<div class="wallet-help">
<h4>Fastest way to pay</h4>
<p>1. Click <strong>Open MetaMask / Rabby</strong> if your wallet is installed in this browser.</p>
<p>2. If that does not open your wallet, click <strong>Open in MetaMask Mobile</strong>.</p>
<p>3. If needed, use <strong>Copy Payment Details</strong> and send manually.</p>
</div>
<div class="wallet-note">
You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
</div>
<div class="copy-row">
<span id="walletStatusText"></span>
<span id="copyStatusText" class="copy-status"></span>
</div>
{% elif pending_crypto_payment.txid %}
<div style="margin-top:0.9rem;"><strong>Transaction Hash:</strong></div>
<code class="lock-code mono">{{ pending_crypto_payment.txid }}</code>
<div style="margin-top:0.75rem;">Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.</div>
{% elif pending_crypto_payment.lock_expired %}
<div style="margin-top:0.75rem;">price has expired - please refresh your quote to update</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<form id="cryptoPickForm" method="post" action="/portal/invoice/{{ invoice.id }}/pay-crypto">
<table class="quote-table">
<thead><tr><th>Asset</th><th>Quoted Amount</th><th>CAD Price</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
{% for q in crypto_options %}
<tr>
<td>
{{ q.label }}
{% if q.recommended %}<span class="quote-badge quote-live">recommended</span>{% endif %}
{% if q.wallet_capable %}<span class="quote-badge quote-live">wallet</span>{% endif %}
</td>
<td>{{ q.display_amount or "—" }}</td>
<td>{% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}</td>
<td>{% if q.available %}<span class="quote-badge quote-live">live</span>{% else %}<span class="quote-badge quote-stale">{{ q.reason or "unavailable" }}</span>{% endif %}</td>
<td><button type="submit" name="asset" value="{{ q.symbol }}" class="quote-pick-btn" {% if not q.available %}disabled{% endif %}>Accept {{ q.symbol }}</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
</div>
{% else %}
<p>No crypto quote snapshot is available for this invoice yet.</p>
{% endif %}
</div>
</div>
{% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %}
</div>
<script>
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage, disableSelector) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff <= 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
if (disableSelector) {
document.querySelectorAll(disableSelector).forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) lockBox.classList.add("expired");
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer && quoteTimer.dataset.expiry) {
bindCountdown("quoteTimerValue", "quoteTimerLabel", quoteTimer.dataset.expiry, "price has expired - please refresh your view to update", "#cryptoPickForm button");
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer && lockTimer.dataset.expiry) {
bindCountdown("lockTimerValue", "lockTimerLabel", lockTimer.dataset.expiry, "price has expired - please refresh your quote to update", "#walletPayButton");
}
const processingTimer = document.getElementById("processingTimerValue");
if (processingTimer && processingTimer.dataset.expiry) {
bindCountdown("processingTimerValue", "processingTimerLabel", processingTimer.dataset.expiry, "price has expired - please refresh your quote to update", null);
}
function toHexBigIntFromDecimal(amountText, decimals) {
const text = String(amountText || "0");
const parts = text.split(".");
const whole = parts[0] || "0";
const frac = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
const combined = (whole + frac).replace(/^0+/, "") || "0";
return "0x" + BigInt(combined).toString(16);
}
function erc20TransferData(to, amountText, decimals) {
const method = "a9059cbb";
const addr = String(to || "").toLowerCase().replace(/^0x/, "").padStart(64, "0");
const amtHex = BigInt(toHexBigIntFromDecimal(amountText, decimals)).toString(16).padStart(64, "0");
return "0x" + method + addr + amtHex;
}
async function switchChain(chainId, chainAddParams) {
const hexChainId = "0x" + Number(chainId).toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
} catch (err) {
const code = err && (err.code ?? err?.data?.originalError?.code);
if ((code === 4902 || String(err).includes("4902")) && chainAddParams) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainAddParams]
});
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
}
throw err;
}
}
function buildMetaMaskMobileLink() {
const currentUrl = window.location.href;
return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, "");
}
const mmLink = document.getElementById("metamaskMobileLink");
if (mmLink) {
mmLink.href = buildMetaMaskMobileLink();
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
const copyBtn = document.getElementById("copyDetailsButton");
if (copyBtn) {
copyBtn.addEventListener("click", async function() {
const copyStatus = document.getElementById("copyStatusText");
const walletAddress = document.getElementById("walletAddressText")?.textContent || "";
const invoiceRef = document.getElementById("invoiceRefText")?.textContent || "";
const amount = document.getElementById("walletPayButton")?.dataset.amount || "";
const asset = document.getElementById("walletPayButton")?.dataset.asset || "";
const payload =
`Asset: ${asset}
Amount: ${amount} ${asset}
Wallet: ${walletAddress}
Reference: ${invoiceRef}`;
try {
await copyText(payload);
if (copyStatus) copyStatus.textContent = "Payment details copied.";
} catch (err) {
if (copyStatus) copyStatus.textContent = "Copy failed.";
}
});
}
const walletButton = document.getElementById("walletPayButton");
function pendingTxStorageKey(invoiceId, paymentId) {
return `otb_pending_tx_${invoiceId}_${paymentId}`;
}
async function submitTxHash(invoiceId, paymentId, asset, txHash) {
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
payment_id: paymentId,
asset: asset,
tx_hash: txHash
})
});
let data = {};
try {
data = await res.json();
} catch (err) {
data = { ok: false, error: "invalid_json_response" };
}
if (!res.ok || !data.ok) {
throw new Error(data.error || `submit_failed_http_${res.status}`);
}
return data;
}
async function tryRecoverPendingTxFromStorage() {
const invoiceId = "{{ invoice.id }}";
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
if (!invoiceId || !paymentId || !asset) return;
{% if pending_crypto_payment and pending_crypto_payment.txid %}
return;
{% endif %}
const key = pendingTxStorageKey(invoiceId, paymentId);
const savedTx = localStorage.getItem(key);
if (!savedTx || !savedTx.startsWith("0x")) return;
const walletStatus = document.getElementById("walletStatusText");
try {
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
await submitTxHash(invoiceId, paymentId, asset, savedTx);
localStorage.removeItem(key);
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
}
}
if (walletButton) {
walletButton.addEventListener("click", async function() {
const walletStatus = document.getElementById("walletStatusText");
const invoiceId = this.dataset.invoiceId;
const paymentId = this.dataset.paymentId;
const asset = this.dataset.asset;
const chainId = this.dataset.chainId;
const assetType = this.dataset.assetType;
const to = this.dataset.to;
const amount = this.dataset.amount;
const decimals = Number(this.dataset.decimals || "18");
const tokenContract = this.dataset.tokenContract || "";
let chainAddParams = null;
try {
chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null;
} catch (err) {
chainAddParams = null;
}
const setStatus = (msg) => {
if (walletStatus) walletStatus.textContent = msg;
};
if (!window.ethereum || !window.ethereum.request) {
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
return;
}
try {
this.disabled = true;
setStatus("Opening wallet...");
await window.ethereum.request({ method: "eth_requestAccounts" });
if (chainId && chainId !== "None" && chainId !== "") {
try {
await switchChain(Number(chainId), chainAddParams);
} catch (err) {
setStatus(`Chain switch failed: ${err.message || err}`);
this.disabled = false;
return;
}
}
let txParams;
if (assetType === "token" && tokenContract) {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: tokenContract,
data: erc20TransferData(to, amount, decimals),
value: "0x0"
};
} else {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: to,
value: toHexBigIntFromDecimal(amount, decimals)
};
}
setStatus("Waiting for wallet confirmation...");
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [txParams]
});
if (!txHash || !String(txHash).startsWith("0x")) {
throw new Error("wallet did not return a tx hash");
}
const storageKey = pendingTxStorageKey(invoiceId, paymentId);
localStorage.setItem(storageKey, String(txHash));
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
await submitTxHash(invoiceId, paymentId, asset, txHash);
localStorage.removeItem(storageKey);
setStatus("Transaction submitted. Reloading into processing view...");
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
setStatus(`Wallet submit failed: ${err.message || err}`);
this.disabled = false;
}
});
}
tryRecoverPendingTxFromStorage();
})();
</script>
<script>
(function() {
const processingAutoRefreshEnabled = {{ 'true' if pending_crypto_payment and pending_crypto_payment.txid and (invoice.status or '')|lower != 'paid' else 'false' }};
if (processingAutoRefreshEnabled) {
setTimeout(function() {
window.location.reload();
}, 10000);
}
})();
</script>
{% include "footer.html" %}
</body>
</html>

96
backups/portal-nav-20260322-030029/portal_login.html

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

76
backups/portal-nav-20260322-030029/portal_set_password.html

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

168
backups/portal-navbar-20260322-031219/portal_dashboard.html

@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top {
display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap;
margin-bottom: 1rem;
}
.portal-actions a {
margin-left: 0.75rem;
text-decoration: underline;
}
.summary-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap:1rem;
margin: 1rem 0 1.25rem 0;
}
.summary-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
}
.summary-card h3 { margin-top:0; margin-bottom:0.4rem; }
table.portal-table {
width: 100%;
border-collapse: collapse;
}
table.portal-table th, table.portal-table td {
padding: 0.8rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
}
table.portal-table th {
background: #e9eef7;
color: #10203f;
}
.invoice-link {
color: inherit;
text-decoration: underline;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
}
.status-paid {
background: rgba(34, 197, 94, 0.18);
color: #4ade80;
}
.status-pending {
background: rgba(245, 158, 11, 0.20);
color: #fbbf24;
}
.status-overdue {
background: rgba(239, 68, 68, 0.18);
color: #f87171;
}
.status-other {
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Client Dashboard</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div>{{ invoice_count }}</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div>{{ total_outstanding }}</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div>{{ total_paid }}</div>
</div>
</div>
<h2>Invoices</h2>
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
(function() {
setTimeout(function() {
window.location.reload();
}, 20000);
})();
</script>
{% include "footer.html" %}
</body>
</html>

88
backups/portal-navbar-20260322-031219/portal_forgot_password.html

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forgot Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Reset Portal Password</h1>
<p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p>
{% if error %}
<div class="portal-msg">{{ error }}</div>
{% endif %}
{% if message %}
<div class="portal-msg">{{ message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/forgot-password">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" value="{{ form_email or '' }}" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Send Reset Code</button>
<a class="portal-btn" href="/portal">Back to Portal Login</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

733
backups/portal-navbar-20260322-031219/portal_invoice_detail.html

@ -0,0 +1,733 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; }
.portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; }
.portal-actions a { margin-left: 0.75rem; text-decoration: underline; }
.detail-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; }
.detail-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-bottom: 1rem; }
.detail-card h3 { margin-top: 0; margin-bottom: 0.4rem; }
table.portal-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; }
table.portal-table th { background: #e9eef7; color: #10203f; }
.status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; }
.status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; }
.status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.pay-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); margin-top: 1.25rem; }
.pay-selector-row { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; margin-top:0.75rem; }
.pay-selector { padding: 10px 12px; min-width: 220px; border-radius: 8px; }
.pay-panel { margin-top: 1rem; padding: 1rem; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; background: rgba(255,255,255,0.02); }
.pay-panel.hidden { display: none; }
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square { background:#16a34a; }
.pay-btn-wallet { background:#2563eb; }
.pay-btn-mobile { background:#7c3aed; }
.pay-btn-copy { background:#374151; }
.error-box { border: 1px solid rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); color: #fecaca; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.success-box { border: 1px solid rgba(34, 197, 94, 0.55); background: rgba(22, 101, 52, 0.18); color: #dcfce7; border-radius: 10px; padding: 12px 14px; margin-bottom: 1rem; }
.snapshot-wrap { position: relative; margin-top: 1rem; border: 1px solid rgba(255,255,255,0.14); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.02); }
.snapshot-header { display:flex; justify-content:space-between; gap:1rem; align-items:flex-start; }
.snapshot-meta { flex: 1 1 auto; min-width: 0; line-height: 1.65; }
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value { font-size: 2rem; font-weight: 800; line-height: 1.1; }
.snapshot-timer-label { margin-top: 0.55rem; font-size: 0.95rem; opacity: 0.95; }
.snapshot-timer-expired { color: #f87171; }
.quote-table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.quote-table th, .quote-table td { padding: 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; vertical-align: top; }
.quote-table th { background: #e9eef7; color: #10203f; }
.quote-badge { display: inline-block; padding: 0.14rem 0.48rem; border-radius: 999px; font-size: 0.78rem; font-weight: 700; margin-left: 0.4rem; }
.quote-live { background: rgba(34, 197, 94, 0.18); color: #4ade80; }
.quote-stale { background: rgba(239, 68, 68, 0.18); color: #f87171; }
.quote-pick-btn { padding: 8px 12px; border-radius: 8px; border: none; background: #2563eb; color: #fff; font-weight: 700; cursor: pointer; }
.quote-pick-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.lock-box { margin-top: 1rem; border: 1px solid rgba(34, 197, 94, 0.28); background: rgba(22, 101, 52, 0.16); border-radius: 12px; padding: 1rem; }
.lock-box.expired { border-color: rgba(239, 68, 68, 0.55); background: rgba(127, 29, 29, 0.22); }
.lock-grid { display:grid; grid-template-columns: 1fr 220px; gap:1rem; align-items:start; }
.lock-code { display:block; margin-top:0.35rem; padding:0.65rem 0.8rem; background: rgba(0,0,0,0.22); border-radius: 8px; overflow-wrap:anywhere; }
.wallet-actions {
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help {
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4 {
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p {
margin: 0.35rem 0;
}
.wallet-note { opacity:0.9; margin-top:0.65rem; }
.mono { font-family: monospace; }
.copy-row {
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target {
flex: 1 1 420px;
min-width: 220px;
}
.copy-status {
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
@media (max-width: 820px) {
.snapshot-header, .lock-grid { grid-template-columns: 1fr; display:block; }
.snapshot-timer-box { width: 100%; margin-top: 1rem; min-height: 110px; }
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-top">
<div>
<h1>Invoice Detail</h1>
<p>{{ client.company_name or client.contact_name or client.email }}</p>
</div>
<div class="portal-actions">
<a href="/portal/dashboard">Back to Dashboard</a>
<a href="https://outsidethebox.top/">Home</a>
<a href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
<a href="/portal/logout">Logout</a>
</div>
</div>
{% if (invoice.status or "")|lower == "paid" %}
<div class="success-box">✓ This invoice has been paid. Thank you!</div>
{% endif %}
{% if crypto_error %}
<div class="error-box">{{ crypto_error }}</div>
{% endif %}
<div class="detail-grid">
<div class="detail-card"><h3>Invoice</h3><div>{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</div></div>
<div class="detail-card">
<h3>Status</h3>
{% set s = (invoice.status or "")|lower %}
{% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %}
<span class="status-badge status-pending">processing</span>
{% elif s == "paid" %}<span class="status-badge status-paid">{{ invoice.status }}</span>
{% elif s == "pending" %}<span class="status-badge status-pending">{{ invoice.status }}</span>
{% elif s == "overdue" %}<span class="status-badge status-overdue">{{ invoice.status }}</span>
{% else %}<span class="status-badge status-other">{{ invoice.status }}</span>{% endif %}
</div>
<div class="detail-card"><h3>Created</h3><div>{{ invoice.created_at }}</div></div>
<div class="detail-card"><h3>Total</h3><div>{{ invoice.total_amount }}</div></div>
<div class="detail-card"><h3>Paid</h3><div>{{ invoice.amount_paid }}</div></div>
<div class="detail-card"><h3>Outstanding</h3><div>{{ invoice.outstanding }}</div></div>
</div>
<h2>Invoice Items</h2>
<table class="portal-table">
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Line Total</th></tr></thead>
<tbody>
{% for item in items %}
<tr><td>{{ item.description }}</td><td>{{ item.quantity }}</td><td>{{ item.unit_price }}</td><td>{{ item.line_total }}</td></tr>
{% else %}
<tr><td colspan="4">No invoice line items found.</td></tr>
{% endfor %}
</tbody>
</table>
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
<div class="pay-card">
<h3>Pay Now</h3>
<div class="pay-selector-row">
<label for="payMethodSelect"><strong>Choose payment method:</strong></label>
<select id="payMethodSelect" class="pay-selector">
<option value="" {% if not pay_mode %}selected{% endif %}>Select…</option>
<option value="etransfer" {% if pay_mode == "etransfer" %}selected{% endif %}>e-Transfer</option>
<option value="square" {% if pay_mode == "square" %}selected{% endif %}>Credit Card</option>
<option value="crypto" {% if pay_mode == "crypto" %}selected{% endif %}>Crypto</option>
</select>
</div>
<div id="panel-etransfer" class="pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}">
<p><strong>Interac e-Transfer</strong><br>Send payment to:<br>payment@outsidethebox.top<br>Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}</p>
</div>
<div id="panel-square" class="pay-panel{% if pay_mode != 'square' %} hidden{% endif %}">
<p><strong>Credit Card (Square)</strong></p>
<a href="/portal/invoice/{{ invoice.id }}/pay-square" target="_blank" rel="noopener noreferrer" class="pay-btn pay-btn-square">Pay with Credit Card</a>
</div>
<div id="panel-crypto" class="pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}">
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
<div class="snapshot-wrap">
<div class="snapshot-header">
<div class="snapshot-meta">
<h3 style="margin-top:0;">Crypto Quote Snapshot</h3>
<div><strong>Quoted At:</strong> {{ invoice.oracle_quote.quoted_at or "—" }}</div>
<div><strong>Source Status:</strong> {{ invoice.oracle_quote.source_status or "—" }}</div>
<div><strong>Frozen Amount:</strong> {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}</div>
{% if pending_crypto_payment %}
<div style="margin-top:0.75rem;"><strong>Your quote is protected after acceptance.</strong></div>
{% else %}
<div style="margin-top:0.75rem;"><strong>Select a crypto asset to accept the quote.</strong></div>
{% endif %}
</div>
{% if pending_crypto_payment and pending_crypto_payment.txid %}
<div class="snapshot-timer-box">
<div id="processingTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.processing_expires_at_iso }}">--:--</div>
<div id="processingTimerLabel" class="snapshot-timer-label">Watching transaction / waiting for confirmation</div>
</div>
{% elif pending_crypto_payment %}
<div class="snapshot-timer-box">
<div id="lockTimerValue" class="snapshot-timer-value" data-expiry="{{ pending_crypto_payment.lock_expires_at_iso }}">--:--</div>
<div id="lockTimerLabel" class="snapshot-timer-label">Quote protected while you open wallet</div>
</div>
{% else %}
<div class="snapshot-timer-box">
<div id="quoteTimerValue" class="snapshot-timer-value" data-expiry="{{ crypto_quote_window_expires_iso }}">--:--</div>
<div id="quoteTimerLabel" class="snapshot-timer-label">This price times out:</div>
</div>
{% endif %}
</div>
{% if pending_crypto_payment and selected_crypto_option %}
<div id="lockBox" class="lock-box{% if pending_crypto_payment.lock_expired or pending_crypto_payment.processing_expired %} expired{% endif %}">
<div class="lock-grid">
<div>
<h3 style="margin-top:0;">{{ selected_crypto_option.label }} Payment Instructions</h3>
<div><strong>Send exactly:</strong> {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}</div>
<div style="margin-top:0.65rem;"><strong>Destination wallet:</strong></div>
<code id="walletAddressText" class="lock-code copy-target">{{ pending_crypto_payment.wallet_address }}</code>
<div style="margin-top:0.65rem;"><strong>Reference / Invoice:</strong></div>
<code id="invoiceRefText" class="lock-code copy-target">{{ pending_crypto_payment.reference }}</code>
{% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
<div class="wallet-actions">
<button
type="button"
id="walletPayButton"
class="pay-btn pay-btn-wallet"
data-invoice-id="{{ invoice.id }}"
data-payment-id="{{ pending_crypto_payment.id }}"
data-asset="{{ selected_crypto_option.symbol }}"
data-chain-id="{{ selected_crypto_option.chain_id }}"
data-asset-type="{{ selected_crypto_option.asset_type }}"
data-to="{{ selected_crypto_option.wallet_address }}"
data-amount="{{ pending_crypto_payment.payment_amount }}"
data-decimals="{{ selected_crypto_option.decimals }}"
data-token-contract="{{ selected_crypto_option.token_contract or '' }}"
data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}'
>
Open MetaMask / Rabby
</button>
<a
id="metamaskMobileLink"
href="#"
target="_blank"
rel="noopener noreferrer"
class="pay-btn pay-btn-mobile"
data-invoice-id="{{ invoice.id }}"
>
Open in MetaMask Mobile
</a>
<button type="button" id="copyDetailsButton" class="pay-btn pay-btn-copy">
Copy Payment Details
</button>
</div>
<div class="wallet-help">
<h4>Fastest way to pay</h4>
<p>1. Click <strong>Open MetaMask / Rabby</strong> if your wallet is installed in this browser.</p>
<p>2. If that does not open your wallet, click <strong>Open in MetaMask Mobile</strong>.</p>
<p>3. If needed, use <strong>Copy Payment Details</strong> and send manually.</p>
</div>
<div class="wallet-note">
You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
</div>
<div class="copy-row">
<span id="walletStatusText"></span>
<span id="copyStatusText" class="copy-status"></span>
</div>
{% elif pending_crypto_payment.txid %}
<div style="margin-top:0.9rem;"><strong>Transaction Hash:</strong></div>
<code class="lock-code mono">{{ pending_crypto_payment.txid }}</code>
<div style="margin-top:0.75rem;">Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.</div>
{% elif pending_crypto_payment.lock_expired %}
<div style="margin-top:0.75rem;">price has expired - please refresh your quote to update</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<form id="cryptoPickForm" method="post" action="/portal/invoice/{{ invoice.id }}/pay-crypto">
<table class="quote-table">
<thead><tr><th>Asset</th><th>Quoted Amount</th><th>CAD Price</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
{% for q in crypto_options %}
<tr>
<td>
{{ q.label }}
{% if q.recommended %}<span class="quote-badge quote-live">recommended</span>{% endif %}
{% if q.wallet_capable %}<span class="quote-badge quote-live">wallet</span>{% endif %}
</td>
<td>{{ q.display_amount or "—" }}</td>
<td>{% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}</td>
<td>{% if q.available %}<span class="quote-badge quote-live">live</span>{% else %}<span class="quote-badge quote-stale">{{ q.reason or "unavailable" }}</span>{% endif %}</td>
<td><button type="submit" name="asset" value="{{ q.symbol }}" class="quote-pick-btn" {% if not q.available %}disabled{% endif %}>Accept {{ q.symbol }}</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endif %}
</div>
{% else %}
<p>No crypto quote snapshot is available for this invoice yet.</p>
{% endif %}
</div>
</div>
{% endif %}
{% if invoice_payments %}
<div class="detail-card" style="margin-top:1.25rem;">
<h3>Payments Applied</h3>
<table class="portal-table">
<thead>
<tr>
<th>Method</th>
<th>Amount</th>
<th>Status</th>
<th>Received</th>
<th>Reference / TXID</th>
</tr>
</thead>
<tbody>
{% for p in invoice_payments %}
<tr>
<td>{{ p.payment_method_label }}</td>
<td>{{ p.payment_amount_display }} {{ p.payment_currency }}</td>
<td>{{ p.payment_status }}</td>
<td>{{ p.received_at_local }}</td>
<td>
{% if p.txid %}
{{ p.txid }}
{% elif p.reference %}
{{ p.reference }}
{% else %}
-
{% endif %}
{% if p.wallet_address %}<br><small>{{ p.wallet_address }}</small>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if pdf_url %}
<div style="margin-top:1rem;"><a href="/portal/invoice/{{ invoice.id }}/pdf" target="_blank" rel="noopener noreferrer">Open Invoice PDF</a></div>
{% endif %}
</div>
<script>
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage, disableSelector) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff <= 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
if (disableSelector) {
document.querySelectorAll(disableSelector).forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) lockBox.classList.add("expired");
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer && quoteTimer.dataset.expiry) {
bindCountdown("quoteTimerValue", "quoteTimerLabel", quoteTimer.dataset.expiry, "price has expired - please refresh your view to update", "#cryptoPickForm button");
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer && lockTimer.dataset.expiry) {
bindCountdown("lockTimerValue", "lockTimerLabel", lockTimer.dataset.expiry, "price has expired - please refresh your quote to update", "#walletPayButton");
}
const processingTimer = document.getElementById("processingTimerValue");
if (processingTimer && processingTimer.dataset.expiry) {
bindCountdown("processingTimerValue", "processingTimerLabel", processingTimer.dataset.expiry, "price has expired - please refresh your quote to update", null);
}
function toHexBigIntFromDecimal(amountText, decimals) {
const text = String(amountText || "0");
const parts = text.split(".");
const whole = parts[0] || "0";
const frac = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
const combined = (whole + frac).replace(/^0+/, "") || "0";
return "0x" + BigInt(combined).toString(16);
}
function erc20TransferData(to, amountText, decimals) {
const method = "a9059cbb";
const addr = String(to || "").toLowerCase().replace(/^0x/, "").padStart(64, "0");
const amtHex = BigInt(toHexBigIntFromDecimal(amountText, decimals)).toString(16).padStart(64, "0");
return "0x" + method + addr + amtHex;
}
async function switchChain(chainId, chainAddParams) {
const hexChainId = "0x" + Number(chainId).toString(16);
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
} catch (err) {
const code = err && (err.code ?? err?.data?.originalError?.code);
if ((code === 4902 || String(err).includes("4902")) && chainAddParams) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [chainAddParams]
});
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }]
});
return;
}
throw err;
}
}
function buildMetaMaskMobileLink() {
const currentUrl = window.location.href;
return "https://link.metamask.io/dapp/" + currentUrl.replace(/^https?:\/\//, "");
}
const mmLink = document.getElementById("metamaskMobileLink");
if (mmLink) {
mmLink.href = buildMetaMaskMobileLink();
}
async function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
const copyBtn = document.getElementById("copyDetailsButton");
if (copyBtn) {
copyBtn.addEventListener("click", async function() {
const copyStatus = document.getElementById("copyStatusText");
const walletAddress = document.getElementById("walletAddressText")?.textContent || "";
const invoiceRef = document.getElementById("invoiceRefText")?.textContent || "";
const amount = document.getElementById("walletPayButton")?.dataset.amount || "";
const asset = document.getElementById("walletPayButton")?.dataset.asset || "";
const payload =
`Asset: ${asset}
Amount: ${amount} ${asset}
Wallet: ${walletAddress}
Reference: ${invoiceRef}`;
try {
await copyText(payload);
if (copyStatus) copyStatus.textContent = "Payment details copied.";
} catch (err) {
if (copyStatus) copyStatus.textContent = "Copy failed.";
}
});
}
const walletButton = document.getElementById("walletPayButton");
function pendingTxStorageKey(invoiceId, paymentId) {
return `otb_pending_tx_${invoiceId}_${paymentId}`;
}
async function submitTxHash(invoiceId, paymentId, asset, txHash) {
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
payment_id: paymentId,
asset: asset,
tx_hash: txHash
})
});
let data = {};
try {
data = await res.json();
} catch (err) {
data = { ok: false, error: "invalid_json_response" };
}
if (!res.ok || !data.ok) {
throw new Error(data.error || `submit_failed_http_${res.status}`);
}
return data;
}
async function tryRecoverPendingTxFromStorage() {
const invoiceId = "{{ invoice.id }}";
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}";
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}";
if (!invoiceId || !paymentId || !asset) return;
{% if pending_crypto_payment and pending_crypto_payment.txid %}
return;
{% endif %}
const key = pendingTxStorageKey(invoiceId, paymentId);
const savedTx = localStorage.getItem(key);
if (!savedTx || !savedTx.startsWith("0x")) return;
const walletStatus = document.getElementById("walletStatusText");
try {
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission...";
await submitTxHash(invoiceId, paymentId, asset, savedTx);
localStorage.removeItem(key);
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`;
}
}
if (walletButton) {
walletButton.addEventListener("click", async function() {
const walletStatus = document.getElementById("walletStatusText");
const invoiceId = this.dataset.invoiceId;
const paymentId = this.dataset.paymentId;
const asset = this.dataset.asset;
const chainId = this.dataset.chainId;
const assetType = this.dataset.assetType;
const to = this.dataset.to;
const amount = this.dataset.amount;
const decimals = Number(this.dataset.decimals || "18");
const tokenContract = this.dataset.tokenContract || "";
let chainAddParams = null;
try {
chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null;
} catch (err) {
chainAddParams = null;
}
const setStatus = (msg) => {
if (walletStatus) walletStatus.textContent = msg;
};
if (!window.ethereum || !window.ethereum.request) {
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile.");
return;
}
try {
this.disabled = true;
setStatus("Opening wallet...");
await window.ethereum.request({ method: "eth_requestAccounts" });
if (chainId && chainId !== "None" && chainId !== "") {
try {
await switchChain(Number(chainId), chainAddParams);
} catch (err) {
setStatus(`Chain switch failed: ${err.message || err}`);
this.disabled = false;
return;
}
}
let txParams;
if (assetType === "token" && tokenContract) {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: tokenContract,
data: erc20TransferData(to, amount, decimals),
value: "0x0"
};
} else {
txParams = {
from: (await window.ethereum.request({ method: "eth_accounts" }))[0],
to: to,
value: toHexBigIntFromDecimal(amount, decimals)
};
}
setStatus("Waiting for wallet confirmation...");
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [txParams]
});
if (!txHash || !String(txHash).startsWith("0x")) {
throw new Error("wallet did not return a tx hash");
}
const storageKey = pendingTxStorageKey(invoiceId, paymentId);
localStorage.setItem(storageKey, String(txHash));
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`);
await submitTxHash(invoiceId, paymentId, asset, txHash);
localStorage.removeItem(storageKey);
setStatus("Transaction submitted. Reloading into processing view...");
const url = new URL(window.location.href);
url.searchParams.set("pay", "crypto");
url.searchParams.set("asset", asset);
url.searchParams.set("payment_id", paymentId);
window.location.href = url.toString();
} catch (err) {
setStatus(`Wallet submit failed: ${err.message || err}`);
this.disabled = false;
}
});
}
tryRecoverPendingTxFromStorage();
})();
</script>
<script>
(function() {
const processingAutoRefreshEnabled = {{ 'true' if pending_crypto_payment and pending_crypto_payment.txid and (invoice.status or '')|lower != 'paid' else 'false' }};
if (processingAutoRefreshEnabled) {
setTimeout(function() {
window.location.reload();
}, 10000);
}
})();
</script>
{% include "footer.html" %}
</body>
</html>

102
backups/portal-navbar-20260322-031219/portal_login.html

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; }
.portal-sub { opacity: 0.9; margin-bottom: 1.25rem; }
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; }
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; }
.portal-links { margin-top: 1rem; }
.portal-links a { margin-right: 1rem; }
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn" type="submit">Sign In</button>
<a class="portal-btn" href="https://outsidethebox.top/">Home</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support">Contact Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

82
backups/portal-navbar-20260322-031219/portal_set_password.html

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set Portal Password - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
.portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; }
.portal-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 1.4rem;
background: rgba(255,255,255,0.03);
box-shadow: 0 10px 24px rgba(0,0,0,0.18);
}
.portal-form { display: grid; gap: 0.9rem; }
.portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.portal-form input {
width: 100%;
padding: 0.8rem 0.9rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.05);
color: inherit;
box-sizing: border-box;
}
.portal-btn {
display: inline-block;
padding: 0.8rem 1rem;
border-radius: 10px;
text-decoration: none;
border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06);
color: inherit;
cursor: pointer;
}
.portal-msg {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.16);
background: rgba(255,255,255,0.04);
}
</style>
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
<div style="background:#111827;padding:10px 20px;">
<a href="https://outsidethebox.top" style="color:#60a5fa;text-decoration:none;font-weight:bold;">
← OutsideTheBox Home
</a>
</div>
<div class="portal-wrap">
<div class="portal-card">
<h1>Create Your Portal Password</h1>
<p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/set-password">
<div>
<label for="password">New Password</label>
<input id="password" name="password" type="password" required>
</div>
<div>
<label for="password2">Confirm Password</label>
<input id="password2" name="password2" type="password" required>
</div>
<div>
<button class="portal-btn" type="submit">Set Password</button>
</div>
</form>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

6392
backups/portal-theme-cleanup-20260322-215133/app.py

File diff suppressed because it is too large Load Diff

125
backups/portal-theme-cleanup-20260322-215133/portal_dashboard.html

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/portal-theme-cleanup-20260322-215133/portal_forgot_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

31
backups/portal-theme-cleanup-20260322-215133/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

64
backups/portal-theme-cleanup-20260322-215133/portal_login.html

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/portal-theme-cleanup-20260322-215133/portal_set_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

125
backups/portal-toggle-cleanup-20260322-213704/portal_dashboard.html

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/portal-toggle-cleanup-20260322-213704/portal_forgot_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

31
backups/portal-toggle-cleanup-20260322-213704/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

64
backups/portal-toggle-cleanup-20260322-213704/portal_login.html

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/portal-toggle-cleanup-20260322-213704/portal_set_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

12
backups/saas-panel-20260322-034651/portal_dashboard.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Client Dashboard - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 1100px; margin: 2rem auto; padding: 1.25rem; } .portal-top { display:flex; justify-content:space-between; align-items:center; gap:1rem; flex-wrap:wrap; margin-bottom: 1rem; } .portal-actions a { margin-left: 0.75rem; text-decoration: underline; } .summary-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; margin: 1rem 0 1.25rem 0; } .summary-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 14px; padding: 1rem; background: rgba(255,255,255,0.03); } .summary-card h3 { margin-top:0; margin-bottom:0.4rem; } table.portal-table { width: 100%; border-collapse: collapse; } table.portal-table th, table.portal-table td { padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.12); text-align: left; } table.portal-table th { background: #e9eef7; color: #10203f; } .invoice-link { color: inherit; text-decoration: underline; font-weight: 600; } .status-badge { display: inline-block; padding: 0.18rem 0.55rem; border-radius: 999px; font-size: 0.86rem; font-weight: 700; } .status-paid { background: rgba(34, 197, 94, 0.18); color: #4ade80; } .status-pending { background: rgba(245, 158, 11, 0.20); color: #fbbf24; } .status-overdue { background: rgba(239, 68, 68, 0.18); color: #f87171; } .status-other { background: rgba(148, 163, 184, 0.20); color: #cbd5e1; } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-top"> <div> <h1>Client Dashboard</h1> <p>{{ client.company_name or client.contact_name or client.email }}</p> </div> <div class="portal-actions"> <a href="/portal/invoices/download-all">Download All Invoices (ZIP)</a> <a href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> <a href="/portal/logout">Logout</a> </div> </div> <div class="summary-grid"> <div class="summary-card"> <h3>Total Invoices</h3> <div>{{ invoice_count }}</div> </div> <div class="summary-card"> <h3>Total Outstanding</h3> <div>{{ total_outstanding }}</div> </div> <div class="summary-card"> <h3>Total Paid</h3> <div>{{ total_paid }}</div> </div> </div> <h2>Invoices</h2> <table class="portal-table"> <thead> <tr> <th>Invoice</th> <th>Status</th> <th>Created</th> <th>Total</th> <th>Paid</th> <th>Outstanding</th> </tr> </thead> <tbody> {% for row in invoices %} <tr> <td> <a class="invoice-link" href="/portal/invoice/{{ row.id }}"> {{ row.invoice_number or ("INV-" ~ row.id) }} </a> </td> <td> {% set s = (row.status or "")|lower %} {% if s == "paid" %} <span class="status-badge status-paid">{{ row.status }}</span> {% elif s == "pending" %} <span class="status-badge status-pending">{{ row.status }}</span> {% elif s == "overdue" %} <span class="status-badge status-overdue">{{ row.status }}</span> {% else %} <span class="status-badge status-other">{{ row.status }}</span> {% endif %} </td> <td>{{ row.created_at }}</td> <td>{{ row.total_amount }}</td> <td>{{ row.amount_paid }}</td> <td>{{ row.outstanding }}</td> </tr> {% else %} <tr> <td colspan="6">No invoices available.</td> </tr> {% endfor %} </tbody> </table>
</div> <script>
(function() { setTimeout(function() { window.location.reload(); }, 20000);
})();
</script> {% include "footer.html" %}
</body>
</html>

9
backups/saas-panel-20260322-034651/portal_login.html

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Client Portal - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-note { margin-top: 1rem; opacity: 0.88; font-size: 0.95rem; } .portal-links { margin-top: 1rem; } .portal-links a { margin-right: 1rem; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>OutsideTheBox Client Portal</h1> <p class="portal-sub">Secure access for invoices, balances, and account information.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/login"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required> </div> <div> <label for="credential">Access Code or Password</label> <input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Sign In</button> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> <div style="margin-top:15px;"> <a href="/portal/forgot-password">Forgot your password?</a> </div> <p class="portal-note"> First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password. This access code is single-use and is cleared after password setup. Future logins use your email address and password. </p> </div>
</div> {% include "footer.html" %}
</body>
</html>

134
backups/saas-panel-20260322-034651/style.css

@ -0,0 +1,134 @@
/* ===== GLOBAL ===== */
body {
background: linear-gradient(135deg, #0a1628, #0c1f3f);
color: #e6edf3;
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
}
/* ===== CONTAINER ===== */
.container {
max-width: 1100px;
margin: 40px auto;
padding: 0 20px;
}
/* ===== HEADERS ===== */
h1 {
font-size: 28px;
margin-bottom: 5px;
}
.subtext {
color: #9fb3c8;
margin-bottom: 25px;
}
/* ===== ACTION BAR ===== */
.action-bar {
display: flex;
gap: 10px;
margin-bottom: 25px;
flex-wrap: wrap;
}
.btn {
background: #132a4a;
border: 1px solid #2c4d75;
color: #e6edf3;
padding: 10px 16px;
border-radius: 8px;
text-decoration: none;
font-size: 14px;
transition: all 0.2s;
}
.btn:hover {
background: #1a3a66;
border-color: #3d6ea8;
}
.btn-primary {
background: #1e6fff;
border-color: #1e6fff;
}
.btn-primary:hover {
background: #3b82ff;
}
/* ===== STATS ===== */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 18px;
}
.stat-card h3 {
font-size: 14px;
color: #9fb3c8;
margin-bottom: 8px;
}
.stat-card p {
font-size: 20px;
font-weight: bold;
}
/* ===== TABLE ===== */
.table-container {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
padding: 12px;
font-size: 13px;
color: #9fb3c8;
background: rgba(255,255,255,0.05);
}
td {
padding: 12px;
border-top: 1px solid rgba(255,255,255,0.05);
}
tr:hover {
background: rgba(255,255,255,0.03);
}
/* ===== STATUS ===== */
.status-paid {
color: #22c55e;
font-weight: bold;
}
.status-unpaid {
color: #f59e0b;
}
/* ===== LINKS ===== */
a {
color: #60a5fa;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

125
backups/shared-brand-20260322-212608/portal_dashboard.html

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/shared-brand-20260322-212608/portal_forgot_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

31
backups/shared-brand-20260322-212608/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

64
backups/shared-brand-20260322-212608/portal_login.html

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

21
backups/shared-brand-20260322-212608/portal_set_password.html

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
</body>
</html>

28
backups/shared-brand-20260322-212608/site_nav.html

@ -0,0 +1,28 @@
<header class="header">
<div class="container">
<div class="nav">
<a class="brand" href="https://outsidethebox.top">
<img src="/static/favicon.png" alt="outsidethebox.top logo" />
<div class="title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<nav class="navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<div class="dropdown">
<a href="#" class="dropdown-toggle">Services</a>
<div class="dropdown-menu">
<a href="https://follow-me.outsidethebox.top">Follow-me Tracker</a>
<a href="https://monitor.outsidethebox.top">Oracle</a>
</div>
</div>
<a href="/portal">Portal</a>
</nav>
</div>
</div>
</header>

794
backups/shared-brand-20260322-212608/style.css

@ -0,0 +1,794 @@
:root{
--bg:#0b0f14;
--card:#121825;
--card-soft:rgba(18,24,37,.78);
--text:#e8eefc;
--muted:#aab6d6;
--line:#24304a;
--accent:#7aa2ff;
--accent2:#62e6b7;
--success:#4ade80;
--warn:#fbbf24;
--danger:#f87171;
--radius:16px;
--shadow:0 16px 40px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji","Segoe UI Emoji";
background:
radial-gradient(1200px 600px at 20% 0%, rgba(122,162,255,.22), transparent 60%),
radial-gradient(900px 500px at 90% 20%, rgba(98,230,183,.15), transparent 60%),
linear-gradient(180deg, #081225 0%, #09172d 100%);
color:var(--text);
line-height:1.45;
}
a{color:inherit}
/* ===== Shared container ===== */
.container{
max-width:1100px;
margin:0 auto;
padding:20px 18px;
}
/* ===== Header / branded nav ===== */
.header{width:100%}
.nav{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:12px 0 22px 0;
}
.brand{
display:flex;
align-items:center;
gap:14px;
text-decoration:none;
}
.brand img{
height:60px;
width:auto;
display:block;
object-fit:contain;
background: rgba(255,255,255,0.92);
padding: 6px 12px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.title{
display:flex;
flex-direction:column;
line-height:1.1;
}
.title strong{letter-spacing:.2px}
.title span{
color:var(--muted);
font-size:13px;
margin-top:2px;
}
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
}
.navlinks a{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color:var(--muted);
border:1px solid transparent;
}
.navlinks a:hover{
color:var(--text);
border-color:rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.navlinks a.active{
color:var(--text);
border-color:rgba(255,255,255,.10);
background:rgba(255,255,255,.04);
}
/* ===== Generic portal shell ===== */
.portal-shell{
max-width:1100px;
margin:16px auto 28px auto;
padding:0 18px 20px 18px;
}
.portal-card,
.detail-card,
.summary-card,
.pay-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.portal-card{
max-width:760px;
margin:24px auto 12px auto;
padding:22px;
}
.portal-page-header{
display:flex;
align-items:flex-start;
justify-content:space-between;
gap:16px;
flex-wrap:wrap;
margin:6px 0 18px 0;
}
.portal-page-title{
margin:0;
font-size:24px;
line-height:1.1;
}
.portal-page-subtitle{
margin:8px 0 0 0;
color:var(--muted);
}
.portal-client-name{
margin:8px 0 0 0;
color:var(--text);
font-size:15px;
}
.portal-toolbar{
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:center;
}
.portal-btn,
.btn,
.pay-btn,
.quote-pick-btn{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:42px;
padding:10px 14px;
border-radius:12px;
text-decoration:none;
border:1px solid rgba(255,255,255,.10);
background: rgba(255,255,255,.05);
color:var(--text);
font-weight:600;
cursor:pointer;
}
.portal-btn:hover,
.btn:hover,
.pay-btn:hover,
.quote-pick-btn:hover{
border-color:rgba(122,162,255,.45);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
text-decoration:none;
}
.portal-btn.primary,
.btn.primary{
background: linear-gradient(135deg, rgba(122,162,255,.95), rgba(98,230,183,.85));
border-color: transparent;
color:#071017;
font-weight:700;
}
.portal-btn.primary:hover,
.btn.primary:hover{
box-shadow:0 0 0 4px rgba(98,230,183,.18);
}
/* ===== Login / forms ===== */
.portal-sub{
color:var(--muted);
margin:0 0 16px 0;
}
.portal-form{
display:grid;
gap:14px;
}
.portal-form label{
display:block;
font-weight:600;
margin-bottom:6px;
}
.portal-form input,
.portal-form select,
.pay-selector{
width:100%;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.14);
background: rgba(255,255,255,.06);
color:var(--text);
box-sizing:border-box;
outline:none;
}
.portal-form input:focus,
.portal-form select:focus,
.pay-selector:focus{
border-color:rgba(122,162,255,.65);
box-shadow:0 0 0 4px rgba(122,162,255,.12);
}
.portal-actions{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:4px;
}
.portal-note{
margin-top:16px;
color:var(--muted);
font-size:14px;
line-height:1.5;
}
.portal-msg,
.error-box,
.success-box{
margin-bottom:16px;
padding:12px 14px;
border-radius:12px;
border:1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.04);
}
.error-box{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
color: #fecaca;
}
.success-box{
border-color: rgba(34, 197, 94, 0.55);
background: rgba(22, 101, 52, 0.18);
color: #dcfce7;
}
/* ===== Dashboard ===== */
.portal-wrap{
max-width:1100px;
margin:0 auto;
padding:0;
}
.summary-grid,
.detail-grid{
display:grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap:14px;
margin: 0 0 18px 0;
}
.summary-card,
.detail-card{
padding:18px;
}
.summary-card h3,
.detail-card h3{
margin:0 0 8px 0;
font-size:14px;
color:var(--muted);
}
.summary-card .summary-value,
.detail-card .detail-value{
font-size:28px;
font-weight:800;
line-height:1.1;
color:var(--text);
}
.summary-card .summary-sub{
margin-top:6px;
font-size:12px;
color:var(--muted);
}
.section-title{
margin:18px 0 10px 0;
font-size:22px;
}
.table-card{
background: var(--card-soft);
border: 1px solid rgba(255,255,255,.07);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow:hidden;
}
/* ===== Tables ===== */
table,
.portal-table,
.quote-table{
width:100%;
border-collapse: collapse;
}
table.portal-table th,
table.portal-table td,
.quote-table th,
.quote-table td{
padding: 0.9rem 0.85rem;
border-bottom: 1px solid rgba(255,255,255,0.10);
text-align: left;
vertical-align: middle;
}
table.portal-table th,
.quote-table th{
background: rgba(255,255,255,.06);
color: var(--text);
font-size:13px;
letter-spacing:.02em;
}
table.portal-table tr:hover td,
.quote-table tr:hover td{
background: rgba(255,255,255,.02);
}
.invoice-link{
color: var(--text);
text-decoration: none;
font-weight: 700;
}
.invoice-link:hover{
color: var(--accent);
text-decoration: underline;
}
/* ===== Badges ===== */
.status-badge,
.quote-badge{
display: inline-block;
padding: 0.22rem 0.62rem;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.status-paid{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.status-pending{ background: rgba(245, 158, 11, 0.20); color: var(--warn); }
.status-overdue{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
.status-other{ background: rgba(148, 163, 184, 0.20); color: #cbd5e1; }
.quote-live{ background: rgba(34, 197, 94, 0.18); color: var(--success); }
.quote-stale{ background: rgba(239, 68, 68, 0.18); color: var(--danger); }
/* ===== Payments / invoice detail ===== */
.pay-card{
padding:18px;
margin-top: 1.25rem;
}
.pay-selector-row{
display:flex;
gap:0.75rem;
align-items:center;
flex-wrap:wrap;
margin-top:0.75rem;
}
.pay-panel{
margin-top: 1rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: rgba(255,255,255,0.02);
}
.pay-panel.hidden{ display:none; }
.pay-btn-square { background:#16a34a; border-color:transparent; color:#fff; }
.pay-btn-wallet { background:#2563eb; border-color:transparent; color:#fff; }
.pay-btn-mobile { background:#7c3aed; border-color:transparent; color:#fff; }
.pay-btn-copy { background:#374151; border-color:transparent; color:#fff; }
.snapshot-wrap{
position: relative;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.02);
}
.snapshot-header{
display:flex;
justify-content:space-between;
gap:1rem;
align-items:flex-start;
}
.snapshot-meta{
flex: 1 1 auto;
min-width: 0;
line-height: 1.65;
}
.snapshot-timer-box{
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value{
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
}
.snapshot-timer-label{
margin-top: 0.55rem;
font-size: 0.95rem;
opacity: 0.95;
}
.snapshot-timer-expired{ color: var(--danger); }
.lock-box{
margin-top: 1rem;
border: 1px solid rgba(34, 197, 94, 0.28);
background: rgba(22, 101, 52, 0.16);
border-radius: 12px;
padding: 1rem;
}
.lock-box.expired{
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.lock-grid{
display:grid;
grid-template-columns: 1fr 220px;
gap:1rem;
align-items:start;
}
.lock-code{
display:block;
margin-top:0.35rem;
padding:0.65rem 0.8rem;
background: rgba(0,0,0,0.22);
border-radius: 8px;
overflow-wrap:anywhere;
}
.wallet-actions{
display:flex;
gap:0.75rem;
flex-wrap:wrap;
margin-top:0.9rem;
align-items:center;
}
.wallet-help{
margin-top: 0.85rem;
padding: 0.9rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
}
.wallet-help h4{
margin: 0 0 0.55rem 0;
font-size: 1rem;
}
.wallet-help p{ margin: 0.35rem 0; }
.wallet-note{ opacity:0.9; margin-top:0.65rem; }
.mono{ font-family: monospace; }
.copy-row{
display:flex;
gap:0.5rem;
flex-wrap:wrap;
align-items:center;
margin-top:0.65rem;
}
.copy-target{
flex: 1 1 420px;
min-width: 220px;
}
.copy-status{
display:inline-block;
margin-left: 0.5rem;
opacity: 0.9;
}
/* ===== Footer ===== */
footer{
margin:28px auto 20px auto;
max-width:1100px;
padding:0 18px;
color:var(--muted);
font-size:13px;
}
/* ===== Responsive ===== */
@media (max-width: 900px){
.summary-grid,
.detail-grid{
grid-template-columns:1fr;
}
.nav{
align-items:flex-start;
flex-direction:column;
}
.navlinks{
justify-content:flex-start;
}
.brand img{
height:54px;
}
}
@media (max-width: 820px){
.snapshot-header,
.lock-grid{
grid-template-columns: 1fr;
display:block;
}
.snapshot-timer-box{
width: 100%;
margin-top: 1rem;
min-height: 110px;
}
}
/* ===== Fixed CAD / Oracle status bar ===== */
body{
padding-bottom: 56px;
}
.otb-statusbar{
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 8px 14px;
background: rgba(8, 16, 32, 0.94);
border-top: 1px solid rgba(255,255,255,.10);
backdrop-filter: blur(8px);
box-shadow: 0 -8px 24px rgba(0,0,0,.28);
}
.otb-statusbar-inner{
width: 100%;
max-width: 1100px;
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
text-align: center;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.otb-statusbar strong{
color: var(--text);
font-weight: 700;
}
.otb-statusbar a{
color: var(--accent2);
text-decoration: none;
font-weight: 600;
}
.otb-statusbar a:hover{
text-decoration: underline;
}
.otb-dot{
width: 6px;
height: 6px;
border-radius: 999px;
display: inline-block;
background: rgba(255,255,255,.25);
flex: 0 0 auto;
}
/* ===== Payment method chips ===== */
.payment-method{
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 7px;
padding: 4px 9px;
border-radius: 999px;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.08);
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .01em;
}
.payment-method::before{
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
background: #7aa2ff;
box-shadow: 0 0 0 3px rgba(255,255,255,.04);
}
.payment-square::before{ background: #7dd3fc; }
.payment-etransfer::before{ background: #86efac; }
.payment-etho::before{ background: #b084ff; }
.payment-etica::before{ background: #4fd1c5; }
.payment-alt::before{ background: #fbbf24; }
.payment-cad::before{ background: #cbd5e1; }
/* ===== Slightly stronger row hover ===== */
table.portal-table tbody tr:hover td,
.quote-table tbody tr:hover td{
background: rgba(255,255,255,.035);
transition: background .14s ease;
}
@media (max-width: 700px){
body{
padding-bottom: 72px;
}
.otb-statusbar-inner{
font-size: 11px;
line-height: 1.25;
}
}
@media (max-width: 900px){
.dropdown{
width:100%;
}
.dropdown-toggle{
width:100%;
}
.dropdown-menu{
position:static;
right:auto;
top:auto;
min-width:100%;
margin-top:6px;
}
}
/* ===== Shared services dropdown ===== */
.navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
align-items:center;
}
.dropdown{
position:relative;
display:inline-block;
}
.dropdown-toggle{
display:inline-block;
cursor:pointer;
}
.dropdown-menu{
position:absolute;
top:calc(100% + 8px);
right:0;
min-width:220px;
display:none;
padding:10px;
border-radius:14px;
background:rgba(18,24,37,.98);
border:1px solid rgba(255,255,255,.08);
box-shadow:0 16px 40px rgba(0,0,0,.35);
z-index:9999;
}
.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu{
display:block;
}
.dropdown-menu a{
display:block;
padding:9px 10px;
border-radius:10px;
color:var(--muted);
text-decoration:none;
white-space:nowrap;
margin:0;
}
.dropdown-menu a + a{
margin-top:4px;
}
.dropdown-menu a:hover{
color:var(--text);
background:rgba(255,255,255,.04);
}
@media (max-width: 900px){
.dropdown{
width:100%;
}
.dropdown-toggle{
width:100%;
}
.dropdown-menu{
position:static;
right:auto;
top:auto;
min-width:100%;
margin-top:6px;
}
}

126
backups/shared-brand-20260322-221933/portal_dashboard.html

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
<script src="/static/brand.js" defer></script>
</body>
</html>

22
backups/shared-brand-20260322-221933/portal_forgot_password.html

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
<script src="/static/brand.js" defer></script>
</body>
</html>

32
backups/shared-brand-20260322-221933/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

65
backups/shared-brand-20260322-221933/portal_login.html

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
<script src="/static/brand.js" defer></script>
</body>
</html>

22
backups/shared-brand-20260322-221933/portal_set_password.html

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>Billing base currency: 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>Methods: Credit Card (via Square), e-Transfer, and enabled crypto assets</span>
</div>
</div>
{% include "footer.html" %}
<script src="/static/brand.js" defer></script>
</body>
</html>

35
backups/shared-brand-20260322-221933/site_nav.html

@ -0,0 +1,35 @@
<header class="site-header">
<div class="site-container">
<div class="site-nav">
<a class="site-brand" href="https://outsidethebox.top">
<img src="https://outsidethebox.top/assets/favicon.png" alt="outsidethebox.top logo" />
<div class="site-title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<div class="site-nav-right">
<nav class="site-navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<div class="dropdown">
<a href="#" class="dropdown-toggle">Services</a>
<div class="dropdown-menu">
<a href="https://follow-me.outsidethebox.top">Follow-me Tracker</a>
<a href="https://monitor.outsidethebox.top">Oracle</a>
</div>
</div>
<a href="https://otb-billing.outsidethebox.top/portal">Portal</a>
</nav>
<label class="otb-theme-switch" title="Toggle light / dark mode">
<input type="checkbox" id="otbThemeToggle" aria-label="Toggle theme" />
<span class="otb-theme-slider"></span>
</label>
</div>
</div>
</div>
</header>

1096
backups/shared-brand-20260322-221933/style.css

File diff suppressed because it is too large Load Diff

129
backups/shared-brand-20260322-222018/portal_dashboard.html

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Dashboard - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-wrap">
<div class="portal-page-header">
<div>
<h1 class="portal-page-title">Client Dashboard</h1>
<p class="portal-client-name">{{ client.company_name or client.contact_name or client.email }}</p>
<p class="portal-page-subtitle">Invoices, balances, and account activity in one place.</p>
</div>
<div class="portal-toolbar">
<a class="portal-btn primary" href="/portal/invoices/download-all">Download All Invoices</a>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
<a class="portal-btn" href="/portal/logout">Logout</a>
</div>
</div>
<div class="summary-grid">
<div class="summary-card">
<h3>Total Invoices</h3>
<div class="summary-value">{{ invoice_count }}</div>
<div class="summary-sub">Invoices currently visible in your portal</div>
</div>
<div class="summary-card">
<h3>Total Outstanding</h3>
<div class="summary-value">{{ total_outstanding }}</div>
<div class="summary-sub">Current unpaid balance</div>
</div>
<div class="summary-card">
<h3>Total Paid</h3>
<div class="summary-value">{{ total_paid }}</div>
<div class="summary-sub">Payments already applied</div>
</div>
</div>
<h2 class="section-title">Invoices</h2>
<div class="table-card">
<table class="portal-table">
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Created</th>
<th>Total</th>
<th>Paid</th>
<th>Outstanding</th>
</tr>
</thead>
<tbody>
{% for row in invoices %}
<tr>
<td>
<a class="invoice-link" href="/portal/invoice/{{ row.id }}">
{{ row.invoice_number or ("INV-" ~ row.id) }}
</a>
</td>
<td>
{% set s = (row.status or "")|lower %}
{% if s == "paid" %}
<span class="status-badge status-paid">{{ row.status }}</span>
{% if row.payment_method_label %}
<div class="payment-method
{% if row.payment_method_label == "Square" %} payment-square{% elif row.payment_method_label == "e-Transfer" %} payment-etransfer{% elif row.payment_method_label == "ETHO" %} payment-etho{% elif row.payment_method_label == "ETI" or row.payment_method_label == "EGAZ" %} payment-etica{% elif row.payment_method_label == "ALT" %} payment-alt{% elif row.payment_method_label == "CAD" %} payment-cad{% endif %}">
{{ row.payment_method_label }}
</div>
{% endif %}
{% elif s == "pending" %}
<span class="status-badge status-pending">{{ row.status }}</span>
{% elif s == "overdue" %}
<span class="status-badge status-overdue">{{ row.status }}</span>
{% else %}
<span class="status-badge status-other">{{ row.status }}</span>
{% endif %}
</td>
<td>{{ row.created_at }}</td>
<td>{{ row.total_amount }}</td>
<td>{{ row.amount_paid }}</td>
<td>{{ row.outstanding }}</td>
</tr>
{% else %}
<tr>
<td colspan="6">No invoices available.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
(function() {
setTimeout(function() { window.location.reload(); }, 20000);
})();
</script>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>All billing is calculated in 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>
Methods:
<strong>Credit Card</strong> <span style="opacity:0.7;">(via Square)</span>,
<strong>e-Transfer</strong>,
and <strong>enabled crypto assets</strong>
</span>
</div>
</div>
<script src="/static/brand.js" defer></script>
</body>
</html>

25
backups/shared-brand-20260322-222018/portal_forgot_password.html

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Forgot Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-card h1 { margin-top: 0; margin-bottom: 0.45rem; } .portal-sub { opacity: 0.9; margin-bottom: 1.25rem; } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-actions { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.4rem; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Reset Portal Password</h1> <p class="portal-sub">Enter your email address and a new single-use access code will be sent if your account exists.</p> {% if error %} <div class="portal-msg">{{ error }}</div> {% endif %} {% if message %} <div class="portal-msg">{{ message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/forgot-password"> <div> <label for="email">Email Address</label> <input id="email" name="email" type="email" value="{{ form_email or '' }}" required> </div> <div class="portal-actions"> <button class="portal-btn" type="submit">Send Reset Code</button> <a class="portal-btn" href="/portal">Back to Portal Login</a> <a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Portal%20Support" href="mailto:support@outsidethebox.top">Customer Support</a> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>All billing is calculated in 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>
Methods:
<strong>Credit Card</strong> <span style="opacity:0.7;">(via Square)</span>,
<strong>e-Transfer</strong>,
and <strong>enabled crypto assets</strong>
</span>
</div>
</div>
<script src="/static/brand.js" defer></script>
</body>
</html>

35
backups/shared-brand-20260322-222018/portal_invoice_detail.html

File diff suppressed because one or more lines are too long

68
backups/shared-brand-20260322-222018/portal_login.html

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Portal - OutsideTheBox</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %}
<div class="portal-shell">
<div class="portal-card">
<h1 class="portal-page-title">OutsideTheBox Client Portal</h1>
<p class="portal-sub">Secure access for invoices, balances, and account information.</p>
{% if portal_message %}
<div class="portal-msg">{{ portal_message }}</div>
{% endif %}
<form class="portal-form" method="post" action="/portal/login">
<div>
<label for="email">Email Address</label>
<input id="email" name="email" type="email" placeholder="client@example.com" value="{{ portal_email or '' }}" required>
</div>
<div>
<label for="credential">Access Code or Password</label>
<input id="credential" name="credential" type="password" placeholder="Enter your one-time access code or password" required>
</div>
<div class="portal-actions">
<button class="portal-btn primary" type="submit">Sign In</button>
<a class="portal-btn" href="mailto:support@outsidethebox.top?subject=Customer%20Support">Customer Support</a>
</div>
</form>
<div style="margin-top:15px;">
<a href="/portal/forgot-password">Forgot your password?</a>
</div>
<p class="portal-note">
First-time users should sign in with the one-time access code provided by OutsideTheBox, then set a password.
This access code is single-use and is cleared after password setup. Future logins use your email address and password.
</p>
</div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>All billing is calculated in 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>
Methods:
<strong>Credit Card</strong> <span style="opacity:0.7;">(via Square)</span>,
<strong>e-Transfer</strong>,
and <strong>enabled crypto assets</strong>
</span>
</div>
</div>
<script src="/static/brand.js" defer></script>
</body>
</html>

25
backups/shared-brand-20260322-222018/portal_set_password.html

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Set Portal Password - OutsideTheBox</title> <link rel="stylesheet" href="/static/css/style.css"> <style> .portal-wrap { max-width: 760px; margin: 2.5rem auto; padding: 1.5rem; } .portal-card { border: 1px solid rgba(255,255,255,0.16); border-radius: 16px; padding: 1.4rem; background: rgba(255,255,255,0.03); box-shadow: 0 10px 24px rgba(0,0,0,0.18); } .portal-form { display: grid; gap: 0.9rem; } .portal-form label { display: block; font-weight: 600; margin-bottom: 0.35rem; } .portal-form input { width: 100%; padding: 0.8rem 0.9rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.05); color: inherit; box-sizing: border-box; } .portal-btn { display: inline-block; padding: 0.8rem 1rem; border-radius: 10px; text-decoration: none; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; } .portal-msg { margin-bottom: 1rem; padding: 0.85rem 1rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.04); } </style> <link rel="icon" type="image/png" href="/static/favicon.png">
</head>
<body>
{% include "includes/site_nav.html" %} <div style="background:#111827;padding:10px 20px;"> </div> <div class="portal-wrap"> <div class="portal-card"> <h1>Create Your Portal Password</h1> <p>Welcome, {{ client_name }}. Your one-time access code worked. Please create a password for future logins.</p> {% if portal_message %} <div class="portal-msg">{{ portal_message }}</div> {% endif %} <form class="portal-form" method="post" action="/portal/set-password"> <div> <label for="password">New Password</label> <input id="password" name="password" type="password" required> </div> <div> <label for="password2">Confirm Password</label> <input id="password2" name="password2" type="password" required> </div> <div> <button class="portal-btn" type="submit">Set Password</button> </div> </form> </div>
</div>
<div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>All billing is calculated in 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>
Methods:
<strong>Credit Card</strong> <span style="opacity:0.7;">(via Square)</span>,
<strong>e-Transfer</strong>,
and <strong>enabled crypto assets</strong>
</span>
</div>
</div>
<script src="/static/brand.js" defer></script>
</body>
</html>

35
backups/shared-brand-20260322-222018/site_nav.html

@ -0,0 +1,35 @@
<header class="site-header">
<div class="site-container">
<div class="site-nav">
<a class="site-brand" href="https://outsidethebox.top">
<img src="https://outsidethebox.top/assets/favicon.png" alt="outsidethebox.top logo" />
<div class="site-title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<div class="site-nav-right">
<nav class="site-navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<div class="dropdown">
<a href="#" class="dropdown-toggle">Services</a>
<div class="dropdown-menu">
<a href="https://follow-me.outsidethebox.top">Follow-me Tracker</a>
<a href="https://monitor.outsidethebox.top">Oracle</a>
</div>
</div>
<a href="https://otb-billing.outsidethebox.top/portal">Portal</a>
</nav>
<label class="otb-theme-switch" title="Toggle light / dark mode">
<input type="checkbox" id="otbThemeToggle" aria-label="Toggle theme" />
<span class="otb-theme-slider"></span>
</label>
</div>
</div>
</div>
</header>

1096
backups/shared-brand-20260322-222018/style.css

File diff suppressed because it is too large Load Diff

1937
logs/crypto_reconciliation_worker.log

File diff suppressed because it is too large Load Diff

91
logs/invoice_reminder_worker.log

@ -0,0 +1,91 @@
[2026-03-14T16:31:25.037816] invoice_reminder_worker starting
[2026-03-14T16:31:25.042017] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-15T09:00:13.715512] invoice_reminder_worker starting
[2026-03-15T09:00:13.720030] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-16T09:00:14.708047] invoice_reminder_worker starting
[2026-03-16T09:00:14.712832] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-17T09:00:14.931911] invoice_reminder_worker starting
[2026-03-17T09:00:14.936172] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-18T09:00:15.000417] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-19T09:00:15.181181] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-20T09:00:14.989596] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-21T09:00:15.739604] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-22T09:00:13.788887] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-23T09:00:13.812301] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-24T09:00:13.771598] invoice_reminder_worker starting
Traceback (most recent call last):
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 123, in <module>
main()
File "/home/def/otb_billing/scripts/invoice_reminder_worker.py", line 98, in main
{recalc_invoice_totals(inv['id'])['total']}
TypeError: 'NoneType' object is not subscriptable
[2026-03-25T09:00:13.760118] invoice_reminder_worker starting
[2026-03-25T09:00:13.764319] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-26T09:00:13.774724] invoice_reminder_worker starting
[2026-03-26T09:00:13.778712] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-27T09:00:13.773589] invoice_reminder_worker starting
[2026-03-27T09:00:13.778961] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-28T09:00:13.783764] invoice_reminder_worker starting
[2026-03-28T09:00:13.787845] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-29T09:00:13.762998] invoice_reminder_worker starting
[2026-03-29T09:00:13.768366] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-30T09:00:14.699862] invoice_reminder_worker starting
[2026-03-30T09:00:14.703938] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-03-31T09:00:14.573494] invoice_reminder_worker starting
[2026-03-31T09:00:14.577931] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-01T09:00:14.690808] invoice_reminder_worker starting
[2026-04-01T09:00:14.695185] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-02T09:00:14.626933] invoice_reminder_worker starting
[2026-04-02T09:00:14.631353] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-03T09:00:14.702593] invoice_reminder_worker starting
[2026-04-03T09:00:14.708129] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-04T09:00:14.832769] invoice_reminder_worker starting
[2026-04-04T09:00:14.837022] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-05T09:00:15.412774] invoice_reminder_worker starting
[2026-04-05T09:00:15.429713] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-06T09:00:13.997050] invoice_reminder_worker starting
[2026-04-06T09:00:14.004077] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-07T09:00:13.691922] invoice_reminder_worker starting
[2026-04-07T09:00:13.696346] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-08T09:00:13.765715] invoice_reminder_worker starting
[2026-04-08T09:00:13.770314] checked=0 reminders_sent=0 overdue_sent=0 skipped=0
[2026-04-09T09:00:14.898994] invoice_reminder_worker starting
[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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save