yaport 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/dist/client/assets/{index-DQ_KWWqB.css → index-BMJd0Oii.css} +1 -1
- package/dist/client/assets/index-CgNFN5bT.js +9 -0
- package/dist/client/index.html +2 -2
- package/dist/server/cli.js +156 -45
- package/package.json +1 -1
- package/dist/client/assets/index-g0iLkETl.js +0 -9
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ English documentation is available below: [English](#english).
|
|
|
11
11
|
- 通过一层或两层跳板机连接目标机器,底层使用 OpenSSH `ProxyJump`。
|
|
12
12
|
- 目标机器或跳板机需要 SSH 密码时,可以在 GUI 填写,也可以通过 CLI 环境变量传入。
|
|
13
13
|
- 面板和 CLI 都能配置连接超时、命令超时,适合慢链路或两层跳板机。
|
|
14
|
-
- 如果远程机器有 nginx 配置,丫口会把能匹配端口的外部入口 URL 展示在端口行里。
|
|
14
|
+
- 如果远程机器有 nginx、OpenResty、Tengine 或 Caddy 配置,丫口会把能匹配端口的外部入口 URL 展示在端口行里。
|
|
15
15
|
- 当前选中的机器会复用 OpenSSH ControlMaster 连接,降低两层跳板和密码认证的重复握手成本。
|
|
16
16
|
|
|
17
17
|
丫口只做只读查询,不扫描网段,也不修改远程机器配置。
|
|
@@ -165,9 +165,9 @@ yaport service uninstall
|
|
|
165
165
|
|
|
166
166
|
- `ss -H -lntup`:监听中的 TCP/UDP 端口。
|
|
167
167
|
- `ps -ww -p <pid> -o pid=,args=`:尽量补全进程命令行。
|
|
168
|
-
- `nginx -T
|
|
168
|
+
- `nginx -T` / `openresty -T` / `tengine -T` / `/etc/caddy/Caddyfile`:对应代理进程正在监听且配置可读时,推断外部入口。
|
|
169
169
|
|
|
170
|
-
`ss` 会先执行;拿到 PID 后,`ps`
|
|
170
|
+
`ss` 会先执行;拿到 PID 后,`ps` 和代理配置读取会并发执行。丫口只在 `ss` 结果里看到 nginx、OpenResty、Tengine 或 Caddy 进程时才读取对应配置,避免拖慢普通机器刷新。任意一个失败都不会影响端口清单。
|
|
171
171
|
|
|
172
172
|
表格展示:
|
|
173
173
|
|
|
@@ -179,18 +179,26 @@ yaport service uninstall
|
|
|
179
179
|
- PID。
|
|
180
180
|
- 外部入口。
|
|
181
181
|
|
|
182
|
-
##
|
|
182
|
+
## 外部入口
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
当代理配置可读时,丫口会按端口匹配外部入口。
|
|
185
185
|
|
|
186
|
-
规则:
|
|
186
|
+
nginx / OpenResty / Tengine 规则:
|
|
187
187
|
|
|
188
188
|
- `listen 443 ssl` 或端口 `443` 展示为 `https://server_name`。
|
|
189
189
|
- 其他端口展示为 `http://server_name:port`。
|
|
190
190
|
- `server_name _`、空值、变量域名不展示。
|
|
191
|
-
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
- 配置读不到不会影响端口清单,只显示“无外部入口”。
|
|
192
|
+
|
|
193
|
+
Caddy 规则:
|
|
194
|
+
|
|
195
|
+
- 读取 `/etc/caddy/Caddyfile` 的站点块地址。
|
|
196
|
+
- `example.com` 展示为 `https://example.com`。
|
|
197
|
+
- `http://example.com:8080` 展示为 `http://example.com:8080`。
|
|
198
|
+
- `_`、变量地址、纯端口地址、通配地址暂不展示。
|
|
199
|
+
|
|
200
|
+
外部入口按机器缓存 5 分钟,包括短时间缓存读取失败,避免静默刷新时重复等待慢失败。
|
|
201
|
+
不同来源会显示不同小图标:nginx / OpenResty / Tengine / Caddy。
|
|
194
202
|
|
|
195
203
|
## Web UI 行为
|
|
196
204
|
|
|
@@ -204,7 +212,7 @@ yaport service uninstall
|
|
|
204
212
|
- “监听端口”指标卡可以按端口分组。
|
|
205
213
|
- “进程”指标卡可以按进程分组。
|
|
206
214
|
- 切换机器时按机器粒度保留上次结果,不会把上一台机器的端口展示到当前机器。
|
|
207
|
-
- 当前选中的机器会使用 OpenSSH `ControlMaster=auto` 和 `ControlPersist=5m`,`ss`、`ps
|
|
215
|
+
- 当前选中的机器会使用 OpenSSH `ControlMaster=auto` 和 `ControlPersist=5m`,`ss`、`ps`、代理配置读取、手动刷新和 1 分钟自动刷新会复用同一条 SSH master connection。
|
|
208
216
|
- 未选中机器的后台静默刷新不启用连接复用,避免维护过多后台连接。
|
|
209
217
|
- 网页在前台活跃时,选中机器默认每 1 分钟静默刷新,未选中机器默认每 5 分钟静默刷新。
|
|
210
218
|
- 机器列表和当前选中机器会写入 `sessionStorage`,刷新页面后能先看到本地缓存,再等待 server 返回最新数据。
|
|
@@ -215,14 +223,14 @@ yaport service uninstall
|
|
|
215
223
|
- 配置文件以 `0600` 权限写入。
|
|
216
224
|
- API 和 CLI JSON 输出不会返回已保存的 SSH 密码。
|
|
217
225
|
- 密码目前是本机明文保存;如果不想落盘,优先使用 SSH key 或 OpenSSH alias。
|
|
218
|
-
-
|
|
226
|
+
- 丫口只从远程机器读取端口、进程和代理配置,不会写远程文件。
|
|
219
227
|
|
|
220
228
|
## 远程机器要求
|
|
221
229
|
|
|
222
230
|
- 本机需要可用的 OpenSSH `ssh` 命令。
|
|
223
231
|
- 远程机器需要有 `ss` 命令。
|
|
224
232
|
- 进程命令行和 PID 是否完整,取决于当前 SSH 用户权限。
|
|
225
|
-
-
|
|
233
|
+
- 外部入口依赖远程机器能执行或读取对应代理配置。
|
|
226
234
|
|
|
227
235
|
## 本地开发
|
|
228
236
|
|
|
@@ -253,7 +261,7 @@ Yaport is read-only. It does not scan networks and does not modify remote machin
|
|
|
253
261
|
- Connect through one or two jump hosts via OpenSSH `ProxyJump`.
|
|
254
262
|
- Support SSH passwords for both target machines and jump hosts, from the GUI or CLI environment variables.
|
|
255
263
|
- Configure connection and command timeouts from both the UI and CLI for slow links or two-hop jump paths.
|
|
256
|
-
- Infer public-facing nginx URLs and show them next to matching listening ports.
|
|
264
|
+
- Infer public-facing nginx, OpenResty, Tengine, and Caddy URLs and show them next to matching listening ports.
|
|
257
265
|
- Reuse an OpenSSH ControlMaster connection for the currently selected machine to reduce repeated handshakes on slow jump paths.
|
|
258
266
|
|
|
259
267
|
## Installation
|
|
@@ -405,9 +413,9 @@ Yaport logs in through local OpenSSH and reads:
|
|
|
405
413
|
|
|
406
414
|
- `ss -H -lntup`: listening TCP/UDP ports.
|
|
407
415
|
- `ps -ww -p <pid> -o pid=,args=`: full process command lines when available.
|
|
408
|
-
- `nginx -T`:
|
|
416
|
+
- `nginx -T` / `openresty -T` / `tengine -T` / `/etc/caddy/Caddyfile`: proxy configuration, when the matching proxy process is listening and the config is readable, to infer external routes.
|
|
409
417
|
|
|
410
|
-
`ss` runs first. After PIDs are available, `ps` and
|
|
418
|
+
`ss` runs first. After PIDs are available, `ps` and proxy config reads run concurrently. Yaport only reads proxy config when the `ss` result already shows nginx, OpenResty, Tengine, or Caddy, so ordinary machines do not pay extra refresh latency. Either one may fail without breaking the port list.
|
|
411
419
|
|
|
412
420
|
The table shows:
|
|
413
421
|
|
|
@@ -419,18 +427,26 @@ The table shows:
|
|
|
419
427
|
- PID.
|
|
420
428
|
- External routes.
|
|
421
429
|
|
|
422
|
-
##
|
|
430
|
+
## External Routes
|
|
423
431
|
|
|
424
|
-
When
|
|
432
|
+
When proxy configuration is readable, Yaport matches external routes by port.
|
|
425
433
|
|
|
426
|
-
|
|
434
|
+
nginx / OpenResty / Tengine rules:
|
|
427
435
|
|
|
428
436
|
- `listen 443 ssl` or port `443` is shown as `https://server_name`.
|
|
429
437
|
- Other ports are shown as `http://server_name:port`.
|
|
430
438
|
- `server_name _`, empty names, and variable names are ignored.
|
|
431
|
-
- If
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
- If config cannot be read, the port list still works and the UI shows no external route.
|
|
440
|
+
|
|
441
|
+
Caddy rules:
|
|
442
|
+
|
|
443
|
+
- Yaport reads site block addresses from `/etc/caddy/Caddyfile`.
|
|
444
|
+
- `example.com` is shown as `https://example.com`.
|
|
445
|
+
- `http://example.com:8080` is shown as `http://example.com:8080`.
|
|
446
|
+
- `_`, variable addresses, port-only addresses, and wildcard addresses are ignored for now.
|
|
447
|
+
|
|
448
|
+
External routes are cached per machine for 5 minutes, including short-lived failure caching, so silent refreshes do not repeatedly wait on slow config reads.
|
|
449
|
+
Routes include a small source icon for nginx, OpenResty, Tengine, or Caddy.
|
|
434
450
|
|
|
435
451
|
## Web UI Behavior
|
|
436
452
|
|
|
@@ -444,7 +460,7 @@ Rules:
|
|
|
444
460
|
- The listening-port metric card can group the table by port.
|
|
445
461
|
- The process metric card can group the table by process.
|
|
446
462
|
- Switching machines keeps the selected machine's own cached result; it never shows another machine's ports as stale data.
|
|
447
|
-
- The selected machine uses OpenSSH `ControlMaster=auto` and `ControlPersist=5m`; `ss`, `ps`,
|
|
463
|
+
- The selected machine uses OpenSSH `ControlMaster=auto` and `ControlPersist=5m`; `ss`, `ps`, proxy config reads, manual refresh, and 1-minute auto refresh reuse the same SSH master connection.
|
|
448
464
|
- Background refreshes for unselected machines do not use connection reuse, so Yaport does not keep extra background master connections open.
|
|
449
465
|
- While the page is visible, the selected machine refreshes silently every 1 minute, and unselected machines refresh silently every 5 minutes.
|
|
450
466
|
- The machine list and selected machine ID are stored in `sessionStorage` for immediate visibility after page reload.
|
|
@@ -455,14 +471,14 @@ Rules:
|
|
|
455
471
|
- The config file is written with `0600` permissions.
|
|
456
472
|
- API responses and CLI JSON output never return stored SSH passwords.
|
|
457
473
|
- Passwords are currently stored locally in plain text. Prefer SSH keys or OpenSSH aliases if you do not want passwords on disk.
|
|
458
|
-
- Yaport only reads remote ports, processes, and
|
|
474
|
+
- Yaport only reads remote ports, processes, and proxy configuration. It does not write remote files.
|
|
459
475
|
|
|
460
476
|
## Remote Requirements
|
|
461
477
|
|
|
462
478
|
- Local machine: OpenSSH `ssh`.
|
|
463
479
|
- Remote machine: `ss`.
|
|
464
480
|
- Process command lines and PIDs depend on the SSH user's permissions.
|
|
465
|
-
-
|
|
481
|
+
- External routes require the matching proxy config command or file to be executable or readable on the remote machine.
|
|
466
482
|
|
|
467
483
|
## Development
|
|
468
484
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--ink:#12211d;--ink-soft:#42514b;--paper:#f7f5ec;--panel:#ebe7d7;--line:#c8cfbf;--rail:#d8e8dc;--teal:#128fa1;--teal-deep:#0b6370;--amber:#c48925;--brick:#a9473b;--white:#fffdf6;--shadow:0 14px 36px #19231e1f;color:var(--ink);background:var(--paper);font-synthesis:none;text-rendering:optimizelegibility;font-family:Aptos,Inter,Segoe UI,system-ui,sans-serif}*{box-sizing:border-box}body{background:linear-gradient(90deg, #12211d08 1px, transparent 1px), linear-gradient(#12211d08 1px, transparent 1px), var(--paper);background-size:24px 24px;min-width:320px;min-height:100vh;margin:0;font-size:14px;line-height:1.35}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible{outline-offset:2px;outline:3px solid #128fa159}.app-shell{grid-template-columns:minmax(280px,326px) 1fr;min-height:100vh;display:grid}.app-shell.sidebar-collapsed{grid-template-columns:64px 1fr}.machine-pane{background:var(--ink);height:100vh;min-height:0;color:var(--white);box-shadow:var(--shadow);flex-direction:column;gap:16px;padding:22px;display:flex;position:sticky;top:0;overflow:hidden}.machine-pane.collapsed{align-items:center;padding:16px 12px}.machine-pane-top{justify-content:space-between;align-items:center;gap:10px;display:flex}.machine-pane.collapsed .machine-pane-top{flex-direction:column}.machine-pane-content{flex-direction:column;flex:auto;gap:16px;min-height:0;padding-right:2px;display:flex;overflow:auto}.brand-block{align-items:center;gap:12px;display:flex}.brand-mark{background:#fffdf614;border:1px solid #fffdf642;border-radius:6px;flex:none;place-items:center;width:40px;height:40px;display:grid}.pane-collapse-button{color:#fffdf6c7;background:#fffdf612;border:1px solid #fffdf629;border-radius:6px;flex:none;place-items:center;width:32px;height:32px;display:inline-grid}.pane-collapse-button:hover{border-color:var(--amber);color:var(--white);background:#c4892529}.eyebrow,.section-label,.form-title,.metric-label{color:var(--ink-soft);letter-spacing:0;text-transform:uppercase;margin:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:700}.machine-pane .eyebrow,.machine-pane .section-label,.machine-pane .form-title{color:#fffdf69e}h1,h2{letter-spacing:0;margin:0;font-family:Arial Narrow,Aptos Display,Segoe UI,sans-serif;font-weight:800}h1{font-size:31px;line-height:1}h2{color:var(--ink);font-size:38px;line-height:1.02}.machine-form{gap:12px;padding-top:2px;display:grid}.add-machine-toggle{width:100%;min-height:40px;color:var(--white);text-align:left;background:#fffdf612;border:1px solid #fffdf629;border-radius:6px;justify-content:space-between;align-items:center;gap:10px;padding:0 12px;display:flex}.add-machine-toggle>span:first-child{align-items:center;gap:8px;font-weight:800;display:inline-flex}.add-machine-toggle:hover,.add-machine-toggle[aria-expanded=true]{border-color:var(--amber);background:#c4892529}.toggle-hint{color:#fffdf694;letter-spacing:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.jump-hosts{gap:8px;padding-top:2px;display:grid}.jump-host-group{background:#fffdf60d;border:1px solid #fffdf624;border-radius:6px;gap:8px;margin:0;padding:10px;display:grid}.jump-host-group legend{color:#fffdf69e;letter-spacing:0;padding:0 5px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.form-title,.section-label,.rail-header{align-items:center;gap:7px;display:flex}label{color:#fffdf6b8;gap:6px;font-size:12px;font-weight:700;display:grid}input,select{width:100%;min-height:38px;color:var(--white);background:#fffdf617;border:1px solid #fffdf62e;border-radius:6px;padding:0 10px}select{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}input::placeholder{color:#fffdf657}.form-grid{grid-template-columns:1fr 96px;gap:10px;display:grid}.primary-button,.secondary-button{border:0;border-radius:6px;justify-content:center;align-items:center;gap:7px;min-height:38px;font-size:13px;font-weight:800;display:inline-flex}.primary-button{background:var(--amber);color:#21180a}.secondary-button{border:1px solid var(--line);background:var(--white);color:var(--ink);padding:0 12px}.primary-button:disabled,.secondary-button:disabled{opacity:.55}.machine-list{gap:8px;min-height:0;display:grid}.machine-item{width:100%;color:var(--white);background:#fffdf612;border:1px solid #fffdf624;border-radius:6px;grid-template-columns:minmax(0,1fr) 34px;align-items:center;display:grid;overflow:hidden}.machine-item:hover,.machine-item.active{border-color:var(--amber);background:#c489252e}.machine-select-button{min-width:0;color:inherit;text-align:left;background:0 0;border:0;gap:4px;padding:11px 10px 11px 12px;display:grid}.machine-terminal-button{color:#fffdf6b3;background:#fffdf614;border:1px solid #fffdf624;border-radius:6px;place-items:center;width:28px;height:28px;display:inline-grid}.machine-terminal-button:hover{border-color:var(--teal);color:var(--white);background:#128fa138}.machine-name{font-size:14px;font-weight:800}.machine-address,.target-line,.quiet-state{color:#fffdf694;letter-spacing:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.machine-address,.target-line{overflow-wrap:anywhere}.quiet-state{border:1px dashed #fffdf633;border-radius:6px;padding:12px}.machine-context-menu{z-index:30;border:1px solid var(--line);background:var(--white);border-radius:6px;min-width:132px;padding:4px;position:fixed;box-shadow:0 12px 28px #12211d2e}.machine-context-menu button{width:100%;min-height:30px;color:var(--ink);text-align:left;background:0 0;border:0;border-radius:4px;align-items:center;gap:7px;padding:0 9px;font-size:12px;font-weight:800;display:flex}.machine-context-menu button:hover{background:#12211d14}.machine-context-menu button.danger{color:var(--brick)}.machine-context-menu button.danger:hover{background:#a9473b1c}.inventory-pane{flex-direction:column;gap:14px;min-width:0;padding:22px 30px;display:flex}.inventory-header{justify-content:space-between;align-items:flex-start;gap:14px;display:flex}.inventory-header .target-line{color:var(--ink-soft);margin:6px 0 0}.error-strip{border-left:5px solid var(--brick);color:#5d211b;white-space:pre-wrap;background:#f2d8cf;padding:10px 12px;font-size:13px;font-weight:700}.signal-rail{border:1px solid var(--line);background:var(--rail);gap:10px;padding:12px;display:grid}.rail-header{color:var(--ink);letter-spacing:0;text-transform:uppercase;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.port-grid{grid-template-columns:repeat(auto-fill,minmax(10px,1fr));align-items:stretch;gap:4px;min-height:38px;display:grid}.port-cell{background:var(--teal);border:1px solid #12211d38;min-height:13px;display:block}.port-cell.udp{background:var(--amber)}.empty-matrix{color:var(--ink-soft);grid-column:1/-1;align-self:center;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.summary-grid{grid-template-columns:repeat(5,minmax(106px,1fr));gap:10px;display:grid}.metric{border:1px solid var(--line);background:var(--white);gap:6px;min-height:72px;padding:10px 12px;display:grid}.metric-topline{justify-content:space-between;align-items:flex-start;gap:8px;min-width:0;display:flex}.metric.cyan{border-bottom:4px solid var(--teal)}.metric.amber{border-bottom:4px solid var(--amber)}.metric-label{align-items:center;gap:6px;min-width:0;display:inline-flex}.metric strong{align-self:end;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:28px;line-height:1}.metric-switch{border:1px solid var(--line);background:#e7e4d8;border-radius:999px;flex:none;align-items:center;width:36px;height:20px;padding:2px;display:inline-flex}.metric-switch span{background:var(--white);border-radius:999px;width:14px;height:14px;transition:transform .14s;display:block;box-shadow:0 1px 3px #12211d38}.metric-switch[aria-checked=true]{border-color:var(--teal);background:#128fa142}.metric-switch[aria-checked=true] span{background:var(--teal-deep);transform:translate(16px)}.table-wrap{border:1px solid var(--line);background:var(--white);overflow:auto}table{border-collapse:collapse;width:100%;min-width:920px}th,td{border-bottom:1px solid var(--line);text-align:left;padding:10px 12px}th{color:var(--ink-soft);letter-spacing:0;text-transform:uppercase;background:#e4eadf;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}td{font-size:13px}tbody tr:hover{background:#f1f4eb}.group-row td{border-bottom:1px solid var(--line);background:#edf0e7;padding:7px 12px}.group-row-label{color:var(--ink-soft);justify-content:space-between;align-items:center;gap:12px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;display:flex}.group-row-label span:first-child{overflow-wrap:anywhere;min-width:0}.group-row-label span:last-child{flex:none}.protocol-pill{min-width:44px;color:var(--teal-deep);background:#128fa129;border-radius:999px;justify-content:center;padding:3px 7px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;display:inline-flex}.protocol-pill.udp{color:#734a08;background:#c4892533}.mono,.port-number{font-variant-numeric:tabular-nums;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace}.port-number{font-weight:800}.process-cell{overflow-wrap:anywhere;min-width:180px;max-width:300px;line-height:1.35}.route-list{flex-wrap:wrap;gap:6px;min-width:190px;display:flex}.route-chip{max-width:280px;color:var(--teal-deep);letter-spacing:0;background:linear-gradient(90deg,#128fa124,#d8e8dc70);border:1px solid #128fa147;border-radius:999px;align-items:center;gap:6px;padding:5px 8px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;text-decoration:none;display:inline-flex}.route-source-badge{width:17px;height:17px;color:var(--white);flex:none;justify-content:center;align-items:center;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:10px;font-weight:900;line-height:1;display:inline-flex}.route-source-badge.nginx{clip-path:polygon(25% 4%,75% 4%,100% 50%,75% 96%,25% 96%,0 50%);background:#05924d;box-shadow:inset 0 -1px #12211d3d}.route-chip-url{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.route-chip:hover{border-color:var(--teal);background:#128fa12e}.route-empty{color:#42514b8a;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.table-empty{height:140px;color:var(--ink-soft);text-align:center;position:sticky;left:0}.spin{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;animation-duration:1ms!important;animation-iteration-count:1!important}}@media (width<=980px){.app-shell,.app-shell.sidebar-collapsed{grid-template-columns:1fr}.machine-pane{height:auto;min-height:auto;padding:20px;position:static}.machine-pane.collapsed{align-items:stretch}.machine-pane.collapsed .machine-pane-top{flex-direction:row}h1{font-size:30px}h2{font-size:34px}.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.metric.wide{grid-column:span 2}}@media (width<=560px){.machine-pane,.inventory-pane{padding:16px}h1{font-size:28px}h2{font-size:30px}.inventory-header{flex-direction:column;align-items:stretch}.form-grid,.summary-grid{grid-template-columns:1fr}.metric.wide{grid-column:auto}.secondary-button{width:100%}}
|
|
1
|
+
:root{--ink:#12211d;--ink-soft:#42514b;--paper:#f7f5ec;--panel:#ebe7d7;--line:#c8cfbf;--rail:#d8e8dc;--teal:#128fa1;--teal-deep:#0b6370;--amber:#c48925;--brick:#a9473b;--white:#fffdf6;--shadow:0 14px 36px #19231e1f;color:var(--ink);background:var(--paper);font-synthesis:none;text-rendering:optimizelegibility;font-family:Aptos,Inter,Segoe UI,system-ui,sans-serif}*{box-sizing:border-box}body{background:linear-gradient(90deg, #12211d08 1px, transparent 1px), linear-gradient(#12211d08 1px, transparent 1px), var(--paper);background-size:24px 24px;min-width:320px;min-height:100vh;margin:0;font-size:14px;line-height:1.35}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible{outline-offset:2px;outline:3px solid #128fa159}.app-shell{grid-template-columns:minmax(280px,326px) 1fr;min-height:100vh;display:grid}.app-shell.sidebar-collapsed{grid-template-columns:64px 1fr}.machine-pane{background:var(--ink);height:100vh;min-height:0;color:var(--white);box-shadow:var(--shadow);flex-direction:column;gap:16px;padding:22px;display:flex;position:sticky;top:0;overflow:hidden}.machine-pane.collapsed{align-items:center;padding:16px 12px}.machine-pane-top{justify-content:space-between;align-items:center;gap:10px;display:flex}.machine-pane.collapsed .machine-pane-top{flex-direction:column}.machine-pane-content{flex-direction:column;flex:auto;gap:16px;min-height:0;padding-right:2px;display:flex;overflow:auto}.brand-block{align-items:center;gap:12px;display:flex}.brand-mark{background:#fffdf614;border:1px solid #fffdf642;border-radius:6px;flex:none;place-items:center;width:40px;height:40px;display:grid}.pane-collapse-button{color:#fffdf6c7;background:#fffdf612;border:1px solid #fffdf629;border-radius:6px;flex:none;place-items:center;width:32px;height:32px;display:inline-grid}.pane-collapse-button:hover{border-color:var(--amber);color:var(--white);background:#c4892529}.eyebrow,.section-label,.form-title,.metric-label{color:var(--ink-soft);letter-spacing:0;text-transform:uppercase;margin:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:700}.machine-pane .eyebrow,.machine-pane .section-label,.machine-pane .form-title{color:#fffdf69e}h1,h2{letter-spacing:0;margin:0;font-family:Arial Narrow,Aptos Display,Segoe UI,sans-serif;font-weight:800}h1{font-size:31px;line-height:1}h2{color:var(--ink);font-size:38px;line-height:1.02}.machine-form{gap:12px;padding-top:2px;display:grid}.add-machine-toggle{width:100%;min-height:40px;color:var(--white);text-align:left;background:#fffdf612;border:1px solid #fffdf629;border-radius:6px;justify-content:space-between;align-items:center;gap:10px;padding:0 12px;display:flex}.add-machine-toggle>span:first-child{align-items:center;gap:8px;font-weight:800;display:inline-flex}.add-machine-toggle:hover,.add-machine-toggle[aria-expanded=true]{border-color:var(--amber);background:#c4892529}.toggle-hint{color:#fffdf694;letter-spacing:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.jump-hosts{gap:8px;padding-top:2px;display:grid}.jump-host-group{background:#fffdf60d;border:1px solid #fffdf624;border-radius:6px;gap:8px;margin:0;padding:10px;display:grid}.jump-host-group legend{color:#fffdf69e;letter-spacing:0;padding:0 5px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.form-title,.section-label,.rail-header{align-items:center;gap:7px;display:flex}label{color:#fffdf6b8;gap:6px;font-size:12px;font-weight:700;display:grid}input,select{width:100%;min-height:38px;color:var(--white);background:#fffdf617;border:1px solid #fffdf62e;border-radius:6px;padding:0 10px}select{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}input::placeholder{color:#fffdf657}.form-grid{grid-template-columns:1fr 96px;gap:10px;display:grid}.primary-button,.secondary-button{border:0;border-radius:6px;justify-content:center;align-items:center;gap:7px;min-height:38px;font-size:13px;font-weight:800;display:inline-flex}.primary-button{background:var(--amber);color:#21180a}.secondary-button{border:1px solid var(--line);background:var(--white);color:var(--ink);padding:0 12px}.primary-button:disabled,.secondary-button:disabled{opacity:.55}.machine-list{gap:8px;min-height:0;display:grid}.machine-item{width:100%;color:var(--white);background:#fffdf612;border:1px solid #fffdf624;border-radius:6px;grid-template-columns:minmax(0,1fr) 34px;align-items:center;display:grid;overflow:hidden}.machine-item:hover,.machine-item.active{border-color:var(--amber);background:#c489252e}.machine-select-button{min-width:0;color:inherit;text-align:left;background:0 0;border:0;gap:4px;padding:11px 10px 11px 12px;display:grid}.machine-terminal-button{color:#fffdf6b3;background:#fffdf614;border:1px solid #fffdf624;border-radius:6px;place-items:center;width:28px;height:28px;display:inline-grid}.machine-terminal-button:hover{border-color:var(--teal);color:var(--white);background:#128fa138}.machine-name{font-size:14px;font-weight:800}.machine-address,.target-line,.quiet-state{color:#fffdf694;letter-spacing:0;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.machine-address,.target-line{overflow-wrap:anywhere}.quiet-state{border:1px dashed #fffdf633;border-radius:6px;padding:12px}.machine-context-menu{z-index:30;border:1px solid var(--line);background:var(--white);border-radius:6px;min-width:132px;padding:4px;position:fixed;box-shadow:0 12px 28px #12211d2e}.machine-context-menu button{width:100%;min-height:30px;color:var(--ink);text-align:left;background:0 0;border:0;border-radius:4px;align-items:center;gap:7px;padding:0 9px;font-size:12px;font-weight:800;display:flex}.machine-context-menu button:hover{background:#12211d14}.machine-context-menu button.danger{color:var(--brick)}.machine-context-menu button.danger:hover{background:#a9473b1c}.inventory-pane{flex-direction:column;gap:14px;min-width:0;padding:22px 30px;display:flex}.inventory-header{justify-content:space-between;align-items:flex-start;gap:14px;display:flex}.inventory-header .target-line{color:var(--ink-soft);margin:6px 0 0}.error-strip{border-left:5px solid var(--brick);color:#5d211b;white-space:pre-wrap;background:#f2d8cf;padding:10px 12px;font-size:13px;font-weight:700}.signal-rail{border:1px solid var(--line);background:var(--rail);gap:10px;padding:12px;display:grid}.rail-header{color:var(--ink);letter-spacing:0;text-transform:uppercase;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800}.port-grid{grid-template-columns:repeat(auto-fill,minmax(10px,1fr));align-items:stretch;gap:4px;min-height:38px;display:grid}.port-cell{background:var(--teal);border:1px solid #12211d38;min-height:13px;display:block}.port-cell.udp{background:var(--amber)}.empty-matrix{color:var(--ink-soft);grid-column:1/-1;align-self:center;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.summary-grid{grid-template-columns:repeat(5,minmax(106px,1fr));gap:10px;display:grid}.metric{border:1px solid var(--line);background:var(--white);gap:6px;min-height:72px;padding:10px 12px;display:grid}.metric-topline{justify-content:space-between;align-items:flex-start;gap:8px;min-width:0;display:flex}.metric.cyan{border-bottom:4px solid var(--teal)}.metric.amber{border-bottom:4px solid var(--amber)}.metric-label{align-items:center;gap:6px;min-width:0;display:inline-flex}.metric strong{align-self:end;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:28px;line-height:1}.metric-switch{border:1px solid var(--line);background:#e7e4d8;border-radius:999px;flex:none;align-items:center;width:36px;height:20px;padding:2px;display:inline-flex}.metric-switch span{background:var(--white);border-radius:999px;width:14px;height:14px;transition:transform .14s;display:block;box-shadow:0 1px 3px #12211d38}.metric-switch[aria-checked=true]{border-color:var(--teal);background:#128fa142}.metric-switch[aria-checked=true] span{background:var(--teal-deep);transform:translate(16px)}.table-wrap{border:1px solid var(--line);background:var(--white);overflow:auto}table{border-collapse:collapse;width:100%;min-width:920px}th,td{border-bottom:1px solid var(--line);text-align:left;padding:10px 12px}th{color:var(--ink-soft);letter-spacing:0;text-transform:uppercase;background:#e4eadf;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}td{font-size:13px}tbody tr:hover{background:#f1f4eb}.group-row td{border-bottom:1px solid var(--line);background:#edf0e7;padding:7px 12px}.group-row-label{color:var(--ink-soft);justify-content:space-between;align-items:center;gap:12px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;display:flex}.group-row-label span:first-child{overflow-wrap:anywhere;min-width:0}.group-row-label span:last-child{flex:none}.protocol-pill{min-width:44px;color:var(--teal-deep);background:#128fa129;border-radius:999px;justify-content:center;padding:3px 7px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;display:inline-flex}.protocol-pill.udp{color:#734a08;background:#c4892533}.mono,.port-number{font-variant-numeric:tabular-nums;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace}.port-number{font-weight:800}.socket-count{color:var(--ink-soft);background:#e5e9de;border-radius:999px;margin-left:8px;padding:2px 6px;font-size:11px;font-weight:800;display:inline-flex}.process-cell{overflow-wrap:anywhere;min-width:180px;max-width:300px;line-height:1.35}.route-list{flex-wrap:wrap;gap:6px;min-width:190px;display:flex}.route-chip{max-width:280px;color:var(--teal-deep);letter-spacing:0;background:linear-gradient(90deg,#128fa124,#d8e8dc70);border:1px solid #128fa147;border-radius:999px;align-items:center;gap:6px;padding:5px 8px;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px;font-weight:800;text-decoration:none;display:inline-flex}.route-source-badge{width:17px;height:17px;color:var(--white);flex:none;justify-content:center;align-items:center;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:10px;font-weight:900;line-height:1;display:inline-flex}.route-source-badge.nginx{clip-path:polygon(25% 4%,75% 4%,100% 50%,75% 96%,25% 96%,0 50%);background:#05924d;box-shadow:inset 0 -1px #12211d3d}.route-source-badge.caddy{background:#1f88e5;border-radius:4px;box-shadow:inset 0 -1px #12211d3d}.route-source-badge.openresty{clip-path:polygon(25% 4%,75% 4%,100% 50%,75% 96%,25% 96%,0 50%);background:#6f9443;box-shadow:inset 0 -1px #12211d3d}.route-source-badge.tengine{background:#b85f19;border-radius:999px;box-shadow:inset 0 -1px #12211d3d}.route-chip-url{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.route-chip:hover{border-color:var(--teal);background:#128fa12e}.route-empty{color:#42514b8a;font-family:SFMono-Regular,Cascadia Mono,Roboto Mono,monospace;font-size:11px}.table-empty{height:140px;color:var(--ink-soft);text-align:center;position:sticky;left:0}.spin{animation:.9s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;animation-duration:1ms!important;animation-iteration-count:1!important}}@media (width<=980px){.app-shell,.app-shell.sidebar-collapsed{grid-template-columns:1fr}.machine-pane{height:auto;min-height:auto;padding:20px;position:static}.machine-pane.collapsed{align-items:stretch}.machine-pane.collapsed .machine-pane-top{flex-direction:row}h1{font-size:30px}h2{font-size:34px}.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.metric.wide{grid-column:span 2}}@media (width<=560px){.machine-pane,.inventory-pane{padding:16px}h1{font-size:28px}h2{font-size:30px}.inventory-header{flex-direction:column;align-items:stretch}.form-grid,.summary-grid{grid-template-columns:1fr}.metric.wide{grid-column:auto}.secondary-button{width:100%}}
|