dsh-remote-plugin 0.5.2 → 0.5.4

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/public/admin.html CHANGED
@@ -3,101 +3,301 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <meta name="theme-color" content="#0b0e1a">
6
+ <meta name="theme-color" content="#05348B">
7
7
  <title>DSH Remote · 管理</title>
8
+ <script>/* 首帧前应用皮肤 */ (function(){try{var t=localStorage.getItem('dshTheme');if(t!=='default'&&t!=='dark'&&t!=='light'&&t!=='neutral'){t=window.matchMedia('(prefers-color-scheme: light)').matches?'light':'default'}document.documentElement.setAttribute('data-theme',t)}catch(e){}})()</script>
8
9
  <link rel="stylesheet" href="../styles.css">
9
10
  <style>
10
- .admin-wrap { max-width: 860px; margin: 0 auto; padding: calc(env(safe-area-inset-top, 0px) + 14px) 14px 40px; }
11
- .admin-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
12
- .admin-title h1 { font-size: 20px; margin: 0; }
13
- .login-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; display: flex; gap: 8px; }
14
- .login-card input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; font: inherit; outline: none; }
15
- .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; margin-bottom: 14px; }
16
- .stat-card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; }
17
- .stat-card .v { font-size: 20px; font-weight: 700; word-break: break-all; }
18
- .stat-card .k { font-size: 11px; color: var(--muted); margin-top: 2px; }
19
- .stat-card.ok .v { color: var(--cyan); } .stat-card.warn .v { color: var(--orange); }
20
- .table-wrap { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; }
21
- table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 760px; }
22
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
23
- th { font-size: 11px; color: var(--muted); letter-spacing: .5px; }
11
+ .admin-wrap { max-width: 1180px; margin: 0 auto; padding: calc(env(safe-area-inset-top, 0px) + 14px) 14px 40px; }
12
+ .admin-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; }
13
+ .admin-title h1 { font-size: 20px; margin: 0; white-space: nowrap; }
14
+ .login-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); padding: 16px; display: flex; gap: 8px; }
15
+ .login-card input { flex: 1; background: var(--dsr-bg); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 10px; padding: 10px 12px; font: inherit; outline: none; }
16
+ .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 8px; margin-bottom: 14px; }
17
+ .stat-card { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); padding: 12px 14px; }
18
+ .stat-card .v { font-size: 20px; font-weight: 700; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
19
+ .stat-card .k { font-size: 11px; color: var(--dsr-muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
20
+ .stat-card.ok .v { color: var(--dsr-success); } .stat-card.warn .v { color: var(--dsr-warning); }
21
+ .table-wrap { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); overflow-x: auto; }
22
+ table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 0; }
23
+ th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--dsr-line); vertical-align: top; }
24
+ th { font-size: 11px; color: var(--dsr-muted); letter-spacing: .5px; white-space: nowrap; }
24
25
  tr:last-child td { border-bottom: none; }
25
- .badge { display: inline-block; font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); }
26
- .badge.app { color: var(--blue); border-color: rgba(91,140,255,.4); background: rgba(91,140,255,.08); }
27
- .badge.admin { color: var(--purple); border-color: rgba(180,140,255,.4); background: rgba(180,140,255,.08); }
28
- .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--line); margin-right: 5px; }
29
- .dot.on { background: var(--cyan); box-shadow: 0 0 6px rgba(125,207,255,.9); }
30
- .dot.off { background: var(--orange); }
26
+ .badge { display: inline-block; font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--dsr-line); color: var(--dsr-muted); white-space: nowrap; }
27
+ .badge.app { color: var(--dsr-accent-strong); border-color: var(--dsr-accent-line); background: var(--dsr-accent-soft); }
28
+ .badge.admin { color: var(--dsr-accent-2); border-color: var(--dsr-accent-2-line); background: var(--dsr-accent-2-soft); }
29
+ .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--dsr-line); margin-right: 5px; }
30
+ .dot.on { background: var(--dsr-success); box-shadow: 0 0 6px var(--dsr-success-line); }
31
+ .dot.off { background: var(--dsr-warning); }
31
32
  .mono { font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
32
- .ua { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
33
- .token-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 10px 0 14px; }
34
- .token-row code { flex: 1; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 12px; word-break: break-all; }
35
- .pair-box { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; margin-bottom: 14px; text-align: center; }
33
+ .nowrap { white-space: nowrap; }
34
+ .ua { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-muted); }
35
+ /* 宽屏: 固定列宽让表格铺满容器, 名称列吸收剩余宽度, 消除临界横向滚动条 */
36
+ @media (min-width: 1000px) {
37
+ table { table-layout: fixed; }
38
+ th:nth-child(1) { width: 80px; }
39
+ th:nth-child(2) { width: 180px; }
40
+ th:nth-child(3) { width: 88px; }
41
+ th:nth-child(4) { width: 120px; }
42
+ th:nth-child(5) { width: 84px; }
43
+ th:nth-child(6) { width: 56px; }
44
+ th:nth-child(7) { width: 110px; }
45
+ th:nth-child(8) { width: 280px; }
46
+ th:nth-child(9) { width: 72px; }
47
+ td:nth-child(2) { white-space: nowrap; }
48
+ td:nth-child(2) b { display: inline-block; max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
49
+ tbody tr:hover { background: var(--dsr-accent-soft); }
50
+ }
51
+ .token-row { display: flex; flex-direction: column; align-items: stretch; gap: 8px; margin: 10px 0 14px; }
52
+ .token-row code { flex: none; width: 100%; background: var(--dsr-bg); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 8px 10px; font-size: 12px; white-space: nowrap; overflow-x: auto; }
53
+ .token-actions { display: flex; gap: 8px; flex-wrap: wrap; }
54
+ .token-actions .mini-btn { flex: 1 1 auto; text-align: center; padding: 6px 8px; font-size: 12.5px; }
55
+ .conn-badge { white-space: nowrap; }
56
+ #btn-close-drawer { white-space: nowrap; }
57
+ @media (max-width: 720px) {
58
+ .admin-title h1 { font-size: 16px; }
59
+ .admin-title .right { gap: 6px; }
60
+ .gh-btn { display: none; }
61
+ #btn-close-drawer span { display: none; }
62
+ #btn-close-drawer { padding: 5px 9px; }
63
+ #btn-console span { display: none; }
64
+ #btn-console { padding: 5px 9px; }
65
+ .conn-badge { padding: 3px 7px; }
66
+ }
67
+ @media (max-width: 760px) {
68
+ .table-wrap { overflow: visible; background: transparent; border: none; }
69
+ .table-wrap table { min-width: 0; }
70
+ .table-wrap thead { display: none; }
71
+ .table-wrap tbody { display: flex; flex-direction: column; gap: 8px; }
72
+ .table-wrap tr { display: block; background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 12px; }
73
+ .table-wrap tr:last-child td { border-bottom: none; }
74
+ .table-wrap td { display: flex; align-items: center; gap: 8px; padding: 4px 0; border-bottom: 1px dashed var(--dsr-divider); }
75
+ .table-wrap td:last-child { border-bottom: none; }
76
+ .table-wrap td[data-label]::before { content: attr(data-label); min-width: 64px; font-size: 11px; color: var(--dsr-muted); flex: none; }
77
+ .table-wrap td.ua { display: none; }
78
+ .table-wrap td.act { justify-content: flex-end; }
79
+ .table-wrap td .mini-btn { margin-left: auto; }
80
+ }
81
+ .pair-box { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: var(--dsr-radius); padding: 14px; margin-bottom: 14px; text-align: center; }
36
82
  .pair-box .pair-title { font-weight: 600; margin-bottom: 8px; }
37
83
  .pair-qr { display: inline-block; background: #fff; border-radius: 8px; padding: 6px; line-height: 0; }
38
84
  .pair-qr svg { display: block; width: 180px; height: 180px; }
39
85
  .pair-box .muted { margin-top: 8px; font-size: 12px; }
40
- .empty { text-align: center; color: var(--muted); padding: 30px 0; }
86
+ .empty { text-align: center; color: var(--dsr-muted); padding: 30px 0; }
41
87
  .empty-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 12px; }
42
88
  a.mini-btn { text-decoration: none; display: inline-flex; align-items: center; }
43
- .gh-btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; color: var(--text); }
89
+ .gh-btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; color: var(--dsr-text); }
44
90
  .gh-btn svg { width: 16px; height: 16px; fill: currentColor; }
45
91
  .admin-title .right { display: flex; align-items: center; gap: 10px; }
92
+ /* 放在 .gh-btn 基础规则之后, 窄屏才能覆盖 display */
93
+ @media (max-width: 720px) {
94
+ a.gh-btn { display: none; }
95
+ }
96
+ @media (max-width: 400px) {
97
+ .admin-title h1 span { display: none; }
98
+ }
46
99
  </style>
47
100
  </head>
48
101
  <body>
49
102
  <div class="admin-wrap">
50
103
  <div class="admin-title">
51
104
  <div style="display:flex;align-items:center;gap:10px">
52
- <a id="btn-console" class="mini-btn" href="../" title="返回控制台">‹ 控制台</a>
53
- <button id="btn-close-drawer" class="mini-btn hidden" title="收起面板">‹ 收起面板</button>
54
- <h1>DSH Remote 管理</h1>
105
+ <a id="btn-console" class="mini-btn" href="../" data-i18n-title="consoleTitle">‹ <span data-i18n="console">控制台</span></a>
106
+ <button id="btn-close-drawer" class="mini-btn hidden" data-i18n-title="collapse">‹ <span data-i18n="collapse">收起面板</span></button>
107
+ <h1>DSH Remote <span data-i18n="admin">管理</span></h1>
55
108
  </div>
56
109
  <div class="right">
57
- <a class="mini-btn gh-btn" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener" title="访问 GitHub 仓库">
110
+ <a class="mini-btn gh-btn" href="https://github.com/Blank-not-black/dsh-Remote" target="_blank" rel="noopener" title="GitHub">
58
111
  <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8a8.01 8.01 0 0 0 5.47 7.59c.4.08.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>
59
- 仓库
112
+ <span data-i18n="repo">仓库</span>
60
113
  </a>
61
- <button id="conn-badge" class="conn-badge off" title="网关面板">未认证</button>
114
+ <button id="btn-theme" class="mini-btn" title="">
115
+ <span id="theme-swatch" class="theme-swatch-dot"></span><span id="theme-label" class="t-label">默认深空</span>
116
+ </button>
117
+ <button id="btn-lang" class="mini-btn" title="Language / 语言">EN</button>
118
+ <button id="conn-badge" class="conn-badge off" title="网关面板"><span data-i18n="unauth">未认证</span></button>
62
119
  </div>
63
120
  </div>
64
121
 
65
122
  <div id="login-view" class="login-card hidden">
66
- <input id="token-input" type="password" placeholder="输入网关访问令牌" autocomplete="off">
67
- <button id="btn-login" class="mini-btn">进入</button>
123
+ <input id="token-input" type="password" placeholder="输入网关访问令牌" autocomplete="off" data-i18n-placeholder="tokenPlaceholder">
124
+ <button id="btn-login" class="mini-btn" data-i18n="enter">进入</button>
68
125
  </div>
69
126
 
70
127
  <div id="main-view" class="hidden">
71
128
  <div class="token-row">
72
129
  <code id="token-full">—</code>
73
- <button id="btn-gateway" class="mini-btn hidden">启动网关</button>
74
- <button id="btn-qr" class="mini-btn hidden">二维码</button>
75
- <button id="btn-rotate" class="mini-btn hidden">轮换令牌</button>
76
- <button id="btn-copy" class="mini-btn">复制令牌</button>
77
- <button id="btn-logout" class="mini-btn">退出</button>
130
+ <div class="token-actions">
131
+ <button id="btn-gateway" class="mini-btn hidden" data-i18n="startGateway">启动网关</button>
132
+ <button id="btn-qr" class="mini-btn hidden" data-i18n="qrCode">二维码</button>
133
+ <button id="btn-rotate" class="mini-btn hidden" data-i18n="rotateToken">轮换令牌</button>
134
+ <button id="btn-copy" class="mini-btn" data-i18n="copyToken">复制令牌</button>
135
+ <button id="btn-logout" class="mini-btn" data-i18n="signOut">退出</button>
136
+ </div>
78
137
  </div>
79
138
 
80
139
  <div id="pair-box" class="pair-box hidden">
81
- <div class="pair-title">手机 App 扫码配对</div>
140
+ <div class="pair-title" data-i18n="pairTitle">手机 App 扫码配对</div>
82
141
  <div class="pair-qr" id="pair-qr"></div>
83
- <div class="muted" id="pair-hint">用 App 内「设置 → 扫码连接」,或系统相机扫一扫自动打开 App</div>
142
+ <div class="muted" id="pair-hint"></div>
84
143
  </div>
85
144
 
86
145
  <div class="stat-grid" id="stats"></div>
87
146
 
88
- <div class="section-head"><span>已连接设备</span><span id="device-summary" class="muted"></span></div>
147
+ <div class="section-head"><span data-i18n="devices">已连接设备</span><span id="device-summary" class="muted"></span></div>
89
148
  <div class="table-wrap">
90
149
  <table>
91
- <thead><tr><th>状态</th><th>名称</th><th>类型</th><th>IP</th><th>通道</th><th>请求</th><th>最后活跃</th><th>UA</th><th></th></tr></thead>
150
+ <thead><tr>
151
+ <th data-i18n="th.status">状态</th><th data-i18n="th.name">名称</th><th data-i18n="th.type">类型</th>
152
+ <th data-i18n="th.ip">IP</th><th data-i18n="th.channels">通道</th><th data-i18n="th.requests">请求</th>
153
+ <th data-i18n="th.lastSeen">最后活跃</th><th data-i18n="th.ua">UA</th><th></th>
154
+ </tr></thead>
92
155
  <tbody id="device-rows"></tbody>
93
156
  </table>
94
- <div id="device-empty" class="empty hidden">暂无设备记录</div>
157
+ <div id="device-empty" class="empty hidden" data-i18n="noDevices">暂无设备记录</div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- 皮肤选择模态 -->
163
+ <div id="modal-theme" class="modal hidden">
164
+ <div class="modal-card">
165
+ <div class="modal-title" data-i18n="theme.panelTitle">选择配色</div>
166
+ <div id="theme-options" class="theme-panel-list"></div>
167
+ <div class="modal-actions">
168
+ <button id="theme-close" class="btn subtle" data-i18n="theme.close">关闭</button>
95
169
  </div>
96
170
  </div>
97
171
  </div>
98
172
 
99
173
  <div id="toast" class="toast hidden"></div>
174
+ <script>
175
+ window.ADMIN_STR = {
176
+ zh: {
177
+ 'console': '控制台',
178
+ 'consoleTitle': '返回控制台',
179
+ 'collapse': '收起面板',
180
+ 'admin': '管理',
181
+ 'theme.default': '默认深空', 'theme.dark': '落日', 'theme.light': '易北爱乐厅', 'theme.neutral': '草原孤塔',
182
+ 'theme.panelTitle': '选择配色', 'theme.close': '关闭',
183
+ 'repo': '仓库',
184
+ 'unauth': '未认证',
185
+ 'tokenPlaceholder': '输入网关访问令牌',
186
+ 'enter': '进入',
187
+ 'startGateway': '启动网关',
188
+ 'stopGateway': '停止网关',
189
+ 'starting': '启动中…',
190
+ 'stopping': '停止中…',
191
+ 'qrCode': '二维码',
192
+ 'rotateToken': '轮换令牌',
193
+ 'copyToken': '复制令牌',
194
+ 'signOut': '退出',
195
+ 'pairTitle': '手机 App 扫码配对',
196
+ 'devices': '已连接设备',
197
+ 'noDevices': '暂无设备记录',
198
+ 'th.status': '状态', 'th.name': '名称', 'th.type': '类型', 'th.ip': 'IP',
199
+ 'th.channels': '通道', 'th.requests': '请求', 'th.lastSeen': '最后活跃', 'th.ua': 'UA',
200
+ 'badge.embedded': '内嵌', 'badge.gateway': '网关', 'badge.connected': '已连接',
201
+ 'badge.gateway.title': '新标签页打开网关管理面板', 'badge.gatewayDown': '网关未运行',
202
+ 'token.pluginNoGateway': '插件模式 · 未接网关, 无需令牌', 'token.unavailable': '未获取到令牌',
203
+ 'toast.tokenInvalid': '令牌无效', 'toast.connFailed': '连接失败',
204
+ 'stat.pluginVersion': '插件版本', 'stat.gatewayVersion': '网关版本',
205
+ 'stat.updateAvailable': '{version} 可用', 'stat.currentV': '当前 v{version}',
206
+ 'stat.download': '去下载', 'stat.embedded': 'DSH 内嵌 · 免网关',
207
+ 'stat.updateCheck': '更新检查: {error}', 'stat.latest': '已是最新(来源检查)', 'stat.notChecked': '未检查更新',
208
+ 'stat.hostIP': '主机 IP · {hostname}', 'stat.ipSep': '、', 'stat.phoneGateway': ' (手机连 8787 网关)', 'stat.phoneThis': ' (手机连这个地址)',
209
+ 'stat.reachable': '可达', 'stat.unreachable': '不可达', 'stat.dshUpstream': 'DSH 上游 {url}',
210
+ 'stat.devicesOnline': '设备在线 / 累计', 'stat.totalRequests': '总请求数', 'stat.authFailures': '认证失败',
211
+ 'stat.uptime': '运行时长 · {host}:{port}',
212
+ 'unit.sec': ' 秒', 'unit.min': ' 分钟', 'unit.hour': ' 小时 ', 'unit.minShort': ' 分', 'unit.day': ' 天 ',
213
+ 'device.installedNotRunning': '网关已安装 · 当前未运行', 'device.noGatewayBinary': '未检测到网关程序',
214
+ 'device.ipRefresh': '{n} 个 IP · 每 5 秒刷新',
215
+ 'device.downloadApp': '下载手机 App', 'device.downloadGateway': '下载网关 ({os})',
216
+ 'device.noBinaryGuide': '本插件包未包含网关程序:下载对应系统的网关并运行',
217
+ 'device.afterRunGuide': '运行网关后回到本页刷新,即可看到设备监控与完整令牌',
218
+ 'device.installedGuide': '网关已随插件安装,当前未运行 — 点击上方「启动网关」开启',
219
+ 'device.afterStartGuide': '启动后本页会自动刷新为网关模式(完整设备监控 + 令牌)',
220
+ 'device.kind.app': '手机App', 'device.kind.admin': '管理页', 'device.kind.web': '浏览器', 'device.kind.unknown': '未知',
221
+ 'device.online': '在线', 'device.offline': '离线', 'device.note': '备注', 'device.kick': '断开',
222
+ 'prompt.note': '给 {ip} 设置备注(留空清除):', 'toast.noteSaved': '备注已保存', 'toast.noteFailed': '保存失败',
223
+ 'confirm.kick': '断开该设备的连接?', 'toast.kicked': '已断开 {ip}', 'toast.opFailed': '操作失败',
224
+ 'toast.tokenCopied': '令牌已复制', 'toast.copyFailed': '复制失败,请手动选择',
225
+ 'toast.gatewayDown': '网关未运行', 'toast.popupBlocked': '浏览器阻止了新窗口,请允许弹窗',
226
+ 'confirm.rotate': '轮换后旧令牌立即失效,手机与浏览器都需要重新扫码/输入。继续?',
227
+ 'toast.rotated': '令牌已轮换,请重新给设备配对', 'toast.rotateFailed': '轮换失败',
228
+ 'toast.rotateFailedMsg': '轮换失败:{msg}',
229
+ 'toast.gatewayStarted': '网关已启动', 'toast.gatewayAlready': '网关已在运行', 'toast.gatewayStopped': '网关已停止',
230
+ 'toast.gatewayPending': '网关启动中,稍后刷新', 'toast.done': '已执行',
231
+ 'toast.opFailedMsg': '操作失败:{msg}',
232
+ 'pair.hint': '{base} · App「设置 → 扫码连接」,或系统相机扫一扫自动打开 App',
233
+ 'pair.failed': '二维码生成失败',
234
+ 'login.title': 'DSH Remote 管理'
235
+ },
236
+ en: {
237
+ 'console': 'Console',
238
+ 'consoleTitle': 'Back to console',
239
+ 'collapse': 'Collapse panel',
240
+ 'admin': 'Admin',
241
+ 'theme.default': 'Default', 'theme.dark': 'Sunset', 'theme.light': 'Elbphilharmonie', 'theme.neutral': 'Prairie Tower',
242
+ 'theme.panelTitle': 'Choose theme', 'theme.close': 'Close',
243
+ 'repo': 'Repo',
244
+ 'unauth': 'Not connected',
245
+ 'tokenPlaceholder': 'Gateway access token',
246
+ 'enter': 'Enter',
247
+ 'startGateway': 'Start gateway',
248
+ 'stopGateway': 'Stop gateway',
249
+ 'starting': 'Starting…',
250
+ 'stopping': 'Stopping…',
251
+ 'qrCode': 'QR code',
252
+ 'rotateToken': 'Rotate token',
253
+ 'copyToken': 'Copy token',
254
+ 'signOut': 'Sign out',
255
+ 'pairTitle': 'Pair with the mobile app',
256
+ 'devices': 'Connected devices',
257
+ 'noDevices': 'No devices yet',
258
+ 'th.status': 'Status', 'th.name': 'Name', 'th.type': 'Type', 'th.ip': 'IP',
259
+ 'th.channels': 'Channels', 'th.requests': 'Requests', 'th.lastSeen': 'Last seen', 'th.ua': 'UA',
260
+ 'badge.embedded': 'Embedded', 'badge.gateway': 'Gateway', 'badge.connected': 'Connected',
261
+ 'badge.gateway.title': 'Open gateway admin in a new tab', 'badge.gatewayDown': 'Gateway not running',
262
+ 'token.pluginNoGateway': 'Plugin mode · no gateway, no token needed', 'token.unavailable': 'Token unavailable',
263
+ 'toast.tokenInvalid': 'Invalid token', 'toast.connFailed': 'Connection failed',
264
+ 'stat.pluginVersion': 'Plugin version', 'stat.gatewayVersion': 'Gateway version',
265
+ 'stat.updateAvailable': 'v{version} available', 'stat.currentV': 'Current v{version}',
266
+ 'stat.download': 'Download', 'stat.embedded': 'Embedded in DSH · no gateway',
267
+ 'stat.updateCheck': 'Update check: {error}', 'stat.latest': 'Up to date (source check)', 'stat.notChecked': 'Not checked',
268
+ 'stat.hostIP': 'Host IP · {hostname}', 'stat.ipSep': ', ', 'stat.phoneGateway': ' (phone connects to gateway 8787)', 'stat.phoneThis': ' (phone connects to this address)',
269
+ 'stat.reachable': 'Reachable', 'stat.unreachable': 'Unreachable', 'stat.dshUpstream': 'DSH upstream {url}',
270
+ 'stat.devicesOnline': 'Devices online / total', 'stat.totalRequests': 'Total requests', 'stat.authFailures': 'Auth failures',
271
+ 'stat.uptime': 'Uptime · {host}:{port}',
272
+ 'unit.sec': 's', 'unit.min': 'min', 'unit.hour': 'h ', 'unit.minShort': 'm', 'unit.day': 'd ',
273
+ 'device.installedNotRunning': 'Gateway installed · not running', 'device.noGatewayBinary': 'Gateway binary not found',
274
+ 'device.ipRefresh': '{n} IPs · refreshed every 5s',
275
+ 'device.downloadApp': 'Download mobile app', 'device.downloadGateway': 'Download gateway ({os})',
276
+ 'device.noBinaryGuide': 'This plugin package does not include the gateway binary: download and run the gateway for your OS',
277
+ 'device.afterRunGuide': 'After starting the gateway, come back and refresh this page to see device monitoring and the full token',
278
+ 'device.installedGuide': 'The gateway is installed with the plugin but not running — click "Start gateway" above',
279
+ 'device.afterStartGuide': 'After startup this page refreshes into gateway mode (full device monitoring + token)',
280
+ 'device.kind.app': 'App', 'device.kind.admin': 'Admin', 'device.kind.web': 'Browser', 'device.kind.unknown': 'Unknown',
281
+ 'device.online': 'Online', 'device.offline': 'Offline', 'device.note': 'Note', 'device.kick': 'Kick',
282
+ 'prompt.note': 'Set note for {ip} (empty to clear):', 'toast.noteSaved': 'Note saved', 'toast.noteFailed': 'Save failed',
283
+ 'confirm.kick': 'Disconnect this device?', 'toast.kicked': 'Disconnected {ip}', 'toast.opFailed': 'Operation failed',
284
+ 'toast.tokenCopied': 'Token copied', 'toast.copyFailed': 'Copy failed, select manually',
285
+ 'toast.gatewayDown': 'Gateway not running', 'toast.popupBlocked': 'Popup blocked, allow popups for this site',
286
+ 'confirm.rotate': 'After rotation the old token is invalid immediately. Phones and browsers must re-scan / re-enter. Continue?',
287
+ 'toast.rotated': 'Token rotated, re-pair your devices', 'toast.rotateFailed': 'Rotate failed',
288
+ 'toast.rotateFailedMsg': 'Rotate failed: {msg}',
289
+ 'toast.gatewayStarted': 'Gateway started', 'toast.gatewayAlready': 'Gateway already running', 'toast.gatewayStopped': 'Gateway stopped',
290
+ 'toast.gatewayPending': 'Gateway starting, refresh later', 'toast.done': 'Done',
291
+ 'toast.opFailedMsg': 'Operation failed: {msg}',
292
+ 'pair.hint': '{base} · App "Settings → Scan to connect", or scan with the system camera to open the App',
293
+ 'pair.failed': 'QR generation failed',
294
+ 'login.title': 'DSH Remote Admin'
295
+ }
296
+ }
297
+ </script>
100
298
  <script src="../qrcode.min.js"></script>
299
+ <script src="../i18n.js"></script>
300
+ <script src="../theme.js"></script>
101
301
  <script src="../admin.js"></script>
102
302
  </body>
103
303
  </html>