agim-cli 1.2.70 → 1.2.72

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.
Files changed (163) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/web/public/assets/{a2a-CzOCrC9J.js → a2a-BbtW9q1L.js} +2 -2
  3. package/dist/web/public/assets/{a2a-CzOCrC9J.js.map → a2a-BbtW9q1L.js.map} +1 -1
  4. package/dist/web/public/assets/{activity-BlasCFZq.js → activity-CJmQLZ6_.js} +2 -2
  5. package/dist/web/public/assets/{activity-BlasCFZq.js.map → activity-CJmQLZ6_.js.map} +1 -1
  6. package/dist/web/public/assets/{admins-CQOShfRl.js → admins-BJP9koxS.js} +2 -2
  7. package/dist/web/public/assets/{admins-CQOShfRl.js.map → admins-BJP9koxS.js.map} +1 -1
  8. package/dist/web/public/assets/{agents-UstP4DoK.js → agents-DC0XFC6e.js} +2 -2
  9. package/dist/web/public/assets/{agents-UstP4DoK.js.map → agents-DC0XFC6e.js.map} +1 -1
  10. package/dist/web/public/assets/{approvals-BvfcdYLU.js → approvals-DiPkpPN4.js} +2 -2
  11. package/dist/web/public/assets/{approvals-BvfcdYLU.js.map → approvals-DiPkpPN4.js.map} +1 -1
  12. package/dist/web/public/assets/{asks-DAJ1edZu.js → asks-CG9chQgT.js} +2 -2
  13. package/dist/web/public/assets/{asks-DAJ1edZu.js.map → asks-CG9chQgT.js.map} +1 -1
  14. package/dist/web/public/assets/{audit-CAOadRDl.js → audit-B40mxlW1.js} +2 -2
  15. package/dist/web/public/assets/{audit-CAOadRDl.js.map → audit-B40mxlW1.js.map} +1 -1
  16. package/dist/web/public/assets/{bell-BuZoWBMK.js → bell-BC8Jqt9f.js} +2 -2
  17. package/dist/web/public/assets/{bell-BuZoWBMK.js.map → bell-BC8Jqt9f.js.map} +1 -1
  18. package/dist/web/public/assets/{bgjobs-C5jF3BWG.js → bgjobs-ClrQ2jzx.js} +2 -2
  19. package/dist/web/public/assets/{bgjobs-C5jF3BWG.js.map → bgjobs-ClrQ2jzx.js.map} +1 -1
  20. package/dist/web/public/assets/{brain-DaFGi4GZ.js → brain-BBx7RF2j.js} +2 -2
  21. package/dist/web/public/assets/{brain-DaFGi4GZ.js.map → brain-BBx7RF2j.js.map} +1 -1
  22. package/dist/web/public/assets/{briefcase-BXKiYFAz.js → briefcase-TihsRkSy.js} +2 -2
  23. package/dist/web/public/assets/{briefcase-BXKiYFAz.js.map → briefcase-TihsRkSy.js.map} +1 -1
  24. package/dist/web/public/assets/{chevron-right-Bi2RmsHt.js → chevron-right-DTLZLXDP.js} +2 -2
  25. package/dist/web/public/assets/{chevron-right-Bi2RmsHt.js.map → chevron-right-DTLZLXDP.js.map} +1 -1
  26. package/dist/web/public/assets/{circle-check-NYJSuZMK.js → circle-check-EHM2OMD5.js} +2 -2
  27. package/dist/web/public/assets/{circle-check-NYJSuZMK.js.map → circle-check-EHM2OMD5.js.map} +1 -1
  28. package/dist/web/public/assets/{circle-check-big-wxQV9Apf.js → circle-check-big-DJXNAZ3u.js} +2 -2
  29. package/dist/web/public/assets/{circle-check-big-wxQV9Apf.js.map → circle-check-big-DJXNAZ3u.js.map} +1 -1
  30. package/dist/web/public/assets/{circle-x-D7EpTdJQ.js → circle-x-DsaHSb_C.js} +2 -2
  31. package/dist/web/public/assets/{circle-x-D7EpTdJQ.js.map → circle-x-DsaHSb_C.js.map} +1 -1
  32. package/dist/web/public/assets/{confirm-dialog-BecM1NAT.js → confirm-dialog-DezSK6wB.js} +2 -2
  33. package/dist/web/public/assets/{confirm-dialog-BecM1NAT.js.map → confirm-dialog-DezSK6wB.js.map} +1 -1
  34. package/dist/web/public/assets/{data-table-Bh5TFhXw.js → data-table-DdSnbgyt.js} +2 -2
  35. package/dist/web/public/assets/{data-table-Bh5TFhXw.js.map → data-table-DdSnbgyt.js.map} +1 -1
  36. package/dist/web/public/assets/{dialog-8m5Uihhi.js → dialog-45x-p0sR.js} +2 -2
  37. package/dist/web/public/assets/{dialog-8m5Uihhi.js.map → dialog-45x-p0sR.js.map} +1 -1
  38. package/dist/web/public/assets/{download-DWvNllY2.js → download-BL4jeu4M.js} +2 -2
  39. package/dist/web/public/assets/{download-DWvNllY2.js.map → download-BL4jeu4M.js.map} +1 -1
  40. package/dist/web/public/assets/{email-Cr1x3fdt.js → email-BGUO-gme.js} +2 -2
  41. package/dist/web/public/assets/{email-Cr1x3fdt.js.map → email-BGUO-gme.js.map} +1 -1
  42. package/dist/web/public/assets/{empty-state-BESUGVnV.js → empty-state-n-LfHo6N.js} +2 -2
  43. package/dist/web/public/assets/{empty-state-BESUGVnV.js.map → empty-state-n-LfHo6N.js.map} +1 -1
  44. package/dist/web/public/assets/{external-link-BwscsXby.js → external-link-CZd6ma6Y.js} +2 -2
  45. package/dist/web/public/assets/{external-link-BwscsXby.js.map → external-link-CZd6ma6Y.js.map} +1 -1
  46. package/dist/web/public/assets/{eye-C7gaSIZv.js → eye-LsErAgqh.js} +2 -2
  47. package/dist/web/public/assets/{eye-C7gaSIZv.js.map → eye-LsErAgqh.js.map} +1 -1
  48. package/dist/web/public/assets/{facts-B9hm9627.js → facts-CxZ3L_7a.js} +2 -2
  49. package/dist/web/public/assets/{facts-B9hm9627.js.map → facts-CxZ3L_7a.js.map} +1 -1
  50. package/dist/web/public/assets/{goals-D3f1Dw2M.js → goals-BjzgqVnW.js} +2 -2
  51. package/dist/web/public/assets/{goals-D3f1Dw2M.js.map → goals-BjzgqVnW.js.map} +1 -1
  52. package/dist/web/public/assets/{health-BLwrXebg.js → health-BYX5IK98.js} +2 -2
  53. package/dist/web/public/assets/{health-BLwrXebg.js.map → health-BYX5IK98.js.map} +1 -1
  54. package/dist/web/public/assets/{heart-pulse-BtioxtCU.js → heart-pulse-D_xUK9r0.js} +2 -2
  55. package/dist/web/public/assets/{heart-pulse-BtioxtCU.js.map → heart-pulse-D_xUK9r0.js.map} +1 -1
  56. package/dist/web/public/assets/{heartbeat-BHPx1z33.js → heartbeat-DnSegIYS.js} +2 -2
  57. package/dist/web/public/assets/{heartbeat-BHPx1z33.js.map → heartbeat-DnSegIYS.js.map} +1 -1
  58. package/dist/web/public/assets/{hot-CuaGMYzV.js → hot-C5TxCTVL.js} +2 -2
  59. package/dist/web/public/assets/{hot-CuaGMYzV.js.map → hot-C5TxCTVL.js.map} +1 -1
  60. package/dist/web/public/assets/{index-Cc8hxJWT.js → index-CbWhcThC.js} +3 -3
  61. package/dist/web/public/assets/{index-Cc8hxJWT.js.map → index-CbWhcThC.js.map} +1 -1
  62. package/dist/web/public/assets/{installed-D_BoGzel.js → installed-D_Hjs7uj.js} +2 -2
  63. package/dist/web/public/assets/{installed-D_BoGzel.js.map → installed-D_Hjs7uj.js.map} +1 -1
  64. package/dist/web/public/assets/{jobs-D0eeD9ye.js → jobs-DWtjJIAD.js} +2 -2
  65. package/dist/web/public/assets/{jobs-D0eeD9ye.js.map → jobs-DWtjJIAD.js.map} +1 -1
  66. package/dist/web/public/assets/{layout-DPxfAgBW.js → layout-54-PzplU.js} +2 -2
  67. package/dist/web/public/assets/{layout-DPxfAgBW.js.map → layout-54-PzplU.js.map} +1 -1
  68. package/dist/web/public/assets/{layout-bRkGiGF-.js → layout-BLK9XuQB.js} +2 -2
  69. package/dist/web/public/assets/{layout-bRkGiGF-.js.map → layout-BLK9XuQB.js.map} +1 -1
  70. package/dist/web/public/assets/{layout-CvRW3Bn_.js → layout-CTyy2kgf.js} +2 -2
  71. package/dist/web/public/assets/{layout-CvRW3Bn_.js.map → layout-CTyy2kgf.js.map} +1 -1
  72. package/dist/web/public/assets/{layout-Wl7Pr1-s.js → layout-CvvZcaiO.js} +2 -2
  73. package/dist/web/public/assets/{layout-Wl7Pr1-s.js.map → layout-CvvZcaiO.js.map} +1 -1
  74. package/dist/web/public/assets/{layout-CQe9ncYF.js → layout-D1-LnIA_.js} +2 -2
  75. package/dist/web/public/assets/{layout-CQe9ncYF.js.map → layout-D1-LnIA_.js.map} +1 -1
  76. package/dist/web/public/assets/{llm-CJGxq9Ru.js → llm-D59Tv6ne.js} +2 -2
  77. package/dist/web/public/assets/{llm-CJGxq9Ru.js.map → llm-D59Tv6ne.js.map} +1 -1
  78. package/dist/web/public/assets/{loader-circle-D4WrM89N.js → loader-circle-U64eR4W2.js} +2 -2
  79. package/dist/web/public/assets/{loader-circle-D4WrM89N.js.map → loader-circle-U64eR4W2.js.map} +1 -1
  80. package/dist/web/public/assets/{map-pin-DGSzw_UH.js → map-pin-D08YlPs3.js} +2 -2
  81. package/dist/web/public/assets/{map-pin-DGSzw_UH.js.map → map-pin-D08YlPs3.js.map} +1 -1
  82. package/dist/web/public/assets/{mcp-DYp7QbX8.js → mcp-Br9fBIWu.js} +2 -2
  83. package/dist/web/public/assets/{mcp-DYp7QbX8.js.map → mcp-Br9fBIWu.js.map} +1 -1
  84. package/dist/web/public/assets/{memos-Dz0jD4R5.js → memos-nZKV3SLq.js} +2 -2
  85. package/dist/web/public/assets/{memos-Dz0jD4R5.js.map → memos-nZKV3SLq.js.map} +1 -1
  86. package/dist/web/public/assets/messengers-C8ss4WVx.js +7 -0
  87. package/dist/web/public/assets/messengers-C8ss4WVx.js.map +1 -0
  88. package/dist/web/public/assets/{native-agent-ChgK3ceB.js → native-agent-BiiYK91m.js} +2 -2
  89. package/dist/web/public/assets/{native-agent-ChgK3ceB.js.map → native-agent-BiiYK91m.js.map} +1 -1
  90. package/dist/web/public/assets/{network-D8uD8vuE.js → network-D9rhTHz5.js} +2 -2
  91. package/dist/web/public/assets/{network-D8uD8vuE.js.map → network-D9rhTHz5.js.map} +1 -1
  92. package/dist/web/public/assets/{outbox-CRcmD2P1.js → outbox-BS_gla4W.js} +2 -2
  93. package/dist/web/public/assets/{outbox-CRcmD2P1.js.map → outbox-BS_gla4W.js.map} +1 -1
  94. package/dist/web/public/assets/{pagination-BK8pXuOn.js → pagination-CSuYj-hv.js} +2 -2
  95. package/dist/web/public/assets/{pagination-BK8pXuOn.js.map → pagination-CSuYj-hv.js.map} +1 -1
  96. package/dist/web/public/assets/{persona-Dk4Y24TF.js → persona-BByKd8Zd.js} +2 -2
  97. package/dist/web/public/assets/{persona-Dk4Y24TF.js.map → persona-BByKd8Zd.js.map} +1 -1
  98. package/dist/web/public/assets/{play-BAwY0kZY.js → play-B_11jKz_.js} +2 -2
  99. package/dist/web/public/assets/{play-BAwY0kZY.js.map → play-B_11jKz_.js.map} +1 -1
  100. package/dist/web/public/assets/{plus-Qg7H_hBC.js → plus-BxhCliGV.js} +2 -2
  101. package/dist/web/public/assets/{plus-Qg7H_hBC.js.map → plus-BxhCliGV.js.map} +1 -1
  102. package/dist/web/public/assets/{policy-bd1_X4KT.js → policy-CHbUM0MU.js} +2 -2
  103. package/dist/web/public/assets/{policy-bd1_X4KT.js.map → policy-CHbUM0MU.js.map} +1 -1
  104. package/dist/web/public/assets/{refresh-ccw-DjeeSzbz.js → refresh-ccw-BKw5Agez.js} +2 -2
  105. package/dist/web/public/assets/{refresh-ccw-DjeeSzbz.js.map → refresh-ccw-BKw5Agez.js.map} +1 -1
  106. package/dist/web/public/assets/{reminders-CyNYrPCo.js → reminders-_MsAKlS7.js} +2 -2
  107. package/dist/web/public/assets/{reminders-CyNYrPCo.js.map → reminders-_MsAKlS7.js.map} +1 -1
  108. package/dist/web/public/assets/{save-B6_ssx8c.js → save-DznSJZGN.js} +2 -2
  109. package/dist/web/public/assets/{save-B6_ssx8c.js.map → save-DznSJZGN.js.map} +1 -1
  110. package/dist/web/public/assets/{schedules-DVxENvba.js → schedules-CiRZXonB.js} +2 -2
  111. package/dist/web/public/assets/{schedules-DVxENvba.js.map → schedules-CiRZXonB.js.map} +1 -1
  112. package/dist/web/public/assets/{search-BJwBDf0Q.js → search-CXr_r7L8.js} +2 -2
  113. package/dist/web/public/assets/{search-BJwBDf0Q.js.map → search-CXr_r7L8.js.map} +1 -1
  114. package/dist/web/public/assets/{security-Co1823U6.js → security-CjySIq-k.js} +2 -2
  115. package/dist/web/public/assets/{security-Co1823U6.js.map → security-CjySIq-k.js.map} +1 -1
  116. package/dist/web/public/assets/{service-BJ2kE446.js → service-LXlgJooI.js} +2 -2
  117. package/dist/web/public/assets/{service-BJ2kE446.js.map → service-LXlgJooI.js.map} +1 -1
  118. package/dist/web/public/assets/{status-badge-3nnQTZXz.js → status-badge-BsPU9NHK.js} +2 -2
  119. package/dist/web/public/assets/{status-badge-3nnQTZXz.js.map → status-badge-BsPU9NHK.js.map} +1 -1
  120. package/dist/web/public/assets/{subtasks-BgHm-5dB.js → subtasks-DM8tJkC7.js} +2 -2
  121. package/dist/web/public/assets/{subtasks-BgHm-5dB.js.map → subtasks-DM8tJkC7.js.map} +1 -1
  122. package/dist/web/public/assets/{table-CNXI0HD-.js → table-CQbO75bn.js} +2 -2
  123. package/dist/web/public/assets/{table-CNXI0HD-.js.map → table-CQbO75bn.js.map} +1 -1
  124. package/dist/web/public/assets/{topn-C1PyG8Ns.js → topn-CaCYQ1NI.js} +2 -2
  125. package/dist/web/public/assets/{topn-C1PyG8Ns.js.map → topn-CaCYQ1NI.js.map} +1 -1
  126. package/dist/web/public/assets/{trash-2-CjkttCxU.js → trash-2-CdgBbY1Z.js} +2 -2
  127. package/dist/web/public/assets/{trash-2-CjkttCxU.js.map → trash-2-CdgBbY1Z.js.map} +1 -1
  128. package/dist/web/public/assets/{use-background-tasks-CLf78_yn.js → use-background-tasks-DUDxcbbl.js} +2 -2
  129. package/dist/web/public/assets/{use-background-tasks-CLf78_yn.js.map → use-background-tasks-DUDxcbbl.js.map} +1 -1
  130. package/dist/web/public/assets/{use-llm-admin-CzL0zcIi.js → use-llm-admin-Bpy7nDu-.js} +2 -2
  131. package/dist/web/public/assets/{use-llm-admin-CzL0zcIi.js.map → use-llm-admin-Bpy7nDu-.js.map} +1 -1
  132. package/dist/web/public/assets/{use-memory-DrJjdPYo.js → use-memory-B00AYXi0.js} +2 -2
  133. package/dist/web/public/assets/{use-memory-DrJjdPYo.js.map → use-memory-B00AYXi0.js.map} +1 -1
  134. package/dist/web/public/assets/{use-observability-1j7-vMy5.js → use-observability-7ETfQyEP.js} +2 -2
  135. package/dist/web/public/assets/{use-observability-1j7-vMy5.js.map → use-observability-7ETfQyEP.js.map} +1 -1
  136. package/dist/web/public/assets/{use-settings-BLcy2mrv.js → use-settings-tzqyzhw3.js} +2 -2
  137. package/dist/web/public/assets/{use-settings-BLcy2mrv.js.map → use-settings-tzqyzhw3.js.map} +1 -1
  138. package/dist/web/public/assets/{use-workspace-BLXvLTDF.js → use-workspace-B5gbK4s5.js} +2 -2
  139. package/dist/web/public/assets/{use-workspace-BLXvLTDF.js.map → use-workspace-B5gbK4s5.js.map} +1 -1
  140. package/dist/web/public/assets/{useQuery-xB5dCeu-.js → useQuery-wnQifGeZ.js} +2 -2
  141. package/dist/web/public/assets/{useQuery-xB5dCeu-.js.map → useQuery-wnQifGeZ.js.map} +1 -1
  142. package/dist/web/public/assets/{vector-EOjQsr7r.js → vector-or1-MfRX.js} +2 -2
  143. package/dist/web/public/assets/{vector-EOjQsr7r.js.map → vector-or1-MfRX.js.map} +1 -1
  144. package/dist/web/public/assets/{viewer-D_I2ikzf.js → viewer-C0f5b5eT.js} +2 -2
  145. package/dist/web/public/assets/{viewer-D_I2ikzf.js.map → viewer-C0f5b5eT.js.map} +1 -1
  146. package/dist/web/public/assets/{workspace-lMCzYJAB.js → workspace-CGE46-O2.js} +2 -2
  147. package/dist/web/public/assets/{workspace-lMCzYJAB.js.map → workspace-CGE46-O2.js.map} +1 -1
  148. package/dist/web/public/assets/{workspaces-Ciyjiowm.js → workspaces-CqDIcIXo.js} +2 -2
  149. package/dist/web/public/assets/{workspaces-Ciyjiowm.js.map → workspaces-CqDIcIXo.js.map} +1 -1
  150. package/dist/web/public/assets/{x-DP0VnoG9.js → x-DBZPbfu3.js} +2 -2
  151. package/dist/web/public/assets/{x-DP0VnoG9.js.map → x-DBZPbfu3.js.map} +1 -1
  152. package/dist/web/public/index.html +1 -1
  153. package/dist/web/server.d.ts.map +1 -1
  154. package/dist/web/server.js +21 -41
  155. package/dist/web/server.js.map +1 -1
  156. package/package.json +1 -1
  157. package/dist/web/public/_app.js +0 -248
  158. package/dist/web/public/assets/messengers-Bewhby2_.js +0 -7
  159. package/dist/web/public/assets/messengers-Bewhby2_.js.map +0 -1
  160. package/dist/web/public/memos.html +0 -352
  161. package/dist/web/public/reminders.html +0 -332
  162. package/dist/web/public/settings.html +0 -2488
  163. package/dist/web/public/tasks.html +0 -3724
@@ -1,352 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Agim — Memos</title>
7
- <script src="/_app.js"></script>
8
- <script>
9
- (function () {
10
- const LANGS = { en: 'en', zh: 'zh' };
11
- const savedLang = localStorage.getItem('im-hub-lang');
12
- const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
13
- window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
14
- document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
15
- const T = {
16
- en: {
17
- title: 'Agim — Memos', h1: '📋 Memos',
18
- backToChat: '↩ Chat', toTasks: 'Tasks', toReminders: 'Reminders', toSettings: 'Settings',
19
- filterAll: 'All', filterGeo: 'With location', filterExpired: 'Expired',
20
- searchPlaceholder: 'search (what / memo / who / address …)',
21
- loading: 'Loading…',
22
- loadFailed: 'Load failed: {err}',
23
- emptyNoMatch: 'No memos match your search.',
24
- emptyNone: 'No memos yet. Just tell an agent in IM "记下…" / "remember that…" and they will save it.',
25
- delete: 'Delete', edit: 'Edit',
26
- confirmDelete: 'Really delete memo #{id}?',
27
- deleted: 'Deleted #{id}',
28
- deleteFailed: 'Delete failed: {err}',
29
- mapBaidu: 'Baidu Map', mapAmap: 'Amap', mapGoogle: 'Google Maps',
30
- },
31
- zh: {
32
- title: 'Agim — 备忘', h1: '📋 备忘',
33
- backToChat: '↩ 对话', toTasks: '任务', toReminders: '提醒', toSettings: '设置',
34
- filterAll: '全部', filterGeo: '有位置', filterExpired: '已过期',
35
- searchPlaceholder: '搜索内容(what / memo / who / 地址 …)',
36
- loading: '加载中…',
37
- loadFailed: '加载失败:{err}',
38
- emptyNoMatch: '没匹配的 memo',
39
- emptyNone: '还没记下任何 memo。在 IM 里说「记下…」让 agent 帮你存。',
40
- delete: '删除', edit: '编辑',
41
- confirmDelete: '确定删除 memo #{id}?',
42
- deleted: '已删除 #{id}',
43
- deleteFailed: '删除失败:{err}',
44
- mapBaidu: '百度地图', mapAmap: '高德地图', mapGoogle: 'Google',
45
- },
46
- };
47
- window.__t = T[window.__lang];
48
- document.title = T[window.__lang].title;
49
- })();
50
- </script>
51
- <style>
52
- :root {
53
- --bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
54
- --card: #fff; --accent: #2563eb; --danger: #dc2626; --warn: #d97706;
55
- --geo: #059669;
56
- }
57
- [data-theme="dark"] {
58
- --bg: #1a1a1a; --fg: #e5e5e5; --muted: #999; --border: #333;
59
- --card: #242424; --accent: #60a5fa; --danger: #f87171; --warn: #fbbf24;
60
- --geo: #34d399;
61
- }
62
- body {
63
- font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif;
64
- background: var(--bg); color: var(--fg); margin: 0; padding: 0;
65
- }
66
- /* Header — kept in sync with tasks.html / reminders.html so all
67
- dashboard pages share the same layout. */
68
- header {
69
- display: flex;
70
- align-items: center;
71
- gap: 16px;
72
- padding: 14px 24px;
73
- border-bottom: 1px solid var(--border);
74
- background: var(--card);
75
- }
76
- header h1 { margin: 0; font-size: 18px; font-weight: 600; flex: 1; }
77
- header a, header button, header select {
78
- color: var(--accent);
79
- text-decoration: none;
80
- font-size: 14px;
81
- background: none;
82
- border: 1px solid var(--border);
83
- padding: 6px 12px;
84
- border-radius: 4px;
85
- cursor: pointer;
86
- }
87
- header select { color: var(--fg); }
88
- header a:hover, header button:hover { border-color: var(--accent); }
89
- main { padding: 20px; max-width: 980px; margin: 0 auto; }
90
- .toolbar {
91
- display: flex; gap: 8px; margin-bottom: 18px; flex-wrap: wrap;
92
- align-items: center;
93
- }
94
- .toolbar input[type="text"] {
95
- flex: 1; min-width: 200px;
96
- padding: 7px 12px; border: 1px solid var(--border); border-radius: 6px;
97
- background: var(--card); color: var(--fg); font-size: 14px;
98
- }
99
- .toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }
100
- .filter-btn {
101
- padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px;
102
- background: transparent; color: var(--muted); cursor: pointer;
103
- font-size: 13px; transition: all 0.15s;
104
- }
105
- .filter-btn:hover { color: var(--fg); border-color: var(--fg); }
106
- .filter-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
107
- .stats { color: var(--muted); font-size: 12px; }
108
- .memo {
109
- background: var(--card); border: 1px solid var(--border); border-radius: 8px;
110
- padding: 12px 16px; margin-bottom: 10px;
111
- display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
112
- }
113
- .memo .body { flex: 1; min-width: 0; }
114
- .memo .what {
115
- font-weight: 600; margin-bottom: 4px;
116
- display: flex; align-items: center; gap: 8px;
117
- }
118
- .memo .icon { font-size: 16px; }
119
- .memo .id { color: var(--muted); font-size: 11px; font-weight: normal; }
120
- .memo .original {
121
- color: var(--muted); margin-bottom: 6px; font-size: 13px;
122
- border-left: 2px solid var(--border); padding-left: 8px;
123
- }
124
- .memo .meta {
125
- color: var(--muted); font-size: 12px; margin-top: 4px;
126
- display: flex; gap: 12px; flex-wrap: wrap;
127
- }
128
- .memo .meta .who::before { content: '👤 '; }
129
- .memo .meta .when::before { content: '🗓 '; }
130
- .memo .meta .geo { color: var(--geo); }
131
- .memo .meta .geo::before { content: '📍 '; }
132
- .memo .meta .platform { opacity: 0.7; }
133
- .memo .meta .expires { color: var(--warn); }
134
- .memo .meta .expires::before { content: '⏳ '; }
135
- .memo .maps { margin-top: 6px; font-size: 12px; display: flex; gap: 12px; flex-wrap: wrap; }
136
- .memo .maps a { color: var(--accent); text-decoration: none; }
137
- .memo .maps a:hover { text-decoration: underline; }
138
- .actions { display: flex; gap: 6px; flex-shrink: 0; }
139
- .btn {
140
- padding: 5px 10px; border: 1px solid var(--border); border-radius: 5px;
141
- background: transparent; color: var(--fg); cursor: pointer; font-size: 12px;
142
- }
143
- .btn:hover { border-color: var(--accent); color: var(--accent); }
144
- .btn.danger:hover { border-color: var(--danger); color: var(--danger); }
145
- .empty { color: var(--muted); text-align: center; padding: 60px 0; }
146
- .toast {
147
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
148
- background: var(--card); border: 1px solid var(--border); padding: 10px 16px;
149
- border-radius: 6px; font-size: 13px;
150
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
151
- transition: opacity 0.2s;
152
- }
153
- .toast.error { color: var(--danger); border-color: var(--danger); }
154
- @media (max-width: 600px) {
155
- .memo { flex-direction: column; }
156
- .actions { width: 100%; justify-content: flex-end; }
157
- }
158
- </style>
159
- </head>
160
- <body>
161
- <header>
162
- <h1 id="page-title"></h1>
163
- <button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
164
- <select id="langSelect" title="Language / 语言">
165
- <option value="en">EN</option>
166
- <option value="zh">中文</option>
167
- </select>
168
- <a href="/" id="lnk-chat"></a>
169
- <a href="/tasks" id="lnk-tasks"></a>
170
- <a href="/reminders" id="lnk-reminders"></a>
171
- <a href="/settings" id="lnk-settings"></a>
172
- </header>
173
- <main>
174
- <div class="toolbar">
175
- <input type="text" id="q" />
176
- <button type="button" class="filter-btn active" data-filter="all" id="btn-filter-all"></button>
177
- <button type="button" class="filter-btn" data-filter="geo" id="btn-filter-geo"></button>
178
- <button type="button" class="filter-btn" data-filter="expired" id="btn-filter-expired"></button>
179
- <span class="stats" id="stats"></span>
180
- </div>
181
- <div id="list"></div>
182
- </main>
183
- <div id="toast" class="toast" style="display:none"></div>
184
-
185
- <script>
186
- (() => {
187
- const T = window.__t;
188
- const $list = document.getElementById('list')
189
- const $toast = document.getElementById('toast')
190
- const $q = document.getElementById('q')
191
- const $stats = document.getElementById('stats')
192
- let currentFilter = 'all'
193
- let queryStr = ''
194
- let debounceTimer = null
195
-
196
- // i18n bind
197
- document.getElementById('page-title').textContent = T.h1;
198
- document.getElementById('lnk-chat').textContent = T.backToChat;
199
- document.getElementById('lnk-tasks').textContent = T.toTasks;
200
- document.getElementById('lnk-reminders').textContent = T.toReminders;
201
- document.getElementById('lnk-settings').textContent = T.toSettings;
202
- document.getElementById('btn-filter-all').textContent = T.filterAll;
203
- document.getElementById('btn-filter-geo').textContent = T.filterGeo;
204
- document.getElementById('btn-filter-expired').textContent = T.filterExpired;
205
- $q.placeholder = T.searchPlaceholder;
206
- if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
207
- (function setupLangSwitcher() {
208
- const sel = document.getElementById('langSelect');
209
- if (!sel) return;
210
- sel.value = window.__lang;
211
- sel.addEventListener('change', () => {
212
- if (sel.value === window.__lang) return;
213
- localStorage.setItem('im-hub-lang', sel.value);
214
- window.location.reload();
215
- });
216
- })();
217
-
218
- function toast(msg, isErr = false) {
219
- $toast.textContent = msg
220
- $toast.className = 'toast' + (isErr ? ' error' : '')
221
- $toast.style.display = 'block'
222
- setTimeout(() => { $toast.style.display = 'none' }, 2400)
223
- }
224
-
225
- function escapeHtml(s) {
226
- if (s == null) return ''
227
- return String(s)
228
- .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
229
- .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
230
- }
231
-
232
- function fmtTime(iso) {
233
- if (!iso) return ''
234
- // memos use 'YYYY-MM-DD HH:MM:SS' (UTC+8 bare-local).
235
- const d = new Date(iso.replace(' ', 'T') + '+08:00')
236
- if (Number.isNaN(d.getTime())) return iso
237
- const isZh = window.__lang === 'zh'
238
- const now = new Date()
239
- const diffMs = now.getTime() - d.getTime()
240
- const absMin = Math.abs(Math.round(diffMs / 60000))
241
- let rel = ''
242
- if (Math.abs(diffMs) < 60_000) rel = isZh ? '刚才' : 'just now'
243
- else if (absMin < 60) rel = isZh ? `${absMin} 分钟前` : `${absMin} min ago`
244
- else if (absMin < 1440) rel = isZh ? `${(absMin / 60).toFixed(1)} 小时前` : `${(absMin / 60).toFixed(1)} h ago`
245
- else rel = isZh ? `${(absMin / 1440).toFixed(1)} 天前` : `${(absMin / 1440).toFixed(1)} d ago`
246
- return `${d.toLocaleString()} · ${rel}`
247
- }
248
-
249
- async function load() {
250
- $list.innerHTML = `<div class="empty">${T.loading}</div>`
251
- try {
252
- const params = new URLSearchParams()
253
- if (queryStr) params.set('query', queryStr)
254
- if (currentFilter === 'geo') params.set('has_location', 'true')
255
- if (currentFilter === 'expired') params.set('include_expired', 'true')
256
- params.set('limit', '200')
257
- const data = await window.imhub.api(`/api/memos?${params.toString()}`)
258
- let items = data.memos || []
259
- if (currentFilter === 'expired') {
260
- // Only the rows that have an expires_at in the past.
261
- const now = new Date()
262
- items = items.filter(m => m.expiresAt && new Date(m.expiresAt.replace(' ', 'T') + '+08:00') < now)
263
- }
264
- render(items)
265
- $stats.textContent = items.length ? (window.__lang === 'zh' ? `${items.length} 条` : `${items.length} memos`) : ''
266
- } catch (err) {
267
- $list.innerHTML = ''
268
- toast(T.loadFailed.replace('{err}', err.message), true)
269
- }
270
- }
271
-
272
- function render(items) {
273
- if (!items.length) {
274
- $list.innerHTML = `<div class="empty">${queryStr ? T.emptyNoMatch : T.emptyNone}</div>`
275
- return
276
- }
277
- $list.innerHTML = items.map(m => {
278
- const icon = m.where_lat != null ? '📍' : '📝'
279
- const idLbl = `<span class="id">#${m.id}</span>`
280
- const what = `<div class="what"><span class="icon">${icon}</span>${escapeHtml(m.what)} ${idLbl}</div>`
281
- const original = (m.memo && m.memo !== m.what)
282
- ? `<div class="original">${escapeHtml(m.memo)}</div>` : ''
283
- const metaBits = []
284
- if (m.who) metaBits.push(`<span class="who">${escapeHtml(m.who)}</span>`)
285
- if (m.whenAt) metaBits.push(`<span class="when">${escapeHtml(m.whenAt)}</span>`)
286
- else if (m.whenText) metaBits.push(`<span class="when">${escapeHtml(m.whenText)}</span>`)
287
- if (m.where_label) metaBits.push(`<span class="geo">${escapeHtml(m.where_label)}</span>`)
288
- else if (m.where_lat != null) metaBits.push(`<span class="geo">${m.where_lat.toFixed(5)}, ${m.where_lng.toFixed(5)}</span>`)
289
- if (m.platform) metaBits.push(`<span class="platform">${escapeHtml(m.platform)}</span>`)
290
- if (m.expiresAt) metaBits.push(`<span class="expires">${escapeHtml(m.expiresAt)}</span>`)
291
- metaBits.push(`<span class="when">${fmtTime(m.createdAt)}</span>`)
292
- const meta = `<div class="meta">${metaBits.join('')}</div>`
293
-
294
- let maps = ''
295
- if (m.mapUrls) {
296
- maps = `<div class="maps">
297
- <a href="${m.mapUrls.baidu}" target="_blank">${T.mapBaidu}</a>
298
- <a href="${m.mapUrls.amap}" target="_blank">${T.mapAmap}</a>
299
- <a href="${m.mapUrls.google}" target="_blank">${T.mapGoogle}</a>
300
- </div>`
301
- }
302
-
303
- return `<div class="memo" data-id="${m.id}">
304
- <div class="body">${what}${original}${meta}${maps}</div>
305
- <div class="actions">
306
- <button type="button" class="btn danger" data-action="delete">${T.delete}</button>
307
- </div>
308
- </div>`
309
- }).join('')
310
- }
311
-
312
- $list.addEventListener('click', async (e) => {
313
- const btn = e.target.closest('button[data-action]')
314
- if (!btn) return
315
- const card = btn.closest('.memo')
316
- if (!card) return
317
- const id = card.dataset.id
318
- const action = btn.dataset.action
319
- if (action === 'delete') {
320
- if (!confirm(T.confirmDelete.replace('{id}', id))) return
321
- try {
322
- await window.imhub.api(`/api/memos/${id}`, { method: 'DELETE' })
323
- toast(T.deleted.replace('{id}', id))
324
- load()
325
- } catch (err) { toast(T.deleteFailed.replace('{err}', err.message), true) }
326
- }
327
- })
328
-
329
- document.querySelectorAll('.filter-btn').forEach(btn => {
330
- btn.addEventListener('click', () => {
331
- document.querySelectorAll('.filter-btn').forEach(b => { b.classList.remove('active') })
332
- btn.classList.add('active')
333
- currentFilter = btn.dataset.filter
334
- load()
335
- })
336
- })
337
-
338
- $q.addEventListener('input', () => {
339
- clearTimeout(debounceTimer)
340
- debounceTimer = setTimeout(() => {
341
- queryStr = $q.value.trim()
342
- load()
343
- }, 280)
344
- })
345
-
346
- load()
347
- // Auto-refresh every 30s so newly-saved memos show up.
348
- setInterval(load, 30_000)
349
- })()
350
- </script>
351
- </body>
352
- </html>
@@ -1,332 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Agim — Reminders</title>
7
- <script src="/_app.js"></script>
8
- <script>
9
- // i18n bootstrap — mirrors tasks.html. _app.js reads window.__lang.
10
- (function () {
11
- const LANGS = { en: 'en', zh: 'zh' };
12
- const savedLang = localStorage.getItem('im-hub-lang');
13
- const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
14
- window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
15
- document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
16
- const T = {
17
- en: {
18
- title: 'Agim — Reminders', h1: '🔔 Reminders',
19
- backToChat: '↩ Chat', toTasks: 'Tasks', toMemos: 'Memos', toSettings: 'Settings',
20
- statusPending: 'Pending', statusFired: 'Fired', statusCancelled: 'Cancelled', statusFailed: 'Failed',
21
- loading: 'Loading…',
22
- loadFailed: 'Load failed: {err}',
23
- emptyStatus: 'No {status} reminders.',
24
- cancel: 'Cancel', snooze: 'Snooze +5min', delete: 'Delete',
25
- fireAt: 'Fires at', recur: 'Repeat',
26
- literalText: 'literal',
27
- confirmCancel: 'Cancel reminder #{id}? (Recurring reminders will stop the whole loop.)',
28
- cancelled: '✅ Cancelled #{id}',
29
- snoozed: '⏰ Snoozed #{id} +5min',
30
- },
31
- zh: {
32
- title: 'Agim — 提醒', h1: '🔔 提醒',
33
- backToChat: '↩ 对话', toTasks: '任务', toMemos: '备忘', toSettings: '设置',
34
- statusPending: '待发', statusFired: '已发', statusCancelled: '已取消', statusFailed: '失败',
35
- loading: '加载中…',
36
- loadFailed: '加载失败:{err}',
37
- emptyStatus: '没有{status}的提醒',
38
- cancel: '取消', snooze: '延 5 分钟', delete: '删除',
39
- fireAt: '触发时间', recur: '重复',
40
- literalText: '字面文本',
41
- confirmCancel: '取消提醒 #{id}?(循环提醒会终止整条循环)',
42
- cancelled: '✅ 已取消 #{id}',
43
- snoozed: '⏰ 已延后 #{id} 5 分钟',
44
- },
45
- };
46
- window.__t = T[window.__lang];
47
- document.title = T[window.__lang].title;
48
- })();
49
- </script>
50
- <style>
51
- :root {
52
- --bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
53
- --card: #fff; --accent: #2563eb; --danger: #dc2626; --warn: #d97706;
54
- --recur: #7c3aed;
55
- }
56
- [data-theme="dark"] {
57
- --bg: #1a1a1a; --fg: #e5e5e5; --muted: #999; --border: #333;
58
- --card: #242424; --accent: #60a5fa; --danger: #f87171; --warn: #fbbf24;
59
- --recur: #a78bfa;
60
- }
61
- body {
62
- font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif;
63
- background: var(--bg); color: var(--fg);
64
- margin: 0; padding: 0;
65
- }
66
- /* Header — kept in sync with tasks.html so all dashboard pages share
67
- the same layout. Flex with h1 flex:1 to push toggles + nav to the
68
- right; uniform border-button look across <a> and <button>. */
69
- header {
70
- display: flex;
71
- align-items: center;
72
- gap: 16px;
73
- padding: 14px 24px;
74
- border-bottom: 1px solid var(--border);
75
- background: var(--card);
76
- }
77
- header h1 { margin: 0; font-size: 18px; font-weight: 600; flex: 1; }
78
- header a, header button, header select {
79
- color: var(--accent);
80
- text-decoration: none;
81
- font-size: 14px;
82
- background: none;
83
- border: 1px solid var(--border);
84
- padding: 6px 12px;
85
- border-radius: 4px;
86
- cursor: pointer;
87
- }
88
- header select { color: var(--fg); }
89
- header a:hover, header button:hover { border-color: var(--accent); }
90
- main { max-width: 880px; margin: 24px auto; padding: 0 16px; }
91
- .filters {
92
- display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
93
- }
94
- .filter-btn {
95
- padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px;
96
- background: var(--card); color: var(--fg); cursor: pointer;
97
- font-size: 13px;
98
- }
99
- .filter-btn.active {
100
- background: var(--accent); color: #fff; border-color: var(--accent);
101
- }
102
- .empty {
103
- text-align: center; color: var(--muted); padding: 60px 20px;
104
- background: var(--card); border-radius: 8px; border: 1px dashed var(--border);
105
- }
106
- .reminder {
107
- display: flex; align-items: flex-start; gap: 12px;
108
- padding: 14px 16px; margin-bottom: 8px;
109
- background: var(--card); border: 1px solid var(--border); border-radius: 8px;
110
- }
111
- .icon { font-size: 20px; flex-shrink: 0; line-height: 1; padding-top: 1px; }
112
- .body { flex: 1; min-width: 0; }
113
- .text { font-weight: 500; word-break: break-word; }
114
- .meta {
115
- color: var(--muted); font-size: 12px; margin-top: 4px;
116
- display: flex; gap: 12px; flex-wrap: wrap;
117
- }
118
- .meta .recur { color: var(--recur); }
119
- .meta .literal { color: var(--warn); }
120
- .actions { display: flex; gap: 6px; flex-shrink: 0; }
121
- .btn {
122
- padding: 5px 10px; border: 1px solid var(--border); border-radius: 5px;
123
- background: transparent; color: var(--fg); cursor: pointer;
124
- font-size: 12px;
125
- }
126
- .btn:hover { border-color: var(--accent); color: var(--accent); }
127
- .btn.danger:hover { border-color: var(--danger); color: var(--danger); }
128
- .toast {
129
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
130
- background: var(--card); border: 1px solid var(--border); padding: 10px 16px;
131
- border-radius: 6px; font-size: 13px;
132
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
133
- transition: opacity 0.2s;
134
- }
135
- .toast.error { color: var(--danger); border-color: var(--danger); }
136
- @media (max-width: 600px) {
137
- .reminder { flex-direction: column; }
138
- .actions { width: 100%; justify-content: flex-end; }
139
- }
140
- </style>
141
- </head>
142
- <body>
143
- <header>
144
- <h1 id="page-title"></h1>
145
- <button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
146
- <select id="langSelect" title="Language / 语言">
147
- <option value="en">EN</option>
148
- <option value="zh">中文</option>
149
- </select>
150
- <a href="/" id="lnk-chat"></a>
151
- <a href="/tasks" id="lnk-tasks"></a>
152
- <a href="/memos" id="lnk-memos"></a>
153
- <a href="/settings" id="lnk-settings"></a>
154
- </header>
155
- <main>
156
- <div class="filters" id="filters">
157
- <button type="button" class="filter-btn active" data-status="pending" id="btn-status-pending"></button>
158
- <button type="button" class="filter-btn" data-status="fired" id="btn-status-fired"></button>
159
- <button type="button" class="filter-btn" data-status="cancelled" id="btn-status-cancelled"></button>
160
- <button type="button" class="filter-btn" data-status="failed" id="btn-status-failed"></button>
161
- </div>
162
- <div id="list"></div>
163
- </main>
164
- <div id="toast" class="toast" style="display:none"></div>
165
-
166
- <script>
167
- (() => {
168
- const T = window.__t;
169
- const $list = document.getElementById('list')
170
- const $toast = document.getElementById('toast')
171
- const $filters = document.getElementById('filters')
172
- let currentStatus = 'pending'
173
-
174
- // i18n bind — header labels, status filter labels, lang switcher.
175
- document.getElementById('page-title').textContent = T.h1;
176
- document.getElementById('lnk-chat').textContent = T.backToChat;
177
- document.getElementById('lnk-tasks').textContent = T.toTasks;
178
- document.getElementById('lnk-memos').textContent = T.toMemos;
179
- document.getElementById('lnk-settings').textContent = T.toSettings;
180
- document.getElementById('btn-status-pending').textContent = T.statusPending;
181
- document.getElementById('btn-status-fired').textContent = T.statusFired;
182
- document.getElementById('btn-status-cancelled').textContent = T.statusCancelled;
183
- document.getElementById('btn-status-failed').textContent = T.statusFailed;
184
- if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
185
- (function setupLangSwitcher() {
186
- const sel = document.getElementById('langSelect');
187
- if (!sel) return;
188
- sel.value = window.__lang;
189
- sel.addEventListener('change', () => {
190
- if (sel.value === window.__lang) return;
191
- localStorage.setItem('im-hub-lang', sel.value);
192
- window.location.reload();
193
- });
194
- })();
195
-
196
- function toast(msg, isErr = false) {
197
- $toast.textContent = msg
198
- $toast.className = `toast${isErr ? ' error' : ''}`
199
- $toast.style.display = 'block'
200
- setTimeout(() => { $toast.style.display = 'none' }, 2500)
201
- }
202
-
203
- function fmtTime(s) {
204
- if (!s) return '-'
205
- // Normalize SQLite "YYYY-MM-DD HH:MM:SS" to RFC3339 — Safari/WebView
206
- // reject the space-separated form (Invalid Date).
207
- let iso = String(s).trim()
208
- if (iso && !iso.includes('T')) iso = iso.replace(' ', 'T')
209
- if (iso && !/[Z+]/.test(iso.slice(-6))) iso = iso + 'Z'
210
- const d = new Date(iso)
211
- if (Number.isNaN(d.getTime())) return s
212
- const isZh = window.__lang === 'zh'
213
- const now = new Date()
214
- const diffMs = d.getTime() - now.getTime()
215
- const absMin = Math.abs(Math.round(diffMs / 60000))
216
- const past = diffMs <= 0
217
- let rel = ''
218
- if (Math.abs(diffMs) < 60_000) rel = isZh ? '刚才' : 'just now'
219
- else if (absMin < 60) rel = isZh
220
- ? `${past ? '已过 ' : ''}${absMin} 分钟${past ? '' : '后'}`
221
- : `${absMin} min ${past ? 'ago' : 'from now'}`
222
- else if (absMin < 1440) rel = isZh
223
- ? `${past ? '已过 ' : ''}${(absMin / 60).toFixed(1)} 小时${past ? '' : '后'}`
224
- : `${(absMin / 60).toFixed(1)} h ${past ? 'ago' : 'from now'}`
225
- else rel = isZh
226
- ? `${past ? '已过 ' : ''}${(absMin / 1440).toFixed(1)} 天${past ? '' : '后'}`
227
- : `${(absMin / 1440).toFixed(1)} d ${past ? 'ago' : 'from now'}`
228
- return `${d.toLocaleString()} · ${rel}`
229
- }
230
-
231
- async function load() {
232
- $list.innerHTML = `<div class="empty">${T.loading}</div>`
233
- try {
234
- const data = await window.imhub.api(`/api/reminders?status=${encodeURIComponent(currentStatus)}`)
235
- render(data.reminders || [])
236
- } catch (err) {
237
- $list.innerHTML = ''
238
- toast(T.loadFailed.replace('{err}', err.message), true)
239
- }
240
- }
241
-
242
- function render(items) {
243
- if (!items.length) {
244
- $list.innerHTML = `<div class="empty">${T.emptyStatus.replace('{status}', statusLabel(currentStatus))}</div>`
245
- return
246
- }
247
- $list.innerHTML = items.map((r) => {
248
- const icon = r.recurrence ? '🔁' : '🔔'
249
- const recurMeta = r.recurrence_label
250
- ? `<span class="recur">↻ ${escapeHtml(r.recurrence_label)}</span>` : ''
251
- const literalMeta = r.prompt_mode === 'literal'
252
- ? `<span class="literal">${T.literalText}</span>` : ''
253
- const platformMeta = r.platform
254
- ? `<span>${escapeHtml(r.platform)}</span>` : ''
255
- const showActions = currentStatus === 'pending'
256
- const actions = showActions
257
- ? `<div class="actions">
258
- <button class="btn" data-act="snooze" data-id="${r.id}">${T.snooze}</button>
259
- <button class="btn danger" data-act="cancel" data-id="${r.id}">${T.cancel}</button>
260
- </div>`
261
- : ''
262
- return `<div class="reminder">
263
- <div class="icon">${icon}</div>
264
- <div class="body">
265
- <div class="text">${escapeHtml(r.text)}</div>
266
- <div class="meta">
267
- <span>#${r.id}</span>
268
- <span>${escapeHtml(fmtTime(r.fire_at))}</span>
269
- ${recurMeta}
270
- ${literalMeta}
271
- ${platformMeta}
272
- </div>
273
- </div>
274
- ${actions}
275
- </div>`
276
- }).join('')
277
- }
278
-
279
- function escapeHtml(s) {
280
- return String(s).replace(/[&<>"']/g, (c) => (
281
- { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c] || c
282
- ))
283
- }
284
-
285
- function statusLabel(s) {
286
- return ({
287
- pending: T.statusPending, fired: T.statusFired,
288
- cancelled: T.statusCancelled, failed: T.statusFailed,
289
- })[s] || s
290
- }
291
-
292
- $filters.addEventListener('click', (e) => {
293
- const t = e.target.closest('.filter-btn')
294
- if (!t) return
295
- currentStatus = t.dataset.status
296
- document.querySelectorAll('.filter-btn').forEach((b) => { b.classList.toggle('active', b === t) })
297
- load()
298
- })
299
-
300
- $list.addEventListener('click', async (e) => {
301
- const btn = e.target.closest('button[data-act]')
302
- if (!btn) return
303
- const id = btn.dataset.id
304
- const act = btn.dataset.act
305
- btn.disabled = true
306
- try {
307
- if (act === 'cancel') {
308
- if (!confirm(T.confirmCancel.replace('{id}', id))) return
309
- await window.imhub.api(`/api/reminders/${id}/cancel`, { method: 'POST' })
310
- toast(T.cancelled.replace('{id}', id))
311
- } else if (act === 'snooze') {
312
- await window.imhub.api(`/api/reminders/${id}/snooze`, {
313
- method: 'POST',
314
- body: JSON.stringify({ duration: '5m' }),
315
- })
316
- toast(T.snoozed.replace('{id}', id))
317
- }
318
- await load()
319
- } catch (err) {
320
- toast(T.loadFailed.replace('{err}', err.message), true)
321
- } finally {
322
- btn.disabled = false
323
- }
324
- })
325
-
326
- // Initial load + auto-refresh every 30s
327
- load()
328
- setInterval(load, 30_000)
329
- })()
330
- </script>
331
- </body>
332
- </html>