@rackbops/plugin-tracker 0.4.0 → 0.6.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 +130 -12
- package/dist/plugin.js +1700 -230
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@ on a rackbops-discord-bot instance. Plan of record: Rackbops/Tooling
|
|
|
5
5
|
`research/city-hall-task-tracker.md` (sections 0, 5.1-5.3, 5.5, 5.8, 5.10). This version is the core
|
|
6
6
|
(rackbops-bot-plugins#78), the Discord surface of the first slice (#79) -- reminders by slash
|
|
7
7
|
command, delivered by DM, answered by button, with history -- the first slice of the web area
|
|
8
|
-
(#80): sign-in by one-time link, my tasks, a task's history, and settings --
|
|
8
|
+
(#80): sign-in by one-time link, my tasks, a task's history, and settings -- renewals and
|
|
9
9
|
the price tracker (#81, plan E6): two more types on the notify side, no model, nothing sent to
|
|
10
|
-
city-hall
|
|
10
|
+
city-hall -- the web area's task editor (#80, slice 2): make, edit, pause, resume and delete
|
|
11
|
+
your own tasks from the web, under the commands' own rules -- and the admin view and forget-me (#80,
|
|
12
|
+
slice 3).
|
|
11
13
|
|
|
12
14
|
## Commands
|
|
13
15
|
|
|
@@ -26,7 +28,7 @@ as in a server (plan 5.5, item 39).
|
|
|
26
28
|
| `/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. |
|
|
27
29
|
| `/task history task` | the owner, an accepted recipient, or an admin | Every run -- due, status, how it was answered, text replies -- and the task's changes; 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. |
|
|
28
30
|
| `/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. |
|
|
29
|
-
| `/task resume task` | the task's owner | Resumes a task paused because a recipient could not be DMed
|
|
31
|
+
| `/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. |
|
|
30
32
|
| `/settings hour hour` | registered | The preferred hour; recurring reminders with no time of their own move to it. |
|
|
31
33
|
| `/web` | registered | A one-time link to sign in to the web area (below), good for 10 minutes and one use. Says so when `TRACKER_WEB_URL` is unset. |
|
|
32
34
|
|
|
@@ -58,9 +60,17 @@ served at a hashed path, cached for a year).
|
|
|
58
60
|
|
|
59
61
|
| Path | What |
|
|
60
62
|
|---|---|
|
|
61
|
-
| `/` | 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. |
|
|
62
|
-
| `/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. |
|
|
63
|
+
| `/` | 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. |
|
|
64
|
+
| `/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, Pause or Resume, and Delete. |
|
|
65
|
+
| `/new/reminder`, `/new/renewal`, `/new/price` | The editor's new-task forms (GET), and making one (POST). |
|
|
66
|
+
| `/tasks/<id>/edit` | The owner's edit form (GET) and saving it (POST). |
|
|
67
|
+
| `/tasks/<id>/pause`, `/resume`, `/delete` | POST only. Delete answers a confirmation first; only a second post carrying `confirm=yes` deletes. |
|
|
63
68
|
| `/settings` | Preferred hour and time zone, checked as `/register` checks them. |
|
|
69
|
+
| `/forget` | Forget me (below): what it deletes (GET); a POST asks for the confirmation; a POST with `confirm=yes` and the word `forget` erases. Linked from Settings. |
|
|
70
|
+
| `/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. |
|
|
71
|
+
| `/admin/tasks` | Admins only: every task in the store, with its owner, status and how many receive it, each linking to its page. |
|
|
72
|
+
| `/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. |
|
|
73
|
+
| `/admin/allow`, `/admin/people/<id>/grant`, `/revoke`, `/resume-delivery`, `/forget`, `/admin/blocks/<id>/lift` | Admins only, POST only: the acts below. |
|
|
64
74
|
| `/signin` | Where a request that is not signed in is sent: says to run `/web`. |
|
|
65
75
|
| `/login?t=...` | The link `/web` gives. |
|
|
66
76
|
|
|
@@ -83,6 +93,33 @@ than `TRACKER_WEB_URL`'s is refused. The sign-in post has no session yet, so it
|
|
|
83
93
|
double-submit token from a `SameSite=Strict` cookie the link's page sets: another site cannot sign
|
|
84
94
|
you in as someone else. Sign out is a POST too.
|
|
85
95
|
|
|
96
|
+
**The task editor** (slice 2). The forms take the options of `/remind`, `/renewal` and `/price`,
|
|
97
|
+
and the same functions check and make them: the same defaults, limits and messages, the page read
|
|
98
|
+
once before a price tracker is made, at most 20 price trackers per person wherever they were made,
|
|
99
|
+
and `near` turned into the same bounded pattern; a person has one new price tracker's page read in
|
|
100
|
+
flight at a time. Every person may have at most 200 active or paused tasks of all types together,
|
|
101
|
+
made anywhere. The length limits are one set (`src/limits.ts`) for the slash options, the forms
|
|
102
|
+
and the server's checks. An edit changes a reminder's text, time and repeat
|
|
103
|
+
(an empty time keeps the one it has, unless the repeat changes); a renewal's name, amount,
|
|
104
|
+
currency, date, unit, every, lead and note (a new amount is the one the next ask quotes; a new date
|
|
105
|
+
gets `/renewal`'s first-ask rule, but a date a run already asked about is not asked again); a price's name, interval,
|
|
106
|
+
drop and baseline, never its page. A schedule change cancels and replaces what is queued and keeps
|
|
107
|
+
snoozes, as a zone move does, and each change is in the task's history. An empty field keeps what
|
|
108
|
+
the task has (an empty note clears it; an empty price name is the page's address). A renewal date
|
|
109
|
+
that is one of the schedule's own period dates keeps the schedule and its anchor as they are, so the
|
|
110
|
+
31st stays the 31st; only a different date re-anchors. A finished task cannot be
|
|
111
|
+
edited. Pause holds everything the task would send until Resume, and a person's delivery resuming
|
|
112
|
+
does not undo it; a run missed while paused fires once, late, on resume, and a zone or hour change
|
|
113
|
+
re-times the run a paused task holds -- same period date or local day, new zone or hour, still one
|
|
114
|
+
run -- so it is neither lost nor sent at the old time. Resume on a task paused
|
|
115
|
+
for failed DMs is `/task resume`. Delete archives the task: it leaves every list, its queued runs
|
|
116
|
+
are dropped and nothing more is sent, but its history is kept (an admin can see every task) and
|
|
117
|
+
its page stays for the owner. Every editor post passes the session, CSRF and `Origin` checks; the
|
|
118
|
+
task is loaded by the id in the path and must be the viewer's own, checked again in the write
|
|
119
|
+
queue, and anyone else's -- even one an admin can see -- answers the same 404 as an unknown id. A
|
|
120
|
+
refused form comes back with what was typed and why. Form bodies are capped at 32 KiB, read no
|
|
121
|
+
further than that.
|
|
122
|
+
|
|
86
123
|
**Headers.** `Content-Security-Policy: default-src 'none'; style-src 'self'; img-src 'none';
|
|
87
124
|
form-action 'self'; frame-ancestors 'none'; base-uri 'none'`, `X-Content-Type-Options: nosniff`,
|
|
88
125
|
`X-Frame-Options: DENY`. Every value on a page is HTML-escaped. An unknown path is 404; a method
|
|
@@ -99,7 +136,86 @@ while the last confirmation is under 24 hours old, and are then signed out and t
|
|
|
99
136
|
again. Concurrent requests share one lookup per person, and after a failed one that person is not
|
|
100
137
|
looked up again for a minute, so an outage or rate limit does not pile up calls. A confirmation
|
|
101
138
|
time in the future (a clock set back) counts as stale. Every lookup asks Discord (`force: true`),
|
|
102
|
-
never discord.js's member cache, which with only the Guilds intent never learns that someone left.
|
|
139
|
+
never discord.js's member cache, which with only the Guilds intent never learns that someone left.
|
|
140
|
+
|
|
141
|
+
**The admin view** (slice 3, plan 5.8, 5.10). An admin is a person whose row in the tracker's store
|
|
142
|
+
has the admin flag -- the one admin definition; `/allow` checks the same flag. `TRACKER_ADMIN_DISCORD_IDS`
|
|
143
|
+
grants it at start; admins grant and revoke it here. The flag is read from the store on every
|
|
144
|
+
request, and again inside the write queue before any admin act, so an admin whose flag is revoked is
|
|
145
|
+
refused on their very next request. To a signed-in person who is not an admin, every `/admin` path --
|
|
146
|
+
any method -- answers the same 404, byte for byte, as an unknown page; a visitor who is not signed in
|
|
147
|
+
is sent to sign in, as on every other signed-in path. An admin sees every person, every task
|
|
148
|
+
and every task's page and history, read-only: an admin never edits, pauses or deletes another
|
|
149
|
+
person's task (the owner's acts answer them 404, as in slice 2). What an admin can do, each through
|
|
150
|
+
the function the command path runs:
|
|
151
|
+
|
|
152
|
+
- **Allow** a person by Discord id: `/allow`'s own `allowPerson`, with the same server-membership
|
|
153
|
+
check when `TRACKER_GUILD_ID` is set (one bounded lookup; no Discord client yet since a restart
|
|
154
|
+
refuses, as an unknown answer does). A form cannot tell a bot's id from a person's; a bot's row can
|
|
155
|
+
never `/register`.
|
|
156
|
+
- **Make admin / Revoke admin** (plan 5.8). The last admin can be neither revoked nor removed. An
|
|
157
|
+
admin who revokes themselves lands on My tasks. Each change is logged (`u1 made u2 an admin`).
|
|
158
|
+
An admin named in `TRACKER_ADMIN_DISCORD_IDS` is made an admin again at every start, so their page
|
|
159
|
+
offers neither Revoke nor Remove, and both are refused: take them out of the configuration first.
|
|
160
|
+
Such an admin may still forget themselves (if another admin is left); the pages say that the next
|
|
161
|
+
start makes them again, as a new person, unless the configuration no longer names them.
|
|
162
|
+
- **Resume delivery** of a person paused after failed DMs: `DeliveryHealth.resume`, what the
|
|
163
|
+
person's own next command does -- the count clears and the tasks nothing else holds go back on.
|
|
164
|
+
- **Lift** a decline block in force: docket's `liftBlock`, recorded as that admin's lift.
|
|
165
|
+
- **Remove from the tracker**: forget-me for that person (below), with the same confirmation.
|
|
166
|
+
|
|
167
|
+
Not here yet, though plan 5.10 lists them: grants, budgets and retry (they belong to the execute
|
|
168
|
+
lane, which is not built) and an admin pause of someone else's task.
|
|
169
|
+
|
|
170
|
+
**Forget me** (plan 5.8). A signed-in person erases themselves from `/forget` (web only: the plan
|
|
171
|
+
asks for no Discord command, so without `TRACKER_WEB_URL` the only path is an admin with the web
|
|
172
|
+
area). Two posts: the first asks, the second must carry `confirm=yes` and the word `forget`. Then,
|
|
173
|
+
in one SQLite transaction in the write queue, **deleted, not archived**:
|
|
174
|
+
|
|
175
|
+
- every task they own, archived ones too, with everything under it: runs, run events, replies
|
|
176
|
+
(anyone's), history, series, recipients, delivery claims and pauses;
|
|
177
|
+
- on everyone else's tasks: their recipient rows, their replies and answers, the history rows they
|
|
178
|
+
made or that name them (an invitation of them, a pause for them), the run events and delivery
|
|
179
|
+
claims of DMs to them, and their pause rows;
|
|
180
|
+
- every decline block they are either side of;
|
|
181
|
+
- their admission, delivery health, web sessions and unused sign-in links, and their person row.
|
|
182
|
+
|
|
183
|
+
What stays, and why: another person's task that they received stays that owner's; one paused only
|
|
184
|
+
because DMs to them failed goes back on. Another person's row keeps its own audit fact but not their
|
|
185
|
+
id -- `admitted_by` of someone they admitted, and `lifted_by` of a block they lifted, become
|
|
186
|
+
`forgotten` ("an admin since forgotten"; null already means "the configuration"). Text is matched
|
|
187
|
+
only in the forms the code writes a person's id into -- docket's consent rows (`u5`, `u5 24h`), the
|
|
188
|
+
tracker's `u5: ...` removals and `delivery to {u5} paused` pauses, docket's `u5 <message>` delivery
|
|
189
|
+
events, and the notifier's error phrases (`recipient u5 cannot be messaged`, ...) -- never as any
|
|
190
|
+
id-like word, so an address with `/u5/` in someone else's text is left alone. A history row or a
|
|
191
|
+
delivery event of theirs is deleted; the error of someone else's run keeps its row and has the id
|
|
192
|
+
replaced with `(forgotten)`. A pause for a recipient now names them by id and shows their current
|
|
193
|
+
name when read; a pause row written before 0.6.0 names the recipient's display name of the time,
|
|
194
|
+
and is deleted when it is on a task they were a recipient of, names their current name, and no
|
|
195
|
+
other recipient of that task has that name. A row written under an older name of theirs, or shared
|
|
196
|
+
with a same-named recipient, is kept: the one known gap. A reply other people wrote on the erased
|
|
197
|
+
person's own tasks is deleted with those tasks. Outside the store, nothing is touched: the DMs the
|
|
198
|
+
bot sent stay in the person's Discord DMs, and the host's log may hold their tracker id.
|
|
199
|
+
|
|
200
|
+
Bytes: the database runs with `secure_delete` on, so a deleted row is overwritten, and the first
|
|
201
|
+
start of 0.6.0 on an existing database runs one `VACUUM`, so pages freed before that are rewritten
|
|
202
|
+
too. That one-time VACUUM is recorded in a small `tracker_meta` table, not by a schema bump: the
|
|
203
|
+
schema stays at 3, so rolling back to 0.5.0 still opens the database. If the VACUUM cannot run (a
|
|
204
|
+
reader holds the file), the start goes on, a warning is logged, and the next start tries again.
|
|
205
|
+
Not a VACUUM per erasure: it rewrites the whole file under an
|
|
206
|
+
exclusive lock, and `secure_delete` already covers every later delete. After an erasure the
|
|
207
|
+
write-ahead log is checkpointed without waiting; when a reader keeps it busy, that is logged and
|
|
208
|
+
SQLite's next checkpoint copies it back.
|
|
209
|
+
|
|
210
|
+
Timing: forget-me first waits, outside the write queue, until no notify or poll tick is running (at
|
|
211
|
+
most 20 seconds; then it answers 503, try again, and deletes nothing), so a reminder DM to them
|
|
212
|
+
already in flight cannot be recorded after they are gone -- and no one else's command waits behind
|
|
213
|
+
it. Inside the queue it only checks that no tick started meanwhile (else the same 503). A DM sent
|
|
214
|
+
outside any tick -- an invitation's -- that fails after they are gone writes nothing: the failure
|
|
215
|
+
count is not kept for a person no longer in the store, and the withdrawn invitation is not
|
|
216
|
+
recorded. A `/price` whose page is read while its person is forgotten makes nothing. Their sessions
|
|
217
|
+
are among the rows, so they are signed out everywhere at once, and the browser's cookie is cleared. They can come back only as someone new: an admin
|
|
218
|
+
`/allow`s them again, and the tracker never reuses an id.
|
|
103
219
|
|
|
104
220
|
**Choose the web origin's domain with care.** A host under the same parent domain as
|
|
105
221
|
`TRACKER_WEB_URL` that you do not control can set a `__Secure-` cookie on the parent domain
|
|
@@ -114,13 +230,15 @@ The `__Host-` prefix, which would stop that, requires `Path=/` and so cannot be
|
|
|
114
230
|
| 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. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
115
231
|
| Notify lane | `src/notify-lane.ts` | docket's `Lanes.tickNotify` on two host ticks, every 60 s, through a view of the store that skips a paused task's due runs: `notify` runs every type but the page readers, `poll` runs only them (`price`) with the Fetch port, so a slow page never holds up a reminder. Registers `reminder`, `renewal` and `price`. The execute lane is not ticked. |
|
|
116
232
|
| 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. |
|
|
117
|
-
| Renewals, prices | `src/tracked.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. |
|
|
233
|
+
| 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. |
|
|
118
234
|
| Delivery | `src/notifier.ts`, `src/claims.ts`, `src/buttons.ts` | docket's `Notifier` over `host.dm`, with the buttons. Each (occurrence, person) is claimed in `delivery_claims` before the DM is sent and settled after; a claim never settled is not resent -- it is logged once, as unconfirmed. |
|
|
119
235
|
| Pause | `src/delivery-health.ts` | The per-person failure count, the pause and the resume. |
|
|
120
236
|
| People | `src/people.ts`, `src/admissions.ts` | Discord id, time zone, preferred hour, admin flag -- in the tracker's store, never usr; who admitted each person and when they registered. `TRACKER_ADMIN_DISCORD_IDS` only ever grants admin. |
|
|
121
237
|
| 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. |
|
|
122
238
|
| Health | `src/health.ts` | `GET /tracker/healthz` (through the bot's HTTP router): `200` `ok`/`starting`, `503` `inactive`/`stale`/`blocked`. |
|
|
123
|
-
|
|
|
239
|
+
| 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. |
|
|
240
|
+
| 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; `editor.ts` reads the editor's forms and calls the shared rules; `signin-link.ts` and `sessions.ts` hold the sign-in; `theme.ts` the stylesheet; `command.ts` is `/web`. |
|
|
241
|
+
| 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. |
|
|
124
242
|
|
|
125
243
|
## Configuration
|
|
126
244
|
|
|
@@ -134,11 +252,11 @@ The `__Host-` prefix, which would stop that, requires `Path=/` and so cannot be
|
|
|
134
252
|
|
|
135
253
|
## Not yet
|
|
136
254
|
|
|
137
|
-
- The rest of the web area (E5):
|
|
138
|
-
|
|
139
|
-
|
|
255
|
+
- The rest of the web area (E5): sharing a task from the web, the JSON task API, and the admin
|
|
256
|
+
view's grants, budgets and retry (with the execute lane). Discord OAuth2 as a second sign-in
|
|
257
|
+
method, if chosen (plan item 41).
|
|
140
258
|
- The city-hall Executor adapter (the execute lane).
|
|
141
|
-
- Editing
|
|
259
|
+
- Editing in Discord (the editor is on the web only), 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).
|
|
142
260
|
- An optional Discord-role gate (plan 5.5), and showing unconfirmed deliveries to admins anywhere but the log.
|
|
143
261
|
|
|
144
262
|
docket-core, docket-types and `@rackbops/styles` are `devDependencies`: `bun build` bundles them
|