@hyzyn/dsh-docker 0.16.2 → 0.17.1
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.en.md +10 -1
- package/README.md +8 -1
- package/client-src/docker.css +70 -23
- package/client-src/index.js +72 -2
- package/client.js +91 -44
- package/package.json +3 -3
- package/scripts/client-smoke.mjs +9 -5
package/README.en.md
CHANGED
|
@@ -69,7 +69,7 @@ dsh plugin --profile web add link:$(pwd)/packages/docker # repo development a
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
The aggregate package `@hyzyn/dsh-all` (or the repo-root bundle) already includes this plugin, so there is no need
|
|
72
|
-
to add it separately. After installing, restart `dsh web` and the "Containers" entry appears in the
|
|
72
|
+
to add it separately. After installing, restart `dsh web` and the "Containers" entry appears **in the panel navigation directly below "Plugins"** (same column and same spec as "Today's Worth Reading / Global search / Terminal"); the
|
|
73
73
|
"Docker Container Panel" card maintains targets and switches, and **saving applies hot** (`settings/updated`
|
|
74
74
|
triggers re-resolution, with no restart needed).
|
|
75
75
|
|
|
@@ -658,6 +658,15 @@ loopback, `localhost`, or a hostname / `/etc/hosts` alias that **resolves to thi
|
|
|
658
658
|
`forbidden: loopback-only`. The request body is capped at 1MB, and responses are uniformly `application/json` +
|
|
659
659
|
`referrer-policy: no-referrer`.
|
|
660
660
|
|
|
661
|
+
**Not available behind a reverse proxy or from another machine**: non-local requests get 403
|
|
662
|
+
`forbidden: loopback-only`. That is deliberate — these routes carry the mutating and execution
|
|
663
|
+
surface (docker stop / remove / exec), which is not opened to remote sessions (see [ROADMAP.md](../../ROADMAP.md)
|
|
664
|
+
item 5 § 5.4). To use this panel from another machine, use an **SSH tunnel**:
|
|
665
|
+
`ssh -L 3080:127.0.0.1:3080 <host>` and open `http://localhost:3080` — the host sees a loopback
|
|
666
|
+
source and a `localhost` Host, so the fence admits it, and the session cookie is minted for that
|
|
667
|
+
authority.
|
|
668
|
+
|
|
669
|
+
|
|
661
670
|
**On top of loopback there is a second gate, the "same-origin proof"**: the four SSE streams (`/logs/stream`,
|
|
662
671
|
`/stats/stream`, `/events/stream`, `/images/pull/stream`) and every write sub-route (`/action`,
|
|
663
672
|
`/images/remove|prune`, `/networks/remove|prune`, `/volumes/remove|prune`, `/exec`, `/elevate` (including
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ dsh plugin --profile web add link:$(pwd)/packages/docker # 仓库开发调试
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
聚合包 `@hyzyn/dsh-all`(或仓库根 bundle)已包含本插件,一次装齐时不必单独 add。装完重启 `dsh web`,
|
|
60
|
-
|
|
60
|
+
侧栏**面板导航里紧跟「插件」那一行**出现「容器」入口(与「今日值得读 / 全局搜索 / 终端」同栏、同规格);「Docker 容器面板」卡片维护目标与开关,**保存即热生效**(`settings/updated`
|
|
61
61
|
触发重解析,无需重启)。
|
|
62
62
|
|
|
63
63
|
> 已在 web profile 里装过 `@hyzyn/dsh-all` 或根 bundle 时**不要**再 add 本包,否则插件行重复挂载,
|
|
@@ -558,6 +558,13 @@ HTTP `POST /api/dsh-docker/config` 是卡片的写入通道,只接受下表这
|
|
|
558
558
|
非本机请求一律 403 `forbidden: loopback-only`。请求体上限 1MB,响应统一 `application/json` +
|
|
559
559
|
`referrer-policy: no-referrer`。
|
|
560
560
|
|
|
561
|
+
**反代 / 另一台机器访问下不可用**:非本机请求一律 403 `forbidden: loopback-only`——这是刻意的,
|
|
562
|
+
这些路由里包含变更与执行面(docker 的 stop / remove / exec),不开放给远程会话(评估与取舍见 [ROADMAP.md](../../ROADMAP.md)
|
|
563
|
+
第 5 项 § 5.4)。需要从另一台机器用这块面板时走 **SSH 隧道**:`ssh -L 3080:127.0.0.1:3080 <host>`,
|
|
564
|
+
浏览器打开 `http://localhost:3080`——宿主侧看到的来源是回环、Host 是 `localhost`,围栏直接放行,
|
|
565
|
+
会话 cookie 也按该 authority 铸造。
|
|
566
|
+
|
|
567
|
+
|
|
561
568
|
**除 loopback 之外还有一道「同源证明」**:四条 SSE(`/logs/stream`、`/stats/stream`、`/events/stream`、
|
|
562
569
|
`/images/pull/stream`)与全部写子路由(`/action`、`/images/remove|prune`、`/networks/remove|prune`、
|
|
563
570
|
`/volumes/remove|prune`、`/exec`、`/elevate`(含 `/elevate/status`、`/elevate/revoke`)、`/connect-local`)要求请求带 `Origin: <同源>` 或
|
package/client-src/docker.css
CHANGED
|
@@ -120,69 +120,116 @@
|
|
|
120
120
|
|
|
121
121
|
/* ============================ 侧边栏入口 ============================ */
|
|
122
122
|
|
|
123
|
+
/*
|
|
124
|
+
* 入口与宿主面板行(`.panelRow`)**同规格**:入口住在宿主的面板导航里、紧跟固定面板
|
|
125
|
+
* 「插件」,与「插件 / 插件市场 / …」并排出现,差一个字号或一个灰度都看得出来。
|
|
126
|
+
*
|
|
127
|
+
* 取值一律跟着宿主走、不抄数字:`font: inherit` 拿宿主侧栏字号(14px)、
|
|
128
|
+
* `color: inherit` 拿 `.root` 的主文字色、行内边距与标签间距取宿主的 `--dsh-sidebar-*`
|
|
129
|
+
* 几何契约、圆角走 radius token、悬停底色用宿主侧栏自己在用的
|
|
130
|
+
* `--dsw-alias-interactive-bg-hover`、键盘焦点走宿主的全局焦点环令牌。
|
|
131
|
+
*
|
|
132
|
+
* 只有「上下内边距 / 最小行高」宿主没暴露令牌(rc.1 是 7px/36px、rc.2 是 5px/32px),
|
|
133
|
+
* 这里给 rc.2 的兜底值。
|
|
134
|
+
*
|
|
135
|
+
* 宿主无全局 box-sizing reset:`box-sizing` 必须显式声明,否则 width:100% + 左右 padding
|
|
136
|
+
* 在 content-box 下比容器宽,右侧被侧边栏裁掉(hover 底色缺一角)。
|
|
137
|
+
*/
|
|
123
138
|
[data-dsh-docker-entry] {
|
|
124
139
|
position: relative;
|
|
125
|
-
/* 与 tty 侧边栏入口同因:宿主无全局 box-sizing reset,width:100% + 左右 padding
|
|
126
|
-
在 content-box 下会比容器宽 24px,多出的右侧会被侧边栏裁掉(hover 底色缺一角)。 */
|
|
127
140
|
box-sizing: border-box;
|
|
128
141
|
display: flex;
|
|
129
142
|
align-items: center;
|
|
130
|
-
gap: var(--
|
|
143
|
+
gap: var(--dsh-sidebar-label-gap, 6px);
|
|
131
144
|
width: 100%;
|
|
132
145
|
min-width: 0;
|
|
133
|
-
height:
|
|
134
|
-
padding:
|
|
135
|
-
border
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
min-height: 32px;
|
|
147
|
+
padding: 5px calc(var(--dsh-sidebar-row-padding, 12px) - var(--dsh-sidebar-paint-inset, 3px));
|
|
148
|
+
border: none;
|
|
149
|
+
border-inline: var(--dsh-sidebar-paint-inset, 3px) solid transparent;
|
|
150
|
+
border-radius: var(--dsw-radius-md);
|
|
151
|
+
background: 0 0;
|
|
152
|
+
background-clip: padding-box;
|
|
153
|
+
color: inherit;
|
|
154
|
+
font: inherit;
|
|
155
|
+
line-height: 22px;
|
|
139
156
|
white-space: nowrap;
|
|
140
157
|
cursor: pointer;
|
|
141
158
|
user-select: none;
|
|
142
|
-
transition: background
|
|
159
|
+
transition: background-color var(--dk-dur) var(--dk-ease);
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
[data-dsh-docker-entry]:hover {
|
|
146
|
-
background: var(--dk-hover);
|
|
147
|
-
color: var(--dk-label);
|
|
163
|
+
background-color: var(--dk-hover);
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
[data-dsh-docker-entry]:focus-visible {
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
/* 单行写:client-lint 的变量名扫描按行取名字,折行会把变量名截断成不存在的名字。 */
|
|
168
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
169
|
+
outline-offset: -2px;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
/*
|
|
173
|
+
* 图标:宿主宽态面板行是 16px(`sidebar.panel.icon` 的 size),折叠 rail 是 18px。
|
|
174
|
+
*
|
|
175
|
+
* 笔画按**宿主的图标口径**覆盖成 1:宿主面板图标是 sprite symbol(`viewBox 16` +
|
|
176
|
+
* `stroke-width="1"`),渲染 16px 时实际笔画 1.0px;`ICON_BOX` 是按 18px 设计的(写了 1.5),
|
|
177
|
+
* 直接塞进 16px 的行里会比邻居粗半档——2026-11-14 真机实测 1.5px vs 宿主 1.0px。
|
|
178
|
+
* 只覆盖入口内的实例,图标常量本身不动(面板头部那枚仍按自己的尺寸用)。
|
|
179
|
+
*/
|
|
155
180
|
.dk_entryIcon {
|
|
181
|
+
flex: none;
|
|
156
182
|
display: inline-flex;
|
|
157
183
|
align-items: center;
|
|
158
184
|
justify-content: center;
|
|
159
|
-
width:
|
|
160
|
-
height:
|
|
185
|
+
width: 16px;
|
|
186
|
+
height: 16px;
|
|
161
187
|
color: currentColor;
|
|
162
188
|
}
|
|
163
189
|
|
|
190
|
+
.dk_entryIcon svg {
|
|
191
|
+
width: 16px;
|
|
192
|
+
height: 16px;
|
|
193
|
+
stroke-width: 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
164
196
|
.dk_entryLabel {
|
|
165
197
|
overflow: hidden;
|
|
166
198
|
text-overflow: ellipsis;
|
|
167
199
|
white-space: nowrap;
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
/*
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
/* (D171)侧边栏折叠:只留图标,与 tty 的终端入口同形——否则标签会从窄栏里溢出一条竖条。
|
|
203
|
+
宿主的 `.collapsed .panelRow` 是 36×36、居中、无左右描边,跟齐。
|
|
204
|
+
|
|
205
|
+
**切 rail 的时机要跟宿主,不能跟「点了收起」**:外壳在点下收起的那一刻就给框架打
|
|
206
|
+
`data-sidebar-collapsed`,但布局要到 settle(150ms)之后才落到 rail(宽内容先固定在展开
|
|
207
|
+
宽度淡出)。跟着框架属性立刻切,收起的第一帧就只剩图标、还被居中推到冻结宽度的一半,
|
|
208
|
+
与同栏的「插件 / 插件市场 / 更多」不在同一拍上(用户报告的「kit 入口和官方行的效果不一样」)。
|
|
209
|
+
|
|
210
|
+
判据是宿主 README 留给集成方的稳定标记 `data-sidebar-scrollport`:**只在展开时**挂在滚动
|
|
211
|
+
视口上。于是 rail = 「框架已标收起」且「入口不在带该标记的视口里」——淡出阶段标记还在,
|
|
212
|
+
保持宽态;settle 后标记消失,与宿主同时切。老宿主没有该标记时 `:not()` 恒真,退回原行为。 */
|
|
213
|
+
[data-sidebar-collapsed] [data-dsh-docker-entry]:not([data-sidebar-scrollport] *) {
|
|
173
214
|
justify-content: center;
|
|
174
215
|
width: 100%;
|
|
175
|
-
|
|
176
|
-
height: 36px;
|
|
216
|
+
min-height: 36px;
|
|
177
217
|
padding: 0;
|
|
178
218
|
margin: 0;
|
|
219
|
+
border-inline: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
[data-sidebar-collapsed] [data-dsh-docker-entry]:not([data-sidebar-scrollport] *) .dk_entryIcon,
|
|
223
|
+
[data-sidebar-collapsed] [data-dsh-docker-entry]:not([data-sidebar-scrollport] *) .dk_entryIcon svg {
|
|
224
|
+
width: 18px;
|
|
225
|
+
height: 18px;
|
|
179
226
|
}
|
|
180
227
|
|
|
181
|
-
[data-sidebar-collapsed] .dk_entryLabel {
|
|
228
|
+
[data-sidebar-collapsed] [data-dsh-docker-entry]:not([data-sidebar-scrollport] *) .dk_entryLabel {
|
|
182
229
|
display: none;
|
|
183
230
|
}
|
|
184
231
|
|
|
185
|
-
[data-sidebar-collapsed] .dk_entryBadge {
|
|
232
|
+
[data-sidebar-collapsed] [data-dsh-docker-entry]:not([data-sidebar-scrollport] *) .dk_entryBadge {
|
|
186
233
|
margin-left: 0;
|
|
187
234
|
padding-left: 0;
|
|
188
235
|
}
|
package/client-src/index.js
CHANGED
|
@@ -1685,6 +1685,16 @@ function sessionHostOf(spec, bookName, liveTarget) {
|
|
|
1685
1685
|
|
|
1686
1686
|
const ICON_BOX =
|
|
1687
1687
|
'<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.8l5.4 3.1v6.2L8 14.2 2.6 11.1V4.9z"/><path d="M2.6 4.9L8 8l5.4-3.1"/><path d="M8 8v6.2"/></svg>'
|
|
1688
|
+
/**
|
|
1689
|
+
* 侧栏入口专用图标:按**宿主的 16 格**画(墨迹 2.5~13.5、笔画 1)。
|
|
1690
|
+
*
|
|
1691
|
+
* 为什么不复用 `ICON_BOX`:那枚按 18px 设计(笔画 1.5、墨迹只占 2.6~13.4),塞进 16px 的行里
|
|
1692
|
+
* 墨迹比宿主邻居小一圈——真机并排时像「左边距不对」(2026-11-14 实测)。面板头部与连接栏
|
|
1693
|
+
* 仍用 `ICON_BOX` / `ICON_BOX_SM`,它们在那儿有自己的尺寸。
|
|
1694
|
+
*/
|
|
1695
|
+
const ICON_BOX_ENTRY =
|
|
1696
|
+
'<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.5 14.3 5.2v5.6L8 14.5 1.7 10.8V5.2z"/><path d="M1.7 5.2 8 8.85l6.3-3.65"/><path d="M8 8.85v5.65"/></svg>'
|
|
1697
|
+
|
|
1688
1698
|
/** 连接栏按钮用的 13px 版本(与 tty 的 SFTP / 隧道按钮同尺寸)。 */
|
|
1689
1699
|
const ICON_BOX_SM =
|
|
1690
1700
|
'<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.8l5.4 3.1v6.2L8 14.2 2.6 11.1V4.9z"/><path d="M2.6 4.9L8 8l5.4-3.1"/><path d="M8 8v6.2"/></svg>'
|
|
@@ -10418,14 +10428,69 @@ window.__ModuleLoader__.load({
|
|
|
10418
10428
|
return undefined
|
|
10419
10429
|
}
|
|
10420
10430
|
|
|
10431
|
+
/**
|
|
10432
|
+
* kit 家族在宿主面板导航里的次序:小的在前。
|
|
10433
|
+
*
|
|
10434
|
+
* 四条入口由四个插件各挂各的、加载顺序不定,所以先后不能靠「谁先插」——按下表的值
|
|
10435
|
+
* 互相让位(见 `placeInPanelNav`):今日值得读 < 全局搜索 < 终端 < 容器。
|
|
10436
|
+
*/
|
|
10437
|
+
const KIT_SIDEBAR_RANK = 40
|
|
10438
|
+
const KIT_ENTRY = '[data-dsh-kit-sidebar-rank]'
|
|
10439
|
+
|
|
10440
|
+
/**
|
|
10441
|
+
* 宿主的面板导航:从宿主的**面板行**(`.panelRow`,类名带 hash 前缀,只能子串匹配)
|
|
10442
|
+
* 往上找它所在的 `nav`。
|
|
10443
|
+
*
|
|
10444
|
+
* 为什么不再挂顶层:入口原先插在侧栏 root 的直接子级(logo 行之后),于是四条入口顶在
|
|
10445
|
+
* 侧栏最上面、与宿主的「插件 / 插件市场 / …」分成两段。宿主把面板行渲染在侧栏滚动区里的
|
|
10446
|
+
* `nav` 中,**不是 root 的直接子元素**——只看 `root.children` 会找不到。
|
|
10447
|
+
*/
|
|
10448
|
+
function panelNavOf(rootNode, entry) {
|
|
10449
|
+
for (const row of rootNode.querySelectorAll('[class*="panelRow"]')) {
|
|
10450
|
+
if (!(row instanceof HTMLElement)) continue
|
|
10451
|
+
if (row === entry || row.contains(entry)) continue
|
|
10452
|
+
if (row.matches(KIT_ENTRY) || row.querySelector(KIT_ENTRY) !== null) continue
|
|
10453
|
+
const nav = row.closest('nav')
|
|
10454
|
+
if (nav !== null && nav.contains(row)) return nav
|
|
10455
|
+
}
|
|
10456
|
+
return undefined
|
|
10457
|
+
}
|
|
10458
|
+
|
|
10459
|
+
/**
|
|
10460
|
+
* 插进宿主面板导航:紧跟固定面板「插件」(宿主 `FIXED_PANEL_ID='plugins'`,order 0、
|
|
10461
|
+
* 永不折叠,所以它总是第一行)。同族按 rank 让位,加载顺序变了也不会颠倒;只在**位置
|
|
10462
|
+
* 不对**时才动 DOM(宿主重渲染后由 MutationObserver 叫回来,位置对就不做事,免得自激)。
|
|
10463
|
+
*/
|
|
10464
|
+
function placeInPanelNav(rootNode, entry) {
|
|
10465
|
+
const nav = panelNavOf(rootNode, entry)
|
|
10466
|
+
if (nav === undefined) return false
|
|
10467
|
+
const row = nav.querySelector('[class*="panelRow"]')
|
|
10468
|
+
if (row === null) return false
|
|
10469
|
+
const wrapper = row.closest('[class*="panelEntry"]')
|
|
10470
|
+
const anchorRow = wrapper !== null && wrapper.parentElement === nav ? wrapper : row
|
|
10471
|
+
if (anchorRow.parentElement !== nav) return false
|
|
10472
|
+
let after = anchorRow
|
|
10473
|
+
for (const node of nav.children) {
|
|
10474
|
+
if (!(node instanceof HTMLElement) || node === entry) continue
|
|
10475
|
+
if (!node.matches(KIT_ENTRY)) continue
|
|
10476
|
+
const rank = Number(node.getAttribute('data-dsh-kit-sidebar-rank'))
|
|
10477
|
+
if (Number.isFinite(rank) && rank < KIT_SIDEBAR_RANK) after = node
|
|
10478
|
+
}
|
|
10479
|
+
if (entry.parentElement !== nav || entry.previousElementSibling !== after) {
|
|
10480
|
+
nav.insertBefore(entry, after.nextSibling)
|
|
10481
|
+
}
|
|
10482
|
+
return true
|
|
10483
|
+
}
|
|
10484
|
+
|
|
10421
10485
|
function createSidebarEntry() {
|
|
10422
10486
|
const entry = document.createElement('div')
|
|
10423
10487
|
entry.dataset.dshDockerEntry = ''
|
|
10488
|
+
entry.dataset.dshKitSidebarRank = String(KIT_SIDEBAR_RANK)
|
|
10424
10489
|
entry.className = 'dk_sidebarEntry'
|
|
10425
10490
|
entry.setAttribute('role', 'button')
|
|
10426
10491
|
entry.setAttribute('aria-label', t('panel.containers'))
|
|
10427
10492
|
entry.innerHTML =
|
|
10428
|
-
'<span class="dk_entryIcon">' +
|
|
10493
|
+
'<span class="dk_entryIcon">' + ICON_BOX_ENTRY + '</span>' +
|
|
10429
10494
|
'<span class="dk_entryLabel">' + t('panel.containers') + '</span>'
|
|
10430
10495
|
entry.addEventListener('click', (event) => {
|
|
10431
10496
|
event.preventDefault()
|
|
@@ -10435,6 +10500,8 @@ window.__ModuleLoader__.load({
|
|
|
10435
10500
|
}
|
|
10436
10501
|
|
|
10437
10502
|
function placeSidebarEntry(rootNode, entry) {
|
|
10503
|
+
if (placeInPanelNav(rootNode, entry)) return true
|
|
10504
|
+
// 宿主没有面板导航(老版本 / 版式换过一轮):退回顶层紧接 logo 行,入口不消失。
|
|
10438
10505
|
const button = newSessionButton(rootNode)
|
|
10439
10506
|
if (button === undefined) return false
|
|
10440
10507
|
if (entry.parentElement !== rootNode) {
|
|
@@ -10487,7 +10554,10 @@ window.__ModuleLoader__.load({
|
|
|
10487
10554
|
tryPlace()
|
|
10488
10555
|
return
|
|
10489
10556
|
}
|
|
10490
|
-
|
|
10557
|
+
// 位置也要复核:入口现在住在宿主的面板 `nav` 里,宿主重渲染可能把它挤到别处
|
|
10558
|
+
// (`rootNode.contains` 仍为真,只看「还在不在」会漏掉被挪走那一档)。
|
|
10559
|
+
// `placeSidebarEntry` 只在位置不对时动 DOM,所以这里不会自己触发自己。
|
|
10560
|
+
placed = placeSidebarEntry(rootNode, entry)
|
|
10491
10561
|
})
|
|
10492
10562
|
|
|
10493
10563
|
tryPlace()
|