@rackbops/plugin-tracker 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/plugin.js +73 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -88,6 +88,7 @@ served at a hashed path, cached for a year).
|
|
|
88
88
|
| `/forget` | Forget me (below): what it deletes (GET); a POST asks for the confirmation; a POST with `confirm=yes` and the word `forget` erases. Linked from Settings. |
|
|
89
89
|
| `/admin` | Admins only: everyone on the list, the decline blocks in force (each with Lift), and a form to allow a person by Discord id. |
|
|
90
90
|
| `/admin/tasks` | Admins only: every task in the store, with its owner, status and how many receive it, each linking to its page. |
|
|
91
|
+
| `/admin/deliveries` | Admins only: the DMs that did not arrive (below), newest first. |
|
|
91
92
|
| `/admin/people/<id>` | Admins only: one person -- Discord id, registration, zone and hour, delivery, task counts, their tasks -- with Make admin or Revoke admin, Resume delivery (when paused), and Remove from the tracker. |
|
|
92
93
|
| `/admin/allow`, `/admin/people/<id>/grant`, `/revoke`, `/resume-delivery`, `/forget`, `/admin/blocks/<id>/lift`, `/admin/tokens/<id>/revoke` | Admins only, POST only: the acts below. |
|
|
93
94
|
| `/api/v1/...` | The JSON task API (below): bearer tokens only, never the cookie. |
|
|
@@ -186,6 +187,15 @@ the function the command path runs:
|
|
|
186
187
|
secret). It stops working at once; the owner is not told. Logged by token id.
|
|
187
188
|
- **Remove from the tracker**: forget-me for that person (below), with the same confirmation.
|
|
188
189
|
|
|
190
|
+
**Deliveries** (0.10.0). `/admin/deliveries`, linked from every admin page, lists the DMs that
|
|
191
|
+
settled `failed`, `unconfirmed` or `deferred` in the last 30 days, newest first, at most 200 rows
|
|
192
|
+
(the page says how many matched when the bound cuts it). Each row: the task (its title, linking to
|
|
193
|
+
its page, its id and owner), the run's due time, the recipient (linking to their admin page), the
|
|
194
|
+
status, attempts and deferrals, the error text, and when it settled. `unconfirmed` means the DM may
|
|
195
|
+
have gone out and is never resent; `deferred` is still owed (a recipient whose delivery is paused,
|
|
196
|
+
say). Before 0.10.0 these showed only in the bot log, where the warnings still go. The page is
|
|
197
|
+
read-only, and like every admin page answers anyone who is not an admin with the unknown page's 404.
|
|
198
|
+
|
|
189
199
|
Not here yet, though plan 5.10 lists them: grants, budgets and retry (they belong to the execute
|
|
190
200
|
lane, which is not built) and an admin pause of someone else's task.
|
|
191
201
|
|
package/dist/plugin.js
CHANGED
|
@@ -5343,6 +5343,9 @@ class SqliteStore {
|
|
|
5343
5343
|
|
|
5344
5344
|
// plugins/tracker/src/roster.ts
|
|
5345
5345
|
var FORGOTTEN = "forgotten";
|
|
5346
|
+
var UNDELIVERED = ["failed", "unconfirmed", "deferred"];
|
|
5347
|
+
var UNDELIVERED_DAYS = 30;
|
|
5348
|
+
var UNDELIVERED_LIMIT = 200;
|
|
5346
5349
|
function historyNames(kind, detail, id) {
|
|
5347
5350
|
switch (kind) {
|
|
5348
5351
|
case "recipient_invited":
|
|
@@ -5437,6 +5440,38 @@ class Roster {
|
|
|
5437
5440
|
admins() {
|
|
5438
5441
|
return this.db.query("SELECT COUNT(*) AS n FROM users WHERE admin = 1").get().n;
|
|
5439
5442
|
}
|
|
5443
|
+
undelivered(now, days = UNDELIVERED_DAYS, limit = UNDELIVERED_LIMIT) {
|
|
5444
|
+
const since = new Date(now.getTime() - days * 86400000).toISOString();
|
|
5445
|
+
const statuses = UNDELIVERED.map(() => "?").join(", ");
|
|
5446
|
+
const where = `d.status IN (${statuses}) AND COALESCE(d.settled_at, d.created_at) >= ?`;
|
|
5447
|
+
const total = this.db.query(`SELECT COUNT(*) AS n FROM deliveries d WHERE ${where}`).get(...UNDELIVERED, since).n;
|
|
5448
|
+
const rows = this.db.query(`SELECT d.occurrence_id, d.user_id, d.status, d.attempts, d.deferrals, d.error, d.settled_at, d.created_at,
|
|
5449
|
+
o.due_at, t.seq AS task_seq, t.title, t.owner_id
|
|
5450
|
+
FROM deliveries d
|
|
5451
|
+
LEFT JOIN occurrences o ON d.occurrence_id = 'o' || o.seq
|
|
5452
|
+
LEFT JOIN tasks t ON o.task_id = 't' || t.seq
|
|
5453
|
+
WHERE ${where}
|
|
5454
|
+
ORDER BY COALESCE(d.settled_at, d.created_at) DESC, d.seq DESC
|
|
5455
|
+
LIMIT ?`).all(...UNDELIVERED, since, limit);
|
|
5456
|
+
const str = (v) => v === null || v === undefined ? null : String(v);
|
|
5457
|
+
return {
|
|
5458
|
+
total: Number(total),
|
|
5459
|
+
rows: rows.map((r) => ({
|
|
5460
|
+
occurrenceId: String(r.occurrence_id),
|
|
5461
|
+
taskId: r.task_seq === null || r.task_seq === undefined ? null : `t${Number(r.task_seq)}`,
|
|
5462
|
+
title: str(r.title),
|
|
5463
|
+
ownerId: str(r.owner_id),
|
|
5464
|
+
dueAt: str(r.due_at),
|
|
5465
|
+
userId: String(r.user_id),
|
|
5466
|
+
status: String(r.status),
|
|
5467
|
+
attempts: Number(r.attempts),
|
|
5468
|
+
deferrals: Number(r.deferrals),
|
|
5469
|
+
error: str(r.error),
|
|
5470
|
+
settledAt: str(r.settled_at),
|
|
5471
|
+
createdAt: String(r.created_at)
|
|
5472
|
+
}))
|
|
5473
|
+
};
|
|
5474
|
+
}
|
|
5440
5475
|
activeBlocks(now) {
|
|
5441
5476
|
const rows = this.db.query(`SELECT seq, owner_id, recipient_id, expires_at, created_at FROM invite_blocks
|
|
5442
5477
|
WHERE lifted_at IS NULL AND (expires_at IS NULL OR expires_at > ?) ORDER BY seq DESC`).all(now.toISOString());
|
|
@@ -7375,7 +7410,7 @@ function hidden(v) {
|
|
|
7375
7410
|
return html`<input type="hidden" name="csrf" value="${v.csrf}">`;
|
|
7376
7411
|
}
|
|
7377
7412
|
function adminNav(v) {
|
|
7378
|
-
return html`<p class="tr-row"><a class="rb-link" href="${v.base}/admin">People</a> <a class="rb-link" href="${v.base}/admin/tasks">All tasks</a></p>`;
|
|
7413
|
+
return html`<p class="tr-row"><a class="rb-link" href="${v.base}/admin">People</a> <a class="rb-link" href="${v.base}/admin/tasks">All tasks</a> <a class="rb-link" href="${v.base}/admin/deliveries">Deliveries</a></p>`;
|
|
7379
7414
|
}
|
|
7380
7415
|
function personHref(v, id, action = "") {
|
|
7381
7416
|
return `${v.base}/admin/people/${encodeURIComponent(id)}${action ? `/${action}` : ""}`;
|
|
@@ -7451,6 +7486,36 @@ ${tasks.length === 0 ? html`<p class="rb-muted">There are no tasks.</p>` : html`
|
|
|
7451
7486
|
</table></div>`}
|
|
7452
7487
|
</section>`);
|
|
7453
7488
|
}
|
|
7489
|
+
var STATUS_WORDS = {
|
|
7490
|
+
failed: "failed",
|
|
7491
|
+
unconfirmed: "unconfirmed (may have gone out; never resent)",
|
|
7492
|
+
deferred: "deferred (still owed)"
|
|
7493
|
+
};
|
|
7494
|
+
function adminDeliveriesPage(v, data) {
|
|
7495
|
+
const tz = v.user.timeZone;
|
|
7496
|
+
const when = (iso) => iso ? formatInstant(iso, tz, data.now) : "--";
|
|
7497
|
+
const who = (id) => id === null ? "--" : data.names.get(id) ?? id;
|
|
7498
|
+
const rows = data.rows.map((d) => html`<tr>
|
|
7499
|
+
<td>${d.taskId ? html`<a class="rb-link" href="${taskHref(v, d.taskId)}">${d.title ?? d.taskId}</a>` : html`<span class="rb-muted">run ${d.occurrenceId} (task gone)</span>`}<br><span class="rb-muted">${d.taskId ? `${d.taskId}, owner ${who(d.ownerId)}` : null}</span></td>
|
|
7500
|
+
<td>${when(d.dueAt)}</td>
|
|
7501
|
+
<td><a class="rb-link" href="${personHref(v, d.userId)}">${who(d.userId)}</a></td>
|
|
7502
|
+
<td>${STATUS_WORDS[d.status] ?? d.status}</td>
|
|
7503
|
+
<td>${d.attempts}</td>
|
|
7504
|
+
<td>${d.deferrals}</td>
|
|
7505
|
+
<td>${d.error ?? "--"}</td>
|
|
7506
|
+
<td>${when(d.settledAt)}</td>
|
|
7507
|
+
</tr>`);
|
|
7508
|
+
const shown = data.total > data.rows.length ? `Showing the newest ${data.rows.length} of ${data.total}.` : `${data.total} in all.`;
|
|
7509
|
+
return framed(v, "Deliveries", html`<section>
|
|
7510
|
+
<h1>Deliveries that did not arrive</h1>
|
|
7511
|
+
${adminNav(v)}
|
|
7512
|
+
<p class="rb-muted">DMs settled failed, unconfirmed or deferred in the last ${UNDELIVERED_DAYS} days, newest first, at most ${UNDELIVERED_LIMIT}. ${data.rows.length === 0 ? null : shown}</p>
|
|
7513
|
+
${data.rows.length === 0 ? html`<p class="rb-muted">None in that time.</p>` : html`<div class="rb-table-scroll"><table class="rb-table">
|
|
7514
|
+
<thead><tr><th scope="col">Task</th><th scope="col">Run due</th><th scope="col">Recipient</th><th scope="col">Status</th><th scope="col">Attempts</th><th scope="col">Deferrals</th><th scope="col">Error</th><th scope="col">Settled</th></tr></thead>
|
|
7515
|
+
<tbody>${rows}</tbody>
|
|
7516
|
+
</table></div>`}
|
|
7517
|
+
</section>`);
|
|
7518
|
+
}
|
|
7454
7519
|
function post2(v, p, action, label, style) {
|
|
7455
7520
|
return html`<form method="post" action="${personHref(v, p.id, action)}">${hidden(v)}<button class="rb-btn ${style} rb-btn--sm" type="submit">${label}</button></form>`;
|
|
7456
7521
|
}
|
|
@@ -7553,6 +7618,10 @@ async function adminGet(a, r) {
|
|
|
7553
7618
|
if (r.kind === "admin-person")
|
|
7554
7619
|
return onePerson(a, r.id);
|
|
7555
7620
|
const people = new Map(a.d.roster.people().map((p) => [p.id, nameOf(p)]));
|
|
7621
|
+
if (r.kind === "admin-deliveries") {
|
|
7622
|
+
const now = a.d.clock.now();
|
|
7623
|
+
return htmlResponse(adminDeliveriesPage(a.v, { ...a.d.roster.undelivered(now), names: people, now }));
|
|
7624
|
+
}
|
|
7556
7625
|
const rows = [];
|
|
7557
7626
|
for (const task of await a.d.store.listTasks()) {
|
|
7558
7627
|
const receiving = (await a.d.store.listRecipients(task.id)).filter((x) => x.state === "accepted").length;
|
|
@@ -8482,6 +8551,8 @@ function route2(path) {
|
|
|
8482
8551
|
return { kind: "admin" };
|
|
8483
8552
|
if (path === "/admin/tasks")
|
|
8484
8553
|
return { kind: "admin-tasks" };
|
|
8554
|
+
if (path === "/admin/deliveries")
|
|
8555
|
+
return { kind: "admin-deliveries" };
|
|
8485
8556
|
if (path === "/admin/allow")
|
|
8486
8557
|
return { kind: "admin-allow" };
|
|
8487
8558
|
const personAct = PERSON_ACTION.exec(path);
|
|
@@ -8830,6 +8901,7 @@ function createWebHandler(w) {
|
|
|
8830
8901
|
return tokensGet(d, v);
|
|
8831
8902
|
case "admin":
|
|
8832
8903
|
case "admin-tasks":
|
|
8904
|
+
case "admin-deliveries":
|
|
8833
8905
|
case "admin-person":
|
|
8834
8906
|
return adminGet(a, r);
|
|
8835
8907
|
default:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/plugin-tracker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The task tracker for Rackbops Clerk: reminders, renewals and price drops by slash command, delivered by DM with buttons, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks, an admin view, forget-me, and a JSON task API with personal API tokens",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "The task tracker for Rackbops Clerk: reminders, renewals and price drops by slash command, delivered by DM with buttons, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks, an admin view (with the DMs that did not arrive), forget-me, and a JSON task API with personal API tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|
|
7
7
|
"files": ["dist"],
|