dsh-workbuddy-xdpool 1.1.0 → 1.2.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/lib/client.js CHANGED
@@ -28,6 +28,10 @@ window.__ModuleLoader__.load({
28
28
  const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
29
29
  /** Plugin-owned daily check-in action endpoint (claim today's reward). */
30
30
  const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
31
+ /** Run one automation job immediately, so the card can verify it on demand. */
32
+ const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
33
+ /** Set or clear one account's reserved-credit floor. */
34
+ const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
31
35
  //#endregion
32
36
  //#region src/client/icon.ts
33
37
  /**
@@ -116,13 +120,16 @@ window.__ModuleLoader__.load({
116
120
  .dsm-workbuddy-xdpool-usage-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
117
121
  .dsm-workbuddy-xdpool-usage-hint{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
118
122
  /* Distribution switch: priority (drain one) vs round-robin (spread). */
119
- .dsm-workbuddy-xdpool-dist{display:flex;align-items:center;gap:8px;padding-left:19px;flex-wrap:wrap}
123
+ .dsm-workbuddy-xdpool-dist{display:flex;flex-direction:column;gap:6px;padding-left:19px;margin-top:8px}
120
124
  .dsm-workbuddy-xdpool-dist-title{color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
121
- .dsm-workbuddy-xdpool-dist-option{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:999px;padding:2px 10px;font-size:11px;line-height:18px;background:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);transition:color .16s,border-color .16s,background .16s}
125
+ .dsm-workbuddy-xdpool-dist-option{appearance:none;font:inherit;cursor:pointer;text-align:left;display:flex;flex-direction:column;justify-content:center;gap:2px;min-height:44px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:8px;padding:6px 10px;background:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);transition:color .16s,border-color .16s,background .16s}
122
126
  .dsm-workbuddy-xdpool-dist-option:hover:not(:disabled):not(.dsm-workbuddy-xdpool-dist-option-active){color:var(--dsw-alias-label-primary,#e6e6e6);border-color:var(--dsw-alias-label-dimmed,#777)}
123
127
  .dsm-workbuddy-xdpool-dist-option:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
124
128
  .dsm-workbuddy-xdpool-dist-option-active{background:var(--dsw-alias-state-success-subtle,rgba(34,160,107,.14));border-color:var(--dsw-alias-state-success-primary,#22a06b);color:var(--dsw-alias-state-success-primary,#22a06b)}
125
129
  .dsm-workbuddy-xdpool-dist-option:disabled{cursor:default;opacity:.6}
130
+ .dsm-workbuddy-xdpool-dist-option-name{font-size:11px;line-height:16px;font-weight:600}
131
+ .dsm-workbuddy-xdpool-dist-option-hint{font-size:10px;line-height:14px;opacity:.8}
132
+ .dsm-workbuddy-xdpool-dist-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
126
133
  .dsm-workbuddy-xdpool-usage-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
127
134
 
128
135
  /* Account list (each account = a labeled subpanel, same as dingminhua). */
@@ -253,11 +260,30 @@ window.__ModuleLoader__.load({
253
260
  .dsm-workbuddy-xdpool-auto-switch:disabled{opacity:.5;cursor:not-allowed}
254
261
  .dsm-workbuddy-xdpool-auto-switch-on{border-color:#28c8b4;color:#28c8b4;background:rgba(40,200,180,.12)}
255
262
  .dsm-workbuddy-xdpool-auto-hint{margin:6px 0 0;color:var(--dsw-alias-label-dimmed,#8a97b5);font-size:11px;line-height:1.5}
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)}
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
+ .dsm-workbuddy-xdpool-auto-total-list{display:flex;flex-direction:column;gap:2px}
267
+ .dsm-workbuddy-xdpool-auto-total-row{color:#28c8b4;font-variant-numeric:tabular-nums}
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)}
270
+ .dsm-workbuddy-xdpool-earned-list{display:flex;flex-direction:column;gap:2px}
271
+ .dsm-workbuddy-xdpool-earned-row{color:#28c8b4;font-variant-numeric:tabular-nums}
272
+ .dsm-workbuddy-xdpool-earned-value{color:#28c8b4;font-weight:600;font-variant-numeric:tabular-nums}
256
273
  .dsm-workbuddy-xdpool-auto-jobs{margin-top:8px;display:flex;flex-direction:column;gap:4px}
257
- .dsm-workbuddy-xdpool-auto-job{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-dimmed,#8a97b5)}
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)}
258
275
  .dsm-workbuddy-xdpool-auto-job-name{flex:0 0 72px;color:var(--dsw-alias-label-primary,#e8e8ea)}
259
276
  .dsm-workbuddy-xdpool-auto-job-when{flex:1 1 auto;font-variant-numeric:tabular-nums}
260
277
  .dsm-workbuddy-xdpool-auto-job-last{flex:0 0 auto;text-align:right;font-variant-numeric:tabular-nums}
278
+ .dsm-workbuddy-xdpool-auto-run{flex:0 0 auto;padding:2px 8px;font-size:10px;line-height:16px}
279
+ /* Reserved credits: one inline number field per account. */
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)}
281
+ .dsm-workbuddy-xdpool-reserve-label{flex:0 0 auto}
282
+ .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}
283
+ .dsm-workbuddy-xdpool-reserve-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
284
+ .dsm-workbuddy-xdpool-reserve-input:disabled{opacity:.6}
285
+ .dsm-workbuddy-xdpool-reserve-unit{flex:0 0 auto}
286
+ .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}
261
287
  `.trim();
262
288
  //#endregion
263
289
  //#region src/client/PoolCard.tsx
@@ -326,6 +352,15 @@ window.__ModuleLoader__.load({
326
352
  * matching hour list off the status document so the panel stays in step with
327
353
  * whatever schedule the scheduler is actually running on.
328
354
  */
355
+ /**
356
+ * How long to watch a manual run before giving up on it.
357
+ *
358
+ * A pass runs one upstream call per account per job, so ~30s for a handful of
359
+ * accounts. The bound exists so a wedged run cannot spin the button forever;
360
+ * the run itself keeps going in the background either way.
361
+ */
362
+ const AUTOMATION_POLL_MS = 2e3;
363
+ const AUTOMATION_POLL_ATTEMPTS = 90;
329
364
  const AUTOMATION_JOBS = [
330
365
  "report",
331
366
  "tasks",
@@ -433,10 +468,35 @@ window.__ModuleLoader__.load({
433
468
  const [statusByRegion, setStatusByRegion] = (0, react.useState)({});
434
469
  /** The document for the tab on screen; undefined until its first answer. */
435
470
  const status = statusByRegion[activeRegion];
471
+ /**
472
+ * Today's automation take, summed across accounts.
473
+ *
474
+ * Summed from the per-account counters rather than kept separately, so the
475
+ * panel total and the per-account lines can never disagree.
476
+ */
477
+ const automationTotals = Object.values(status?.automation?.earningsToday ?? {}).reduce((sum, entry) => ({
478
+ credit: sum.credit + entry.credit,
479
+ energy: sum.energy + entry.energy,
480
+ claimed: sum.claimed + entry.claimed,
481
+ checkinCredit: sum.checkinCredit + entry.checkinCredit,
482
+ bonusCredit: sum.bonusCredit + entry.bonusCredit,
483
+ travelCredit: sum.travelCredit + entry.travelCredit
484
+ }), {
485
+ credit: 0,
486
+ energy: 0,
487
+ claimed: 0,
488
+ checkinCredit: 0,
489
+ bonusCredit: 0,
490
+ travelCredit: 0
491
+ });
436
492
  const [error, setError] = (0, react.useState)(void 0);
437
493
  const [busy, setBusy] = (0, react.useState)(false);
438
494
  const [cooldownBusy, setCooldownBusy] = (0, react.useState)(false);
439
495
  const [automationBusy, setAutomationBusy] = (0, react.useState)(false);
496
+ /** The automation job currently running from the card, if any. */
497
+ const [automationRun, setAutomationRun] = (0, react.useState)(void 0);
498
+ /** Account id whose reserved-credit floor is being saved, if any. */
499
+ const [reserveBusy, setReserveBusy] = (0, react.useState)(void 0);
440
500
  const [flash, setFlash] = (0, react.useState)(void 0);
441
501
  /** Account id whose daily claim is currently in flight. */
442
502
  const [checkinBusyId, setCheckinBusyId] = (0, react.useState)(void 0);
@@ -490,8 +550,10 @@ window.__ModuleLoader__.load({
490
550
  }));
491
551
  setError(void 0);
492
552
  }
553
+ return value;
493
554
  } catch (cause) {
494
555
  if (mounted.current && signal?.aborted !== true) setError(cause instanceof Error ? cause.message : String(cause));
556
+ return;
495
557
  }
496
558
  }, []);
497
559
  (0, react.useEffect)(() => {
@@ -724,6 +786,83 @@ window.__ModuleLoader__.load({
724
786
  if (mounted.current) setAutomationBusy(false);
725
787
  }
726
788
  };
789
+ /**
790
+ /**
791
+ * Run the whole automation pass now.
792
+ *
793
+ * The route only STARTS the pass: a full run takes tens of seconds, which is
794
+ * far too long to hold a request open. This watches the scheduler status until
795
+ * the run settles, so the panel can show "running" honestly and report the
796
+ * result when it lands.
797
+ */
798
+ const runAutomationJob = async () => {
799
+ setAutomationRun("all");
800
+ setFlash(void 0);
801
+ try {
802
+ const response = await fetch(POOL_AUTOMATION_RUN_PATH, {
803
+ method: "POST",
804
+ headers: {
805
+ "accept": "application/json",
806
+ "content-type": "application/json"
807
+ },
808
+ credentials: "same-origin",
809
+ body: JSON.stringify({ job: "all" })
810
+ });
811
+ const body = await response.json();
812
+ if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
813
+ if (body.started === false) {
814
+ if (mounted.current) setFlash(t?.("row.autoAlreadyRunning") ?? "A run is already in progress");
815
+ }
816
+ let settled = false;
817
+ for (let attempt = 0; attempt < AUTOMATION_POLL_ATTEMPTS; attempt += 1) {
818
+ await new Promise((resolve) => setTimeout(resolve, AUTOMATION_POLL_MS));
819
+ if (!mounted.current) return;
820
+ if ((await refresh(activeRegion))?.automation?.runInProgress === false) {
821
+ settled = true;
822
+ break;
823
+ }
824
+ }
825
+ await refresh(activeRegion);
826
+ if (mounted.current) setFlash(settled ? t?.("row.autoRunDone") ?? "Automation pass finished" : t?.("row.autoRunTimeout") ?? "Still running; check back in a moment");
827
+ } catch (cause) {
828
+ if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
829
+ } finally {
830
+ if (mounted.current) setAutomationRun(void 0);
831
+ }
832
+ };
833
+ /**
834
+ * Save one account reserved-credit floor.
835
+ *
836
+ * A reserve only protects credits if the pool knows the balance, so this
837
+ * also refreshes the account list afterwards: the reserve badge appears
838
+ * as soon as the reading crosses the floor.
839
+ */
840
+ const saveCreditReserve = async (accountId, reserve) => {
841
+ setReserveBusy(accountId);
842
+ setFlash(void 0);
843
+ try {
844
+ const response = await fetch(POOL_CREDIT_RESERVE_PATH, {
845
+ method: "POST",
846
+ headers: {
847
+ "accept": "application/json",
848
+ "content-type": "application/json"
849
+ },
850
+ credentials: "same-origin",
851
+ body: JSON.stringify({
852
+ accountId,
853
+ reserve
854
+ })
855
+ });
856
+ const body = await response.json();
857
+ if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
858
+ await refresh(activeRegion);
859
+ if (mounted.current) setFlash(reserve > 0 ? t?.("row.reserveSaved", { credits: reserve }) ?? `Keeping ${reserve} credits` : t?.("row.reserveCleared") ?? "Reserve cleared");
860
+ } catch (cause) {
861
+ if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
862
+ } finally {
863
+ if (mounted.current) setReserveBusy(void 0);
864
+ }
865
+ };
727
866
  const saveModels = async () => {
728
867
  if (draft === void 0 || status === void 0) return;
729
868
  if (enabledCount === 0) {
@@ -852,26 +991,35 @@ window.__ModuleLoader__.load({
852
991
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
853
992
  className: "dsm-workbuddy-xdpool-dist-title",
854
993
  children: t?.("row.distTitle") ?? "Account usage"
855
- }), [
856
- "priority",
857
- "balanced",
858
- "round-robin"
859
- ].map((option) => {
860
- const active = (status.distribution ?? "priority") === option;
861
- const label = option === "priority" ? t?.("row.distPriority") ?? "Priority" : option === "balanced" ? t?.("row.distBalanced") ?? "Balanced" : t?.("row.distRoundRobin") ?? "Round-robin";
862
- const hint = option === "priority" ? t?.("row.distPriorityHint") ?? "" : option === "balanced" ? t?.("row.distBalancedHint") ?? "" : t?.("row.distRoundRobinHint") ?? "";
863
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
- type: "button",
865
- role: "radio",
866
- "aria-checked": active,
867
- title: hint,
868
- disabled: !modelsEditable,
869
- className: "dsm-workbuddy-xdpool-dist-option" + (active ? " dsm-workbuddy-xdpool-dist-option-active" : ""),
870
- onClick: () => {
871
- setDistribution(option);
872
- },
873
- children: label
874
- }, option);
994
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
995
+ className: "dsm-workbuddy-xdpool-dist-options",
996
+ children: [
997
+ "priority",
998
+ "balanced",
999
+ "round-robin"
1000
+ ].map((option) => {
1001
+ const active = (status.distribution ?? "priority") === option;
1002
+ const label = option === "priority" ? t?.("row.distPriority") ?? "Priority" : option === "balanced" ? t?.("row.distBalanced") ?? "Balanced" : t?.("row.distRoundRobin") ?? "Round-robin";
1003
+ const hint = option === "priority" ? t?.("row.distPriorityHint") ?? "" : option === "balanced" ? t?.("row.distBalancedHint") ?? "" : t?.("row.distRoundRobinHint") ?? "";
1004
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1005
+ type: "button",
1006
+ role: "radio",
1007
+ "aria-checked": active,
1008
+ title: hint,
1009
+ disabled: !modelsEditable,
1010
+ className: "dsm-workbuddy-xdpool-dist-option" + (active ? " dsm-workbuddy-xdpool-dist-option-active" : ""),
1011
+ onClick: () => {
1012
+ setDistribution(option);
1013
+ },
1014
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1015
+ className: "dsm-workbuddy-xdpool-dist-option-name",
1016
+ children: label
1017
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1018
+ className: "dsm-workbuddy-xdpool-dist-option-hint",
1019
+ children: hint
1020
+ })]
1021
+ }, option);
1022
+ })
875
1023
  })]
876
1024
  })
877
1025
  ]
@@ -896,31 +1044,74 @@ window.__ModuleLoader__.load({
896
1044
  }) : null]
897
1045
  })]
898
1046
  }),
899
- status?.automation === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1047
+ activeRegion !== "cn" || status?.automation === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
900
1048
  className: "dsm-workbuddy-xdpool-auto",
901
1049
  "aria-label": t?.("row.autoTitle") ?? "Automation",
902
1050
  children: [
903
1051
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
904
1052
  className: "dsm-workbuddy-xdpool-auto-head",
905
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
906
- className: "dsm-workbuddy-xdpool-auto-title",
907
- children: t?.("row.autoTitle") ?? "Automation"
908
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
909
- type: "button",
910
- role: "switch",
911
- "aria-checked": status.automation.enabled,
912
- disabled: !settingsWritable || automationBusy,
913
- className: `dsm-workbuddy-xdpool-auto-switch${status.automation.enabled ? " dsm-workbuddy-xdpool-auto-switch-on" : ""}`,
914
- onClick: () => {
915
- setAutomationEnabled(!status.automation.enabled);
916
- },
917
- children: automationBusy ? t?.("row.autoBusy") ?? "Saving…" : status.automation.enabled ? t?.("row.autoOn") ?? "On" : t?.("row.autoOff") ?? "Off"
918
- })]
1053
+ children: [
1054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1055
+ className: "dsm-workbuddy-xdpool-auto-title",
1056
+ children: t?.("row.autoTitle") ?? "Automation"
1057
+ }),
1058
+ !status.automation.enabled ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1059
+ type: "button",
1060
+ className: "dsm-btn dsm-btn-outline dsm-workbuddy-xdpool-auto-run",
1061
+ disabled: automationRun !== void 0,
1062
+ onClick: () => {
1063
+ runAutomationJob();
1064
+ },
1065
+ children: automationRun !== void 0 ? t?.("row.autoRunning") ?? "Running…" : t?.("row.autoRunAll") ?? "Run all now"
1066
+ }),
1067
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1068
+ type: "button",
1069
+ role: "switch",
1070
+ "aria-checked": status.automation.enabled,
1071
+ disabled: !settingsWritable || automationBusy,
1072
+ className: `dsm-workbuddy-xdpool-auto-switch${status.automation.enabled ? " dsm-workbuddy-xdpool-auto-switch-on" : ""}`,
1073
+ onClick: () => {
1074
+ setAutomationEnabled(!status.automation.enabled);
1075
+ },
1076
+ children: automationBusy ? t?.("row.autoBusy") ?? "Saving…" : status.automation.enabled ? t?.("row.autoOn") ?? "On" : t?.("row.autoOff") ?? "Off"
1077
+ })
1078
+ ]
919
1079
  }),
920
1080
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
921
1081
  className: "dsm-workbuddy-xdpool-auto-hint",
922
1082
  children: status.automation.enabled ? t?.("row.autoHintOn") ?? "Reports activity, claims task rewards and checks in once a day." : t?.("row.autoHintOff") ?? "Off: no background requests are made for you."
923
1083
  }),
1084
+ automationTotals.credit === 0 && automationTotals.checkinCredit === 0 && automationTotals.bonusCredit === 0 && automationTotals.travelCredit === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1085
+ className: "dsm-workbuddy-xdpool-auto-total",
1086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1087
+ className: "dsm-workbuddy-xdpool-auto-total-label",
1088
+ children: t?.("row.autoToday") ?? "Today"
1089
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1090
+ className: "dsm-workbuddy-xdpool-auto-total-list",
1091
+ children: [
1092
+ automationTotals.credit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1093
+ className: "dsm-workbuddy-xdpool-auto-total-row",
1094
+ children: t?.("row.autoFromTasks", {
1095
+ credit: automationTotals.credit,
1096
+ energy: automationTotals.energy,
1097
+ count: automationTotals.claimed
1098
+ }) ?? `Tasks +${automationTotals.credit}`
1099
+ }) : null,
1100
+ automationTotals.checkinCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1101
+ className: "dsm-workbuddy-xdpool-auto-total-row",
1102
+ children: t?.("row.autoFromCheckin", { credit: automationTotals.checkinCredit }) ?? `Check-in +${automationTotals.checkinCredit}`
1103
+ }) : null,
1104
+ automationTotals.bonusCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1105
+ className: "dsm-workbuddy-xdpool-auto-total-row",
1106
+ children: t?.("row.autoFromBonus", { credit: automationTotals.bonusCredit }) ?? `Streak +${automationTotals.bonusCredit}`
1107
+ }) : null,
1108
+ automationTotals.travelCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1109
+ className: "dsm-workbuddy-xdpool-auto-total-row",
1110
+ children: t?.("row.autoFromTravel", { credit: automationTotals.travelCredit }) ?? `Buddy +${automationTotals.travelCredit}`
1111
+ }) : null
1112
+ ]
1113
+ })]
1114
+ }),
924
1115
  status.automation.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
925
1116
  className: "dsm-workbuddy-xdpool-auto-jobs",
926
1117
  children: AUTOMATION_JOBS.map((kind) => {
@@ -1029,6 +1220,9 @@ window.__ModuleLoader__.load({
1029
1220
  onClaimCheckin: (accountId) => {
1030
1221
  claimCheckin(accountId);
1031
1222
  },
1223
+ onSaveCreditReserve: (accountId, reserve) => {
1224
+ saveCreditReserve(accountId, reserve);
1225
+ },
1032
1226
  onToggleDisabled: (accountId, disabled) => {
1033
1227
  toggleAccountDisabled(accountId, disabled);
1034
1228
  },
@@ -1094,7 +1288,7 @@ window.__ModuleLoader__.load({
1094
1288
  });
1095
1289
  }
1096
1290
  /** One account block: label + status tag + meta + optional credit panels. */
1097
- function AccountBlock({ account, t, checkinBusyId, onClaimCheckin, accountBusyId, onToggleDisabled }) {
1291
+ function AccountBlock({ account, t, checkinBusyId, onClaimCheckin, accountBusyId, onToggleDisabled, onSaveCreditReserve, reserveBusyId }) {
1098
1292
  const isDisabled = account.disabled === true;
1099
1293
  const isCooling = account.cooling === true;
1100
1294
  const cooldownUntil = account.cooldownUntil !== void 0 ? Date.parse(account.cooldownUntil) : void 0;
@@ -1131,38 +1325,78 @@ window.__ModuleLoader__.load({
1131
1325
  ]
1132
1326
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1133
1327
  className: "dsm-workbuddy-xdpool-account-body",
1134
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1135
- className: "dsm-workbuddy-xdpool-account-copy",
1136
- children: [
1137
- account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1138
- className: "dsm-workbuddy-xdpool-account-meta",
1139
- children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
1140
- }) : null,
1141
- isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1142
- className: "dsm-workbuddy-xdpool-account-meta",
1328
+ children: [
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1330
+ className: "dsm-workbuddy-xdpool-account-copy",
1331
+ children: [
1332
+ account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1333
+ className: "dsm-workbuddy-xdpool-account-meta",
1334
+ children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
1335
+ }) : null,
1336
+ isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1337
+ className: "dsm-workbuddy-xdpool-account-meta",
1338
+ children: [
1339
+ t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
1340
+ " · ",
1341
+ t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
1342
+ ]
1343
+ }) : null,
1344
+ modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1345
+ className: "dsm-workbuddy-xdpool-account-modelcool",
1346
+ children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1347
+ className: "dsm-workbuddy-xdpool-account-modelcool-chip",
1348
+ children: t?.("row.modelCooling", {
1349
+ model: mc.modelId,
1350
+ time: formatDateTime(mc.until)
1351
+ }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
1352
+ }, mc.modelId))
1353
+ }) : null
1354
+ ]
1355
+ }),
1356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountStats, {
1357
+ account,
1358
+ t,
1359
+ checkinBusy: checkinBusyId === account.id,
1360
+ onClaim: onClaimCheckin
1361
+ }),
1362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditReserveRow, {
1363
+ account,
1364
+ t,
1365
+ busy: reserveBusyId === account.id,
1366
+ onSave: onSaveCreditReserve
1367
+ }),
1368
+ account.automationToday === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1369
+ className: "dsm-workbuddy-xdpool-earned",
1370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1371
+ className: "dsm-workbuddy-xdpool-earned-label",
1372
+ children: t?.("row.autoEarned") ?? "Automation today"
1373
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1374
+ className: "dsm-workbuddy-xdpool-earned-list",
1143
1375
  children: [
1144
- t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
1145
- " · ",
1146
- t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
1376
+ account.automationToday.credit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1377
+ className: "dsm-workbuddy-xdpool-earned-row",
1378
+ children: t?.("row.autoFromTasks", {
1379
+ credit: account.automationToday.credit,
1380
+ energy: account.automationToday.energy,
1381
+ count: account.automationToday.claimed
1382
+ }) ?? `Tasks +${account.automationToday.credit}`
1383
+ }) : null,
1384
+ account.automationToday.checkinCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1385
+ className: "dsm-workbuddy-xdpool-earned-row",
1386
+ children: t?.("row.autoFromCheckin", { credit: account.automationToday.checkinCredit }) ?? `Check-in +${account.automationToday.checkinCredit}`
1387
+ }) : null,
1388
+ account.automationToday.bonusCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1389
+ className: "dsm-workbuddy-xdpool-earned-row",
1390
+ children: t?.("row.autoFromBonus", { credit: account.automationToday.bonusCredit }) ?? `Streak +${account.automationToday.bonusCredit}`
1391
+ }) : null,
1392
+ account.automationToday.travelCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1393
+ className: "dsm-workbuddy-xdpool-earned-row",
1394
+ children: t?.("row.autoFromTravel", { credit: account.automationToday.travelCredit }) ?? `Buddy +${account.automationToday.travelCredit}`
1395
+ }) : null
1147
1396
  ]
1148
- }) : null,
1149
- modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1150
- className: "dsm-workbuddy-xdpool-account-modelcool",
1151
- children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1152
- className: "dsm-workbuddy-xdpool-account-modelcool-chip",
1153
- children: t?.("row.modelCooling", {
1154
- model: mc.modelId,
1155
- time: formatDateTime(mc.until)
1156
- }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
1157
- }, mc.modelId))
1158
- }) : null
1159
- ]
1160
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountStats, {
1161
- account,
1162
- t,
1163
- checkinBusy: checkinBusyId === account.id,
1164
- onClaim: onClaimCheckin
1165
- })]
1397
+ })]
1398
+ })
1399
+ ]
1166
1400
  })]
1167
1401
  });
1168
1402
  }
@@ -1177,6 +1411,61 @@ window.__ModuleLoader__.load({
1177
1411
  * layout the LaoDing plugin family uses, so the numbers stay scannable and the
1178
1412
  * claim button sits where the eye already is.
1179
1413
  */
1414
+ /**
1415
+ * Reserved-credit control for one account.
1416
+ *
1417
+ * The value is committed on blur or Enter rather than on every keystroke:
1418
+ * each save is a settings write plus a status refresh, and a per-character
1419
+ * save would hammer both.
1420
+ */
1421
+ function CreditReserveRow({ account, t, busy, onSave }) {
1422
+ const [draft, setDraft] = (0, react.useState)(String(account.creditReserve ?? 0));
1423
+ (0, react.useEffect)(() => {
1424
+ setDraft(String(account.creditReserve ?? 0));
1425
+ }, [account.creditReserve]);
1426
+ const commit = () => {
1427
+ const parsed = Number.parseInt(draft, 10);
1428
+ const next = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
1429
+ if (next === (account.creditReserve ?? 0)) {
1430
+ setDraft(String(next));
1431
+ return;
1432
+ }
1433
+ onSave(account.id, next);
1434
+ };
1435
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
+ className: "dsm-workbuddy-xdpool-reserve",
1437
+ children: [
1438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1439
+ className: "dsm-workbuddy-xdpool-reserve-label",
1440
+ children: t?.("row.reserveTitle") ?? "Keep at least"
1441
+ }),
1442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1443
+ type: "number",
1444
+ min: 0,
1445
+ step: 1,
1446
+ value: draft,
1447
+ disabled: busy,
1448
+ className: "dsm-workbuddy-xdpool-reserve-input",
1449
+ "aria-label": t?.("row.reserveTitle") ?? "Keep at least",
1450
+ onChange: (event) => {
1451
+ setDraft(event.target.value);
1452
+ },
1453
+ onBlur: commit,
1454
+ onKeyDown: (event) => {
1455
+ if (event.key === "Enter") event.target.blur();
1456
+ }
1457
+ }),
1458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1459
+ className: "dsm-workbuddy-xdpool-reserve-unit",
1460
+ children: busy ? t?.("row.reserveSaving") ?? "Saving…" : t?.("row.reserveUnit") ?? "credits"
1461
+ }),
1462
+ account.reserved === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1463
+ className: "dsm-workbuddy-xdpool-reserve-badge",
1464
+ children: t?.("row.reserveHolding") ?? "Reserved: skipped"
1465
+ }) : null
1466
+ ]
1467
+ });
1468
+ }
1180
1469
  function AccountStats({ account, t, checkinBusy, onClaim }) {
1181
1470
  const credits = account.credits;
1182
1471
  const checkin = account.checkin;
@@ -1461,14 +1750,14 @@ window.__ModuleLoader__.load({
1461
1750
  "row.imageCapable": "image input",
1462
1751
  "row.rate": "{rate}x credits",
1463
1752
  "row.accountsRescan": "Detect accounts again",
1753
+ "row.accountsScanning": "Detecting…",
1464
1754
  "row.distTitle": "Account usage",
1465
1755
  "row.distPriority": "Priority",
1466
- "row.distPriorityHint": "Drain one account before moving to the next",
1756
+ "row.distPriorityHint": "Use one account until it runs out, then move to the next",
1467
1757
  "row.distRoundRobin": "Round-robin",
1468
- "row.distRoundRobinHint": "Spread the spend evenly across accounts",
1758
+ "row.distRoundRobinHint": "Take turns in order, spreading the spend evenly",
1469
1759
  "row.distBalanced": "Balanced",
1470
- "row.distBalancedHint": "Draw from every account, favouring whichever has been idle longest",
1471
- "row.accountsScanning": "Detecting…",
1760
+ "row.distBalancedHint": "Draw at random, favouring the account idle longest",
1472
1761
  "row.resetCooldowns": "Clear all cooldowns",
1473
1762
  "row.resetCooldownsBusy": "Clearing…",
1474
1763
  "row.resetCooldownsDone": "Cooldowns cleared",
@@ -1485,7 +1774,30 @@ window.__ModuleLoader__.load({
1485
1774
  "row.autoJob_checkin": "Daily check-in",
1486
1775
  "row.autoJob_streak": "Streak bonus",
1487
1776
  "row.autoHourNone": "not scheduled",
1488
- "row.autoNever": "not run yet"
1777
+ "row.autoNever": "not run yet",
1778
+ "row.autoRun": "Run now",
1779
+ "row.autoRunning": "Running…",
1780
+ "row.autoRan": "Done: {count} account(s) ok, {failed} failed",
1781
+ "row.autoRanTasks": "Done: {claimed} task(s) claimed, +{credit} credits, +{energy} energy",
1782
+ "row.reserveTitle": "Keep at least",
1783
+ "row.reserveUnit": "credits",
1784
+ "row.reserveSaving": "Saving…",
1785
+ "row.reserveHolding": "Reserved: skipped",
1786
+ "row.reserveSaved": "Keeping {credits} credits",
1787
+ "row.reserveCleared": "Reserve cleared",
1788
+ "row.autoToday": "Today",
1789
+ "row.autoEarned": "Automation today",
1790
+ "row.autoEnergy": "energy",
1791
+ "row.autoTasksClaimed": "{count} task(s)",
1792
+ "row.autoRunAll": "Run now",
1793
+ "row.autoRunDone": "Automation pass finished",
1794
+ "row.autoRunTimeout": "Still running; check back in a moment",
1795
+ "row.autoAlreadyRunning": "A run is already in progress",
1796
+ "row.autoRanAll": "Ran {jobs} job(s), {ok} ok, {failed} failed",
1797
+ "row.autoFromTasks": "Tasks +{credit} credits · +{energy} energy · {count} task(s)",
1798
+ "row.autoFromCheckin": "Check-in +{credit} credits",
1799
+ "row.autoFromBonus": "Streak bonus +{credit} credits",
1800
+ "row.autoFromTravel": "Buddy travel +{credit} credits"
1489
1801
  };
1490
1802
  const zh = {
1491
1803
  "row.title": "WorkBuddy 池(dsh-workbuddy-xdpool)",
@@ -1566,14 +1878,14 @@ window.__ModuleLoader__.load({
1566
1878
  "row.imageCapable": "图片输入",
1567
1879
  "row.rate": "{rate}x 积分",
1568
1880
  "row.accountsRescan": "重新检测账号",
1881
+ "row.accountsScanning": "正在检测…",
1569
1882
  "row.distTitle": "账号使用方式",
1570
1883
  "row.distPriority": "优先模式",
1571
- "row.distPriorityHint": "优先用完一个账号再换下一个",
1884
+ "row.distPriorityHint": "先用完一个账号,用完再换下一个",
1572
1885
  "row.distRoundRobin": "轮换模式",
1573
- "row.distRoundRobinHint": "积分平均分摊到各账号",
1574
- "row.distBalanced": "均衡",
1575
- "row.distBalancedHint": "每个账号都参与抽取,闲置越久权重越高",
1576
- "row.accountsScanning": "正在检测…",
1886
+ "row.distRoundRobinHint": "按顺序轮流使用,积分均匀分摊",
1887
+ "row.distBalanced": "均衡模式",
1888
+ "row.distBalancedHint": "随机抽取,闲置越久的账号被选中概率越高",
1577
1889
  "row.resetCooldowns": "清除所有冷却",
1578
1890
  "row.resetCooldownsBusy": "正在清除…",
1579
1891
  "row.resetCooldownsDone": "冷却已清除",
@@ -1590,7 +1902,30 @@ window.__ModuleLoader__.load({
1590
1902
  "row.autoJob_checkin": "每日签到",
1591
1903
  "row.autoJob_streak": "连登奖励",
1592
1904
  "row.autoHourNone": "未排程",
1593
- "row.autoNever": "尚未执行"
1905
+ "row.autoNever": "尚未执行",
1906
+ "row.autoRun": "立即运行",
1907
+ "row.autoRunning": "运行中…",
1908
+ "row.autoRan": "完成:{count} 个账号正常,{failed} 个失败",
1909
+ "row.autoRanTasks": "完成:领取 {claimed} 个任务,+{credit} 积分,+{energy} 能量",
1910
+ "row.reserveTitle": "保留积分",
1911
+ "row.reserveUnit": "积分",
1912
+ "row.reserveSaving": "保存中…",
1913
+ "row.reserveHolding": "已保留·暂停使用",
1914
+ "row.reserveSaved": "已保留 {credits} 积分",
1915
+ "row.reserveCleared": "已取消保留",
1916
+ "row.autoToday": "今日自动化",
1917
+ "row.autoEarned": "今日自动化获得",
1918
+ "row.autoEnergy": "能量",
1919
+ "row.autoTasksClaimed": "{count} 个任务",
1920
+ "row.autoRunAll": "立即运行",
1921
+ "row.autoRunDone": "自动化已执行完成",
1922
+ "row.autoRunTimeout": "仍在执行中,稍后查看结果",
1923
+ "row.autoAlreadyRunning": "已有一次执行正在进行",
1924
+ "row.autoRanAll": "已运行 {jobs} 项,{ok} 个正常,{failed} 个失败",
1925
+ "row.autoFromTasks": "任务 +{credit} 积分 · +{energy} 能量 · {count} 个",
1926
+ "row.autoFromCheckin": "签到 +{credit} 积分",
1927
+ "row.autoFromBonus": "连登奖励 +{credit} 积分",
1928
+ "row.autoFromTravel": "猫猫旅行 +{credit} 积分"
1594
1929
  };
1595
1930
  //#endregion
1596
1931
  //#region src/client/index.tsx