@rackbops/plugin-tracker 0.26.0 → 0.28.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 +97 -55
- package/dist/plugin.js +1367 -768
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -25,8 +25,10 @@ for these; your own browser finds them, such as Claude in Chrome running in it -
|
|
|
25
25
|
listing within your limits once, until you press Done, and, while the model runner is set up, has
|
|
26
26
|
the model look at each new listing first -- is it the thing, and what does it show about the
|
|
27
27
|
seller -- before DMing it -- and the daily "today and
|
|
28
|
-
overdue" digest (plan 5.5): one DM a day at each person's preferred hour listing their
|
|
29
|
-
and renewals due today or overdue
|
|
28
|
+
overdue" digest (plan 5.5): at most one DM a day at each person's preferred hour listing their
|
|
29
|
+
reminders and renewals due today or overdue, never two due within 20 hours of each other. That
|
|
30
|
+
skips one local day after a move east into a day whose hour has come, after moving the preferred
|
|
31
|
+
hour more than 4 hours earlier, and after a move west across the date line. The model runs never happen here: they go
|
|
30
32
|
through city-hall to Rackbops/docket-runner on roshne's own host (plan 5.12), and the whole execute
|
|
31
33
|
lane stays off until the city-hall Executor is configured (Configuration below).
|
|
32
34
|
|
|
@@ -37,19 +39,19 @@ as in a server (plan 5.5, item 39).
|
|
|
37
39
|
|
|
38
40
|
| Command | Who | What |
|
|
39
41
|
|---|---|---|
|
|
40
|
-
| `/allow user` | a tracker admin | Admits a person: they can now `/register`. With the usr link on (0.21.0), run in a server, it also allows them in usr as `tracker:member` and keeps their usr user id. The admin must be linked in usr and hold `tracker:register` and `tracker:member` there. Run again, it asks usr again (usr only ever adds), so it links someone not yet linked and relinks someone whose usr link was made anew. A usr refusal or outage leaves the admission standing and says what to do; in a DM it says to run it in the server. |
|
|
42
|
+
| `/allow user` | a tracker admin | Admits a person: they can now `/register`. With the usr link on (0.21.0), run in a server, it also allows them in usr as `tracker:member` and keeps their usr user id. The admin must be linked in usr and hold `tracker:register` and `tracker:member` there. Run again, it asks usr again (usr only ever adds), so it links someone not yet linked and relinks someone whose usr link was made anew. The usr user id is final only once they have signed up on usr: one who finishes signing up while signed in to a usr account they already had is moved to it by usr, and the account `/allow` made is dropped, so their usr sign-in fails until a second `/allow` links the one they have. A usr refusal or outage leaves the admission standing and says what to do; in a DM it says to run it in the server. |
|
|
41
43
|
| `/register [hour] [zone]` | an admitted person | Signs up, or changes the preferred hour (0-23, default 9) and time zone (default `America/New_York`). The reply says an admin can see every task. With the usr link on (0.21.0), run in a server, it also carries a linked person's one-time usr sign-up link, until usr says they have signed up; in a DM it says to run it in the server. If usr says they are not allowed yet (a usr admin removed their link), it forgets their link and says to ask for `/allow` again. See **First admin and usr** below. |
|
|
42
44
|
| `/remind text [when] [repeat]` | a registered person | A reminder by DM. `when` is docket's `parseWhen` grammar (`in 20 minutes`, `tomorrow 9am`, `fri at 17:30`); `repeat` is once (the default), daily, weekly or monthly. A repeating one with no `when` starts today at the preferred hour. |
|
|
43
45
|
| `/renewal name amount currency renews [unit] [every] [lead] [note]` | registered | A subscription, domain, warranty or membership (docket's `renewal`, a `period` schedule). `renews` is the next renewal or expiry date, `YYYY-MM-DD`, today or later; `unit` yearly (the default), monthly, weekly or daily, `every` how many of those; the ask comes `lead` days before (default 7) at the preferred hour, with Keep, Cancel, Renewed and Snooze. If that ask is already past but the date is not, the first ask comes within a minute (a zone or hour change before it fires drops it for the next period's). Keep and Renewed record what was paid; Cancel ends it. |
|
|
44
46
|
| `/price url [name] [hours] [drop] [baseline] [near]` | registered | A price (docket's `price`, a `poll` schedule every `hours`, default 12, at most 168). `url` may be any public web page (Page reads, under What it does), eBay's and BGG's included, or a link that redirects to one; only the want-list watcher refuses eBay. The page is read once at once, and nothing is created unless a price is found in it; then the first check, within a minute, DMs the starting price, and a drop of `drop` percent or more (default 10) from the `baseline` -- the last (default), first or highest price seen -- DMs the owner once per crossing. `near` is the words just before the price, for a page with no structured price. At most 20 per person. |
|
|
45
|
-
| `/research question [context] [deadline] [at]` | registered | A one-off research request (docket's `research`, a `once` schedule at `at`, default now). A research run looks it up on the web (read-and-web tools only, plan 5.6), a reviewer run checks the draft against its sources, and only a passed answer is DMed to you (and accepted recipients); its claims are kept as findings on the task's page. `deadline` (docket's `parseWhen`) goes into the request; a run that would start past it makes no call and tells you. `question` must hold the question itself: fewer than 3 characters, or no letter at all (a stray `0`), is refused before anything is made. At most 5 waiting per person; every run counts against the daily budget (below). Answers that research is not available while the city-hall Executor is not set up. |
|
|
47
|
+
| `/research question [context] [deadline] [at] [sources]` | registered | A one-off research request (docket's `research`, a `once` schedule at `at`, default now). A research run looks it up on the web (read-and-web tools only, plan 5.6), a reviewer run checks the draft against its sources, and only a passed answer is DMed to you (and accepted recipients); its claims are kept as findings on the task's page. `deadline` (docket's `parseWhen`) goes into the request; a run that would start past it makes no call and tells you. `question` must hold the question itself: fewer than 3 characters, or no letter at all (a stray `0`), is refused before anything is made. At most 5 waiting per person; every run counts against the daily budget (below). `sources: browser` (0.28.0, docket-types 0.8.0's `sources: "sent"`; default `web`) answers from pages your own browser gathers instead -- ask Claude in Chrome to gather them and send them in through the task API (`POST /api/v1/tasks/<id>/sources`, then `/sources/ready`; Task API below), for sites the research cannot open, BoardGameGeek and eBay: the request waits for them, its run queued 14 days out or 10 minutes before the deadline, whichever is sooner (a deadline less than 20 minutes away is refused; the margin makes a missed deadline unlikely, not impossible, since a run held by the budget or behind another Job can still start late), and moved to now by `ready`; a request whose `ready` never comes runs then on whatever pages are in, and with none makes no model call and DMs you so. `at` does not go with it, and it needs the web area. Answers that research is not available while the city-hall Executor is not set up. |
|
|
46
48
|
| `/scout new interests [lens] [for] [notes] [every]` | registered | An interest scout (docket-types' `scout` type, since 0.6.0; plan 1.2 row 1): every `every` days (default 1, at most 30) at your preferred hour, a model run looks on the web for 5 to 10 things published or available in the last 30 days that fit the `interests` (separated by commas, at most 20, each at most 80 characters) through the `lens` -- `general` (the default), `birthday` (fun or a little grandiose), `anniversary` (a romantic angle) or `christmas` (tied to their interests) -- for `for` (someone else, e.g. Anne; empty means you), weighing `notes`, and DMs them to you (and accepted recipients). A link it showed before is not shown again (it remembers the newest 300); fewer than five come with the run's reason. At most 3 scouts per person; each run counts against the daily budget (below), up to 1.50 USD. Answers that the scout is not available while the city-hall Executor is not set up. |
|
|
47
|
-
| `/want name source [target] [max] [currency] [hours] [judge]` | registered | A want-list watch (docket-types' `wantlist` type, or `wantjudge` when judged, since 0.6.0; plan 1.2 row 2). `source` is `page`: `target` is a shop's listing or search page, checked like `/price`'s page and read once at once -- nothing is made unless its structured product data (JSON-LD `ItemList`s and `Product`s) lists something; `bgg`: `target` is a BoardGameGeek game's address or id, read through BGG's XML API while `TRACKER_BGG_TOKEN` is set; or `ebay`: an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the watcher never opens eBay, page or API. `target` is the words to search eBay for (the name when empty); the answer gives the watch's id, how listings reach it -- you search eBay in your own browser, such as with Claude in Chrome, and send what it finds to `POST /api/v1/tasks/<id>/listings` (Task API below), or, while the alert-email route is on (0.25.0, Alert emails below), forward eBay's saved-search alert emails to your alert address, which the answer gives -- and the eBay search itself, with `max` as eBay's own top price, to save on eBay too. `bgg` while the token is unset (BGG declined the application) is an inbox watch the same way: `target` is then a BGG game (kept as its id) or the words to look for (the name when empty). An inbox watch needs the web area (`TRACKER_WEB_URL`), since its listings arrive through the task API; without it, it is refused. Every `hours` (default 12 for a page, 24 for BGG, 1 for an inbox watch, whose reads cost nothing, at most 168) on the `poll` tick, the watch DMs each listing at or under `max` (then only listings that show a price) and in `currency` (when set) that it has not DMed before, up to 5 lines with the rest in `/task history`'s second message (up to 20, as many as fit in one message, the newest kept; the task's page in the web area shows the newest 50), with a Done button that ends the watch. Each listing DMed is a finding. Three reads in a row that find nothing tell you once (a page whose list is empty is a search with no results, not nothing read). At most 20 new listings a run, the rest the next run. A redirect to eBay is refused on every hop. At most 20 watches per person, judged or not. eBay's pages are refused as a `page`. **Judged** (`judge`, default yes while the model runner is set up; `judge: true` without it is refused; fixed once made): the reads stay plain code, and new listings within the limits go to one `claude -p` Job first (docket-types' `wantjudge`, plan 5.4, item 63), which may open only those listings' pages on the watch's own site (never eBay, no web search) and says for each `match`, `maybe` or `no`, why, and what its page shows about the seller, as signals; the DM shows the `match` and `maybe` ones with those notes, and every listing is a finding with its verdict. 12 turns, 0.50 USD, 5 minutes a look; a look that fails for good sends the listings unchecked. Its reads are uncharged and its looks charged, like any model run; both wait while its owner is at a daily ceiling (with the budgets on). The judge may open only pages on the watch's own host (the pasted page's, or BGG's), and none at all for an inbox watch: it judges from what each listing says. |
|
|
49
|
+
| `/want name source [target] [max] [currency] [hours] [judge]` | registered | A want-list watch (docket-types' `wantlist` type, or `wantjudge` when judged, since 0.6.0; plan 1.2 row 2). `source` is `page`: `target` is a shop's listing or search page, checked like `/price`'s page and read once at once -- nothing is made unless its structured product data (JSON-LD `ItemList`s and `Product`s) lists something; `bgg`: `target` is a BoardGameGeek game's address or id, read through BGG's XML API while `TRACKER_BGG_TOKEN` is set; or `ebay`: an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the watcher never opens eBay, page or API. `target` is the words to search eBay for (the name when empty); the answer gives the watch's id, how listings reach it -- you search eBay in your own browser, such as with Claude in Chrome, and send what it finds to `POST /api/v1/tasks/<id>/listings` (Task API below), or, while the alert-email route is on (0.25.0, Alert emails below), forward eBay's saved-search alert emails to your alert address, which the answer gives -- and the eBay search itself, with `max` as eBay's own top price, to save on eBay too. `bgg` while the token is unset (BGG declined the application) is an inbox watch the same way: `target` is then a BGG game (kept as its id) or the words to look for (the name when empty). An inbox watch needs the web area (`TRACKER_WEB_URL`), since its listings arrive through the task API; without it, it is refused. It also needs the bot's `HTTP_PORT`, which the tracker cannot see: with `TRACKER_WEB_URL` set and `HTTP_PORT` unset, the watch is made but nothing can send it listings. Every `hours` (default 12 for a page, 24 for BGG, 1 for an inbox watch, whose reads cost nothing, at most 168) on the `poll` tick, the watch DMs each listing at or under `max` (then only listings that show a price) and in `currency` (when set) that it has not DMed before, up to 5 lines with the rest in `/task history`'s second message (up to 20, as many as fit in one message, the newest kept; the task's page in the web area shows the newest 50), with a Done button that ends the watch. Each listing DMed is a finding. Three reads in a row that find nothing tell you once (a page whose list is empty is a search with no results, not nothing read). At most 20 new listings a run, the rest the next run. A redirect to eBay is refused on every hop. At most 20 watches per person, judged or not. eBay's pages are refused as a `page`. **Judged** (`judge`, default yes while the model runner is set up; `judge: true` without it is refused; fixed once made): the reads stay plain code, and new listings within the limits go to one `claude -p` Job first (docket-types' `wantjudge`, plan 5.4, item 63), which may open only those listings' pages on the watch's own site (never eBay, no web search) and says for each `match`, `maybe` or `no`, why, and what its page shows about the seller, as signals; the DM shows the `match` and `maybe` ones with those notes, and every listing is a finding with its verdict. 12 turns, 0.50 USD, 5 minutes a look; a look that fails for good sends the listings unchecked. Its reads are uncharged and its looks charged, like any model run; both wait while its owner is at a daily ceiling (with the budgets on). The judge may open only pages on the watch's own host (the pasted page's, or BGG's), and none at all for an inbox watch: it judges from what each listing says. |
|
|
48
50
|
| `/scout edit task [interests] [lens] [for] [notes] [every]` | registered | Changes your scout: `interests` replaces the whole list; anything left out stays; `for` or `notes` set to `-` clears it. A change of `every` keeps the start day. |
|
|
49
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. |
|
|
50
52
|
| `/task done task` / `/task snooze task [until]` | the task's owner | Answers the task's latest reminder (snooze: an hour, or until `until`). |
|
|
51
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. |
|
|
52
|
-
| `/task history task` | the owner, an accepted recipient, or an admin |
|
|
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). |
|
|
53
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
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. |
|
|
55
57
|
| `/settings hour hour` | registered | The preferred hour; recurring reminders with no time of their own move to it. |
|
|
@@ -78,7 +80,11 @@ happened.
|
|
|
78
80
|
**Pause after failed DMs** (plan 5.5). Three DMs in a row that the host says cannot be delivered
|
|
79
81
|
pause the person's delivery and every active task that would DM them, theirs and the ones they
|
|
80
82
|
receive (a `paused` event in each task's history; one for a recipient is shown only to that
|
|
81
|
-
recipient, the task's owner and admins -- a row from before 0.6.0, only to the owner and admins
|
|
83
|
+
recipient, the task's owner and admins -- a row from before 0.6.0, only to the owner and admins;
|
|
84
|
+
another recipient sees `paused: delivery paused` in its place, as they see the owner's own delivery
|
|
85
|
+
pause, and `resumed: delivery resumed` for the resume after either, and a failed run's error that
|
|
86
|
+
names another recipient, forgotten or not, reads `a recipient cannot be messaged`);
|
|
87
|
+
a DM to them still owed waits for the resume.
|
|
82
88
|
A task that comes to DM them while they stay paused -- one made on the web or through the task API,
|
|
83
89
|
one its owner had paused and resumes by hand, one a pause stopped part way never reached -- pauses
|
|
84
90
|
the same way at its first DM, which then waits too. Only a send for a task already finished (a
|
|
@@ -241,9 +247,10 @@ served at a hashed path, cached for a year).
|
|
|
241
247
|
| `/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. |
|
|
242
248
|
| `/admin/tasks` | Admins only: every task in the store, with its owner, status and how many receive it, each linking to its page. |
|
|
243
249
|
| `/admin/deliveries` | Admins only: the DMs that did not arrive (below), newest first. |
|
|
244
|
-
| `/admin/
|
|
250
|
+
| `/admin/usage` | Admins only: model usage per budget day (below). |
|
|
251
|
+
| `/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; their daily ceiling, with the form to change it; and their API tokens, each with Revoke. |
|
|
245
252
|
| `/admin/usr-link` | Admins only, POST only, with the usr link on: Link everyone to usr (below). |
|
|
246
|
-
| `/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. |
|
|
253
|
+
| `/admin/allow`, `/admin/people/<id>/grant`, `/revoke`, `/resume-delivery`, `/ceiling`, `/forget`, `/admin/blocks/<id>/lift`, `/admin/tokens/<id>/revoke` | Admins only, POST only: the acts below. |
|
|
247
254
|
| `/api/v1/...` | The JSON task API (below): bearer tokens only, never the cookie. |
|
|
248
255
|
| `/signin` | Where a request that is not signed in is sent (a POST, or any request with the usr link off): says to run `/web`, and with the usr link on offers Sign in with usr. |
|
|
249
256
|
| `/login?t=...` | The link `/web` gives. |
|
|
@@ -267,20 +274,30 @@ minutes, fetched again on an unknown key at most every 30 seconds), `iss` "usr",
|
|
|
267
274
|
never a delegation token. A valid token whose account holds `tracker:member` and is linked to a
|
|
268
275
|
registered person (`users.usr_subject`) opens a session as the one-time link does, once server
|
|
269
276
|
membership is checked (as `/web` checks it), and the browser goes on to the page and query it asked
|
|
270
|
-
for
|
|
277
|
+
for, marked `?usr=in`: arriving there without the session (a browser that refuses the cookie) says
|
|
278
|
+
so instead of signing in again. A signed-in GET still carrying a `?usr=` mark goes on to the same
|
|
279
|
+
address without it. With no valid token the browser is sent to usr's `/api/auth/sso/refresh` once, returning to
|
|
271
280
|
the same address marked `?usr=1`; coming back still without one (usr's keys could not be fetched,
|
|
272
281
|
the clocks disagree) says the sign-in could not be confirmed, instead of going round again. A token
|
|
273
|
-
without the member role
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
without the member role goes the same way once, since it can predate an `/allow` (an `nz_id` lives
|
|
283
|
+
30 minutes by default); still without it, or for an account linked to no one registered here, the
|
|
284
|
+
page shows what to ask an admin for. While signed in, a usr cookie for the person's own account that
|
|
285
|
+
no longer carries `tracker:member` signs them out of every session and revokes their API tokens, but
|
|
286
|
+
only on a GET back from usr's refresh: any other request with it, a POST included, is first sent
|
|
287
|
+
there (a POST's form is not kept: the browser comes back to that page, or to My tasks). That happens
|
|
288
|
+
only in the web area: the task API never reads usr, so a token keeps working until the person next
|
|
289
|
+
uses the web area while still signed in to it; opened signed out, it only shows the not-member page
|
|
290
|
+
and the tokens stay (revoking their tokens or admission in the tracker ends them at once). Sign out
|
|
291
|
+
ends the tracker's session only, not usr's: for 12 hours after it, a GET does not sign back in from
|
|
292
|
+
usr's cookie unless it carries `?usr=go` (the sign-in page's "Sign in with usr" button, whose trip to
|
|
293
|
+
usr ends those 12 hours); a `/web` link signs in as always. Any site under usr's cookie domain can set an
|
|
279
294
|
`nz_id` of its own, so every host under that domain must be trusted. The one-time `/web` link stays
|
|
280
295
|
beside it. `src/usr-identity.ts` uses WebCrypto only.
|
|
281
296
|
|
|
282
297
|
**Sessions and forms.** The session cookie is `__Secure-tracker-session`, `HttpOnly; Secure;
|
|
283
|
-
SameSite=Lax; Path=/tracker/`, for 7 days from sign-in
|
|
298
|
+
SameSite=Lax; Path=/tracker/`, for 7 days from sign-in; a browser never sends it to the bare
|
|
299
|
+
`/tracker`, which redirects to `/tracker/` first. A person holds at most 20 sessions: a 21st ends
|
|
300
|
+
their oldest. The store keeps only SHA-256 hashes of
|
|
284
301
|
link tokens and session ids. Every request re-reads the person: one no longer in the tracker's
|
|
285
302
|
store, or no longer registered, is signed out of every session. Every state change is a POST
|
|
286
303
|
carrying the session's CSRF token (compared in constant time), and a POST with an `Origin` other
|
|
@@ -291,8 +308,8 @@ you in as someone else. Sign out is a POST too.
|
|
|
291
308
|
**The task editor** (slice 2). The forms take the options of `/remind`, `/renewal`, `/price` and
|
|
292
309
|
`/research` (#82), and the same functions check and make them: the same defaults, limits and messages, the page read
|
|
293
310
|
once before a price tracker is made, at most 20 price trackers per person wherever they were made,
|
|
294
|
-
and `near` turned into the same bounded pattern; a person has one
|
|
295
|
-
flight at a time. Every person may have at most 200 active or paused tasks of all types together,
|
|
311
|
+
and `near` turned into the same bounded pattern; a person has one first page read (a new price
|
|
312
|
+
tracker's or `page` watch's) in flight at a time. Every person may have at most 200 active or paused tasks of all types together,
|
|
296
313
|
made anywhere. The length limits are one set (`src/limits.ts`) for the slash options, the forms
|
|
297
314
|
and the server's checks. An edit changes a reminder's text, time and repeat
|
|
298
315
|
(an empty time keeps the one it has, unless the repeat changes); a renewal's name, amount,
|
|
@@ -314,7 +331,7 @@ task is loaded by the id in the path and must be the viewer's own, checked again
|
|
|
314
331
|
queue, and anyone else's -- even one an admin can see -- answers the same 404 as an unknown id. A
|
|
315
332
|
refused form comes back with what was typed and why. A research request made on the web is
|
|
316
333
|
`/research`'s exactly -- the question, the optional context, deadline and start in `/research`'s
|
|
317
|
-
words, the cap of 5 waiting, the daily budget -- and, as in Discord, it is never edited: its page
|
|
334
|
+
words, where its pages come from (the web, or your browser), the cap of 5 waiting, the daily budget -- and, as in Discord, it is never edited: its page
|
|
318
335
|
has Pause, Resume and Delete but no Edit. Making one only queues its run; nothing reaches city-hall
|
|
319
336
|
until the execute tick. Form bodies are capped at 32 KiB, read no
|
|
320
337
|
further than that.
|
|
@@ -355,9 +372,10 @@ the function the command path runs:
|
|
|
355
372
|
- **Make admin / Revoke admin** (plan 5.8). The last admin can be neither revoked nor removed. An
|
|
356
373
|
admin who revokes themselves lands on My tasks. Each change is logged (`u1 made u2 an admin`).
|
|
357
374
|
An admin named in `TRACKER_ADMIN_DISCORD_IDS` is made an admin again at every start, so their page
|
|
358
|
-
offers neither Revoke nor Remove, and both are refused: take them out of the
|
|
359
|
-
Such an admin may still forget themselves (if another admin is left)
|
|
360
|
-
|
|
375
|
+
offers another admin neither Revoke nor Remove, and both are refused: take them out of the
|
|
376
|
+
configuration first. Such an admin may still forget themselves (if another admin is left): their
|
|
377
|
+
own page offers Remove but not Revoke, and the pages say that the next start makes them again, as
|
|
378
|
+
a new person, unless the configuration no longer names them.
|
|
361
379
|
- **Resume delivery** of a person paused after failed DMs: `DeliveryHealth.resume`, what the
|
|
362
380
|
person's own next command does -- the count clears and the tasks nothing else holds go back on.
|
|
363
381
|
- **Lift** a decline block in force: docket's `liftBlock`, recorded as that admin's lift.
|
|
@@ -424,7 +442,9 @@ in one SQLite transaction in the write queue, **deleted, not archived**:
|
|
|
424
442
|
person row.
|
|
425
443
|
|
|
426
444
|
What stays, and why: another person's task that they received stays that owner's; one paused only
|
|
427
|
-
because DMs to them failed goes back on
|
|
445
|
+
because DMs to them failed goes back on (one that cannot finish, on a store error, is named in an
|
|
446
|
+
admin's answer and logged with its status then -- still paused, or active with its next run maybe
|
|
447
|
+
missing -- and the rest of the erasure stands). Another person's row keeps its own audit fact but not their
|
|
428
448
|
id -- `admitted_by` of someone they admitted, `lifted_by` of a block they lifted, and `set_by` of a
|
|
429
449
|
change they made to someone's ceiling become
|
|
430
450
|
`forgotten` ("an admin since forgotten"; null already means "the configuration"). Text is matched
|
|
@@ -440,7 +460,9 @@ and is deleted when it is on a task they were a recipient of, names their curren
|
|
|
440
460
|
other recipient of that task has that name. A row written under an older name of theirs, or shared
|
|
441
461
|
with a same-named recipient, is kept: the one known gap. A reply other people wrote on the erased
|
|
442
462
|
person's own tasks is deleted with those tasks. Outside the store, nothing is touched: the DMs the
|
|
443
|
-
bot sent stay in the person's Discord DMs, and the host's log may hold their tracker id.
|
|
463
|
+
bot sent stay in the person's Discord DMs, and the host's log may hold their tracker id. With the
|
|
464
|
+
usr link on, their usr account -- which holds their Discord id and `tracker:member` -- is usr's:
|
|
465
|
+
forget-me never calls usr, so it stays until it is removed there. **city-hall
|
|
444
466
|
keeps its own copy of a research Job** -- the spec (the question and context are in its prompt) and
|
|
445
467
|
the result -- under city-hall's retention, not the tracker's (Nazu, plan 5.8): forget-me deletes the
|
|
446
468
|
tracker's `executor_jobs` rows for the erased runs (no tombstone is kept; the keys name only the
|
|
@@ -457,7 +479,9 @@ write-ahead log is checkpointed without waiting; when a reader keeps it busy, th
|
|
|
457
479
|
SQLite's next checkpoint copies it back.
|
|
458
480
|
|
|
459
481
|
Timing: forget-me first waits, outside the write queue, until no notify or poll tick is running (at
|
|
460
|
-
most
|
|
482
|
+
most 6 seconds, under the bot's 10-second bound on a web request, so the answer reaches the browser;
|
|
483
|
+
then it answers 503, try again, and deletes nothing; the queue turn after the wait has no bound of
|
|
484
|
+
its own, but nothing slow runs in the queue), so a reminder DM to them
|
|
461
485
|
already in flight cannot be recorded after they are gone -- and no one else's command waits behind
|
|
462
486
|
it. Inside the queue it only checks that no tick started meanwhile (else the same 503). A DM sent
|
|
463
487
|
outside any tick -- an invitation's -- that fails after they are gone writes nothing: the failure
|
|
@@ -495,9 +519,11 @@ eBay's saved-search alerts reach eBay watches by mail (0.25.0), while `TRACKER_M
|
|
|
495
519
|
is deployed by pasting `mail-worker/worker.js` into the Cloudflare dashboard's Worker editor; it is
|
|
496
520
|
not in the plugin's npm bundle.
|
|
497
521
|
- **What a message does.** Gmail's forwarding confirmation: its code is DMed, to enter in Gmail's
|
|
498
|
-
Forwarding and POP/IMAP settings. Since 0.26.0 it is any sender at Google naming forwarding
|
|
499
|
-
the code
|
|
500
|
-
|
|
522
|
+
Forwarding and POP/IMAP settings. Since 0.26.0 it is any sender at Google naming forwarding. Since
|
|
523
|
+
0.27.0 the code is a run of digits standing alone in the subject or shortly after the word
|
|
524
|
+
"code" in the body, and Gmail's verify link (a `/mail/vf-` address on `mail.google.com`,
|
|
525
|
+
`mail-settings.google.com` or a subdomain of either, never its cancel link) is DMed too; digits inside
|
|
526
|
+
an address are never the code. An eBay alert: the
|
|
501
527
|
matches above "You might also like" (never eBay's suggestions) -- title, the item's bare
|
|
502
528
|
`/itm/<number>` address, the price's lower end -- go into the inbox of each of the person's live
|
|
503
529
|
eBay watches whose words are the saved search's (case and spacing aside), exactly as listings sent
|
|
@@ -507,7 +533,9 @@ eBay's saved-search alerts reach eBay watches by mail (0.25.0), while `TRACKER_M
|
|
|
507
533
|
dropped. A message is never stored, only the listings read from it, and its words are never
|
|
508
534
|
logged: one that does nothing is logged only with why (the owner is not registered, or its
|
|
509
535
|
sender's domain -- a Google no-reply sender whole -- which bodies it had, and whether its
|
|
510
|
-
subject names forwarding
|
|
536
|
+
subject names forwarding; for Google's no-reply senders also its subject's shape, letters `a`,
|
|
537
|
+
digits `9` and each address `<addr>`, and counts from its body as read, plain text else the
|
|
538
|
+
HTML's text).
|
|
511
539
|
- **Only eBay's own domains** (ebay.com, .co.uk, .de, .ca, .com.au and the rest of eBay's sites;
|
|
512
540
|
never a lookalike such as `ebay.com.de`) count as eBay, for the sender and for every item link.
|
|
513
541
|
The sender is its `From` address, not a checked signature: the private address is the guard, and
|
|
@@ -569,8 +597,15 @@ with `Retry-After`. A limited request is refused before its owner is looked up.
|
|
|
569
597
|
token does not look up (unknown, revoked, expired, malformed) share one global bucket of 30,
|
|
570
598
|
refilled at one every two seconds; past it they get `429` instead of `401`, and valid tokens are
|
|
571
599
|
unaffected. A body is at most
|
|
572
|
-
16 KiB, read no further. One new price tracker's page read in flight per person,
|
|
573
|
-
web editor (`409 busy`).
|
|
600
|
+
16 KiB, read no further. One new price tracker's or `page` watch's first read in flight per person,
|
|
601
|
+
shared with the web editor (`409 busy`). That first read gives up 8 seconds after the request
|
|
602
|
+
arrived -- counted from before the token check, the membership re-check, the body and the create's
|
|
603
|
+
first turn in the queue, not from when the read starts -- and then makes nothing (`503 slow_page`
|
|
604
|
+
with `Retry-After`; the web form says the same words). The person stays busy until the read itself
|
|
605
|
+
ends (15 s at most), so a retry cannot start a second one. The about 2 seconds of the host's 10 s
|
|
606
|
+
left after a read in time are a margin for making the task in the queue, not a bound: a queue
|
|
607
|
+
slower than that can still make it after the host has answered `504`, so after a `504` on a create,
|
|
608
|
+
`GET /tasks` before trying again.
|
|
574
609
|
|
|
575
610
|
**Requests.** A `POST` or `PATCH` body is one JSON object with `Content-Type: application/json`
|
|
576
611
|
(else `415`), and names only the fields listed for that type (else `400 unknown_field`), each of
|
|
@@ -582,11 +617,11 @@ only for a want-list watch's `max` in a `PATCH`, where it clears the top price;
|
|
|
582
617
|
refused as the wrong JSON type (send `""` to clear a note).
|
|
583
618
|
|
|
584
619
|
Which types: `GET /tasks`, `GET /tasks/<id>`, pause, resume and `DELETE` work on any task you own,
|
|
585
|
-
whatever its type. `POST /tasks` takes
|
|
620
|
+
whatever its type. `POST /tasks` takes `reminder`, `renewal` and `price`, by the web editor's rules,
|
|
586
621
|
`wantlist`, by `/want`'s rules, and, while the model runner is set up on this bot, `research`
|
|
587
622
|
(#82), by `/research`'s rules, and `scout` (#83), by `/scout new`'s; while it is not, a `research`
|
|
588
|
-
or `scout` create answers `503 unavailable`. `PATCH` takes
|
|
589
|
-
(`wantlist` or `wantjudge`) and `scout` (a scout made while the runner was set up stays editable):
|
|
623
|
+
or `scout` create answers `503 unavailable`. `PATCH` takes `reminder`, `renewal`, `price`, a
|
|
624
|
+
want-list watch (`wantlist` or `wantjudge`) and `scout` (a scout made while the runner was set up stays editable):
|
|
590
625
|
a research request cannot be edited, here or in Discord, and a `PATCH` of one answers
|
|
591
626
|
`409 conflict`.
|
|
592
627
|
|
|
@@ -595,11 +630,13 @@ a research request cannot be edited, here or in Discord, and a `PATCH` of one an
|
|
|
595
630
|
| `GET /api/v1/me` | The token's owner (id, name, zone, preferred hour) and the token (id, name, made, expires). |
|
|
596
631
|
| `GET /api/v1/types` | Each type this bot makes now (research and scout only while the model runner is set up), whether a `PATCH` takes it (`editable`; false for research, whose `edit` is empty), and its create and edit fields: name, JSON type, required (never, for an edit), description, and limits (`maxLength`, `minimum`, `maximum`, `enum`). This describes the tracker's own editor fields -- what these endpoints take -- and is **not** docket-core's `TaskType.intake` (`IntakeSpec`), which describes a type's config. |
|
|
597
632
|
| `GET /api/v1/tasks` | Your tasks that are not deleted -- active, paused and done -- oldest first. Not the ones shared with you. |
|
|
598
|
-
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research`, `scout` or `wantlist`, and the rest are that type's create fields. `201`, with `Location`. A price's page is read first, and nothing is made unless a price is found in it. A research request's fields are `question` (required), `context`, `deadline` and `
|
|
633
|
+
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research`, `scout` or `wantlist`, and the rest are that type's create fields. `201`, with `Location`. A price's page is read first, and nothing is made unless a price is found in it. A research request's fields are `question` (required), `context`, `deadline`, `at` and `sources` (`web`, the default, or `browser`: it waits for the pages your browser sends in, below), all text. A scout's are `interests` (required, text, separated by commas or new lines), `lens`, `for` and `notes` (text) and `every` (a whole number of days). A want-list watch's (`wantlist`) are `name` (required), `target` (required but for an inbox watch), `source` (`page`, `bgg` or `ebay`), `max` (a number), `currency` (text), `hours` (a whole number) and `judge` (`"yes"` or `"no"`; `"yes"` without the model runner is a `503`); a judged one comes back as `type: "wantjudge"`, edited by the same fields but `judge`; a task's JSON shows its `source` and `target`, which no edit changes -- an inbox watch's `source` reads back as `inbox` and its `target` is its inbox key -- and for an inbox watch its `site` (`ebay` or `bgg`), its words to `search` for and a BGG `game` when one was named. |
|
|
599
634
|
| `GET /api/v1/tasks/<id>` | One of your tasks (a deleted one too), with its history: the newest runs and changes, as `/task history` shows them. |
|
|
600
635
|
| `PATCH /api/v1/tasks/<id>` | Edits it: the fields to change. A price's page is not editable. |
|
|
601
636
|
| `POST /api/v1/tasks/<id>/pause`, `/resume` | Body `{}`. As the web's Pause and Resume, and `/task resume`. |
|
|
602
|
-
| `POST /api/v1/tasks/<id>/listings` | Sends listings in to your inbox watch (an eBay watch, or a BGG one made without BGG's API): `{"listings": [{"title", "url", "price", "currency", "condition", "seller"}, ...]}`, 1 to 40 of them, `title` and an absolute http(s) `url` required, `price` a number or text such as `"$30.00"`, `currency` three letters. Each is cleaned by docket-types' `submittedListing` (an eBay item's address becomes its bare `/itm/<number>`, so one item stays one listing); the usable ones are kept, the others counted: `200 {"accepted", "rejected", "message"}`. None usable, or the wrong shape, is `400`; more than 40 is `400 too_many`; anyone else's, a deleted or finished watch, or not a watch, is `404`; a page or BGG-API watch is `409` (its listings are read, not sent). The next look DMs the new ones within the watch's limits, each once. The newest 100 per watch are kept, as many as one look reads: more than 100 sent between two looks loses the oldest. Body up to 64 KiB. |
|
|
637
|
+
| `POST /api/v1/tasks/<id>/listings` | Sends listings in to your inbox watch (an eBay watch, or a BGG one made without BGG's API): `{"listings": [{"title", "url", "price", "currency", "condition", "seller"}, ...]}`, 1 to 40 of them, `title` and an absolute http(s) `url` on the watch's own site (one of eBay's own domains, `EBAY_DOMAINS` in `src/web/api-listings.ts`, or a subdomain, for an eBay watch; `boardgamegeek.com` or a subdomain for a BGG one) required, `price` a number or text such as `"$30.00"`, `currency` three letters. Each is cleaned by docket-types' `submittedListing` (an eBay item's address becomes its bare `/itm/<number>`, so one item stays one listing); the usable ones are kept, the others counted: `200 {"accepted", "rejected", "message"}`. None usable, or the wrong shape, is `400`; more than 40 is `400 too_many`; anyone else's, a deleted or finished watch, or not a watch, is `404`; a page or BGG-API watch is `409` (its listings are read, not sent). The next look DMs the new ones within the watch's limits, each once. The newest 100 per watch are kept, as many as one look reads: more than 100 sent between two looks loses the oldest. Body up to 64 KiB. |
|
|
638
|
+
| `POST /api/v1/tasks/<id>/sources` | Sends pages in to your research request made with `sources: browser` (0.28.0, #125): `{"pages": [{"url", "title", "text"}, ...]}`, 1 to 10 of them, an absolute http(s) `url` and the page's main `text` required. Each is cleaned by docket-types' `gatheredPages` (the text cut to 12,000 characters, the title to one line), and a second page with the same address in one request is left out; a page whose address the request already holds replaces it in its place: `200 {"accepted", "rejected", "overBudget", "pages", "message"}`, `accepted` being how many were stored, `rejected` how many were unusable (no absolute http(s) address or no text, or an address sent twice), `overBudget` how many were left out whole because their text would take the request past 99,800 characters of page text in all (what docket's 100,000-character budget across a request's pages uses whole, the text a replaced page held not counted), and `pages` how many it holds. A request holds at most 10 pages: more is `409 too_many`, saying how many it holds; when no page fits the text budget, `409 too_much_text`. None usable, or the wrong shape, is `400`; more than 10 in one request `400 too_many`; anyone else's, a deleted or finished one, or not a research request, is `404`; a web request, or one whose run was moved or has started, is `409`. Body up to 1 MiB. |
|
|
639
|
+
| `POST /api/v1/tasks/<id>/sources/ready` | Body `{}`. Starts that request on the pages it holds: its queued run is moved to now (docket-core's `reschedule`), and the execute lane submits it within a minute or two. No page in yet, a paused request, or one no longer waiting is `409`; the others as for `/sources`. From then the request takes no more pages; nor does it once a run of it has started, `ready` or not (the run at its expiry). Both routes take the task's lock: while the model runner has the task, either is `409 busy`. |
|
|
603
640
|
| `DELETE /api/v1/tasks/<id>` | Deletes it as the web does: archived, nothing more sent, history kept. No confirmation step. |
|
|
604
641
|
|
|
605
642
|
A task is `{"id", "type", "title", "status", "cadence", "nextAt", "createdAt", "updatedAt",
|
|
@@ -607,7 +644,7 @@ A task is `{"id", "type", "title", "status", "cadence", "nextAt", "createdAt", "
|
|
|
607
644
|
`deleted`; `cadence` is the schedule in words, in your zone; `nextAt` the next run's instant (UTC),
|
|
608
645
|
null when paused or nothing is due; `settings` exactly the fields a `PATCH` takes, as the task has
|
|
609
646
|
them. A research request's `settings` are what was asked -- `question`, and `context` and
|
|
610
|
-
`deadline` (an instant) when given -- shown to you alone and changed by no `PATCH
|
|
647
|
+
`deadline` (an instant) when given, and `sources` (`web` or `browser`) -- shown to you alone and changed by no `PATCH`; a browser request also has a top-level `sources: "browser"` (the same value as `settings.sources`, for a helper that reads only the top level), `pages` (how many are in) and `waitingForPages` (true until `ready` moved its run, or a run of it started). A price's `url` is read-only -- another page is another tracker -- so it is not in `settings`
|
|
611
648
|
and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
612
649
|
`{"task", "message"}`, `message` being the words the web and the command say.
|
|
613
650
|
|
|
@@ -618,19 +655,23 @@ and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
|
618
655
|
| 400 | `invalid` | A rule refused a value; `message` is the rule's own words, as the web editor shows them. Also a field of the wrong JSON type, or no `type`. |
|
|
619
656
|
| 400 | `unknown_field` | A field that type does not take (on a pause or resume, any field). |
|
|
620
657
|
| 400 | `invalid_json` | The body is not JSON, or not one object. |
|
|
658
|
+
| 400 | `too_many` | More than 40 listings in one `/listings` request. |
|
|
621
659
|
| 401 | `unauthorized` | No `Authorization` header. With `WWW-Authenticate: Bearer realm="tracker"`. |
|
|
622
|
-
| 401 | `invalid_token` | A token unknown, revoked or expired, or whose owner is no longer registered (their tokens are then deleted): one answer for all
|
|
660
|
+
| 401 | `invalid_token` | A token unknown, revoked or expired, or whose owner is no longer registered (their tokens are then deleted): one answer for all, with `WWW-Authenticate`. An owner forgotten while their write waited in the queue gets the same code with its own message and no `WWW-Authenticate`. |
|
|
623
661
|
| 403 | `origin_refused` | The request carried an `Origin` header. |
|
|
624
662
|
| 403 | `not_member` | The owner has left every `TRACKER_GUILD_ID` server (or lacks the `TRACKER_GUILD_ROLES` role where one is asked); their tokens are revoked. |
|
|
625
663
|
| 404 | `not_found` | No such endpoint, or no such task of yours. Anyone else's task -- one shared with you, or any task to an admin's token -- answers exactly as an unknown id. |
|
|
626
664
|
| 405 | `method_not_allowed` | With `Allow`. |
|
|
627
|
-
| 409 | `conflict` | A finished task edited, a research request edited,
|
|
665
|
+
| 409 | `conflict` | A finished task edited, a research request edited, a pause of a task not active (a resume of one not paused), or pages sent to (or `ready` for) a research request that does not wait for them. |
|
|
628
666
|
| 409 | `limit_reached` | 200 active or paused tasks, 20 price trackers, 5 research requests waiting, 3 scouts, or 20 want-list watches, as the commands count them. |
|
|
667
|
+
| 409 | `too_many` | Pages sent to a research request that would then hold more than 10. (As a `400`, more than a request takes in one body.) |
|
|
668
|
+
| 409 | `too_much_text` | None of the pages sent to a research request fits in the text it may still hold (99,800 characters in all). |
|
|
629
669
|
| 409 | `busy` | Your last new price tracker's page is still being read, or the task is with the model runner right now (try again in a minute). |
|
|
630
|
-
| 413 | `too_large` | Body over 16 KiB (64 KiB for `/listings`). |
|
|
670
|
+
| 413 | `too_large` | Body over 16 KiB (64 KiB for `/listings`, 1 MiB for `/sources`). |
|
|
631
671
|
| 415 | `unsupported_media_type` | Not `application/json`. |
|
|
632
672
|
| 429 | `rate_limited` | With `Retry-After`: this token's bucket, or the shared one for bad tokens. |
|
|
633
|
-
| 503 | `
|
|
673
|
+
| 503 | `slow_page` | A new price's or page watch's page did not answer within 8 seconds of the request's arrival: nothing was made. With `Retry-After`. |
|
|
674
|
+
| 503 | `starting`, `membership_unknown`, `unavailable` | The plugin is starting; the owner's membership could not be checked for 24 hours (the lookup goes through the Discord client of the last command or button the bot handled, so after a restart none runs until someone uses one); the type is not available on this bot. |
|
|
634
675
|
|
|
635
676
|
**Example.**
|
|
636
677
|
|
|
@@ -659,15 +700,15 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
659
700
|
|
|
660
701
|
| Piece | File | Notes |
|
|
661
702
|
|---|---|---|
|
|
662
|
-
| 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. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
703
|
+
| 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. |
|
|
663
704
|
| 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`. |
|
|
664
|
-
| 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
|
|
665
|
-
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts`, `src/scout.ts` (the `scout`
|
|
705
|
+
| 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
|
+
| 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`) | 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. |
|
|
666
707
|
| 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. |
|
|
667
|
-
| 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 want-list page read (docket-types' `page` source) carries `NEVER_EBAY` and is refused on any hop to eBay; a price read is not. |
|
|
708
|
+
| 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. |
|
|
668
709
|
| Renewals, prices | `src/tracked.ts`, `src/price.ts`, `src/series.ts`, `src/page.ts` | `/renewal`, `/price` and `/task decide`; the series lines of `/task history`. The series (docket's `series` table, schema 1) holds a renewal's paid amounts and a price's readings. |
|
|
669
710
|
| Delivery | `src/notifier.ts`, `src/buttons.ts` | docket's `Notifier` over `host.dm`, with the buttons. docket claims each (run, person) in the store's `deliveries` before the DM and settles it after. The notifier maps the host's answers to docket's errors: cannot be messaged (50007) or an unknown user is `DeliveryFailedError(msg, true)` (failed for good; 50007 and 10013 count toward the pause); a message refused for its content is a plain `DeliveryFailedError` (retried); a paused person is `ExecutorUnavailableError` (deferred); anything else is rethrown, so docket settles it unconfirmed and never resends it. |
|
|
670
|
-
| Daily digest | `src/digest.ts` | Plan 5.5's "today and overdue" DM, on the `notify` tick after the runs due now, no model. For each person whose preferred hour has come in their zone, the day is claimed first (docket's `claimNotice`, key `digest:<user>:<YYYY-MM-DD>`, research-triage's exactly-once pattern; forget-me erases it), then their own active reminders and renewals are read: overdue is the latest fired run before today with no done or decision from the owner, due today is a run not yet fired today or the latest fired run today still unanswered. A snoozed run, an older run behind a newer one, other types and received tasks are left out. Nothing listed, no DM; too long for one message, it caps both sections at the same, smaller number of lines (the most that fit) and counts the rest; a paused person is passed by unclaimed; a failed send is not retried. |
|
|
711
|
+
| Daily digest | `src/digest.ts` | Plan 5.5's "today and overdue" DM, on the `notify` tick after the runs due now, no model. For each person whose preferred hour has come in their zone, the day is claimed first (docket's `claimNotice`, key `digest:<user>:<YYYY-MM-DD>`, research-triage's exactly-once pattern; forget-me erases it) unless a read of their last claim finds it already claimed or due less than 20 hours before today's hour (a move east, a preferred hour moved over 4 hours earlier, or a move west across the date line; a claim dated in the future counts as none), then their own active reminders and renewals are read: overdue is the latest fired run before today with no done or decision from the owner, due today is a run not yet fired today or the latest fired run today still unanswered. A snoozed run, an older run behind a newer one, other types and received tasks are left out. Nothing listed, no DM; too long for one message, it caps both sections at the same, smaller number of lines (the most that fit) and counts the rest; a paused person is passed by unclaimed; a failed send is not retried. |
|
|
671
712
|
| Task lock | `src/locks.ts` | One task at a time: its runs, answers and edits never overlap (docket 0.4.0 asks the host to serialize per task). |
|
|
672
713
|
| Pause | `src/delivery-health.ts` | The per-person failure count, the pause and the resume. |
|
|
673
714
|
| People | `src/people.ts`, `src/admissions.ts` | Discord id, time zone, preferred hour, admin flag -- in the tracker's store, with each person's usr user id once the usr link links them (`src/usr.ts`, 0.20.0); who admitted each person and when they registered. `TRACKER_ADMIN_DISCORD_IDS` only ever grants admin. |
|
|
@@ -676,7 +717,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
676
717
|
| Admin, forget-me | `src/admin.ts`, `src/roster.ts`; `src/web/admin.ts`, `src/web/admin-pages.ts` | The admin acts and forget-me, Discord-free; the SQL docket's Store has no method for (listing people and blocks, erasing a person); the routes and pages. |
|
|
677
718
|
| Web area | `src/web/` | `app.ts` authenticates and dispatches (pure over the injected store and clock), `routes.ts` names the paths; `pages.ts`, `editor-pages.ts` and `html.ts` render; `form-input.ts` reads the editor's fields and `editor.ts` calls the shared rules with them; `signin-link.ts` and `sessions.ts` hold the sign-in; `theme.ts` the stylesheet; `command.ts` is `/web`. |
|
|
678
719
|
| Task API | `src/web/api.ts`, `src/web/api-tasks.ts`, `src/web/api-tokens.ts`, `src/web/tokens.ts`, `src/web/token-pages.ts` | The JSON API's authentication, gates and rate limit; its task routes over editor.ts's writes; the tokens (SQL, hashed); the tokens page and its routes. |
|
|
679
|
-
| Task rules | `src/reminders.ts`, `src/tracked.ts`, `src/price.ts`, `src/edit.ts`, `src/manage.ts`, `src/limits.ts` | What makes, edits, pauses, resumes and deletes a task, and the limits on it, Discord-free, for the commands and the web editor alike. |
|
|
720
|
+
| Task rules | `src/reminders.ts`, `src/tracked.ts`, `src/price.ts`, `src/research.ts`, `src/scout.ts`, `src/want.ts`, `src/edit.ts`, `src/manage.ts`, `src/limits.ts` | What makes, edits, pauses, resumes and deletes a task, and the limits on it, Discord-free, for the commands and the web editor alike. |
|
|
680
721
|
|
|
681
722
|
## Configuration
|
|
682
723
|
|
|
@@ -691,7 +732,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
691
732
|
| `TRACKER_CITY_HALL_KEY` | yes | The source bearer key city-hall checks on `/api/execute/jobs` (its `CITY_HALL_API_KEY`, city-hall#18). Never logged. Kept in the instance's own env, never in a repo. |
|
|
692
733
|
| `TRACKER_CITY_HALL_CAPABILITY` | no | The capability tag every Job names, one only docket-runner carries (plan item 71), e.g. `claude-cli:subscription`. No default: a guessed tag could send the tracker's Jobs to another agent. |
|
|
693
734
|
| `TRACKER_CITY_HALL_ACCESS_CLIENT_ID`, `TRACKER_CITY_HALL_ACCESS_CLIENT_SECRET` | yes | A Cloudflare Access service token for city-hall's edge (`CF-Access-Client-Id` / `-Secret`), when one is in front. Both or neither. |
|
|
694
|
-
| `TRACKER_BGG_TOKEN` | yes | BoardGameGeek's XML API Bearer token for the tracker's registered application. Set = `/want source: bgg` watches a game's BGG marketplace; unset or empty = `/want source: bgg` makes an inbox watch instead, whose listings are sent in (as for eBay). Never logged.
|
|
735
|
+
| `TRACKER_BGG_TOKEN` | yes | BoardGameGeek's XML API Bearer token for the tracker's registered application. Set = `/want source: bgg` watches a game's BGG marketplace; unset or empty = `/want source: bgg` makes an inbox watch instead, whose listings are sent in (as for eBay). Never logged. BGG declined the tracker's application (applied 2026-09-29, non-commercial), so no token exists today; if BGG ever grants one, put it in the instance's `.env` and recreate the bot. BGG's terms ask for attribution: every BGG line in a DM says "via BoardGameGeek". |
|
|
695
736
|
| `TRACKER_USR_URL` | no | The https origin of our usr (Rackbops/usr), e.g. `https://id.example.com` (no path). With `TRACKER_USR_KEY` it turns the usr link on (0.20.0): people stay in the tracker, and each is linked to a usr account by usr's user id (`users.usr_subject`). Unset = off, and the tracker behaves as before (migration 9 still adds the empty column). `/allow` and `/register` use it since 0.21.0, and the web sign-in since 0.22.0 (Signing in through usr, above). |
|
|
696
737
|
| `TRACKER_USR_KEY` | yes | The tracker's usr API key, with the role `usr:discord` only (usr's `/api/discord/allow` and `/register-link`), never `tracker:admin` or `tracker:register`, and a usr Discord service (`PUT /api/discord/services/<keyId>`) set to the tracker's app. Required with `TRACKER_USR_URL`; set without it, or with whitespace in it, refuses to load. Never logged. |
|
|
697
738
|
| `TRACKER_MAIL_DOMAIN` | no | The domain the alert-email route (0.25.0, Alert emails below) receives mail at, e.g. `alerts.example.com`: lower case, at least one dot. With `TRACKER_MAIL_KEY`; both unset = off (`/tracker/mail/inbound` answers 404 and no address is shown). One without the other, a domain that is not one, or a key under 32 characters or with white space refuses to load, never echoing either. |
|
|
@@ -720,10 +761,11 @@ checks they are a member of the server with its role, as `/allow` does; re-reads
|
|
|
720
761
|
usr, so someone forgotten before then is never sent; allows them in usr as `tracker:member`, with the pressing
|
|
721
762
|
admin as the one who allowed them; then links them. usr records each link against
|
|
722
763
|
`TRACKER_GUILD_ID`'s first server, so with no gate it refuses and says to `/allow` in the server
|
|
723
|
-
instead. Only a 400 from usr
|
|
724
|
-
refusal or failure (the admin not linked in usr or missing `tracker:register` or `tracker:member`,
|
|
764
|
+
instead. Only a 400 from usr whose reason starts with one of the person's own fields (`discord_user_id`,
|
|
765
|
+
`display_name`) is theirs, listed by name while the rest go on; any other 400, refusal or failure (the admin not linked in usr or missing `tracker:register` or `tracker:member`,
|
|
725
766
|
the key, usr unreachable) stops the run, since it would fail everyone the same way. It never starts
|
|
726
|
-
on its own.
|
|
767
|
+
on its own. It passes over everyone already linked, so it does not fix a link usr moved at sign-up
|
|
768
|
+
(`/allow` above): run `/allow` for that person again.
|
|
727
769
|
|
|
728
770
|
**First admin and usr.** usr's `/api/discord/allow` needs the admin running `/allow` to be linked in
|
|
729
771
|
usr already, so the first admin cannot be `/allow`ed by anyone. With the usr link on, a configured
|
|
@@ -740,8 +782,8 @@ From then on they `/allow` everyone else. (usr's own "Setup" step 3, in `docs/di
|
|
|
740
782
|
(E10, deferred). Discord OAuth2 as a second sign-in
|
|
741
783
|
method, if chosen (plan item 41).
|
|
742
784
|
- For the alert emails: checking the sender's DKIM signature, not only its `From` address (the private address is the guard today), and a captured alert with several matches.
|
|
743
|
-
- For the want-list watcher (#83): a captured BGG response to replace the hand-written fixture
|
|
744
|
-
|
|
785
|
+
- For the want-list watcher (#83): a captured BGG response to replace the hand-written fixture, if
|
|
786
|
+
BGG ever grants a token (it declined the application), a live check that the CLI holds the judge to its `WebFetch(domain:...)` rules,
|
|
745
787
|
and giving the scout, wantlist and wantjudge types back to `Rackbops/docket`.
|
|
746
788
|
- The rest of #82: per-type grants beyond tier 0 (research needs only `notify`), the transcripts
|
|
747
789
|
policy, and live verification against a real city-hall and docket-runner
|
|
@@ -750,7 +792,7 @@ From then on they `/allow` everyone else. (usr's own "Setup" step 3, in `docs/di
|
|
|
750
792
|
[Lepid-Labs/city-hall#17](https://github.com/Lepid-Labs/city-hall/issues/17); the research lane
|
|
751
793
|
still needs a deployed city-hall with a runner).
|
|
752
794
|
- Editing in Discord (the editor is on the web only; `/scout edit` is the one exception), and changing a price tracker's page or `near` after it is made (make a new one); a free-form pattern for `price` (`near` is the safe subset: a user's regular expression run on a large page could hang the bot).
|
|
753
|
-
- Showing
|
|
795
|
+
- Showing a run's deliveries in its history (admins see the ones that did not arrive on `/admin/deliveries`).
|
|
754
796
|
|
|
755
797
|
docket-core, docket-types and `@rackbops/styles` are `devDependencies`: `bun build` bundles them
|
|
756
798
|
into `dist/plugin.js` (the theme's CSS as text), and the bot loads that file without installing
|