From 3a5a4beb3fce84ea3ed3a303fd7dbf2daa0db6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Sun, 24 May 2026 01:45:52 +0300 Subject: [PATCH] feat(keenetic): full edit form for all router fields Replace KeenDNS-only edit with a complete form (name, URL, login, password, AnyDesk). PATCH API now supports rename and keeps password when the field is left empty. Co-authored-by: Cursor --- server/api/keenetic.py | 59 +++++++++++++++++++++----- server/static/css/style.css | 35 ++++++++++++++++ server/static/js/app.js | 83 +++++++++++++++++++++++++++++-------- 3 files changed, 149 insertions(+), 28 deletions(-) diff --git a/server/api/keenetic.py b/server/api/keenetic.py index 7a417c1..a3df6d4 100644 --- a/server/api/keenetic.py +++ b/server/api/keenetic.py @@ -253,9 +253,17 @@ async def keenetic_add_bulk(request: Request, user: str = Depends(require_auth)) @router.patch("/{name}") async def keenetic_update(name: str, request: Request, user: str = Depends(require_auth)): - """Update KeenDNS/web URL for a router (web_url + host).""" + """Update router fields: name, web_url/host, login, password, anydesk.""" body = await request.json() - web_url_raw = (body.get("web_url") or body.get("url") or "").strip() + devices = _load_keenetic() + idx = next((i for i, d in enumerate(devices) if d["name"] == name), None) + if idx is None: + return {"status": "error", "detail": "router not found"} + + dev = devices[idx] + old_name = name + + web_url_raw = (body.get("web_url") or body.get("url") or body.get("host") or "").strip() if not web_url_raw: return {"status": "error", "detail": "web_url required"} @@ -264,20 +272,49 @@ async def keenetic_update(name: str, request: Request, user: str = Depends(requi except ValueError as e: return {"status": "error", "detail": str(e)} - devices = _load_keenetic() - idx = next((i for i, d in enumerate(devices) if d["name"] == name), None) - if idx is None: - return {"status": "error", "detail": "router not found"} + dev["web_url"] = web_url + dev["host"] = host - devices[idx]["web_url"] = web_url - devices[idx]["host"] = host + if "login" in body: + login = (body.get("login") or "").strip() + if not login: + return {"status": "error", "detail": "login required"} + dev["login"] = login + + password = body.get("password") + if password: + dev["password"] = password + + if "anydesk" in body: + dev["anydesk"] = (body.get("anydesk") or "").strip() + + new_name = (body.get("name") or "").strip() + final_name = old_name + if new_name: + if new_name != old_name: + if any(d["name"] == new_name for j, d in enumerate(devices) if j != idx): + return {"status": "error", "detail": f"Роутер «{new_name}» уже существует"} + final_name = new_name + dev["name"] = new_name + + devices[idx] = dev _save_keenetic(devices) - keenetic_metrics.pop(name, None) - result = {"status": "ok", "web_url": web_url, "host": host} + keenetic_metrics.pop(old_name, None) + if final_name != old_name: + keenetic_metrics.pop(final_name, None) + + result = { + "status": "ok", + "name": final_name, + "web_url": web_url, + "host": host, + "login": dev.get("login", "admin"), + "anydesk": dev.get("anydesk", ""), + } if body.get("refresh", True): try: - metrics = await _refresh_device_locked(devices[idx]) + metrics = await _refresh_device_locked(dev) result["metrics"] = metrics except Exception as e: result["refresh_error"] = str(e) diff --git a/server/static/css/style.css b/server/static/css/style.css index 2074638..a415e61 100644 --- a/server/static/css/style.css +++ b/server/static/css/style.css @@ -458,6 +458,41 @@ main { width: 100%; } +.keenetic-url-edit .form-group { + margin-bottom: 8px; +} + +.keenetic-url-edit .form-group label { + font-size: 12px; + opacity: 0.85; +} + +.keenetic-pass-wrap { + display: flex; + gap: 6px; + align-items: center; +} + +.keenetic-pass-wrap input { + flex: 1; +} + +.keenetic-pass-toggle { + flex-shrink: 0; + padding: 6px 10px; + font-size: 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-secondary); + cursor: pointer; +} + +.keenetic-pass-toggle:hover { + border-color: var(--accent); + color: var(--accent); +} + .server-card-actions .keenetic-url-btn.active { border-color: var(--accent); color: var(--accent); diff --git a/server/static/js/app.js b/server/static/js/app.js index 93d2be3..e581352 100644 --- a/server/static/js/app.js +++ b/server/static/js/app.js @@ -55,7 +55,20 @@ document.addEventListener('click', function(e) { if (keenSaveBtn) { e.stopPropagation(); e.preventDefault(); - saveKeeneticUrl(keenSaveBtn.dataset.name); + saveKeeneticEdit(keenSaveBtn.dataset.name); + return; + } + + const keenPassToggle = e.target.closest('.keenetic-pass-toggle'); + if (keenPassToggle) { + e.stopPropagation(); + e.preventDefault(); + const input = document.getElementById(keenPassToggle.dataset.target); + if (input) { + const show = input.type === 'password'; + input.type = show ? 'text' : 'password'; + keenPassToggle.textContent = show ? '🙈' : '👁'; + } return; } @@ -1250,6 +1263,8 @@ function renderKeenetic() { const urlEditId = 'keen-url-' + dev.name.replace(/[^a-zA-Z0-9_-]/g, '_'); const urlValue = escHtml(keeneticWebUrl(dev)); + const loginValue = escHtml(dev.login || 'admin'); + const anydeskValue = escHtml(dev.anydesk || ''); return `
@@ -1285,15 +1300,34 @@ function renderKeenetic() {
` : `
${error ? '⚠️ ' + error : 'Нет данных — нажмите 🔄'}
`}