mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 01:03:15 +02:00
feat(repeaters): My Repeaters panel (stage 1)
New full-screen panel (main menu / FAB) for repeater administration: - repeaters table (saved per-pubkey admin password, login metadata); plaintext per observer_brokers precedent (single-user LAN app) - REST /api/repeaters: list merged with device contact truth, add (device REP contacts only), set/clear password, remove, login - device_manager: _repeater_lock serializes all repeater ops (companion firmware has a single pending-request slot); repeater_login now filters LOGIN_SUCCESS by pubkey_prefix and captures is_admin/ permissions into an in-memory session store - login timeout UX: wrong password and unreachable repeater are indistinguishable (firmware stays silent) - error message names both - panel UI: add-picker, password modal (remembered password), per- repeater path editor reusing /api/contacts/<pk>/paths + Leaflet repeater map picker (adapted from the DM panel) - meshcore pin bumped to >=2.3.7 (send_login_sync era API, fixed LOGIN_SUCCESS/LOGIN_FAILED parsing; container already ships 2.3.7) Stage 2 (management panel with Status/Telemetry/Neighbors/CLI/ Settings/Actions tools) follows after user review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -715,6 +715,55 @@ class Database:
|
||||
cursor = conn.execute("DELETE FROM observer_brokers WHERE id = ?", (broker_id,))
|
||||
return cursor.rowcount > 0
|
||||
|
||||
# ================================================================
|
||||
# My Repeaters (saved repeater logins for the admin panel)
|
||||
# ================================================================
|
||||
|
||||
def get_repeaters(self) -> List[Dict]:
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM repeaters ORDER BY added_at"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_repeater(self, public_key: str) -> Optional[Dict]:
|
||||
with self._connect() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM repeaters WHERE public_key = ?", (public_key,)
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def add_repeater(self, public_key: str) -> bool:
|
||||
"""Add a repeater entry. Returns False if it already exists."""
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"INSERT OR IGNORE INTO repeaters (public_key) VALUES (?)",
|
||||
(public_key,)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def set_repeater_password(self, public_key: str, password: str) -> bool:
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"UPDATE repeaters SET password = ? WHERE public_key = ?",
|
||||
(password, public_key)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def update_repeater_login(self, public_key: str, role: str) -> bool:
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"UPDATE repeaters SET last_login_at = datetime('now'), "
|
||||
"last_login_role = ? WHERE public_key = ?",
|
||||
(role, public_key)
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def delete_repeater(self, public_key: str) -> bool:
|
||||
with self._connect() as conn:
|
||||
cursor = conn.execute("DELETE FROM repeaters WHERE public_key = ?", (public_key,))
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def set_channel_scope(self, channel_idx: int, region_id: Optional[int]) -> None:
|
||||
"""Set or clear the region mapping for a channel.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user