Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
59a08356
Commit
59a08356
authored
Apr 22, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Code cleanup and fixes
parent
bf4e59fe
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
618 additions
and
138 deletions
+618
-138
database.py
aisbf/database.py
+63
-28
main.py
main.py
+145
-7
setup.py
setup.py
+2
-0
error.html
templates/dashboard/error.html
+30
-0
wallet.html
templates/dashboard/wallet.html
+378
-103
No files found.
aisbf/database.py
View file @
59a08356
...
...
@@ -50,6 +50,40 @@ def get_db_executor():
return
_db_executor
class
_MySQLConnectionWrapper
:
"""Wrapper that gives mysql.connector connections a reliable context manager protocol.
mysql-connector-python's C extension (__enter__/__exit__) has version-dependent
behaviour (some versions return a cursor from __enter__, others close the connection
in __exit__ unexpectedly). This wrapper always yields the raw connection and
handles commit/rollback/close explicitly.
"""
def
__init__
(
self
,
conn
):
self
.
_conn
=
conn
def
__enter__
(
self
):
return
self
.
_conn
def
__exit__
(
self
,
exc_type
,
exc_val
,
exc_tb
):
if
exc_type
is
None
:
self
.
_conn
.
commit
()
else
:
try
:
self
.
_conn
.
rollback
()
except
Exception
:
pass
# Connection is intentionally left open: cursor and conn variables in the
# calling function remain valid after the with-block exits (matching SQLite's
# context-manager behaviour). The connection is closed by GC when the caller
# function returns and conn goes out of scope.
return
False
# Forward attribute access so the wrapper can be used directly as well
def
__getattr__
(
self
,
name
):
return
getattr
(
self
.
_conn
,
name
)
class
DatabaseManager
:
"""
Manages database for persistent tracking of context dimensions and rate limiting.
...
...
@@ -103,9 +137,9 @@ class DatabaseManager:
port
=
self
.
db_config
[
'mysql_port'
],
user
=
self
.
db_config
[
'mysql_user'
],
password
=
self
.
db_config
[
'mysql_password'
],
database
=
self
.
db_config
[
'mysql_database'
]
database
=
self
.
db_config
[
'mysql_database'
]
,
)
return
conn
return
_MySQLConnectionWrapper
(
conn
)
except
Exception
as
e
:
logger
.
error
(
f
"MySQL connection failed: {e}"
)
raise
...
...
@@ -119,27 +153,20 @@ class DatabaseManager:
async
def
execute
(
self
,
sql
:
str
,
params
:
dict
=
None
):
"""Execute SQL query and return result with mappings (compatible with AsyncSession interface)"""
_params
=
params
or
{}
def
_sync_execute
():
with
self
.
_get_connection
()
as
conn
:
cursor
=
conn
.
cursor
()
cursor
.
row_factory
=
sqlite3
.
Row
params
=
params
or
{}
# Safe parameter handling - use native database parameter binding
if
self
.
db_type
==
'sqlite'
:
# SQLite natively supports :named parameters directly
cursor
.
execute
(
sql
,
params
)
cursor
=
conn
.
cursor
()
cursor
.
row_factory
=
sqlite3
.
Row
cursor
.
execute
(
sql
,
_params
)
else
:
# For MySQL, safely convert named parameters to %s placeholders
param_names
=
[]
def
replace_param
(
match
):
param_names
.
append
(
match
.
group
(
1
))
return
'
%
s'
cursor
=
conn
.
cursor
(
dictionary
=
True
)
import
re
p
rocessed_sql
=
re
.
sub
(
r':(\w+)'
,
replace_param
,
sql
)
p
arams_list
=
[
params
[
name
]
for
name
in
param_names
]
cursor
.
execute
(
processed_sql
,
params_list
)
p
aram_names
=
[]
p
rocessed_sql
=
re
.
sub
(
r':(\w+)'
,
lambda
m
:
(
param_names
.
append
(
m
.
group
(
1
)),
'
%
s'
)[
1
],
sql
)
cursor
.
execute
(
processed_sql
,
[
_params
[
n
]
for
n
in
param_names
]
)
if
cursor
.
description
:
rows
=
[
dict
(
row
)
for
row
in
cursor
.
fetchall
()]
# Simulate SQLAlchemy Result object with mappings() method
...
...
@@ -3328,6 +3355,7 @@ def DatabaseManager__init__(self, db_config: Optional[Dict[str, Any]] = None, da
self
.
db_config
=
db_config
self
.
db_type
=
self
.
db_config
.
get
(
'type'
,
'sqlite'
)
.
lower
()
self
.
executor
=
get_db_executor
()
if
self
.
db_type
==
'mysql'
:
# Import the module-level MYSQL_AVAILABLE flag
...
...
@@ -3542,13 +3570,18 @@ def DatabaseManager__initialize_database(self):
# ''')
#
# try:
# cursor.execute('''
# CREATE INDEX IF NOT EXISTS idx_model_embeddings_provider_model
# ON model_embeddings(provider_id, model_name)
# ''')
# except:
# pass
# # Index creation moved to separate migration
#
# Create admin settings table for system configuration
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS admin_settings (
id INTEGER PRIMARY KEY {auto_increment},
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
'''
)
# Create users table for multi-user management
cursor
.
execute
(
f
'''
CREATE TABLE IF NOT EXISTS users (
...
...
@@ -3997,9 +4030,8 @@ def DatabaseManager__initialize_database(self):
# Run configuration database migrations if this is a CONFIG database
if
self
.
database_type
==
DatabaseRegistry
.
TYPE_CONFIG
:
self
.
_run_config_migrations
(
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
)
conn
.
commit
()
logger
.
info
(
f
"Database tables initialized successfully for {self.database_type} database"
)
conn
.
commit
()
logger
.
info
(
f
"Database tables initialized successfully for {self.database_type} database"
)
def
DatabaseManager__create_config_tables
(
self
,
cursor
,
auto_increment
,
timestamp_default
,
boolean_type
):
...
...
@@ -4176,6 +4208,7 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
is_visible {boolean_type} DEFAULT 1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
...
...
@@ -4204,6 +4237,7 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
max_autoselections INTEGER DEFAULT -1,
max_rotation_models INTEGER DEFAULT -1,
max_autoselection_models INTEGER DEFAULT -1,
is_visible {boolean_type} DEFAULT 1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default}
)
...
...
@@ -4227,7 +4261,8 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
(
'max_rotation_models'
,
'INTEGER DEFAULT -1'
),
(
'max_autoselection_models'
,
'INTEGER DEFAULT -1'
),
(
'is_default'
,
f
'{boolean_type} DEFAULT 0'
),
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
)
(
'is_active'
,
f
'{boolean_type} DEFAULT 1'
),
(
'is_visible'
,
f
'{boolean_type} DEFAULT 1'
)
]
col_count
=
0
for
col_name
,
col_def
in
tier_columns
:
...
...
main.py
View file @
59a08356
...
...
@@ -494,6 +494,8 @@ app = FastAPI(
# Initialize Jinja2 templates with custom globals for proxy-aware URLs
templates
=
Jinja2Templates
(
directory
=
"templates"
)
# Add root templates directory to search path for parent template resolution
templates
.
env
.
loader
.
searchpath
.
insert
(
0
,
"templates"
)
# Monkey patch TemplateResponse to automatically add dashboard context variables
original_template_response
=
templates
.
TemplateResponse
...
...
@@ -8141,9 +8143,13 @@ async def dashboard_wallet(request: Request):
wallet_manager
=
WalletManager
(
db
)
wallet
=
await
wallet_manager
.
get_wallet
(
user_id
)
all_gateways
=
db
.
get_payment_gateway_settings
()
enabled_gateways
=
{
k
:
v
for
k
,
v
in
all_gateways
.
items
()
if
v
.
get
(
'enabled'
,
False
)}
return
templates
.
TemplateResponse
(
"dashboard/wallet.html"
,
{
"request"
:
request
,
"wallet"
:
wallet
"wallet"
:
wallet
,
"enabled_gateways"
:
enabled_gateways
,
})
except
ImportError
:
return
HTMLResponse
(
"Wallet functionality not available"
,
status_code
=
503
)
...
...
@@ -8154,33 +8160,165 @@ async def dashboard_wallet(request: Request):
"error"
:
"Failed to load wallet. Please try again later."
},
status_code
=
500
)
@
app
.
post
(
"/dashboard/wallet/topup"
)
async
def
dashboard_wallet_topup
(
request
:
Request
):
"""Session-authenticated wallet top-up — supports all admin-enabled gateways."""
from
fastapi.responses
import
JSONResponse
auth_check
=
require_dashboard_auth
(
request
)
if
auth_check
:
return
JSONResponse
({
"error"
:
"Unauthorized"
},
status_code
=
401
)
user_id
=
request
.
session
.
get
(
'user_id'
)
try
:
body
=
await
request
.
json
()
except
Exception
:
return
JSONResponse
({
"error"
:
"Invalid request body"
},
status_code
=
400
)
method
=
(
body
.
get
(
'payment_method'
)
or
''
)
.
lower
()
amount
=
body
.
get
(
'amount'
)
try
:
amount
=
float
(
amount
)
except
(
TypeError
,
ValueError
):
return
JSONResponse
({
"error"
:
"Invalid amount"
},
status_code
=
400
)
if
amount
<
5
or
amount
>
500
:
return
JSONResponse
({
"error"
:
"Amount must be between $5 and $500"
},
status_code
=
400
)
db
=
DatabaseRegistry
.
get_config_database
()
gateways
=
db
.
get_payment_gateway_settings
()
gw
=
gateways
.
get
(
method
,
{})
if
not
gw
.
get
(
'enabled'
,
False
):
return
JSONResponse
({
"error"
:
f
"Payment method '{method}' is not enabled"
},
status_code
=
400
)
# Crypto: return deposit address (manual transfer)
crypto_methods
=
{
'bitcoin'
,
'ethereum'
,
'usdt'
,
'usdc'
}
if
method
in
crypto_methods
:
address
=
gw
.
get
(
'address'
,
''
)
if
not
address
:
return
JSONResponse
({
"error"
:
"Crypto address not configured"
},
status_code
=
503
)
return
JSONResponse
({
"type"
:
"crypto"
,
"method"
:
method
,
"address"
:
address
,
"amount"
:
amount
,
"network"
:
gw
.
get
(
'network'
,
''
),
"confirmations"
:
gw
.
get
(
'confirmations'
,
3
),
})
# Stripe: create checkout session
if
method
==
'stripe'
:
try
:
payment_service
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
if
payment_service
and
hasattr
(
payment_service
,
'stripe_handler'
):
from
decimal
import
Decimal
intent
=
await
payment_service
.
stripe_handler
.
create_payment_intent
(
user_id
,
Decimal
(
str
(
amount
)),
metadata
=
{
"type"
:
"wallet_topup"
}
)
return
JSONResponse
({
"type"
:
"stripe"
,
"client_secret"
:
intent
.
client_secret
})
# Fallback: redirect to Stripe-hosted checkout via publishable key
import
stripe
stripe
.
api_key
=
gw
.
get
(
'secret_key'
,
''
)
session
=
stripe
.
checkout
.
Session
.
create
(
payment_method_types
=
[
'card'
],
line_items
=
[{
'price_data'
:
{
'currency'
:
'usd'
,
'product_data'
:
{
'name'
:
'Wallet Top-Up'
},
'unit_amount'
:
int
(
amount
*
100
),
},
'quantity'
:
1
,
}],
mode
=
'payment'
,
success_url
=
str
(
request
.
base_url
)
+
'dashboard/wallet?topup=success'
,
cancel_url
=
str
(
request
.
base_url
)
+
'dashboard/wallet?topup=cancelled'
,
metadata
=
{
'type'
:
'wallet_topup'
,
'user_id'
:
str
(
user_id
)},
)
return
JSONResponse
({
"type"
:
"stripe"
,
"checkout_url"
:
session
.
url
})
except
Exception
as
e
:
logger
.
error
(
f
"Stripe top-up error: {e}"
)
return
JSONResponse
({
"error"
:
"Stripe checkout failed. Please try again."
},
status_code
=
502
)
# PayPal: create order
if
method
==
'paypal'
:
try
:
payment_service
=
getattr
(
request
.
app
.
state
,
'payment_service'
,
None
)
if
payment_service
and
hasattr
(
payment_service
,
'paypal_handler'
):
from
decimal
import
Decimal
order
=
await
payment_service
.
paypal_handler
.
create_order
(
user_id
,
Decimal
(
str
(
amount
)),
metadata
=
{
"type"
:
"wallet_topup"
}
)
return
JSONResponse
({
"type"
:
"paypal"
,
"order_id"
:
order
.
id
})
# Fallback: direct PayPal redirect
client_id
=
gw
.
get
(
'client_id'
,
''
)
sandbox
=
gw
.
get
(
'sandbox'
,
True
)
paypal_base
=
"https://www.sandbox.paypal.com"
if
sandbox
else
"https://www.paypal.com"
return
JSONResponse
({
"type"
:
"paypal"
,
"paypal_base"
:
paypal_base
,
"client_id"
:
client_id
,
"amount"
:
amount
,
})
except
Exception
as
e
:
logger
.
error
(
f
"PayPal top-up error: {e}"
)
return
JSONResponse
({
"error"
:
"PayPal checkout failed. Please try again."
},
status_code
=
502
)
return
JSONResponse
({
"error"
:
f
"Unsupported payment method: {method}"
},
status_code
=
400
)
@
app
.
get
(
"/dashboard/wallet/transactions"
)
async
def
dashboard_wallet_transactions
(
request
:
Request
,
limit
:
int
=
50
,
offset
:
int
=
0
):
"""Session-authenticated wallet transaction history (used by the wallet dashboard page)."""
auth_check
=
require_dashboard_auth
(
request
)
if
auth_check
:
from
fastapi.responses
import
JSONResponse
return
JSONResponse
({
"error"
:
"Unauthorized"
},
status_code
=
401
)
user_id
=
request
.
session
.
get
(
'user_id'
)
try
:
from
aisbf.payments.wallet.manager
import
WalletManager
db
=
DatabaseRegistry
.
get_config_database
()
wallet_manager
=
WalletManager
(
db
)
transactions
=
await
wallet_manager
.
get_transactions
(
user_id
,
limit
=
limit
,
offset
=
offset
)
return
transactions
except
Exception
as
e
:
logger
.
error
(
f
"Failed to load wallet transactions: {e}"
)
from
fastapi.responses
import
JSONResponse
return
JSONResponse
({
"error"
:
"Failed to load transactions"
},
status_code
=
500
)
@
app
.
get
(
"/dashboard/billing"
)
async
def
dashboard_billing
(
request
:
Request
):
"""User payment transaction history page"""
auth_check
=
require_dashboard_auth
(
request
)
if
auth_check
:
return
auth_check
db
=
DatabaseRegistry
.
get_config_database
()
user_id
=
request
.
session
.
get
(
'user_id'
)
# Get user payment methods
payment_methods
=
db
.
get_user_payment_methods
(
user_id
)
# Get payment transactions
transactions
=
db
.
get_user_payment_transactions
(
user_id
)
# Get enabled payment gateways
enabled_gateways
=
[]
gateways
=
db
.
get_payment_gateway_settings
()
for
gateway
,
settings
in
gateways
.
items
():
if
settings
.
get
(
'enabled'
,
False
):
enabled_gateways
.
append
(
gateway
)
# Get user wallet
currency_settings
=
db
.
get_currency_settings
()
currency_code
=
currency_settings
.
get
(
'currency_code'
,
'EUR'
)
wallet
=
db
.
get_user_wallet
(
user_id
)
or
{
'balance'
:
'0.00'
,
'currency_code'
:
currency_code
,
'auto_topup_enabled'
:
False
}
try
:
from
aisbf.payments.wallet.manager
import
WalletManager
wallet_manager
=
WalletManager
(
db
)
wallet
=
await
wallet_manager
.
get_wallet
(
user_id
)
except
Exception
:
wallet
=
{
'balance'
:
'0.00'
,
'currency_code'
:
currency_code
,
'auto_topup_enabled'
:
False
}
return
templates
.
TemplateResponse
(
request
=
request
,
...
...
setup.py
View file @
59a08356
...
...
@@ -230,6 +230,8 @@ setup(
'templates/dashboard/add_payment_method.html'
,
'templates/dashboard/paypal_connect.html'
,
'templates/dashboard/cache_settings.html'
,
'templates/dashboard/wallet.html'
,
'templates/dashboard/error.html'
,
]),
# Install static files (extension and favicon)
(
'share/aisbf/static'
,
[
...
...
templates/dashboard/error.html
0 → 100644
View file @
59a08356
{% extends "base.html" %}
{% block title %}Error{% endblock %}
{% block content %}
<div
class=
"container mt-5"
>
<div
class=
"row justify-content-center"
>
<div
class=
"col-md-6"
>
<div
class=
"card"
>
<div
class=
"card-header bg-danger text-white"
>
<h5
class=
"mb-0"
>
Error
</h5>
</div>
<div
class=
"card-body"
>
<div
class=
"alert alert-danger"
>
{{ error }}
</div>
<div
class=
"d-flex justify-content-between"
>
<a
href=
"{{ request.url_for('dashboard_index') }}"
class=
"btn btn-primary"
>
<i
class=
"fas fa-home"
></i>
Go to Dashboard
</a>
<button
onclick=
"history.back()"
class=
"btn btn-outline-secondary"
>
<i
class=
"fas fa-arrow-left"
></i>
Go Back
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
templates/dashboard/wallet.html
View file @
59a08356
{% extends "
dashboard/
base.html" %}
{% extends "base.html" %}
{% block title %}Wallet{% endblock %}
{% block content %}
<div
class=
"container mt-4"
>
<h1
class=
"mb-4"
>
Wallet
</h1>
<div
class=
"row"
>
<div
class=
"col-md-6"
>
<div
class=
"card mb-4"
>
<div
class=
"card-header"
>
<h5>
Balance
</h5>
</div>
<div
class=
"card-body"
>
<h2
class=
"display-4"
>
${{ wallet.balance }}
</h2>
<p
class=
"text-muted"
>
Currency: {{ wallet.currency_code }}
</p>
<hr>
<div
class=
"mb-3"
>
<label
class=
"form-label"
>
Top Up Amount
</label>
<div
class=
"btn-group w-100 mb-2"
role=
"group"
>
<button
type=
"button"
class=
"btn btn-outline-primary amount-btn"
data-amount=
"10"
>
$10
</button>
<button
type=
"button"
class=
"btn btn-outline-primary amount-btn"
data-amount=
"15"
>
$15
</button>
<button
type=
"button"
class=
"btn btn-outline-primary amount-btn"
data-amount=
"20"
>
$20
</button>
<button
type=
"button"
class=
"btn btn-outline-primary amount-btn"
data-amount=
"50"
>
$50
</button>
<button
type=
"button"
class=
"btn btn-outline-primary amount-btn"
data-amount=
"100"
>
$100
</button>
</div>
<input
type=
"number"
id=
"custom-amount"
class=
"form-control"
placeholder=
"Custom amount (5-500)"
step=
"0.01"
min=
"5"
max=
"500"
>
</div>
<div
class=
"d-grid gap-2"
>
<button
id=
"topup-stripe"
class=
"btn btn-primary"
>
Top Up with Stripe
</button>
<button
id=
"topup-paypal"
class=
"btn btn-outline-primary"
>
Top Up with PayPal
</button>
</div>
</div>
<h2
style=
"margin-bottom: 20px;"
><i
class=
"fas fa-wallet me-2"
></i>
Wallet
</h2>
<!-- Balance Banner -->
<div
style=
"background: linear-gradient(135deg, #1a4a2e, #0f3460); border: 2px solid #28a745; border-radius: 8px; padding: 24px; margin-bottom: 20px;"
>
<div
style=
"display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;"
>
<div>
<div
style=
"color: #a0a0a0; font-size: 14px; margin-bottom: 6px;"
>
Available Balance
</div>
<div
style=
"font-size: 42px; font-weight: bold; color: #4ade80;"
>
${{ "%.2f"|format(wallet.balance|float) }}
</div>
<div
style=
"color: #a0a0a0; font-size: 13px; margin-top: 6px;"
>
Currency:
<span
style=
"color: #e0e0e0;"
>
{{ wallet.currency_code }}
</span>
·
Wallet ID:
<span
style=
"color: #e0e0e0; font-family: monospace;"
>
{{ wallet.id }}
</span>
</div>
<div
class=
"card"
>
<div
class=
"card-header"
>
<h5>
Auto Top Up Settings
</h5>
</div>
<div
class=
"card-body"
>
<div
class=
"form-check form-switch mb-3"
>
<input
class=
"form-check-input"
type=
"checkbox"
id=
"auto-topup-enabled"
{%
if
wallet
.
auto_topup_enabled
%}
checked
{%
endif
%}
>
<label
class=
"form-check-label"
for=
"auto-topup-enabled"
>
Enable Auto Top Up
</label>
</div>
<div
id=
"auto-topup-settings"
{%
if
not
wallet
.
auto_topup_enabled
%}
style=
"display:none;"
{%
endif
%}
>
<div
class=
"mb-3"
>
<label
class=
"form-label"
>
Auto Top Up Amount
</label>
<input
type=
"number"
id=
"auto-topup-amount"
class=
"form-control"
value=
"{{ wallet.auto_topup_amount or '' }}"
step=
"0.01"
min=
"10"
>
</div>
<div
class=
"mb-3"
>
<label
class=
"form-label"
>
Top Up When Balance Below
</label>
<input
type=
"number"
id=
"auto-topup-threshold"
class=
"form-control"
value=
"{{ wallet.auto_topup_threshold or '' }}"
step=
"0.01"
min=
"1"
>
</div>
<div
class=
"mb-3"
>
<label
class=
"form-label"
>
Payment Method
</label>
<select
id=
"auto-topup-payment-method"
class=
"form-select"
>
<!-- Options populated via JS -->
</select>
</div>
<button
id=
"save-auto-topup"
class=
"btn btn-primary"
>
Save Settings
</button>
</div>
</div>
</div>
<div
style=
"text-align: right;"
>
{% if wallet.auto_topup_enabled %}
<span
style=
"background: #28a745; color: white; padding: 4px 12px; border-radius: 10px; font-size: 13px;"
>
<i
class=
"fas fa-sync-alt me-1"
></i>
Auto Top-Up Active
</span>
{% else %}
<span
style=
"background: #6c757d; color: white; padding: 4px 12px; border-radius: 10px; font-size: 13px;"
>
<i
class=
"fas fa-sync-alt me-1"
></i>
Auto Top-Up Off
</span>
{% endif %}
</div>
</div>
</div>
<div
style=
"display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;"
>
<!-- Top Up -->
<div
style=
"background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px;"
>
<h3
style=
"margin: 0 0 18px 0; color: #4a9eff;"
>
<i
class=
"fas fa-plus-circle me-2"
></i>
Top Up Wallet
</h3>
{% set fiat_gateways = enabled_gateways.keys() | select('in', ['stripe', 'paypal']) | list %}
{% set crypto_gateways = enabled_gateways.keys() | select('in', ['bitcoin', 'ethereum', 'usdt', 'usdc']) | list %}
{% if not enabled_gateways %}
<div
style=
"color: #a0a0a0; text-align: center; padding: 20px 10px;"
>
<i
class=
"fas fa-info-circle fa-2x"
style=
"margin-bottom: 12px; display: block; color: #4a9eff;"
></i>
No payment methods are currently enabled.
<br>
<span
style=
"font-size: 13px;"
>
Please contact the administrator to enable a payment gateway.
</span>
</div>
{% else %}
{% if fiat_gateways %}
<div
style=
"margin-bottom: 16px;"
>
<div
style=
"color: #a0a0a0; font-size: 13px; margin-bottom: 8px;"
>
Quick amounts
</div>
<div
style=
"display: flex; gap: 8px; flex-wrap: wrap;"
>
{% for amt in [10, 15, 20, 50, 100] %}
<button
type=
"button"
class=
"amount-btn"
data-amount=
"{{ amt }}"
style=
"background: #1a1a2e; border: 1px solid #4a9eff; color: #4a9eff; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all .15s;"
>
${{ amt }}
</button>
{% endfor %}
</div>
</div>
<div
style=
"margin-bottom: 16px;"
>
<label
style=
"display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;"
>
Custom amount
</label>
<input
type=
"number"
id=
"custom-amount"
style=
"width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; color: #e0e0e0; font-size: 15px;"
placeholder=
"Enter amount ($5 – $500)"
step=
"0.01"
min=
"5"
max=
"500"
>
</div>
<div
style=
"display: flex; flex-direction: column; gap: 10px; {% if crypto_gateways %}margin-bottom: 16px;{% endif %}"
>
{% for name in fiat_gateways %}
{% if name == 'stripe' %}
<button
class=
"topup-fiat-btn"
data-method=
"stripe"
style=
"background: #635bff; border: none; color: white; padding: 11px; border-radius: 6px; cursor: pointer; font-size: 15px; font-weight: 600; transition: background .15s;"
>
<i
class=
"fab fa-cc-stripe me-2"
></i>
Top Up with Stripe
</button>
{% elif name == 'paypal' %}
<button
class=
"topup-fiat-btn"
data-method=
"paypal"
style=
"background: #003087; border: 1px solid #009cde; color: white; padding: 11px; border-radius: 6px; cursor: pointer; font-size: 15px; font-weight: 600; transition: background .15s;"
>
<i
class=
"fab fa-paypal me-2"
></i>
Top Up with PayPal
</button>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if crypto_gateways %}
{% if fiat_gateways %}
<hr
style=
"border-color: #0f3460; margin: 4px 0 14px 0;"
>
{% endif %}
<div
style=
"color: #a0a0a0; font-size: 13px; margin-bottom: 10px;"
>
<i
class=
"fas fa-coins me-1"
></i>
Crypto deposit — send to address below
</div>
{% for name in crypto_gateways %}
{% set cfg = enabled_gateways[name] %}
{% set address = cfg.get('wallet_address') or cfg.get('address') or '' %}
<div
style=
"background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; padding: 12px; margin-bottom: 10px;"
>
<div
style=
"display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;"
>
<span
style=
"color: #e0e0e0; font-weight: 600;"
>
{% if name == 'bitcoin' %}
<i
class=
"fab fa-bitcoin me-1"
style=
"color:#f7931a;"
></i>
{% elif name == 'ethereum' %}
<i
class=
"fab fa-ethereum me-1"
style=
"color:#627eea;"
></i>
{% else %}
<i
class=
"fas fa-coins me-1"
style=
"color:#26a17b;"
></i>
{% endif %}
{{ name | upper }}
</span>
<button
onclick=
"copyAddress('{{ address }}', this)"
style=
"background: #4a9eff; border: none; color: white; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;"
>
<i
class=
"fas fa-copy me-1"
></i>
Copy
</button>
</div>
<div
style=
"font-family: monospace; font-size: 12px; color: #a0a0a0; word-break: break-all; background: #0f1631; padding: 8px; border-radius: 4px;"
>
{{ address or 'Address not configured' }}
</div>
<div
style=
"color: #6c757d; font-size: 11px; margin-top: 6px;"
>
Send {{ name | upper }} to this address. Balance will be credited after on-chain confirmation.
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
</div>
<!-- Auto Top-Up -->
<div
style=
"background: #16213e; border: 2px solid #17a2b8; border-radius: 8px; padding: 20px;"
>
<h3
style=
"margin: 0 0 18px 0; color: #17a2b8;"
>
<i
class=
"fas fa-sync-alt me-2"
></i>
Auto Top-Up Settings
</h3>
<div
style=
"display: flex; align-items: center; gap: 12px; margin-bottom: 20px;"
>
<div
style=
"position: relative; width: 48px; height: 26px; flex-shrink: 0;"
>
<input
type=
"checkbox"
id=
"auto-topup-enabled"
{%
if
wallet
.
auto_topup_enabled
%}
checked
{%
endif
%}
style=
"opacity: 0; width: 0; height: 0; position: absolute;"
>
<label
for=
"auto-topup-enabled"
id=
"toggle-label"
style=
"position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #6c757d; border-radius: 26px; transition: .3s;"
>
<span
id=
"toggle-knob"
style=
"position: absolute; height: 20px; width: 20px; left: 3px; bottom: 3px; background: white; border-radius: 50%; transition: .3s;"
></span>
</label>
</div>
<span
style=
"color: #e0e0e0; font-size: 15px;"
>
Enable Auto Top-Up
</span>
</div>
<div
class=
"col-md-6"
>
<div
class=
"card"
>
<div
class=
"card-header"
>
<h5>
Transaction History
</h5>
<div
id=
"auto-topup-settings"
{%
if
not
wallet
.
auto_topup_enabled
%}
style=
"display:none;"
{%
endif
%}
>
<div
style=
"margin-bottom: 14px;"
>
<label
style=
"display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;"
>
Top-up amount
</label>
<div
style=
"position: relative;"
>
<span
style=
"position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: #4ade80; font-weight: bold;"
>
$
</span>
<input
type=
"number"
id=
"auto-topup-amount"
style=
"width: 100%; padding: 10px 10px 10px 26px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; color: #e0e0e0; font-size: 15px;"
value=
"{{ wallet.auto_topup_amount or '' }}"
step=
"0.01"
min=
"10"
>
</div>
<div
class=
"card-body p-0"
>
<div
class=
"table-responsive"
>
<table
class=
"table table-striped mb-0"
>
<thead>
<tr>
<th>
Date
</th>
<th>
Type
</th>
<th>
Amount
</th>
<th>
Status
</th>
</tr>
</thead>
<tbody
id=
"transaction-list"
>
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
<div
style=
"margin-bottom: 14px;"
>
<label
style=
"display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;"
>
Trigger when balance falls below
</label>
<div
style=
"position: relative;"
>
<span
style=
"position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: #ffc107; font-weight: bold;"
>
$
</span>
<input
type=
"number"
id=
"auto-topup-threshold"
style=
"width: 100%; padding: 10px 10px 10px 26px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; color: #e0e0e0; font-size: 15px;"
value=
"{{ wallet.auto_topup_threshold or '' }}"
step=
"0.01"
min=
"1"
>
</div>
</div>
<div
style=
"margin-bottom: 18px;"
>
<label
style=
"display: block; color: #a0a0a0; font-size: 13px; margin-bottom: 6px;"
>
Payment method
</label>
<select
id=
"auto-topup-payment-method"
style=
"width: 100%; padding: 10px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 6px; color: #e0e0e0; font-size: 14px;"
>
{% for name in enabled_gateways.keys() | select('in', ['stripe', 'paypal']) %}
<option
value=
"{{ name }}"
{%
if
wallet
.
auto_topup_payment_method =
=
name
%}
selected
{%
endif
%}
>
{{ name | capitalize }}
</option>
{% endfor %}
</select>
</div>
<button
id=
"save-auto-topup"
style=
"background: #17a2b8; border: none; color: white; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background .15s;"
>
<i
class=
"fas fa-save me-2"
></i>
Save Settings
</button>
</div>
{% if not wallet.auto_topup_enabled %}
<div
style=
"color: #a0a0a0; font-size: 13px; line-height: 1.6;"
>
Enable auto top-up to automatically reload your wallet when the balance drops below a threshold. This ensures uninterrupted API access.
</div>
{% endif %}
</div>
</div>
<!-- Transaction History -->
<div
style=
"background: #16213e; border: 2px solid #6f42c1; border-radius: 8px; padding: 20px;"
>
<h3
style=
"margin: 0 0 20px 0; color: #a57bff;"
>
<i
class=
"fas fa-history me-2"
></i>
Transaction History
</h3>
<div
style=
"overflow-x: auto;"
>
<table
style=
"width: 100%; border-collapse: collapse;"
>
<thead>
<tr
style=
"background: #0f3460; color: #e0e0e0;"
>
<th
style=
"padding: 12px 14px; text-align: left; font-weight: 600; border-bottom: 1px solid #1a1a2e;"
>
Date
</th>
<th
style=
"padding: 12px 14px; text-align: left; font-weight: 600; border-bottom: 1px solid #1a1a2e;"
>
Type
</th>
<th
style=
"padding: 12px 14px; text-align: left; font-weight: 600; border-bottom: 1px solid #1a1a2e;"
>
Description
</th>
<th
style=
"padding: 12px 14px; text-align: right; font-weight: 600; border-bottom: 1px solid #1a1a2e;"
>
Amount
</th>
<th
style=
"padding: 12px 14px; text-align: center; font-weight: 600; border-bottom: 1px solid #1a1a2e;"
>
Status
</th>
</tr>
</thead>
<tbody
id=
"transaction-list"
>
<tr
id=
"tx-loading"
>
<td
colspan=
"5"
style=
"padding: 40px; text-align: center; color: #a0a0a0;"
>
<i
class=
"fas fa-spinner fa-spin fa-2x"
></i>
<div
style=
"margin-top: 10px;"
>
Loading transactions…
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<style>
.amount-btn
:hover
,
.amount-btn.active
{
background
:
#4a9eff
!important
;
color
:
#fff
!important
;
border-color
:
#4a9eff
!important
;
}
.topup-fiat-btn
[
data-method
=
"stripe"
]
:hover:not
(
:disabled
)
{
background
:
#4f49d0
!important
;
}
.topup-fiat-btn
[
data-method
=
"paypal"
]
:hover:not
(
:disabled
)
{
background
:
#00256b
!important
;
}
#save-auto-topup
:hover
{
background
:
#138496
!important
;
}
#toggle-label
.on
{
background
:
#17a2b8
!important
;
}
#toggle-label
.on
#toggle-knob
{
transform
:
translateX
(
22px
);
}
</style>
<script>
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
// ── Amount buttons ──────────────────────────────────────────
let
selectedAmount
=
null
;
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
btn
=>
{
btn
.
addEventListener
(
'click'
,
function
()
{
btn
.
addEventListener
(
'click'
,
function
()
{
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
b
=>
b
.
classList
.
remove
(
'active'
));
this
.
classList
.
add
(
'active'
);
selectedAmount
=
this
.
dataset
.
amount
;
selectedAmount
=
parseFloat
(
this
.
dataset
.
amount
)
;
document
.
getElementById
(
'custom-amount'
).
value
=
''
;
});
});
document
.
getElementById
(
'custom-amount'
).
addEventListener
(
'input'
,
function
()
{
document
.
getElementById
(
'custom-amount'
).
addEventListener
(
'input'
,
function
()
{
document
.
querySelectorAll
(
'.amount-btn'
).
forEach
(
b
=>
b
.
classList
.
remove
(
'active'
));
selectedAmount
=
this
.
value
;
selectedAmount
=
parseFloat
(
this
.
value
)
||
null
;
});
document
.
getElementById
(
'auto-topup-enabled'
).
addEventListener
(
'change'
,
function
()
{
document
.
getElementById
(
'auto-topup-settings'
).
style
.
display
=
this
.
checked
?
'block'
:
'none'
;
function
getAmount
()
{
const
custom
=
parseFloat
(
document
.
getElementById
(
'custom-amount'
).
value
);
return
custom
||
selectedAmount
;
}
// ── Fiat top-up buttons ─────────────────────────────────────
document
.
querySelectorAll
(
'.topup-fiat-btn'
).
forEach
(
btn
=>
{
btn
.
addEventListener
(
'click'
,
function
()
{
const
amount
=
getAmount
();
if
(
!
amount
||
amount
<
5
||
amount
>
500
)
{
alert
(
'Please select or enter an amount between $5 and $500.'
);
return
;
}
const
method
=
this
.
dataset
.
method
;
const
orig
=
this
.
innerHTML
;
this
.
disabled
=
true
;
this
.
innerHTML
=
'<i class="fas fa-spinner fa-spin me-2"></i>Processing…'
;
fetch
(
'/dashboard/wallet/topup'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
({
amount
,
payment_method
:
method
})
})
.
then
(
r
=>
r
.
json
())
.
then
(
data
=>
{
if
(
data
.
type
===
'stripe'
&&
data
.
checkout_url
)
{
window
.
location
.
href
=
data
.
checkout_url
;
}
else
if
(
data
.
type
===
'paypal'
&&
data
.
approval_url
)
{
window
.
location
.
href
=
data
.
approval_url
;
}
else
{
this
.
disabled
=
false
;
this
.
innerHTML
=
orig
;
alert
(
data
.
error
||
'Failed to initiate checkout.'
);
}
})
.
catch
(()
=>
{
this
.
disabled
=
false
;
this
.
innerHTML
=
orig
;
alert
(
'Network error. Please try again.'
);
});
});
});
// ── Auto top-up toggle ──────────────────────────────────────
const
toggle
=
document
.
getElementById
(
'auto-topup-enabled'
);
const
label
=
document
.
getElementById
(
'toggle-label'
);
const
settings
=
document
.
getElementById
(
'auto-topup-settings'
);
function
syncToggle
()
{
if
(
toggle
.
checked
)
{
label
.
classList
.
add
(
'on'
);
label
.
style
.
background
=
'#17a2b8'
;
document
.
getElementById
(
'toggle-knob'
).
style
.
transform
=
'translateX(22px)'
;
settings
.
style
.
display
=
'block'
;
}
else
{
label
.
classList
.
remove
(
'on'
);
label
.
style
.
background
=
'#6c757d'
;
document
.
getElementById
(
'toggle-knob'
).
style
.
transform
=
'translateX(0)'
;
settings
.
style
.
display
=
'none'
;
}
}
syncToggle
();
toggle
.
addEventListener
(
'change'
,
syncToggle
);
// ── Save auto top-up ────────────────────────────────────────
document
.
getElementById
(
'save-auto-topup'
).
addEventListener
(
'click'
,
function
()
{
const
payload
=
{
enabled
:
toggle
.
checked
,
topup_amount
:
parseFloat
(
document
.
getElementById
(
'auto-topup-amount'
).
value
)
||
null
,
threshold_amount
:
parseFloat
(
document
.
getElementById
(
'auto-topup-threshold'
).
value
)
||
null
,
payment_method
:
document
.
getElementById
(
'auto-topup-payment-method'
).
value
||
null
};
fetch
(
'/api/wallet/auto-topup'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
},
body
:
JSON
.
stringify
(
payload
)
})
.
then
(
r
=>
r
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
this
.
textContent
=
'✓ Saved'
;
setTimeout
(()
=>
{
this
.
innerHTML
=
'<i class="fas fa-save me-2"></i>Save Settings'
;
},
2000
);
}
else
{
alert
(
data
.
error
||
'Failed to save settings.'
);
}
})
.
catch
(()
=>
alert
(
'Network error. Please try again.'
));
});
// Load transactions
fetch
(
'/api/wallet/transactions'
)
.
then
(
res
=>
res
.
json
())
// ── Transaction history ─────────────────────────────────────
const
typeLabels
=
{
credit
:
'<span style="color:#4ade80;">▲ Credit</span>'
,
debit
:
'<span style="color:#f87171;">▼ Debit</span>'
,
refund
:
'<span style="color:#60a5fa;">↩ Refund</span>'
,
topup
:
'<span style="color:#4ade80;">+ Top-Up</span>'
,
payment
:
'<span style="color:#f87171;">− Payment</span>'
,
};
const
statusBadge
=
{
completed
:
'background:#28a745;color:white;'
,
pending
:
'background:#ffc107;color:black;'
,
failed
:
'background:#dc3545;color:white;'
,
refunded
:
'background:#6c757d;color:white;'
,
};
fetch
(
'/dashboard/wallet/transactions'
)
.
then
(
r
=>
r
.
json
())
.
then
(
transactions
=>
{
const
list
=
document
.
getElementById
(
'transaction-list'
);
transactions
.
forEach
(
tx
=>
{
document
.
getElementById
(
'tx-loading'
).
remove
();
if
(
!
transactions
||
transactions
.
length
===
0
)
{
list
.
innerHTML
=
`<tr><td colspan="5" style="padding:40px;text-align:center;color:#a0a0a0;">
<i class="fas fa-receipt fa-3x" style="margin-bottom:12px;display:block;"></i>
No transactions yet.
</td></tr>`
;
return
;
}
transactions
.
forEach
((
tx
,
i
)
=>
{
const
date
=
tx
.
created_at
?
new
Date
(
tx
.
created_at
)
:
null
;
const
dateStr
=
date
?
date
.
toLocaleDateString
()
:
'—'
;
const
timeStr
=
date
?
date
.
toLocaleTimeString
([],
{
hour
:
'2-digit'
,
minute
:
'2-digit'
})
:
''
;
const
typeKey
=
(
tx
.
type
||
''
).
toLowerCase
();
const
typeHtml
=
typeLabels
[
typeKey
]
||
`<span style="color:#e0e0e0;">
${
tx
.
type
||
'—'
}
</span>`
;
const
amt
=
parseFloat
(
tx
.
amount
)
||
0
;
const
amtColor
=
amt
>=
0
?
'#4ade80'
:
'#f87171'
;
const
amtStr
=
(
amt
>=
0
?
'+'
:
''
)
+
'$'
+
Math
.
abs
(
amt
).
toFixed
(
2
);
const
status
=
(
tx
.
status
||
'unknown'
).
toLowerCase
();
const
badgeStyle
=
statusBadge
[
status
]
||
'background:#6c757d;color:white;'
;
const
rowBg
=
i
%
2
===
0
?
'#1a1a2e'
:
'#16213e'
;
const
row
=
document
.
createElement
(
'tr'
);
row
.
style
.
cssText
=
`background:
${
rowBg
}
;border-bottom:1px solid #0f3460;`
;
row
.
innerHTML
=
`
<td>
${
new
Date
(
tx
.
created_at
).
toLocaleDateString
()}
</td>
<td>
${
tx
.
type
}
</td>
<td class="
${
tx
.
amount
>=
0
?
'text-success'
:
'text-danger'
}
">
${
tx
.
amount
>=
0
?
'+'
:
''
}
$
${
Math
.
abs
(
tx
.
amount
)}
</td>
<td><span class="badge bg-secondary">
${
tx
.
status
}
</span></td>
`
;
<td style="padding:12px 14px;color:#e0e0e0;">
<div style="font-weight:600;">
${
dateStr
}
</div>
<small style="color:#a0a0a0;">
${
timeStr
}
</small>
</td>
<td style="padding:12px 14px;">
${
typeHtml
}
</td>
<td style="padding:12px 14px;color:#c0c0c0;font-size:13px;">
${
tx
.
description
||
'—'
}
</td>
<td style="padding:12px 14px;text-align:right;font-weight:700;font-size:16px;color:
${
amtColor
}
;">
${
amtStr
}
</td>
<td style="padding:12px 14px;text-align:center;">
<span style="
${
badgeStyle
}
padding:4px 10px;border-radius:10px;font-size:12px;white-space:nowrap;">
${
tx
.
status
||
'unknown'
}
</span>
</td>`
;
list
.
appendChild
(
row
);
});
})
.
catch
(()
=>
{
document
.
getElementById
(
'tx-loading'
).
innerHTML
=
'<td colspan="5" style="padding:30px;text-align:center;color:#f87171;">Failed to load transactions.</td>'
;
});
});
function
copyAddress
(
addr
,
btn
)
{
navigator
.
clipboard
.
writeText
(
addr
).
then
(()
=>
{
const
orig
=
btn
.
innerHTML
;
btn
.
innerHTML
=
'<i class="fas fa-check me-1"></i>Copied!'
;
btn
.
style
.
background
=
'#28a745'
;
setTimeout
(()
=>
{
btn
.
innerHTML
=
orig
;
btn
.
style
.
background
=
''
;
},
2000
);
}).
catch
(()
=>
alert
(
'Copy failed — please copy the address manually.'
));
}
</script>
{% endblock %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment