agim-cli 1.1.3 → 1.1.5
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/CHANGELOG.md +81 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/core/job-board.d.ts +5 -0
- package/dist/core/job-board.d.ts.map +1 -1
- package/dist/core/job-board.js +4 -0
- package/dist/core/job-board.js.map +1 -1
- package/dist/web/public/memos.html +116 -35
- package/dist/web/public/reminders.html +133 -35
- package/dist/web/public/tasks.html +569 -16
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +275 -4
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,48 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Agim — Reminders</title>
|
|
7
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>
|
|
8
50
|
<style>
|
|
9
51
|
:root {
|
|
10
52
|
--bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
|
|
@@ -21,17 +63,30 @@
|
|
|
21
63
|
background: var(--bg); color: var(--fg);
|
|
22
64
|
margin: 0; padding: 0;
|
|
23
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>. */
|
|
24
69
|
header {
|
|
25
|
-
display: flex;
|
|
26
|
-
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 16px;
|
|
73
|
+
padding: 14px 24px;
|
|
74
|
+
border-bottom: 1px solid var(--border);
|
|
27
75
|
background: var(--card);
|
|
28
76
|
}
|
|
29
|
-
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
30
|
-
header
|
|
31
|
-
|
|
32
|
-
|
|
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;
|
|
33
87
|
}
|
|
34
|
-
header
|
|
88
|
+
header select { color: var(--fg); }
|
|
89
|
+
header a:hover, header button:hover { border-color: var(--accent); }
|
|
35
90
|
main { max-width: 880px; margin: 24px auto; padding: 0 16px; }
|
|
36
91
|
.filters {
|
|
37
92
|
display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
|
|
@@ -86,20 +141,23 @@
|
|
|
86
141
|
</head>
|
|
87
142
|
<body>
|
|
88
143
|
<header>
|
|
89
|
-
<h1
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
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>
|
|
96
154
|
</header>
|
|
97
155
|
<main>
|
|
98
156
|
<div class="filters" id="filters">
|
|
99
|
-
<button type="button" class="filter-btn active" data-status="pending"
|
|
100
|
-
<button type="button" class="filter-btn" data-status="fired"
|
|
101
|
-
<button type="button" class="filter-btn" data-status="cancelled"
|
|
102
|
-
<button type="button" class="filter-btn" data-status="failed"
|
|
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>
|
|
103
161
|
</div>
|
|
104
162
|
<div id="list"></div>
|
|
105
163
|
</main>
|
|
@@ -107,11 +165,34 @@
|
|
|
107
165
|
|
|
108
166
|
<script>
|
|
109
167
|
(() => {
|
|
168
|
+
const T = window.__t;
|
|
110
169
|
const $list = document.getElementById('list')
|
|
111
170
|
const $toast = document.getElementById('toast')
|
|
112
171
|
const $filters = document.getElementById('filters')
|
|
113
172
|
let currentStatus = 'pending'
|
|
114
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
|
+
|
|
115
196
|
function toast(msg, isErr = false) {
|
|
116
197
|
$toast.textContent = msg
|
|
117
198
|
$toast.className = `toast${isErr ? ' error' : ''}`
|
|
@@ -119,34 +200,48 @@
|
|
|
119
200
|
setTimeout(() => { $toast.style.display = 'none' }, 2500)
|
|
120
201
|
}
|
|
121
202
|
|
|
122
|
-
function fmtTime(
|
|
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'
|
|
123
210
|
const d = new Date(iso)
|
|
124
|
-
if (Number.isNaN(d.getTime())) return
|
|
211
|
+
if (Number.isNaN(d.getTime())) return s
|
|
212
|
+
const isZh = window.__lang === 'zh'
|
|
125
213
|
const now = new Date()
|
|
126
214
|
const diffMs = d.getTime() - now.getTime()
|
|
127
215
|
const absMin = Math.abs(Math.round(diffMs / 60000))
|
|
216
|
+
const past = diffMs <= 0
|
|
128
217
|
let rel = ''
|
|
129
|
-
if (Math.abs(diffMs) < 60_000) rel = '刚才'
|
|
130
|
-
else if (absMin < 60) rel =
|
|
131
|
-
|
|
132
|
-
|
|
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'}`
|
|
133
228
|
return `${d.toLocaleString()} · ${rel}`
|
|
134
229
|
}
|
|
135
230
|
|
|
136
231
|
async function load() {
|
|
137
|
-
$list.innerHTML =
|
|
232
|
+
$list.innerHTML = `<div class="empty">${T.loading}</div>`
|
|
138
233
|
try {
|
|
139
234
|
const data = await window.imhub.api(`/api/reminders?status=${encodeURIComponent(currentStatus)}`)
|
|
140
235
|
render(data.reminders || [])
|
|
141
236
|
} catch (err) {
|
|
142
237
|
$list.innerHTML = ''
|
|
143
|
-
toast(
|
|
238
|
+
toast(T.loadFailed.replace('{err}', err.message), true)
|
|
144
239
|
}
|
|
145
240
|
}
|
|
146
241
|
|
|
147
242
|
function render(items) {
|
|
148
243
|
if (!items.length) {
|
|
149
|
-
$list.innerHTML = `<div class="empty"
|
|
244
|
+
$list.innerHTML = `<div class="empty">${T.emptyStatus.replace('{status}', statusLabel(currentStatus))}</div>`
|
|
150
245
|
return
|
|
151
246
|
}
|
|
152
247
|
$list.innerHTML = items.map((r) => {
|
|
@@ -154,14 +249,14 @@
|
|
|
154
249
|
const recurMeta = r.recurrence_label
|
|
155
250
|
? `<span class="recur">↻ ${escapeHtml(r.recurrence_label)}</span>` : ''
|
|
156
251
|
const literalMeta = r.prompt_mode === 'literal'
|
|
157
|
-
?
|
|
252
|
+
? `<span class="literal">${T.literalText}</span>` : ''
|
|
158
253
|
const platformMeta = r.platform
|
|
159
254
|
? `<span>${escapeHtml(r.platform)}</span>` : ''
|
|
160
255
|
const showActions = currentStatus === 'pending'
|
|
161
256
|
const actions = showActions
|
|
162
257
|
? `<div class="actions">
|
|
163
|
-
<button class="btn" data-act="snooze" data-id="${r.id}"
|
|
164
|
-
<button class="btn danger" data-act="cancel" data-id="${r.id}"
|
|
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>
|
|
165
260
|
</div>`
|
|
166
261
|
: ''
|
|
167
262
|
return `<div class="reminder">
|
|
@@ -188,7 +283,10 @@
|
|
|
188
283
|
}
|
|
189
284
|
|
|
190
285
|
function statusLabel(s) {
|
|
191
|
-
return ({
|
|
286
|
+
return ({
|
|
287
|
+
pending: T.statusPending, fired: T.statusFired,
|
|
288
|
+
cancelled: T.statusCancelled, failed: T.statusFailed,
|
|
289
|
+
})[s] || s
|
|
192
290
|
}
|
|
193
291
|
|
|
194
292
|
$filters.addEventListener('click', (e) => {
|
|
@@ -207,19 +305,19 @@
|
|
|
207
305
|
btn.disabled = true
|
|
208
306
|
try {
|
|
209
307
|
if (act === 'cancel') {
|
|
210
|
-
if (!confirm(
|
|
308
|
+
if (!confirm(T.confirmCancel.replace('{id}', id))) return
|
|
211
309
|
await window.imhub.api(`/api/reminders/${id}/cancel`, { method: 'POST' })
|
|
212
|
-
toast(
|
|
310
|
+
toast(T.cancelled.replace('{id}', id))
|
|
213
311
|
} else if (act === 'snooze') {
|
|
214
312
|
await window.imhub.api(`/api/reminders/${id}/snooze`, {
|
|
215
313
|
method: 'POST',
|
|
216
314
|
body: JSON.stringify({ duration: '5m' }),
|
|
217
315
|
})
|
|
218
|
-
toast(
|
|
316
|
+
toast(T.snoozed.replace('{id}', id))
|
|
219
317
|
}
|
|
220
318
|
await load()
|
|
221
319
|
} catch (err) {
|
|
222
|
-
toast(
|
|
320
|
+
toast(T.loadFailed.replace('{err}', err.message), true)
|
|
223
321
|
} finally {
|
|
224
322
|
btn.disabled = false
|
|
225
323
|
}
|