@rackbops/plugin-tracker 0.31.0 → 0.31.2
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 +3 -3
- package/dist/plugin.js +82 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ as in a server (plan 5.5, item 39).
|
|
|
51
51
|
| `/tasks` | registered | Your active tasks and the ones you receive, each with its next run; then your paused ones, each naming up to three people it could not DM (then how many more). A list longer than one message (Discord's 2000 characters) is cut between whole lines and keeps your paused tasks in preference to the active ones that do not fit, since their ids are what `/task resume` needs (any paused ones that still do not fit are counted); it says how many active ones it left out and, when the web area is set up, points to `/web`, which lists them all. |
|
|
52
52
|
| `/task done task` / `/task snooze task [until]` | the task's owner | Answers the task's latest reminder (snooze: an hour, or until `until`). |
|
|
53
53
|
| `/task decide task choice [amount]` | the renewal's owner | Answers the renewal's latest ask -- keep, cancel or renewed -- with the amount actually paid when it changed (a button cannot carry one). Use it instead of the button, not after it: a run is answered once. |
|
|
54
|
-
| `/task history task` | the owner, an accepted recipient, or an admin | The newest 10 runs -- due, status, how it was answered, text replies -- and the newest 5 of the task's changes, older ones counted; sent with Discord's suppress-embeds flag, so a link in a reply, a finding or an error does not unfurl into a preview; for a renewal, what each period cost and the total; for a price, the last check and the readings with the low and the high; for a want-list watch, how many listings it has kept, and once there are any, a second message with the newest of them, up to 20 and as many as fit in one message, then how many more there are (and, while the web area is set up, that the task's page there shows the newest 50). |
|
|
55
|
-
| `/task share task user` | the task's owner | Invites an admitted, registered member who can be DMed: they get one consent DM with accept and decline. |
|
|
54
|
+
| `/task history task` | the owner, an accepted recipient, or an admin | The newest 10 runs -- due, status, how it was answered, text replies -- and the newest 5 of the task's changes, older ones counted (only the shown rows are loaded; SQLite still counts the rest, and for the runs sorts them too); sent with Discord's suppress-embeds flag, so a link in a reply, a finding or an error does not unfurl into a preview; for a renewal, what each period cost and the total; for a price, the last check and the readings with the low and the high; for a want-list watch, how many listings it has kept, and once there are any, a second message with the newest of them, up to 20 and as many as fit in one message, then how many more there are (and, while the web area is set up, that the task's page there shows the newest 50). |
|
|
55
|
+
| `/task share task user` | the task's owner | Invites an admitted, registered member who can be DMed: they get one consent DM with accept and decline. An invitation still open when the task is deleted or finishes is withdrawn, with a history event; a late Accept or Decline is refused with "That task was deleted." or "That task is done." and records nothing (one on a paused task still counts). |
|
|
56
56
|
| `/task resume task` | the task's owner | Resumes a paused task: one paused because a recipient could not be DMed goes on without that recipient; one its owner paused on the web simply goes on. |
|
|
57
57
|
| `/settings hour hour` | registered | The preferred hour; recurring reminders with no time of their own move to it. |
|
|
58
58
|
| `/web` | registered | A one-time link to sign in to the web area (below), good for 10 minutes and one use. Says so when `TRACKER_WEB_URL` is unset. |
|
|
@@ -703,7 +703,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
703
703
|
|---|---|---|
|
|
704
704
|
| Store | `src/store.ts`, `src/schema.ts` | docket's `Store` port on `bun:sqlite`, in `<dataDir>/tracker/tracker.sqlite` (WAL). A Discord id belongs to at most one user. Schema versioned by `PRAGMA user_version`; a shipped migration is never edited. Schema 5 (0.9.0) is docket 0.4.0's: a run's `record`, a series point's `key`, `deliveries` (from `delivery_claims`), `usage` and `notices`. Schema 6 (0.11.0) is docket 0.5.0's, all additive: `findings`, a charge's `usage.key`, and the Executor's `executor_jobs`. Forget-me erases a person's findings and their runs' Job records too. Schema 8 (0.19.0) adds `want_inbox`, the listings sent in for an inbox watch. Schema 9 (0.20.0) adds `users.usr_subject`, a person's usr user id, null until linked, unique when set. Schema 10 (0.25.0) adds `mail_addresses`. Schema 11 (0.28.0) adds `research_sources`, the pages a browser sent in for a research request (at most 10 per request, one per address, 99,800 characters of text in all), dropped once the request ends (by the execute tick's housekeeping, so only while the execute lane is configured) and always erased by forget-me. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
705
705
|
| Notify lane | `src/notify-lane.ts` | docket's `Lanes.tickNotify` on two host ticks, every 60 s, one pass per task with work (a due run, a run in flight, a DM owed or claimed), each under that task's lock through a view of the store limited to that task: `notify` runs every type but the page readers, `poll` runs only them (`price`, `wantlist`) with the Fetch port, so a slow page never holds up a reminder. A task the execute tick holds is left for the next notify tick, never waited on. Registers `reminder`, `renewal`, `price`, `research`, `scout`, `wantlist` and `wantjudge`. |
|
|
706
|
-
| Want-list watcher | `src/want.ts`, `src/inbox.ts`, `src/web/api-listings.ts`; the types and sources are `@rackbops/docket-types`' (0.7.0: `wantlist`, `wantjudge`, `pageSource`, `bggSource`, `inboxSource`) | `/want`'s rules; the `wantlist` type over a `Source` port; the `inbox` source over the `want_inbox` table (schema 8, 0.19.0: the listings sent in per watch, keyed `ebay-` or `bgg-` and 24 random hex digits, the newest 100 kept, as many as one look reads; forget-me erases them with the person's tasks), filled by the task API's listings route; the `page` source (JSON-LD listings through the fenced Fetch port, no HTML guessing) and the `bgg` source (BGG's XML API with the Bearer token, on `boardgamegeek.com`, never `www.`, at least 5 s between requests (a read due sooner is put back for the next tick, never waited on), read by a bounded `indexOf` parser, never a pattern over the whole body; a 401 or 403 is the token refused, a 202, 429 or 5xx a miss). The BGG fixture is hand-written to BGG's documented shape: BGG declined the application, so there is no token to capture a real response with. The judged watch (`wantjudge`) is an execute-lane type: its `prepare` reads the source in plain code (an uncharged `NoJob` when nothing is new), keeps new listings in the task's state as `pending` and asks for a follow-up, whose `prepare` makes the judge Job; `finish` maps the verdicts back by number, so a Job collected after a restart still has its listings. On this lane a BGG read due too soon waits out the spacing once. |
|
|
706
|
+
| Want-list watcher | `src/want.ts`, `src/inbox.ts`, `src/web/api-listings.ts`; the types and sources are `@rackbops/docket-types`' (0.7.0: `wantlist`, `wantjudge`, `pageSource`, `bggSource`, `inboxSource`) | `/want`'s rules; the `wantlist` type over a `Source` port; the `inbox` source over the `want_inbox` table (schema 8, 0.19.0: the listings sent in per watch, keyed `ebay-` or `bgg-` and 24 random hex digits, the newest 100 kept, as many as one look reads; a watch's rows go when it is deleted or finishes (#208), and forget-me erases them with the person's tasks), filled by the task API's listings route; the `page` source (JSON-LD listings through the fenced Fetch port, no HTML guessing) and the `bgg` source (BGG's XML API with the Bearer token, on `boardgamegeek.com`, never `www.`, at least 5 s between requests (a read due sooner is put back for the next tick, never waited on), read by a bounded `indexOf` parser, never a pattern over the whole body; a 401 or 403 is the token refused, a 202, 429 or 5xx a miss). The BGG fixture is hand-written to BGG's documented shape: BGG declined the application, so there is no token to capture a real response with. The judged watch (`wantjudge`) is an execute-lane type: its `prepare` reads the source in plain code (an uncharged `NoJob` when nothing is new), keeps new listings in the task's state as `pending` and asks for a follow-up, whose `prepare` makes the judge Job; `finish` maps the verdicts back by number, so a Job collected after a restart still has its listings. On this lane a BGG read due too soon waits out the spacing once. |
|
|
707
707
|
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts`, `src/research-sources.ts`, `src/web/api-sources.ts`, `src/scout.ts` (the `scout` and `research` types are docket-types'; `research` over `researchType({ readSources })`, 0.8.0, reading a browser request's pages from `research_sources`, and since 0.29.0 a fuller pass's, given only with the web area) | Only when the city-hall Executor is configured: a third host tick, `execute`, that starts docket's `Lanes.tickExecute` in the background (one at a time, at most once per host tick, about a minute) over every task with a due execute-lane run, reserving those tasks and then holding their locks in id order, through one docket `Lanes` kept while the plugin is active (docket keeps its usage-limit pause on it). The Executor is city-hall#18's source pair (above), its I/O injected; `executor_jobs` keeps each Job key's city-hall id. `/research`'s and `/scout`'s rules, and the scout type. |
|
|
708
708
|
| Budgets, usage | `src/ceilings.ts`, `src/usage.ts`; `src/web/ceiling-pages.ts`, `src/web/usage-pages.ts` | The execute lane's `BudgetPolicy` (docket's defaults plus each person's raise, or no ceiling at all while `TRACKER_BUDGET_UNLIMITED` is on), the ceiling changes, and the pure usage report `/admin/usage` renders from docket's `usage` table. |
|
|
709
709
|
| Page reads | `src/fetch.ts` | docket's `Fetch` port for `price` and the want-list's `page` source: http or https on the default port, no credentials, every resolved address public (no loopback, private, link-local, CGNAT, multicast or reserved range, IPv4 or IPv6), redirects followed by hand and re-checked (at most 5; a redirect to something that is not a web address is refused), 15 s including the name lookup, at most 3 MB kept. The body is then rebuilt in linear time (`src/page.ts`) to just what extraction reads -- at most 200 JSON-LD blocks and 300 meta tags, the first ones in page order (room for a shop search page with one JSON-LD block per product beside its other blocks; the `page` source keeps at most 100 listings a page, docket-types' `MAX_LISTINGS`, and a look takes at most 20 new ones, the rest on the next look), and the page with every `<` blanked, so `near` still reads text, attributes and script data -- because docket's extraction patterns take quadratic time on a page of unclosed tags. Tag names are matched with only `A`-`Z` folded to lower case, so a letter that changes length when lower-cased (the Turkish `İ`) does not shift the tags after it out of reach. A read the tick's abort cuts short requeues its run instead of counting a miss. A DNS answer that changes between the check and the read is not caught here. A refusal says why -- the name did not resolve, or it is not on the public internet -- so anyone who may make a price tracker or page watch (a command, the web editor or the task API) can learn which names the bot host's resolver maps to private addresses; one read at a time per person and the API's rate limit slow that, they do not stop it. A want-list page read (docket-types' `page` source) carries `NEVER_EBAY` and is refused on any hop to eBay; a price read is not. |
|
package/dist/plugin.js
CHANGED
|
@@ -120,13 +120,6 @@ async function visibleOccurrences(store, actor, taskId) {
|
|
|
120
120
|
const runs = await store.listOccurrences({ taskId });
|
|
121
121
|
return seesAll(actor, task) ? runs : runs.map((o) => ({ ...o, record: null }));
|
|
122
122
|
}
|
|
123
|
-
async function visibleReplies(store, actor, taskId) {
|
|
124
|
-
const task = await visibleTask(store, actor, taskId);
|
|
125
|
-
if (!task)
|
|
126
|
-
return null;
|
|
127
|
-
const replies = await store.listReplies(taskId);
|
|
128
|
-
return seesAll(actor, task) ? replies : replies.filter((r) => r.userId === actor.userId);
|
|
129
|
-
}
|
|
130
123
|
var ABOUT_A_RECIPIENT = new Set([
|
|
131
124
|
"recipient_invited",
|
|
132
125
|
"recipient_accepted",
|
|
@@ -136,15 +129,6 @@ var ABOUT_A_RECIPIENT = new Set([
|
|
|
136
129
|
"blocked",
|
|
137
130
|
"block_lifted"
|
|
138
131
|
]);
|
|
139
|
-
async function visibleHistory(store, actor, taskId) {
|
|
140
|
-
const task = await visibleTask(store, actor, taskId);
|
|
141
|
-
if (!task)
|
|
142
|
-
return null;
|
|
143
|
-
const events = await store.listTaskEvents(taskId);
|
|
144
|
-
if (seesAll(actor, task))
|
|
145
|
-
return events;
|
|
146
|
-
return events.filter((e) => !ABOUT_A_RECIPIENT.has(e.kind) || e.detail.split(" ")[0] === actor.userId);
|
|
147
|
-
}
|
|
148
132
|
async function visibleSeries(store, actor, taskId, filter) {
|
|
149
133
|
if (!await visibleTask(store, actor, taskId))
|
|
150
134
|
return null;
|
|
@@ -5014,6 +4998,17 @@ function toDelivery(r) {
|
|
|
5014
4998
|
settledAt: str2(r.settled_at)
|
|
5015
4999
|
};
|
|
5016
5000
|
}
|
|
5001
|
+
var ABOUT_A_RECIPIENT2 = ["recipient_invited", "recipient_accepted", "recipient_declined", "recipient_opted_out", "recipient_removed", "blocked", "block_lifted"];
|
|
5002
|
+
var PREV_PAUSE_OR_RESUME = "(SELECT x.kind FROM task_events x WHERE x.task_id = e.task_id AND x.kind IN ('paused', 'resumed') AND x.seq < e.seq ORDER BY x.seq DESC LIMIT 1)";
|
|
5003
|
+
function recipientShown(taskId, viewerId) {
|
|
5004
|
+
const about = ABOUT_A_RECIPIENT2.map((k) => `'${k}'`).join(", ");
|
|
5005
|
+
const sql = `e.task_id = ? AND (
|
|
5006
|
+
(e.kind NOT IN ('paused', 'resumed') AND (e.kind NOT IN (${about}) OR e.detail = ? OR instr(e.detail, ?) = 1))
|
|
5007
|
+
OR (e.kind = 'resumed' AND ${PREV_PAUSE_OR_RESUME} = 'paused')
|
|
5008
|
+
OR (e.kind = 'paused' AND NOT (${PREV_PAUSE_OR_RESUME} IS 'paused' AND instr(e.detail, 'delivery to ') = 1 AND instr(e.detail, ?) <> 1))
|
|
5009
|
+
)`;
|
|
5010
|
+
return [sql, [taskId, viewerId, `${viewerId} `, `delivery to {${viewerId}} paused:`]];
|
|
5011
|
+
}
|
|
5017
5012
|
function deleteDeliveriesOf(db, userId) {
|
|
5018
5013
|
return db.query("DELETE FROM deliveries WHERE user_id = ?").run(userId).changes;
|
|
5019
5014
|
}
|
|
@@ -5111,6 +5106,21 @@ class SqliteStore {
|
|
|
5111
5106
|
};
|
|
5112
5107
|
this.run(`UPDATE tasks SET title = ?, config = ?, state = ?, schedule = ?, capabilities = ?, status = ?, updated_at = ?
|
|
5113
5108
|
WHERE seq = ?`, next.title, json(next.config), json(next.state), json(next.schedule), json(next.capabilities), next.status, next.updatedAt, seqOf("t", id));
|
|
5109
|
+
const ended = (status) => status === "done" || status === "archived";
|
|
5110
|
+
if (ended(next.status) && !ended(cur.status)) {
|
|
5111
|
+
this.run("DELETE FROM want_inbox WHERE task_id = ?", id);
|
|
5112
|
+
const why = next.status === "archived" ? "deleted" : "finished";
|
|
5113
|
+
for (const r of this.all("SELECT * FROM task_recipients WHERE task_id = ? AND state = 'invited' ORDER BY seq", id).map(toRecipient)) {
|
|
5114
|
+
this.run("DELETE FROM task_recipients WHERE task_id = ? AND user_id = ?", id, r.userId);
|
|
5115
|
+
this.insertTaskEvent({
|
|
5116
|
+
taskId: id,
|
|
5117
|
+
actorId: null,
|
|
5118
|
+
kind: "recipient_removed",
|
|
5119
|
+
detail: `${r.userId}: the task was ${why}, so the invitation is withdrawn`,
|
|
5120
|
+
at: patch.at
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5114
5124
|
return toTask(this.mustGet("tasks", "t", id, "task"));
|
|
5115
5125
|
}
|
|
5116
5126
|
async listRecipients(taskId) {
|
|
@@ -5228,6 +5238,9 @@ class SqliteStore {
|
|
|
5228
5238
|
return this.all("SELECT * FROM events WHERE occurrence_id = ? AND seq > ? ORDER BY seq", occurrenceId, cursor ? Number(cursor.seq) : 0).map(toEvent);
|
|
5229
5239
|
}
|
|
5230
5240
|
async addTaskEvent(e) {
|
|
5241
|
+
return this.insertTaskEvent(e);
|
|
5242
|
+
}
|
|
5243
|
+
insertTaskEvent(e) {
|
|
5231
5244
|
const { lastInsertRowid } = this.run("INSERT INTO task_events (task_id, actor_id, kind, detail, at) VALUES (?, ?, ?, ?, ?)", e.taskId, e.actorId, e.kind, e.detail, e.at);
|
|
5232
5245
|
return toTaskEvent(this.mustGet("task_events", "h", idOf("h", lastInsertRowid), "task event"));
|
|
5233
5246
|
}
|
|
@@ -5241,6 +5254,33 @@ class SqliteStore {
|
|
|
5241
5254
|
async listReplies(taskId) {
|
|
5242
5255
|
return this.all("SELECT * FROM replies WHERE task_id = ? ORDER BY seq", taskId).map(toReply);
|
|
5243
5256
|
}
|
|
5257
|
+
async runsTail(taskId, now, limit) {
|
|
5258
|
+
const past = "task_id = ? AND NOT (status = 'queued' AND due_at > ?)";
|
|
5259
|
+
const total = Number(this.one(`SELECT COUNT(*) AS n FROM occurrences WHERE ${past}`, taskId, now)?.n ?? 0);
|
|
5260
|
+
const rows = this.all(`SELECT * FROM occurrences WHERE ${past} ORDER BY due_at DESC, created_at DESC, seq DESC LIMIT ?`, taskId, now, limit);
|
|
5261
|
+
const next = this.one("SELECT * FROM occurrences WHERE task_id = ? AND status = 'queued' AND due_at > ? ORDER BY due_at, created_at, seq LIMIT 1", taskId, now);
|
|
5262
|
+
return { rows: rows.reverse().map(toOccurrence), total, next: next ? toOccurrence(next) : null };
|
|
5263
|
+
}
|
|
5264
|
+
async repliesTo(taskId, occurrenceIds, userId) {
|
|
5265
|
+
if (occurrenceIds.length === 0)
|
|
5266
|
+
return [];
|
|
5267
|
+
const ids = `occurrence_id IN (${occurrenceIds.map(() => "?").join(", ")})`;
|
|
5268
|
+
const own = userId === null ? "" : " AND user_id = ?";
|
|
5269
|
+
return this.all(`SELECT * FROM replies WHERE task_id = ? AND ${ids}${own} ORDER BY seq`, taskId, ...occurrenceIds, ...userId === null ? [] : [userId]).map(toReply);
|
|
5270
|
+
}
|
|
5271
|
+
async changesTail(taskId, viewerId, limit) {
|
|
5272
|
+
if (viewerId === null) {
|
|
5273
|
+
const total = Number(this.one("SELECT COUNT(*) AS n FROM task_events WHERE task_id = ?", taskId)?.n ?? 0);
|
|
5274
|
+
const rows = this.all("SELECT * FROM task_events WHERE task_id = ? ORDER BY seq DESC LIMIT ?", taskId, limit).reverse();
|
|
5275
|
+
return { rows: rows.map(toTaskEvent), total, before: null };
|
|
5276
|
+
}
|
|
5277
|
+
const [shown, params] = recipientShown(taskId, viewerId);
|
|
5278
|
+
const total = Number(this.one(`SELECT COUNT(*) AS n FROM task_events e WHERE ${shown}`, ...params)?.n ?? 0);
|
|
5279
|
+
const rows = this.all(`SELECT e.* FROM task_events e WHERE ${shown} ORDER BY e.seq DESC LIMIT ?`, ...params, limit).reverse();
|
|
5280
|
+
const first = rows[0];
|
|
5281
|
+
const before = first ? this.one(`SELECT e.* FROM task_events e WHERE ${shown} AND e.kind IN ('paused', 'resumed') AND e.seq < ? ORDER BY e.seq DESC LIMIT 1`, ...params, Number(first.seq)) : null;
|
|
5282
|
+
return { rows: rows.map(toTaskEvent), total, before: before ? toTaskEvent(before) : null };
|
|
5283
|
+
}
|
|
5244
5284
|
async addSeriesPoint(p) {
|
|
5245
5285
|
const key = p.key ?? null;
|
|
5246
5286
|
const { changes, lastInsertRowid } = this.run("INSERT INTO series (task_id, key, at, value, unit, note) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (key) WHERE key IS NOT NULL DO NOTHING", p.taskId, key, p.at, p.value, p.unit ?? null, p.note ?? null);
|
|
@@ -9150,7 +9190,22 @@ async function press(d, user, ref) {
|
|
|
9150
9190
|
const taskId = await taskOfRef(d, ref);
|
|
9151
9191
|
return taskId === null ? pressLocked(d, user, ref) : d.locks.turn(taskId, () => pressLocked(d, user, ref), () => ({ ok: false, error: TASK_BUSY }));
|
|
9152
9192
|
}
|
|
9193
|
+
async function consentRefusal(d, ref) {
|
|
9194
|
+
const decoded = decodeReplyRef(ref);
|
|
9195
|
+
if (decoded?.kind !== "accept" && decoded?.kind !== "decline")
|
|
9196
|
+
return null;
|
|
9197
|
+
const taskId = await taskOfRef(d, ref);
|
|
9198
|
+
const task = taskId === null ? null : await d.store.getTask(taskId);
|
|
9199
|
+
if (!task || task.status === "archived")
|
|
9200
|
+
return "That task was deleted.";
|
|
9201
|
+
if (task.status === "done")
|
|
9202
|
+
return "That task is done.";
|
|
9203
|
+
return null;
|
|
9204
|
+
}
|
|
9153
9205
|
async function pressLocked(d, user, ref) {
|
|
9206
|
+
const refused = await consentRefusal(d, ref);
|
|
9207
|
+
if (refused)
|
|
9208
|
+
return { ok: false, error: refused };
|
|
9154
9209
|
const verdict = await replyForRef(d.store, ref, user.id);
|
|
9155
9210
|
if (!verdict.ok)
|
|
9156
9211
|
return { ok: false, error: verdict.error };
|
|
@@ -9616,14 +9671,14 @@ async function loadHistory(d, user, taskId) {
|
|
|
9616
9671
|
if (!task)
|
|
9617
9672
|
return null;
|
|
9618
9673
|
const now = d.clock.now();
|
|
9619
|
-
const all = await visibleOccurrences(d.store, actor, task.id) ?? [];
|
|
9620
|
-
const upcoming = (o) => o.status === "queued" && Date.parse(o.dueAt) > now.getTime();
|
|
9621
|
-
const runs = all.filter((o) => !upcoming(o));
|
|
9622
|
-
const next = all.find(upcoming);
|
|
9623
|
-
const replies = await visibleReplies(d.store, actor, task.id) ?? [];
|
|
9624
9674
|
const seesEveryone = user.admin || task.ownerId === user.id;
|
|
9625
|
-
const
|
|
9626
|
-
const
|
|
9675
|
+
const tailed = await d.store.runsTail(task.id, now.toISOString(), HISTORY_RUNS);
|
|
9676
|
+
const runs = seesEveryone ? tailed : { ...tailed, rows: tailed.rows.map((o) => ({ ...o, record: null })) };
|
|
9677
|
+
const next = runs.next;
|
|
9678
|
+
const replies = await d.store.repliesTo(task.id, runs.rows.map((o) => o.id), seesEveryone ? null : user.id);
|
|
9679
|
+
const tail = await d.store.changesTail(task.id, seesEveryone ? null : user.id, HISTORY_CHANGES);
|
|
9680
|
+
const lead = tail.before?.kind === "paused" ? [tail.before] : [];
|
|
9681
|
+
const changes = seesEveryone ? tail.rows : changesForRecipient([...lead, ...tail.rows], user.id).slice(lead.length);
|
|
9627
9682
|
const people = new Map;
|
|
9628
9683
|
for (const r of replies) {
|
|
9629
9684
|
if (!people.has(r.userId)) {
|
|
@@ -9648,10 +9703,10 @@ async function loadHistory(d, user, taskId) {
|
|
|
9648
9703
|
task,
|
|
9649
9704
|
cadence: task.schedule && owner ? describeSchedule(task.schedule, owner, user.timeZone, now) : "no schedule",
|
|
9650
9705
|
next: next ? formatInstant(next.dueAt, user.timeZone, now) : null,
|
|
9651
|
-
runs: runs.
|
|
9652
|
-
earlierRuns: Math.max(0, runs.
|
|
9653
|
-
changes: changes.
|
|
9654
|
-
earlierChanges: Math.max(0,
|
|
9706
|
+
runs: runs.rows.map((o) => runView(o, replies, people, user, now, seesEveryone)),
|
|
9707
|
+
earlierRuns: Math.max(0, runs.total - HISTORY_RUNS),
|
|
9708
|
+
changes: changes.map((e) => changeView(e, user, now, names)),
|
|
9709
|
+
earlierChanges: Math.max(0, tail.total - HISTORY_CHANGES),
|
|
9655
9710
|
findings: findings.map((f) => findingView(f, user, now))
|
|
9656
9711
|
};
|
|
9657
9712
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/plugin-tracker",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"description": "The task tracker for Rackbops Clerk: reminders, renewals, price drops, one-off research (on the web, or on pages your own browser gathers and sends in through the task API, with a fuller pass on pages the research could not open) and an interest scout (through city-hall, with their findings), a want-list watcher (listing pages you paste, BoardGameGeek through its API once its token is set, and eBay and BoardGameGeek listings you send in through the task API or that eBay's saved-search alert emails forwarded to your alert address carry, which the watcher never opens; with the model checking each new listing and its seller first, through city-hall) by slash command, delivered by DM with buttons, and a daily DM at each person's hour of what is due today and overdue, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /research, /scout, /want, /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 (research requests too, made but not changed, and scouts, made and changed), an admin view (with the DMs that did not arrive, raising a person's daily model ceiling, and daily model usage per person), an operator switch that turns the daily budgets off for the alpha, forget-me, and a JSON task API with personal API tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|