feat: add configuration management UI for fees, pairings, and application settings
This commit is contained in:
+20
-30
@@ -58,41 +58,34 @@ Recommended when you want Coolify to build from source and optionally auto-deplo
|
|||||||
|
|
||||||
1. Open your Coolify project and select Create New Resource.
|
1. Open your Coolify project and select Create New Resource.
|
||||||
2. Choose deployment source:
|
2. Choose deployment source:
|
||||||
|
- Public repo: use `Public repository` and provide HTTPS URL.
|
||||||
- Public repo: use `Public repository` and provide HTTPS URL.
|
- Private Gitea repo: use deploy key flow from the Gitea guide.
|
||||||
- Private Gitea repo: use deploy key flow from the Gitea guide.
|
|
||||||
|
|
||||||
3. Set repository URL for this project:
|
3. Set repository URL for this project:
|
||||||
|
- `https://git.allucanget.biz/allucanget/arbitrade.git` (public)
|
||||||
- `https://git.allucanget.biz/allucanget/arbitrade.git` (public)
|
- or SSH URL if private deploy key is used.
|
||||||
- or SSH URL if private deploy key is used.
|
|
||||||
|
|
||||||
4. Choose build pack:
|
4. Choose build pack:
|
||||||
|
- Prefer `Dockerfile` for this repo, because Docker build behavior is explicitly defined.
|
||||||
- Prefer `Dockerfile` for this repo, because Docker build behavior is explicitly defined.
|
- Use `Nixpacks` only if you intentionally want auto-detected build logic.
|
||||||
- Use `Nixpacks` only if you intentionally want auto-detected build logic.
|
|
||||||
|
|
||||||
5. Configure branch and base directory:
|
5. Configure branch and base directory:
|
||||||
|
- Branch: your deploy branch (for example `main`)
|
||||||
- Branch: your deploy branch (for example `main`)
|
- Base directory: `/`
|
||||||
- Base directory: `/`
|
|
||||||
|
|
||||||
6. Configure network:
|
6. Configure network:
|
||||||
|
- Exposed port: `9090`
|
||||||
- Exposed port: `9090`
|
- Domain: set your Coolify domain/custom domain
|
||||||
- Domain: set your Coolify domain/custom domain
|
|
||||||
|
|
||||||
7. Configure environment variables and secrets from the Common Runtime Configuration section.
|
7. Configure environment variables and secrets from the Common Runtime Configuration section.
|
||||||
8. Add persistent storage mount `/app/data`.
|
8. Add persistent storage mount `/app/data`.
|
||||||
9. Configure health check:
|
9. Configure health check:
|
||||||
|
- Path: `/health`
|
||||||
- Path: `/health`
|
- Ensure container includes `curl` or `wget` if using UI-defined checks.
|
||||||
- Ensure container includes `curl` or `wget` if using UI-defined checks.
|
|
||||||
|
|
||||||
10. Click Deploy and verify:
|
10. Click Deploy and verify:
|
||||||
|
- Deployment logs complete successfully.
|
||||||
- Deployment logs complete successfully.
|
- `GET /health` returns success.
|
||||||
- `GET /health` returns success.
|
|
||||||
|
|
||||||
Optional (Git webhook auto-deploy with Gitea):
|
Optional (Git webhook auto-deploy with Gitea):
|
||||||
|
|
||||||
@@ -113,23 +106,20 @@ Image:
|
|||||||
2. In Coolify, select Create New Resource.
|
2. In Coolify, select Create New Resource.
|
||||||
3. Choose Application deployment based on Docker Image.
|
3. Choose Application deployment based on Docker Image.
|
||||||
4. Set image reference:
|
4. Set image reference:
|
||||||
|
- Registry: `git.allucanget.biz`
|
||||||
- Registry: `git.allucanget.biz`
|
- Image: `git.allucanget.biz/allucanget/arbitrade:latest`
|
||||||
- Image: `git.allucanget.biz/allucanget/arbitrade:latest`
|
|
||||||
|
|
||||||
5. Configure registry credentials in Coolify if your registry requires auth.
|
5. Configure registry credentials in Coolify if your registry requires auth.
|
||||||
6. Leave build/install/start commands empty unless you need overrides.
|
6. Leave build/install/start commands empty unless you need overrides.
|
||||||
7. Set network and health:
|
7. Set network and health:
|
||||||
|
- Exposed port: `9090`
|
||||||
- Exposed port: `9090`
|
- Health check path: `/health`
|
||||||
- Health check path: `/health`
|
|
||||||
|
|
||||||
8. Add environment variables and secrets from the Common Runtime Configuration section.
|
8. Add environment variables and secrets from the Common Runtime Configuration section.
|
||||||
9. Add persistent storage mount `/app/data`.
|
9. Add persistent storage mount `/app/data`.
|
||||||
10. Deploy and verify:
|
10. Deploy and verify:
|
||||||
|
- Logs show container start success.
|
||||||
- Logs show container start success.
|
- `GET /health` returns success.
|
||||||
- `GET /health` returns success.
|
|
||||||
|
|
||||||
Update flow for new releases:
|
Update flow for new releases:
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class ConfigurationService:
|
|||||||
self._audit_repo = audit_repo
|
self._audit_repo = audit_repo
|
||||||
self._config_version = 0
|
self._config_version = 0
|
||||||
self._loaded_settings: dict[str, Any] = {}
|
self._loaded_settings: dict[str, Any] = {}
|
||||||
|
self._last_updated_at: datetime | None = None
|
||||||
self._load_database_settings()
|
self._load_database_settings()
|
||||||
|
|
||||||
def _load_database_settings(self) -> None:
|
def _load_database_settings(self) -> None:
|
||||||
@@ -95,9 +96,11 @@ class ConfigurationService:
|
|||||||
|
|
||||||
self._loaded_settings[setting.key] = parsed_value
|
self._loaded_settings[setting.key] = parsed_value
|
||||||
|
|
||||||
# Merge with environment settings (environment takes precedence)
|
# Track the latest update time
|
||||||
# This is a simplified approach - in reality we'd want to merge properly
|
if db_settings:
|
||||||
# but for now we'll just load DB settings and let environment override them
|
latest_updated = max(
|
||||||
|
setting.updated_at for setting in db_settings if setting.updated_at)
|
||||||
|
self._last_updated_at = latest_updated
|
||||||
|
|
||||||
# Initialize with default values from settings model
|
# Initialize with default values from settings model
|
||||||
self._initialize_default_settings()
|
self._initialize_default_settings()
|
||||||
@@ -112,6 +115,38 @@ class ConfigurationService:
|
|||||||
"""Get a configuration setting value."""
|
"""Get a configuration setting value."""
|
||||||
return self._loaded_settings.get(key, default)
|
return self._loaded_settings.get(key, default)
|
||||||
|
|
||||||
|
def get_config_version(self) -> int:
|
||||||
|
"""Get the current configuration version for hot-reloading."""
|
||||||
|
return self._config_version
|
||||||
|
|
||||||
|
def get_last_updated_at(self) -> datetime | None:
|
||||||
|
"""Get the timestamp of the last configuration update."""
|
||||||
|
return self._last_updated_at
|
||||||
|
|
||||||
|
def is_config_outdated(self) -> bool:
|
||||||
|
"""Check if configuration has been updated since last load."""
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from arbitrade.storage.repositories import ConfigSettingRepository
|
||||||
|
setting_repo = ConfigSettingRepository(self._store)
|
||||||
|
|
||||||
|
# Get the latest update timestamp from database
|
||||||
|
latest_db_update = setting_repo.get_latest_updated_at()
|
||||||
|
|
||||||
|
# Compare with our last loaded timestamp
|
||||||
|
if latest_db_update and self._last_updated_at:
|
||||||
|
return latest_db_update > self._last_updated_at
|
||||||
|
elif latest_db_update:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reload_if_changed(self) -> bool:
|
||||||
|
"""Reload configuration if it has been updated in the database."""
|
||||||
|
if self.is_config_outdated():
|
||||||
|
self._load_database_settings()
|
||||||
|
self._config_version += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def set_setting(self, key: str, value: Any, updated_by: str | None = None) -> None:
|
def set_setting(self, key: str, value: Any, updated_by: str | None = None) -> None:
|
||||||
"""Set a configuration setting value and persist to database."""
|
"""Set a configuration setting value and persist to database."""
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
@@ -160,3 +195,16 @@ class ConfigurationService:
|
|||||||
else:
|
else:
|
||||||
# Create new setting
|
# Create new setting
|
||||||
updated_setting = setting_repo.create_setting(setting)
|
updated_setting = setting_repo.create_setting(setting)
|
||||||
|
|
||||||
|
# Update in-memory cache
|
||||||
|
self._loaded_settings[key] = value
|
||||||
|
|
||||||
|
# Update version for hot reloading
|
||||||
|
self._config_version += 1
|
||||||
|
|
||||||
|
# Update last updated timestamp
|
||||||
|
self._last_updated_at = updated_setting.updated_at
|
||||||
|
|
||||||
|
def get_all_settings(self) -> dict[str, Any]:
|
||||||
|
"""Get all configuration settings."""
|
||||||
|
return self._loaded_settings.copy()
|
||||||
|
|||||||
@@ -590,6 +590,21 @@ class ConfigSettingRepository:
|
|||||||
for row in cursor.fetchall()
|
for row in cursor.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_latest_updated_at(self) -> datetime | None:
|
||||||
|
"""Get the latest updated_at timestamp from config_settings table."""
|
||||||
|
with self._store.connect() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT MAX(updated_at) as latest_updated_at
|
||||||
|
FROM config_settings
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
# Convert string timestamp to datetime
|
||||||
|
return datetime.fromisoformat(row[0].replace('Z', '+00:00'))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ConfigPairingRepository:
|
class ConfigPairingRepository:
|
||||||
def __init__(self, store: DuckDBStore) -> None:
|
def __init__(self, store: DuckDBStore) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Fee Configuration{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>Fee Configuration</h2>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="alert"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Configure Pairing Fees</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/dashboard/config/fees/save">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pairing</th>
|
||||||
|
<th>Crypto/Crypto Maker</th>
|
||||||
|
<th>Crypto/Crypto Taker</th>
|
||||||
|
<th>Crypto/Fiat Maker</th>
|
||||||
|
<th>Crypto/Fiat Taker</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pairing in pairings %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ pairing.base_asset }}/{{ pairing.quote_asset }}
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="pairing_id_{{ pairing.id }}"
|
||||||
|
value="{{ pairing.id }}"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
min="0"
|
||||||
|
max="0.05"
|
||||||
|
class="form-control"
|
||||||
|
name="maker_fee_{{ pairing.id }}"
|
||||||
|
placeholder="0.0010"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
min="0"
|
||||||
|
max="0.05"
|
||||||
|
class="form-control"
|
||||||
|
name="taker_fee_{{ pairing.id }}"
|
||||||
|
placeholder="0.0020"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
min="0"
|
||||||
|
max="0.05"
|
||||||
|
class="form-control"
|
||||||
|
name="maker_fee_{{ pairing.id }}_fiat"
|
||||||
|
placeholder="0.0010"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
min="0"
|
||||||
|
max="0.05"
|
||||||
|
class="form-control"
|
||||||
|
name="taker_fee_{{ pairing.id }}_fiat"
|
||||||
|
placeholder="0.0020"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Fees</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Currency Pairings{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>Currency Pairings</h2>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="alert"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Create Pairing Form -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Create New Pairing</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/dashboard/config/pairs/create">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="base_asset" class="form-label">Base Asset</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="base_asset"
|
||||||
|
name="base_asset"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="quote_asset" class="form-label"
|
||||||
|
>Quote Asset</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="quote_asset"
|
||||||
|
name="quote_asset"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="source" class="form-label">Source</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="source"
|
||||||
|
name="source"
|
||||||
|
placeholder="e.g., Kraken, Binance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="enabled"
|
||||||
|
name="enabled"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="enabled">Enabled</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
Create Pairing
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing Pairings Table -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Existing Pairings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Base Asset</th>
|
||||||
|
<th>Quote Asset</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pairing in pairings %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ pairing.base_asset }}</td>
|
||||||
|
<td>{{ pairing.quote_asset }}</td>
|
||||||
|
<td>
|
||||||
|
{% if pairing.enabled %}
|
||||||
|
<span class="badge bg-success">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ pairing.source or '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="/dashboard/config/pairs/delete"
|
||||||
|
style="display: inline"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="base_asset"
|
||||||
|
value="{{ pairing.base_asset }}"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="quote_asset"
|
||||||
|
value="{{ pairing.quote_asset }}"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
onclick="
|
||||||
|
return confirm(
|
||||||
|
'Are you sure you want to delete this pairing?',
|
||||||
|
);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Application Settings{% endblock %} {%
|
||||||
|
block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2>Application Settings</h2>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="alert"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Configure Settings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/dashboard/config/settings/save">
|
||||||
|
{% for section_name, settings in settings_by_section.items() %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>{{ section_name }}</h5>
|
||||||
|
{% for setting in settings %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="setting_{{ setting.key }}" class="form-label"
|
||||||
|
>{{ setting.key }}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="setting_{{ setting.key }}"
|
||||||
|
name="setting_{{ setting.key }}"
|
||||||
|
value="{{ setting.value_json }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user