Add Go rewrite with Postgres 17 and Dokploy Docker Compose
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* Amnezia Admin — Cyberpunk theme overrides
|
||||
*/
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: #0b0d1a; }
|
||||
::-webkit-scrollbar-thumb { background: #252b55; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--admin-accent); }
|
||||
|
||||
/* Panels & cards */
|
||||
.admin-content .card,
|
||||
.dash-panel,
|
||||
.sl-panel {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.12);
|
||||
background: linear-gradient(160deg, rgba(13, 16, 37, 0.95) 0%, rgba(17, 22, 51, 0.88) 100%);
|
||||
box-shadow: var(--admin-shadow);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.admin-content .card:hover,
|
||||
.dash-panel:hover,
|
||||
.sl-panel:hover {
|
||||
border-color: rgba(0, 240, 255, 0.22);
|
||||
}
|
||||
|
||||
.admin-content .card-header h2,
|
||||
.dash-panel-head h2,
|
||||
.sl-panel__head h2 {
|
||||
font-family: var(--admin-font-display);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #e0e6ff;
|
||||
}
|
||||
|
||||
.dash-panel-head i,
|
||||
.sl-panel__head i,
|
||||
.admin-content .card-header > i {
|
||||
color: var(--admin-accent);
|
||||
filter: drop-shadow(0 0 6px var(--admin-accent-glow));
|
||||
}
|
||||
|
||||
/* Stats tiles */
|
||||
.dash-stat {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.12);
|
||||
background: linear-gradient(160deg, rgba(13, 16, 37, 0.95) 0%, rgba(17, 22, 51, 0.85) 100%);
|
||||
}
|
||||
|
||||
.dash-stat:hover {
|
||||
border-color: rgba(0, 240, 255, 0.35);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35), 0 0 20px rgba(0, 240, 255, 0.08);
|
||||
}
|
||||
|
||||
.dash-stat-icon {
|
||||
border-radius: var(--admin-radius);
|
||||
}
|
||||
|
||||
.dash-stat-label {
|
||||
font-family: var(--admin-font-mono);
|
||||
letter-spacing: 0.1em;
|
||||
color: #555d80;
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.dash-action {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.1);
|
||||
background: rgba(17, 22, 51, 0.6);
|
||||
}
|
||||
|
||||
.dash-action:hover {
|
||||
border-color: rgba(0, 240, 255, 0.35);
|
||||
background: rgba(0, 240, 255, 0.06);
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.08);
|
||||
}
|
||||
|
||||
.dash-action--primary {
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
background: linear-gradient(135deg, rgba(0, 240, 255, 0.12) 0%, rgba(255, 45, 123, 0.06) 100%);
|
||||
}
|
||||
|
||||
.dash-action--primary:hover {
|
||||
border-color: rgba(0, 240, 255, 0.6);
|
||||
box-shadow: 0 0 28px rgba(0, 240, 255, 0.15);
|
||||
}
|
||||
|
||||
.dash-action-icon {
|
||||
border-radius: var(--admin-radius);
|
||||
background: rgba(0, 240, 255, 0.1);
|
||||
color: var(--admin-accent);
|
||||
border: 1px solid rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
.dash-action-text strong {
|
||||
font-family: var(--admin-font-display);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #e0e6ff;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.dash-alert {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(255, 226, 61, 0.35);
|
||||
background: rgba(255, 226, 61, 0.06);
|
||||
color: #ffe23d;
|
||||
}
|
||||
|
||||
/* Code & mono */
|
||||
.admin-content code,
|
||||
.code-inline,
|
||||
.dash-info-user code,
|
||||
.sl-url-result__text,
|
||||
.sl-id,
|
||||
.sl-creation__cid,
|
||||
.mono {
|
||||
font-family: var(--admin-font-mono) !important;
|
||||
}
|
||||
|
||||
.code-inline,
|
||||
.admin-page-links .code-inline,
|
||||
.admin-page-settings .code-inline,
|
||||
.dash-info-user code {
|
||||
background: rgba(0, 240, 255, 0.08);
|
||||
color: var(--admin-accent);
|
||||
border: 1px solid rgba(0, 240, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.sl-table-wrap {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.15);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 240, 255, 0.04);
|
||||
}
|
||||
|
||||
.sl-table thead th {
|
||||
background: rgba(13, 16, 37, 0.98);
|
||||
color: var(--admin-accent) !important;
|
||||
font-family: var(--admin-font-mono);
|
||||
letter-spacing: 0.1em;
|
||||
border-bottom-color: rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
.sl-table tbody td {
|
||||
color: #c8d0f0 !important;
|
||||
border-bottom-color: rgba(26, 31, 61, 0.8);
|
||||
}
|
||||
|
||||
.sl-table tbody tr:hover td {
|
||||
background: rgba(0, 240, 255, 0.04);
|
||||
}
|
||||
|
||||
.sl-id {
|
||||
color: var(--admin-accent-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sl-url-link {
|
||||
color: #8892b8;
|
||||
}
|
||||
|
||||
.sl-url-link:hover {
|
||||
color: var(--admin-accent);
|
||||
text-shadow: 0 0 8px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
.sl-input,
|
||||
.admin-content .form-control,
|
||||
.admin-content .form-select,
|
||||
.admin-content input[type="text"],
|
||||
.admin-content input[type="number"],
|
||||
.admin-content input[type="date"],
|
||||
.admin-content input[type="datetime-local"],
|
||||
.admin-content input[type="search"],
|
||||
.admin-content textarea,
|
||||
.admin-content select {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.18);
|
||||
background: rgba(5, 6, 15, 0.8);
|
||||
color: #e0e6ff;
|
||||
font-family: var(--admin-font-body);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.sl-input:focus,
|
||||
.admin-content .form-control:focus,
|
||||
.admin-content .form-select:focus,
|
||||
.admin-content input:focus,
|
||||
.admin-content textarea:focus,
|
||||
.admin-content select:focus {
|
||||
border-color: var(--admin-accent);
|
||||
box-shadow: 0 0 0 2px rgba(0, 240, 255, 0.15), 0 0 16px rgba(0, 240, 255, 0.1);
|
||||
background: rgba(5, 6, 15, 0.95);
|
||||
color: #e0e6ff;
|
||||
}
|
||||
|
||||
.sl-search__bar {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.18);
|
||||
background: rgba(5, 6, 15, 0.7);
|
||||
}
|
||||
|
||||
.sl-search__bar:focus-within {
|
||||
border-color: rgba(0, 240, 255, 0.45);
|
||||
box-shadow: 0 0 20px rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
.sl-search__btn {
|
||||
background: rgba(0, 240, 255, 0.15);
|
||||
color: var(--admin-accent);
|
||||
border-radius: var(--admin-radius);
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sl-search__btn:hover {
|
||||
background: rgba(0, 240, 255, 0.25);
|
||||
box-shadow: 0 0 16px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.sl-btn-generate,
|
||||
.sl-btn-save-timing,
|
||||
.sl-btn-cleanup,
|
||||
.sl-btn-migrate,
|
||||
.admin-content .btn-primary {
|
||||
border-radius: var(--admin-radius);
|
||||
font-family: var(--admin-font-display);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid rgba(0, 240, 255, 0.45);
|
||||
background: linear-gradient(135deg, rgba(0, 240, 255, 0.18) 0%, rgba(0, 240, 255, 0.06) 100%);
|
||||
color: var(--admin-accent);
|
||||
box-shadow: 0 0 16px rgba(0, 240, 255, 0.1);
|
||||
transition: box-shadow 0.15s, border-color 0.15s, transform 0.12s;
|
||||
}
|
||||
|
||||
.sl-btn-generate:hover,
|
||||
.sl-btn-save-timing:hover,
|
||||
.sl-btn-cleanup:hover:not(:disabled),
|
||||
.sl-btn-migrate:hover,
|
||||
.admin-content .btn-primary:hover {
|
||||
border-color: var(--admin-accent);
|
||||
background: linear-gradient(135deg, rgba(0, 240, 255, 0.28) 0%, rgba(255, 45, 123, 0.1) 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 0 24px var(--admin-accent-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.sl-btn-copy,
|
||||
.admin-content .btn-outline-secondary {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.25);
|
||||
color: #8892b8;
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.sl-btn-copy:hover,
|
||||
.admin-content .btn-outline-secondary:hover {
|
||||
border-color: var(--admin-accent);
|
||||
color: var(--admin-accent);
|
||||
background: rgba(0, 240, 255, 0.06);
|
||||
}
|
||||
|
||||
.sl-chip {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.2);
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sl-chip.active,
|
||||
.sl-chip:hover {
|
||||
border-color: var(--admin-accent);
|
||||
background: rgba(0, 240, 255, 0.12);
|
||||
color: var(--admin-accent);
|
||||
box-shadow: 0 0 12px rgba(0, 240, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Config cards */
|
||||
.sl-creation {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.12);
|
||||
background: rgba(5, 6, 15, 0.5);
|
||||
}
|
||||
|
||||
.sl-creation:hover {
|
||||
border-color: rgba(0, 240, 255, 0.25);
|
||||
}
|
||||
|
||||
.sl-creation--deleted {
|
||||
border-color: rgba(255, 56, 96, 0.2);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sl-configs-details summary {
|
||||
font-family: var(--admin-font-mono);
|
||||
color: #8892b8;
|
||||
}
|
||||
|
||||
.sl-configs-details[open] summary {
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
|
||||
/* Server sidebar (configs page) */
|
||||
.sl-sidebar {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.15);
|
||||
background: rgba(13, 16, 37, 0.9);
|
||||
}
|
||||
|
||||
.sl-sidebar__item.is-active,
|
||||
.sl-sidebar__item:hover {
|
||||
border-color: rgba(0, 240, 255, 0.35);
|
||||
background: rgba(0, 240, 255, 0.08);
|
||||
box-shadow: inset 3px 0 0 var(--admin-accent);
|
||||
}
|
||||
|
||||
.sl-sidebar__item.is-active .sl-sidebar__name {
|
||||
color: var(--admin-accent);
|
||||
text-shadow: 0 0 8px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.sl-pager__btn {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.18);
|
||||
background: rgba(5, 6, 15, 0.7);
|
||||
font-family: var(--admin-font-mono);
|
||||
}
|
||||
|
||||
.sl-pager__btn:hover:not(.is-disabled):not(.is-active) {
|
||||
border-color: var(--admin-accent);
|
||||
color: var(--admin-accent);
|
||||
box-shadow: 0 0 12px rgba(0, 240, 255, 0.12);
|
||||
}
|
||||
|
||||
.sl-pager__page.is-active {
|
||||
background: var(--admin-accent);
|
||||
color: #05060f;
|
||||
border-color: var(--admin-accent);
|
||||
box-shadow: 0 0 16px var(--admin-accent-glow);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Timing form modal */
|
||||
.sl-timing-form {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.25);
|
||||
background: linear-gradient(160deg, #0d1025 0%, #111633 100%);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6), 0 0 1px rgba(0, 240, 255, 0.3);
|
||||
}
|
||||
|
||||
.sl-timing-form__head strong {
|
||||
font-family: var(--admin-font-display);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
|
||||
.sl-timing-form__section {
|
||||
font-family: var(--admin-font-mono);
|
||||
color: var(--admin-accent-2);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.sl-form-module {
|
||||
border-color: rgba(0, 240, 255, 0.12);
|
||||
background: rgba(8, 12, 28, 0.5);
|
||||
}
|
||||
|
||||
.sl-form-module__head {
|
||||
font-family: var(--admin-font-mono);
|
||||
color: var(--admin-accent-2);
|
||||
letter-spacing: 0.1em;
|
||||
background: rgba(0, 240, 255, 0.04);
|
||||
border-bottom-color: rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
.sl-switch input:checked + .sl-switch__track {
|
||||
background: rgba(0, 240, 255, 0.85);
|
||||
border-color: var(--admin-accent);
|
||||
box-shadow: 0 0 14px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
.sl-switch-row--master {
|
||||
background: rgba(0, 240, 255, 0.05);
|
||||
border-color: rgba(0, 240, 255, 0.18);
|
||||
}
|
||||
|
||||
.sl-server-toolbar__btn {
|
||||
font-family: var(--admin-font-mono);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.sl-server-toolbar__count {
|
||||
font-family: var(--admin-font-mono);
|
||||
color: var(--admin-accent);
|
||||
border-color: rgba(0, 240, 255, 0.2);
|
||||
background: rgba(0, 240, 255, 0.06);
|
||||
}
|
||||
|
||||
.sl-server-pick:has(.js-server-pick:checked) {
|
||||
background: rgba(0, 240, 255, 0.07);
|
||||
}
|
||||
|
||||
.sl-btn-save-timing {
|
||||
font-family: var(--admin-font-display);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
background: transparent;
|
||||
color: var(--admin-accent);
|
||||
border: 1px solid var(--admin-accent);
|
||||
box-shadow: 0 0 16px rgba(0, 240, 255, 0.15);
|
||||
}
|
||||
|
||||
.sl-btn-save-timing:hover {
|
||||
background: rgba(0, 240, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* URL result */
|
||||
.sl-url-result {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 255, 157, 0.3);
|
||||
background: rgba(0, 255, 157, 0.05);
|
||||
}
|
||||
|
||||
.sl-url-result__label {
|
||||
color: #00ff9d;
|
||||
font-family: var(--admin-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Status filter pills */
|
||||
.sl-status-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.65rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sl-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.38rem 0.72rem;
|
||||
border-radius: var(--admin-radius);
|
||||
border: 1px solid rgba(0, 240, 255, 0.18);
|
||||
background: rgba(5, 6, 15, 0.7);
|
||||
color: #8892b8;
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
transition: border-color 0.15s, color 0.15s, box-shadow 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.sl-status-pill__count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.35rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
font-size: 0.68rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sl-status-pill:hover {
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
color: var(--admin-accent);
|
||||
box-shadow: 0 0 12px rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
.sl-status-pill.is-active {
|
||||
color: #05060f;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 16px rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
.sl-status-pill.is-active .sl-status-pill__count {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sl-status-pill--active.is-active {
|
||||
background: #00ff9d;
|
||||
box-shadow: 0 0 16px rgba(0, 255, 157, 0.35);
|
||||
}
|
||||
|
||||
.sl-status-pill--expired.is-active {
|
||||
background: #ffe23d;
|
||||
color: #1a1400;
|
||||
box-shadow: 0 0 16px rgba(255, 226, 61, 0.35);
|
||||
}
|
||||
|
||||
.sl-status-pill--expired.is-active .sl-status-pill__count {
|
||||
color: #1a1400;
|
||||
}
|
||||
|
||||
.sl-status-pill--cleaned.is-active {
|
||||
background: #ff3860;
|
||||
box-shadow: 0 0 16px rgba(255, 56, 96, 0.35);
|
||||
}
|
||||
|
||||
.sl-status-pill:not(.sl-status-pill--active):not(.sl-status-pill--expired):not(.sl-status-pill--cleaned).is-active {
|
||||
background: var(--admin-accent);
|
||||
box-shadow: 0 0 16px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
.sl-controls-bar .sl-toolbar {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sl-controls-bar {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.sl-status-filters {
|
||||
margin-bottom: 0;
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Controls bar */
|
||||
.sl-controls-bar {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.12);
|
||||
background: rgba(13, 16, 37, 0.6);
|
||||
}
|
||||
|
||||
.sl-toggle span {
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.78rem;
|
||||
color: #8892b8;
|
||||
}
|
||||
|
||||
/* Renewal codes page */
|
||||
.admin-page-renewal .rc-code-display,
|
||||
.admin-page-renewal .rc-code-pill {
|
||||
color: var(--admin-accent);
|
||||
font-family: var(--admin-font-mono);
|
||||
text-shadow: 0 0 8px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
.admin-page-renewal .rc-days-badge {
|
||||
color: #00ff9d;
|
||||
background: rgba(0, 255, 157, 0.1);
|
||||
border: 1px solid rgba(0, 255, 157, 0.25);
|
||||
border-radius: var(--admin-radius);
|
||||
font-family: var(--admin-font-mono);
|
||||
}
|
||||
|
||||
.admin-page-renewal .rc-usage-fill {
|
||||
background: linear-gradient(90deg, var(--admin-accent), var(--admin-accent-3));
|
||||
box-shadow: 0 0 8px var(--admin-accent-glow);
|
||||
}
|
||||
|
||||
/* Settings form */
|
||||
.admin-page-settings .form-label,
|
||||
.sl-field__label {
|
||||
font-family: var(--admin-font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #8892b8;
|
||||
}
|
||||
|
||||
/* Server labels page */
|
||||
.sl-server-row {
|
||||
border-radius: var(--admin-radius);
|
||||
border-color: rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
.sl-server-row:hover {
|
||||
border-color: rgba(0, 240, 255, 0.25);
|
||||
background: rgba(0, 240, 255, 0.03);
|
||||
}
|
||||
|
||||
.sl-hint a {
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
|
||||
.sl-hint--warn {
|
||||
color: #ffe23d;
|
||||
border-color: rgba(255, 226, 61, 0.25);
|
||||
background: rgba(255, 226, 61, 0.06);
|
||||
}
|
||||
|
||||
/* Icons glow on hover */
|
||||
.admin-content .fa-solid,
|
||||
.admin-content .fa-regular {
|
||||
transition: filter 0.15s;
|
||||
}
|
||||
|
||||
.sl-panel__head:hover i,
|
||||
.dash-panel-head:hover i {
|
||||
filter: drop-shadow(0 0 8px var(--admin-accent-glow));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Amnezia Admin — страница входа (изолирована от admin.css)
|
||||
*/
|
||||
.login-page {
|
||||
--login-bg: #070b14;
|
||||
--login-card: rgba(17, 24, 39, 0.92);
|
||||
--login-border: rgba(56, 189, 248, 0.22);
|
||||
--login-text: #f1f5f9;
|
||||
--login-muted: #94a3b8;
|
||||
--login-accent: #38bdf8;
|
||||
--login-accent-deep: #0ea5e9;
|
||||
--login-input-bg: #0f172a;
|
||||
--login-input-border: #334155;
|
||||
--login-glow: rgba(56, 189, 248, 0.35);
|
||||
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--login-text);
|
||||
background: var(--login-bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -10%, rgba(56, 189, 248, 0.18), transparent 55%),
|
||||
radial-gradient(ellipse 40% 35% at 100% 80%, rgba(99, 102, 241, 0.12), transparent 50%),
|
||||
radial-gradient(ellipse 35% 30% at 0% 90%, rgba(14, 165, 233, 0.1), transparent 45%),
|
||||
linear-gradient(165deg, #070b14 0%, #0c1222 45%, #070b14 100%);
|
||||
}
|
||||
|
||||
.login-page::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.35;
|
||||
background-image:
|
||||
linear-gradient(rgba(56, 189, 248, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(56, 189, 248, 0.04) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 20%, transparent 75%);
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--login-card);
|
||||
border: 1px solid var(--login-border);
|
||||
border-radius: 1.125rem;
|
||||
padding: 2rem 1.75rem 1.75rem;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04) inset,
|
||||
0 24px 64px rgba(0, 0, 0, 0.55),
|
||||
0 0 48px rgba(56, 189, 248, 0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
text-align: center;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.login-brand-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 1rem;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
color: var(--login-accent);
|
||||
background: linear-gradient(145deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.08));
|
||||
border: 1px solid rgba(56, 189, 248, 0.35);
|
||||
box-shadow: 0 0 28px var(--login-glow);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--login-text);
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--login-muted);
|
||||
}
|
||||
|
||||
.login-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-field label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--login-muted);
|
||||
}
|
||||
|
||||
.login-field label i {
|
||||
color: var(--login-accent);
|
||||
margin-right: 0.25rem;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.65rem 0.85rem;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
color: var(--login-text) !important;
|
||||
-webkit-text-fill-color: var(--login-text);
|
||||
background: var(--login-input-bg) !important;
|
||||
border: 1px solid var(--login-input-border);
|
||||
border-radius: 0.5rem;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.login-input::placeholder {
|
||||
color: #64748b;
|
||||
-webkit-text-fill-color: #64748b;
|
||||
}
|
||||
|
||||
.login-input:hover {
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.login-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--login-accent);
|
||||
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.7rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--login-accent-deep) 0%, #0284c7 100%);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s, box-shadow 0.15s, filter 0.15s;
|
||||
box-shadow: 0 4px 20px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
filter: brightness(1.08);
|
||||
box-shadow: 0 6px 28px rgba(14, 165, 233, 0.45);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 1.25rem 0;
|
||||
color: var(--login-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.login-divider::before,
|
||||
.login-divider::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--login-input-border);
|
||||
}
|
||||
|
||||
.login-btn-oauth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--login-text);
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: 1px solid var(--login-input-border);
|
||||
border-radius: 0.5rem;
|
||||
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.login-btn-oauth:hover {
|
||||
border-color: rgba(56, 189, 248, 0.45);
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
color: var(--login-accent);
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin: 1.25rem 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--login-muted);
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: var(--login-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.login-footer code {
|
||||
color: #7dd3fc;
|
||||
font-size: 0.78em;
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* Secure Link — портал пользователя (index, cabinet)
|
||||
*/
|
||||
.portal-page {
|
||||
--portal-bg: #070b14;
|
||||
--portal-card: rgba(17, 24, 39, 0.94);
|
||||
--portal-border: rgba(56, 189, 248, 0.22);
|
||||
--portal-accent: #38bdf8;
|
||||
--portal-accent-2: #a78bfa;
|
||||
--portal-text: #f1f5f9;
|
||||
--portal-muted: #94a3b8;
|
||||
--portal-ok: #34d399;
|
||||
--portal-warn: #fbbf24;
|
||||
--portal-err: #f87171;
|
||||
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--portal-text);
|
||||
background: var(--portal-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portal-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 45% at 20% -5%, rgba(56, 189, 248, 0.14), transparent 55%),
|
||||
radial-gradient(ellipse 50% 40% at 100% 100%, rgba(139, 92, 246, 0.12), transparent 50%),
|
||||
linear-gradient(165deg, #070b14 0%, #0c1222 50%, #070b14 100%);
|
||||
}
|
||||
|
||||
.portal-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.3;
|
||||
background-image:
|
||||
linear-gradient(rgba(56, 189, 248, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(56, 189, 248, 0.04) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
mask-image: radial-gradient(ellipse 80% 60% at 50% 30%, black 15%, transparent 70%);
|
||||
}
|
||||
|
||||
.portal-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.25rem 3rem;
|
||||
}
|
||||
|
||||
.portal-wrap--wide {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.portal-hero-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 1rem;
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
color: var(--portal-accent);
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
border: 1px solid var(--portal-border);
|
||||
box-shadow: 0 0 32px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
.portal-brand {
|
||||
font-family: "Orbitron", sans-serif;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--portal-accent);
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.portal-title {
|
||||
font-family: "Orbitron", sans-serif;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.portal-lead {
|
||||
color: var(--portal-muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.portal-auth-card {
|
||||
background: var(--portal-card);
|
||||
border: 1px solid var(--portal-border);
|
||||
border-radius: 1rem;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.portal-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(42, 51, 82, 0.8);
|
||||
}
|
||||
|
||||
.portal-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0.85rem 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
color: var(--portal-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.portal-tab:hover {
|
||||
color: var(--portal-text);
|
||||
background: rgba(56, 189, 248, 0.06);
|
||||
}
|
||||
|
||||
.portal-tab.is-active {
|
||||
color: var(--portal-accent);
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
box-shadow: inset 0 -2px 0 var(--portal-accent);
|
||||
}
|
||||
|
||||
.portal-form {
|
||||
padding: 1.35rem 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.portal-label {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.portal-optional {
|
||||
font-weight: 400;
|
||||
color: var(--portal-muted);
|
||||
}
|
||||
|
||||
.portal-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.55rem 0.75rem;
|
||||
margin-bottom: 0.85rem;
|
||||
font-size: 0.9rem;
|
||||
color: #f8fafc;
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.45rem;
|
||||
}
|
||||
|
||||
.portal-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--portal-accent);
|
||||
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
|
||||
}
|
||||
|
||||
.portal-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.6rem 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-family: inherit;
|
||||
transition: filter 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.portal-btn--primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #0ea5e9, #0284c7);
|
||||
box-shadow: 0 4px 20px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.portal-btn--accent {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #8b5cf6, #6366f1);
|
||||
box-shadow: 0 4px 18px rgba(139, 92, 246, 0.35);
|
||||
}
|
||||
|
||||
.portal-btn--outline {
|
||||
color: var(--portal-accent);
|
||||
background: transparent;
|
||||
border: 1px solid rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
|
||||
.portal-btn--ghost {
|
||||
color: var(--portal-muted);
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.portal-btn:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.portal-form-note {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--portal-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.portal-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.portal-alert--err {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid rgba(248, 113, 113, 0.35);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.portal-alert--ok {
|
||||
background: rgba(52, 211, 153, 0.1);
|
||||
border: 1px solid rgba(52, 211, 153, 0.35);
|
||||
color: #a7f3d0;
|
||||
}
|
||||
|
||||
.portal-alert--warn {
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.portal-footer {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.portal-footer a {
|
||||
color: var(--portal-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.portal-footer a:hover {
|
||||
color: var(--portal-accent);
|
||||
}
|
||||
|
||||
/* Cabinet */
|
||||
.portal-cabinet-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.portal-user-line {
|
||||
font-size: 0.85rem;
|
||||
color: var(--portal-muted);
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.portal-head-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.portal-panel {
|
||||
background: var(--portal-card);
|
||||
border: 1px solid rgba(42, 51, 82, 0.9);
|
||||
border-radius: 0.85rem;
|
||||
padding: 1.15rem 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.portal-panel--renew {
|
||||
border-color: rgba(167, 139, 250, 0.35);
|
||||
}
|
||||
|
||||
.portal-panel--warn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
border-color: rgba(251, 191, 36, 0.35);
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.portal-panel--empty {
|
||||
text-align: center;
|
||||
color: var(--portal-muted);
|
||||
}
|
||||
|
||||
.portal-panel__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.65rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.portal-panel__title i {
|
||||
color: var(--portal-accent);
|
||||
}
|
||||
|
||||
.portal-muted {
|
||||
color: var(--portal-muted);
|
||||
}
|
||||
|
||||
.portal-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.portal-grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.portal-links-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.portal-link-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border-radius: 0.55rem;
|
||||
border: 1px solid #334155;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.portal-link-card:hover {
|
||||
border-color: rgba(56, 189, 248, 0.45);
|
||||
}
|
||||
|
||||
.portal-link-card.is-active {
|
||||
border-color: var(--portal-accent);
|
||||
box-shadow: 0 0 20px rgba(56, 189, 248, 0.15);
|
||||
}
|
||||
|
||||
.portal-link-card__id {
|
||||
font-weight: 700;
|
||||
color: #7dd3fc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.portal-link-card__meta,
|
||||
.portal-link-card__uses {
|
||||
font-size: 0.75rem;
|
||||
color: var(--portal-muted);
|
||||
}
|
||||
|
||||
.portal-badge {
|
||||
align-self: flex-start;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.portal-badge--ok {
|
||||
background: rgba(52, 211, 153, 0.15);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(52, 211, 153, 0.35);
|
||||
}
|
||||
|
||||
.portal-badge--warn {
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
color: #fcd34d;
|
||||
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||
}
|
||||
|
||||
.portal-badge--muted {
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
color: #94a3b8;
|
||||
border: 1px solid rgba(100, 116, 139, 0.4);
|
||||
}
|
||||
|
||||
.portal-migrate-card {
|
||||
border: 1px solid rgba(42, 51, 82, 0.8);
|
||||
border-radius: 0.55rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}
|
||||
|
||||
.portal-migrate-card summary {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.portal-page--cabinet .server-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.55rem 0.75rem;
|
||||
margin-bottom: 0.35rem;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.45rem;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.portal-page--cabinet .server-item.is-selected {
|
||||
border-color: var(--portal-accent);
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
}
|
||||
|
||||
.portal-renew-form .portal-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.portal-sub-card .portal-stat-num {
|
||||
font-family: "Orbitron", sans-serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.portal-feature-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.85rem;
|
||||
color: var(--portal-muted);
|
||||
}
|
||||
|
||||
.portal-feature-list li {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.w-100 { width: 100%; }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
{{define "page_admin_configs"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Конфигурации"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-filter"></i> Фильтр</h2></div>
|
||||
<form method="get" action="{{appURL "admin/configs"}}" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<div>
|
||||
<label class="form-label">ID ссылки</label>
|
||||
<input class="form-control input-inline" type="number" name="link_id" value="{{if .LinkIDFilter}}{{.LinkIDFilter}}{{end}}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">ID сервера</label>
|
||||
<input class="form-control input-inline" type="number" name="server_id" value="{{if .ServerIDFilter}}{{.ServerIDFilter}}{{end}}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-ghost"><i class="fa-solid fa-magnifying-glass"></i> Показать</button>
|
||||
<a class="btn btn-ghost" href="{{appURL "admin/configs"}}">Сбросить</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-key"></i> Конфигурации ({{len .Rows}}{{if .Truncated}}+{{end}})</h2></div>
|
||||
{{if .Truncated}}<p class="hint">Показаны не все записи — сузьте фильтр по ссылке или серверу.</p>{{end}}
|
||||
{{if not .Rows}}
|
||||
<p class="hint">Ничего не найдено.</p>
|
||||
{{else}}
|
||||
<div class="tbl-wrap">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>ID</th><th>Ссылка</th><th>Сервер</th><th>Протокол</th><th>Имя</th><th>Создана</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
<tr>
|
||||
<td>#{{.ID}}</td>
|
||||
<td><a href="{{appURL "admin/links"}}?id={{.LinkID}}" class="mono">#{{.LinkID}}</a></td>
|
||||
<td>{{.ServerID}}</td>
|
||||
<td>{{.Protocol}}</td>
|
||||
<td class="mono">{{.ConnectionName}}</td>
|
||||
<td class="sl-muted">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
|
||||
<td>
|
||||
<form method="post" action="{{appURL "admin/configs"}}" onsubmit="return confirm('Удалить конфигурацию #{{.ID}}?');">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="delete_creation">
|
||||
<input type="hidden" name="link_id" value="{{.LinkID}}">
|
||||
<input type="hidden" name="creation_id" value="{{.ID}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,39 @@
|
||||
{{define "page_admin_index"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Дашборд"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
{{if .Maintenance}}<div class="alert-err"><i class="fa-solid fa-screwdriver-wrench"></i> Включён режим технических работ. <a href="{{appURL "admin/settings"}}">Отключить в настройках</a></div>{{end}}
|
||||
|
||||
<div class="stat-grid">
|
||||
<div class="stat-tile"><i class="fa-solid fa-link"></i><div><span class="stat-val">{{.TotalLinks}}</span><span class="stat-label">Всего ссылок</span></div></div>
|
||||
<div class="stat-tile"><i class="fa-solid fa-circle-check"></i><div><span class="stat-val">{{.ActiveLinks}}</span><span class="stat-label">Активные</span></div></div>
|
||||
<div class="stat-tile"><i class="fa-solid fa-hourglass-end"></i><div><span class="stat-val">{{.ExpiredLinks}}</span><span class="stat-label">Истёкшие</span></div></div>
|
||||
<div class="stat-tile"><i class="fa-solid fa-broom"></i><div><span class="stat-val">{{.CleanedLinks}}</span><span class="stat-label">Очищенные</span></div></div>
|
||||
</div>
|
||||
<div class="stat-grid stat-grid-compact">
|
||||
<div class="stat-tile"><i class="fa-solid fa-ban"></i><div><span class="stat-val">{{.LimitLinks}}</span><span class="stat-label">Лимит исчерпан</span></div></div>
|
||||
<div class="stat-tile"><i class="fa-solid fa-rotate"></i><div><span class="stat-val">{{.CodesCount}}</span><span class="stat-label">Кодов продления</span></div></div>
|
||||
<div class="stat-tile">
|
||||
<i class="fa-solid fa-server"></i>
|
||||
<div>
|
||||
<span class="stat-val">{{if .PanelOK}}OK{{else if .PanelURL}}Ошибка{{else}}—{{end}}</span>
|
||||
<span class="stat-label">Панель Amnezia</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .PanelErr}}<div class="alert-err"><i class="fa-solid fa-triangle-exclamation"></i> Панель недоступна: {{.PanelErr}}</div>{{end}}
|
||||
{{if not .PanelURL}}<div class="alert-err"><i class="fa-solid fa-triangle-exclamation"></i> URL панели и токен не заданы. <a href="{{appURL "admin/settings"}}">Перейти в настройки</a></div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-bolt"></i> Быстрые действия</h2></div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary" href="{{appURL "admin/links"}}"><i class="fa-solid fa-plus"></i> Ссылки</a>
|
||||
<a class="btn btn-ghost" href="{{appURL "admin/configs"}}"><i class="fa-solid fa-key"></i> Конфигурации</a>
|
||||
<a class="btn btn-ghost" href="{{appURL "admin/renewal"}}"><i class="fa-solid fa-rotate"></i> Коды продления</a>
|
||||
<a class="btn btn-ghost" href="{{appURL "admin/servers"}}"><i class="fa-solid fa-server"></i> Серверы</a>
|
||||
<a class="btn btn-ghost" href="{{appURL "admin/settings"}}"><i class="fa-solid fa-gear"></i> Настройки</a>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,180 @@
|
||||
{{define "page_admin_links"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Ссылки"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-plus"></i> Новая ссылка</h2></div>
|
||||
<form method="post" action="{{appURL "admin/links"}}" class="form-row-2">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div>
|
||||
<label class="form-label">Срок (дней от первого конфига)</label>
|
||||
<select class="form-select" name="days">
|
||||
{{range .AllowedDurations}}<option value="{{.}}">{{.}} дн.</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Лимит конфигов</label>
|
||||
<input class="form-control" type="number" name="max_uses" value="5" min="1" max="1000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Подписка до (необязательно)</label>
|
||||
<input class="form-control" type="date" name="subscription_until">
|
||||
</div>
|
||||
<div class="d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-plus"></i> Создать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-filter"></i> Список ссылок ({{.Total}})</h2></div>
|
||||
<form method="get" action="{{appURL "admin/links"}}" class="links-toolbar mb-2">
|
||||
<select class="form-select form-select-sm" name="status" style="max-width:220px;" onchange="this.form.submit()">
|
||||
<option value="" {{if eq2 .StatusFilter ""}}selected{{end}}>Все статусы</option>
|
||||
<option value="active" {{if eq2 .StatusFilter "active"}}selected{{end}}>Активные</option>
|
||||
<option value="expired" {{if eq2 .StatusFilter "expired"}}selected{{end}}>Истёкшие</option>
|
||||
<option value="limit" {{if eq2 .StatusFilter "limit"}}selected{{end}}>Лимит исчерпан</option>
|
||||
<option value="cleaned" {{if eq2 .StatusFilter "cleaned"}}selected{{end}}>Очищенные</option>
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{{if not .Links}}
|
||||
<p class="hint">Ссылок не найдено.</p>
|
||||
{{else}}
|
||||
<div class="tbl-wrap">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>ID</th><th>Токен</th><th>Статус</th><th>Использовано</th><th>Срок</th><th>Конфигов</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Links}}
|
||||
<tr>
|
||||
<td class="sl-td-id">#{{.ID}}</td>
|
||||
<td><a class="mono sl-url-link" href="{{appURL "share"}}?k={{.Token}}" target="_blank">{{.Token}}</a></td>
|
||||
<td>
|
||||
{{if eq2 .Status "active"}}<span class="badge badge-ok">активна</span>{{end}}
|
||||
{{if eq2 .Status "expired"}}<span class="badge badge-warn">истекла</span>{{end}}
|
||||
{{if eq2 .Status "limit"}}<span class="badge badge-info">лимит</span>{{end}}
|
||||
{{if eq2 .Status "cleaned"}}<span class="badge badge-err">очищена</span>{{end}}
|
||||
</td>
|
||||
<td>{{.UseCount}} / {{.MaxUses}}</td>
|
||||
<td class="sl-muted">{{.ExpiresLabel}}</td>
|
||||
<td>{{.ActiveConfigs}}</td>
|
||||
<td class="sl-td-actions">
|
||||
<a class="btn btn-sm btn-ghost" href="{{appURL "admin/links"}}?id={{.ID}}"><i class="fa-solid fa-eye"></i></a>
|
||||
<form method="post" action="{{appURL "admin/links"}}" style="display:inline" onsubmit="return confirm('Удалить ссылку #{{.ID}}?');">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="link_id" value="{{.ID}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sl-pager mt-2">
|
||||
{{if gt .Page 1}}<a class="btn btn-sm btn-ghost" href="{{appURL "admin/links"}}?status={{.StatusFilter}}&page={{sub .Page 1}}">« Назад</a>{{end}}
|
||||
<span class="hint d-inline-block mx-2">Страница {{.Page}}</span>
|
||||
{{if gt .Total (mul .Page .PerPage)}}<a class="btn btn-sm btn-ghost" href="{{appURL "admin/links"}}?status={{.StatusFilter}}&page={{add .Page 1}}">Вперёд »</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Detail}}
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-link"></i> Ссылка #{{.Detail.ID}}</h2></div>
|
||||
<p class="mono">{{appURL "share"}}?k={{.Detail.Token}}</p>
|
||||
|
||||
<form method="post" action="{{appURL "admin/links"}}" class="form-row-2">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="link_id" value="{{.Detail.ID}}">
|
||||
<div>
|
||||
<label class="form-label">Срок (дней)</label>
|
||||
<input class="form-control" type="number" name="validity_days" value="{{if .Detail.ValidityDays}}{{.Detail.ValidityDays}}{{else}}30{{end}}" min="1" max="3650">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Лимит конфигов</label>
|
||||
<input class="form-control" type="number" name="max_uses" value="{{.Detail.MaxUses}}" min="1" max="1000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Явная дата истечения (необязательно)</label>
|
||||
<input class="form-control" type="date" name="expires_at">
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="checkbox" name="floating_expiry" value="1" id="floating_expiry" checked>
|
||||
<label class="form-check-label hint" for="floating_expiry">Плавающий срок (от первого конфига)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Подписка до</label>
|
||||
<input class="form-control" type="date" name="subscription_until">
|
||||
</div>
|
||||
<div class="d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-floppy-disk"></i> Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{appURL "admin/links"}}" class="d-flex gap-2 align-items-end mt-3">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="extend">
|
||||
<input type="hidden" name="link_id" value="{{.Detail.ID}}">
|
||||
<input type="hidden" name="back_id" value="{{.Detail.ID}}">
|
||||
<div>
|
||||
<label class="form-label">Продлить на, дней</label>
|
||||
<input class="form-control input-inline" type="number" name="days" value="30" min="1" max="3650">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-ghost"><i class="fa-solid fa-rotate"></i> Продлить</button>
|
||||
</form>
|
||||
|
||||
{{if .AllServers}}
|
||||
<form method="post" action="{{appURL "admin/links"}}" class="mt-3">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="set_allowed_servers">
|
||||
<input type="hidden" name="link_id" value="{{.Detail.ID}}">
|
||||
<input type="hidden" name="back_id" value="{{.Detail.ID}}">
|
||||
<label class="form-label d-block">Разрешённые серверы (пусто = все)</label>
|
||||
<div class="tn-servers__scroll d-flex flex-wrap gap-2 mb-2">
|
||||
{{$allowed := .AllowedServerIDs}}
|
||||
{{range .AllServers}}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="server_ids[]" value="{{.ID}}" id="srv{{.ID}}" {{if intIn $allowed .ID}}checked{{end}}>
|
||||
<label class="form-check-label" for="srv{{.ID}}">{{.Label}}</label>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-ghost"><i class="fa-solid fa-floppy-disk"></i> Сохранить список серверов</button>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
<h3 class="config-section-title mt-4">Активные конфигурации ({{len .Creations}})</h3>
|
||||
{{if not .Creations}}<p class="hint">Пока нет.</p>{{end}}
|
||||
{{range .Creations}}
|
||||
<div class="creation-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="mono">#{{.ID}} · {{.ConnectionName}} · сервер {{.ServerID}} · {{.Protocol}}</span>
|
||||
<form method="post" action="{{appURL "admin/links"}}" onsubmit="return confirm('Удалить конфигурацию #{{.ID}}?');">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="delete_creation">
|
||||
<input type="hidden" name="link_id" value="{{$.Detail.ID}}">
|
||||
<input type="hidden" name="creation_id" value="{{.ID}}">
|
||||
<input type="hidden" name="back_id" value="{{$.Detail.ID}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .DeletedCreations}}
|
||||
<h3 class="config-section-title config-section-title--deleted mt-3">Удалённые ({{len .DeletedCreations}})</h3>
|
||||
{{range .DeletedCreations}}
|
||||
<div class="creation-card creation-card--deleted">
|
||||
<span class="mono">#{{.ID}} · {{.ConnectionName}} · сервер {{.ServerID}} · {{.Protocol}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,81 @@
|
||||
{{define "page_admin_renewal"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Коды продления"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-plus"></i> Новый код</h2></div>
|
||||
<form method="post" action="{{appURL "admin/renewal"}}" class="form-row-2">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div>
|
||||
<label class="form-label">Код (пусто = сгенерировать)</label>
|
||||
<input class="form-control mono" name="code" placeholder="ABCD1234" maxlength="32">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Дней продления</label>
|
||||
<input class="form-control" type="number" name="add_days" value="30" min="1" max="3650">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Лимит активаций</label>
|
||||
<input class="form-control" type="number" name="max_uses" value="1" min="1" max="100000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Назначение</label>
|
||||
<select class="form-select" name="code_target">
|
||||
<option value="guest">Гостевая ссылка</option>
|
||||
<option value="member">Личный кабинет</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">ID гостевой ссылки (необязательно)</label>
|
||||
<input class="form-control" type="number" name="share_link_id" min="1">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Срок действия кода (необязательно)</label>
|
||||
<input class="form-control" type="date" name="code_expires_at">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Заметка</label>
|
||||
<input class="form-control" name="note" maxlength="255">
|
||||
</div>
|
||||
<div class="d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-plus"></i> Создать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-list"></i> Список кодов ({{len .Codes}})</h2></div>
|
||||
{{if not .Codes}}
|
||||
<p class="hint">Кодов пока нет.</p>
|
||||
{{else}}
|
||||
<div class="tbl-wrap">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Код</th><th>Дней</th><th>Активаций</th><th>Назначение</th><th>Ссылка</th><th>Срок кода</th><th>Заметка</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Codes}}
|
||||
<tr>
|
||||
<td class="mono">{{.Code}}</td>
|
||||
<td>{{.AddDays}}</td>
|
||||
<td>{{.UseCount}} / {{.MaxUses}}</td>
|
||||
<td>{{if eq2 .CodeTarget "member"}}<span class="badge badge-info">кабинет</span>{{else}}<span class="badge badge-ok">гость</span>{{end}}</td>
|
||||
<td>{{if .ShareLinkID}}#{{.ShareLinkID}}{{else}}—{{end}}</td>
|
||||
<td class="sl-muted">{{if .CodeExpiresAt}}{{.CodeExpiresAt.Format "2006-01-02"}}{{else}}—{{end}}</td>
|
||||
<td>{{if .Note}}{{.Note}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
<form method="post" action="{{appURL "admin/renewal"}}" onsubmit="return confirm('Удалить код {{.Code}}?');">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="code_id" value="{{.ID}}">
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,82 @@
|
||||
{{define "page_admin_servers"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Серверы"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
{{if .PanelErr}}<div class="alert-err"><i class="fa-solid fa-triangle-exclamation"></i> Панель недоступна: {{.PanelErr}}</div>{{end}}
|
||||
{{if not .PanelReachable}}<p class="hint">Панель Amnezia сейчас не отвечает — список серверов основан на сохранённых названиях (panel_server_labels) и настройках.</p>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-plus"></i> Добавить сервер вручную</h2></div>
|
||||
<form method="post" action="{{appURL "admin/servers"}}" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div>
|
||||
<label class="form-label">ID сервера в панели</label>
|
||||
<input class="form-control input-inline" type="number" name="new_server_id" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Название</label>
|
||||
<input class="form-control" name="new_server_title" required placeholder="🇩🇪 Германия">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-ghost"><i class="fa-solid fa-plus"></i> Добавить</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-server"></i> Серверы ({{len .Rows}})</h2></div>
|
||||
{{if not .Rows}}
|
||||
<p class="hint">Серверов пока нет. Задайте URL панели и токен в настройках, либо добавьте сервер вручную.</p>
|
||||
{{else}}
|
||||
<form id="deleteServerForm" method="post" action="{{appURL "admin/servers"}}" class="d-none">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="delete_label">
|
||||
<input type="hidden" id="delServerId" name="server_id" value="">
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{appURL "admin/servers"}}">
|
||||
{{template "admin_csrf_field"}}
|
||||
<input type="hidden" name="action" value="save">
|
||||
{{range .Rows}}<input type="hidden" name="server_ids" value="{{.ID}}">{{end}}
|
||||
|
||||
<div class="tbl-wrap">
|
||||
<table class="table table-sm mb-0 align-middle">
|
||||
<thead><tr><th>ID</th><th>Название</th><th>Протоколы</th><th>Флаг</th><th>Скорость</th><th>Откл.</th><th>Панель</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
{{$row := .}}
|
||||
<tr>
|
||||
<td class="mono">#{{.ID}}</td>
|
||||
<td><input class="form-control form-control-sm" name="servers[{{.ID}}][title]" value="{{.Title}}"></td>
|
||||
<td>
|
||||
{{range $.AllProtocols}}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="servers[{{$row.ID}}][protocols][]" value="{{.}}" id="p{{$row.ID}}_{{.}}" {{if strIn $row.Protocols .}}checked{{end}}>
|
||||
<label class="form-check-label hint" for="p{{$row.ID}}_{{.}}">{{.}}</label>
|
||||
</div>
|
||||
{{end}}
|
||||
</td>
|
||||
<td><input class="form-control form-control-sm input-inline" name="servers[{{.ID}}][flag]" value="{{.Flag}}" placeholder="🇩🇪"></td>
|
||||
<td><input class="form-control form-control-sm input-inline" name="servers[{{.ID}}][speed]" value="{{.Speed}}" placeholder="1 Gbps"></td>
|
||||
<td class="text-center"><input class="form-check-input" type="checkbox" name="servers[{{.ID}}][disabled]" value="1" {{if .Disabled}}checked{{end}}></td>
|
||||
<td class="sl-muted">{{if .PanelName}}{{.PanelName}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
<button type="submit" form="deleteServerForm" formnovalidate
|
||||
onclick="document.getElementById('delServerId').value='{{.ID}}'; return confirm('Удалить сервер #{{.ID}} из списка?');"
|
||||
class="btn btn-sm btn-danger">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> Сохранить всё</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,80 @@
|
||||
{{define "page_admin_settings"}}
|
||||
{{template "admin_shell_open" dict "Active" .Active "Admin" .Admin "Title" "Настройки"}}
|
||||
{{template "admin_flash" .}}
|
||||
|
||||
{{if .TestResult}}<div class="alert-ok"><i class="fa-solid fa-circle-check"></i> {{.TestResult}}</div>{{end}}
|
||||
{{if .TestError}}<div class="alert-err"><i class="fa-solid fa-triangle-exclamation"></i> {{.TestError}}</div>{{end}}
|
||||
|
||||
<form method="post" action="{{appURL "admin/settings"}}">
|
||||
{{template "admin_csrf_field"}}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-server"></i> Панель Amnezia</h2></div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL панели</label>
|
||||
<input class="form-control" name="panel_url" value="{{.PanelURL}}" placeholder="https://panel.example.com">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API-токен</label>
|
||||
<input class="form-control mono" name="api_token" value="{{.APIToken}}" placeholder="awp_...">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" name="action" value="save" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> Сохранить</button>
|
||||
<button type="submit" name="action" value="test" formnovalidate class="btn btn-ghost"><i class="fa-solid fa-plug-circle-check"></i> Проверить соединение</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-screwdriver-wrench"></i> Режим работы</h2></div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="maintenance" name="maintenance" value="1" {{if .Maintenance}}checked{{end}}>
|
||||
<label class="form-check-label" for="maintenance">Технические работы (запретить создание/перенос конфигов)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-key"></i> Вход через Pocket ID (OIDC)</h2></div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL Pocket ID</label>
|
||||
<input class="form-control" name="pocket_url" value="{{.PocketURL}}" placeholder="https://id.example.com">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Client ID</label>
|
||||
<input class="form-control mono" name="pocket_client_id" value="{{.PocketClientID}}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Client Secret</label>
|
||||
<input class="form-control mono" type="password" name="pocket_secret" value="{{.PocketSecret}}" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-address-card"></i> Личный кабинет — значения по умолчанию</h2></div>
|
||||
<div class="form-row-2">
|
||||
<div>
|
||||
<label class="form-label">Дней подписки по умолчанию</label>
|
||||
<input class="form-control" type="number" name="member_days" value="{{.MemberDays}}" min="1" max="3650" placeholder="30">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Лимит конфигов по умолчанию</label>
|
||||
<input class="form-control" type="number" name="member_max_uses" value="{{.MemberMaxUses}}" min="1" max="1000" placeholder="5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-compact">
|
||||
<div class="card-header"><h2><i class="fa-solid fa-code"></i> Дополнительно (JSON)</h2></div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label hint-tight">Метки серверов, переопределение (server_labels_json)</label>
|
||||
<textarea class="form-control tn-textarea mono" name="server_labels_json" rows="3" placeholder='{"1":"Германия"}'>{{.ServerLabelsJSON}}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label hint-tight">Уведомления о трафике (traffic_notices_json)</label>
|
||||
<textarea class="form-control tn-textarea mono" name="traffic_notices_json" rows="3" placeholder='{"1":"Ограничение 100GB/мес"}'>{{.TrafficNotices}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="action" value="save" class="btn btn-primary"><i class="fa-solid fa-floppy-disk"></i> Сохранить все настройки</button>
|
||||
</form>
|
||||
{{template "admin_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,47 @@
|
||||
{{define "page_install"}}
|
||||
{{template "guest_shell_open" dict "Title" "Установка администратора" "Lang" "ru"}}
|
||||
<div class="cyber-shell">
|
||||
<span class="cyber-corner cyber-corner--tl"></span><span class="cyber-corner cyber-corner--tr"></span>
|
||||
<span class="cyber-corner cyber-corner--bl"></span><span class="cyber-corner cyber-corner--br"></span>
|
||||
<div class="cyber-hero">
|
||||
<div class="hero-icon"><i class="fa-solid fa-user-shield"></i></div>
|
||||
<div class="hero-brand">Cyber // Access</div>
|
||||
<h1>Установка</h1>
|
||||
<p class="lead">Создание единственной учётной записи администратора.</p>
|
||||
</div>
|
||||
|
||||
{{if .DBError}}
|
||||
<div class="alert alert-danger">{{.DBError}}</div>
|
||||
<p class="hint">Проверьте <code>DATABASE_URL</code> и что миграции применены.</p>
|
||||
{{else if .Installed}}
|
||||
<div class="alert alert-info">Администратор уже создан.</div>
|
||||
<a class="btn btn-primary w-100" href="{{appURL "login"}}"><i class="fa-solid fa-right-to-bracket"></i> Перейти ко входу</a>
|
||||
{{else}}
|
||||
{{if .Error}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-user-plus"></i> Регистрация администратора</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "install"}}" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="username">Логин</label>
|
||||
<input class="form-control" id="username" name="username" required minlength="3" maxlength="64" pattern="[a-zA-Z0-9._-]+" placeholder="admin">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password">Пароль</label>
|
||||
<input class="form-control" id="password" name="password" type="password" required minlength="10" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password2">Пароль ещё раз</label>
|
||||
<input class="form-control" id="password2" name="password2" type="password" required minlength="10" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-check"></i> Создать администратора и войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint text-center">После установки ограничьте доступ к <code>/install</code> на продакшене.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,42 @@
|
||||
{{define "page_login"}}
|
||||
{{template "guest_shell_open" dict "Title" "Вход — Amnezia Admin" "Lang" "ru"}}
|
||||
<div class="cyber-shell">
|
||||
<span class="cyber-corner cyber-corner--tl"></span><span class="cyber-corner cyber-corner--tr"></span>
|
||||
<span class="cyber-corner cyber-corner--bl"></span><span class="cyber-corner cyber-corner--br"></span>
|
||||
<div class="cyber-hero">
|
||||
<div class="hero-icon"><i class="fa-solid fa-shield-halved"></i></div>
|
||||
<div class="hero-brand">Cyber // Access</div>
|
||||
<h1>Amnezia Admin</h1>
|
||||
<p class="lead">Вход в панель управления</p>
|
||||
</div>
|
||||
|
||||
{{if .Error}}<div class="alert alert-danger"><i class="fa-solid fa-triangle-exclamation"></i> {{.Error}}</div>{{end}}
|
||||
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-lock"></i> Вход</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "login"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="username"><i class="fa-solid fa-user"></i> Логин</label>
|
||||
<input class="form-control" id="username" name="username" type="text" required autocomplete="username" placeholder="admin">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password"><i class="fa-solid fa-lock"></i> Пароль</label>
|
||||
<input class="form-control" id="password" name="password" type="password" required autocomplete="current-password" placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-right-to-bracket"></i> Войти</button>
|
||||
</form>
|
||||
|
||||
{{if .PocketEnabled}}
|
||||
<div class="text-center text-secondary my-3">или</div>
|
||||
<a href="{{appURL "login"}}?oidc=1" class="btn btn-outline-info w-100"><i class="fa-solid fa-key"></i> Войти через Pocket ID</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .NoAdmins}}<p class="hint text-center">Нет учётных записей — <a href="{{appURL "install"}}">установка</a></p>{{end}}
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,128 @@
|
||||
{{define "page_cabinet"}}
|
||||
{{template "guest_shell_open" dict "Title" "Личный кабинет" "Lang" (lang)}}
|
||||
<div class="cyber-shell">
|
||||
<span class="cyber-corner cyber-corner--tl"></span><span class="cyber-corner cyber-corner--tr"></span>
|
||||
<span class="cyber-corner cyber-corner--bl"></span><span class="cyber-corner cyber-corner--br"></span>
|
||||
<div class="cyber-hero">
|
||||
<div class="hero-icon"><i class="fa-solid fa-user-shield"></i></div>
|
||||
<div class="hero-brand">{{t "hero_brand"}}</div>
|
||||
<h1>{{.Member.Username}}</h1>
|
||||
<p class="lead">Личный кабинет VPN</p>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{{appURL "cabinet/logout"}}"><i class="fa-solid fa-right-from-bracket"></i> Выйти</a>
|
||||
</div>
|
||||
|
||||
{{template "guest_flash" .}}
|
||||
|
||||
{{if .Maintenance}}<div class="alert alert-warning">{{htmlSafe (t "maintenance_banner")}}</div>{{end}}
|
||||
|
||||
<div class="share-stats">
|
||||
<div class="card share-panel"><div class="share-panel__body text-center">
|
||||
<div class="stat__value--sm fw-bold">{{if .Sub}}{{.SubLabel}}{{else}}—{{end}}</div>
|
||||
<div class="hint mb-0">{{t "stat_valid_until"}}</div>
|
||||
</div></div>
|
||||
<div class="card share-panel"><div class="share-panel__body text-center">
|
||||
<div class="stat__value--sm fw-bold">{{if .Sub}}{{.ConfigsRemaining}} {{t "stat_of"}} {{.Sub.MaxConfigs}}{{else}}—{{end}}</div>
|
||||
<div class="hint mb-0">{{t "stat_remaining"}}</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
{{if .Expired}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head share-panel__head--pink"><i class="fa-solid fa-hourglass-end"></i> {{t "expired_card"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "cabinet"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="renew">
|
||||
<div class="mb-2"><input class="form-control" name="code" placeholder="{{t "renewal_code_ph"}}" required></div>
|
||||
<button class="btn btn-primary w-100" type="submit"><i class="fa-solid fa-key"></i> {{t "renewal_btn"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
{{if .LimitReached}}
|
||||
<div class="alert alert-warning">{{t "limit_reached" "max" (printf "%d" .Sub.MaxConfigs)}}</div>
|
||||
{{else}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-plus"></i> {{t "field_location"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "cabinet"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{t "field_location"}}</label>
|
||||
<select class="form-select" name="server_id" required {{if not .Servers}}disabled{{end}}>
|
||||
{{range .Servers}}<option value="{{.ID}}" {{if .Disabled}}disabled{{end}}>{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">{{t "field_protocol"}}</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_wg" value="wireguard" checked>
|
||||
<label class="form-check-label" for="proto_wg">WireGuard</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_awg" value="awg2">
|
||||
<label class="form-check-label" for="proto_awg">AmneziaWG 2</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_vless" value="vless">
|
||||
<label class="form-check-label" for="proto_vless">VLESS</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">{{htmlSafe (t "hint_wait")}}</p>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-bolt"></i> {{t "btn_get_config"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Bundles}}
|
||||
<div class="card share-panel card--download">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-list"></i> {{t "configs_title"}}</div>
|
||||
<div class="share-panel__body">
|
||||
{{range .Bundles}}
|
||||
<div class="dl-bundle">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong class="mono">{{.ConnectionName}}</strong>
|
||||
<span class="badge text-bg-secondary">{{.Protocol}}</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{{if .Conf}}<a class="btn btn-sm btn-outline-info" href="{{appURL "cabinet/download"}}?cre={{.CreationID}}&part=conf"><i class="fa-solid fa-download"></i> {{.Conf.Filename}}</a>{{end}}
|
||||
{{if .Vpn}}<a class="btn btn-sm btn-outline-info" href="{{appURL "cabinet/download"}}?cre={{.CreationID}}&part=vpn"><i class="fa-solid fa-download"></i> {{.Vpn.Filename}}</a>{{end}}
|
||||
{{if .Zip}}<a class="btn btn-sm btn-outline-secondary" href="{{appURL "cabinet/download"}}?cre={{.CreationID}}&part=zip"><i class="fa-solid fa-file-zipper"></i> .zip</a>{{end}}
|
||||
</div>
|
||||
<form method="post" action="{{appURL "cabinet"}}" class="mt-2" onsubmit="return confirm('Удалить эту конфигурацию?');">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="config_id" value="{{.CreationID}}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fa-solid fa-trash"></i> Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .Expired}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-key"></i> {{t "renewal_title"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<p class="hint">{{t "renewal_hint"}}</p>
|
||||
<form method="post" action="{{appURL "cabinet"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="renew">
|
||||
<div class="mb-2"><input class="form-control" name="code" placeholder="{{t "renewal_code_ph"}}" required></div>
|
||||
<button class="btn btn-outline-info w-100" type="submit"><i class="fa-solid fa-arrow-rotate-right"></i> {{t "renewal_btn"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,57 @@
|
||||
{{define "page_cabinet_auth"}}
|
||||
{{template "guest_shell_open" dict "Title" "Личный кабинет — вход" "Lang" (lang)}}
|
||||
<div class="cyber-shell">
|
||||
<span class="cyber-corner cyber-corner--tl"></span><span class="cyber-corner cyber-corner--tr"></span>
|
||||
<span class="cyber-corner cyber-corner--bl"></span><span class="cyber-corner cyber-corner--br"></span>
|
||||
<div class="cyber-hero">
|
||||
<div class="hero-icon"><i class="fa-solid fa-user-lock"></i></div>
|
||||
<div class="hero-brand">{{t "hero_brand"}}</div>
|
||||
<h1>Личный кабинет</h1>
|
||||
<p class="lead">Войдите или зарегистрируйтесь, чтобы получить свою подписку VPN.</p>
|
||||
</div>
|
||||
|
||||
{{if .Error}}<div class="alert alert-danger"><i class="fa-solid fa-triangle-exclamation"></i> {{.Error}}</div>{{end}}
|
||||
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-right-to-bracket"></i> Вход</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "cabinet/login"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Логин</label>
|
||||
<input class="form-control" name="username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input class="form-control" name="password" type="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-right-to-bracket"></i> Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head share-panel__head--pink"><i class="fa-solid fa-user-plus"></i> Регистрация</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "cabinet/register"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Логин</label>
|
||||
<input class="form-control" name="username" required minlength="3" maxlength="64" pattern="[a-zA-Z0-9._-]+" autocomplete="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">E-mail (необязательно)</label>
|
||||
<input class="form-control" name="email" type="email" autocomplete="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Пароль</label>
|
||||
<input class="form-control" name="password" type="password" required minlength="8" autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-info w-100"><i class="fa-solid fa-user-plus"></i> Создать аккаунт</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,67 @@
|
||||
{{/* Shared cyber-dark admin shell. Call "admin_shell_open" with a dict of
|
||||
Active/Admin/Title, write page content, then "admin_shell_close". */}}
|
||||
{{define "admin_shell_open"}}<!doctype html>
|
||||
<html lang="ru" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Title}} — Amnezia Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=JetBrains+Mono:wght@500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet" referrerpolicy="no-referrer">
|
||||
<link href="{{appURL "static/css/admin.css"}}" rel="stylesheet">
|
||||
<link href="{{appURL "static/css/admin-cyber.css"}}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="admin-app">
|
||||
<button id="adminSidebarToggle" class="btn btn-sm position-fixed m-2" type="button" aria-label="Меню" onclick="document.body.classList.toggle('admin-sidebar-open')">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<aside class="admin-sidebar">
|
||||
<a class="admin-brand" href="{{appURL "admin"}}">
|
||||
<span class="admin-brand-icon"><i class="fa-solid fa-shield-halved"></i></span>
|
||||
<span>
|
||||
<span class="admin-brand-text d-block">Amnezia Admin</span>
|
||||
<span class="admin-brand-sub">Share Panel</span>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="admin-nav nav flex-column">
|
||||
<a class="nav-link {{if eq2 .Active "index"}}active{{end}}" href="{{appURL "admin"}}"><i class="fa-solid fa-gauge-high"></i> Дашборд</a>
|
||||
<a class="nav-link {{if eq2 .Active "links"}}active{{end}}" href="{{appURL "admin/links"}}"><i class="fa-solid fa-link"></i> Ссылки</a>
|
||||
<a class="nav-link {{if eq2 .Active "configs"}}active{{end}}" href="{{appURL "admin/configs"}}"><i class="fa-solid fa-key"></i> Конфигурации</a>
|
||||
<a class="nav-link {{if eq2 .Active "renewal"}}active{{end}}" href="{{appURL "admin/renewal"}}"><i class="fa-solid fa-rotate"></i> Коды продления</a>
|
||||
<a class="nav-link {{if eq2 .Active "servers"}}active{{end}}" href="{{appURL "admin/servers"}}"><i class="fa-solid fa-server"></i> Серверы</a>
|
||||
<a class="nav-link {{if eq2 .Active "settings"}}active{{end}}" href="{{appURL "admin/settings"}}"><i class="fa-solid fa-gear"></i> Настройки</a>
|
||||
</nav>
|
||||
<div class="admin-sidebar-footer">
|
||||
{{if .Admin}}<div class="hint mb-2"><i class="fa-solid fa-user"></i> {{.Admin.Username}}</div>{{end}}
|
||||
<a class="btn btn-outline-danger btn-sm w-100" href="{{appURL "logout"}}"><i class="fa-solid fa-right-from-bracket"></i> Выйти</a>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="admin-main">
|
||||
<header class="admin-topbar">
|
||||
<div class="admin-topbar-title">
|
||||
<h1><i class="fa-solid fa-angle-right text-primary"></i> {{.Title}}</h1>
|
||||
</div>
|
||||
<div class="admin-topbar-meta">
|
||||
<span class="badge-protocol"><i class="fa-solid fa-circle-nodes"></i> Go</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="admin-content">
|
||||
{{end}}
|
||||
|
||||
{{define "admin_shell_close"}}
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "admin_flash"}}
|
||||
{{if .OK}}<div class="alert-ok"><i class="fa-solid fa-circle-check"></i><div>{{.OK}}</div></div>{{end}}
|
||||
{{if .Error}}<div class="alert-err"><i class="fa-solid fa-triangle-exclamation"></i><div>{{.Error}}</div></div>{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "admin_csrf_field"}}<input type="hidden" name="_csrf" value="{{csrf}}">{{end}}
|
||||
@@ -0,0 +1,56 @@
|
||||
{{/* Shared shell for guest-facing pages: share, faq, rules, status, login,
|
||||
install, cabinet. Each page calls "guest_shell_open" with a dict of
|
||||
Title/BodyClass/Lang, writes its own content, then "guest_shell_close". */}}
|
||||
{{define "guest_shell_open"}}<!doctype html>
|
||||
<html lang="{{.Lang}}" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=JetBrains+Mono:wght@500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet" referrerpolicy="no-referrer">
|
||||
<link href="{{appURL "static/css/portal.css"}}" rel="stylesheet">
|
||||
<link href="{{appURL "static/css/share.css"}}" rel="stylesheet">
|
||||
</head>
|
||||
<body class="share-app">
|
||||
<div class="cyber-bg" aria-hidden="true"></div>
|
||||
<div class="wrap">
|
||||
{{end}}
|
||||
|
||||
{{define "guest_shell_close"}}
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
{{define "guest_flash"}}
|
||||
{{if .OK}}<div class="alert alert-success d-flex align-items-center gap-2" role="alert"><i class="fa-solid fa-circle-check"></i><div>{{.OK}}</div></div>{{end}}
|
||||
{{if .Error}}<div class="alert alert-danger d-flex align-items-center gap-2" role="alert"><i class="fa-solid fa-triangle-exclamation"></i><div>{{.Error}}</div></div>{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "guest_lang_switch"}}
|
||||
<div class="d-flex lang-switch">
|
||||
<span class="lang-switch__label"><i class="fa-solid fa-globe"></i></span>
|
||||
<a class="lang-switch__btn {{if eq2 (lang) "ru"}}active{{end}}" href="?{{.QS}}lang=ru">RU</a>
|
||||
<a class="lang-switch__btn {{if eq2 (lang) "en"}}active{{end}}" href="?{{.QS}}lang=en">EN</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "guest_subnav"}}
|
||||
<div class="guest-subnav d-flex justify-content-center gap-3 flex-wrap">
|
||||
<a href="{{appURL "faq"}}"><i class="fa-solid fa-circle-question"></i> {{t "faq_link_short"}}</a>
|
||||
<a href="{{appURL "rules"}}"><i class="fa-solid fa-scale-balanced"></i> {{t "rules_link_short"}}</a>
|
||||
<a href="{{appURL "status"}}"><i class="fa-solid fa-tower-broadcast"></i> {{t "status_link"}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "guest_footer"}}
|
||||
<div class="share-footer">
|
||||
<span class="share-footer__line"></span>
|
||||
<span class="share-footer__text">Amnezia VPN Share © {{year}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{{define "page_faq"}}
|
||||
{{template "guest_shell_open" dict "Title" (t "faq_page_title") "Lang" (lang)}}
|
||||
<div class="cyber-shell share-faq-page">
|
||||
<div class="cyber-hero cyber-hero--compact">
|
||||
<div class="hero-icon hero-icon--sm"><i class="fa-solid fa-circle-question"></i></div>
|
||||
<h1>{{t "faq_title"}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="faq-list">
|
||||
{{range .Items}}
|
||||
<div class="faq-item">
|
||||
<div class="faq-item__q">{{.Question}}</div>
|
||||
<div class="faq-item__a">{{.Answer}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "guest_subnav" .}}
|
||||
<p class="text-center mt-3"><a class="guest-back-link" href="{{appURL "share"}}"><i class="fa-solid fa-arrow-left"></i> Назад</a></p>
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,47 @@
|
||||
{{define "page_rules"}}
|
||||
{{template "guest_shell_open" dict "Title" (t "rules_page_title") "Lang" (lang)}}
|
||||
<div class="cyber-shell share-faq-page">
|
||||
<div class="cyber-hero cyber-hero--compact">
|
||||
<div class="hero-icon hero-icon--sm"><i class="fa-solid fa-scale-balanced"></i></div>
|
||||
<h1>{{t "rules_title"}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="rules-block rules-block--cyan">
|
||||
<div class="rules-block__title"><i class="fa-solid fa-circle-check"></i> Разрешено</div>
|
||||
<div class="rules-block__body">
|
||||
<ul class="rules-list">
|
||||
<li>Личное использование сервиса для защиты своего трафика.</li>
|
||||
<li>Создание нескольких конфигураций в рамках лимита вашей ссылки/подписки.</li>
|
||||
<li>Перенос своей конфигурации на другой узел при необходимости.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-block rules-block--pink">
|
||||
<div class="rules-block__title"><i class="fa-solid fa-ban"></i> Запрещено</div>
|
||||
<div class="rules-block__body">
|
||||
<ul class="rules-list">
|
||||
<li>Передача ссылки/доступа третьим лицам для коммерческого использования.</li>
|
||||
<li>Массовая автоматическая генерация конфигураций (боты, скрипты).</li>
|
||||
<li>Использование сервиса для рассылки спама, DDoS-атак или иной незаконной деятельности.</li>
|
||||
<li>Попытки обойти лимиты создания конфигураций или скомпрометировать сервис.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-block rules-block--yellow">
|
||||
<div class="rules-block__title"><i class="fa-solid fa-triangle-exclamation"></i> Ответственность</div>
|
||||
<div class="rules-block__body">
|
||||
<p>Администрация оставляет за собой право отозвать доступ без предупреждения при нарушении данных правил.
|
||||
Сервис предоставляется «как есть», без гарантий непрерывной доступности.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="rules-doc__updated text-center hint">Обновлено: {{year}}</p>
|
||||
|
||||
{{template "guest_subnav" .}}
|
||||
<p class="text-center mt-3"><a class="guest-back-link" href="{{appURL "share"}}"><i class="fa-solid fa-arrow-left"></i> Назад</a></p>
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,173 @@
|
||||
{{define "page_share"}}
|
||||
{{template "guest_shell_open" dict "Title" (t "page_title") "Lang" (lang)}}
|
||||
{{if .NotFound}}
|
||||
<div class="cyber-shell text-center">
|
||||
<div class="hero-icon"><i class="fa-solid fa-link-slash"></i></div>
|
||||
<h1>{{t "err_link_not_found"}}</h1>
|
||||
<p class="lead">{{t "err_bad_link"}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="d-flex justify-content-end lang-switch">
|
||||
<span class="lang-switch__label"><i class="fa-solid fa-globe"></i></span>
|
||||
<a class="lang-switch__btn {{if eq2 (lang) "ru"}}active{{end}}" href="?k={{.Token}}&lang=ru">RU</a>
|
||||
<a class="lang-switch__btn {{if eq2 (lang) "en"}}active{{end}}" href="?k={{.Token}}&lang=en">EN</a>
|
||||
</div>
|
||||
|
||||
<div class="cyber-shell">
|
||||
<span class="cyber-corner cyber-corner--tl"></span><span class="cyber-corner cyber-corner--tr"></span>
|
||||
<span class="cyber-corner cyber-corner--bl"></span><span class="cyber-corner cyber-corner--br"></span>
|
||||
<div class="cyber-hero">
|
||||
<div class="hero-icon"><i class="fa-solid fa-shield-halved"></i></div>
|
||||
<div class="hero-brand">{{t "hero_brand"}}</div>
|
||||
<h1>{{t "hero_title"}}</h1>
|
||||
<p class="lead">{{t "hero_lead"}}</p>
|
||||
</div>
|
||||
|
||||
{{template "guest_flash" .}}
|
||||
|
||||
{{if .Maintenance}}<div class="alert alert-warning">{{htmlSafe (t "maintenance_banner")}}</div>{{end}}
|
||||
|
||||
<div class="share-stats">
|
||||
<div class="card share-panel"><div class="share-panel__body text-center">
|
||||
<div class="stat__value--sm fw-bold">{{.Link.ExpiresLabel}}</div>
|
||||
<div class="hint mb-0">{{t "term_label"}}</div>
|
||||
</div></div>
|
||||
<div class="card share-panel"><div class="share-panel__body text-center">
|
||||
<div class="stat__value--sm fw-bold">{{sub .Link.MaxUses .Link.UseCount}} {{t "stat_of"}} {{.Link.MaxUses}}</div>
|
||||
<div class="hint mb-0">{{t "stat_remaining"}}</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
{{if .Expired}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head share-panel__head--pink"><i class="fa-solid fa-hourglass-end"></i> {{t "expired_card"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<p class="hint">{{t "renewal_hint"}}</p>
|
||||
<form method="post" action="{{appURL "share"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="renew">
|
||||
<input type="hidden" name="k" value="{{.Token}}">
|
||||
<div class="mb-2"><input class="form-control" name="code" placeholder="{{t "renewal_code_ph"}}" required></div>
|
||||
<button class="btn btn-primary w-100" type="submit"><i class="fa-solid fa-key"></i> {{t "renewal_btn"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
||||
{{if .Maintenance}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head share-panel__head--pink"><i class="fa-solid fa-screwdriver-wrench"></i> {{t "maintenance_card_title"}}</div>
|
||||
<div class="share-panel__body"><p class="hint mb-0">{{t "maintenance_card_body"}}</p></div>
|
||||
</div>
|
||||
{{else if .LimitReached}}
|
||||
<div class="alert alert-warning">{{t "limit_reached" "max" (printf "%d" .Link.MaxUses)}}</div>
|
||||
{{else}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-plus"></i> {{t "field_location"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<form method="post" action="{{appURL "share"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<input type="hidden" name="k" value="{{.Token}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{t "field_location"}}</label>
|
||||
<select class="form-select" name="server_id" required {{if not .Servers}}disabled{{end}}>
|
||||
{{range .Servers}}<option value="{{.ID}}" {{if .Disabled}}disabled{{end}}>{{if .Flag}}{{.Flag}} {{end}}{{.Label}}{{if .Speed}} ({{t "server_speed_prefix" "speed" .Speed}}){{end}}</option>{{end}}
|
||||
</select>
|
||||
{{if not .Servers}}<div class="hint">{{t "js_servers_empty"}}</div>{{end}}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">{{t "field_protocol"}}</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_wg" value="wireguard" checked>
|
||||
<label class="form-check-label" for="proto_wg">WireGuard</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_awg" value="awg2">
|
||||
<label class="form-check-label" for="proto_awg">AmneziaWG 2</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="protocol" id="proto_vless" value="vless">
|
||||
<label class="form-check-label" for="proto_vless">VLESS</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">{{htmlSafe (t "hint_wait")}}</p>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa-solid fa-bolt"></i> {{t "btn_get_config"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Bundles}}
|
||||
<div class="card share-panel card--download">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-list"></i> {{t "configs_title"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<p class="share-panel__intro">{{htmlSafe (t "configs_intro" "count" (printf "%d" (len .Bundles)))}}</p>
|
||||
{{range .Bundles}}
|
||||
<div class="dl-bundle">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong class="mono">{{.ConnectionName}}</strong>
|
||||
<span class="badge text-bg-secondary">{{.Protocol}}</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
{{if .Conf}}<a class="btn btn-sm btn-outline-info" href="{{shareDL $.Token .CreationID "conf"}}"><i class="fa-solid fa-download"></i> {{.Conf.Filename}}</a>{{end}}
|
||||
{{if .Vpn}}<a class="btn btn-sm btn-outline-info" href="{{shareDL $.Token .CreationID "vpn"}}"><i class="fa-solid fa-download"></i> {{.Vpn.Filename}}</a>{{end}}
|
||||
{{if .Zip}}<a class="btn btn-sm btn-outline-secondary" href="{{shareDL $.Token .CreationID "zip"}}"><i class="fa-solid fa-file-zipper"></i> {{t "btn_download_zip"}}</a>{{end}}
|
||||
</div>
|
||||
|
||||
{{if not $.Maintenance}}
|
||||
<details class="mb-2">
|
||||
<summary class="hint" style="cursor:pointer;">{{t "migrate_title"}}</summary>
|
||||
<form method="post" action="{{appURL "share"}}" class="mt-2">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="migrate">
|
||||
<input type="hidden" name="k" value="{{$.Token}}">
|
||||
<input type="hidden" name="creation_id" value="{{.CreationID}}">
|
||||
<div class="row g-2">
|
||||
<div class="col-7">
|
||||
<select class="form-select form-select-sm" name="new_server_id" required>
|
||||
{{range $.Servers}}<option value="{{.ID}}" {{if .Disabled}}disabled{{end}}>{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<select class="form-select form-select-sm" name="new_protocol">
|
||||
<option value="wireguard">WireGuard</option>
|
||||
<option value="awg2">AmneziaWG 2</option>
|
||||
<option value="vless">VLESS</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">{{t "migrate_warning"}}</p>
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning w-100"><i class="fa-solid fa-arrows-turn-right"></i> {{t "migrate_submit"}}</button>
|
||||
</form>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .Expired}}
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__head"><i class="fa-solid fa-key"></i> {{t "renewal_title"}}</div>
|
||||
<div class="share-panel__body">
|
||||
<p class="hint">{{t "renewal_hint"}}</p>
|
||||
<form method="post" action="{{appURL "share"}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrf}}">
|
||||
<input type="hidden" name="action" value="renew">
|
||||
<input type="hidden" name="k" value="{{.Token}}">
|
||||
<div class="mb-2"><input class="form-control" name="code" placeholder="{{t "renewal_code_ph"}}" required></div>
|
||||
<button class="btn btn-outline-info w-100" type="submit"><i class="fa-solid fa-arrow-rotate-right"></i> {{t "renewal_btn"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{template "guest_subnav" .}}
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{end}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,37 @@
|
||||
{{define "page_status"}}
|
||||
{{template "guest_shell_open" dict "Title" (t "status_page_title") "Lang" (lang)}}
|
||||
<div class="cyber-shell share-faq-page">
|
||||
<div class="cyber-hero cyber-hero--compact">
|
||||
<div class="hero-icon hero-icon--sm"><i class="fa-solid fa-tower-broadcast"></i></div>
|
||||
<h1>{{t "status_link"}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="card share-panel">
|
||||
<div class="share-panel__body">
|
||||
{{if not .Servers}}
|
||||
<p class="hint mb-0">{{t "js_servers_empty"}}</p>
|
||||
{{else}}
|
||||
<div class="tbl-wrap">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead><tr><th>Сервер</th><th>Статус</th><th>Задержка</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Servers}}
|
||||
<tr>
|
||||
<td>{{if .Flag}}{{.Flag}} {{end}}{{.Label}}</td>
|
||||
<td>{{if .Alive}}<span class="badge-ok badge"><i class="fa-solid fa-circle-check"></i> {{t "status_up"}}</span>{{else}}<span class="badge-err badge"><i class="fa-solid fa-circle-xmark"></i> {{t "status_down"}}</span>{{end}}</td>
|
||||
<td>{{if .Alive}}{{.Ms}} ms{{else}}—{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "guest_subnav" .}}
|
||||
<p class="text-center mt-3"><a class="guest-back-link" href="{{appURL "share"}}"><i class="fa-solid fa-arrow-left"></i> Назад</a></p>
|
||||
</div>
|
||||
{{template "guest_footer" .}}
|
||||
{{template "guest_shell_close" .}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user