@rackbops/plugin-tracker 0.18.1 → 0.23.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 +65 -16
- package/dist/plugin.js +941 -92
- package/package.json +26 -5
package/README.md
CHANGED
|
@@ -18,11 +18,13 @@ ceilings off for the alpha and an admin page of daily model usage to evaluate it
|
|
|
18
18
|
interest scout (#83, plan E9, category 1): a model run every few days that looks on the web for
|
|
19
19
|
things that fit someone's interests through a gift lens, DMs what it found, and does not show the
|
|
20
20
|
same link twice -- and the want-list watcher (#83, plan E9, category 2): a watch on one wanted thing
|
|
21
|
-
that reads a listing page you paste, or BoardGameGeek's marketplace
|
|
22
|
-
|
|
23
|
-
you
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
that reads a listing page you paste, or BoardGameGeek's marketplace through BGG's API once its
|
|
22
|
+
token is set, every few hours with no model, or, for eBay (and BGG without that token), the
|
|
23
|
+
listings you send in through the task API every hour -- the tracker never opens eBay or BGG
|
|
24
|
+
itself; your own browser finds them, such as Claude in Chrome running in it -- and DMs each new
|
|
25
|
+
listing within your limits once, until you press Done, and, while the model runner is set up, has
|
|
26
|
+
the model look at each new listing first -- is it the thing, and what does it show about the
|
|
27
|
+
seller -- before DMing it -- and the daily "today and
|
|
26
28
|
overdue" digest (plan 5.5): one DM a day at each person's preferred hour listing their reminders
|
|
27
29
|
and renewals due today or overdue. The model runs never happen here: they go
|
|
28
30
|
through city-hall to Rackbops/docket-runner on roshne's own host (plan 5.12), and the whole execute
|
|
@@ -35,14 +37,14 @@ as in a server (plan 5.5, item 39).
|
|
|
35
37
|
|
|
36
38
|
| Command | Who | What |
|
|
37
39
|
|---|---|---|
|
|
38
|
-
| `/allow user` | a tracker admin | Admits a person: they can now `/register`. |
|
|
39
|
-
| `/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. |
|
|
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. |
|
|
41
|
+
| `/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. |
|
|
40
42
|
| `/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. |
|
|
41
43
|
| `/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. |
|
|
42
44
|
| `/price url [name] [hours] [drop] [baseline] [near]` | registered | A price (docket's `price`, a `poll` schedule every `hours`, default 12, at most 168). 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. |
|
|
43
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. |
|
|
44
46
|
| `/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. |
|
|
45
|
-
| `/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
|
|
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 tracker 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); later eBay's saved-search alert emails -- 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`, 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. |
|
|
46
48
|
| `/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. |
|
|
47
49
|
| `/tasks` | registered | Your active tasks and the ones you receive, each with its next run; then your paused ones. |
|
|
48
50
|
| `/task done task` / `/task snooze task [until]` | the task's owner | Answers the task's latest reminder (snooze: an hour, or until `until`). |
|
|
@@ -194,7 +196,7 @@ served at a hashed path, cached for a year).
|
|
|
194
196
|
|---|---|
|
|
195
197
|
| `/` | My tasks: the same list as `/tasks` (active tasks owned and received, next run in your zone; paused ones and why), each linking to its history, and links to make a new one. |
|
|
196
198
|
| `/tasks/<id>` | A task's history: the same as `/task history`, for the owner, an accepted recipient or an admin. Anyone else gets the same 404 as an unknown id. The owner also sees Edit (not for a research request), Pause or Resume, and Delete. |
|
|
197
|
-
| `/new/wantlist` | A new want-list watch (GET, POST), by `/want`'s rules (`judge` too, yes or no): a page
|
|
199
|
+
| `/new/wantlist` | A new want-list watch (GET, POST), by `/want`'s rules (`judge` too, yes or no): a page, a BGG game, or eBay (an inbox watch: `target` is the words to search for, the name when empty). Its edit changes the name, top price, currency and hours; an emptied top price or currency clears it, and where it looks -- an inbox watch's key and words too -- stays as made. |
|
|
198
200
|
| `/new/reminder`, `/new/renewal`, `/new/price`, `/new/research`, `/new/scout` | The editor's new-task forms (GET), and making one (POST). `/new/research` and `/new/scout` are linked from My tasks only while the model runner is set up; without it the page says the type is not available instead of showing a form. A scout's interests go in a text box, separated by commas or one per line; its edit form shows the list one per line, and an emptied "Who it is for" or "Notes" clears it. |
|
|
199
201
|
| `/tasks/<id>/edit` | The owner's edit form (GET) and saving it (POST). |
|
|
200
202
|
| `/tasks/<id>/pause`, `/resume`, `/delete` | POST only. Delete answers a confirmation first; only a second post carrying `confirm=yes` deletes. |
|
|
@@ -206,9 +208,10 @@ served at a hashed path, cached for a year).
|
|
|
206
208
|
| `/admin/tasks` | Admins only: every task in the store, with its owner, status and how many receive it, each linking to its page. |
|
|
207
209
|
| `/admin/deliveries` | Admins only: the DMs that did not arrive (below), newest first. |
|
|
208
210
|
| `/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. |
|
|
211
|
+
| `/admin/usr-link` | Admins only, POST only, with the usr link on: Link everyone to usr (below). |
|
|
209
212
|
| `/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. |
|
|
210
213
|
| `/api/v1/...` | The JSON task API (below): bearer tokens only, never the cookie. |
|
|
211
|
-
| `/signin` | Where a request that is not signed in is sent: says to run `/web
|
|
214
|
+
| `/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. |
|
|
212
215
|
| `/login?t=...` | The link `/web` gives. |
|
|
213
216
|
|
|
214
217
|
**Signing in** (plan item 41; the one-time link is the method for now, kept in
|
|
@@ -221,6 +224,27 @@ same-origin` and `Cache-Control: no-store` (every page does). Not `no-referrer`:
|
|
|
221
224
|
sends a form post's `Origin` as `null`, which the Origin check refuses, so no form would work; the
|
|
222
225
|
pages link nowhere else, so the token never leaves in a Referer either way.
|
|
223
226
|
|
|
227
|
+
**Signing in through usr** (0.22.0, while the usr link is on: `TRACKER_USR_URL`). A GET with no
|
|
228
|
+
session reads usr's `nz_id` cookie, which usr sets on the parent domain named by its
|
|
229
|
+
`USR_SSO_COOKIE_DOMAIN`. That domain must cover `TRACKER_WEB_URL`'s host: otherwise usr refuses the
|
|
230
|
+
return address and keeps the browser on its own site. The token is checked offline, as usr's
|
|
231
|
+
`docs/sso-verifier.md` says: an ES256 signature against usr's `/.well-known/jwks.json` (cached five
|
|
232
|
+
minutes, fetched again on an unknown key at most every 30 seconds), `iss` "usr", not expired, and
|
|
233
|
+
never a delegation token. A valid token whose account holds `tracker:member` and is linked to a
|
|
234
|
+
registered person (`users.usr_subject`) opens a session as the one-time link does, once server
|
|
235
|
+
membership is checked (as `/web` checks it), and the browser goes on to the page and query it asked
|
|
236
|
+
for. With no valid token the browser is sent to usr's `/api/auth/sso/refresh` once, returning to
|
|
237
|
+
the same address marked `?usr=1`; coming back still without one (usr's keys could not be fetched,
|
|
238
|
+
the clocks disagree) says the sign-in could not be confirmed, instead of going round again. A token
|
|
239
|
+
without the member role, or for an account linked to no one registered here, shows what to ask an
|
|
240
|
+
admin for. While signed in, a usr cookie for the person's own account that no longer carries
|
|
241
|
+
`tracker:member` signs them out of every session and revokes their API tokens. Sign out ends the
|
|
242
|
+
tracker's session only, not usr's: for 12 hours after it, a plain GET does not sign back in from
|
|
243
|
+
usr's cookie; the sign-in page's "Sign in with usr" button (and usr's trip back from it) or a `/web`
|
|
244
|
+
link does. Any site under usr's cookie domain can set an
|
|
245
|
+
`nz_id` of its own, so every host under that domain must be trusted. The one-time `/web` link stays
|
|
246
|
+
beside it. `src/usr-identity.ts` uses WebCrypto only.
|
|
247
|
+
|
|
224
248
|
**Sessions and forms.** The session cookie is `__Secure-tracker-session`, `HttpOnly; Secure;
|
|
225
249
|
SameSite=Lax; Path=/tracker/`, for 7 days from sign-in. The store keeps only SHA-256 hashes of
|
|
226
250
|
link tokens and session ids. Every request re-reads the person: one no longer in the tracker's
|
|
@@ -487,10 +511,11 @@ editable): a research request cannot be edited, here or in Discord, and a `PATCH
|
|
|
487
511
|
| `GET /api/v1/me` | The token's owner (id, name, zone, preferred hour) and the token (id, name, made, expires). |
|
|
488
512
|
| `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. |
|
|
489
513
|
| `GET /api/v1/tasks` | Your tasks that are not deleted -- active, paused and done -- oldest first. Not the ones shared with you. |
|
|
490
|
-
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research` or `scout`, 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 `at`, 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`
|
|
514
|
+
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research` or `scout`, 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 `at`, 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. |
|
|
491
515
|
| `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. |
|
|
492
516
|
| `PATCH /api/v1/tasks/<id>` | Edits it: the fields to change. A price's page is not editable. |
|
|
493
517
|
| `POST /api/v1/tasks/<id>/pause`, `/resume` | Body `{}`. As the web's Pause and Resume, and `/task resume`. |
|
|
518
|
+
| `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. |
|
|
494
519
|
| `DELETE /api/v1/tasks/<id>` | Deletes it as the web does: archived, nothing more sent, history kept. No confirmation step. |
|
|
495
520
|
|
|
496
521
|
A task is `{"id", "type", "title", "status", "cadence", "nextAt", "createdAt", "updatedAt",
|
|
@@ -518,7 +543,7 @@ and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
|
518
543
|
| 409 | `conflict` | A finished task edited, a research request edited, or a pause of a task not active (a resume of one not paused). |
|
|
519
544
|
| 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. |
|
|
520
545
|
| 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). |
|
|
521
|
-
| 413 | `too_large` | Body over 16 KiB. |
|
|
546
|
+
| 413 | `too_large` | Body over 16 KiB (64 KiB for `/listings`). |
|
|
522
547
|
| 415 | `unsupported_media_type` | Not `application/json`. |
|
|
523
548
|
| 429 | `rate_limited` | With `Retry-After`: this token's bucket, or the shared one for bad tokens. |
|
|
524
549
|
| 503 | `starting`, `membership_unknown`, `unavailable` | The plugin is starting; the owner's membership could not be checked for 24 hours; the type is not available on this bot. |
|
|
@@ -550,9 +575,9 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
550
575
|
|
|
551
576
|
| Piece | File | Notes |
|
|
552
577
|
|---|---|---|
|
|
553
|
-
| 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. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
578
|
+
| 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. |
|
|
554
579
|
| 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`. |
|
|
555
|
-
| Want-list watcher | `src/want.ts`; the types and sources are `@rackbops/docket-types`' (0.
|
|
580
|
+
| 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 until a real response is captured with the token. 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. |
|
|
556
581
|
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts`, `src/scout.ts` (the `scout` type is docket-types') | 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 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. |
|
|
557
582
|
| 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. |
|
|
558
583
|
| Page reads | `src/fetch.ts` | docket's `Fetch` port for `price`: 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), 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 -- JSON-LD, meta tags, 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. 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. |
|
|
@@ -561,7 +586,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
561
586
|
| 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; a paused person is passed by unclaimed; a failed send is not retried. |
|
|
562
587
|
| 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). |
|
|
563
588
|
| Pause | `src/delivery-health.ts` | The per-person failure count, the pause and the resume. |
|
|
564
|
-
| People | `src/people.ts`, `src/admissions.ts` | Discord id, time zone, preferred hour, admin flag -- in the tracker's store,
|
|
589
|
+
| 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. |
|
|
565
590
|
| Commands | `src/discord.ts`, `src/interactions.ts`, `src/discord-common.ts` (discord.js); `src/actions.ts`, `src/press.ts`, `src/history.ts`, `src/access.ts` | The discord.js files read options and render; the rest is Discord-free over the injected store, clock and notifier. Store writes are handled one at a time; Discord lookups and DMs run outside that queue. |
|
|
566
591
|
| Health | `src/health.ts` | `GET /tracker/healthz` (through the bot's HTTP router): `200` `ok`/`starting`, `503` `inactive`/`stale`/`blocked`. |
|
|
567
592
|
| 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. |
|
|
@@ -582,7 +607,10 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
582
607
|
| `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. |
|
|
583
608
|
| `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. |
|
|
584
609
|
| `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. |
|
|
585
|
-
| `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 =
|
|
610
|
+
| `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. To get one: register an application at boardgamegeek.com/applications (roshne applied 2026-09-29, non-commercial; approval takes a week or more), then put the token in the instance's `.env` and recreate the bot. BGG's terms ask for attribution: every BGG line in a DM says "via BoardGameGeek". |
|
|
611
|
+
| `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). |
|
|
612
|
+
| `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. |
|
|
613
|
+
| `TRACKER_USR_APP` | no | The usr app the tracker's roles live under (`tracker:member`, `tracker:register`): lowercase words joined by `-`. It must match the app on the key's usr Discord service row, which is what usr goes by. Unset = `tracker`. Needs `TRACKER_USR_URL`. |
|
|
586
614
|
|
|
587
615
|
**The execute lane is off until all three of `TRACKER_CITY_HALL_URL`, `_KEY` and `_CAPABILITY` are
|
|
588
616
|
set**: no `execute` tick, and `/research` answers that research is not available, so nobody can
|
|
@@ -597,6 +625,27 @@ key>`, so two databases never collide.
|
|
|
597
625
|
|
|
598
626
|
`/tracker/healthz` and the web area need the bot's `HTTP_PORT` set; without it there is no HTTP at all.
|
|
599
627
|
|
|
628
|
+
**Link everyone to usr** (0.23.0, the admin page, with the usr link on). People admitted before the
|
|
629
|
+
link was on are not linked to usr. One press starts a run in the background (the bot answers a web
|
|
630
|
+
request it waited on for 10 seconds with a 504, so the press does not wait) and goes back to the
|
|
631
|
+
admin page, which shows how far the run has got and then how it ended; a second press while it runs
|
|
632
|
+
starts nothing. For each person on the list with a Discord id and no usr link, one at a time, it
|
|
633
|
+
checks they are a member of the server with its role, as `/allow` does; re-reads them just before asking
|
|
634
|
+
usr, so someone forgotten before then is never sent; allows them in usr as `tracker:member`, with the pressing
|
|
635
|
+
admin as the one who allowed them; then links them. usr records each link against
|
|
636
|
+
`TRACKER_GUILD_ID`'s first server, so with no gate it refuses and says to `/allow` in the server
|
|
637
|
+
instead. Only a 400 from usr about the person's own details is theirs, listed by name while the rest go on; any other
|
|
638
|
+
refusal or failure (the admin not linked in usr or missing `tracker:register` or `tracker:member`,
|
|
639
|
+
the key, usr unreachable) stops the run, since it would fail everyone the same way. It never starts
|
|
640
|
+
on its own.
|
|
641
|
+
|
|
642
|
+
**First admin and usr.** usr's `/api/discord/allow` needs the admin running `/allow` to be linked in
|
|
643
|
+
usr already, so the first admin cannot be `/allow`ed by anyone. With the usr link on, a configured
|
|
644
|
+
admin (`TRACKER_ADMIN_DISCORD_IDS`) who is not linked gets usr's open sign-up link from `/register`,
|
|
645
|
+
run in a server. They finish signing up on usr, a usr admin gives them `tracker:register` and
|
|
646
|
+
`tracker:member` there, and they run `/allow` on themselves, which links their tracker account.
|
|
647
|
+
From then on they `/allow` everyone else. (usr's own "Setup" step 3, in `docs/discord-registration.md`, has the owner `/register` through the bot; the tracker uses the open link for that one step only, since its `/register` otherwise asks with the allow policy.)
|
|
648
|
+
|
|
600
649
|
## Not yet
|
|
601
650
|
|
|
602
651
|
- The rest of the web area (E5): sharing a task from the web, and the admin view's grants and
|