diff --git a/app/templates/admin.html b/app/templates/admin.html
index 3955f09..2f69c47 100644
--- a/app/templates/admin.html
+++ b/app/templates/admin.html
@@ -241,20 +241,25 @@
Список пользователей
+
{% for u in users %}
-
+
+
+
{{u.username}}{% if u.first_name or u.last_name %} — {{ (u.first_name + ' ' + u.last_name)|trim }}{% endif %}{% if not u.active %} выключен{% endif %}
+
+
+
+
{% endfor %}
-
Редактирование пользователя
+
Новый пользователь
+
Пароль не задаётся вручную — он сгенерируется и уйдёт пользователю на почту (username) сразу после создания.
@@ -331,22 +323,28 @@
Список WEB
+
{% for s in web_services %}
-
-
Редактирование WEB
+
Новый WEB-сервис
Для пользователей показывается только название и описание. Технический тип скрыт.
@@ -395,7 +393,7 @@
Save
- Delete
+ Delete
Clear
@@ -422,7 +420,7 @@
-
+
Иконка сервиса

@@ -435,43 +433,6 @@
-
-
- Добавить WEB
- Горячий пул задается сверху и общий для всех WEB сервисов.
-
-
-
-
-
-
-
-
-
-
-
- Категории
-
- {% for c in categories %}
-
- {% endfor %}
-
- Add WEB
@@ -483,22 +444,28 @@
Список RDP
+
+ Добавить RDP
{% for s in rdp_services %}
-
-
-
-
{{s.name}}
-
-
- {{service_health[s.id].health}} | {{service_health[s.id].running}} / {{service_health[s.id].desired}} | active: {{service_health[s.id].active_sessions}}
-
+
+
+

+
+
{{s.name}}{% if not s.active %} выключен{% endif %}
+
+
+ {{service_health[s.id].health}} | {{service_health[s.id].running}} / {{service_health[s.id].desired}} | active: {{service_health[s.id].active_sessions}}
+
+
-
+
{{ "⏻" if s.active else "▶" }}
+
+ {% else %}
+
RDP-сервисов пока нет
{% endfor %}
-
Редактирование RDP
+
Новый RDP-сервис
Используйте поля host/port/user. Поле target собирается автоматически.
@@ -529,8 +496,8 @@
Save
- Prewarm now
- Delete
+ Prewarm now
+ Delete
Clear
@@ -556,7 +523,7 @@
-
@@ -639,7 +575,7 @@
- {{ "⏻" if s.active else "▶" }}
+ {{ "⏻" if s.active else "▶" }}
{% else %}
Пилотов пока нет
@@ -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();
}