dsh-qoder-connect 0.2.1 → 0.2.3-alpha.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/lib/client.js CHANGED
@@ -158,11 +158,11 @@ window.__ModuleLoader__.load({
158
158
  global: void 0
159
159
  };
160
160
  /**
161
- * Publish one variant's freshly fetched status document. Downstream
162
- * subscribers (both sidebar cards and the dashboard, through whichever
163
- * observable wraps this store) re-render on the revision bump.
161
+ * Store one variant's document and sign-in fact — the raw publish, with no
162
+ * sequence bookkeeping. Direct callers go through {@link noteQuotaStatus};
163
+ * read results go through {@link publishRead}.
164
164
  */
165
- function noteQuotaStatus(variantId, status) {
165
+ function storeStatus(variantId, status) {
166
166
  if (variantId === "qoder" && statusDocuments.cn !== status) {
167
167
  statusDocuments.cn = status;
168
168
  statusFetchedAt.cn = Date.now();
@@ -174,6 +174,16 @@ window.__ModuleLoader__.load({
174
174
  }
175
175
  noteQuotaSignIn(variantId, status.status === "signed-in");
176
176
  }
177
+ /**
178
+ * Publish one variant's document as an AUTHORITATIVE fact: the host just
179
+ * answered a write, or a surface synthesized the outcome (clearing a PAT).
180
+ * It supersedes every read that started before it, so a response predating
181
+ * the write cannot roll the screen back.
182
+ */
183
+ function noteQuotaStatus(variantId, status) {
184
+ storeStatus(variantId, status);
185
+ if (variantId === "qoder" || variantId === "qoder-global") publishedSeq[variantId] = readSeq[variantId];
186
+ }
177
187
  /** Read one variant's latest status document. */
178
188
  function quotaStatus(variantId) {
179
189
  return variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
@@ -207,6 +217,186 @@ window.__ModuleLoader__.load({
207
217
  if (fetchedAt === void 0 || document === void 0) return false;
208
218
  return Date.now() - fetchedAt < maxAgeMs;
209
219
  }
220
+ /** The last read failure per variant, cleared by the next successful read. */
221
+ const readFailures = {
222
+ qoder: void 0,
223
+ "qoder-global": void 0
224
+ };
225
+ /** Read one variant's last read failure, if the last read failed. */
226
+ function quotaReadFailure(variantId) {
227
+ return variantId === "qoder" ? readFailures.qoder : readFailures["qoder-global"];
228
+ }
229
+ /**
230
+ * Cadence an expanded card reads at — one request per variant per minute,
231
+ * whatever the number of cards showing it.
232
+ */
233
+ const CARD_POLL_MS = 6e4;
234
+ /** The status route each variant polls (mirrors the host's locked route table). */
235
+ const STATUS_ROUTES = {
236
+ qoder: QODER_STATUS_PATH,
237
+ "qoder-global": QODER_GLOBAL_STATUS_PATH
238
+ };
239
+ /**
240
+ * Who needs a variant live. A `card` holder (an expanded settings card) reads
241
+ * every minute; a `surface` holder (an enabled sidebar card, an open quota
242
+ * panel) reads at the settings interval. A variant with any holder is read at
243
+ * the TIGHTEST cadence its holders ask for — an expanded card beside an
244
+ * enabled sidebar card costs ONE request a minute between them, not two.
245
+ */
246
+ const demand = {
247
+ qoder: {
248
+ cards: 0,
249
+ surfaces: 0
250
+ },
251
+ "qoder-global": {
252
+ cards: 0,
253
+ surfaces: 0
254
+ }
255
+ };
256
+ /** The single ticker every holder shares; undefined while nothing is live. */
257
+ let ticker;
258
+ /**
259
+ * Read-start sequences per variant, and the sequence of the last PUBLISHED
260
+ * read. A read publishes only when no later-started read has published yet,
261
+ * so a response that predates a newer one can never roll the screen back —
262
+ * the guard the per-card `readSeq` used to provide, now shared across every
263
+ * surface of the variant.
264
+ */
265
+ const readSeq = {
266
+ qoder: 0,
267
+ "qoder-global": 0
268
+ };
269
+ const publishedSeq = {
270
+ qoder: 0,
271
+ "qoder-global": 0
272
+ };
273
+ /** Whether the page is visible; a hidden page reads nothing and catches up on return. */
274
+ function pageHidden() {
275
+ return typeof document !== "undefined" && document.hidden === true;
276
+ }
277
+ /**
278
+ * Publish one read's outcome under the sequence guard: a read publishes only
279
+ * when no read that started later has published yet, so a response that
280
+ * predates a newer one — whichever surface started it — can never roll the
281
+ * screen back.
282
+ */
283
+ function publishRead(variantId, seq, outcome) {
284
+ if (seq <= publishedSeq[variantId]) return;
285
+ if ("doc" in outcome) {
286
+ storeStatus(variantId, outcome.doc);
287
+ publishReadFailure(variantId, void 0);
288
+ } else publishReadFailure(variantId, outcome.failure);
289
+ publishedSeq[variantId] = seq;
290
+ }
291
+ /** Publish (or clear) one variant's read failure; identity-stable between changes. */
292
+ function publishReadFailure(variantId, failure) {
293
+ const previous = readFailures[variantId];
294
+ if (previous === failure) return;
295
+ if (previous !== void 0 && failure !== void 0 && previous.kind === failure.kind && (previous.kind !== "transport" || failure.kind !== "transport" || previous.message === failure.message)) return;
296
+ readFailures[variantId] = failure;
297
+ bump();
298
+ }
299
+ /** A 200 whose body is not a status document — reported as `invalid`, not as a transport failure. */
300
+ var InvalidStatusDocumentError = class extends Error {};
301
+ /**
302
+ * Fetch one variant's status document and publish it (or its failure) for
303
+ * every surface. The request is never shared with another read: a tick reads
304
+ * each live variant once, and an explicit read must be able to postdate a
305
+ * write, so it starts its own request.
306
+ *
307
+ * @returns the published document, or undefined when the read failed.
308
+ */
309
+ async function readVariant(variantId) {
310
+ const seq = ++readSeq[variantId];
311
+ try {
312
+ const response = await fetch(STATUS_ROUTES[variantId], {
313
+ headers: { accept: "application/json" },
314
+ credentials: "same-origin"
315
+ });
316
+ const body = await response.json().catch(() => void 0);
317
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
318
+ if (!isQoderWebStatus(body)) throw new InvalidStatusDocumentError();
319
+ publishRead(variantId, seq, { doc: body });
320
+ return body;
321
+ } catch (error) {
322
+ publishRead(variantId, seq, { failure: error instanceof InvalidStatusDocumentError ? { kind: "invalid" } : {
323
+ kind: "transport",
324
+ message: error instanceof Error ? error.message : "request failed"
325
+ } });
326
+ return;
327
+ }
328
+ }
329
+ /** Whether a variant is due for its next read: held at all, past its cadence, and not parked signed-out. */
330
+ function dueForRead(variantId) {
331
+ const holders = demand[variantId];
332
+ if (holders.cards === 0 && holders.surfaces === 0) return false;
333
+ if (holders.cards > 0 && holders.surfaces === 0 && quotaStatus(variantId)?.status === "signed-out") return false;
334
+ if (holders.cards > 0) return true;
335
+ return !quotaStatusIsFresh(variantId, Math.max(CARD_POLL_MS, pollIntervalMs));
336
+ }
337
+ /** One tick: read every variant that is due, once, whatever the number of holders. */
338
+ function tickQuotaPoll() {
339
+ if (pageHidden()) return;
340
+ for (const variantId of ["qoder", "qoder-global"]) if (dueForRead(variantId)) readVariant(variantId);
341
+ }
342
+ function startTicker() {
343
+ if (ticker !== void 0) return;
344
+ ticker = window.setInterval(tickQuotaPoll, CARD_POLL_MS);
345
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", tickQuotaPoll);
346
+ }
347
+ function stopTicker() {
348
+ if (ticker === void 0) return;
349
+ window.clearInterval(ticker);
350
+ ticker = void 0;
351
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", tickQuotaPoll);
352
+ }
353
+ /** Record one holder of `kind` and start the shared ticker when it is the first. */
354
+ function addHolder(variantId, kind) {
355
+ const holders = demand[variantId];
356
+ holders[kind] += 1;
357
+ if (holders.cards + holders.surfaces === 1) startTicker();
358
+ }
359
+ /** Drop one holder and stop the shared ticker when nothing is live any more. */
360
+ function removeHolder(variantId, kind) {
361
+ const holders = demand[variantId];
362
+ holders[kind] = Math.max(0, holders[kind] - 1);
363
+ if (demand.qoder.cards + demand.qoder.surfaces === 0 && demand["qoder-global"].cards + demand["qoder-global"].surfaces === 0) stopTicker();
364
+ }
365
+ /** Sync the holder count with the ticker, reading at once when the document is due. */
366
+ function acquire(variantId, kind) {
367
+ const readNow = !quotaStatusIsFresh(variantId, kind === "cards" ? CARD_POLL_MS : Math.max(CARD_POLL_MS, pollIntervalMs));
368
+ addHolder(variantId, kind);
369
+ if (readNow) readVariant(variantId);
370
+ return () => {
371
+ removeHolder(variantId, kind);
372
+ };
373
+ }
374
+ /**
375
+ * Acquire one variant as an EXPANDED CARD shows it: reads every minute while
376
+ * any card holds it, stops on a signed-out document (nothing changes without
377
+ * a PAT). The release disposer belongs to the card's effect lifetime.
378
+ */
379
+ function acquireLiveCard(variantId) {
380
+ return acquire(variantId, "cards");
381
+ }
382
+ /**
383
+ * Acquire one variant as a CADENCE-BOUND SURFACE shows it (an enabled sidebar
384
+ * card, an open quota panel): reads at the settings interval, whatever the
385
+ * sign-in state. The release disposer belongs to the surface's lifetime.
386
+ */
387
+ function acquireLiveSurface(variantId) {
388
+ return acquire(variantId, "surfaces");
389
+ }
390
+ /**
391
+ * Force one shared read now. The explicit path — a manual refresh, or the
392
+ * re-read that follows a write — always starts its own request, so its answer
393
+ * can postdate the write that preceded it.
394
+ *
395
+ * @returns whether the read produced a document.
396
+ */
397
+ function refreshQuotaStatus(variantId) {
398
+ return readVariant(variantId).then((doc) => doc !== void 0);
399
+ }
210
400
  //#endregion
211
401
  //#region src/client/QuotaSettingsCard.tsx
212
402
  /**
@@ -706,7 +896,6 @@ window.__ModuleLoader__.load({
706
896
  };
707
897
  /** Both cards, in display order (China first). */
708
898
  const QODER_CARD_VARIANTS = [QODER_CN_CARD, QODER_GLOBAL_CARD];
709
- const POLL_INTERVAL_MS = 6e4;
710
899
  const cardStyle = {
711
900
  listStyle: "none",
712
901
  borderWidth: "0.5px",
@@ -1584,18 +1773,15 @@ window.__ModuleLoader__.load({
1584
1773
  }
1585
1774
  /** Render Qoder PAT state, quota, catalog, and context capacities as one expandable card. */
1586
1775
  function QoderPluginCard(props) {
1587
- const { t, scope, signedIn, variant, unified } = props;
1776
+ const { t, scope, signedIn, variant, unified, defaultOpen } = props;
1588
1777
  if (t === void 0) throw new Error("Qoder plugin card requires its translation function");
1589
1778
  const isUnified = unified === true;
1590
1779
  const liveSignIn = (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSignInState);
1591
1780
  const [activeVariantId, setActiveVariantId] = (0, react.useState)("qoder");
1592
1781
  const currentVariant = isUnified ? activeVariantId === "qoder" ? QODER_CN_CARD : QODER_GLOBAL_CARD : variant ?? QODER_CN_CARD;
1593
- const [open, setOpen] = (0, react.useState)(false);
1782
+ const [open, setOpen] = (0, react.useState)(defaultOpen === true);
1594
1783
  const [hovered, setHovered] = (0, react.useState)(false);
1595
1784
  const [headerFocused, setHeaderFocused] = (0, react.useState)(false);
1596
- const [status, setStatus] = (0, react.useState)();
1597
- const [signedInState, setSignedInState] = (0, react.useState)();
1598
- const [readFailure, setReadFailure] = (0, react.useState)();
1599
1785
  const [busy, setBusy] = (0, react.useState)(false);
1600
1786
  const [patDraft, setPatDraft] = (0, react.useState)("");
1601
1787
  const [replacing, setReplacing] = (0, react.useState)(false);
@@ -1607,8 +1793,9 @@ window.__ModuleLoader__.load({
1607
1793
  const [checkingIn, setCheckingIn] = (0, react.useState)(false);
1608
1794
  const [clearingLogs, setClearingLogs] = (0, react.useState)(false);
1609
1795
  const [checkInNotice, setCheckInNotice] = (0, react.useState)();
1796
+ /** A write action's failure, reported on the same line as a read failure. */
1797
+ const [writeFailure, setWriteFailure] = (0, react.useState)();
1610
1798
  const mounted = (0, react.useRef)(true);
1611
- const readSeq = (0, react.useRef)(0);
1612
1799
  const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
1613
1800
  (0, react.useEffect)(() => {
1614
1801
  mounted.current = true;
@@ -1623,83 +1810,70 @@ window.__ModuleLoader__.load({
1623
1810
  manualControllers.current.add(controller);
1624
1811
  return controller;
1625
1812
  }, []);
1813
+ /**
1814
+ * The shown variant's shared read state.
1815
+ *
1816
+ * The document and the last read failure arrive through ONE revision bump,
1817
+ * whichever surface read them: the shared poller reads each live variant
1818
+ * once for every surface that shows it, so this card expanded here and the
1819
+ * same card in the plugin-manager panel cost one request a minute between
1820
+ * them, not two. Before the first document lands a failed read shows as an
1821
+ * error document; beside a document already on screen it shows as a notice
1822
+ * under the account row, exactly as the per-card fetch did.
1823
+ */
1824
+ (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSettingsRevision);
1825
+ const readFailure = quotaReadFailure(currentVariant.id);
1826
+ const sharedDocument = quotaStatus(currentVariant.id);
1827
+ const status = readFailure !== void 0 && sharedDocument === void 0 ? {
1828
+ status: "error",
1829
+ message: readFailure.kind === "invalid" ? t("statusResponseInvalid") : readFailure.message
1830
+ } : sharedDocument;
1831
+ const signedInState = status?.status === "signed-in" ? true : status?.status === "signed-out" ? false : void 0;
1626
1832
  const authKey = status === void 0 || status.status === "error" ? void 0 : status.authKey;
1627
- const refresh = (0, react.useCallback)(async (signal) => {
1628
- const seq = ++readSeq.current;
1629
- const current = () => mounted.current && signal?.aborted !== true && seq === readSeq.current;
1630
- try {
1631
- const response = await fetch(currentVariant.statusPath, {
1632
- headers: { accept: "application/json" },
1633
- credentials: "same-origin",
1634
- ...signal === void 0 ? {} : { signal }
1635
- });
1636
- const value = await response.json().catch(() => void 0);
1637
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1638
- if (!isQoderWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1639
- if (!current()) return false;
1640
- setStatus(value);
1641
- if (value.status === "signed-in") {
1642
- setSignedInState(true);
1643
- noteQuotaStatus(currentVariant.id, value);
1644
- } else if (value.status === "signed-out") {
1645
- setSignedInState(false);
1646
- noteQuotaStatus(currentVariant.id, value);
1647
- }
1648
- setReadFailure(void 0);
1649
- return true;
1650
- } catch (error) {
1651
- const message = error instanceof Error ? error.message : t("requestFailed");
1652
- if (current()) {
1653
- setReadFailure(message);
1654
- setStatus((previous) => previous === void 0 ? {
1655
- status: "error",
1656
- message
1657
- } : previous);
1658
- }
1659
- return false;
1660
- }
1661
- }, [currentVariant.statusPath, t]);
1833
+ /**
1834
+ * Force one shared read of the shown variant.
1835
+ *
1836
+ * The store's read always starts its own request — an explicit read must be
1837
+ * able to postdate the write or the click that precedes it — and publishes
1838
+ * for every surface of the variant, so a manual refresh here updates the
1839
+ * sidebar cards and the quota panel too. A successful read also clears a
1840
+ * write action's notice, exactly as the per-card fetch did.
1841
+ */
1842
+ const refresh = (0, react.useCallback)(async () => {
1843
+ const ok = await refreshQuotaStatus(currentVariant.id);
1844
+ if (ok) setWriteFailure(void 0);
1845
+ return ok;
1846
+ }, [currentVariant.id]);
1847
+ /**
1848
+ * The expanded card is a poll holder of the shown variant.
1849
+ *
1850
+ * The shared ticker reads the variant once a minute while any card holds it
1851
+ * and parks it on a signed-out document (nothing changes without a PAT);
1852
+ * collapsing or unmounting releases the demand. Opening re-reads at once
1853
+ * only when the shared document is past its minute — a second card opening
1854
+ * beside a live one shows the numbers already on screen.
1855
+ */
1856
+ (0, react.useEffect)(() => {
1857
+ if (!open) return void 0;
1858
+ return acquireLiveCard(currentVariant.id);
1859
+ }, [open, currentVariant.id]);
1860
+ /**
1861
+ * Opening (or switching variant) clears the transient entry state — a
1862
+ * discarded draft must not wait for the user on the next open. The document
1863
+ * itself is shared state now and stays on screen while the card re-opens.
1864
+ */
1662
1865
  (0, react.useEffect)(() => {
1663
1866
  if (!open) return;
1664
- setStatus(void 0);
1665
- setSignedInState(void 0);
1666
- setReadFailure(void 0);
1667
1867
  setPatDraft("");
1668
1868
  setReplacing(false);
1669
1869
  setPatError(void 0);
1670
1870
  setPatNotice(void 0);
1671
- const controller = new AbortController();
1672
- refresh(controller.signal);
1673
- return () => {
1674
- controller.abort();
1675
- };
1676
- }, [
1677
- open,
1678
- currentVariant.statusPath,
1679
- refresh
1680
- ]);
1681
- (0, react.useEffect)(() => {
1682
- if (!open || signedInState === false) return;
1683
- const controller = new AbortController();
1684
- const timer = window.setInterval(() => {
1685
- refresh(controller.signal);
1686
- }, POLL_INTERVAL_MS);
1687
- return () => {
1688
- window.clearInterval(timer);
1689
- controller.abort();
1690
- };
1691
- }, [
1692
- open,
1693
- refresh,
1694
- signedInState
1695
- ]);
1871
+ }, [open, currentVariant.id]);
1696
1872
  const manualRefresh = async () => {
1697
1873
  setBusy(true);
1698
- const controller = trackController();
1699
1874
  try {
1700
- await refresh(controller.signal);
1875
+ await refresh();
1701
1876
  } finally {
1702
- manualControllers.current.delete(controller);
1703
1877
  if (mounted.current) setBusy(false);
1704
1878
  }
1705
1879
  };
@@ -1729,14 +1903,14 @@ window.__ModuleLoader__.load({
1729
1903
  });
1730
1904
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1731
1905
  } catch (error) {
1732
- if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
1906
+ if (mounted.current && controller.signal.aborted !== true) setWriteFailure(error instanceof Error ? error.message : t("requestFailed"));
1733
1907
  manualControllers.current.delete(controller);
1734
1908
  return;
1735
1909
  } finally {
1736
1910
  if (mounted.current) setBusy(false);
1737
1911
  }
1738
1912
  try {
1739
- await refresh(controller.signal);
1913
+ await refresh();
1740
1914
  } finally {
1741
1915
  manualControllers.current.delete(controller);
1742
1916
  }
@@ -1777,7 +1951,7 @@ window.__ModuleLoader__.load({
1777
1951
  if (mounted.current) setCheckingIn(false);
1778
1952
  }
1779
1953
  try {
1780
- await refresh(controller.signal);
1954
+ await refresh();
1781
1955
  } finally {
1782
1956
  manualControllers.current.delete(controller);
1783
1957
  }
@@ -1813,7 +1987,7 @@ window.__ModuleLoader__.load({
1813
1987
  if (mounted.current) setClearingLogs(false);
1814
1988
  }
1815
1989
  try {
1816
- await refresh(controller.signal);
1990
+ await refresh();
1817
1991
  } finally {
1818
1992
  manualControllers.current.delete(controller);
1819
1993
  }
@@ -1856,9 +2030,9 @@ window.__ModuleLoader__.load({
1856
2030
  const reason = typeof value === "object" && value !== null && "reason" in value ? String(value["reason"]) : t("requestFailed");
1857
2031
  throw new Error(reason);
1858
2032
  }
1859
- await refresh(controller.signal);
2033
+ await refresh();
1860
2034
  } catch (error) {
1861
- if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
2035
+ if (mounted.current && controller.signal.aborted !== true) setWriteFailure(error instanceof Error ? error.message : t("requestFailed"));
1862
2036
  } finally {
1863
2037
  manualControllers.current.delete(controller);
1864
2038
  if (mounted.current) setBusy(false);
@@ -1916,7 +2090,7 @@ window.__ModuleLoader__.load({
1916
2090
  setReplacing(false);
1917
2091
  setPatNotice(t("patSaved"));
1918
2092
  noteQuotaSignIn(currentVariant.id, true);
1919
- await refresh(controller.signal);
2093
+ await refresh();
1920
2094
  } catch (error) {
1921
2095
  if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
1922
2096
  } finally {
@@ -1960,15 +2134,11 @@ window.__ModuleLoader__.load({
1960
2134
  return;
1961
2135
  }
1962
2136
  setPatDraft("");
1963
- const signedOutDoc = {
2137
+ noteQuotaStatus(currentVariant.id, {
1964
2138
  status: "signed-out",
1965
2139
  authKey: key
1966
- };
1967
- setStatus(signedOutDoc);
1968
- setSignedInState(false);
1969
- noteQuotaStatus(currentVariant.id, signedOutDoc);
1970
- noteQuotaSignIn(currentVariant.id, false);
1971
- await refresh(controller.signal);
2140
+ });
2141
+ await refresh();
1972
2142
  } catch (error) {
1973
2143
  if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
1974
2144
  } finally {
@@ -2208,9 +2378,9 @@ window.__ModuleLoader__.load({
2208
2378
  })] })
2209
2379
  ]
2210
2380
  }),
2211
- readFailure === void 0 || signedInState === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2381
+ readFailure === void 0 && writeFailure === void 0 || signedInState === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2212
2382
  style: errorStyle,
2213
- children: t("statusRefreshFailed", { message: readFailure })
2383
+ children: t("statusRefreshFailed", { message: readFailure !== void 0 ? readFailure.kind === "invalid" ? t("statusResponseInvalid") : readFailure.message : writeFailure })
2214
2384
  }),
2215
2385
  status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2216
2386
  status.pat === void 0 ? null : patSummaryLine(status.pat),
@@ -3032,9 +3202,11 @@ window.__ModuleLoader__.load({
3032
3202
  * row, and opens a dashboard in the layout's keyed `main` slot on click. In
3033
3203
  * the 56px rail it collapses to a 36px icon button carrying the ring.
3034
3204
  *
3035
- * Data comes from the variant's status route (poll, paused while hidden);
3036
- * strings come from the `panel.qoder-quota` locale namespace; classes come
3037
- * from `./quota-styles.ts` (`qdp-` prefix).
3205
+ * Data comes from the shared status store: the card acquires its variant and
3206
+ * the store's ONE ticker reads each live variant once for every surface
3207
+ * (this card, the settings cards, the dashboard), paused while the page is
3208
+ * hidden; strings come from the `panel.qoder-quota` locale namespace; classes
3209
+ * come from `./quota-styles.ts` (`qdp-` prefix).
3038
3210
  */
3039
3211
  /** Fallback translator: renders keys bare rather than throwing unbound. */
3040
3212
  const fallbackT = (key) => key;
@@ -3138,51 +3310,14 @@ window.__ModuleLoader__.load({
3138
3310
  const variantId = statusPath !== void 0 ? variantOfStatusPath(statusPath) : "qoder";
3139
3311
  const nameKey = variantId === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
3140
3312
  const wide = props.wide !== false;
3141
- const [failed, setFailed] = (0, react.useState)(false);
3142
3313
  (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSettingsRevision);
3143
3314
  const enabled = variantId === "qoder" ? quotaToggles().cn : quotaToggles().global;
3315
+ const failed = quotaReadFailure(variantId) !== void 0;
3144
3316
  const status = quotaStatus(variantId);
3145
3317
  const signedIn = status?.status === "signed-in";
3146
3318
  (0, react.useEffect)(() => {
3147
3319
  if (statusPath === void 0 || !enabled) return void 0;
3148
- let disposed = false;
3149
- let timer;
3150
- const controller = new AbortController();
3151
- const refresh = async () => {
3152
- try {
3153
- const response = await fetch(statusPath, {
3154
- signal: controller.signal,
3155
- headers: { accept: "application/json" }
3156
- });
3157
- const body = await response.json();
3158
- if (disposed) return;
3159
- if (!response.ok || !isQoderWebStatus(body)) {
3160
- setFailed(true);
3161
- return;
3162
- }
3163
- setFailed(false);
3164
- noteQuotaStatus(variantId, body);
3165
- } catch {
3166
- if (!disposed) setFailed(true);
3167
- }
3168
- };
3169
- const isHidden = () => typeof document !== "undefined" && document.hidden;
3170
- const loop = () => {
3171
- if (isHidden()) return;
3172
- refresh();
3173
- };
3174
- timer = window.setInterval(loop, Math.max(6e4, quotaPollMs()));
3175
- refresh();
3176
- const onVisible = () => {
3177
- if (!isHidden()) loop();
3178
- };
3179
- if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
3180
- return () => {
3181
- disposed = true;
3182
- controller.abort();
3183
- if (timer !== void 0) window.clearInterval(timer);
3184
- if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
3185
- };
3320
+ return acquireLiveSurface(variantId);
3186
3321
  }, [
3187
3322
  statusPath,
3188
3323
  enabled,
@@ -3301,10 +3436,11 @@ window.__ModuleLoader__.load({
3301
3436
  * pattern): CN and international are separate accounts with separate package
3302
3437
  * lists, so mixing them into one column would misattribute every number.
3303
3438
  *
3304
- * The panel fetches BOTH routes itself on mount and on the shared poll
3305
- * interval — a user opening the panel must never wait for the sidebar cards'
3306
- * next tick, and must never see a stale "sign in" just because no poll had
3307
- * run yet. The tab defaults to the variant whose card was clicked.
3439
+ * The panel holds its shown variant live through the shared store — it reads
3440
+ * at once on open (unless the shared document is still within the settings
3441
+ * interval, which a click then shows as cached numbers) and the shared ticker
3442
+ * keeps it current from then on, so a user opening the panel never waits for
3443
+ * a poll and never meets a stale "sign in" because nothing had read yet.
3308
3444
  */
3309
3445
  function QuotaDashboard(props) {
3310
3446
  const { t = fallbackT, statusPaths, refresh, close, useQuotaDashboard, onVariantPicked } = props;
@@ -3316,7 +3452,7 @@ window.__ModuleLoader__.load({
3316
3452
  const activeVariant = variantOfStatusPath(activePathResolved);
3317
3453
  const status = quotaStatus(activeVariant);
3318
3454
  const loading = state.loading;
3319
- const fetchedAt = state.fetchedAt;
3455
+ const fetchedAt = quotaStatusFetchedAt(activeVariant);
3320
3456
  const credits = status !== void 0 && "credits" in status ? status.credits : void 0;
3321
3457
  const rows = credits === void 0 ? [] : buildPackageRows(credits.accounts ?? []);
3322
3458
  const nameKey = activeVariant === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
@@ -4078,6 +4214,20 @@ window.__ModuleLoader__.load({
4078
4214
  /** The shared block's title, fixed by the contract so all three plugins agree. */
4079
4215
  const SHARED_SECTION_LABEL = "插件设置";
4080
4216
  /**
4217
+ * The plugin manager's bundle-configuration seat and this bundle's key in it.
4218
+ *
4219
+ * The sidebar's Plugins panel (the `plugins` main panel the Host's plugin
4220
+ * manager registers) renders one bundle's own configuration on the bundle's
4221
+ * detail page — between the description and the component rows — through the
4222
+ * `plugins.bundle.config` keyed slot, keyed by the bundle's npm package name.
4223
+ * The key is what the page's configuration ledger reads to decide whether the
4224
+ * configuration section shows at all, so it must spell this package's name
4225
+ * exactly; the slot itself is declared by the Host (see
4226
+ * `plugin-manager-slots.ts` for the structural restatement).
4227
+ */
4228
+ const PLUGIN_MANAGER_SLOT = "plugins.bundle.config";
4229
+ const PLUGIN_MANAGER_KEY = "dsh-qoder-connect";
4230
+ /**
4081
4231
  * The shared 《插件设置》 section the 0.1.7 card list lives in.
4082
4232
  *
4083
4233
  * It renders nothing but its child slot: every connect plugin contributes its
@@ -4102,7 +4252,8 @@ window.__ModuleLoader__.load({
4102
4252
  }
4103
4253
  /**
4104
4254
  * Register card copy, the shared quota-settings card, the two variant cards,
4105
- * and the sidebar quota cards.
4255
+ * the sidebar quota cards, and the same unified card on this bundle's page in
4256
+ * the sidebar's Plugins panel.
4106
4257
  *
4107
4258
  * The entire body is wrapped so that a DSH slot-API breaking change (for
4108
4259
  * example the rc.6→rc.7 `id`→`key` / `order`→`priority` rename) degrades
@@ -4157,20 +4308,27 @@ window.__ModuleLoader__.load({
4157
4308
  applySnapshot();
4158
4309
  scope.subscribe(applySnapshot);
4159
4310
  };
4160
- const registerUnifiedCard = () => {
4161
- const inject = () => ({
4162
- t,
4163
- scope: quotaScope,
4164
- signedIn: () => quotaSignInState(),
4165
- unified: true
4166
- });
4167
- return ctx.slots.inject(SHARED_ITEM_SLOT, () => ctx.slots.register({
4168
- name: SHARED_ITEM_SLOT,
4169
- id: SHARED_ITEM_ID,
4170
- order: 30,
4171
- inject
4172
- }, QoderPluginCard));
4173
- };
4311
+ /**
4312
+ * The unified card's inject face, shared by its two surfaces: the shared
4313
+ * 《插件设置》 block and this bundle's own page in the sidebar's Plugins
4314
+ * panel. One face keeps both cards reading the same live values — the same
4315
+ * scope, the same sign-in fact, the same `unified` layout — so a save on
4316
+ * either surface is a save for both. The Plugins-panel registration opens
4317
+ * the card at once; the settings block lists it collapsed.
4318
+ */
4319
+ const unifiedCardInject = (defaultOpen = false) => ({
4320
+ t,
4321
+ signedIn: () => quotaSignInState(),
4322
+ scope: quotaScope,
4323
+ unified: true,
4324
+ defaultOpen
4325
+ });
4326
+ const registerUnifiedCard = () => ctx.slots.inject(SHARED_ITEM_SLOT, () => ctx.slots.register({
4327
+ name: SHARED_ITEM_SLOT,
4328
+ id: SHARED_ITEM_ID,
4329
+ order: 30,
4330
+ inject: unifiedCardInject
4331
+ }, QoderPluginCard));
4174
4332
  /**
4175
4333
  * Attach this plugin's card to the shared 《插件设置》 block, building the
4176
4334
  * block first if nobody has.
@@ -4209,6 +4367,33 @@ window.__ModuleLoader__.load({
4209
4367
  });
4210
4368
  };
4211
4369
  /**
4370
+ * Contribute the same unified card to this bundle's page in the sidebar's
4371
+ * Plugins panel, beside its enable switch and component rows.
4372
+ *
4373
+ * `ctx.slots.inject` defers the factory until the Host's plugin manager
4374
+ * declares the seat (its `main` registration commits the child table), so
4375
+ * a deployment that ships no plugin manager — or loads it after this
4376
+ * bundle — never throws: the callback simply never runs, and the shared
4377
+ * 《插件设置》 block stays the single surface, exactly as before. The
4378
+ * keyed key is this package's name, the same key the page's configuration
4379
+ * ledger reads to decide whether the configuration section shows.
4380
+ *
4381
+ * Its own boundary mirrors the dashboard and footer-card ones: a slot-API
4382
+ * breaking change degrades to a console.error instead of taking the
4383
+ * settings block or the model channel with it.
4384
+ */
4385
+ const joinPluginManagerBlock = () => {
4386
+ try {
4387
+ ctx.slots.inject(PLUGIN_MANAGER_SLOT, () => ctx.slots.register({
4388
+ name: PLUGIN_MANAGER_SLOT,
4389
+ key: PLUGIN_MANAGER_KEY,
4390
+ inject: () => unifiedCardInject(true)
4391
+ }, QoderPluginCard));
4392
+ } catch (error) {
4393
+ console.error("[dsh-qoder-connect] could not join the plugin manager page:", error);
4394
+ }
4395
+ };
4396
+ /**
4212
4397
  * 插件自有配置(`<profile>/.dsh-qoder-connect/settings.json`):两条宿主线的
4213
4398
  * 读写都走宿主半的 settings face,不再经过 settingsScope / configForms。
4214
4399
  *
@@ -4220,13 +4405,9 @@ window.__ModuleLoader__.load({
4220
4405
  ownQuotaScope.load();
4221
4406
  adoptQuotaScope(ownQuotaScope);
4222
4407
  joinSharedSettingsBlock();
4408
+ joinPluginManagerBlock();
4223
4409
  const QUOTA_PANEL_ID = "qoder-quota-panel";
4224
4410
  const CONVERSATION_PANEL_ID = "conversation";
4225
- const dashboardDocuments = {
4226
- cn: void 0,
4227
- global: void 0
4228
- };
4229
- let dashboardFetchedAt;
4230
4411
  let dashboardLoading = false;
4231
4412
  let dashboardRequestedPath = QODER_STATUS_PATH;
4232
4413
  /** Whether the dashboard is the CURRENT center panel (its mount owns this). */
@@ -4250,15 +4431,11 @@ window.__ModuleLoader__.load({
4250
4431
  * mutator builds the next snapshot and publishes exactly once.
4251
4432
  */
4252
4433
  let dashboardSnap = {
4253
- documents: [void 0, void 0],
4254
- fetchedAt: void 0,
4255
4434
  loading: false,
4256
4435
  activePath: QODER_STATUS_PATH
4257
4436
  };
4258
4437
  const rebuildSnapshot = () => {
4259
4438
  const next = {
4260
- documents: [dashboardDocuments.cn, dashboardDocuments.global],
4261
- fetchedAt: dashboardFetchedAt,
4262
4439
  loading: dashboardLoading,
4263
4440
  activePath: dashboardRequestedPath
4264
4441
  };
@@ -4291,6 +4468,12 @@ window.__ModuleLoader__.load({
4291
4468
  * variant with NO result yet always fetches. A manual Refresh click
4292
4469
  * (force=true) bypasses the freshness check: an explicit user action
4293
4470
  * always re-reads.
4471
+ *
4472
+ * The read itself is the SHARED one: it publishes through the store's
4473
+ * sequence guard, so this panel, the sidebar cards and the settings cards
4474
+ * converge on the same document — and a settings card polling the variant
4475
+ * already keeps this panel current, which is why the panel no longer
4476
+ * keeps a timer of its own.
4294
4477
  */
4295
4478
  const refreshDashboard = async (options = {}) => {
4296
4479
  if (dashboardLoading) return;
@@ -4299,44 +4482,36 @@ window.__ModuleLoader__.load({
4299
4482
  dashboardLoading = true;
4300
4483
  rebuildSnapshot();
4301
4484
  try {
4302
- const result = await fetchStatusDocument(variantId === "qoder" ? QODER_STATUS_PATH : QODER_GLOBAL_STATUS_PATH);
4303
- if (result !== void 0) noteQuotaStatus(variantId, result);
4304
- dashboardFetchedAt = Date.now();
4485
+ await refreshQuotaStatus(variantId);
4305
4486
  } finally {
4306
4487
  dashboardLoading = false;
4307
4488
  rebuildSnapshot();
4308
4489
  }
4309
4490
  };
4310
- let dashboardTimer;
4311
- const startDashboardPoll = () => {
4312
- if (dashboardTimer !== void 0) return;
4313
- refreshDashboard();
4314
- dashboardTimer = window.setInterval(() => {
4315
- if (document.hidden) return;
4316
- refreshDashboard();
4317
- }, Math.max(6e4, quotaPollMs()));
4491
+ /**
4492
+ * The panel's hold on its shown variant. The hold is what keeps the
4493
+ * variant live: the shared ticker reads it at the settings interval while
4494
+ * the panel stands — the cadence the panel's own timer used to keep — and
4495
+ * an expanded settings card beside it tightens the read to once a minute.
4496
+ * A variant switch moves the hold to the newly shown variant.
4497
+ */
4498
+ let dashboardHold;
4499
+ const holdDashboardVariant = (path) => {
4500
+ dashboardHold?.();
4501
+ dashboardHold = acquireLiveSurface(variantOfStatusPath(path));
4318
4502
  };
4319
- const stopDashboardPoll = () => {
4320
- if (dashboardTimer === void 0) return;
4321
- window.clearInterval(dashboardTimer);
4322
- dashboardTimer = void 0;
4503
+ const releaseDashboardVariant = () => {
4504
+ dashboardHold?.();
4505
+ dashboardHold = void 0;
4323
4506
  };
4324
- async function fetchStatusDocument(path) {
4325
- try {
4326
- const response = await fetch(path, { headers: { accept: "application/json" } });
4327
- const body = await response.json();
4328
- return response.ok && isQoderWebStatus(body) ? body : void 0;
4329
- } catch {
4330
- return;
4331
- }
4332
- }
4333
4507
  function QuotaDashboardWithLifecycle(props) {
4334
4508
  (0, react.useEffect)(() => {
4335
4509
  quotaPanelOpen = true;
4336
- startDashboardPoll();
4510
+ holdDashboardVariant(dashboardRequestedPath);
4511
+ refreshDashboard();
4337
4512
  return () => {
4338
4513
  quotaPanelOpen = false;
4339
- stopDashboardPoll();
4514
+ releaseDashboardVariant();
4340
4515
  };
4341
4516
  }, []);
4342
4517
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaDashboard, { ...props });
@@ -4350,6 +4525,7 @@ window.__ModuleLoader__.load({
4350
4525
  },
4351
4526
  onVariantPicked: (path) => {
4352
4527
  dashboardRequestedPath = path;
4528
+ holdDashboardVariant(path);
4353
4529
  notifyDashboard();
4354
4530
  refreshDashboard();
4355
4531
  },