mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
feat(settings): add Settings modal with configurable DM retry parameters
Replace hardcoded DM retry logic with user-configurable settings stored in app_settings DB. Settings modal opens from menu with tab-based UI (ready for future settings tabs). Defaults: 3 direct + 1 flood retries (was 8+2), 30s/60s intervals, 60s grace period. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+23
-9
@@ -1089,21 +1089,32 @@ class DeviceManager:
|
||||
"""Background retry with same timestamp for dedup on receiver.
|
||||
|
||||
Strategy depends on whether contact has a known DIRECT path:
|
||||
- DIRECT path known: up to 10 attempts (8 DIRECT + 2 FLOOD), 30s wait
|
||||
- No path (FLOOD): up to 3 attempts, 60s wait
|
||||
- DIRECT path known: direct_max_retries DIRECT + direct_flood_retries FLOOD
|
||||
- No path (FLOOD): flood_max_retries attempts
|
||||
Settings loaded from app_settings DB table (key: dm_retry_settings).
|
||||
"""
|
||||
from meshcore.events import EventType
|
||||
|
||||
# Load configurable retry settings from DB
|
||||
_defaults = {
|
||||
'direct_max_retries': 3, 'direct_flood_retries': 1,
|
||||
'flood_max_retries': 3, 'direct_interval': 30,
|
||||
'flood_interval': 60, 'grace_period': 60,
|
||||
}
|
||||
saved = self.db.get_setting_json('dm_retry_settings', {})
|
||||
cfg = {**_defaults, **(saved or {})}
|
||||
|
||||
has_path = contact.get('out_path_len', -1) > 0
|
||||
|
||||
if has_path:
|
||||
max_attempts = 10
|
||||
flood_at = 8 # reset path to flood at this attempt
|
||||
min_wait = 30.0 # seconds between DIRECT attempts
|
||||
# +1 counts the initial send
|
||||
max_attempts = cfg['direct_max_retries'] + cfg['direct_flood_retries'] + 1
|
||||
flood_at = cfg['direct_max_retries'] + 1
|
||||
min_wait = float(cfg['direct_interval'])
|
||||
else:
|
||||
max_attempts = 3
|
||||
flood_at = None # already flood, no reset needed
|
||||
min_wait = 60.0 # seconds between FLOOD attempts
|
||||
max_attempts = cfg['flood_max_retries'] + 1
|
||||
flood_at = None
|
||||
min_wait = float(cfg['flood_interval'])
|
||||
|
||||
wait_s = max(suggested_timeout / 1000 * 1.2, min_wait)
|
||||
mode = "DIRECT" if has_path else "FLOOD"
|
||||
@@ -1124,6 +1135,9 @@ class DeviceManager:
|
||||
# Retry with same timestamp, incrementing attempt
|
||||
for attempt in range(1, max_attempts):
|
||||
if flood_at and attempt >= flood_at:
|
||||
# Switch to FLOOD mode: reset path and use flood interval
|
||||
min_wait = float(cfg['flood_interval'])
|
||||
wait_s = max(suggested_timeout / 1000 * 1.2, min_wait)
|
||||
try:
|
||||
await self.mc.commands.reset_path(contact)
|
||||
logger.info(f"DM retry {attempt}: reset path to flood")
|
||||
@@ -1161,7 +1175,7 @@ class DeviceManager:
|
||||
logger.warning(f"DM retry exhausted ({max_attempts} {mode} attempts) for dm_id={dm_id}")
|
||||
# Keep pending acks for grace period so late ACKs can still be matched
|
||||
self._retry_tasks.pop(dm_id, None)
|
||||
await asyncio.sleep(60)
|
||||
await asyncio.sleep(cfg['grace_period'])
|
||||
stale = [k for k, v in self._pending_acks.items() if v == dm_id]
|
||||
if stale:
|
||||
for k in stale:
|
||||
|
||||
@@ -443,26 +443,6 @@ def check_dm_delivery(ack_codes: list) -> Tuple[bool, Dict, str]:
|
||||
return False, {}, str(e)
|
||||
|
||||
|
||||
def get_retry_ack_codes() -> set:
|
||||
"""Get retry ACK codes. Simplified in v2."""
|
||||
return set()
|
||||
|
||||
|
||||
def get_auto_retry_config() -> Tuple[bool, Dict]:
|
||||
"""Get auto-retry config. Using meshcore library's built-in retry."""
|
||||
return True, {
|
||||
'enabled': True,
|
||||
'max_attempts': 3,
|
||||
'max_flood': 2,
|
||||
'note': 'v2 uses meshcore library built-in retry (send_msg_with_retry)'
|
||||
}
|
||||
|
||||
|
||||
def set_auto_retry_config(enabled=None, max_attempts=None, max_flood=None) -> Tuple[bool, Dict]:
|
||||
"""Set auto-retry config. Stub in v2."""
|
||||
return get_auto_retry_config()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Device Settings
|
||||
# =============================================================================
|
||||
|
||||
+55
-14
@@ -257,6 +257,42 @@ def save_retention_settings(retention_settings: dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DM Retry Settings
|
||||
# =============================================================================
|
||||
|
||||
DM_RETRY_DEFAULTS = {
|
||||
'direct_max_retries': 3, # DIRECT retries before switching to FLOOD
|
||||
'direct_flood_retries': 1, # FLOOD retries after DIRECT exhausted
|
||||
'flood_max_retries': 3, # FLOOD retries when no path known
|
||||
'direct_interval': 30, # seconds between DIRECT retries
|
||||
'flood_interval': 60, # seconds between FLOOD retries
|
||||
'grace_period': 60, # seconds to wait for late ACKs after exhaustion
|
||||
}
|
||||
|
||||
|
||||
def get_dm_retry_settings() -> dict:
|
||||
"""Get DM retry settings from database."""
|
||||
db = _get_db()
|
||||
if db:
|
||||
saved = db.get_setting_json('dm_retry_settings', {})
|
||||
return {**DM_RETRY_DEFAULTS, **saved}
|
||||
return dict(DM_RETRY_DEFAULTS)
|
||||
|
||||
|
||||
def save_dm_retry_settings(settings: dict) -> bool:
|
||||
"""Save DM retry settings to database."""
|
||||
db = _get_db()
|
||||
if not db:
|
||||
return False
|
||||
try:
|
||||
db.set_setting_json('dm_retry_settings', settings)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save DM retry settings: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@api_bp.route('/messages', methods=['GET'])
|
||||
def get_messages():
|
||||
"""
|
||||
@@ -2071,32 +2107,37 @@ def send_dm_message():
|
||||
|
||||
@api_bp.route('/dm/auto_retry', methods=['GET'])
|
||||
def get_auto_retry_config():
|
||||
"""Get auto-retry configuration."""
|
||||
"""Get DM retry settings."""
|
||||
try:
|
||||
success, data = cli.get_auto_retry_config()
|
||||
if success:
|
||||
return jsonify(data), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to get config'}), 500
|
||||
return jsonify(get_dm_retry_settings()), 200
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/dm/auto_retry', methods=['POST'])
|
||||
def set_auto_retry_config():
|
||||
"""Update auto-retry configuration."""
|
||||
"""Update DM retry settings."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': 'Missing JSON body'}), 400
|
||||
|
||||
success, result = cli.set_auto_retry_config(
|
||||
enabled=data.get('enabled'),
|
||||
max_attempts=data.get('max_attempts'),
|
||||
max_flood=data.get('max_flood')
|
||||
)
|
||||
if success:
|
||||
return jsonify(result), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to update config'}), 500
|
||||
# Validate numeric fields
|
||||
valid_keys = set(DM_RETRY_DEFAULTS.keys())
|
||||
settings = {}
|
||||
for key in valid_keys:
|
||||
if key in data:
|
||||
val = data[key]
|
||||
if not isinstance(val, (int, float)) or val < 0:
|
||||
return jsonify({'success': False, 'error': f'Invalid value for {key}'}), 400
|
||||
settings[key] = int(val)
|
||||
|
||||
if not settings:
|
||||
return jsonify({'success': False, 'error': 'No valid settings provided'}), 400
|
||||
|
||||
if save_dm_retry_settings(settings):
|
||||
return jsonify({**get_dm_retry_settings(), 'success': True}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to save settings'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@@ -1637,6 +1637,95 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('statsTabBtn')?.addEventListener('shown.bs.tab', loadDeviceStats);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Settings Modal
|
||||
// =============================================================================
|
||||
|
||||
const DM_RETRY_DEFAULTS = {
|
||||
direct_max_retries: 3,
|
||||
direct_flood_retries: 1,
|
||||
flood_max_retries: 3,
|
||||
direct_interval: 30,
|
||||
flood_interval: 60,
|
||||
grace_period: 60
|
||||
};
|
||||
|
||||
const DM_RETRY_FIELDS = {
|
||||
direct_max_retries: 'settDirectMaxRetries',
|
||||
direct_flood_retries: 'settDirectFloodRetries',
|
||||
flood_max_retries: 'settFloodMaxRetries',
|
||||
direct_interval: 'settDirectInterval',
|
||||
flood_interval: 'settFloodInterval',
|
||||
grace_period: 'settGracePeriod'
|
||||
};
|
||||
|
||||
function populateDmRetryForm(data) {
|
||||
for (const [key, elId] of Object.entries(DM_RETRY_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
if (el) el.value = data[key] ?? DM_RETRY_DEFAULTS[key];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDmRetrySettings() {
|
||||
try {
|
||||
const resp = await fetch('/api/dm/auto_retry');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
populateDmRetryForm(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load DM retry settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDmRetrySettings() {
|
||||
const payload = {};
|
||||
for (const [key, elId] of Object.entries(DM_RETRY_FIELDS)) {
|
||||
const el = document.getElementById(elId);
|
||||
const val = parseInt(el.value, 10);
|
||||
if (isNaN(val) || val < parseInt(el.min) || val > parseInt(el.max)) {
|
||||
showNotification(`Invalid value for ${el.previousElementSibling?.textContent || key}`, 'danger');
|
||||
el.focus();
|
||||
return;
|
||||
}
|
||||
payload[key] = val;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/dm/auto_retry', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (resp.ok) {
|
||||
showNotification('Settings saved', 'success');
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
showNotification(err.error || 'Failed to save', 'danger');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Failed to save settings', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const settingsModal = document.getElementById('settingsModal');
|
||||
if (settingsModal) {
|
||||
settingsModal.addEventListener('show.bs.modal', loadDmRetrySettings);
|
||||
}
|
||||
|
||||
const dmRetryForm = document.getElementById('dmRetrySettingsForm');
|
||||
if (dmRetryForm) {
|
||||
dmRetryForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveDmRetrySettings();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('settingsResetBtn')?.addEventListener('click', () => {
|
||||
populateDmRetryForm(DM_RETRY_DEFAULTS);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Cleanup inactive contacts
|
||||
*/
|
||||
|
||||
+70
-2
@@ -165,11 +165,11 @@
|
||||
<small class="d-block text-muted">Database backup & restore</small>
|
||||
</div>
|
||||
</button>
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="settingsBtn" disabled>
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" data-bs-toggle="modal" data-bs-target="#settingsModal" data-bs-dismiss="offcanvas">
|
||||
<i class="bi bi-gear" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<span>Settings</span>
|
||||
<small class="d-block text-muted">Coming soon</small>
|
||||
<small class="d-block text-muted">Application settings</small>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -334,6 +334,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-gear"></i> Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabSettingsMessages" type="button">Messages</button>
|
||||
</li>
|
||||
<!-- Future tabs added here -->
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="tabSettingsMessages">
|
||||
<div id="settingsMessagesContent">
|
||||
<form id="dmRetrySettingsForm">
|
||||
<h6 class="text-muted mb-2">When path is known (DIRECT)</h6>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Direct retries</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settDirectMaxRetries" min="0" max="20" value="3">
|
||||
<div class="form-text">Attempts via known path before switching to flood</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Flood retries after direct</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settDirectFloodRetries" min="0" max="5" value="1">
|
||||
<div class="form-text">Flood attempts after direct retries exhausted</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label mb-0 small">Interval (seconds)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settDirectInterval" min="5" max="300" value="30">
|
||||
<div class="form-text">Wait between direct retries</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted mb-2">When no path (FLOOD)</h6>
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-0 small">Max retries</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settFloodMaxRetries" min="0" max="10" value="3">
|
||||
<div class="form-text">Flood attempts when no path is known</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label mb-0 small">Interval (seconds)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settFloodInterval" min="5" max="300" value="60">
|
||||
<div class="form-text">Wait between flood retries</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted mb-2">Other</h6>
|
||||
<div class="mb-3">
|
||||
<label class="form-label mb-0 small">Grace period (seconds)</label>
|
||||
<input type="number" class="form-control form-control-sm" id="settGracePeriod" min="10" max="300" value="60">
|
||||
<div class="form-text">Wait for late ACKs after all retries exhausted</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="settingsResetBtn">Reset to defaults</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Modal (Leaflet) -->
|
||||
<div class="modal fade" id="mapModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
|
||||
Reference in New Issue
Block a user