From 441652b079ec1bad6a466e139d230b5d8b8f797a Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Thu, 23 Jul 2026 09:20:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20=D0=B2=20out?= =?UTF-8?q?put/index.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По умолчанию — Статистика: доступность TCP, IP, пользователи, матрица присутствия. Серверы открываются отдельными вкладками. Co-authored-by: Cursor --- roles/hysteria2/tasks/export.yml | 1 + roles/hysteria2/tasks/export_global.yml | 68 +- .../templates/export/global-index.html.j2 | 694 ++++++++++++------ 3 files changed, 541 insertions(+), 222 deletions(-) diff --git a/roles/hysteria2/tasks/export.yml b/roles/hysteria2/tasks/export.yml index 06bea17..26c76f4 100644 --- a/roles/hysteria2/tasks/export.yml +++ b/roles/hysteria2/tasks/export.yml @@ -46,6 +46,7 @@ mode: salamander domain: {{ hysteria2_domain }} port: {{ hysteria2_listen_port }} + host_ip: "{{ ansible_host | default('') }}" obfs_password: "{{ hysteria2_obfs_password }}" users: {% for user in hysteria2_export_users %} diff --git a/roles/hysteria2/tasks/export_global.yml b/roles/hysteria2/tasks/export_global.yml index af0bbd8..882d410 100644 --- a/roles/hysteria2/tasks/export_global.yml +++ b/roles/hysteria2/tasks/export_global.yml @@ -29,11 +29,17 @@ when: not item.skipped | default(false) vars: _info: "{{ item.content | b64decode | from_yaml }}" + _server_name: "{{ _info.server }}" _entry: - name: "{{ _info.server }}" + name: "{{ _server_name }}" domain: "{{ _info.domain }}" port: "{{ _info.port }}" mode: "{{ _info.mode | default('salamander') }}" + host_ip: >- + {{ + _info.host_ip + | default(hostvars[_server_name].ansible_host | default(''), true) + }} obfs_password: "{{ _info.obfs_password | default('') }}" dir: "{{ item.item.path | dirname | basename }}" users: "{{ _info.users }}" @@ -45,6 +51,64 @@ when: hysteria2_global_servers | default([]) | length > 0 become: false +- name: Probe TCP reachability of exported servers + ansible.builtin.wait_for: + host: "{{ item.host_ip if (item.host_ip | default('') | length > 0) else item.domain }}" + port: "{{ item.port | int }}" + timeout: 3 + state: started + loop: "{{ hysteria2_global_servers | default([]) }}" + loop_control: + label: "{{ item.name }}" + register: _hysteria2_probe + failed_when: false + changed_when: false + become: false + when: hysteria2_global_servers | default([]) | length > 0 + +- name: Merge probe result for each server + ansible.builtin.set_fact: + hysteria2_global_servers_enriched: >- + {{ + (hysteria2_global_servers_enriched | default([])) + + [ + hysteria2_global_servers[item] | combine({ + 'reachable': not ((_hysteria2_probe.results | default([]))[item].failed | default(true)), + 'probe_seconds': (_hysteria2_probe.results | default([]))[item].elapsed | default(none) + }) + ] + }} + loop: "{{ range(0, hysteria2_global_servers | length) | list }}" + when: hysteria2_global_servers | default([]) | length > 0 + become: false + +- name: Replace servers list with enriched data + ansible.builtin.set_fact: + hysteria2_global_servers: "{{ hysteria2_global_servers_enriched }}" + when: hysteria2_global_servers_enriched is defined + become: false + +- name: Compute unique users across servers + ansible.builtin.set_fact: + hysteria2_unique_users: >- + {{ + hysteria2_global_servers + | map(attribute='users') + | map('map', attribute='name') + | flatten + | unique + | sort + }} + hysteria2_reachable_count: >- + {{ + hysteria2_global_servers + | selectattr('reachable') + | list + | length + }} + when: hysteria2_global_servers | default([]) | length > 0 + become: false + - name: Generate global HTML index ansible.builtin.template: src: export/global-index.html.j2 @@ -53,6 +117,8 @@ vars: generated_at: "{{ now(utc=false, fmt='%Y-%m-%d %H:%M:%S') }}" total_users: "{{ hysteria2_global_servers | map(attribute='users') | map('length') | sum }}" + unique_users: "{{ hysteria2_unique_users | default([]) }}" + reachable_count: "{{ hysteria2_reachable_count | default(0) }}" when: hysteria2_global_servers | default([]) | length > 0 become: false diff --git a/roles/hysteria2/templates/export/global-index.html.j2 b/roles/hysteria2/templates/export/global-index.html.j2 index ff0c12e..374c5e8 100644 --- a/roles/hysteria2/templates/export/global-index.html.j2 +++ b/roles/hysteria2/templates/export/global-index.html.j2 @@ -15,11 +15,14 @@ --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff; - --accent-2: #a371f7; + --accent-2: #3fb950; --accent-soft: rgba(88, 166, 255, 0.12); - --accent-soft-2: rgba(163, 113, 247, 0.12); --success: #3fb950; --success-soft: rgba(63, 185, 80, 0.15); + --danger: #f85149; + --danger-soft: rgba(248, 81, 73, 0.15); + --warn: #d29922; + --warn-soft: rgba(210, 153, 34, 0.15); --radius: 14px; --shadow: 0 12px 40px rgba(0, 0, 0, 0.35); } @@ -33,96 +36,241 @@ --text: #1f2328; --muted: #656d76; --accent: #0969da; - --accent-2: #8250df; + --accent-2: #1a7f37; --accent-soft: rgba(9, 105, 218, 0.08); - --accent-soft-2: rgba(130, 80, 223, 0.08); --success: #1a7f37; --success-soft: rgba(26, 127, 55, 0.1); + --danger: #cf222e; + --danger-soft: rgba(207, 34, 46, 0.1); + --warn: #9a6700; + --warn-soft: rgba(154, 103, 0, 0.1); --shadow: 0 10px 30px rgba(31, 35, 40, 0.08); } } * { box-sizing: border-box; margin: 0; padding: 0; } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - background: var(--bg); + font-family: "Segoe UI", "SF Pro Display", "Helvetica Neue", Arial, sans-serif; + background: + radial-gradient(ellipse 80% 50% at 20% -10%, var(--accent-soft), transparent), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(63, 185, 80, 0.08), transparent), + var(--bg); color: var(--text); line-height: 1.5; min-height: 100vh; } .hero { - background: - radial-gradient(ellipse 80% 60% at 50% -10%, var(--accent-soft), transparent), - radial-gradient(ellipse 60% 40% at 90% 10%, var(--accent-soft-2), transparent), - var(--bg-soft); - border-bottom: 1px solid var(--border); - padding: 2.5rem 1.5rem 2rem; + padding: 2rem 1.5rem 1.25rem; text-align: center; } .hero h1 { font-size: clamp(1.75rem, 4vw, 2.25rem); font-weight: 800; letter-spacing: -0.03em; - margin-bottom: 0.5rem; - background: linear-gradient(135deg, var(--accent), var(--accent-2)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + margin-bottom: 0.35rem; + color: var(--accent); } .hero p { color: var(--muted); font-size: 0.95rem; } - .stats { - display: flex; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; - margin-top: 1.25rem; - } - .stat { - padding: 0.5rem 1rem; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--bg-card); - font-size: 0.85rem; - font-weight: 600; - } - .stat span { color: var(--accent); } - .nav { + .tabs { position: sticky; top: 0; z-index: 50; display: flex; - gap: 0.5rem; + gap: 0.45rem; flex-wrap: wrap; justify-content: center; - padding: 0.85rem 1rem; - background: color-mix(in srgb, var(--bg-soft) 88%, transparent); - backdrop-filter: blur(10px); + padding: 0.75rem 1rem; + background: color-mix(in srgb, var(--bg-soft) 90%, transparent); + backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); } - .nav a { - padding: 0.4rem 0.85rem; - border-radius: 999px; + .tab { + appearance: none; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); - text-decoration: none; - font-size: 0.82rem; - font-weight: 600; - transition: border-color 0.15s, background 0.15s, color 0.15s; + padding: 0.5rem 0.95rem; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 650; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, color 0.15s, box-shadow 0.15s; } - .nav a:hover { + .tab:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); } + .tab.active { + border-color: var(--accent); + background: var(--accent-soft); + color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); + } + .tab .dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + margin-right: 0.35rem; + vertical-align: middle; + background: var(--muted); + } + .tab .dot.up { background: var(--success); box-shadow: 0 0 8px var(--success); } + .tab .dot.down { background: var(--danger); } .container { - max-width: 980px; + max-width: 1080px; margin: 0 auto; padding: 1.5rem 1rem 3rem; } - .server-section { - scroll-margin-top: 4.5rem; - margin-bottom: 2.5rem; + .tab-panel { display: none; animation: fadeIn 0.2s ease; } + .tab-panel.active { display: block; } + @keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } } + .metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.85rem; + margin-bottom: 1.5rem; + } + .metric { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.1rem 1.2rem; + box-shadow: var(--shadow); + } + .metric .label { + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + margin-bottom: 0.35rem; + } + .metric .value { + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--accent); + } + .metric .sub { + margin-top: 0.2rem; + font-size: 0.78rem; + color: var(--muted); + } + .section-title { + font-size: 1.05rem; + font-weight: 700; + margin: 0 0 0.85rem; + } + .server-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; + margin-bottom: 1.75rem; + } + .server-stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.15rem 1.25rem; + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + gap: 0.75rem; + } + .server-stat-card header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.75rem; + } + .server-stat-card h3 { + font-size: 1.1rem; + font-weight: 750; + } + .badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.25rem 0.65rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + white-space: nowrap; + } + .badge.up { + background: var(--success-soft); + color: var(--success); + border: 1px solid color-mix(in srgb, var(--success) 35%, transparent); + } + .badge.down { + background: var(--danger-soft); + color: var(--danger); + border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent); + } + .meta-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.55rem 0.85rem; + } + .meta-item label { + display: block; + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted); + } + .meta-item span, + .meta-item code { + font-size: 0.88rem; + font-weight: 600; + word-break: break-all; + } + .meta-item code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.8rem; + } + .matrix-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-card); + box-shadow: var(--shadow); + } + table.matrix { + width: 100%; + border-collapse: collapse; + min-width: 480px; + } + table.matrix th, + table.matrix td { + padding: 0.65rem 0.85rem; + border-bottom: 1px solid var(--border); + text-align: center; + font-size: 0.85rem; + } + table.matrix th { + background: var(--bg-soft); + font-weight: 700; + position: sticky; + top: 0; + } + table.matrix th:first-child, + table.matrix td:first-child { + text-align: left; + font-weight: 650; + white-space: nowrap; + } + table.matrix tr:last-child td { border-bottom: none; } + .cell-yes { + color: var(--success); + font-weight: 800; + } + .cell-no { color: var(--muted); opacity: 0.45; } .server-header { display: flex; flex-wrap: wrap; @@ -137,15 +285,8 @@ box-shadow: var(--shadow); border-left: 4px solid var(--accent); } - .server-header h2 { - font-size: 1.35rem; - font-weight: 700; - } - .server-header .domain { - color: var(--muted); - font-size: 0.9rem; - margin-top: 0.15rem; - } + .server-header h2 { font-size: 1.35rem; font-weight: 700; } + .server-header .domain { color: var(--muted); font-size: 0.9rem; margin-top: 0.15rem; } .server-link { display: inline-flex; align-items: center; @@ -160,17 +301,6 @@ font-weight: 600; } .server-link:hover { background: var(--accent-soft); border-color: var(--accent); } - .badge-salamander { - display: inline-block; - margin-top: 0.35rem; - padding: 0.2rem 0.6rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 700; - background: var(--accent-soft-2); - color: var(--accent-2); - } - .server-obfs { margin-top: 0.75rem; width: 100%; } .users-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); @@ -255,136 +385,6 @@ transform: scale(1.03); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); } - .lightbox { - position: fixed; - inset: 0; - z-index: 200; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - box-sizing: border-box; - background: rgba(8, 12, 18, 0.72); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - opacity: 0; - visibility: hidden; - pointer-events: none; - transition: opacity 0.22s ease, visibility 0.22s ease; - } - .lightbox.open { - opacity: 1; - visibility: visible; - pointer-events: auto; - } - .lightbox-panel { - position: relative; - display: flex; - flex-direction: column; - width: min(94vw, 900px); - height: calc(100dvh - 2rem); - max-height: calc(100dvh - 2rem); - padding: 1.25rem 1.25rem 1rem; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 18px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45); - text-align: center; - overflow: hidden; - box-sizing: border-box; - transform: translateY(12px) scale(0.96); - transition: transform 0.22s ease; - } - .lightbox.open .lightbox-panel { - transform: translateY(0) scale(1); - } - .lightbox-panel img { - display: block; - max-width: 100%; - max-height: 100%; - width: auto; - height: auto; - aspect-ratio: 1; - object-fit: contain; - background: #fff; - border-radius: 12px; - padding: 1rem; - box-sizing: border-box; - image-rendering: pixelated; - } - .lightbox-media { - flex: 1 1 auto; - min-height: 0; - display: flex; - align-items: center; - justify-content: center; - } - .lightbox-caption { - flex-shrink: 0; - margin-top: 0.85rem; - font-size: 0.95rem; - font-weight: 650; - color: var(--text); - } - .lightbox-hint { - flex-shrink: 0; - margin-top: 0.25rem; - font-size: 0.75rem; - color: var(--muted); - } - .lightbox-actions { - flex-shrink: 0; - margin-top: 1rem; - display: flex; - justify-content: center; - } - .lightbox-download { - display: inline-flex; - align-items: center; - gap: 0.45rem; - padding: 0.55rem 1rem; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-input); - color: var(--accent); - text-decoration: none; - font-size: 0.85rem; - font-weight: 650; - transition: color 0.15s, border-color 0.15s, background 0.15s; - } - .lightbox-download:hover { - border-color: var(--accent); - background: var(--accent-soft); - } - .lightbox-download svg { - width: 16px; - height: 16px; - flex-shrink: 0; - } - .lightbox-close { - position: absolute; - top: 0.65rem; - right: 0.65rem; - width: 36px; - height: 36px; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--bg-input); - color: var(--muted); - cursor: pointer; - font-size: 1.25rem; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.15s, border-color 0.15s, background 0.15s; - } - .lightbox-close:hover { - color: var(--accent); - border-color: var(--accent); - background: var(--accent-soft); - } - body.lightbox-open { overflow: hidden; } .files { display: flex; flex-wrap: wrap; @@ -430,43 +430,268 @@ z-index: 100; } .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; } + .lightbox { + position: fixed; + inset: 0; + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + box-sizing: border-box; + background: rgba(8, 12, 18, 0.72); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.22s ease, visibility 0.22s ease; + } + .lightbox.open { + opacity: 1; + visibility: visible; + pointer-events: auto; + } + .lightbox-panel { + position: relative; + display: flex; + flex-direction: column; + width: min(94vw, 900px); + height: calc(100dvh - 2rem); + max-height: calc(100dvh - 2rem); + padding: 1.25rem 1.25rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 18px; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45); + text-align: center; + overflow: hidden; + box-sizing: border-box; + transform: translateY(12px) scale(0.96); + transition: transform 0.22s ease; + } + .lightbox.open .lightbox-panel { transform: translateY(0) scale(1); } + .lightbox-media { + flex: 1 1 auto; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + } + .lightbox-panel img { + display: block; + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + aspect-ratio: 1; + object-fit: contain; + background: #fff; + border-radius: 12px; + padding: 1rem; + box-sizing: border-box; + image-rendering: pixelated; + } + .lightbox-caption { + flex-shrink: 0; + margin-top: 0.85rem; + font-size: 0.95rem; + font-weight: 650; + color: var(--text); + } + .lightbox-hint { + flex-shrink: 0; + margin-top: 0.25rem; + font-size: 0.75rem; + color: var(--muted); + } + .lightbox-actions { + flex-shrink: 0; + margin-top: 1rem; + display: flex; + justify-content: center; + } + .lightbox-download { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.55rem 1rem; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--accent); + text-decoration: none; + font-size: 0.85rem; + font-weight: 650; + transition: color 0.15s, border-color 0.15s, background 0.15s; + } + .lightbox-download:hover { + border-color: var(--accent); + background: var(--accent-soft); + } + .lightbox-download svg { width: 16px; height: 16px; flex-shrink: 0; } + .lightbox-close { + position: absolute; + top: 0.65rem; + right: 0.65rem; + width: 36px; + height: 36px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-input); + color: var(--muted); + cursor: pointer; + font-size: 1.25rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s, border-color 0.15s, background 0.15s; + } + .lightbox-close:hover { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-soft); + } + body.lightbox-open { overflow: hidden; } + .probe-note { + margin-top: 1rem; + font-size: 0.78rem; + color: var(--muted); + }
-

Hysteria2 · Salamander

-

Общий каталог VPN-подключений с обфускацией Salamander

-
-
{{ hysteria2_global_servers | length }} серверов
-
{{ total_users }} пользователей
-
+

Hysteria2

+

Каталог VPN и сводная статистика

-