feat: service credentials (login/password) on dashboard cards with copy button
This commit is contained in:
+7
-1
@@ -197,6 +197,8 @@ class Service(Base):
|
|||||||
type: Mapped[ServiceType] = mapped_column(Enum(ServiceType), index=True)
|
type: Mapped[ServiceType] = mapped_column(Enum(ServiceType), index=True)
|
||||||
target: Mapped[str] = mapped_column(Text)
|
target: Mapped[str] = mapped_column(Text)
|
||||||
comment: Mapped[str] = mapped_column(Text, default="")
|
comment: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
svc_login: Mapped[str] = mapped_column(String(256), default="")
|
||||||
|
svc_password: Mapped[str] = mapped_column(String(256), default="")
|
||||||
icon_path: Mapped[str] = mapped_column(Text, default="")
|
icon_path: Mapped[str] = mapped_column(Text, default="")
|
||||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
warm_pool_size: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
@@ -1255,6 +1257,8 @@ def ensure_schema_compatibility() -> None:
|
|||||||
with engine.begin() as conn:
|
with engine.begin() as conn:
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS warm_pool_size INTEGER NOT NULL DEFAULT 0"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS warm_pool_size INTEGER NOT NULL DEFAULT 0"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS comment TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS comment TEXT NOT NULL DEFAULT ''"))
|
||||||
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_login VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
|
||||||
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
text(
|
text(
|
||||||
@@ -2677,6 +2681,8 @@ def create_service(payload: dict, request: Request, _: User = Depends(require_ad
|
|||||||
type=service_type,
|
type=service_type,
|
||||||
target=target,
|
target=target,
|
||||||
comment=payload.get("comment", ""),
|
comment=payload.get("comment", ""),
|
||||||
|
svc_login=payload.get("svc_login", ""),
|
||||||
|
svc_password=payload.get("svc_password", ""),
|
||||||
active=payload.get("active", True),
|
active=payload.get("active", True),
|
||||||
warm_pool_size=max(0, int(payload.get("warm_pool_size", 0))),
|
warm_pool_size=max(0, int(payload.get("warm_pool_size", 0))),
|
||||||
)
|
)
|
||||||
@@ -2741,7 +2747,7 @@ def edit_service(service_id: int, payload: dict, request: Request, _: User = Dep
|
|||||||
service = db.get(Service, service_id)
|
service = db.get(Service, service_id)
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail="Service not found")
|
raise HTTPException(status_code=404, detail="Service not found")
|
||||||
for key in ["name", "slug", "target", "active", "comment"]:
|
for key in ["name", "slug", "target", "active", "comment", "svc_login", "svc_password"]:
|
||||||
if key in payload:
|
if key in payload:
|
||||||
setattr(service, key, payload[key])
|
setattr(service, key, payload[key])
|
||||||
if "type" in payload:
|
if "type" in payload:
|
||||||
|
|||||||
@@ -344,6 +344,72 @@ button {
|
|||||||
border-color: #0f5b94;
|
border-color: #0f5b94;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.tile-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.svc-credentials {
|
||||||
|
background: linear-gradient(135deg, #f0f6fc 0%, #e8f2f9 100%);
|
||||||
|
border: 1px solid #c7d9ea;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
padding: 0.55rem 0.85rem 0.65rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
.svc-cred-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.svc-cred-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
color: #5a7d9a;
|
||||||
|
min-width: 3.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.svc-cred-value {
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #1a3a52;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.svc-cred-masked {
|
||||||
|
letter-spacing: .12em;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.svc-cred-copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #b0c8de;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #3a6d96;
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
transition: background .15s, color .15s, border-color .15s;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.svc-cred-copy::before { content: "\2398"; font-size: 0.9rem; }
|
||||||
|
.svc-cred-copy.copied { background: #1a8a4a; border-color: #1a8a4a; color: #fff; }
|
||||||
|
.svc-cred-copy.copied::before { content: "\2713"; }
|
||||||
|
.svc-cred-copy:hover:not(.copied) { background: #e6f0f9; border-color: #7aabcf; }
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -355,6 +421,7 @@ button {
|
|||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.06);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
|
transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.tile:hover {
|
.tile:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
@@ -546,6 +613,11 @@ button {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
|
.dashboard-page .svc-credentials {
|
||||||
|
background: rgba(224, 240, 252, 0.7);
|
||||||
|
border-color: rgba(180, 210, 235, 0.6);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.made-by-wrap {
|
.made-by-wrap {
|
||||||
@@ -643,6 +715,11 @@ button {
|
|||||||
background: rgba(255, 255, 255, 0.9) !important;
|
background: rgba(255, 255, 255, 0.9) !important;
|
||||||
border: 1px solid rgba(198, 218, 235, 0.9) !important;
|
border: 1px solid rgba(198, 218, 235, 0.9) !important;
|
||||||
}
|
}
|
||||||
|
.dashboard-page .svc-credentials {
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
background: rgba(232, 244, 253, 0.95) !important;
|
||||||
|
border-color: rgba(180, 210, 235, 0.9) !important;
|
||||||
|
}
|
||||||
.dashboard-page .panel {
|
.dashboard-page .panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -812,3 +889,7 @@ button {
|
|||||||
.tile-comment th { background: #eaf2fb; font-weight: 700; }
|
.tile-comment th { background: #eaf2fb; font-weight: 700; }
|
||||||
.tile-comment del { text-decoration: line-through; color: #7a9aaf; }
|
.tile-comment del { text-decoration: line-through; color: #7a9aaf; }
|
||||||
.tile-comment input[type=checkbox] { margin-right: 0.3em; }
|
.tile-comment input[type=checkbox] { margin-right: 0.3em; }
|
||||||
|
|
||||||
|
.tile-wrap:has(.svc-credentials) .tile {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
|
<input class="list-search" id="web_search" placeholder="Поиск WEB сервиса..." oninput="filterList('web_search', '#web_list .web-item')" />
|
||||||
<div class="list-box" id="web_list">
|
<div class="list-box" id="web_list">
|
||||||
{% for s in web_services %}
|
{% 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}})'>
|
<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}})'>
|
||||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
<div>
|
<div>
|
||||||
<div>{{s.name}}</div>
|
<div>{{s.name}}</div>
|
||||||
@@ -158,6 +158,16 @@
|
|||||||
<textarea id="w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
<textarea id="w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="field-col">
|
||||||
|
<span>Логин сервиса (показывается на карточке)</span>
|
||||||
|
<input id="w_svc_login" placeholder="Например: admin" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-col">
|
||||||
|
<span>Пароль сервиса (показывается на карточке)</span>
|
||||||
|
<input id="w_svc_password" placeholder="Пароль для входа в сервис" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="field-col">
|
<label class="field-col">
|
||||||
<span>Статус</span>
|
<span>Статус</span>
|
||||||
<select id="w_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="w_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
@@ -231,6 +241,8 @@
|
|||||||
<label class="field-col">
|
<label class="field-col">
|
||||||
<span>Описание для пользователя</span>
|
<span>Описание для пользователя</span>
|
||||||
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
<textarea id="new_w_comment" placeholder="Коротко: что это за сервис"></textarea>
|
||||||
|
<input id="new_w_svc_login" placeholder="Логин сервиса (необязательно)" />
|
||||||
|
<input id="new_w_svc_password" placeholder="Пароль сервиса (необязательно)" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="field-col">
|
<label class="field-col">
|
||||||
@@ -257,7 +269,7 @@
|
|||||||
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
|
<input class="list-search" id="rdp_search" placeholder="Поиск RDP сервиса..." oninput="filterList('rdp_search', '#rdp_list .rdp-item')" />
|
||||||
<div class="list-box" id="rdp_list">
|
<div class="list-box" id="rdp_list">
|
||||||
{% for s in rdp_services %}
|
{% 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}})'>
|
<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}})'>
|
||||||
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<img class="service-thumb" src="{{ s.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
<div>
|
<div>
|
||||||
<div>{{s.name}}</div>
|
<div>{{s.name}}</div>
|
||||||
@@ -288,6 +300,8 @@
|
|||||||
</select>
|
</select>
|
||||||
<input id="r_target" placeholder="Собранный target (авто)" />
|
<input id="r_target" placeholder="Собранный target (авто)" />
|
||||||
<textarea id="r_comment" placeholder="Описание для пользователя"></textarea>
|
<textarea id="r_comment" placeholder="Описание для пользователя"></textarea>
|
||||||
|
<input id="r_svc_login" placeholder="Логин сервиса (показывается на карточке)" />
|
||||||
|
<input id="r_svc_password" placeholder="Пароль сервиса (показывается на карточке)" />
|
||||||
<input id="r_pool" type="number" min="0" placeholder="Количество заранее прогретых слотов" />
|
<input id="r_pool" type="number" min="0" placeholder="Количество заранее прогретых слотов" />
|
||||||
<select id="r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
<select id="r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,6 +385,8 @@
|
|||||||
</select>
|
</select>
|
||||||
<input id="new_r_target" placeholder="Собранный target (авто)" />
|
<input id="new_r_target" placeholder="Собранный target (авто)" />
|
||||||
<textarea id="new_r_comment" placeholder="Описание для пользователя"></textarea>
|
<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_pool" type="number" min="0" value="1" 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>
|
<select id="new_r_active"><option value="true">active</option><option value="false">inactive</option></select>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,12 +725,14 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectWebService(id, name, slug, target, comment, iconPath, active) {
|
function selectWebService(id, name, slug, target, comment, iconPath, active, svcLogin, svcPassword) {
|
||||||
document.getElementById('w_id').value = id;
|
document.getElementById('w_id').value = id;
|
||||||
document.getElementById('w_name').value = name;
|
document.getElementById('w_name').value = name;
|
||||||
document.getElementById('w_slug').value = slug;
|
document.getElementById('w_slug').value = slug;
|
||||||
document.getElementById('w_target').value = target;
|
document.getElementById('w_target').value = target;
|
||||||
document.getElementById('w_comment').value = comment || '';
|
document.getElementById('w_comment').value = comment || '';
|
||||||
|
document.getElementById('w_svc_login').value = svcLogin || '';
|
||||||
|
document.getElementById('w_svc_password').value = svcPassword || '';
|
||||||
document.getElementById('w_active').value = String(active);
|
document.getElementById('w_active').value = String(active);
|
||||||
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
|
document.getElementById('w_icon_preview').src = iconPath || placeholderIcon;
|
||||||
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
|
setCategoryChecks('.w_cat', serviceCategoryMap[id] || []);
|
||||||
@@ -737,6 +755,8 @@
|
|||||||
type: 'WEB',
|
type: 'WEB',
|
||||||
target: document.getElementById('new_w_target').value,
|
target: document.getElementById('new_w_target').value,
|
||||||
comment: document.getElementById('new_w_comment').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,
|
||||||
category_ids: checkedCategoryIds('.new_w_cat'),
|
category_ids: checkedCategoryIds('.new_w_cat'),
|
||||||
active: document.getElementById('new_w_active').value === 'true',
|
active: document.getElementById('new_w_active').value === 'true',
|
||||||
});
|
});
|
||||||
@@ -753,6 +773,8 @@
|
|||||||
type: 'WEB',
|
type: 'WEB',
|
||||||
target: document.getElementById('w_target').value,
|
target: document.getElementById('w_target').value,
|
||||||
comment: document.getElementById('w_comment').value,
|
comment: document.getElementById('w_comment').value,
|
||||||
|
svc_login: document.getElementById('w_svc_login').value,
|
||||||
|
svc_password: document.getElementById('w_svc_password').value,
|
||||||
category_ids: checkedCategoryIds('.w_cat'),
|
category_ids: checkedCategoryIds('.w_cat'),
|
||||||
active: document.getElementById('w_active').value === 'true',
|
active: document.getElementById('w_active').value === 'true',
|
||||||
});
|
});
|
||||||
@@ -760,7 +782,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearWebForm() {
|
function clearWebForm() {
|
||||||
['w_id','w_name','w_slug','w_target','w_comment'].forEach(id => document.getElementById(id).value = '');
|
['w_id','w_name','w_slug','w_target','w_comment','w_svc_login','w_svc_password'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.getElementById('w_active').value = 'true';
|
document.getElementById('w_active').value = 'true';
|
||||||
setCategoryChecks('.w_cat', []);
|
setCategoryChecks('.w_cat', []);
|
||||||
document.getElementById('w_icon_preview').src = placeholderIcon;
|
document.getElementById('w_icon_preview').src = placeholderIcon;
|
||||||
@@ -843,7 +865,7 @@
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool) {
|
function selectRdpService(id, name, slug, target, comment, iconPath, active, pool, svcLogin, svcPassword) {
|
||||||
const cfg = parseRdpTarget(target);
|
const cfg = parseRdpTarget(target);
|
||||||
document.getElementById('r_id').value = id;
|
document.getElementById('r_id').value = id;
|
||||||
document.getElementById('r_name').value = name;
|
document.getElementById('r_name').value = name;
|
||||||
@@ -854,6 +876,8 @@
|
|||||||
document.getElementById('r_domain').value = cfg.domain;
|
document.getElementById('r_domain').value = cfg.domain;
|
||||||
document.getElementById('r_sec').value = cfg.sec;
|
document.getElementById('r_sec').value = cfg.sec;
|
||||||
document.getElementById('r_comment').value = comment || '';
|
document.getElementById('r_comment').value = comment || '';
|
||||||
|
document.getElementById('r_svc_login').value = svcLogin || '';
|
||||||
|
document.getElementById('r_svc_password').value = svcPassword || '';
|
||||||
document.getElementById('r_active').value = String(active);
|
document.getElementById('r_active').value = String(active);
|
||||||
document.getElementById('r_pool').value = pool;
|
document.getElementById('r_pool').value = pool;
|
||||||
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
|
setCategoryChecks('.r_cat', serviceCategoryMap[id] || []);
|
||||||
@@ -873,6 +897,8 @@
|
|||||||
type: 'RDP',
|
type: 'RDP',
|
||||||
target,
|
target,
|
||||||
comment: document.getElementById('new_r_comment').value,
|
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,
|
||||||
category_ids: checkedCategoryIds('.new_r_cat'),
|
category_ids: checkedCategoryIds('.new_r_cat'),
|
||||||
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
|
warm_pool_size: parseInt(document.getElementById('new_r_pool').value || '0', 10),
|
||||||
active: document.getElementById('new_r_active').value === 'true',
|
active: document.getElementById('new_r_active').value === 'true',
|
||||||
@@ -890,6 +916,8 @@
|
|||||||
type: 'RDP',
|
type: 'RDP',
|
||||||
target,
|
target,
|
||||||
comment: document.getElementById('r_comment').value,
|
comment: document.getElementById('r_comment').value,
|
||||||
|
svc_login: document.getElementById('r_svc_login').value,
|
||||||
|
svc_password: document.getElementById('r_svc_password').value,
|
||||||
category_ids: checkedCategoryIds('.r_cat'),
|
category_ids: checkedCategoryIds('.r_cat'),
|
||||||
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
|
warm_pool_size: parseInt(document.getElementById('r_pool').value || '0', 10),
|
||||||
active: document.getElementById('r_active').value === 'true',
|
active: document.getElementById('r_active').value === 'true',
|
||||||
@@ -898,7 +926,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearRdpForm() {
|
function clearRdpForm() {
|
||||||
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool'].forEach(id => document.getElementById(id).value = '');
|
['r_id','r_name','r_slug','r_target','r_host','r_port','r_domain','r_comment','r_pool','r_svc_login','r_svc_password'].forEach(id => document.getElementById(id).value = '');
|
||||||
document.getElementById('rdp_slots_box').style.display = 'none';
|
document.getElementById('rdp_slots_box').style.display = 'none';
|
||||||
document.getElementById('r_sec').value = '';
|
document.getElementById('r_sec').value = '';
|
||||||
document.getElementById('r_active').value = 'true';
|
document.getElementById('r_active').value = 'true';
|
||||||
|
|||||||
@@ -77,23 +77,43 @@
|
|||||||
<section class="grid service-grid">
|
<section class="grid service-grid">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
{% set svc_cats = service_categories.get(service.id, []) %}
|
{% set svc_cats = service_categories.get(service.id, []) %}
|
||||||
<a class="tile" href="/go/{{ service.slug }}">
|
<div class="tile-wrap">
|
||||||
<div class="tile-icon-box">
|
<a class="tile" href="/go/{{ service.slug }}">
|
||||||
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
<div class="tile-icon-box">
|
||||||
</div>
|
<img class="tile-icon" src="{{ service.icon_path or '/static/service-placeholder.svg' }}" alt="icon" />
|
||||||
<h3>{{ service.name }}</h3>
|
|
||||||
<p>Открыть сервис</p>
|
|
||||||
{% if service.comment %}
|
|
||||||
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if svc_cats %}
|
|
||||||
<div class="service-categories">
|
|
||||||
{% for category in svc_cats %}
|
|
||||||
<span class="service-cat-badge">{{ category.name }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p>Открыть сервис</p>
|
||||||
|
{% if service.comment %}
|
||||||
|
<small class="tile-comment">{{ service_comment_html.get(service.id, '') }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if svc_cats %}
|
||||||
|
<div class="service-categories">
|
||||||
|
{% for category in svc_cats %}
|
||||||
|
<span class="service-cat-badge">{{ category.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if service.svc_login or service.svc_password %}
|
||||||
|
<div class="svc-credentials">
|
||||||
|
{% if service.svc_login %}
|
||||||
|
<div class="svc-cred-row">
|
||||||
|
<span class="svc-cred-label">Логин</span>
|
||||||
|
<span class="svc-cred-value">{{ service.svc_login }}</span>
|
||||||
|
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_login }}" title="Копировать логин"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if service.svc_password %}
|
||||||
|
<div class="svc-cred-row">
|
||||||
|
<span class="svc-cred-label">Пароль</span>
|
||||||
|
<span class="svc-cred-value svc-cred-masked">{{ service.svc_password }}</span>
|
||||||
|
<button class="svc-cred-copy" type="button" data-copy="{{ service.svc_password }}" title="Копировать пароль"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
{% if selected_category_slug %}
|
{% if selected_category_slug %}
|
||||||
@@ -194,5 +214,21 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.svc-cred-copy').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
const text = btn.dataset.copy;
|
||||||
|
try { await navigator.clipboard.writeText(text); } catch(_) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text; ta.style.position='fixed'; ta.style.opacity='0';
|
||||||
|
document.body.appendChild(ta); ta.select();
|
||||||
|
document.execCommand('copy'); document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(() => btn.classList.remove('copied'), 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user