mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -253,9 +253,17 @@ async def keenetic_add_bulk(request: Request, user: str = Depends(require_auth))
|
|||||||
|
|
||||||
@router.patch("/{name}")
|
@router.patch("/{name}")
|
||||||
async def keenetic_update(name: str, request: Request, user: str = Depends(require_auth)):
|
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()
|
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:
|
if not web_url_raw:
|
||||||
return {"status": "error", "detail": "web_url required"}
|
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:
|
except ValueError as e:
|
||||||
return {"status": "error", "detail": str(e)}
|
return {"status": "error", "detail": str(e)}
|
||||||
|
|
||||||
devices = _load_keenetic()
|
dev["web_url"] = web_url
|
||||||
idx = next((i for i, d in enumerate(devices) if d["name"] == name), None)
|
dev["host"] = host
|
||||||
if idx is None:
|
|
||||||
return {"status": "error", "detail": "router not found"}
|
|
||||||
|
|
||||||
devices[idx]["web_url"] = web_url
|
if "login" in body:
|
||||||
devices[idx]["host"] = host
|
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)
|
_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):
|
if body.get("refresh", True):
|
||||||
try:
|
try:
|
||||||
metrics = await _refresh_device_locked(devices[idx])
|
metrics = await _refresh_device_locked(dev)
|
||||||
result["metrics"] = metrics
|
result["metrics"] = metrics
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result["refresh_error"] = str(e)
|
result["refresh_error"] = str(e)
|
||||||
|
|||||||
@@ -458,6 +458,41 @@ main {
|
|||||||
width: 100%;
|
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 {
|
.server-card-actions .keenetic-url-btn.active {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|||||||
@@ -55,7 +55,20 @@ document.addEventListener('click', function(e) {
|
|||||||
if (keenSaveBtn) {
|
if (keenSaveBtn) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,6 +1263,8 @@ function renderKeenetic() {
|
|||||||
|
|
||||||
const urlEditId = 'keen-url-' + dev.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const urlEditId = 'keen-url-' + dev.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
const urlValue = escHtml(keeneticWebUrl(dev));
|
const urlValue = escHtml(keeneticWebUrl(dev));
|
||||||
|
const loginValue = escHtml(dev.login || 'admin');
|
||||||
|
const anydeskValue = escHtml(dev.anydesk || '');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="server-card ${isOnline ? 'online' : 'offline'}" data-keen="${escHtml(dev.name)}">
|
<div class="server-card ${isOnline ? 'online' : 'offline'}" data-keen="${escHtml(dev.name)}">
|
||||||
@@ -1285,15 +1300,34 @@ function renderKeenetic() {
|
|||||||
</div>
|
</div>
|
||||||
` : `<div style="padding:20px 0;text-align:center;opacity:0.5">${error ? '⚠️ ' + error : 'Нет данных — нажмите 🔄'}</div>`}
|
` : `<div style="padding:20px 0;text-align:center;opacity:0.5">${error ? '⚠️ ' + error : 'Нет данных — нажмите 🔄'}</div>`}
|
||||||
<div id="${urlEditId}" class="keenetic-url-edit" style="display:none">
|
<div id="${urlEditId}" class="keenetic-url-edit" style="display:none">
|
||||||
<div class="keenetic-url-edit-title">✏️ Адрес KeenDNS</div>
|
<div class="keenetic-url-edit-title">✏️ Редактирование</div>
|
||||||
<div class="form-group" style="margin-bottom:8px">
|
<div class="form-group">
|
||||||
<label for="${urlEditId}-input">Адрес KeenDNS</label>
|
<label for="${urlEditId}-name">Название</label>
|
||||||
<input type="text" id="${urlEditId}-input" value="${urlValue}" placeholder="https://example.keenetic.pro:8443">
|
<input type="text" id="${urlEditId}-name" value="${escHtml(dev.name)}" placeholder="Home Router">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${urlEditId}-url">Адрес KeenDNS / URL</label>
|
||||||
|
<input type="text" id="${urlEditId}-url" value="${urlValue}" placeholder="https://example.keenetic.pro:8443">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${urlEditId}-login">Логин</label>
|
||||||
|
<input type="text" id="${urlEditId}-login" value="${loginValue}" placeholder="admin">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${urlEditId}-password">Пароль</label>
|
||||||
|
<div class="keenetic-pass-wrap">
|
||||||
|
<input type="password" id="${urlEditId}-password" placeholder="Оставьте пустым, чтобы не менять" autocomplete="new-password">
|
||||||
|
<button type="button" class="keenetic-pass-toggle" data-target="${urlEditId}-password" title="Показать/скрыть">👁</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${urlEditId}-anydesk">AnyDesk ID</label>
|
||||||
|
<input type="text" id="${urlEditId}-anydesk" value="${anydeskValue}" placeholder="1020687391">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-primary keenetic-url-save" style="width:100%" data-name="${escHtml(dev.name)}">Сохранить</button>
|
<button type="button" class="btn-primary keenetic-url-save" style="width:100%" data-name="${escHtml(dev.name)}">Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-card-actions">
|
<div class="server-card-actions">
|
||||||
<button type="button" class="keenetic-url-btn" data-name="${escHtml(dev.name)}">✏️ KeenDNS</button>
|
<button type="button" class="keenetic-url-btn" data-name="${escHtml(dev.name)}">✏️ Редактировать</button>
|
||||||
<button type="button" onclick="event.stopPropagation();refreshKeenetic(${JSON.stringify(dev.name)})">🔄 Обновить</button>
|
<button type="button" onclick="event.stopPropagation();refreshKeenetic(${JSON.stringify(dev.name)})">🔄 Обновить</button>
|
||||||
<button type="button" onclick="event.stopPropagation();rebootKeenetic(${JSON.stringify(dev.name)})">🔁 Reboot</button>
|
<button type="button" onclick="event.stopPropagation();rebootKeenetic(${JSON.stringify(dev.name)})">🔁 Reboot</button>
|
||||||
<button type="button" class="danger" onclick="event.stopPropagation();deleteKeenetic(${JSON.stringify(dev.name)})">🗑</button>
|
<button type="button" class="danger" onclick="event.stopPropagation();deleteKeenetic(${JSON.stringify(dev.name)})">🗑</button>
|
||||||
@@ -1592,7 +1626,7 @@ function toggleKeeneticUrlEdit(name) {
|
|||||||
card.querySelector('.keenetic-url-btn')?.classList.toggle('active', show);
|
card.querySelector('.keenetic-url-btn')?.classList.toggle('active', show);
|
||||||
}
|
}
|
||||||
if (show) {
|
if (show) {
|
||||||
const input = document.getElementById(keeneticUrlEditId(name) + '-input');
|
const input = document.getElementById(keeneticUrlEditId(name) + '-name');
|
||||||
if (input) {
|
if (input) {
|
||||||
input.focus();
|
input.focus();
|
||||||
input.select();
|
input.select();
|
||||||
@@ -1601,22 +1635,38 @@ function toggleKeeneticUrlEdit(name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveKeeneticUrl(name) {
|
async function saveKeeneticEdit(oldName) {
|
||||||
const input = document.getElementById(keeneticUrlEditId(name) + '-input');
|
const id = keeneticUrlEditId(oldName);
|
||||||
if (!input) return;
|
const newName = document.getElementById(id + '-name')?.value.trim();
|
||||||
const web_url = input.value.trim();
|
const web_url = document.getElementById(id + '-url')?.value.trim();
|
||||||
if (!web_url) {
|
const login = document.getElementById(id + '-login')?.value.trim();
|
||||||
alert('Укажите адрес KeenDNS');
|
const password = document.getElementById(id + '-password')?.value;
|
||||||
|
const anydesk = document.getElementById(id + '-anydesk')?.value.trim() || '';
|
||||||
|
|
||||||
|
if (!newName) {
|
||||||
|
alert('Укажите название');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const saveBtn = input.closest('.keenetic-url-edit')?.querySelector('.btn-primary');
|
if (!web_url) {
|
||||||
|
alert('Укажите адрес KeenDNS / URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!login) {
|
||||||
|
alert('Укажите логин');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveBtn = document.getElementById(id)?.querySelector('.btn-primary');
|
||||||
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '⏳ Сохранение...'; }
|
if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = '⏳ Сохранение...'; }
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/api/keenetic/${encodeURIComponent(name)}`, {
|
const body = {name: newName, web_url, login, anydesk, refresh: true};
|
||||||
|
if (password) body.password = password;
|
||||||
|
|
||||||
|
const resp = await fetch(`/api/keenetic/${encodeURIComponent(oldName)}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({web_url, refresh: true}),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.status !== 'ok') {
|
if (data.status !== 'ok') {
|
||||||
@@ -1624,7 +1674,6 @@ async function saveKeeneticUrl(name) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadKeenetic();
|
await loadKeenetic();
|
||||||
toggleKeeneticUrlEdit(name);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Ошибка: ' + e.message);
|
alert('Ошибка: ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user