dsh-vaultwarden 0.9.2 → 0.9.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to `dsh-vaultwarden` are documented here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.9.3] — 2026-10-21
8
+
9
+ ### 修复
10
+
11
+ - 修复首次加载后侧栏一直停在占位块、下方留一大片空白的问题。根因是「读取资料夹」与「读取侧栏统计」被放在同一个 `Promise.all` 里:后者决定侧栏画什么,前者只用于「移动到文件夹」,但前者一旦挂住,就会把已经返回的统计一起扣住,侧栏因此永远停在"加载中",而列表早已渲染出全部条目。
12
+ - 两次读取改为各自结算:各带自己的超时,谁先到谁先渲染,互不等待。资料夹读取超时 5 秒,侧栏统计 6 秒(且整条重试链共用一个上限,不是每次尝试各算一遍)。
13
+ - 统计读取失败时侧栏落到三个结构项并退出忙碌态,不再保留永远不会兑现的占位块。
14
+ - 占位骨架条的颜色由背景层色改为宿主边框色:原先在白底面板上几乎不可见,一片空白看起来像空盒子而不是"加载中"。
15
+
7
16
  ## [0.9.2] — 2026-10-21
8
17
 
9
18
  ### 变更
package/lib/client.js CHANGED
@@ -396,7 +396,7 @@ const nextOffsetOf = (payload, fallback) => {
396
396
  * `scripts/build.sh` stamps both constants from package.json on every
397
397
  * build, so this fallback is never hand-maintained and cannot drift.
398
398
  */
399
- const BUILD_VERSION = '0.9.2'
399
+ const BUILD_VERSION = '0.9.3'
400
400
  const BUILD_REPOSITORY = 'https://github.com/BuvkB/dsh-vaultwarden'
401
401
  /**
402
402
  * Turn a package.json repository url into something a browser can open.
@@ -1709,7 +1709,13 @@ const nextOffsetOf = (payload, fallback) => {
1709
1709
  * The pulse is in the stylesheet, so a reader who asked for reduced motion
1710
1710
  * gets a static bar rather than a blinking one.
1711
1711
  */
1712
- skeletonBar: { height: 10, borderRadius: 999, background: TOKEN.bg2, flex: 'none' },
1712
+ // The host's border colour, not `bg2`. `bg2` is a layer tint (#f5f5f5 on
1713
+ // a white surface), and a bar painted in it is invisible on the panel's
1714
+ // own background: the rail then reads as an empty box rather than as
1715
+ // "still loading". The border token is by definition visible against the
1716
+ // surface it borders, in both themes, and no browser-support question
1717
+ // attaches to it the way it would to a colour mix.
1718
+ skeletonBar: { height: 10, borderRadius: 999, background: TOKEN.border, flex: 'none' },
1713
1719
  /** A skeleton line: bar + optional label column, one per field row. */
1714
1720
  skeletonField: { display: 'grid', gridTemplateColumns: '92px minmax(0, 1fr) 28px', gap: 12, alignItems: 'center' },
1715
1721
  /** The list's own placeholder row: avatar tile + two bars + the chevron slot. */
@@ -2732,6 +2738,34 @@ const nextOffsetOf = (payload, fallback) => {
2732
2738
  return /load failed|failed to fetch|networkerror|network error|fetch failed/i.test(text)
2733
2739
  }
2734
2740
 
2741
+ /**
2742
+ * Put a ceiling on one read.
2743
+ *
2744
+ * A promise that never settles is worse than one that rejects: `await`
2745
+ * simply waits, so a request the host never answers — a request dropped by a
2746
+ * captive portal, a socket the phone's radio parked until the tab wakes —
2747
+ * leaves the sidebar shimmering with no rows and no error, which is the
2748
+ * one failure mode the reader cannot act on. The timeout turns that into an
2749
+ * ordinary rejection so the caller's existing fallback runs.
2750
+ *
2751
+ * Silent by construction: the timeout rejects with the same shape a failed
2752
+ * call would (no host code), so callers cannot tell a slow host from a
2753
+ * dropped one, and neither can the reader — who is told the counts are
2754
+ * unknown, not which of a dozen network faults produced that.
2755
+ */
2756
+ const withTimeout = (promise, ms, label) =>
2757
+ new Promise((resolve, reject) => {
2758
+ const timer = setTimeout(() => {
2759
+ const error = new Error(`${label} 超时`)
2760
+ error.code = 'timeout'
2761
+ reject(error)
2762
+ }, ms)
2763
+ promise.then(
2764
+ (value) => { clearTimeout(timer); resolve(value) },
2765
+ (failure) => { clearTimeout(timer); reject(failure) },
2766
+ )
2767
+ })
2768
+
2735
2769
  /** Build the RPC caller over the authenticated `/api` channel. */
2736
2770
  const makeInvoke = (connection) => async (method, args = {}) => {
2737
2771
  if (!connection || typeof connection.rpc?.call !== 'function') {
@@ -2781,6 +2815,30 @@ const nextOffsetOf = (payload, fallback) => {
2781
2815
  * meantime, which is a worse bug than the one this fixes.
2782
2816
  */
2783
2817
  const CLIPBOARD_TTL_MS = 20000
2818
+ /**
2819
+ * Ceiling on the sidebar's overview read, in milliseconds.
2820
+ *
2821
+ * A read that never settles is worse than one that rejects: `await` simply
2822
+ * waits, so a request the host never answers — dropped by a captive portal,
2823
+ * or a socket the phone's radio parked until the tab wakes — would leave the
2824
+ * sidebar shimmering with no rows and no error, which is the one failure
2825
+ * mode the reader cannot act on. The timeout turns that into an ordinary
2826
+ * rejection so the caller's existing fallback runs.
2827
+ *
2828
+ * Long enough that a merely slow network still gets its counts, short enough
2829
+ * that the rail settles into its structural places when the answer is simply
2830
+ * not coming: the sidebar is furniture, and a count that never arrives must
2831
+ * not hold the shape of the panel hostage.
2832
+ */
2833
+ const OVERVIEW_TIMEOUT_MS = 6000
2834
+ /**
2835
+ * The same ceiling for the other read that describes a place rather than a
2836
+ * row. Shorter than the overview's, because nothing waits on it: the folder
2837
+ * list only feeds the move-to-folder step, so a slow answer is better
2838
+ * dropped than kept open — but it still must not stay open forever, which
2839
+ * is exactly how the sidebar used to get stuck.
2840
+ */
2841
+ const READ_TIMEOUT_MS = 5000
2784
2842
  /**
2785
2843
  * Write `text` to the clipboard and schedule its removal.
2786
2844
  *
@@ -5075,15 +5133,52 @@ const nextOffsetOf = (payload, fallback) => {
5075
5133
  // either method, or a read that fails, leaves the sidebar on the one
5076
5134
  // place that needs no counts at all.
5077
5135
  await readVault(nextQuery, { quiet: Boolean(paintedSection ?? painted), identity, section: view })
5078
- const [folderPayload, overviewPayload] = await Promise.all([
5079
- invoke('folders').catch(() => null),
5080
- invoke('overview').catch((failure) => ({ failed: failure })),
5081
- ])
5082
- setFolders(Array.isArray(folderPayload?.folders) ? folderPayload.folders : [])
5083
- if (overviewPayload && !overviewPayload.failed) {
5084
- openCache.overview = { key: identity, overview: overviewPayload, at: Date.now() }
5085
- setOverview(overviewPayload)
5086
- }
5136
+ // These two reads are settled INDEPENDENTLY, not awaited together.
5137
+ //
5138
+ // They used to sit in one Promise.all, which made the slower read the
5139
+ // gate for the faster one: the rail is drawn from `overview`, and
5140
+ // `folders` only feeds the move-to-folder step, but a folders call
5141
+ // that never settled held the already-arrived overview hostage. The
5142
+ // panel then showed its rows (readVault had finished) under a rail
5143
+ // stuck on its ghost block, forever — the list said 428 entries and
5144
+ // the sidebar still said "loading". That is the reported bug, and no
5145
+ // amount of retrying or timing out the overview can fix it, because
5146
+ // the overview had already succeeded.
5147
+ //
5148
+ // So each read gets its own ceiling and its own landing. Whatever
5149
+ // arrives is applied at once; whatever does not is dropped on the
5150
+ // floor by its own timer. Nothing here waits for anything else.
5151
+ const foldersRead = withTimeout(invoke('folders'), READ_TIMEOUT_MS, 'folders')
5152
+ .then((payload) => setFolders(Array.isArray(payload?.folders) ? payload.folders : []))
5153
+ .catch(() => { /* an older host, or a read that failed: keep the folders on screen */ })
5154
+ // Retry once: a single transient failure on a slow network used to
5155
+ // leave the rail shimmering with no rows and no way to get them — the
5156
+ // panel looked broken rather than busy. One quiet retry covers the
5157
+ // dropped first attempt without doubling the cost of a host that
5158
+ // genuinely lacks the method.
5159
+ //
5160
+ // ONE ceiling over the chain, not one per attempt: bounding each try
5161
+ // separately makes the worst case the sum of them (two six-second
5162
+ // waits), and the sum is exactly the case this exists for — a request
5163
+ // that never answers, where the retry cannot help because the first
5164
+ // one is still open. A single timer covers both attempts and the gap
5165
+ // between them.
5166
+ const overviewRead = withTimeout(invoke('overview').catch(() => invoke('overview')), OVERVIEW_TIMEOUT_MS, 'overview')
5167
+ .then((payload) => {
5168
+ openCache.overview = { key: identity, overview: payload, at: Date.now() }
5169
+ setOverview(payload)
5170
+ })
5171
+ .catch(() => {
5172
+ // Nothing arrived: stop pretending the rail is loading. An empty
5173
+ // overview renders the three structural places and lets the nav
5174
+ // fall out of its busy state, which is honest — the counts are
5175
+ // unknown, but the rail is not stuck. A rail that kept claiming
5176
+ // to be busy while the list underneath was already full is the
5177
+ // shape of a hung panel, which is the one thing this file spends
5178
+ // its length trying to avoid.
5179
+ setOverview({ failed: true, sections: [], types: [], folderList: [], folderCount: 0, unfiled: 0 })
5180
+ })
5181
+ await Promise.all([foldersRead, overviewRead])
5087
5182
  },
5088
5183
  [invoke, readVault],
5089
5184
  )
@@ -6109,6 +6204,14 @@ const nextOffsetOf = (payload, fallback) => {
6109
6204
  navPlaceholders(t),
6110
6205
  )
6111
6206
  }
6207
+ // The read failed and no retry is coming: the rail keeps its three
6208
+ // structural places and stops claiming to be busy. Ghosts here would be
6209
+ // a placeholder for something that is never going to arrive, which is
6210
+ // the shape of a hung panel — the one thing this file spends its length
6211
+ // trying to avoid.
6212
+ if (overview.failed) {
6213
+ return h('nav', { 'data-vw-nav': '', style: S.nav, 'aria-label': t('navSections') }, places)
6214
+ }
6112
6215
  const types = overview.types ?? []
6113
6216
  if (types.length) {
6114
6217
  places.push(h('div', { key: 'g-types', 'data-vw-nav-group': 'types', style: S.navGroup }, `${t('sectionTypes')} (${types.length})`))
@@ -6145,7 +6248,7 @@ const nextOffsetOf = (payload, fallback) => {
6145
6248
  const refreshOverview = async () => {
6146
6249
  const identity = cacheKey(openCache.config?.config)
6147
6250
  try {
6148
- const payload = await invoke('overview')
6251
+ const payload = await withTimeout(invoke('overview'), OVERVIEW_TIMEOUT_MS, 'overview')
6149
6252
  openCache.overview = { key: identity, overview: payload, at: Date.now() }
6150
6253
  setOverview(payload)
6151
6254
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vaultwarden",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Vaultwarden/Bitwarden real-time sync for DeepSeek Harness: WebSocket push notifications, on-disk vault cache with revision probing, read/write tools and an entry browser panel. Independent implementation; see THIRD-PARTY-NOTICES.md.",
5
5
  "repository": {
6
6
  "type": "git",