mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-03 22:31:18 +00:00
feat: zombie radio banner on all pages + zombie alert config screen options
- base.html: persistent danger banner appears on every web page when is_radio_zombie is true; shows datetime zombie was detected; includes "Restart Bot Processing" button (POST /api/admin/zombie-recover) that clears _radio_zombie_detected and _radio_fail_count on the live bot object and removes the persisted DB flag; banner turns green on success - config.html: new "Zombie Radio Alert" card with enable/disable toggle and alert-email field; "Save" writes to bot_metadata (immediate, survives restarts); "Save to config.ini" also persists values to config.ini and keeps the in-memory config in sync; card shows current config.ini values as baseline defaults - app.py: inject_template_vars context processor now provides radio_zombie and radio_zombie_since to all templates; added GET/POST /api/config/zombie-alert endpoints (GET returns both bot_metadata and config.ini values; POST supports write_to_config flag); added POST /api/admin/zombie-recover endpoint; stored config_path on self for write-back use - scheduler.py: send_zombie_alert_email now prefers bot_metadata (zombie.alert_enabled, zombie.alert_email) over config.ini so web UI changes take effect without a restart; uses isinstance(..., str) guard so mock/None values safely fall through to config.ini defaults
This commit is contained in:
+22
-3
@@ -1201,7 +1201,18 @@ class MessageScheduler:
|
||||
import ssl as _ssl
|
||||
from email.message import EmailMessage
|
||||
|
||||
if not self.bot.config.getboolean('Bot', 'radio_zombie_alert_enabled', fallback=True):
|
||||
# Prefer bot_metadata (set via web UI) over config.ini so the config page
|
||||
# takes effect without requiring a config.ini edit or bot restart.
|
||||
# Use isinstance(…, str) so a missing/mock value safely falls through.
|
||||
try:
|
||||
alert_enabled_meta = self.bot.db_manager.get_metadata('zombie.alert_enabled')
|
||||
except Exception:
|
||||
alert_enabled_meta = None
|
||||
if isinstance(alert_enabled_meta, str) and alert_enabled_meta:
|
||||
alert_enabled = alert_enabled_meta.lower() == 'true'
|
||||
else:
|
||||
alert_enabled = self.bot.config.getboolean('Bot', 'radio_zombie_alert_enabled', fallback=True)
|
||||
if not alert_enabled:
|
||||
return
|
||||
|
||||
smtp_host = self._get_notif('smtp_host')
|
||||
@@ -1211,8 +1222,16 @@ class MessageScheduler:
|
||||
from_name = self._get_notif('from_name') or 'MeshCore Bot'
|
||||
from_email = self._get_notif('from_email')
|
||||
|
||||
# Alert recipients: dedicated config key, falls back to nightly recipients
|
||||
alert_email_cfg = self.bot.config.get('Bot', 'radio_zombie_alert_email', fallback='').strip()
|
||||
# Alert recipients: bot_metadata first, then config.ini, then nightly recipients
|
||||
try:
|
||||
_email_meta = self.bot.db_manager.get_metadata('zombie.alert_email')
|
||||
alert_email_meta = _email_meta.strip() if isinstance(_email_meta, str) else ''
|
||||
except Exception:
|
||||
alert_email_meta = ''
|
||||
alert_email_cfg = (
|
||||
alert_email_meta
|
||||
or self.bot.config.get('Bot', 'radio_zombie_alert_email', fallback='').strip()
|
||||
)
|
||||
if alert_email_cfg:
|
||||
recipients = [r.strip() for r in alert_email_cfg.split(',') if r.strip()]
|
||||
else:
|
||||
|
||||
+112
-1
@@ -142,6 +142,7 @@ class BotDataViewer:
|
||||
|
||||
# Load configuration
|
||||
self.config = self._load_config(config_path)
|
||||
self.config_path = config_path # kept for config.ini write-back endpoints
|
||||
|
||||
# Resolve db_path relative to the config file's directory — matches core.py's bot_root
|
||||
# property which is Path(config_file).parent.resolve(). Using self.bot_root (the project
|
||||
@@ -277,15 +278,30 @@ class BotDataViewer:
|
||||
bot_name = (self.config.get('Bot', 'bot_name', fallback='MeshCore Bot') or '').strip() or 'MeshCore Bot'
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
bot_name = 'MeshCore Bot'
|
||||
try:
|
||||
radio_zombie = self.db_manager.get_metadata('bot.radio_zombie') == 'true'
|
||||
radio_zombie_since = self.db_manager.get_metadata('bot.radio_zombie_since') or None
|
||||
except Exception:
|
||||
radio_zombie = False
|
||||
radio_zombie_since = None
|
||||
return {
|
||||
'greeter_enabled': greeter_enabled,
|
||||
'feed_manager_enabled': feed_manager_enabled,
|
||||
'bot_name': bot_name,
|
||||
'version_info': version_info,
|
||||
'radio_zombie': radio_zombie,
|
||||
'radio_zombie_since': radio_zombie_since,
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.exception("Template context processor failed: %s", e)
|
||||
return {'greeter_enabled': False, 'feed_manager_enabled': False, 'bot_name': 'MeshCore Bot', 'version_info': version_info}
|
||||
return {
|
||||
'greeter_enabled': False,
|
||||
'feed_manager_enabled': False,
|
||||
'bot_name': 'MeshCore Bot',
|
||||
'version_info': version_info,
|
||||
'radio_zombie': False,
|
||||
'radio_zombie_since': None,
|
||||
}
|
||||
|
||||
def _init_databases(self):
|
||||
"""Initialize database connections"""
|
||||
@@ -1488,6 +1504,101 @@ class BotDataViewer:
|
||||
self.logger.info(f"Maintenance config updated: {', '.join(saved)}")
|
||||
return jsonify({'success': True, 'saved': saved})
|
||||
|
||||
# ── Zombie radio alert config ────────────────────────────────────────
|
||||
|
||||
@self.app.route('/api/config/zombie-alert')
|
||||
def api_config_zombie_alert_get() -> "Response":
|
||||
"""Return zombie alert settings.
|
||||
|
||||
Response includes both ``bot_metadata`` values (set via web UI) and
|
||||
``config_ini`` values (read from config.ini) so the browser can
|
||||
show config.ini as the baseline defaults.
|
||||
"""
|
||||
meta: dict[str, str] = {}
|
||||
for key in ('zombie.alert_enabled', 'zombie.alert_email'):
|
||||
short = key.split('.', 1)[1]
|
||||
val = self.db_manager.get_metadata(key)
|
||||
meta[short] = val if isinstance(val, str) else ''
|
||||
if not meta.get('alert_enabled'):
|
||||
meta['alert_enabled'] = 'false'
|
||||
ini: dict[str, str] = {
|
||||
'alert_enabled': (
|
||||
'true'
|
||||
if self.config.getboolean('Bot', 'radio_zombie_alert_enabled', fallback=False)
|
||||
else 'false'
|
||||
),
|
||||
'alert_email': self.config.get('Bot', 'radio_zombie_alert_email', fallback=''),
|
||||
}
|
||||
return jsonify({'meta': meta, 'config_ini': ini})
|
||||
|
||||
@self.app.route('/api/config/zombie-alert', methods=['POST'])
|
||||
def api_config_zombie_alert_post() -> "Response":
|
||||
"""Save zombie alert settings to bot_metadata.
|
||||
|
||||
If ``write_to_config`` is ``true`` in the request body, the values
|
||||
are also written back to config.ini under ``[Bot]``. The config
|
||||
object in memory is updated immediately so the scheduler reads the
|
||||
new values without a restart.
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
allowed = {'alert_enabled', 'alert_email'}
|
||||
saved = []
|
||||
for field in allowed:
|
||||
if field in data:
|
||||
self.db_manager.set_metadata(f'zombie.{field}', str(data[field]))
|
||||
saved.append(field)
|
||||
self.logger.info("Zombie alert config updated (metadata): %s", ', '.join(saved))
|
||||
|
||||
write_to_config = str(data.get('write_to_config', '')).lower() == 'true'
|
||||
config_saved = False
|
||||
if write_to_config:
|
||||
try:
|
||||
if not self.config.has_section('Bot'):
|
||||
self.config.add_section('Bot')
|
||||
if 'alert_enabled' in data:
|
||||
self.config.set(
|
||||
'Bot', 'radio_zombie_alert_enabled',
|
||||
'true' if str(data['alert_enabled']).lower() == 'true' else 'false',
|
||||
)
|
||||
if 'alert_email' in data:
|
||||
self.config.set('Bot', 'radio_zombie_alert_email', str(data['alert_email']))
|
||||
with open(self.config_path, 'w') as fh:
|
||||
self.config.write(fh)
|
||||
config_saved = True
|
||||
self.logger.info("Zombie alert settings written to config.ini")
|
||||
except OSError as exc:
|
||||
self.logger.error("Failed to write zombie alert settings to config.ini: %s", exc)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Could not write config.ini — check file permissions',
|
||||
}), 500
|
||||
|
||||
return jsonify({'success': True, 'saved': saved, 'config_saved': config_saved})
|
||||
|
||||
# ── Zombie recover ───────────────────────────────────────────────────
|
||||
|
||||
@self.app.route('/api/admin/zombie-recover', methods=['POST'])
|
||||
def api_admin_zombie_recover() -> "Response":
|
||||
"""Clear zombie state so bot resumes processing after a radio power cycle.
|
||||
|
||||
Clears the ``_radio_zombie_detected`` flag on the live bot object (if
|
||||
accessible) and removes the persisted flag from bot_metadata so the
|
||||
web-viewer banner disappears on the next page load.
|
||||
"""
|
||||
try:
|
||||
self.db_manager.set_metadata('bot.radio_zombie', 'false')
|
||||
self.db_manager.set_metadata('bot.radio_zombie_since', '')
|
||||
bot = getattr(self, 'bot', None)
|
||||
if bot is not None:
|
||||
bot._radio_zombie_detected = False
|
||||
bot._radio_fail_count = 0
|
||||
bot._last_radio_probe = 0 # force probe on next cycle
|
||||
self.logger.info("Zombie state cleared via web UI recover action")
|
||||
return jsonify({'success': True, 'message': 'Zombie state cleared; bot will resume'})
|
||||
except Exception:
|
||||
self.logger.exception("Error clearing zombie state")
|
||||
return jsonify({'success': False, 'error': 'Internal error — see server logs'}), 500
|
||||
|
||||
# ── Maintenance status ───────────────────────────────────────────────
|
||||
|
||||
@self.app.route('/api/maintenance/backup_now', methods=['POST'])
|
||||
|
||||
@@ -455,6 +455,63 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Zombie Radio Banner (persistent — shown on all pages when radio is in zombie state) -->
|
||||
{% if radio_zombie %}
|
||||
<div id="zombie-banner" class="alert alert-danger mb-0 rounded-0 border-0 border-bottom border-danger" role="alert" style="border-width: 2px !important;">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<i class="fas fa-skull-crossbones me-2"></i>
|
||||
<strong>Radio Zombie State Detected</strong> — The bot radio firmware is unresponsive.
|
||||
A physical <strong>power cycle</strong> of the radio hardware is required.
|
||||
Disconnect/reconnect will <em>not</em> fix this.
|
||||
{% if radio_zombie_since %}
|
||||
<span class="ms-3 text-danger-emphasis small opacity-75">
|
||||
<i class="fas fa-clock me-1"></i>Since: {{ radio_zombie_since }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center flex-shrink-0">
|
||||
<span class="small opacity-75">After power cycling:</span>
|
||||
<button id="zombie-recover-btn" class="btn btn-warning btn-sm fw-semibold"
|
||||
onclick="zombieRecover()" type="button">
|
||||
<i class="fas fa-power-off me-1"></i>Restart Bot Processing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function zombieRecover() {
|
||||
var btn = document.getElementById('zombie-recover-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Restarting\u2026';
|
||||
fetch('/api/admin/zombie-recover', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var banner = document.getElementById('zombie-banner');
|
||||
if (data.success) {
|
||||
banner.className = 'alert alert-success mb-0 rounded-0 border-0 border-bottom border-success';
|
||||
banner.innerHTML = '<div class="container-fluid"><i class="fas fa-check-circle me-2"></i>' +
|
||||
'<strong>Reconnect scheduled.</strong> Refresh this page in a few seconds to confirm the radio is back online.</div>';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-power-off me-1"></i>Restart Bot Processing';
|
||||
alert('Error: ' + (data.error || 'Unknown error — check server logs'));
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-power-off me-1"></i>Restart Bot Processing';
|
||||
alert('Network error — could not reach the server.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container-fluid mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user