@linxin666/dsh-client-ui-task-board 0.3.2 → 0.3.4

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
@@ -451,10 +451,30 @@ window.__ModuleLoader__.load({
451
451
  * fresh updatedAt. Pure ledger transition (no persistence or notify — the
452
452
  * controller orchestrates those).
453
453
  *
454
- * An explicit `undefined` in the patch clears the field (the task falls
455
- * back to the runtime default); an unknown permission string is ignored so
456
- * stale UI can never persist a value the execution service rejects.
454
+ * An explicit `undefined` in the patch clears an execution-target field
455
+ * (the task falls back to the runtime default); content fields are trimmed
456
+ * and never cleared. An unknown permission string is ignored so stale UI
457
+ * can never persist a value the execution service rejects.
457
458
  */
459
+ /**
460
+ * The fields that edit the task's content (what the user reads and what the
461
+ * next execution sends). Unlike the execution targets they stay editable only
462
+ * while the task has never started executing — after the first run the
463
+ * recorded prompt is the record of what actually ran, so it becomes read-only.
464
+ */
465
+ const TASK_CONTENT_FIELDS = [
466
+ "title",
467
+ "description",
468
+ "prompt"
469
+ ];
470
+ /**
471
+ * Whether a task's content may still be edited: the task must be on-board
472
+ * (not archived) and must never have started executing. Fail-closed: a
473
+ * running, settled, or cancelled-before-launch task keeps its content fixed.
474
+ */
475
+ function canEditTaskContent(task) {
476
+ return task.archivedAt === void 0 && task.status !== "running" && task.executions.length === 0;
477
+ }
458
478
  /** Keep an unknown permission string from entering the ledger. */
459
479
  function normalizePermission(current, value) {
460
480
  if (value === void 0) return void 0;
@@ -479,6 +499,11 @@ window.__ModuleLoader__.load({
479
499
  ...patch,
480
500
  updatedAt: now
481
501
  };
502
+ for (const field of TASK_CONTENT_FIELDS) {
503
+ if (!(field in patch)) continue;
504
+ const value = patch[field];
505
+ next[field] = value === void 0 ? task[field] : value.trim();
506
+ }
482
507
  if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
483
508
  if (mode !== void 0 || "mode" in patch) next.mode = mode;
484
509
  if (permission !== void 0 || "permission" in patch) next.permission = permission;
@@ -641,17 +666,23 @@ window.__ModuleLoader__.load({
641
666
  input
642
667
  }, id) ? this.tasks.find((task) => task.id === id) : void 0;
643
668
  }
644
- updateTask(id, patch) {
645
- if (this.deps.transport !== void 0) {
646
- this.commitRemote({
647
- kind: "update",
648
- taskId: id,
649
- patch
650
- }, id);
651
- return;
652
- }
669
+ /**
670
+ * Apply an editable-field patch (task content + execution targets).
671
+ * Host-backed: the Host ledger owns the fail-closed checks (the content of
672
+ * an executed task is read-only) and confirms the mutation, so the resolved
673
+ * value reflects whether the Host accepted it; the legacy in-memory path
674
+ * applies and persists synchronously.
675
+ * @returns true when the patch was accepted by the authority.
676
+ */
677
+ async updateTask(id, patch) {
678
+ if (this.deps.transport !== void 0) return await this.commitRemote({
679
+ kind: "update",
680
+ taskId: id,
681
+ patch
682
+ }, id);
653
683
  this.tasks = [...applyUpdateTask(this.tasks, id, patch, this.now())];
654
684
  this.persistAndNotify();
685
+ return true;
655
686
  }
656
687
  /**
657
688
  * Replace (a part of) the picker option sets the UI feeds (workspace list
@@ -1143,7 +1174,10 @@ window.__ModuleLoader__.load({
1143
1174
  "new.cancel": "取消",
1144
1175
  "new.required": "标题不能为空",
1145
1176
  "detail.title": "任务详情",
1177
+ "detail.edit": "编辑",
1146
1178
  "detail.close": "关闭",
1179
+ "edit.title": "编辑任务",
1180
+ "edit.save": "保存",
1147
1181
  "detail.prompt": "执行 Prompt",
1148
1182
  "detail.description": "描述",
1149
1183
  "detail.execution": "执行记录",
@@ -1264,7 +1298,10 @@ window.__ModuleLoader__.load({
1264
1298
  "new.cancel": "Cancel",
1265
1299
  "new.required": "Title is required",
1266
1300
  "detail.title": "Task Detail",
1301
+ "detail.edit": "Edit",
1267
1302
  "detail.close": "Close",
1303
+ "edit.title": "Edit Task",
1304
+ "edit.save": "Save",
1268
1305
  "detail.prompt": "Run Prompt",
1269
1306
  "detail.description": "Description",
1270
1307
  "detail.execution": "Execution History",
@@ -1877,6 +1914,125 @@ window.__ModuleLoader__.load({
1877
1914
  });
1878
1915
  }
1879
1916
  //#endregion
1917
+ //#region src/client/board/EditTaskModal.tsx
1918
+ /**
1919
+ * Edit-task modal: title + description + the prompt the next execution will
1920
+ * send, pre-filled from the task. Shown only for tasks that have never
1921
+ * started executing (the detail view gates on canEditTaskContent); the Host
1922
+ * still re-checks at submit, so a task that started running while the modal
1923
+ * was open fails closed and the error surfaces here.
1924
+ */
1925
+ /** Edit-task form overlay. */
1926
+ function EditTaskModal({ controller, task, onClose }) {
1927
+ const [title, setTitle] = (0, react.useState)(task.title);
1928
+ const [description, setDescription] = (0, react.useState)(task.description);
1929
+ const [prompt, setPrompt] = (0, react.useState)(task.prompt);
1930
+ const [error, setError] = (0, react.useState)(void 0);
1931
+ const [pending, setPending] = (0, react.useState)(false);
1932
+ const submit = async () => {
1933
+ if (title.trim() === "") {
1934
+ setError(t("new.required"));
1935
+ return;
1936
+ }
1937
+ setPending(true);
1938
+ if (await controller.updateTask(task.id, {
1939
+ title,
1940
+ description,
1941
+ prompt
1942
+ })) {
1943
+ onClose();
1944
+ return;
1945
+ }
1946
+ setPending(false);
1947
+ setError(controller.getSnapshot().transportError ?? t("new.required"));
1948
+ };
1949
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1950
+ className: board_module_css_default.modalBackdrop,
1951
+ onMouseDown: (event) => {
1952
+ if (event.target === event.currentTarget) onClose();
1953
+ },
1954
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1955
+ className: board_module_css_default.modal,
1956
+ role: "dialog",
1957
+ "aria-label": t("edit.title"),
1958
+ onSubmit: (event) => {
1959
+ event.preventDefault();
1960
+ submit();
1961
+ },
1962
+ children: [
1963
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1964
+ className: board_module_css_default.modalTitle,
1965
+ children: t("edit.title")
1966
+ }),
1967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1968
+ className: board_module_css_default.field,
1969
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1970
+ className: board_module_css_default.fieldLabel,
1971
+ children: t("new.title")
1972
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1973
+ className: board_module_css_default.input,
1974
+ value: title,
1975
+ autoFocus: true,
1976
+ placeholder: t("new.titlePlaceholder"),
1977
+ onChange: (event) => {
1978
+ setTitle(event.target.value);
1979
+ setError(void 0);
1980
+ }
1981
+ })]
1982
+ }),
1983
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1984
+ className: board_module_css_default.field,
1985
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1986
+ className: board_module_css_default.fieldLabel,
1987
+ children: t("new.description")
1988
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1989
+ className: board_module_css_default.input,
1990
+ rows: 3,
1991
+ value: description,
1992
+ placeholder: t("new.descriptionPlaceholder"),
1993
+ onChange: (event) => {
1994
+ setDescription(event.target.value);
1995
+ }
1996
+ })]
1997
+ }),
1998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1999
+ className: board_module_css_default.field,
2000
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2001
+ className: board_module_css_default.fieldLabel,
2002
+ children: t("new.prompt")
2003
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2004
+ className: board_module_css_default.input,
2005
+ rows: 4,
2006
+ value: prompt,
2007
+ placeholder: t("new.promptPlaceholder"),
2008
+ onChange: (event) => {
2009
+ setPrompt(event.target.value);
2010
+ }
2011
+ })]
2012
+ }),
2013
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2014
+ className: board_module_css_default.formError,
2015
+ children: error
2016
+ }),
2017
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2018
+ className: board_module_css_default.modalFooter,
2019
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2020
+ type: "button",
2021
+ className: board_module_css_default.ghostButton,
2022
+ onClick: onClose,
2023
+ children: t("new.cancel")
2024
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2025
+ type: "submit",
2026
+ className: board_module_css_default.primaryButton,
2027
+ disabled: pending,
2028
+ children: t("edit.save")
2029
+ })]
2030
+ })
2031
+ ]
2032
+ })
2033
+ });
2034
+ }
2035
+ //#endregion
1880
2036
  //#region src/client/board/TaskDetail.tsx
1881
2037
  /**
1882
2038
  * Task detail: the full view of one task — content, prompt, execution
@@ -2156,10 +2312,14 @@ window.__ModuleLoader__.load({
2156
2312
  /** Task detail overlay. */
2157
2313
  function TaskDetail({ controller, task }) {
2158
2314
  const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
2315
+ const [showEdit, setShowEdit] = (0, react.useState)(false);
2159
2316
  const [latest, setLatest] = (0, react.useState)(task);
2160
2317
  (0, react.useEffect)(() => {
2161
2318
  setLatest(task);
2162
2319
  }, [task]);
2320
+ (0, react.useEffect)(() => {
2321
+ setShowEdit(false);
2322
+ }, [task.id]);
2163
2323
  const current = latest;
2164
2324
  const snapshot = controller.getSnapshot();
2165
2325
  const running = current.status === "running";
@@ -2172,177 +2332,196 @@ window.__ModuleLoader__.load({
2172
2332
  onMouseDown: (event) => {
2173
2333
  if (event.target === event.currentTarget) controller.closeTask();
2174
2334
  },
2175
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2176
- className: board_module_css_default.detail,
2177
- role: "dialog",
2178
- "aria-label": t("detail.title"),
2179
- children: [
2180
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2181
- className: board_module_css_default.detailHeader,
2182
- children: [
2183
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2184
- className: board_module_css_default.detailTitle,
2185
- children: current.title
2186
- }),
2187
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2188
- className: board_module_css_default.statusBadge,
2189
- "data-status": archived ? "archived" : current.status,
2190
- children: archived ? t("board.archive") : t(STATUS_KEY[current.status])
2191
- }),
2192
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2193
- type: "button",
2194
- className: board_module_css_default.iconButton,
2195
- "aria-label": t("detail.close"),
2196
- onClick: () => {
2197
- controller.closeTask();
2198
- },
2199
- children: "×"
2200
- })
2201
- ]
2202
- }),
2203
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
- className: board_module_css_default.detailBody,
2205
- children: [
2206
- transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2207
- className: board_module_css_default.formError,
2208
- children: [
2209
- t("board.hostError", { error: transportError }),
2210
- " ",
2211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2212
- type: "button",
2213
- className: board_module_css_default.linkButton,
2214
- onClick: () => {
2215
- controller.retryHostSync();
2216
- },
2217
- children: t("board.retryHost")
2218
- })
2219
- ]
2220
- }),
2221
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2222
- className: board_module_css_default.detailSection,
2223
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.description") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2224
- className: board_module_css_default.detailText,
2225
- children: current.description !== "" ? current.description : "—"
2226
- })]
2227
- }),
2228
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2229
- className: board_module_css_default.detailSection,
2230
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.prompt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2231
- className: board_module_css_default.promptBlock,
2232
- children: current.prompt !== "" ? current.prompt : current.title
2233
- })]
2234
- }),
2235
- !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
2236
- controller,
2237
- task: current,
2238
- pending
2239
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
2240
- controller,
2241
- task: current,
2242
- pending
2243
- })] }),
2244
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2245
- className: board_module_css_default.detailSection,
2246
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.execution") }), current.executions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2247
- className: board_module_css_default.detailText,
2248
- children: t("detail.noExecution")
2249
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2250
- className: board_module_css_default.executionList,
2251
- children: [...current.executions].reverse().map((execution) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionRow, {
2252
- execution,
2253
- timeZone,
2254
- onOpen: (sessionId) => {
2255
- controller.openSession(sessionId);
2256
- }
2257
- }, execution.id))
2258
- })]
2259
- }),
2260
- !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2261
- className: board_module_css_default.detailSection,
2262
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("board.status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2263
- className: board_module_css_default.moveRow,
2264
- children: MANUAL_STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2265
- type: "button",
2266
- className: board_module_css_default.ghostButton,
2267
- disabled: current.status === status || running || pending,
2268
- onClick: () => {
2269
- controller.moveTask(current.id, status);
2270
- },
2271
- children: t(`status.move.${status}`)
2272
- }, status))
2273
- })]
2274
- })
2275
- ]
2276
- }),
2277
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2278
- className: board_module_css_default.detailFooter,
2279
- children: [
2280
- !archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2281
- className: board_module_css_default.detailMeta,
2282
- children: [t("board.pending"), "…"]
2283
- }),
2284
- !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2285
- type: "button",
2286
- className: board_module_css_default.primaryButton,
2287
- disabled: running || pending,
2288
- onClick: () => {
2289
- controller.rerunTask(current.id).then(() => {
2290
- if (controller.getSnapshot().transportError === void 0) controller.closeTask();
2291
- });
2292
- },
2293
- children: current.executions.length === 0 ? t("detail.run") : t("detail.rerun")
2294
- }),
2295
- archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2296
- type: "button",
2297
- className: board_module_css_default.primaryButton,
2298
- disabled: pending,
2299
- onClick: () => {
2300
- controller.restoreTask(current.id);
2301
- },
2302
- children: t("detail.restore")
2303
- }) : (current.status === "done" || current.status === "failed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2304
- type: "button",
2305
- className: board_module_css_default.ghostButton,
2306
- disabled: pending,
2307
- onClick: () => {
2308
- controller.archiveTask(current.id);
2309
- },
2310
- children: t("detail.archive")
2311
- }),
2312
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2313
- type: "button",
2314
- className: board_module_css_default.dangerButton,
2315
- disabled: pending,
2316
- onClick: () => {
2317
- setConfirmDelete(true);
2318
- },
2319
- children: t("detail.delete")
2320
- }),
2321
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2322
- className: board_module_css_default.detailMeta,
2323
- children: [
2324
- t("board.created"),
2325
- " ",
2326
- formatTime(current.createdAt, timeZone),
2327
- archived && ` · ${t("detail.archivedAt", { time: formatTime(current.archivedAt, timeZone) })}`
2328
- ]
2329
- })
2330
- ]
2331
- })
2332
- ]
2333
- }), confirmDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
2334
- title: t("delete.title"),
2335
- message: t("delete.confirm", { name: current.title }),
2336
- confirmLabel: t("delete.ok"),
2337
- danger: true,
2338
- onCancel: () => {
2339
- setConfirmDelete(false);
2340
- },
2341
- onConfirm: () => {
2342
- setConfirmDelete(false);
2343
- controller.deleteTask(current.id);
2344
- }
2345
- })]
2335
+ children: [
2336
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2337
+ className: board_module_css_default.detail,
2338
+ role: "dialog",
2339
+ "aria-label": t("detail.title"),
2340
+ children: [
2341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2342
+ className: board_module_css_default.detailHeader,
2343
+ children: [
2344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2345
+ className: board_module_css_default.detailTitle,
2346
+ children: current.title
2347
+ }),
2348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2349
+ className: board_module_css_default.statusBadge,
2350
+ "data-status": archived ? "archived" : current.status,
2351
+ children: archived ? t("board.archive") : t(STATUS_KEY[current.status])
2352
+ }),
2353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2354
+ type: "button",
2355
+ className: board_module_css_default.iconButton,
2356
+ "aria-label": t("detail.close"),
2357
+ onClick: () => {
2358
+ controller.closeTask();
2359
+ },
2360
+ children: "×"
2361
+ })
2362
+ ]
2363
+ }),
2364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2365
+ className: board_module_css_default.detailBody,
2366
+ children: [
2367
+ transportError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2368
+ className: board_module_css_default.formError,
2369
+ children: [
2370
+ t("board.hostError", { error: transportError }),
2371
+ " ",
2372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2373
+ type: "button",
2374
+ className: board_module_css_default.linkButton,
2375
+ onClick: () => {
2376
+ controller.retryHostSync();
2377
+ },
2378
+ children: t("board.retryHost")
2379
+ })
2380
+ ]
2381
+ }),
2382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2383
+ className: board_module_css_default.detailSection,
2384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.description") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2385
+ className: board_module_css_default.detailText,
2386
+ children: current.description !== "" ? current.description : "—"
2387
+ })]
2388
+ }),
2389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2390
+ className: board_module_css_default.detailSection,
2391
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.prompt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
2392
+ className: board_module_css_default.promptBlock,
2393
+ children: current.prompt !== "" ? current.prompt : current.title
2394
+ })]
2395
+ }),
2396
+ !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionSettingsSection, {
2397
+ controller,
2398
+ task: current,
2399
+ pending
2400
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScheduleSection, {
2401
+ controller,
2402
+ task: current,
2403
+ pending
2404
+ })] }),
2405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2406
+ className: board_module_css_default.detailSection,
2407
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("detail.execution") }), current.executions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2408
+ className: board_module_css_default.detailText,
2409
+ children: t("detail.noExecution")
2410
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2411
+ className: board_module_css_default.executionList,
2412
+ children: [...current.executions].reverse().map((execution) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExecutionRow, {
2413
+ execution,
2414
+ timeZone,
2415
+ onOpen: (sessionId) => {
2416
+ controller.openSession(sessionId);
2417
+ }
2418
+ }, execution.id))
2419
+ })]
2420
+ }),
2421
+ !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2422
+ className: board_module_css_default.detailSection,
2423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("board.status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2424
+ className: board_module_css_default.moveRow,
2425
+ children: MANUAL_STATUSES.map((status) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2426
+ type: "button",
2427
+ className: board_module_css_default.ghostButton,
2428
+ disabled: current.status === status || running || pending,
2429
+ onClick: () => {
2430
+ controller.moveTask(current.id, status);
2431
+ },
2432
+ children: t(`status.move.${status}`)
2433
+ }, status))
2434
+ })]
2435
+ })
2436
+ ]
2437
+ }),
2438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
2439
+ className: board_module_css_default.detailFooter,
2440
+ children: [
2441
+ !archived && pending && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2442
+ className: board_module_css_default.detailMeta,
2443
+ children: [t("board.pending"), "…"]
2444
+ }),
2445
+ !archived && canEditTaskContent(current) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2446
+ type: "button",
2447
+ className: board_module_css_default.ghostButton,
2448
+ disabled: pending,
2449
+ onClick: () => {
2450
+ setShowEdit(true);
2451
+ },
2452
+ children: t("detail.edit")
2453
+ }),
2454
+ !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2455
+ type: "button",
2456
+ className: board_module_css_default.primaryButton,
2457
+ disabled: running || pending,
2458
+ onClick: () => {
2459
+ controller.rerunTask(current.id).then(() => {
2460
+ if (controller.getSnapshot().transportError === void 0) controller.closeTask();
2461
+ });
2462
+ },
2463
+ children: current.executions.length === 0 ? t("detail.run") : t("detail.rerun")
2464
+ }),
2465
+ archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2466
+ type: "button",
2467
+ className: board_module_css_default.primaryButton,
2468
+ disabled: pending,
2469
+ onClick: () => {
2470
+ controller.restoreTask(current.id);
2471
+ },
2472
+ children: t("detail.restore")
2473
+ }) : (current.status === "done" || current.status === "failed") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2474
+ type: "button",
2475
+ className: board_module_css_default.ghostButton,
2476
+ disabled: pending,
2477
+ onClick: () => {
2478
+ controller.archiveTask(current.id);
2479
+ },
2480
+ children: t("detail.archive")
2481
+ }),
2482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2483
+ type: "button",
2484
+ className: board_module_css_default.dangerButton,
2485
+ disabled: pending,
2486
+ onClick: () => {
2487
+ setConfirmDelete(true);
2488
+ },
2489
+ children: t("detail.delete")
2490
+ }),
2491
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2492
+ className: board_module_css_default.detailMeta,
2493
+ children: [
2494
+ t("board.created"),
2495
+ " ",
2496
+ formatTime(current.createdAt, timeZone),
2497
+ archived && ` · ${t("detail.archivedAt", { time: formatTime(current.archivedAt, timeZone) })}`
2498
+ ]
2499
+ })
2500
+ ]
2501
+ })
2502
+ ]
2503
+ }),
2504
+ confirmDelete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
2505
+ title: t("delete.title"),
2506
+ message: t("delete.confirm", { name: current.title }),
2507
+ confirmLabel: t("delete.ok"),
2508
+ danger: true,
2509
+ onCancel: () => {
2510
+ setConfirmDelete(false);
2511
+ },
2512
+ onConfirm: () => {
2513
+ setConfirmDelete(false);
2514
+ controller.deleteTask(current.id);
2515
+ }
2516
+ }),
2517
+ showEdit && !archived && canEditTaskContent(current) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditTaskModal, {
2518
+ controller,
2519
+ task: current,
2520
+ onClose: () => {
2521
+ setShowEdit(false);
2522
+ }
2523
+ })
2524
+ ]
2346
2525
  });
2347
2526
  }
2348
2527
  //#endregion
@@ -2767,7 +2946,7 @@ window.__ModuleLoader__.load({
2767
2946
  }
2768
2947
  //#endregion
2769
2948
  //#region \0dsh-css:packages/dsh-task-board/src/client/settings-card.module.css.mjs
2770
- const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
2949
+ const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;box-sizing:border-box;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{box-sizing:border-box;border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
2771
2950
  const tagId = "@linxin666/dsh-client-ui-task-board/settings-card.module.css";
2772
2951
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2773
2952
  const tag = document.createElement("style");
@@ -2852,7 +3031,7 @@ window.__ModuleLoader__.load({
2852
3031
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2853
3032
  className: settings_card_module_css_default.description,
2854
3033
  title: description,
2855
- children: description
3034
+ children: props.descriptionNode ?? description
2856
3035
  })]
2857
3036
  }), state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2858
3037
  className: settings_card_module_css_default.pending,
@@ -2877,7 +3056,7 @@ window.__ModuleLoader__.load({
2877
3056
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2878
3057
  className: settings_card_module_css_default.description,
2879
3058
  title: description,
2880
- children: description
3059
+ children: props.descriptionNode ?? description
2881
3060
  })]
2882
3061
  }),
2883
3062
  state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3298,7 +3477,7 @@ window.__ModuleLoader__.load({
3298
3477
  /**
3299
3478
  * Write every staged edit, then re-seed from what the Host accepted.
3300
3479
  *
3301
- * When the scope carries the optional batch surface (the dsh-web-ui
3480
+ * When the scope carries the optional batch surface (the dsh-web
3302
3481
  * bridge scope), every planned write rides one mutation so cross-field
3303
3482
  * validate hooks (baseURL+model) judge the batch as a unit instead of
3304
3483
  * deadlocking on per-field writes. Otherwise the per-field loop runs.
@@ -3668,6 +3847,76 @@ window.__ModuleLoader__.load({
3668
3847
  }
3669
3848
  };
3670
3849
  //#endregion
3850
+ //#region src/client/telemetry.ts
3851
+ const VISITOR_KEY = "dsh-web-ui-telemetry-visitor";
3852
+ const DAY_KEY_PREFIX = "dsh-web-ui-telemetry-day:";
3853
+ const ENDPOINT = "https://dsh-market.com/api/telemetry/event";
3854
+ /** The building package's version, when the bundle carries it. */
3855
+ function bakedVersion() {
3856
+ try {
3857
+ return "0.3.4";
3858
+ } catch {
3859
+ return;
3860
+ }
3861
+ }
3862
+ /** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
3863
+ function visitorId() {
3864
+ try {
3865
+ const existing = localStorage.getItem(VISITOR_KEY);
3866
+ if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing;
3867
+ const fresh = crypto.randomUUID().replaceAll("-", "");
3868
+ localStorage.setItem(VISITOR_KEY, fresh);
3869
+ return fresh;
3870
+ } catch {
3871
+ return null;
3872
+ }
3873
+ }
3874
+ /** Drop stale per-day dedup keys so localStorage does not grow forever. */
3875
+ function pruneDayKeys(today) {
3876
+ try {
3877
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
3878
+ const key = localStorage.key(index);
3879
+ if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) localStorage.removeItem(key);
3880
+ }
3881
+ } catch {}
3882
+ }
3883
+ /**
3884
+ * Fire the daily heartbeat for the given items at most once per UTC day per
3885
+ * browser. Never throws and never blocks the caller. Items without an explicit
3886
+ * version inherit the bundle's baked build version.
3887
+ */
3888
+ function reportDailyHeartbeat(items) {
3889
+ try {
3890
+ if (items.length === 0) return;
3891
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3892
+ if (navigator.webdriver) return;
3893
+ if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return;
3894
+ const visitor = visitorId();
3895
+ if (visitor === null) return;
3896
+ pruneDayKeys(today);
3897
+ const payloadItems = items.map((item) => {
3898
+ const out = { name: item.name };
3899
+ const version = item.version ?? bakedVersion();
3900
+ if (version !== void 0) out.version = version;
3901
+ if (item.channel !== void 0) out.channel = item.channel;
3902
+ return out;
3903
+ });
3904
+ const body = JSON.stringify({
3905
+ kind: "heartbeat",
3906
+ visitor,
3907
+ items: payloadItems
3908
+ });
3909
+ fetch(ENDPOINT, {
3910
+ method: "POST",
3911
+ headers: { "content-type": "application/json" },
3912
+ body,
3913
+ keepalive: true
3914
+ }).then((response) => {
3915
+ if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, "1");
3916
+ }).catch(() => {});
3917
+ } catch {}
3918
+ }
3919
+ //#endregion
3671
3920
  //#region src/client/index.ts
3672
3921
  /** Locale namespace this plugin owns. */
3673
3922
  const NS = "task-board";
@@ -3688,6 +3937,7 @@ window.__ModuleLoader__.load({
3688
3937
  * @param ctx - client root context (services: sessions, workspaces).
3689
3938
  */
3690
3939
  function apply(ctx) {
3940
+ reportDailyHeartbeat([{ name: "@linxin666/dsh-client-ui-task-board" }]);
3691
3941
  if (!claimTaskboardApply()) return;
3692
3942
  ctx.effect(() => releaseTaskboardApply, "task-board: apply claim");
3693
3943
  ctx.effect(() => {