The «Перенос» tab moves a panel + VPN server to a panel-only server without users noticing: the new panel issues a one-time awgmove:// code, the old one sends its ui.db, subscription keys and hub certificate encrypted with it, and becomes node #0 of the new panel. Server id 0 is kept, so config servers[] is byte-identical. The old server relays its former ports (awg-relay, PROXY v1 header trusted only from RELAY_FROM), so sen:// links, nodes and the API keep working. The same is available from the CLI: migrate export/import/cancel, rehome. Nodes now learn the panel's address: the hub sends it in ready, the agent stores it in CORE_ENDPOINT, dials it first and confirms with core_ok. The new panel shows when the old server can be turned off: devices that got the new address, nodes that confirmed it, master keys never opened. «Серверы для всех» on the master keys tab adds or removes servers on every live key in one go, with a dry run that warns about API keys that would lose sight of some keys, and sets the servers new keys get.
312 lines
13 KiB
CSS
312 lines
13 KiB
CSS
/*
|
|
Copyright (c) 2026 Ivan Vasilev
|
|
This source code is licensed under the MIT license found in the
|
|
LICENSE file in the root directory of this source tree.
|
|
*/
|
|
/* Стили вкладки «Мастер-ключи»: дерево «мастер-ключ → устройства».
|
|
Общая дизайн-система — в App.css. */
|
|
|
|
.mk-notice {
|
|
font-size: 13px;
|
|
color: var(--error-text);
|
|
background: var(--error-bg);
|
|
border-radius: 16px;
|
|
padding: 10px 16px;
|
|
}
|
|
.mk-empty { font-size: 14px; color: var(--neutral-text); text-align: center; padding: 48px 0; }
|
|
.mk-limit-input { width: 88px !important; }
|
|
|
|
/* ── таблица (десктоп) ── */
|
|
.mk-table td, .mk-table th { white-space: nowrap; }
|
|
.mk-table .td-actions { width: 150px; }
|
|
.mk-table .mk-device-id { display: inline-block; max-width: 170px; vertical-align: middle; }
|
|
.mk-table th, .mk-table td { padding-left: 10px; padding-right: 10px; }
|
|
.mk-table th:first-child, .mk-table td:first-child { padding-left: 20px; }
|
|
.mk-table th:last-child, .mk-table td:last-child { padding-right: 16px; }
|
|
.mk-tr-master td { background: var(--surface-container); font-weight: 500; padding-top: 6px; padding-bottom: 6px; }
|
|
.mk-tr-master:hover td { background: var(--surface-container); }
|
|
.mk-name { display: inline-flex; align-items: center; gap: 8px; color: var(--primary); }
|
|
.mk-master-uuid {
|
|
display: block;
|
|
font-family: var(--mono); font-size: 12px; font-weight: 400;
|
|
color: var(--neutral-text);
|
|
overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.mk-name .mk-caret { margin-left: -8px; }
|
|
.mk-tr-master .mk-name { color: var(--on-surface); }
|
|
.mk-tr-master .mk-name svg { color: var(--primary); }
|
|
.mk-tr-tools td { background: var(--surface-container); padding-top: 4px; padding-bottom: 12px; } /* слева/справа — общие отступы колонок таблицы: блок стоит под заголовком «Имя» */
|
|
.mk-tr-tools:hover { background: transparent; }
|
|
.mk-tr-device td, .mk-tr-free td { padding-top: 10px; padding-bottom: 10px; }
|
|
.mk-flag { color: var(--neutral-text); font-size: 12px; }
|
|
/* чипы приоритета и торрентов в одной ячейке — как во вкладке «Пользователи» */
|
|
.mk-table .user-chips { display: inline-flex; flex-wrap: nowrap; gap: 6px; }
|
|
.mk-ip { font-family: var(--mono); }
|
|
/* иерархия: линия слева от имени и «ножка» к каждой строке; у последней линия обрывается */
|
|
.mk-tr-device td:first-child, .mk-tr-free td:first-child {
|
|
background: linear-gradient(var(--outline), var(--outline)) 30px 0 / 2px 100% no-repeat;
|
|
}
|
|
.mk-tr-device.mk-last td:first-child, .mk-tr-free.mk-last td:first-child { background-size: 2px 50%; }
|
|
.mk-indent { position: relative; display: inline-block; padding-left: 36px; }
|
|
.mk-indent::before {
|
|
content: ''; position: absolute; left: 10px; top: 50%; width: 16px;
|
|
border-top: 2px solid var(--outline);
|
|
}
|
|
.mk-tr-free td { color: var(--neutral-dot); font-size: 12px; }
|
|
.mk-tr-free:hover { background: transparent; }
|
|
|
|
/* ── дерево (мобильный) ── */
|
|
.mk-tree, .mk-children { list-style: none; margin: 0; padding: 0; }
|
|
.mk-tree { display: none; flex-direction: column; gap: 12px; } /* только мобильный */
|
|
|
|
.mk-root {
|
|
background: var(--surface-bright);
|
|
border: 1px solid var(--outline-variant);
|
|
border-radius: var(--r-card);
|
|
padding: 12px 18px 14px;
|
|
}
|
|
|
|
/* родитель */
|
|
.mk-master { display: flex; align-items: center; gap: 10px; color: var(--primary); min-height: 42px; }
|
|
.mk-master-label {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--on-surface);
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mk-caret {
|
|
width: 28px; height: 28px;
|
|
border: 0; border-radius: 50%;
|
|
background: transparent;
|
|
color: var(--neutral-text);
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
transition: transform 0.15s;
|
|
}
|
|
.mk-caret:hover { background: var(--state-neutral); }
|
|
.mk-caret--folded { transform: rotate(-90deg); }
|
|
|
|
.mk-tools {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
gap: 12px;
|
|
align-items: stretch;
|
|
margin: 6px 0 8px;
|
|
}
|
|
/* два одинаковых блока: слева настройки ключа, справа серверы */
|
|
.mk-box {
|
|
display: flex; flex-direction: column; gap: 10px;
|
|
background: var(--surface-bright);
|
|
border: 1px solid var(--outline-variant);
|
|
border-radius: 16px;
|
|
padding: 12px 16px 14px;
|
|
}
|
|
.mk-box-title { margin: 0; font-size: 13px; font-weight: 500; color: var(--label); }
|
|
.mk-box .mk-box-title { margin-bottom: 2px; }
|
|
/* строка настройки: слева название и пояснение, справа контрол */
|
|
.mk-setting {
|
|
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
|
padding: 10px 0;
|
|
border-top: 1px solid var(--outline-variant);
|
|
}
|
|
.mk-box-title + .mk-setting { border-top: 0; padding-top: 4px; }
|
|
.mk-setting:last-child { padding-bottom: 0; }
|
|
.mk-setting-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
.mk-setting-name { font-size: 14px; font-weight: 500; color: var(--on-surface); }
|
|
.mk-setting-hint { font-size: 12px; color: var(--neutral-text); }
|
|
.mk-setting-ctl { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
.mk-uuid {
|
|
font-family: var(--mono); font-size: 12px; color: var(--on-surface);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
|
|
}
|
|
.mk-setting-ctl .btn { height: 36px; padding: 0 16px; font-size: 13px; }
|
|
.mk-limit-input { height: 36px; width: 80px !important; text-align: center; }
|
|
|
|
/* прогресс занятых слотов с числом внутри; полный — цветом «ошибка».
|
|
Текст дублируется: тёмный на дорожке, светлый поверх заливки (обрезается ею). */
|
|
.mk-meter {
|
|
--mk-w: 136px;
|
|
--meter-fill: var(--success-dot); /* < 2/3 — зелёный */
|
|
--meter-on: #2a2118; /* цвет числа поверх заливки */
|
|
position: relative;
|
|
width: var(--mk-w); height: 20px;
|
|
border-radius: var(--r-pill);
|
|
background: var(--outline-variant);
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
}
|
|
.mk-meter-fill {
|
|
position: absolute; left: 0; top: 0; bottom: 0;
|
|
background: var(--meter-fill);
|
|
overflow: hidden;
|
|
transition: width 0.25s, background 0.25s;
|
|
}
|
|
/* числа по краям внутри полоски: текущее слева, лимит справа */
|
|
.mk-meter-text {
|
|
position: absolute; left: 0; top: 0;
|
|
width: var(--mk-w); height: 100%;
|
|
box-sizing: border-box;
|
|
padding: 0 11px;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
font-size: 12px; font-weight: 500;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--on-surface);
|
|
}
|
|
.mk-meter-text--on { color: var(--meter-on); }
|
|
.mk-meter--warn { --meter-fill: var(--warn-dot); } /* от 2/3 — оранжевый */
|
|
.mk-meter--full { --meter-fill: var(--error-dot); --meter-on: #fff; } /* лимит — красный */
|
|
:root[data-theme="dark"] .mk-meter--full { --meter-on: #2a2118; }
|
|
|
|
.mk-box .tip-wrap { width: 100%; }
|
|
|
|
/* дети: вертикальная линия слева и «ножка» к каждому узлу */
|
|
.mk-children { margin: 8px 0 0 18px; padding-left: 28px; border-left: 2px solid var(--outline); }
|
|
.mk-node { position: relative; padding: 4px 0; }
|
|
.mk-node::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -28px; top: 50%;
|
|
width: 28px;
|
|
border-top: 2px solid var(--outline);
|
|
}
|
|
/* последний узел: линия родителя обрывается на его «ножке» */
|
|
.mk-node:last-child::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: -30px; top: calc(50% + 1px); bottom: 0;
|
|
width: 2px;
|
|
background: var(--surface-bright);
|
|
}
|
|
|
|
.mk-device {
|
|
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
background: var(--surface-container);
|
|
border-radius: 14px;
|
|
padding: 8px 14px;
|
|
}
|
|
.mk-device-main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 160px; }
|
|
.mk-device-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
.mk-device-id {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
color: var(--neutral-text);
|
|
background: var(--surface-bright);
|
|
border-radius: var(--r-pill);
|
|
padding: 1px 8px;
|
|
min-width: 0;
|
|
max-width: 200px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mk-device-name { font-size: 14px; font-weight: 500; color: var(--on-surface); }
|
|
.mk-device-sub { font-size: 12px; color: var(--neutral-text); }
|
|
.mk-device-ip {
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
width: 100%;
|
|
color: var(--neutral-text);
|
|
}
|
|
.mk-device-ip .mk-ip { font-size: 13px; }
|
|
.mk-device-ip .tip-wrap { width: auto; }
|
|
.mk-device-traffic { display: flex; gap: 12px; font-family: var(--mono); font-size: 12px; color: var(--neutral-text); }
|
|
.mk-metric { display: inline-flex; gap: 4px; }
|
|
.mk-metric i, .mk-metric b { font-style: normal; font-weight: 400; }
|
|
|
|
.mk-free {
|
|
border: 1px dashed var(--outline);
|
|
border-radius: 14px;
|
|
padding: 10px 14px;
|
|
font-size: 13px;
|
|
color: var(--neutral-dot);
|
|
}
|
|
|
|
/* ── мобильный (design.md, часть IV): одна колонка, тач-цели ≥ 44px, rx/tx блоками
|
|
50/50, действия рядом с большим пальцем ── */
|
|
@media (max-width: 640px) {
|
|
.mk-tree { display: flex; }
|
|
.mk-limit-input { width: 100% !important; }
|
|
|
|
.mk-root { padding: 12px; }
|
|
|
|
/* родитель: шапка, затем ряд действий на всю ширину */
|
|
.mk-master { flex-wrap: wrap; row-gap: 10px; }
|
|
.mk-master .actions { width: 100%; gap: 8px; }
|
|
.mk-caret { width: 44px; height: 44px; margin: -6px 0 -6px -8px; }
|
|
.mk-meter { --mk-w: 112px; height: 24px; }
|
|
|
|
/* настройки узла: поля и кнопки на всю ширину */
|
|
.mk-tools { grid-template-columns: minmax(0, 1fr); margin: 8px 0 4px; }
|
|
.mk-setting { flex-direction: column; align-items: stretch; gap: 8px; }
|
|
.mk-setting-ctl { width: 100%; }
|
|
.mk-setting-ctl .btn { flex: 1; height: 44px; }
|
|
.mk-setting-ctl .mk-limit-input { width: 100% !important; flex: 1; height: 44px; }
|
|
.mk-tools .btn { width: 100%; height: 44px; justify-content: center; }
|
|
|
|
/* дерево уже: отступы поменьше, «ножки» короче */
|
|
.mk-children { margin-left: 8px; padding-left: 14px; }
|
|
.mk-node { padding: 4px 0; }
|
|
.mk-node::before { left: -14px; width: 14px; }
|
|
.mk-node:last-child::after { left: -16px; }
|
|
|
|
/* устройство — карточка: заголовок + чип, метрики 50/50, ряд действий */
|
|
.mk-device { border-radius: 16px; padding: 12px; gap: 10px; }
|
|
.mk-device-main { flex: 1 1 0; }
|
|
.mk-device-title { flex-wrap: wrap; row-gap: 2px; }
|
|
.mk-device-id { max-width: 100%; }
|
|
.mk-device-traffic { width: 100%; gap: 8px; }
|
|
.mk-metric {
|
|
flex: 1; flex-direction: column; gap: 2px;
|
|
background: var(--surface-bright);
|
|
border-radius: 16px; padding: 8px 12px;
|
|
}
|
|
.mk-metric i { font-family: var(--font, inherit); font-size: 12px; color: var(--neutral-text); }
|
|
.mk-metric-name::after { content: attr(data-sfx); }
|
|
.mk-metric b { font-size: 16px; font-weight: 500; color: var(--on-surface); }
|
|
|
|
/* кнопки действий: контурные, на всю ширину ряда, 44px */
|
|
.mk-device .actions { width: 100%; gap: 8px; }
|
|
.mk-master .btn-icon, .mk-device .btn-icon {
|
|
flex: 1; width: auto; height: 44px;
|
|
border: 1px solid var(--outline);
|
|
border-radius: var(--r-pill);
|
|
}
|
|
.mk-master .btn-icon--danger, .mk-device .btn-icon--danger { border-color: var(--error-dot); }
|
|
|
|
.mk-free { padding: 12px; font-size: 13px; }
|
|
}
|
|
|
|
/* «Серверы для всех» (BulkServersModal) */
|
|
.bs-modal { max-width: 620px; }
|
|
.bs-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.bs-row {
|
|
display: flex; align-items: center; gap: 10px;
|
|
background: var(--surface-container); border-radius: 12px; padding: 8px 8px 8px 14px;
|
|
font-size: 13px; color: var(--on-surface);
|
|
}
|
|
.bs-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
|
|
.bs-sub { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: var(--neutral-text); }
|
|
.bs-sub .server-pick-ip { margin-left: 0; }
|
|
.bs-opts { display: flex; gap: 4px; flex-shrink: 0; }
|
|
.bs-opt {
|
|
min-height: 36px; padding: 0 12px;
|
|
font: inherit; font-size: 13px; color: var(--on-surface);
|
|
background: var(--surface-bright);
|
|
border: 1px solid var(--outline); border-radius: var(--r-pill);
|
|
cursor: pointer; white-space: nowrap;
|
|
}
|
|
.bs-opt:hover { background: var(--state-primary); }
|
|
.bs-opt--on { background: var(--primary-container); color: var(--on-primary-container); border-color: transparent; font-weight: 500; }
|
|
.bs-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--on-surface); cursor: pointer; }
|
|
.bs-check input { accent-color: var(--primary); width: 18px; height: 18px; margin: 0; }
|
|
@media (max-width: 640px) {
|
|
.bs-row { flex-wrap: wrap; }
|
|
.bs-opts { width: 100%; }
|
|
.bs-opt { flex: 1; }
|
|
}
|
|
.bs-modal .btn:disabled { opacity: .5; cursor: default; }
|