@hyzyn/dsh-docker 0.16.1 → 0.17.0

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 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 sidebar; 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
- 侧边栏出现「容器」入口;「Docker 容器面板」卡片维护目标与开关,**保存即热生效**(`settings/updated`
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: <同源>` 或
@@ -120,47 +120,79 @@
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(--dk-gap-md);
143
+ gap: var(--dsh-sidebar-label-gap, 6px);
131
144
  width: 100%;
132
145
  min-width: 0;
133
- height: var(--dk-h-lg);
134
- padding: 0 var(--dk-gap-lg);
135
- border-radius: var(--dk-r-md);
136
- color: var(--dk-label-2);
137
- font-size: 13px;
138
- line-height: 20px;
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 var(--dk-dur) var(--dk-ease), color var(--dk-dur) var(--dk-ease);
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
- outline: none;
152
- box-shadow: var(--dk-ring);
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: 18px;
160
- height: 18px;
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;
@@ -168,14 +200,21 @@
168
200
  }
169
201
 
170
202
  /* 侧边栏折叠(DSH 外壳在祖先元素上打 data-sidebar-collapsed):只留图标,
171
- 与 tty 的终端入口同形——否则标签会从窄栏里溢出一条竖条 */
203
+ 与 tty 的终端入口同形——否则标签会从窄栏里溢出一条竖条。
204
+ 宿主的 `.collapsed .panelRow` 是 36×36、居中、无左右描边,跟齐。 */
172
205
  [data-sidebar-collapsed] [data-dsh-docker-entry] {
173
206
  justify-content: center;
174
207
  width: 100%;
175
- /* 折叠 rail 里宿主的行是 36×36,跟齐(与 tty 入口一致) */
176
- height: 36px;
208
+ min-height: 36px;
177
209
  padding: 0;
178
210
  margin: 0;
211
+ border-inline: 0;
212
+ }
213
+
214
+ [data-sidebar-collapsed] .dk_entryIcon,
215
+ [data-sidebar-collapsed] .dk_entryIcon svg {
216
+ width: 18px;
217
+ height: 18px;
179
218
  }
180
219
 
181
220
  [data-sidebar-collapsed] .dk_entryLabel {
@@ -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">' + ICON_BOX + '</span>' +
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
- if (!rootNode.contains(entry)) placed = placeSidebarEntry(rootNode, entry)
10557
+ // 位置也要复核:入口现在住在宿主的面板 `nav` 里,宿主重渲染可能把它挤到别处
10558
+ // (`rootNode.contains` 仍为真,只看「还在不在」会漏掉被挪走那一档)。
10559
+ // `placeSidebarEntry` 只在位置不对时动 DOM,所以这里不会自己触发自己。
10560
+ placed = placeSidebarEntry(rootNode, entry)
10491
10561
  })
10492
10562
 
10493
10563
  tryPlace()