dsh-workbuddy-xdpool 1.4.1 → 1.6.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
@@ -255,39 +255,44 @@ window.__ModuleLoader__.load({
255
255
  .dsm-workbuddy-xdpool-auto{margin-top:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:10px;background:var(--dsw-alias-bg-module-platform,#202126)}
256
256
  .dsm-workbuddy-xdpool-auto-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
257
257
  .dsm-workbuddy-xdpool-auto-title{color:var(--dsw-alias-label-primary,#e8e8ea);font-size:12px;font-weight:600}
258
- .dsm-workbuddy-xdpool-auto-switch{padding:3px 12px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:999px;background:transparent;color:var(--dsw-alias-label-dimmed,#8a97b5);font-size:11px;cursor:pointer;transition:color .16s,border-color .16s,background .16s}
258
+ .dsm-workbuddy-xdpool-auto-switch{padding:3px 12px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;cursor:pointer;transition:color .16s,border-color .16s,background .16s}
259
259
  .dsm-workbuddy-xdpool-auto-switch:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,#777);color:var(--dsw-alias-label-primary,#e8e8ea)}
260
260
  .dsm-workbuddy-xdpool-auto-switch:disabled{opacity:.5;cursor:not-allowed}
261
261
  .dsm-workbuddy-xdpool-auto-switch-on{border-color:#28c8b4;color:#28c8b4;background:rgba(40,200,180,.12)}
262
- .dsm-workbuddy-xdpool-auto-hint{margin:6px 0 0;color:var(--dsw-alias-label-dimmed,#8a97b5);font-size:11px;line-height:1.5}
262
+ .dsm-workbuddy-xdpool-auto-hint{margin:6px 0 0;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:1.6}
263
263
  .dsm-workbuddy-xdpool-auto-total{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-top:8px;padding:6px 8px;border-radius:8px;background:rgba(40,200,180,.08);font-size:11px}
264
- .dsm-workbuddy-xdpool-auto-total-label{color:var(--dsw-alias-label-dimmed,#8a97b5)}
264
+ .dsm-workbuddy-xdpool-auto-total-label{color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px}
265
265
  .dsm-workbuddy-xdpool-auto-total{display:flex;flex-direction:column;gap:4px;margin-top:8px;padding:6px 8px;border-radius:8px;background:rgba(40,200,180,.08);font-size:11px}
266
266
  .dsm-workbuddy-xdpool-auto-total-list{display:flex;flex-direction:column;gap:2px}
267
267
  .dsm-workbuddy-xdpool-auto-total-row{color:#28c8b4;font-variant-numeric:tabular-nums}
268
268
  .dsm-workbuddy-xdpool-earned{display:flex;flex-direction:column;gap:4px;margin-top:8px;padding-top:8px;border-top:1px dashed var(--dsw-alias-border-l2,#36373b);font-size:11px}
269
- .dsm-workbuddy-xdpool-earned-label{color:var(--dsw-alias-label-dimmed,#8a97b5)}
269
+ .dsm-workbuddy-xdpool-earned-label{color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px}
270
270
  .dsm-workbuddy-xdpool-earned-list{display:flex;flex-direction:column;gap:2px}
271
271
  .dsm-workbuddy-xdpool-earned-row{color:#28c8b4;font-variant-numeric:tabular-nums}
272
272
  .dsm-workbuddy-xdpool-earned-value{color:#28c8b4;font-weight:600;font-variant-numeric:tabular-nums}
273
273
  .dsm-workbuddy-xdpool-auto-jobs{margin-top:8px;display:flex;flex-direction:column;gap:4px}
274
- .dsm-workbuddy-xdpool-auto-job{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-dimmed,#8a97b5)}
275
- .dsm-workbuddy-xdpool-auto-job-name{flex:0 0 72px;color:var(--dsw-alias-label-primary,#e8e8ea)}
276
- .dsm-workbuddy-xdpool-auto-job-when{flex:1 1 auto;font-variant-numeric:tabular-nums}
277
- .dsm-workbuddy-xdpool-auto-job-last{flex:0 0 auto;text-align:right;font-variant-numeric:tabular-nums}
278
- /* The job detail wraps to its own full-width line: it is a list of reward
279
- titles, which needs the whole row rather than a cramped right-hand slot. */
280
- .dsm-workbuddy-xdpool-auto-job-detail{flex:1 1 100%;color:var(--dsw-alias-label-secondary,#b8bcc4);line-height:1.5;word-break:break-word}
281
- .dsm-workbuddy-xdpool-auto-job-note{flex:0 0 auto;color:var(--dsw-alias-label-secondary,#b8bcc4)}
282
- .dsm-workbuddy-xdpool-auto-run{flex:0 0 auto;padding:2px 8px;font-size:10px;line-height:16px}
274
+ .dsm-workbuddy-xdpool-auto-job{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
275
+ .dsm-workbuddy-xdpool-auto-job-name{flex:0 0 auto;min-width:72px;color:var(--dsw-alias-label-primary,#e8e8ea);font-weight:500}
276
+ .dsm-workbuddy-xdpool-auto-job-when{flex:1 1 auto;color:var(--dsw-alias-label-secondary,#c6c9d0);font-variant-numeric:tabular-nums}
277
+ .dsm-workbuddy-xdpool-auto-job-last{flex:0 0 auto;text-align:right;color:var(--dsw-alias-label-secondary,#c6c9d0);font-variant-numeric:tabular-nums;white-space:nowrap}
278
+ .dsm-workbuddy-xdpool-auto-job-note{flex:0 0 auto;color:var(--dsw-alias-label-secondary,#c6c9d0)}
283
279
  /* Reserved credits: one inline number field per account. */
284
280
  .dsm-workbuddy-xdpool-reserve{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:8px;padding-top:8px;border-top:1px dashed var(--dsw-alias-border-l2,#36373b);font-size:11px;color:var(--dsw-alias-label-dimmed,#8a97b5)}
285
281
  .dsm-workbuddy-xdpool-reserve-label{flex:0 0 auto}
286
- .dsm-workbuddy-xdpool-reserve-input{width:76px;padding:2px 6px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary,#e8e8ea);font:inherit;font-variant-numeric:tabular-nums}
282
+ .dsm-workbuddy-xdpool-reserve-input{width:84px;padding:3px 8px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary,#e8e8ea);font:inherit;font-variant-numeric:tabular-nums;transition:border-color .16s,background .16s}
283
+ .dsm-workbuddy-xdpool-reserve-input:focus{outline:none;border-color:var(--dsw-alias-state-success-primary,#22a06b);background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 7%, transparent)}
287
284
  .dsm-workbuddy-xdpool-reserve-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
288
285
  .dsm-workbuddy-xdpool-reserve-input:disabled{opacity:.6}
289
286
  .dsm-workbuddy-xdpool-reserve-unit{flex:0 0 auto}
290
287
  .dsm-workbuddy-xdpool-reserve-badge{flex:0 0 auto;padding:1px 7px;border-radius:999px;background:rgba(232,90,90,.14);color:#e85a5a;font-size:10px}
288
+ /* Explicit Save: the field is a draft, so the button (not a blur) is what
289
+ commits it — and the outcome is reported right here, next to the control. */
290
+ .dsm-workbuddy-xdpool-reserve-save{flex:0 0 auto;appearance:none;font:inherit;font-size:11px;font-weight:600;line-height:16px;padding:3px 12px;border-radius:6px;border:1px solid color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 55%, transparent);background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 14%, transparent);color:var(--dsw-alias-state-success-primary,#22a06b);cursor:pointer;transition:opacity .16s,background .16s,border-color .16s,color .16s}
291
+ .dsm-workbuddy-xdpool-reserve-save:hover:not(:disabled){background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 24%, transparent);border-color:var(--dsw-alias-state-success-primary,#22a06b)}
292
+ .dsm-workbuddy-xdpool-reserve-save:disabled{cursor:default;border-color:var(--dsw-alias-border-l2,#3a3d45);background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0);opacity:.75}
293
+ .dsm-workbuddy-xdpool-reserve-note{flex:0 0 auto;font-size:11px;line-height:16px}
294
+ .dsm-workbuddy-xdpool-reserve-note-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
295
+ .dsm-workbuddy-xdpool-reserve-note-bad{color:var(--dsw-alias-state-error-primary,#ef4444);font-weight:600}
291
296
  `.trim();
292
297
  //#endregion
293
298
  //#region src/client/PoolCard.tsx
@@ -843,6 +848,17 @@ window.__ModuleLoader__.load({
843
848
  * also refreshes the account list afterwards: the reserve badge appears
844
849
  * as soon as the reading crosses the floor.
845
850
  */
851
+ /**
852
+ * Save one account reserved-credit floor.
853
+ *
854
+ * A reserve only protects credits if the pool knows the balance, so this also
855
+ * refreshes the account list afterwards: the reserve badge appears as soon as
856
+ * the reading crosses the floor.
857
+ *
858
+ * Returns whether the host CONFIRMED the write. The card keys its inline
859
+ * "saved / not saved" note off this, so a failure is shown where the user is
860
+ * looking instead of only in the card-level notice line.
861
+ */
846
862
  const saveCreditReserve = async (accountId, reserve) => {
847
863
  setReserveBusy(accountId);
848
864
  setFlash(void 0);
@@ -863,8 +879,10 @@ window.__ModuleLoader__.load({
863
879
  if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
864
880
  await refresh(activeRegion);
865
881
  if (mounted.current) setFlash(reserve > 0 ? t?.("row.reserveSaved", { credits: reserve }) ?? `Keeping ${reserve} credits` : t?.("row.reserveCleared") ?? "Reserve cleared");
882
+ return true;
866
883
  } catch (cause) {
867
884
  if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
885
+ return false;
868
886
  } finally {
869
887
  if (mounted.current) setReserveBusy(void 0);
870
888
  }
@@ -1234,9 +1252,7 @@ window.__ModuleLoader__.load({
1234
1252
  onClaimCheckin: (accountId) => {
1235
1253
  claimCheckin(accountId);
1236
1254
  },
1237
- onSaveCreditReserve: (accountId, reserve) => {
1238
- saveCreditReserve(accountId, reserve);
1239
- },
1255
+ onSaveCreditReserve: (accountId, reserve) => saveCreditReserve(accountId, reserve),
1240
1256
  onToggleDisabled: (accountId, disabled) => {
1241
1257
  toggleAccountDisabled(accountId, disabled);
1242
1258
  },
@@ -1432,19 +1448,41 @@ window.__ModuleLoader__.load({
1432
1448
  * each save is a settings write plus a status refresh, and a per-character
1433
1449
  * save would hammer both.
1434
1450
  */
1451
+ /**
1452
+ * The reserved-credit floor for one account.
1453
+ *
1454
+ * Saving is an EXPLICIT action, not a blur side effect. The old version
1455
+ * committed `onBlur`, which meant a value could be written without the user
1456
+ * asking for it — and when the write silently failed, the only trace was a
1457
+ * notice line at the top of the card that is easy to miss. That is how
1458
+ * "I typed a number, reopened, and it says 0 again" happened with no visible
1459
+ * error to explain it.
1460
+ *
1461
+ * Now: the field is a draft, Save is enabled only when the draft differs from
1462
+ * what the host last reported, and the outcome (saving / saved / failed) is
1463
+ * shown inline next to the button. Enter also saves, so keyboard flow is not
1464
+ * lost.
1465
+ */
1435
1466
  function CreditReserveRow({ account, t, busy, onSave }) {
1436
- const [draft, setDraft] = (0, react.useState)(String(account.creditReserve ?? 0));
1467
+ const saved = account.creditReserve ?? 0;
1468
+ const [draft, setDraft] = (0, react.useState)(String(saved));
1469
+ const [settled, setSettled] = (0, react.useState)(saved);
1470
+ const [note, setNote] = (0, react.useState)(void 0);
1437
1471
  (0, react.useEffect)(() => {
1438
- setDraft(String(account.creditReserve ?? 0));
1439
- }, [account.creditReserve]);
1440
- const commit = () => {
1441
- const parsed = Number.parseInt(draft, 10);
1442
- const next = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
1443
- if (next === (account.creditReserve ?? 0)) {
1472
+ setSettled(saved);
1473
+ setDraft((current) => current === String(saved) ? current : String(saved));
1474
+ }, [saved]);
1475
+ const parsed = Number.parseInt(draft, 10);
1476
+ const next = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
1477
+ const dirty = next !== settled;
1478
+ const commit = async () => {
1479
+ if (busy || !dirty) return;
1480
+ setNote(void 0);
1481
+ if (await onSave(account.id, next)) {
1482
+ setSettled(next);
1444
1483
  setDraft(String(next));
1445
- return;
1446
- }
1447
- onSave(account.id, next);
1484
+ setNote("saved");
1485
+ } else setNote("failed");
1448
1486
  };
1449
1487
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1450
1488
  className: "dsm-workbuddy-xdpool-reserve",
@@ -1463,16 +1501,33 @@ window.__ModuleLoader__.load({
1463
1501
  "aria-label": t?.("row.reserveTitle") ?? "Keep at least",
1464
1502
  onChange: (event) => {
1465
1503
  setDraft(event.target.value);
1504
+ setNote(void 0);
1466
1505
  },
1467
- onBlur: commit,
1468
1506
  onKeyDown: (event) => {
1469
- if (event.key === "Enter") event.target.blur();
1507
+ if (event.key === "Enter") commit();
1470
1508
  }
1471
1509
  }),
1472
1510
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1473
1511
  className: "dsm-workbuddy-xdpool-reserve-unit",
1474
- children: busy ? t?.("row.reserveSaving") ?? "Saving…" : t?.("row.reserveUnit") ?? "credits"
1512
+ children: t?.("row.reserveUnit") ?? "credits"
1513
+ }),
1514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1515
+ type: "button",
1516
+ className: "dsm-workbuddy-xdpool-reserve-save",
1517
+ disabled: busy || !dirty,
1518
+ onClick: () => {
1519
+ commit();
1520
+ },
1521
+ children: busy ? t?.("row.reserveSaving") ?? "Saving…" : t?.("row.reserveSave") ?? "Save"
1475
1522
  }),
1523
+ note === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1524
+ className: "dsm-workbuddy-xdpool-reserve-note dsm-workbuddy-xdpool-reserve-note-ok",
1525
+ children: next > 0 ? t?.("row.reserveSaved", { credits: next }) ?? `Keeping ${next} credits` : t?.("row.reserveCleared") ?? "Reserve cleared"
1526
+ }) : null,
1527
+ note === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1528
+ className: "dsm-workbuddy-xdpool-reserve-note dsm-workbuddy-xdpool-reserve-note-bad",
1529
+ children: t?.("row.reserveFailed") ?? "Not saved — try again"
1530
+ }) : null,
1476
1531
  account.reserved === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1477
1532
  className: "dsm-workbuddy-xdpool-reserve-badge",
1478
1533
  children: t?.("row.reserveHolding") ?? "Reserved: skipped"
@@ -1797,9 +1852,11 @@ window.__ModuleLoader__.load({
1797
1852
  "row.reserveTitle": "Keep at least",
1798
1853
  "row.reserveUnit": "credits",
1799
1854
  "row.reserveSaving": "Saving…",
1855
+ "row.reserveSave": "Save",
1800
1856
  "row.reserveHolding": "Reserved: skipped",
1801
1857
  "row.reserveSaved": "Keeping {credits} credits",
1802
1858
  "row.reserveCleared": "Reserve cleared",
1859
+ "row.reserveFailed": "Not saved — try again",
1803
1860
  "row.autoToday": "Today",
1804
1861
  "row.autoEarned": "Automation today",
1805
1862
  "row.autoEnergy": "energy",
@@ -1926,9 +1983,11 @@ window.__ModuleLoader__.load({
1926
1983
  "row.reserveTitle": "保留积分",
1927
1984
  "row.reserveUnit": "积分",
1928
1985
  "row.reserveSaving": "保存中…",
1986
+ "row.reserveSave": "保存",
1929
1987
  "row.reserveHolding": "已保留·暂停使用",
1930
1988
  "row.reserveSaved": "已保留 {credits} 积分",
1931
1989
  "row.reserveCleared": "已取消保留",
1990
+ "row.reserveFailed": "保存失败,请重试",
1932
1991
  "row.autoToday": "今日自动化",
1933
1992
  "row.autoEarned": "今日自动化获得",
1934
1993
  "row.autoEnergy": "能量",
package/lib/index.d.ts CHANGED
@@ -764,8 +764,12 @@ export declare function defaultDesktopAuthDirs(platform?: NodeJS.Platform, home?
764
764
  * Parse a WorkBuddy auth document. Accepts the nested desktop shape
765
765
  * `{"auth":{...},"account":{...}}` and the flat panel shape; returns undefined
766
766
  * when there is no usable access token.
767
+ *
768
+ * `decrypt` opens the desktop app's `$wbEncrypted` field wrapper (5.6.0+, both
769
+ * platforms). Absent means "plain-string builds only", which is what every
770
+ * caller without an at-rest key should pass.
767
771
  */
768
- export declare function parseWorkBuddyAuth(text: string, sourcePath: string): WorkBuddyCredential | undefined;
772
+ export declare function parseWorkBuddyAuth(text: string, sourcePath: string, decrypt?: (field: unknown) => string): WorkBuddyCredential | undefined;
769
773
  export declare function workbuddyAccountId(credential: Pick<WorkBuddyCredential, 'uin' | 'uid' | 'nickname'>): string;
770
774
  /** Every directory the pool should scan, in probe order. */
771
775
  export declare function candidateAuthDirs(env?: NodeJS.ProcessEnv): string[];
@@ -1332,7 +1336,7 @@ type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1332
1336
  export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1333
1337
  /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1334
1338
  export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1335
- export declare function dayKey(date: Date): string;
1339
+ export declare function dayKey(date: Date, timeZone?: string): string;
1336
1340
  /**
1337
1341
  * Whether `now`'s local hour is one of `hours`.
1338
1342
  *
@@ -1467,6 +1471,15 @@ export declare class WorkBuddyScheduler {
1467
1471
  * rate-limits per account, so overlapping passes would only trip that limit.
1468
1472
  * A job that throws is recorded and the loop continues.
1469
1473
  */
1474
+ /**
1475
+ * Whether `kind` is due at `now`: its earliest configured hour has passed in
1476
+ * the scheduling timezone, and no hour of today has been consumed yet.
1477
+ *
1478
+ * Hours are consumed per SLOT (one entry per configured hour), so a job with
1479
+ * two hours still runs twice a day — but a job whose hour passed while DSH was
1480
+ * closed runs immediately on the next tick instead of waiting for tomorrow.
1481
+ */
1482
+ private isDue;
1470
1483
  private tick;
1471
1484
  /** Run one job against every eligible account and record the outcome. */
1472
1485
  /**