dsh-task-ledger 0.0.46 → 0.0.47

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/dist/client.js CHANGED
@@ -10,6 +10,8 @@ window.__ModuleLoader__.load({
10
10
  /** host 路由前缀(全仓唯一;与 host/routes.ts 的 API_PREFIX 对应)。 */
11
11
  const API_PREFIX = "api/task-ledger";
12
12
  const DEFAULT_TIMEOUT_MS = 8e3;
13
+ /** 趋势端点读 history,比快路径慢一个量级(20 万条实测 4s+),超时单给。 */
14
+ const TREND_TIMEOUT_MS = 3e4;
13
15
  /**
14
16
  * 错误响应榨人话:host 路由(routes.ts)发的是 `{ error: "..." }`,那里的话是**能直接上界面的**
15
17
  * (带工作区名、python3 起不来之类的原文)。不带它界面上只剩「HTTP 500」,等于没报错。
@@ -40,9 +42,24 @@ window.__ModuleLoader__.load({
40
42
  clearTimeout(timer);
41
43
  }
42
44
  }
43
- /** 首页快照(在办全量 + 两个完成类统计 + 各工作区身份)。 */
44
- function fetchBoard() {
45
- return fetchJson("/board");
45
+ /**
46
+ * 首页快照(快路径:在办全量 + 统计 + 各工作区身份;**不含趋势**)。
47
+ * `fresh` = 让 host 先清 1.5s 读缓存再算(手动刷新 / 首读都要最新,不吃上一屏的缓存)。
48
+ */
49
+ function fetchBoardSnapshot(opts = {}) {
50
+ return fetchJson(`/board/snapshot${freshSuffix(opts)}`);
51
+ }
52
+ /**
53
+ * 趋势(慢路径:近 8 周 + 四个自然周期,host 侧要读 history)。
54
+ * 超时给得比默认宽:历史量大时读得久(实测 20 万条量级 4s+);
55
+ * 旧宿主没有该端点 ⇒ 404,直接抛给调用方兜底,不阻塞列表。
56
+ */
57
+ function fetchTrend(opts = {}) {
58
+ return fetchJson(`/board/trend${freshSuffix(opts)}`, {}, TREND_TIMEOUT_MS);
59
+ }
60
+ /** `?fresh=1` 只在真要最新值时带;不带就吃 host 的 1.5s 缓存。 */
61
+ function freshSuffix(opts) {
62
+ return opts.fresh === true ? "?fresh=1" : "";
46
63
  }
47
64
  //#endregion
48
65
  //#region src/client/store.ts
@@ -66,7 +83,10 @@ window.__ModuleLoader__.load({
66
83
  keyword: "",
67
84
  othersKeyword: ""
68
85
  },
69
- refreshing: false
86
+ refreshing: false,
87
+ trend: null,
88
+ trendStatus: "idle",
89
+ trendError: null
70
90
  };
71
91
  const listeners = /* @__PURE__ */ new Set();
72
92
  /** 首读只触发一次(谁先订阅谁触发),之后一律靠 `refresh()` / `setQuery()`。 */
@@ -96,17 +116,24 @@ window.__ModuleLoader__.load({
96
116
  listeners.delete(listener);
97
117
  };
98
118
  }
99
- /** 重读:在办快照 + 完成类统计与近 8 周趋势(一次 `/board` 全带回)。 */
119
+ /**
120
+ * 重读(2026-10-18 拆端点):**两段**——先快路径 `/board/snapshot`(只读在办,首屏立即可渲染),
121
+ * 再慢路径 `/board/trend`(读 history,后台拉;迟到按 token 丢弃,不盖新快照)。
122
+ * 趋势失败(含旧宿主 404)不阻塞列表:trendStatus 置 error,图表区显示兜底文案。
123
+ * 两段都带 `fresh=1`:本函数只由首读与手动刷新触发,都要最新值(host 缓存清两次无害,清是全局的)。
124
+ */
100
125
  async function refresh() {
101
126
  const token = ++runToken;
102
127
  set({ refreshing: true });
103
128
  try {
104
- const board = await fetchBoard();
129
+ const board = await fetchBoardSnapshot({ fresh: true });
105
130
  if (token !== runToken) return;
106
131
  set({
107
132
  status: "ready",
108
133
  board,
109
- error: null
134
+ error: null,
135
+ trendStatus: "loading",
136
+ trendError: null
110
137
  });
111
138
  } catch (error) {
112
139
  if (token !== runToken) return;
@@ -114,9 +141,26 @@ window.__ModuleLoader__.load({
114
141
  status: "error",
115
142
  error: error instanceof Error ? error.message : String(error)
116
143
  });
144
+ return;
117
145
  } finally {
118
146
  if (token === runToken) set({ refreshing: false });
119
147
  }
148
+ try {
149
+ const trend = await fetchTrend({ fresh: true });
150
+ if (token !== runToken) return;
151
+ set({
152
+ trend,
153
+ trendStatus: "ready",
154
+ trendError: null
155
+ });
156
+ } catch (error) {
157
+ if (token !== runToken) return;
158
+ set({
159
+ trend: null,
160
+ trendStatus: "error",
161
+ trendError: error instanceof Error ? error.message : String(error)
162
+ });
163
+ }
120
164
  }
121
165
  /** 改筛选:全部在浏览器里完成(快照已含全部在办),不发请求。 */
122
166
  function setQuery(patch) {
@@ -156,10 +200,11 @@ window.__ModuleLoader__.load({
156
200
  return String(b.task.created ?? b.task.started ?? "").localeCompare(String(a.task.created ?? a.task.started ?? ""));
157
201
  }
158
202
  /**
159
- * 当前筛选下的两区视图(用户 2026-10-17 定稿):
203
+ * 当前筛选下的两区视图(用户 2026-10-18 调整):
160
204
  * - 第一区「我的待办」= 我名下 doing/blocked,吃工作区下拉 + 标题搜索,按创建时间倒序;
161
- * - 第二区「其他待办」= 其余任务按工作区分组,吃**状态档**(segmented)+
162
- * **标题 / 执行人搜索**(usersKeyword);一个整表(外框连着),不是一块一块的。
205
+ * - 第二区「全部待办」= **所有**在办任务按工作区分组(含我的——用户明确不要过滤掉自己),
206
+ * 吃**状态档**(segmented)+ **标题 / 执行人搜索**(othersKeyword);一个整表(外框连着)。
207
+ * ⇒ 我的任务会同时出现在两个区,这是预期(顶部快看 / 底部全量)。
163
208
  */
164
209
  function selectView(current) {
165
210
  const { board, query } = current;
@@ -167,7 +212,7 @@ window.__ModuleLoader__.load({
167
212
  mine: [],
168
213
  groups: [],
169
214
  total: 0,
170
- othersCounts: {
215
+ allCounts: {
171
216
  total: 0,
172
217
  doing: 0,
173
218
  open: 0,
@@ -175,13 +220,13 @@ window.__ModuleLoader__.load({
175
220
  stale: 0
176
221
  }
177
222
  };
178
- const othersAll = board.tasks.filter((task) => !isMine(task, board));
179
- const othersCounts = {
180
- total: othersAll.length,
181
- doing: othersAll.filter((task) => task.status === "doing").length,
182
- open: othersAll.filter((task) => task.status === "open").length,
183
- blocked: othersAll.filter((task) => task.status === "blocked").length,
184
- stale: othersAll.filter((task) => task.stale === true).length
223
+ const allTasks = board.tasks;
224
+ const allCounts = {
225
+ total: allTasks.length,
226
+ doing: allTasks.filter((task) => task.status === "doing").length,
227
+ open: allTasks.filter((task) => task.status === "open").length,
228
+ blocked: allTasks.filter((task) => task.status === "blocked").length,
229
+ stale: allTasks.filter((task) => task.stale === true).length
185
230
  };
186
231
  const mineRows = board.tasks.filter((task) => isMine(task, board)).filter((task) => query.workspace === "" || task.workspace.id === query.workspace).filter((task) => hitKeyword(query.keyword, [
187
232
  task.title,
@@ -196,7 +241,7 @@ window.__ModuleLoader__.load({
196
241
  task,
197
242
  mine: true
198
243
  }));
199
- const rest = query.scope === "mine" ? [] : board.tasks.filter((task) => !isMine(task, board)).filter((task) => statusHit(task, query.status)).filter((task) => hitKeyword(query.othersKeyword, [task.title, task.owner]));
244
+ const rest = query.scope === "mine" ? [] : board.tasks.filter((task) => statusHit(task, query.status)).filter((task) => hitKeyword(query.othersKeyword, [task.title, task.owner]));
200
245
  const byWorkspace = /* @__PURE__ */ new Map();
201
246
  for (const task of rest) {
202
247
  const entry = byWorkspace.get(task.workspace.id) ?? {
@@ -224,8 +269,8 @@ window.__ModuleLoader__.load({
224
269
  return {
225
270
  mine: mineRows,
226
271
  groups,
227
- total: mineRows.length + groups.reduce((sum, group) => sum + group.rows.length, 0),
228
- othersCounts
272
+ total: groups.reduce((sum, group) => sum + group.rows.length, 0),
273
+ allCounts
229
274
  };
230
275
  }
231
276
  //#endregion
@@ -1166,6 +1211,11 @@ body[data-ds-dark-theme]{
1166
1211
  const board = state.board;
1167
1212
  const view = (0, react.useMemo)(() => selectView(state), [state]);
1168
1213
  const stats = board?.stats;
1214
+ const trend = state.trend;
1215
+ const periods = trend?.trend;
1216
+ const trendPending = state.trendStatus === "idle" || state.trendStatus === "loading";
1217
+ /** 快照 + 趋势两段各自的降级工作区(op 不同,合并展示,别装没看见)。 */
1218
+ const degraded = [...board?.degraded ?? [], ...trend?.degraded ?? []];
1169
1219
  const workspaceOptions = (0, react.useMemo)(() => [{
1170
1220
  value: "",
1171
1221
  label: t("workspaceAll")
@@ -1181,11 +1231,12 @@ body[data-ds-dark-theme]{
1181
1231
  hint,
1182
1232
  interactive: false
1183
1233
  });
1184
- /** 统计台卡:不可点(首页没有已结案可筛),hover 给具体日期区间。 */
1234
+ /** 统计台卡:不可点(首页没有已结案可筛),hover 给具体日期区间。
1235
+ * 趋势未到 / 读挂 ⇒ 数字留 '—'(StatCard 对 0 的处理)、小字整条不显示,**不拿 0 充数**。 */
1185
1236
  const periodCard = (label, period) => (0, react.createElement)(StatCard, {
1186
1237
  label: t(label),
1187
1238
  value: period?.done ?? 0,
1188
- hint: t("statCreatedHint", { n: period?.created ?? 0 }),
1239
+ hint: period === void 0 ? void 0 : t("statCreatedHint", { n: period.created }),
1189
1240
  interactive: false,
1190
1241
  title: period === void 0 ? void 0 : t("periodRange", {
1191
1242
  from: period.from,
@@ -1193,7 +1244,7 @@ body[data-ds-dark-theme]{
1193
1244
  })
1194
1245
  });
1195
1246
  const [chartScope, setChartScope] = (0, react.useState)("all");
1196
- const chartWeeks = (0, react.useMemo)(() => (board === null ? [] : board.closed.weekly[chartScope]).map((bucket) => {
1247
+ const chartWeeks = (0, react.useMemo)(() => (trend?.closed.weekly[chartScope] ?? []).map((bucket) => {
1197
1248
  const end = bucket.end ?? bucket.week ?? "";
1198
1249
  return {
1199
1250
  label: end.slice(5) || bucket.week?.slice(5) || "",
@@ -1203,7 +1254,7 @@ body[data-ds-dark-theme]{
1203
1254
  title: chartScope === "mine" ? `截至 ${end}(${t("chartOnlyMine")})` : `截至 ${end}`
1204
1255
  };
1205
1256
  }), [
1206
- board,
1257
+ trend,
1207
1258
  chartScope,
1208
1259
  t
1209
1260
  ]);
@@ -1262,7 +1313,7 @@ body[data-ds-dark-theme]{
1262
1313
  }),
1263
1314
  title: t("statStaleTitle", { n: stats.staleDays }),
1264
1315
  interactive: false
1265
- }), periodCard("statWeekDone", board?.trend.thisWeek), periodCard("statLastWeekDone", board?.trend.lastWeek), periodCard("statMonthDone", board?.trend.thisMonth), periodCard("statLastMonthDone", board?.trend.lastMonth)), (0, react.createElement)("div", {
1316
+ }), periodCard("statWeekDone", periods?.thisWeek), periodCard("statLastWeekDone", periods?.lastWeek), periodCard("statMonthDone", periods?.thisMonth), periodCard("statLastMonthDone", periods?.lastMonth)), (0, react.createElement)("div", {
1266
1317
  className: "dtl-card",
1267
1318
  style: chartCardStyle
1268
1319
  }, (0, react.createElement)("div", { style: {
@@ -1275,7 +1326,13 @@ body[data-ds-dark-theme]{
1275
1326
  onChange: (event) => {
1276
1327
  setChartScope(event.target.checked ? "mine" : "all");
1277
1328
  }
1278
- }), t("chartOnlyMine"))), chartEmpty ? (0, react.createElement)("div", { style: {
1329
+ }), t("chartOnlyMine"))), trendPending ? (0, react.createElement)("div", { style: {
1330
+ ...hintStyle,
1331
+ padding: "var(--dtl-space-4) 0"
1332
+ } }, t("chartLoading")) : state.trendStatus === "error" ? (0, react.createElement)("div", { style: {
1333
+ ...hintStyle,
1334
+ padding: "var(--dtl-space-4) 0"
1335
+ } }, `${t("chartUnavailable")}${state.trendError === null ? "" : `:${state.trendError}`}`) : chartEmpty ? (0, react.createElement)("div", { style: {
1279
1336
  ...hintStyle,
1280
1337
  padding: "var(--dtl-space-4) 0"
1281
1338
  } }, t("chartEmpty")) : (0, react.createElement)(GroupedBarChart, {
@@ -1321,7 +1378,7 @@ body[data-ds-dark-theme]{
1321
1378
  color: "var(--dtl-danger)",
1322
1379
  flex: "none",
1323
1380
  fontWeight: 600
1324
- } }, t("degradedTitle")), (0, react.createElement)("span", null, (board.degraded ?? []).map((item) => t("degradedItem", {
1381
+ } }, t("degradedTitle")), (0, react.createElement)("span", null, degraded.map((item) => t("degradedItem", {
1325
1382
  title: item.workspaceTitle,
1326
1383
  message: item.message
1327
1384
  })).join(";"))) : null, (0, react.createElement)("div", { style: regionHeadRowStyle }, (0, react.createElement)("span", { style: {
@@ -1363,7 +1420,7 @@ body[data-ds-dark-theme]{
1363
1420
  } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, { size: 16 })), (0, react.createElement)("span", { style: {
1364
1421
  ...regionHeadTitleStyle,
1365
1422
  fontSize: "var(--dtl-font-md)"
1366
- } }, t("regionOthers")), (0, react.createElement)("span", { style: { flex: "1 1 auto" } }), (0, react.createElement)(Segmented, {
1423
+ } }, t("regionAll")), (0, react.createElement)("span", { style: { flex: "1 1 auto" } }), (0, react.createElement)(Segmented, {
1367
1424
  id: "dtl-others-status",
1368
1425
  value: state.query.status,
1369
1426
  label: t("statusLabel"),
@@ -1371,27 +1428,27 @@ body[data-ds-dark-theme]{
1371
1428
  {
1372
1429
  value: "all",
1373
1430
  label: t("statusAll"),
1374
- badge: orZero(view.othersCounts.total)
1431
+ badge: orZero(view.allCounts.total)
1375
1432
  },
1376
1433
  {
1377
1434
  value: "active",
1378
1435
  label: t("statusActive"),
1379
- badge: orZero(view.othersCounts.doing)
1436
+ badge: orZero(view.allCounts.doing)
1380
1437
  },
1381
1438
  {
1382
1439
  value: "open",
1383
1440
  label: t("statusOpen"),
1384
- badge: orZero(view.othersCounts.open)
1441
+ badge: orZero(view.allCounts.open)
1385
1442
  },
1386
1443
  {
1387
1444
  value: "blocked",
1388
1445
  label: t("statusBlocked"),
1389
- badge: orZero(view.othersCounts.blocked)
1446
+ badge: orZero(view.allCounts.blocked)
1390
1447
  },
1391
1448
  {
1392
1449
  value: "stale",
1393
1450
  label: t("statusStale"),
1394
- badge: orZero(view.othersCounts.stale)
1451
+ badge: orZero(view.allCounts.stale)
1395
1452
  }
1396
1453
  ],
1397
1454
  onChange: (next) => {
@@ -1414,15 +1471,15 @@ body[data-ds-dark-theme]{
1414
1471
  fontSize: "var(--dtl-font-lg)",
1415
1472
  fontWeight: 600,
1416
1473
  marginBottom: "var(--dtl-space-1)"
1417
- } }, t("othersEmptyTitle")), (0, react.createElement)("div", { style: {
1474
+ } }, t("allEmptyTitle")), (0, react.createElement)("div", { style: {
1418
1475
  fontSize: "var(--dtl-font-md)",
1419
1476
  color: "var(--dtl-fg-2)",
1420
1477
  marginBottom: "var(--dtl-space-3)"
1421
- } }, t("othersEmptyHint")), (0, react.createElement)(Button, {
1478
+ } }, t("allEmptyHint")), (0, react.createElement)(Button, {
1422
1479
  variant: "primary",
1423
1480
  size: "md",
1424
1481
  onClick: onBack
1425
- }, t("othersEmptyAction"))) : (0, react.createElement)("div", { style: { marginTop: "var(--dtl-space-2)" } }, view.groups.map((group) => (0, react.createElement)(WorkspaceGroupView, {
1482
+ }, t("allEmptyAction"))) : (0, react.createElement)("div", { style: { marginTop: "var(--dtl-space-2)" } }, view.groups.map((group) => (0, react.createElement)(WorkspaceGroupView, {
1426
1483
  key: group.workspaceId,
1427
1484
  t,
1428
1485
  group,
@@ -1433,7 +1490,7 @@ body[data-ds-dark-theme]{
1433
1490
  function orZero(n) {
1434
1491
  return n > 0 ? n : void 0;
1435
1492
  }
1436
- /** 一个工作区分组(其他待办表格段):段头(点+名+统计)→ 列头行 → 任务行。
1493
+ /** 一个工作区分组(全部待办表格段):段头(点+名+统计)→ 列头行 → 任务行。
1437
1494
  * 统计口径(用户 2026-10-18):共 N|待办 N|进行中 N 常显(0 也显);
1438
1495
  * 阻塞 / 停滞**只在 >0 时出现**。 */
1439
1496
  function WorkspaceGroupView(props) {
@@ -1504,6 +1561,8 @@ body[data-ds-dark-theme]{
1504
1561
  chartCaption: "完成(按结案时间)/ 新增(按创建时间),周一起桶;我的 = owner 是我",
1505
1562
  chartBucket: "{week} 当周:完成 {done} 条,新增 {created} 条",
1506
1563
  chartEmpty: "近 8 周没有记录",
1564
+ chartLoading: "趋势读取中…(列表已就绪,统计稍后到)",
1565
+ chartUnavailable: "趋势暂时读不到(不影响上面的任务列表)",
1507
1566
  chartScopeAll: "所有",
1508
1567
  chartScopeMine: "我的",
1509
1568
  chartOnlyMine: "只看我的",
@@ -1525,7 +1584,7 @@ body[data-ds-dark-theme]{
1525
1584
  workspaceAll: "全部工作区",
1526
1585
  searchPlaceholder: "搜索",
1527
1586
  regionMine: "我的待办",
1528
- regionOthers: "其他待办",
1587
+ regionAll: "全部待办",
1529
1588
  showingCount: "显示 {n} 条",
1530
1589
  groupTotal: "共 {n} 条",
1531
1590
  groupOpen: "待办 {n} 条",
@@ -1537,9 +1596,9 @@ body[data-ds-dark-theme]{
1537
1596
  colOwner: "执行人",
1538
1597
  colStatus: "状态",
1539
1598
  colTime: "时长",
1540
- othersEmptyTitle: "没有可显示的事项",
1541
- othersEmptyHint: "切换上面的状态过滤试试,或新建一个任务。",
1542
- othersEmptyAction: "新建任务",
1599
+ allEmptyTitle: "没有可显示的事项",
1600
+ allEmptyHint: "切换上面的状态过滤试试,或新建一个任务。",
1601
+ allEmptyAction: "新建任务",
1543
1602
  expandAria: "展开过程文档",
1544
1603
  collapseAria: "收起",
1545
1604
  ownerUnclaimed: "待认领",
@@ -1615,6 +1674,8 @@ body[data-ds-dark-theme]{
1615
1674
  chartCaption: "done (by close time) / created (by create time), weeks start Monday; mine = owner is me",
1616
1675
  chartBucket: "week of {week}: {done} done, {created} created",
1617
1676
  chartEmpty: "Nothing in the last 8 weeks",
1677
+ chartLoading: "Loading trend… (list is ready, stats will follow)",
1678
+ chartUnavailable: "Trend is unavailable right now (the task lists above are unaffected)",
1618
1679
  chartScopeAll: "All",
1619
1680
  chartScopeMine: "Mine",
1620
1681
  chartOnlyMine: "Mine only",
@@ -1636,7 +1697,7 @@ body[data-ds-dark-theme]{
1636
1697
  workspaceAll: "All workspaces",
1637
1698
  searchPlaceholder: "Search",
1638
1699
  regionMine: "My todos",
1639
- regionOthers: "Other tasks",
1700
+ regionAll: "All tasks",
1640
1701
  showingCount: "{n} shown",
1641
1702
  groupTotal: "{n} total",
1642
1703
  groupOpen: "{n} open",
@@ -1648,9 +1709,9 @@ body[data-ds-dark-theme]{
1648
1709
  colOwner: "Owner",
1649
1710
  colStatus: "Status",
1650
1711
  colTime: "Duration",
1651
- othersEmptyTitle: "Nothing to show",
1652
- othersEmptyHint: "Try another status filter above, or create a task.",
1653
- othersEmptyAction: "New task",
1712
+ allEmptyTitle: "Nothing to show",
1713
+ allEmptyHint: "Try another status filter above, or create a task.",
1714
+ allEmptyAction: "New task",
1654
1715
  expandAria: "Expand worklog",
1655
1716
  collapseAria: "Collapse",
1656
1717
  ownerUnclaimed: "unclaimed",