Merge add/edit forms for WEB, RDP, Users (same pattern as Pilots)

Same restructure as the Pilots tab: one form instead of two stacked
ones, a + button at the top of each list to switch it into create
mode, and an inline on/off toggle per list row. Applies to the WEB,
RDP and Users tabs - save() now POSTs when the hidden id is empty
and PUTs otherwise, delete/prewarm/icon/health/ACL boxes show only
once something real is selected.

No backend changes - the create and edit endpoints already existed
independently, this only changes which one the frontend calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 07:51:59 +00:00
parent cfd337a673
commit a476627c34
+116 -176
View File
@@ -241,20 +241,25 @@
<div class="list-title">Список пользователей</div>
<input class="list-search" id="users_search" placeholder="Поиск пользователя..." oninput="filterList('users_search', '#users_list .user-item')" />
<div class="list-box" id="users_list">
<button class="list-item pilot-add-item" onclick="startNewUser()">+ Добавить пользователя</button>
{% for u in users %}
<button class="list-item user-item" data-user-id="{{u.id}}" data-filter="{{u.username|lower}}" onclick='selectUser({{u.id}}, {{u.username|tojson}}, {{u.active|tojson}}, {{u.is_admin|tojson}}, {{u.expires_at.isoformat()|tojson}}, {{u.first_name|tojson}}, {{u.last_name|tojson}})'>
<div>{{u.username}}{% if u.first_name or u.last_name %} <small style="opacity:.6">— {{ (u.first_name + ' ' + u.last_name)|trim }}</small>{% endif %}</div>
<small class="user-days" data-exp="{{u.expires_at.isoformat()}}"></small>
</button>
<div class="list-item user-item" data-user-id="{{u.id}}" data-filter="{{u.username|lower}}" style="display:flex;align-items:center;justify-content:space-between;cursor:pointer" onclick='selectUser({{u.id}}, {{u.username|tojson}}, {{u.active|tojson}}, {{u.is_admin|tojson}}, {{u.expires_at.isoformat()|tojson}}, {{u.first_name|tojson}}, {{u.last_name|tojson}})'>
<div style="min-width:0">
<div>{{u.username}}{% if u.first_name or u.last_name %} <small style="opacity:.6">— {{ (u.first_name + ' ' + u.last_name)|trim }}</small>{% endif %}{% if not u.active %} <span class="pilot-inactive-tag">выключен</span>{% endif %}</div>
<small class="user-days" data-exp="{{u.expires_at.isoformat()}}"></small>
</div>
<button type="button" class="pilot-toggle-btn" title="{{ 'Деактивировать' if u.active else 'Активировать' }}" onclick='event.stopPropagation(); toggleUserActive({{u.id}}, {{(not u.active)|tojson}})'>{{ "⏻" if u.active else "▶" }}</button>
</div>
{% endfor %}
</div>
</div>
<div>
<div class="list-title">Редактирование пользователя</div>
<div class="list-title" id="user_form_title">Новый пользователь</div>
<div class="field-help" id="user_pwd_help">Пароль не задаётся вручную — он сгенерируется и уйдёт пользователю на почту (username) сразу после создания.</div>
<div class="form-grid">
<input id="u_id" type="hidden" />
<input id="u_name" placeholder="username" />
<input id="u_name" placeholder="username (email)" />
<input id="u_first_name" placeholder="Имя" />
<input id="u_last_name" placeholder="Фамилия" />
<input id="u_exp" type="date" required />
@@ -264,11 +269,11 @@
<div class="actions">
<button onclick="saveUser()">Save</button>
<button id="user_reset_pwd_btn" onclick="resetUserPassword()" style="display:none;">🔑 Сбросить пароль</button>
<button onclick="deleteUser()">Delete</button>
<button id="user_delete_btn" onclick="deleteUser()" style="display:none;">Delete</button>
<button onclick="clearUserForm()">Clear</button>
</div>
<div style="margin-top:1rem;">
<div id="user_acl_box" style="display:none;margin-top:1rem;">
<div class="list-title">ACL выбранного пользователя</div>
<div class="field-help">Пилоты сюда не входят — доступ к ним назначается на вкладке «Pilots» (там же, где и слот).</div>
<div class="acl-grid">
@@ -282,19 +287,6 @@
</div>
<button onclick="saveAclForSelectedUser()">Save ACL</button>
</div>
<hr>
<div class="list-title">Добавить пользователя</div>
<div class="field-help">Пароль не задаётся вручную — он сгенерируется и уйдёт пользователю на почту (username) сразу после создания.</div>
<div class="form-grid">
<input id="new_u_name" placeholder="username (email)" />
<input id="new_u_first_name" placeholder="Имя" />
<input id="new_u_last_name" placeholder="Фамилия" />
<input id="new_u_exp" type="date" required />
<select id="new_u_active"><option value="true">active</option><option value="false">inactive</option></select>
<select id="new_u_admin"><option value="false">user</option><option value="true">admin</option></select>
</div>
<button onclick="createUser()">Add User</button>
</div>
</div>
</section>
@@ -331,22 +323,28 @@
<div class="list-title">Список WEB</div>
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
<div class="list-box" id="web_list">
<button class="list-item pilot-add-item" onclick="startNewWeb()">+ Добавить WEB</button>
{% for s in web_services %}
<button class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
<div>
<div>{{s.name}}</div>
<small>
<span class="status-dot status-{{ service_health[s.id].health }}"></span>
{{service_health[s.id].health}} | прогрето: {{service_health[s.id].running}} / {{service_health[s.id].desired}} | занято: {{service_health[s.id].active_sessions}}
</small>
<div class="list-item service-row web-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" style="justify-content:space-between;cursor:pointer" onclick='selectWebService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
<div class="service-row" style="min-width:0">
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
<div style="min-width:0">
<div>{{s.name}}{% if not s.active %} <span class="pilot-inactive-tag">выключен</span>{% endif %}</div>
<small>
<span class="status-dot status-{{ service_health[s.id].health }}"></span>
{{service_health[s.id].health}} | прогрето: {{service_health[s.id].running}} / {{service_health[s.id].desired}} | занято: {{service_health[s.id].active_sessions}}
</small>
</div>
</div>
</button>
<button type="button" class="pilot-toggle-btn" title="{{ 'Деактивировать' if s.active else 'Активировать' }}" onclick='event.stopPropagation(); toggleServiceActive({{s.id}}, {{(not s.active)|tojson}})'>{{ "⏻" if s.active else "▶" }}</button>
</div>
{% else %}
<div class="list-item" style="opacity:.6;cursor:default">WEB-сервисов пока нет</div>
{% endfor %}
</div>
</div>
<div>
<div class="list-title">Редактирование WEB</div>
<div class="list-title" id="web_form_title">Новый WEB-сервис</div>
<div class="field-help">Для пользователей показывается только название и описание. Технический тип скрыт.</div>
<div class="form-grid labelled-grid">
<input id="w_id" type="hidden" />
@@ -395,7 +393,7 @@
</div>
<div class="actions">
<button onclick="saveWebService()">Save</button>
<button onclick="deleteService('w_id')">Delete</button>
<button id="w_delete_btn" onclick="deleteService('w_id')" style="display:none;">Delete</button>
<button onclick="clearWebForm()">Clear</button>
</div>
@@ -422,7 +420,7 @@
</div>
</div>
<div class="icon-box" style="margin-top:1rem;">
<div class="icon-box" id="w_icon_box" style="display:none; margin-top:1rem;">
<div class="list-title">Иконка сервиса</div>
<div class="icon-row">
<img id="w_icon_preview" class="service-icon-preview" src="/static/service-placeholder.svg" alt="icon" />
@@ -435,43 +433,6 @@
</div>
</div>
</div>
<hr>
<div class="list-title">Добавить WEB</div>
<div class="field-help">Горячий пул задается сверху и общий для всех WEB сервисов.</div>
<div class="form-grid labelled-grid">
<input id="new_w_slug" type="hidden" />
<label class="field-col">
<span>Название сервиса</span>
<input id="new_w_name" placeholder="Например: CRM, Router, Wiki" oninput="autogenSlug('new_w_name','new_w_slug')" />
</label>
<label class="field-col">
<span>URL для открытия</span>
<input id="new_w_target" placeholder="Например: https://crm.company.local" />
</label>
<label class="field-col">
<span>Описание для пользователя</span>
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
<input id="new_w_svc_login" placeholder="Логин сервиса (необязательно)" />
<input id="new_w_svc_password" placeholder="Пароль сервиса (необязательно)" />
<input id="new_w_svc_cred_hint" placeholder="Подсказка к логину/паролю (необязательно)" />
</label>
<label class="field-col">
<span>Статус</span>
<select id="new_w_active"><option value="true">active</option><option value="false">inactive</option></select>
</label>
</div>
<div class="list-title">Категории</div>
<div class="acl-grid compact-grid" id="new_w_categories">
{% for c in categories %}
<label><input type="checkbox" class="new_w_cat" value="{{c.id}}" /> {{c.name}}</label>
{% endfor %}
</div>
<button onclick="createWebService()">Add WEB</button>
</div>
</div>
</section>
@@ -483,22 +444,28 @@
<div class="list-title">Список RDP</div>
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
<div class="list-box" id="rdp_list">
<button class="list-item pilot-add-item" onclick="startNewRdp()">+ Добавить RDP</button>
{% for s in rdp_services %}
<button class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
<div>
<div>{{s.name}}</div>
<small>
<span class="status-dot status-{{ service_health[s.id].health }}"></span>
{{service_health[s.id].health}} | {{service_health[s.id].running}} / {{service_health[s.id].desired}} | active: {{service_health[s.id].active_sessions}}
</small>
<div class="list-item service-row rdp-item" data-service-id="{{s.id}}" data-filter="{{(s.name ~ ' ' ~ s.slug)|lower}}" style="justify-content:space-between;cursor:pointer" onclick='selectRdpService({{s.id}}, {{s.name|tojson}}, {{s.slug|tojson}}, {{s.target|tojson}}, {{s.comment|tojson}}, {{s.icon_path|tojson}}, {{s.active|tojson}}, {{s.warm_pool_size}}, {{s.svc_login|tojson}}, {{s.svc_password|tojson}}, {{s.svc_cred_hint|tojson}})'>
<div class="service-row" style="min-width:0">
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
<div style="min-width:0">
<div>{{s.name}}{% if not s.active %} <span class="pilot-inactive-tag">выключен</span>{% endif %}</div>
<small>
<span class="status-dot status-{{ service_health[s.id].health }}"></span>
{{service_health[s.id].health}} | {{service_health[s.id].running}} / {{service_health[s.id].desired}} | active: {{service_health[s.id].active_sessions}}
</small>
</div>
</div>
</button>
<button type="button" class="pilot-toggle-btn" title="{{ 'Деактивировать' if s.active else 'Активировать' }}" onclick='event.stopPropagation(); toggleServiceActive({{s.id}}, {{(not s.active)|tojson}})'>{{ "⏻" if s.active else "▶" }}</button>
</div>
{% else %}
<div class="list-item" style="opacity:.6;cursor:default">RDP-сервисов пока нет</div>
{% endfor %}
</div>
</div>
<div>
<div class="list-title">Редактирование RDP</div>
<div class="list-title" id="rdp_form_title">Новый RDP-сервис</div>
<div class="field-help">Используйте поля host/port/user. Поле target собирается автоматически.</div>
<div class="form-grid">
<input id="r_id" type="hidden" />
@@ -529,8 +496,8 @@
</div>
<div class="actions">
<button onclick="saveRdpService()">Save</button>
<button onclick="prewarmNow('r_id')">Prewarm now</button>
<button onclick="deleteService('r_id')">Delete</button>
<button id="r_prewarm_btn" onclick="prewarmNow('r_id')" style="display:none;">Prewarm now</button>
<button id="r_delete_btn" onclick="deleteService('r_id')" style="display:none;">Delete</button>
<button onclick="clearRdpForm()">Clear</button>
</div>
@@ -556,7 +523,7 @@
</div>
</div>
<div class="icon-box" style="margin-top:1rem;">
<div class="icon-box" id="r_icon_box" style="display:none; margin-top:1rem;">
<div class="list-title">Иконка сервиса</div>
<div class="icon-row">
<img id="r_icon_preview" class="service-icon-preview" src="/static/service-placeholder.svg" alt="icon" />
@@ -583,37 +550,6 @@
<button onclick="addRdpSlot()">+ Добавить слот</button>
</div>
</div>
<hr>
<div class="list-title">Добавить RDP</div>
<div class="field-help">Для большинства кейсов достаточно host + user + password.</div>
<div class="form-grid">
<input id="new_r_name" placeholder="Название сервиса" oninput="autogenSlug('new_r_name','new_r_slug')" />
<input id="new_r_slug" placeholder="Системный slug" />
<input id="new_r_host" placeholder="RDP host" oninput="buildRdpTarget('new_r')" />
<input id="new_r_port" type="number" min="1" max="65535" placeholder="RDP порт (3389)" oninput="buildRdpTarget('new_r')" />
<input id="new_r_domain" placeholder="Домен (опционально)" oninput="buildRdpTarget('new_r')" />
<select id="new_r_sec" onchange="buildRdpTarget('new_r')">
<option value="">auto</option>
<option value="nla">nla</option>
<option value="tls">tls</option>
<option value="rdp">rdp</option>
</select>
<input id="new_r_target" placeholder="Собранный target (авто)" readonly style="background:rgba(255,255,255,.05);color:#888;cursor:default" />
<textarea id="new_r_comment" placeholder="Описание для пользователя"></textarea>
<input id="new_r_svc_login" placeholder="Логин сервиса (необязательно)" />
<input id="new_r_svc_password" placeholder="Пароль сервиса (необязательно)" />
<input id="new_r_svc_cred_hint" placeholder="Подсказка к логину/паролю (необязательно)" />
<input id="new_r_pool" type="number" min="0" value="1" placeholder="Количество прогретых слотов" />
<select id="new_r_active"><option value="true">active</option><option value="false">inactive</option></select>
</div>
<div class="list-title">Категории</div>
<div class="acl-grid compact-grid" id="new_r_categories">
{% for c in categories %}
<label><input type="checkbox" class="new_r_cat" value="{{c.id}}" /> {{c.name}}</label>
{% endfor %}
</div>
<button onclick="createRdpService()">Add RDP</button>
</div>
</div>
</section>
@@ -639,7 +575,7 @@
</small>
</div>
</div>
<button type="button" class="pilot-toggle-btn" title="{{ 'Деактивировать' if s.active else 'Активировать' }}" onclick='event.stopPropagation(); togglePilotActive({{s.id}}, {{(not s.active)|tojson}})'>{{ "⏻" if s.active else "▶" }}</button>
<button type="button" class="pilot-toggle-btn" title="{{ 'Деактивировать' if s.active else 'Активировать' }}" onclick='event.stopPropagation(); toggleServiceActive({{s.id}}, {{(not s.active)|tojson}})'>{{ "⏻" if s.active else "▶" }}</button>
</div>
{% else %}
<div class="list-item" style="opacity:.6;cursor:default">Пилотов пока нет</div>
@@ -963,30 +899,21 @@
document.getElementById('u_active').value = String(active);
document.getElementById('u_admin').value = String(isAdmin);
document.getElementById('user_reset_pwd_btn').style.display = '';
document.getElementById('user_delete_btn').style.display = '';
document.getElementById('user_acl_box').style.display = '';
document.getElementById('user_pwd_help').style.display = 'none';
document.getElementById('user_form_title').textContent = username;
markSelected('.user-item', 'data-user-id', id);
syncAclForSelectedUser();
}
async function createUser() {
const expDate = document.getElementById('new_u_exp').value;
if (!expDate) return alert('Выберите дату деактивации');
const result = await api('/api/admin/users', 'POST', {
username: document.getElementById('new_u_name').value,
first_name: document.getElementById('new_u_first_name').value,
last_name: document.getElementById('new_u_last_name').value,
expires_at: expiryToApi(expDate),
active: document.getElementById('new_u_active').value === 'true',
is_admin: document.getElementById('new_u_admin').value === 'true',
});
if (result.password) {
alert(`Письмо не отправлено (${result.email_status}). Пароль: ${result.password}\nПередайте его пользователю вручную.`);
}
location.reload();
function startNewUser() {
clearUserForm();
document.getElementById('u_name').focus();
}
async function saveUser() {
const id = document.getElementById('u_id').value;
if (!id) return alert('Выберите пользователя');
const expDate = document.getElementById('u_exp').value;
if (!expDate) return alert('Выберите дату деактивации');
const payload = {
@@ -997,8 +924,16 @@
active: document.getElementById('u_active').value === 'true',
is_admin: document.getElementById('u_admin').value === 'true',
};
await api(`/api/admin/users/${id}`, 'PUT', payload);
location.reload();
if (id) {
await api(`/api/admin/users/${id}`, 'PUT', payload);
location.reload();
} else {
const result = await api('/api/admin/users', 'POST', payload);
if (result.password) {
alert(`Письмо не отправлено (${result.email_status}). Пароль: ${result.password}\nПередайте его пользователю вручную.`);
}
location.reload();
}
}
async function resetUserPassword() {
@@ -1022,14 +957,23 @@
}
function clearUserForm() {
['u_id','u_name','u_exp'].forEach(id => document.getElementById(id).value = '');
['u_id','u_name','u_first_name','u_last_name','u_exp'].forEach(id => document.getElementById(id).value = '');
document.getElementById('u_active').value = 'true';
document.getElementById('u_admin').value = 'false';
document.getElementById('user_reset_pwd_btn').style.display = 'none';
document.getElementById('user_delete_btn').style.display = 'none';
document.getElementById('user_acl_box').style.display = 'none';
document.getElementById('user_pwd_help').style.display = '';
document.getElementById('user_form_title').textContent = 'Новый пользователь';
document.querySelectorAll('.acl_service').forEach(x => x.checked = false);
document.querySelectorAll('.user-item').forEach((el) => el.classList.remove('selected-item'));
}
async function toggleUserActive(id, nextActive) {
await api(`/api/admin/users/${id}`, 'PUT', {active: nextActive});
location.reload();
}
function syncAclForSelectedUser() {
const userId = parseInt(document.getElementById('u_id').value || '0', 10);
const allowed = new Set((aclMap[userId] || []));
@@ -1062,38 +1006,28 @@
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
document.getElementById('w_health_box').style.display = 'block';
document.getElementById('w_icon_box').style.display = '';
document.getElementById('w_delete_btn').style.display = '';
document.getElementById('web_form_title').textContent = `WEB: ${name}`;
markSelected('.web-item', 'data-service-id', id);
refreshSelectedServiceStatus('web');
}
function startNewWeb() {
clearWebForm();
document.getElementById('w_name').focus();
}
async function saveWebPoolSize() {
const size = parseInt(document.getElementById('web_pool_size').value || '0', 10);
await api('/api/admin/web-pool-size', 'PUT', {size});
location.reload();
}
async function createWebService() {
const slug = document.getElementById('new_w_slug').value || slugifyRu(document.getElementById('new_w_name').value);
await api('/api/admin/services', 'POST', {
name: document.getElementById('new_w_name').value,
slug,
type: 'WEB',
target: document.getElementById('new_w_target').value,
comment: document.getElementById('new_w_comment').value,
svc_login: document.getElementById('new_w_svc_login').value,
svc_password: document.getElementById('new_w_svc_password').value,
svc_cred_hint: document.getElementById('new_w_svc_cred_hint').value,
category_ids: checkedCategoryIds('.new_w_cat'),
active: document.getElementById('new_w_active').value === 'true',
});
location.reload();
}
async function saveWebService() {
const id = document.getElementById('w_id').value;
if (!id) return alert('Выберите WEB сервис');
const slug = document.getElementById('w_slug').value || slugifyRu(document.getElementById('w_name').value);
await api(`/api/admin/services/${id}`, 'PUT', {
const payload = {
name: document.getElementById('w_name').value,
slug,
type: 'WEB',
@@ -1104,7 +1038,12 @@
svc_cred_hint: document.getElementById('w_svc_cred_hint').value,
category_ids: checkedCategoryIds('.w_cat'),
active: document.getElementById('w_active').value === 'true',
});
};
if (id) {
await api(`/api/admin/services/${id}`, 'PUT', payload);
} else {
await api('/api/admin/services', 'POST', payload);
}
location.reload();
}
@@ -1114,6 +1053,9 @@
setCategoryChecks('.w_cat', []);
document.getElementById('w_icon_preview').src = placeholderIcon;
document.getElementById('w_health_box').style.display = 'none';
document.getElementById('w_icon_box').style.display = 'none';
document.getElementById('w_delete_btn').style.display = 'none';
document.getElementById('web_form_title').textContent = 'Новый WEB-сервис';
document.querySelectorAll('.web-item').forEach((el) => el.classList.remove('selected-item'));
}
@@ -1211,37 +1153,26 @@
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
document.getElementById('r_icon_preview').src = iconPath || placeholderIcon;
document.getElementById('r_health_box').style.display = 'block';
document.getElementById('r_icon_box').style.display = '';
document.getElementById('r_delete_btn').style.display = '';
document.getElementById('r_prewarm_btn').style.display = '';
document.getElementById('rdp_form_title').textContent = `RDP: ${name}`;
markSelected('.rdp-item', 'data-service-id', id);
renderRdpSlots(id);
refreshSelectedServiceStatus('rdp');
}
async function createRdpService() {
const slug = document.getElementById('new_r_slug').value || slugifyRu(document.getElementById('new_r_name').value);
const target = buildRdpTarget('new_r');
await api('/api/admin/services', 'POST', {
name: document.getElementById('new_r_name').value,
slug,
type: 'RDP',
target,
comment: document.getElementById('new_r_comment').value,
svc_login: document.getElementById('new_r_svc_login').value,
svc_password: document.getElementById('new_r_svc_password').value,
svc_cred_hint: document.getElementById('new_r_svc_cred_hint').value,
category_ids: checkedCategoryIds('.new_r_cat'),
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
active: document.getElementById('new_r_active').value === 'true',
});
location.reload();
function startNewRdp() {
clearRdpForm();
document.getElementById('r_name').focus();
}
async function saveRdpService() {
const id = document.getElementById('r_id').value;
if (!id) return alert('Выберите RDP сервис');
const target = buildRdpTarget('r');
await api(`/api/admin/services/${id}`, 'PUT', {
const payload = {
name: document.getElementById('r_name').value,
slug: document.getElementById('r_slug').value,
slug: document.getElementById('r_slug').value || slugifyRu(document.getElementById('r_name').value),
type: 'RDP',
target,
comment: document.getElementById('r_comment').value,
@@ -1251,7 +1182,12 @@
category_ids: checkedCategoryIds('.r_cat'),
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
active: document.getElementById('r_active').value === 'true',
});
};
if (id) {
await api(`/api/admin/services/${id}`, 'PUT', payload);
} else {
await api('/api/admin/services', 'POST', payload);
}
location.reload();
}
@@ -1263,6 +1199,10 @@
setCategoryChecks('.r_cat', []);
document.getElementById('r_icon_preview').src = placeholderIcon;
document.getElementById('r_health_box').style.display = 'none';
document.getElementById('r_icon_box').style.display = 'none';
document.getElementById('r_delete_btn').style.display = 'none';
document.getElementById('r_prewarm_btn').style.display = 'none';
document.getElementById('rdp_form_title').textContent = 'Новый RDP-сервис';
document.querySelectorAll('.rdp-item').forEach((el) => el.classList.remove('selected-item'));
}
@@ -1340,7 +1280,7 @@
document.getElementById('p_name').focus();
}
async function togglePilotActive(id, nextActive) {
async function toggleServiceActive(id, nextActive) {
await api(`/api/admin/services/${id}`, 'PUT', {active: nextActive});
location.reload();
}