From debb711b7142c614bda8f11077e788054fd43535 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Wed, 1 Jul 2026 21:56:55 +0200 Subject: [PATCH] feat(analyzer): seed Letsmesh as a normal DB row and flip switch UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Letsmesh Analyzer is now a regular analyzers row, seeded once via an app_settings flag on first startup. It can be renamed, disabled, or deleted like any user entry. The frontend drops the special built-in pseudo-row and the built-in slot in the chooser. Click resolution updated: 0 enabled → toast "No analyzer configured"; 1 enabled → open directly; multiple with a default → open default; multiple without default → chooser. The row switch and the edit modal now show "Enabled" (checked = active), which matches intuition. The DB column remains is_disabled so no data migration is needed — the UI simply inverts the value on read/write. Co-Authored-By: Claude Opus 4.7 --- app/database.py | 19 +++++++++++++ app/device_manager.py | 1 - app/main.py | 3 ++ app/routes/api.py | 5 ++-- app/static/js/app.js | 63 ++++++++++++++--------------------------- app/templates/base.html | 4 +-- 6 files changed, 48 insertions(+), 47 deletions(-) diff --git a/app/database.py b/app/database.py index a7835af..b2d4433 100644 --- a/app/database.py +++ b/app/database.py @@ -558,6 +558,25 @@ class Database: # Analyzers (user-configured MeshCore Analyzer services) # ================================================================ + LETSMESH_ANALYZER_URL_TEMPLATE = 'https://analyzer.letsmesh.net/packets?packet_hash={packetHash}' + LETSMESH_ANALYZER_NAME = 'Letsmesh Analyzer' + _LETSMESH_SEED_FLAG = 'analyzer_letsmesh_seeded' + + def seed_default_analyzers(self) -> None: + """Seed the Letsmesh Analyzer row on first startup after this feature was added. + + Uses an app_settings flag so the seed happens exactly once per installation. + If the user later renames, disables or deletes the row, we won't re-create it. + """ + if self.get_setting(self._LETSMESH_SEED_FLAG) is not None: + return + with self._connect() as conn: + conn.execute( + "INSERT OR IGNORE INTO analyzers (name, url_template) VALUES (?, ?)", + (self.LETSMESH_ANALYZER_NAME, self.LETSMESH_ANALYZER_URL_TEMPLATE) + ) + self.set_setting(self._LETSMESH_SEED_FLAG, '1') + def create_analyzer(self, name: str, url_template: str) -> int: """Insert a new analyzer. Raises sqlite3.IntegrityError on duplicate name.""" with self._connect() as conn: diff --git a/app/device_manager.py b/app/device_manager.py index f6e78bb..60fe016 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -20,7 +20,6 @@ from urllib.parse import urlparse, parse_qs from Crypto.Cipher import AES -LETSMESH_ANALYZER_URL_TEMPLATE = 'https://analyzer.letsmesh.net/packets?packet_hash={packetHash}' GRP_TXT_TYPE_BYTE = 0x05 logger = logging.getLogger(__name__) diff --git a/app/main.py b/app/main.py index 11220d3..2f4c4dd 100644 --- a/app/main.py +++ b/app/main.py @@ -253,6 +253,9 @@ def create_app(): db = Database(db_path) app.db = db + # One-time seed: add Letsmesh Analyzer row so it behaves like any other entry. + db.seed_default_analyzers() + # Migrate settings from .webui_settings.json to DB (one-time) settings_file = Path(config.MC_CONFIG_DIR) / ".webui_settings.json" if settings_file.exists() and db.get_setting('manual_add_contacts') is None: diff --git a/app/routes/api.py b/app/routes/api.py index ef31e55..df5693b 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -20,7 +20,7 @@ from flask import Blueprint, jsonify, request, send_file, current_app from app.meshcore import cli, parser from app.meshcore.regions import derive_scope_key_hex, is_valid_region_name from app.config import config, runtime_config -from app.device_manager import decode_path_len, LETSMESH_ANALYZER_URL_TEMPLATE +from app.device_manager import decode_path_len from app.archiver import manager as archive_manager from app.contacts_cache import get_all_names, get_all_contacts @@ -4257,7 +4257,7 @@ def _validate_analyzer_url_template(url_template: str): @api_bp.route('/analyzers', methods=['GET']) def list_analyzers_api(): - """List user-configured analyzers and the built-in Letsmesh URL template.""" + """List user-configured analyzers.""" try: db = _get_db() if not db: @@ -4265,7 +4265,6 @@ def list_analyzers_api(): return jsonify({ 'success': True, 'analyzers': db.list_analyzers(), - 'letsmesh_url_template': LETSMESH_ANALYZER_URL_TEMPLATE, }), 200 except Exception as e: logger.error(f"Error listing analyzers: {e}") diff --git a/app/static/js/app.js b/app/static/js/app.js index 6464164..aae4ea6 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -3160,7 +3160,6 @@ async function clearDefaultRegion() { const ANALYZER_PLACEHOLDER = '{packetHash}'; window.analyzerCache = window.analyzerCache || { analyzers: [], - letsmesh_url_template: 'https://analyzer.letsmesh.net/packets?packet_hash={packetHash}', loaded: false, }; @@ -3185,9 +3184,6 @@ async function loadAnalyzers() { const data = await resp.json(); if (!data.success) throw new Error(data.error || 'Failed'); window.analyzerCache.analyzers = data.analyzers || []; - if (data.letsmesh_url_template) { - window.analyzerCache.letsmesh_url_template = data.letsmesh_url_template; - } window.analyzerCache.loaded = true; renderAnalyzersList(); } catch (e) { @@ -3204,26 +3200,15 @@ function renderAnalyzersList() { if (!listEl) return; const analyzers = window.analyzerCache.analyzers || []; - const builtinRow = ` -
- - - -
-
Letsmesh Analyzer built-in
- ${escapeHtml(window.analyzerCache.letsmesh_url_template)} -
-
- `; - if (analyzers.length === 0) { - listEl.innerHTML = builtinRow + - '
No custom analyzers. Click "Add analyzer" to add one.
'; + listEl.innerHTML = + '
No analyzers configured. Click "Add analyzer" to add one.
'; return; } const rows = analyzers.map(a => { const disabled = !!a.is_disabled; + const enabled = !disabled; const isDefault = !!a.is_default; const starIcon = isDefault ? 'bi-star-fill text-warning' : 'bi-star'; const disabledBadge = disabled @@ -3241,10 +3226,10 @@ function renderAnalyzersList() {
${safeName}${disabledBadge}
${escapeHtml(a.url_template)} -
+
+ id="analyzerEnabled_${a.id}" ${enabled ? 'checked' : ''} + onchange="toggleAnalyzerDisabled(${a.id}, !this.checked)">
- `; - const customItems = sorted.map(a => ` + listEl.innerHTML = sorted.map(a => ` `).join(''); - listEl.innerHTML = builtinItem + customItems; const modal = bootstrap.Modal.getOrCreateInstance(modalEl); listEl.querySelectorAll('button[data-url]').forEach(btn => { diff --git a/app/templates/base.html b/app/templates/base.html index 34d6f0f..f436521 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1056,8 +1056,8 @@
Must include {packetHash}.
- - + +