From 815284289ecace0f9bb52d5f759899702909803d Mon Sep 17 00:00:00 2001 From: zwitschi Date: Tue, 2 Jun 2026 16:07:30 +0200 Subject: [PATCH] feat: add configuration management UI for fees, pairings, and application settings --- DEPLOYMENT.md | 50 +++--- src/arbitrade/config/service.py | 54 ++++++- src/arbitrade/storage/repositories.py | 15 ++ .../web/templates/dashboard/config_fees.html | 106 ++++++++++++ .../web/templates/dashboard/config_pairs.html | 153 ++++++++++++++++++ .../templates/dashboard/config_settings.html | 52 ++++++ 6 files changed, 397 insertions(+), 33 deletions(-) create mode 100644 src/arbitrade/web/templates/dashboard/config_fees.html create mode 100644 src/arbitrade/web/templates/dashboard/config_pairs.html create mode 100644 src/arbitrade/web/templates/dashboard/config_settings.html diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 480ed0c..91842f6 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -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. 2. Choose deployment source: - -- Public repo: use `Public repository` and provide HTTPS URL. -- Private Gitea repo: use deploy key flow from the Gitea guide. + - Public repo: use `Public repository` and provide HTTPS URL. + - Private Gitea repo: use deploy key flow from the Gitea guide. 3. Set repository URL for this project: - -- `https://git.allucanget.biz/allucanget/arbitrade.git` (public) -- or SSH URL if private deploy key is used. + - `https://git.allucanget.biz/allucanget/arbitrade.git` (public) + - or SSH URL if private deploy key is used. 4. Choose build pack: - -- Prefer `Dockerfile` for this repo, because Docker build behavior is explicitly defined. -- Use `Nixpacks` only if you intentionally want auto-detected build logic. + - Prefer `Dockerfile` for this repo, because Docker build behavior is explicitly defined. + - Use `Nixpacks` only if you intentionally want auto-detected build logic. 5. Configure branch and base directory: - -- Branch: your deploy branch (for example `main`) -- Base directory: `/` + - Branch: your deploy branch (for example `main`) + - Base directory: `/` 6. Configure network: - -- Exposed port: `9090` -- Domain: set your Coolify domain/custom domain + - Exposed port: `9090` + - Domain: set your Coolify domain/custom domain 7. Configure environment variables and secrets from the Common Runtime Configuration section. 8. Add persistent storage mount `/app/data`. 9. Configure health check: - -- Path: `/health` -- Ensure container includes `curl` or `wget` if using UI-defined checks. + - Path: `/health` + - Ensure container includes `curl` or `wget` if using UI-defined checks. 10. Click Deploy and verify: - -- Deployment logs complete successfully. -- `GET /health` returns success. + - Deployment logs complete successfully. + - `GET /health` returns success. Optional (Git webhook auto-deploy with Gitea): @@ -113,23 +106,20 @@ Image: 2. In Coolify, select Create New Resource. 3. Choose Application deployment based on Docker Image. 4. Set image reference: - -- Registry: `git.allucanget.biz` -- Image: `git.allucanget.biz/allucanget/arbitrade:latest` + - Registry: `git.allucanget.biz` + - Image: `git.allucanget.biz/allucanget/arbitrade:latest` 5. Configure registry credentials in Coolify if your registry requires auth. 6. Leave build/install/start commands empty unless you need overrides. 7. Set network and health: - -- Exposed port: `9090` -- Health check path: `/health` + - Exposed port: `9090` + - Health check path: `/health` 8. Add environment variables and secrets from the Common Runtime Configuration section. 9. Add persistent storage mount `/app/data`. 10. Deploy and verify: - -- Logs show container start success. -- `GET /health` returns success. + - Logs show container start success. + - `GET /health` returns success. Update flow for new releases: diff --git a/src/arbitrade/config/service.py b/src/arbitrade/config/service.py index 4f79b6d..de22c67 100644 --- a/src/arbitrade/config/service.py +++ b/src/arbitrade/config/service.py @@ -64,6 +64,7 @@ class ConfigurationService: self._audit_repo = audit_repo self._config_version = 0 self._loaded_settings: dict[str, Any] = {} + self._last_updated_at: datetime | None = None self._load_database_settings() def _load_database_settings(self) -> None: @@ -95,9 +96,11 @@ class ConfigurationService: self._loaded_settings[setting.key] = parsed_value - # Merge with environment settings (environment takes precedence) - # This is a simplified approach - in reality we'd want to merge properly - # but for now we'll just load DB settings and let environment override them + # Track the latest update time + if db_settings: + 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 self._initialize_default_settings() @@ -112,6 +115,38 @@ class ConfigurationService: """Get a configuration setting value.""" 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: """Set a configuration setting value and persist to database.""" # Import here to avoid circular imports @@ -160,3 +195,16 @@ class ConfigurationService: else: # Create new 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() diff --git a/src/arbitrade/storage/repositories.py b/src/arbitrade/storage/repositories.py index 7f64805..d137398 100644 --- a/src/arbitrade/storage/repositories.py +++ b/src/arbitrade/storage/repositories.py @@ -590,6 +590,21 @@ class ConfigSettingRepository: 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: def __init__(self, store: DuckDBStore) -> None: diff --git a/src/arbitrade/web/templates/dashboard/config_fees.html b/src/arbitrade/web/templates/dashboard/config_fees.html new file mode 100644 index 0000000..e7aeb7f --- /dev/null +++ b/src/arbitrade/web/templates/dashboard/config_fees.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} {% block title %}Fee Configuration{% endblock %} {% +block content %} +
+
+
+

Fee Configuration

+ + {% if message %} + + {% endif %} + +
+
+
Configure Pairing Fees
+
+
+
+
+ + + + + + + + + + + + {% for pairing in pairings %} + + + + + + + + {% endfor %} + +
PairingCrypto/Crypto MakerCrypto/Crypto TakerCrypto/Fiat MakerCrypto/Fiat Taker
+ {{ pairing.base_asset }}/{{ pairing.quote_asset }} + + + + + + + + + +
+
+ +
+
+
+
+
+
+{% endblock %} diff --git a/src/arbitrade/web/templates/dashboard/config_pairs.html b/src/arbitrade/web/templates/dashboard/config_pairs.html new file mode 100644 index 0000000..5ae9d5d --- /dev/null +++ b/src/arbitrade/web/templates/dashboard/config_pairs.html @@ -0,0 +1,153 @@ +{% extends "base.html" %} {% block title %}Currency Pairings{% endblock %} {% +block content %} +
+
+
+

Currency Pairings

+ + {% if message %} + + {% endif %} + + +
+
+
Create New Pairing
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+
+
Existing Pairings
+
+
+
+ + + + + + + + + + + + {% for pairing in pairings %} + + + + + + + + {% endfor %} + +
Base AssetQuote AssetEnabledSourceActions
{{ pairing.base_asset }}{{ pairing.quote_asset }} + {% if pairing.enabled %} + Yes + {% else %} + No + {% endif %} + {{ pairing.source or '—' }} +
+ + + +
+
+
+
+
+
+
+
+{% endblock %} diff --git a/src/arbitrade/web/templates/dashboard/config_settings.html b/src/arbitrade/web/templates/dashboard/config_settings.html new file mode 100644 index 0000000..6ed28fa --- /dev/null +++ b/src/arbitrade/web/templates/dashboard/config_settings.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} {% block title %}Application Settings{% endblock %} {% +block content %} +
+
+
+

Application Settings

+ + {% if message %} + + {% endif %} + +
+
+
Configure Settings
+
+
+
+ {% for section_name, settings in settings_by_section.items() %} +
+
{{ section_name }}
+ {% for setting in settings %} +
+ + +
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %}