@rackbops/plugin-tracker 0.11.0 → 0.12.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 +38 -25
- package/dist/plugin.js +90 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ At start the plugin runs docket's `recover()`: a run left running is requeued, a
|
|
|
69
69
|
by a stop mid-send is settled unconfirmed and logged.
|
|
70
70
|
|
|
71
71
|
**Research and the execute lane** (docket 0.5.0, #82). A research request is two model Jobs, run
|
|
72
|
-
one after the other through city-hall's execute lane (Lepid-Labs/city-hall#18) by the runner that
|
|
72
|
+
one after the other through city-hall's execute lane ([Lepid-Labs/city-hall#18](https://github.com/Lepid-Labs/city-hall/pull/18)) by the runner that
|
|
73
73
|
carries `TRACKER_CITY_HALL_CAPABILITY` -- only docket-runner, on roshne's subscription (plan items
|
|
74
74
|
70, 71). The tracker submits each Job with `POST /api/execute/jobs` under a prefixed key,
|
|
75
75
|
`rackbops-tracker:<database id>:<docket's Job key>` -- the database id is random, made once and kept
|
|
@@ -103,15 +103,18 @@ the first sight (a judgement, not a measured figure): past it the Job answers "p
|
|
|
103
103
|
docket gives the run up, and the admins are told once. It logs "the model runner is paused" naming
|
|
104
104
|
the outcome once an hour per Job, and tells the admins once per Job and outcome. docket's own
|
|
105
105
|
usage-limit pause (which waits for the reset the CLI named) applies only to a `usage_limit` result
|
|
106
|
-
that reaches the tracker: city-hall#18 as
|
|
107
|
-
as when a job that was requeued later fails at its maximum of
|
|
106
|
+
that reaches the tracker: under city-hall#18 as merged, that happens only when the job ends failed
|
|
107
|
+
with that result still stored, as when a job that was requeued later fails at its maximum of
|
|
108
|
+
expired leases.
|
|
108
109
|
The plugin never calls a model, holds no Claude credential and no `ANTHROPIC_*` variable; model
|
|
109
110
|
output is data, cleaned by docket before it reaches a DM or a finding, and every research DM goes
|
|
110
111
|
out with no allowed mentions: the host sends every `dm` with `allowedMentions: { parse: [] }`
|
|
111
|
-
(CONTEXT.md). **The wire is city-hall#18's
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
(CONTEXT.md). **The wire is city-hall#18's**
|
|
113
|
+
([Lepid-Labs/city-hall#18](https://github.com/Lepid-Labs/city-hall/pull/18), merged as 90a06ec),
|
|
114
|
+
but its decision record 0002 is still "proposed" pending Nazu's review
|
|
115
|
+
([Lepid-Labs/city-hall#17](https://github.com/Lepid-Labs/city-hall/issues/17)): the source pair is
|
|
116
|
+
a stand-in for the source and responder contracts, so it may change, and this adapter with it --
|
|
117
|
+
the requeue reading above and `lastOutcome` (present in the merged 90a06ec) included.
|
|
115
118
|
|
|
116
119
|
**Research while the lane is off.** If an instance that had city-hall configured loses that
|
|
117
120
|
configuration, its research requests stay queued: nothing runs them until research is available
|
|
@@ -148,8 +151,8 @@ served at a hashed path, cached for a year).
|
|
|
148
151
|
| Path | What |
|
|
149
152
|
|---|---|
|
|
150
153
|
| `/` | 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. |
|
|
151
|
-
| `/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. |
|
|
152
|
-
| `/new/reminder`, `/new/renewal`, `/new/price` | The editor's new-task forms (GET), and making one (POST). |
|
|
154
|
+
| `/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. |
|
|
155
|
+
| `/new/reminder`, `/new/renewal`, `/new/price`, `/new/research` | The editor's new-task forms (GET), and making one (POST). `/new/research` is linked from My tasks only while research is available; without it the page says research is not available instead of showing a form. |
|
|
153
156
|
| `/tasks/<id>/edit` | The owner's edit form (GET) and saving it (POST). |
|
|
154
157
|
| `/tasks/<id>/pause`, `/resume`, `/delete` | POST only. Delete answers a confirmation first; only a second post carrying `confirm=yes` deletes. |
|
|
155
158
|
| `/settings` | Preferred hour and time zone, checked as `/register` checks them. Links to API tokens and Forget me. |
|
|
@@ -184,8 +187,8 @@ than `TRACKER_WEB_URL`'s is refused. The sign-in post has no session yet, so it
|
|
|
184
187
|
double-submit token from a `SameSite=Strict` cookie the link's page sets: another site cannot sign
|
|
185
188
|
you in as someone else. Sign out is a POST too.
|
|
186
189
|
|
|
187
|
-
**The task editor** (slice 2). The forms take the options of `/remind`, `/renewal` and
|
|
188
|
-
and the same functions check and make them: the same defaults, limits and messages, the page read
|
|
190
|
+
**The task editor** (slice 2). The forms take the options of `/remind`, `/renewal`, `/price` and
|
|
191
|
+
`/research` (#82), and the same functions check and make them: the same defaults, limits and messages, the page read
|
|
189
192
|
once before a price tracker is made, at most 20 price trackers per person wherever they were made,
|
|
190
193
|
and `near` turned into the same bounded pattern; a person has one new price tracker's page read in
|
|
191
194
|
flight at a time. Every person may have at most 200 active or paused tasks of all types together,
|
|
@@ -208,7 +211,11 @@ are dropped and nothing more is sent, but its history is kept (an admin can see
|
|
|
208
211
|
its page stays for the owner. Every editor post passes the session, CSRF and `Origin` checks; the
|
|
209
212
|
task is loaded by the id in the path and must be the viewer's own, checked again in the write
|
|
210
213
|
queue, and anyone else's -- even one an admin can see -- answers the same 404 as an unknown id. A
|
|
211
|
-
refused form comes back with what was typed and why.
|
|
214
|
+
refused form comes back with what was typed and why. A research request made on the web is
|
|
215
|
+
`/research`'s exactly -- the question, the optional context, deadline and start in `/research`'s
|
|
216
|
+
words, the cap of 5 waiting, the daily budget -- and, as in Discord, it is never edited: its page
|
|
217
|
+
has Pause, Resume and Delete but no Edit. Making one only queues its run; nothing reaches city-hall
|
|
218
|
+
until the execute tick. Form bodies are capped at 32 KiB, read no
|
|
212
219
|
further than that.
|
|
213
220
|
|
|
214
221
|
**Headers.** `Content-Security-Policy: default-src 'none'; style-src 'self'; img-src 'none';
|
|
@@ -393,15 +400,18 @@ a price back the page's address -- exactly as the web editor's empty fields do.
|
|
|
393
400
|
value: it is refused as the wrong JSON type (send `""` to clear a note).
|
|
394
401
|
|
|
395
402
|
Which types: `GET /tasks`, `GET /tasks/<id>`, pause, resume and `DELETE` work on any task you own,
|
|
396
|
-
whatever its type
|
|
397
|
-
|
|
403
|
+
whatever its type. `POST /tasks` takes the three editor types (reminder, renewal, price) and,
|
|
404
|
+
while research is available on this bot, `research` (#82), by `/research`'s rules; while it is not,
|
|
405
|
+
a `research` create answers `503 unavailable`. `PATCH` takes the three editor types only: a
|
|
406
|
+
research request cannot be edited, here or in Discord, and a `PATCH` of one answers
|
|
407
|
+
`409 conflict`.
|
|
398
408
|
|
|
399
409
|
| Method and path | What |
|
|
400
410
|
|---|---|
|
|
401
411
|
| `GET /api/v1/me` | The token's owner (id, name, zone, preferred hour) and the token (id, name, made, expires). |
|
|
402
|
-
| `GET /api/v1/types` | Each type
|
|
412
|
+
| `GET /api/v1/types` | Each type this bot makes now (research only while it is available), 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. |
|
|
403
413
|
| `GET /api/v1/tasks` | Your tasks that are not deleted -- active, paused and done -- oldest first. Not the ones shared with you. |
|
|
404
|
-
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal` or `
|
|
414
|
+
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price` or `research`, 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. |
|
|
405
415
|
| `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. |
|
|
406
416
|
| `PATCH /api/v1/tasks/<id>` | Edits it: the fields to change. A price's page is not editable. |
|
|
407
417
|
| `POST /api/v1/tasks/<id>/pause`, `/resume` | Body `{}`. As the web's Pause and Resume, and `/task resume`. |
|
|
@@ -411,7 +421,8 @@ A task is `{"id", "type", "title", "status", "cadence", "nextAt", "createdAt", "
|
|
|
411
421
|
"settings"}`, and a price also has a top-level `url`: `status` is `active`, `paused`, `done` or
|
|
412
422
|
`deleted`; `cadence` is the schedule in words, in your zone; `nextAt` the next run's instant (UTC),
|
|
413
423
|
null when paused or nothing is due; `settings` exactly the fields a `PATCH` takes, as the task has
|
|
414
|
-
them. A
|
|
424
|
+
them. A research request's `settings` are what was asked -- `question`, and `context` and
|
|
425
|
+
`deadline` (an instant) when given -- shown to you alone and changed by no `PATCH`. A price's `url` is read-only -- another page is another tracker -- so it is not in `settings`
|
|
415
426
|
and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
416
427
|
`{"task", "message"}`, `message` being the words the web and the command say.
|
|
417
428
|
|
|
@@ -428,9 +439,9 @@ and a `PATCH` naming it is refused (`unknown_field`). A write answers
|
|
|
428
439
|
| 403 | `not_member` | The owner has left every `TRACKER_GUILD_ID` server; their tokens are revoked. |
|
|
429
440
|
| 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. |
|
|
430
441
|
| 405 | `method_not_allowed` | With `Allow`. |
|
|
431
|
-
| 409 | `conflict` | A finished task edited, or a pause of a task not active (a resume of one not paused). |
|
|
432
|
-
| 409 | `limit_reached` | 200 active or paused tasks,
|
|
433
|
-
| 409 | `busy` | Your last new price tracker's page is still being read. |
|
|
442
|
+
| 409 | `conflict` | A finished task edited, a research request edited, or a pause of a task not active (a resume of one not paused). |
|
|
443
|
+
| 409 | `limit_reached` | 200 active or paused tasks, 20 price trackers, or 5 research requests waiting, as the commands count them. |
|
|
444
|
+
| 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). |
|
|
434
445
|
| 413 | `too_large` | Body over 16 KiB. |
|
|
435
446
|
| 415 | `unsupported_media_type` | Not `application/json`. |
|
|
436
447
|
| 429 | `rate_limited` | With `Retry-After`: this token's bucket, or the shared one for bad tokens. |
|
|
@@ -507,14 +518,16 @@ key>`, so two databases never collide.
|
|
|
507
518
|
## Not yet
|
|
508
519
|
|
|
509
520
|
- The rest of the web area (E5): sharing a task from the web, and the admin view's grants, budgets
|
|
510
|
-
(raising a person's ceiling) and retry.
|
|
511
|
-
|
|
512
|
-
not `/task done`, `snooze`, `decide`, `share` or the settings -- and no intake agent uses it yet
|
|
521
|
+
(raising a person's ceiling) and retry. The task API makes reminders, renewals, prices and
|
|
522
|
+
research requests -- not `/task done`, `snooze`, `decide`, `share` or the settings -- and no intake agent uses it yet
|
|
513
523
|
(E10, deferred). Discord OAuth2 as a second sign-in
|
|
514
524
|
method, if chosen (plan item 41).
|
|
515
525
|
- The rest of #82: per-type grants beyond tier 0 (research needs only `notify`), the transcripts
|
|
516
|
-
policy, and live verification against a real city-hall and docket-runner
|
|
517
|
-
|
|
526
|
+
policy, and live verification against a real city-hall and docket-runner
|
|
527
|
+
([Lepid-Labs/city-hall#18](https://github.com/Lepid-Labs/city-hall/pull/18) is merged as
|
|
528
|
+
90a06ec, its decision record still "proposed" pending
|
|
529
|
+
[Lepid-Labs/city-hall#17](https://github.com/Lepid-Labs/city-hall/issues/17); the research lane
|
|
530
|
+
still needs a deployed city-hall with a runner). The scout and the want-list (E9).
|
|
518
531
|
- 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).
|
|
519
532
|
- An optional Discord-role gate (plan 5.5), and showing unconfirmed deliveries to admins anywhere but the log, or a run's deliveries in its history.
|
|
520
533
|
|
package/dist/plugin.js
CHANGED
|
@@ -8289,14 +8289,15 @@ function cadence(entry, viewer, now) {
|
|
|
8289
8289
|
return "once";
|
|
8290
8290
|
return describeSchedule(s, entry.from ?? viewer, viewer.timeZone, now);
|
|
8291
8291
|
}
|
|
8292
|
-
function newLinks(v) {
|
|
8292
|
+
function newLinks(v, research) {
|
|
8293
8293
|
return html`<p class="tr-row">
|
|
8294
8294
|
<a class="rb-btn rb-btn--primary rb-btn--sm" href="${v.base}/new/reminder">New reminder</a>
|
|
8295
8295
|
<a class="rb-btn rb-btn--ghost rb-btn--sm" href="${v.base}/new/renewal">New renewal</a>
|
|
8296
8296
|
<a class="rb-btn rb-btn--ghost rb-btn--sm" href="${v.base}/new/price">New price tracker</a>
|
|
8297
|
+
${research ? html`<a class="rb-btn rb-btn--ghost rb-btn--sm" href="${v.base}/new/research">New research request</a>` : null}
|
|
8297
8298
|
</p>`;
|
|
8298
8299
|
}
|
|
8299
|
-
function tasksPage(v, data, now, flash = null) {
|
|
8300
|
+
function tasksPage(v, data, now, flash = null, research = false) {
|
|
8300
8301
|
const rows = data.entries.map((e) => html`<tr>
|
|
8301
8302
|
<td><a class="rb-link" href="${v.base}/tasks/${e.task.id}">${e.task.title}</a></td>
|
|
8302
8303
|
<td>${e.from ? name3(e.from) : "you"}</td>
|
|
@@ -8315,7 +8316,7 @@ function tasksPage(v, data, now, flash = null) {
|
|
|
8315
8316
|
<h1>My tasks</h1>
|
|
8316
8317
|
${flash}
|
|
8317
8318
|
<p class="rb-muted">Times are in ${v.user.timeZone}.</p>
|
|
8318
|
-
${newLinks(v)}
|
|
8319
|
+
${newLinks(v, research)}
|
|
8319
8320
|
${active}
|
|
8320
8321
|
</section>
|
|
8321
8322
|
${paused}
|
|
@@ -8405,6 +8406,7 @@ function errorPage(base, title, message) {
|
|
|
8405
8406
|
|
|
8406
8407
|
// plugins/tracker/src/web/editor-pages.ts
|
|
8407
8408
|
var EDITOR_TYPES = ["reminder", "renewal", "price"];
|
|
8409
|
+
var NEW_TYPES = [...EDITOR_TYPES, "research"];
|
|
8408
8410
|
var REPEAT_OPTIONS = [["none", "once"], ["day", "daily"], ["week", "weekly"], ["month", "monthly"]];
|
|
8409
8411
|
var UNIT_OPTIONS = [["year", "yearly"], ["month", "monthly"], ["week", "weekly"], ["day", "daily"]];
|
|
8410
8412
|
var BASELINE_OPTIONS = [["last", "the last price seen"], ["first", "the first price seen"], ["peak", "the highest price seen"]];
|
|
@@ -8414,8 +8416,16 @@ var PRICE_SETTINGS = [
|
|
|
8414
8416
|
{ name: "drop", label: "Alert on a drop of at least (percent)", kind: "text", decimal: true, help: "1 to 90; 10 when empty." },
|
|
8415
8417
|
{ name: "baseline", label: "Measure the drop from", kind: "select", options: BASELINE_OPTIONS }
|
|
8416
8418
|
];
|
|
8419
|
+
var RESEARCH_FIELDS = [
|
|
8420
|
+
{ name: "question", label: "What to look into", kind: "textarea", required: true, maxlength: MAX_QUESTION_CHARS },
|
|
8421
|
+
{ name: "context", label: "Context", kind: "textarea", maxlength: MAX_CONTEXT_CHARS, help: "Anything that narrows it down. Only you see it here; it goes to the model with the question." },
|
|
8422
|
+
{ name: "deadline", label: "Deadline", kind: "text", maxlength: MAX_WHEN, help: 'For example "fri 17:00". Past it nothing more is run.' },
|
|
8423
|
+
{ name: "at", label: "Start", kind: "text", maxlength: MAX_WHEN, help: "Leave it empty to start now." }
|
|
8424
|
+
];
|
|
8417
8425
|
function fieldsFor(type, mode) {
|
|
8418
8426
|
switch (type) {
|
|
8427
|
+
case "research":
|
|
8428
|
+
return mode === "new" ? RESEARCH_FIELDS : [];
|
|
8419
8429
|
case "reminder":
|
|
8420
8430
|
return [
|
|
8421
8431
|
{ name: "text", label: "Remind me to", kind: "text", required: true, maxlength: MAX_REMINDER_TEXT },
|
|
@@ -8458,6 +8468,13 @@ function field(f, values) {
|
|
|
8458
8468
|
<label class="rb-label" for="${f.name}">${f.label}</label>
|
|
8459
8469
|
<select class="rb-select" id="${f.name}" name="${f.name}">${(f.options ?? []).map(([v, label]) => html`<option value="${v}"${v === value ? html` selected` : null}>${label}</option>`)}</select>
|
|
8460
8470
|
${help}
|
|
8471
|
+
</div>`;
|
|
8472
|
+
}
|
|
8473
|
+
if (f.kind === "textarea") {
|
|
8474
|
+
return html`<div class="rb-field">
|
|
8475
|
+
<label class="rb-label" for="${f.name}">${f.label}</label>
|
|
8476
|
+
<textarea class="rb-textarea" id="${f.name}" name="${f.name}" rows="4"${f.maxlength ? html` maxlength="${f.maxlength}"` : null}${f.required ? html` required` : null}>${value}</textarea>
|
|
8477
|
+
${help}
|
|
8461
8478
|
</div>`;
|
|
8462
8479
|
}
|
|
8463
8480
|
const type = f.kind === "number" ? "number" : f.kind === "date" ? "date" : f.kind === "url" ? "url" : "text";
|
|
@@ -8473,19 +8490,25 @@ function alert(error) {
|
|
|
8473
8490
|
function taskHref(v, id, action = "") {
|
|
8474
8491
|
return `${v.base}/tasks/${encodeURIComponent(id)}${action ? `/${action}` : ""}`;
|
|
8475
8492
|
}
|
|
8476
|
-
var NOUN = { reminder: "reminder", renewal: "renewal", price: "price tracker" };
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8493
|
+
var NOUN = { reminder: "reminder", renewal: "renewal", price: "price tracker", research: "research request" };
|
|
8494
|
+
var INTRO = {
|
|
8495
|
+
price: "I read the page once now; nothing is made unless I find a price in it.",
|
|
8496
|
+
research: "A research run looks it up on the web, then a second run checks the answer against its sources; only an answer that passes is DMed to you. It can take a while, and requests share a daily budget."
|
|
8497
|
+
};
|
|
8498
|
+
function newTaskPage(v, type, values, error, off) {
|
|
8499
|
+
const text = INTRO[type];
|
|
8500
|
+
const intro = text ? html`<p class="rb-muted">${text}</p>` : null;
|
|
8501
|
+
const form = off ? html`<div class="rb-alert rb-alert--warning" role="status"><p>${prose(off)}</p></div>` : html`${alert(error)}
|
|
8484
8502
|
<form method="post" action="${v.base}/new/${type}" class="tr-stack">
|
|
8485
8503
|
<input type="hidden" name="csrf" value="${v.csrf}">
|
|
8486
8504
|
${fieldsFor(type, "new").map((f) => field(f, values))}
|
|
8487
8505
|
<div><button class="rb-btn rb-btn--primary" type="submit">Create</button></div>
|
|
8488
|
-
</form
|
|
8506
|
+
</form>`;
|
|
8507
|
+
return framed(v, `New ${NOUN[type]}`, html`<section>
|
|
8508
|
+
<p><a class="rb-link" href="${v.base}/">My tasks</a></p>
|
|
8509
|
+
<h1>New ${NOUN[type]}</h1>
|
|
8510
|
+
${intro}
|
|
8511
|
+
${form}
|
|
8489
8512
|
</section>`);
|
|
8490
8513
|
}
|
|
8491
8514
|
function editTaskPage(v, task, values, o) {
|
|
@@ -9233,6 +9256,21 @@ function reminderInput(form) {
|
|
|
9233
9256
|
const when = opt(form, "when");
|
|
9234
9257
|
return { text: str2(form, "text"), ...when !== undefined ? { when } : {}, repeat: str2(form, "repeat") || "none" };
|
|
9235
9258
|
}
|
|
9259
|
+
function lines(v) {
|
|
9260
|
+
return v?.replace(/\r\n?/g, `
|
|
9261
|
+
`);
|
|
9262
|
+
}
|
|
9263
|
+
function researchInput(form) {
|
|
9264
|
+
const context = lines(opt(form, "context"));
|
|
9265
|
+
const deadline = opt(form, "deadline");
|
|
9266
|
+
const at = opt(form, "at");
|
|
9267
|
+
return {
|
|
9268
|
+
question: lines(str2(form, "question")) ?? "",
|
|
9269
|
+
...context !== undefined ? { context } : {},
|
|
9270
|
+
...deadline !== undefined ? { deadline } : {},
|
|
9271
|
+
...at !== undefined ? { at } : {}
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9236
9274
|
function renewalInput(form) {
|
|
9237
9275
|
const every = int(form, "every");
|
|
9238
9276
|
const lead = int(form, "lead");
|
|
@@ -9336,6 +9374,8 @@ async function makeTask(w, type, form) {
|
|
|
9336
9374
|
return asUser(w, (u) => createReminder(w.d, u, reminderInput(form)));
|
|
9337
9375
|
if (type === "renewal")
|
|
9338
9376
|
return asUser(w, (u) => createRenewal(w.d, u, renewalInput(form)));
|
|
9377
|
+
if (type === "research")
|
|
9378
|
+
return asUser(w, (u) => createResearch(w.d, u, researchInput(form)));
|
|
9339
9379
|
const input = priceInput(form);
|
|
9340
9380
|
if (w.reading.has(w.userId))
|
|
9341
9381
|
return { ok: false, error: READING };
|
|
@@ -9369,9 +9409,12 @@ function actOn(w, taskId, action) {
|
|
|
9369
9409
|
var DEFAULTS = {
|
|
9370
9410
|
reminder: { repeat: "none" },
|
|
9371
9411
|
renewal: { unit: "year", every: "1", lead: "7" },
|
|
9372
|
-
price: { hours: "12", drop: "10", baseline: "last" }
|
|
9412
|
+
price: { hours: "12", drop: "10", baseline: "last" },
|
|
9413
|
+
research: {}
|
|
9373
9414
|
};
|
|
9374
9415
|
function newGet(e, type) {
|
|
9416
|
+
if (type === "research" && !e.d.research)
|
|
9417
|
+
return htmlResponse(newTaskPage(e.v, type, {}, undefined, RESEARCH_OFF));
|
|
9375
9418
|
return htmlResponse(newTaskPage(e.v, type, type === "renewal" ? { ...DEFAULTS.renewal, currency: "USD" } : DEFAULTS[type]));
|
|
9376
9419
|
}
|
|
9377
9420
|
async function newPost(e, type, form) {
|
|
@@ -9438,7 +9481,11 @@ function jsonKind(f) {
|
|
|
9438
9481
|
return "integer";
|
|
9439
9482
|
return f.decimal ? "number" : "string";
|
|
9440
9483
|
}
|
|
9441
|
-
function
|
|
9484
|
+
function offeredTypes(d) {
|
|
9485
|
+
return d.research ? NEW_TYPES : EDITOR_TYPES;
|
|
9486
|
+
}
|
|
9487
|
+
var NOT_EDITABLE = "A research request cannot be edited. Delete it and ask again.";
|
|
9488
|
+
function typesAnswer(d) {
|
|
9442
9489
|
const describe = (fields, mode) => fields.map((f) => ({
|
|
9443
9490
|
name: f.name,
|
|
9444
9491
|
type: jsonKind(f),
|
|
@@ -9451,7 +9498,14 @@ function typesAnswer() {
|
|
|
9451
9498
|
}));
|
|
9452
9499
|
return {
|
|
9453
9500
|
status: 200,
|
|
9454
|
-
body: {
|
|
9501
|
+
body: {
|
|
9502
|
+
types: offeredTypes(d).map((type) => ({
|
|
9503
|
+
type,
|
|
9504
|
+
editable: type !== "research",
|
|
9505
|
+
create: describe(fieldsFor(type, "new"), "new"),
|
|
9506
|
+
edit: describe(fieldsFor(type, "edit"), "edit")
|
|
9507
|
+
}))
|
|
9508
|
+
}
|
|
9455
9509
|
};
|
|
9456
9510
|
}
|
|
9457
9511
|
function asFields(body, type, mode) {
|
|
@@ -9481,8 +9535,12 @@ function refusal(error) {
|
|
|
9481
9535
|
return notFound();
|
|
9482
9536
|
if (error === NO_LONGER_LISTED)
|
|
9483
9537
|
return problem(401, "invalid_token", "The token's owner is no longer on this tracker's list.");
|
|
9484
|
-
if (error === READING)
|
|
9538
|
+
if (error === READING || error === TASK_BUSY)
|
|
9485
9539
|
return problem(409, "busy", error);
|
|
9540
|
+
if (error === NOT_EDITABLE)
|
|
9541
|
+
return problem(409, "conflict", error);
|
|
9542
|
+
if (error === RESEARCH_OFF)
|
|
9543
|
+
return problem(503, "unavailable", error);
|
|
9486
9544
|
if (error === FINISHED || /^That task is [a-z]+, not [a-z]+\.$/.test(error))
|
|
9487
9545
|
return problem(409, "conflict", error);
|
|
9488
9546
|
if (/^You already (have|track) [0-9]+ /.test(error))
|
|
@@ -9501,6 +9559,14 @@ async function taskJson(d, user, task) {
|
|
|
9501
9559
|
const type = EDITOR_TYPES.includes(task.type) ? task.type : null;
|
|
9502
9560
|
const values = editValues(d, user, task);
|
|
9503
9561
|
const settings = {};
|
|
9562
|
+
if (task.type === "research") {
|
|
9563
|
+
const c = task.config;
|
|
9564
|
+
settings.question = c.question;
|
|
9565
|
+
if (c.context !== undefined)
|
|
9566
|
+
settings.context = c.context;
|
|
9567
|
+
if (c.deadline !== undefined)
|
|
9568
|
+
settings.deadline = c.deadline;
|
|
9569
|
+
}
|
|
9504
9570
|
if (type) {
|
|
9505
9571
|
for (const f of fieldsFor(type, "edit")) {
|
|
9506
9572
|
if (f.name === "when")
|
|
@@ -9559,9 +9625,11 @@ async function written(w, user, result, status, base) {
|
|
|
9559
9625
|
}
|
|
9560
9626
|
async function createAnswer(w, user, body, base) {
|
|
9561
9627
|
const type = body.type;
|
|
9562
|
-
if (typeof type !== "string" || !
|
|
9563
|
-
return problem(400, "invalid", `\`type\` is one of ${
|
|
9628
|
+
if (typeof type !== "string" || !NEW_TYPES.includes(type)) {
|
|
9629
|
+
return problem(400, "invalid", `\`type\` is one of ${offeredTypes(w.d).join(", ")}.`);
|
|
9564
9630
|
}
|
|
9631
|
+
if (type === "research" && !w.d.research)
|
|
9632
|
+
return refusal(RESEARCH_OFF);
|
|
9565
9633
|
const form = asFields(body, type, "new");
|
|
9566
9634
|
if (!(form instanceof URLSearchParams))
|
|
9567
9635
|
return form;
|
|
@@ -9570,7 +9638,7 @@ async function createAnswer(w, user, body, base) {
|
|
|
9570
9638
|
async function editAnswer(w, user, id, body, base) {
|
|
9571
9639
|
const task = await editableTask(w.d, user, id);
|
|
9572
9640
|
if (!task)
|
|
9573
|
-
return notFound();
|
|
9641
|
+
return (await ownLiveTask(w.d, user, id))?.type === "research" ? refusal(NOT_EDITABLE) : notFound();
|
|
9574
9642
|
const form = asFields(body, task.type, "edit");
|
|
9575
9643
|
if (!(form instanceof URLSearchParams))
|
|
9576
9644
|
return form;
|
|
@@ -9756,7 +9824,7 @@ async function handleApi(w, request, path) {
|
|
|
9756
9824
|
if (method === "DELETE" && r.kind === "task")
|
|
9757
9825
|
return json2(await actAnswer(writer, user, r.id, "delete"));
|
|
9758
9826
|
if (r.kind === "types")
|
|
9759
|
-
return json2(typesAnswer());
|
|
9827
|
+
return json2(typesAnswer(w.d));
|
|
9760
9828
|
if (r.kind === "tasks")
|
|
9761
9829
|
return json2(await listAnswer(w.d, user));
|
|
9762
9830
|
if (r.kind === "task")
|
|
@@ -9896,7 +9964,7 @@ function route2(path) {
|
|
|
9896
9964
|
const created = NEW_PATH.exec(path);
|
|
9897
9965
|
if (created) {
|
|
9898
9966
|
const type = created[1];
|
|
9899
|
-
return
|
|
9967
|
+
return NEW_TYPES.includes(type) ? { kind: "new", type } : null;
|
|
9900
9968
|
}
|
|
9901
9969
|
const one = TASK_PATH.exec(path) ?? TASK_ACTION.exec(path);
|
|
9902
9970
|
if (!one)
|
|
@@ -10217,7 +10285,7 @@ function createWebHandler(w) {
|
|
|
10217
10285
|
const done = url.searchParams.get("done");
|
|
10218
10286
|
switch (r.kind) {
|
|
10219
10287
|
case "tasks":
|
|
10220
|
-
return htmlResponse(tasksPage(v, await loadTaskList(d, auth.user), now, notice(done)));
|
|
10288
|
+
return htmlResponse(tasksPage(v, await loadTaskList(d, auth.user), now, notice(done), d.research === true));
|
|
10221
10289
|
case "task":
|
|
10222
10290
|
return taskPage(e, r.id, done);
|
|
10223
10291
|
case "edit":
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/plugin-tracker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The task tracker for Rackbops Clerk: reminders, renewals, price drops and one-off research (through city-hall, with its findings) by slash command, delivered by DM with buttons, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /research, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks, an admin view (with the DMs that did not arrive), forget-me, and a JSON task API with personal API tokens",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "The task tracker for Rackbops Clerk: reminders, renewals, price drops and one-off research (through city-hall, with its findings) by slash command, delivered by DM with buttons, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /research, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks (research requests too, made but not changed), an admin view (with the DMs that did not arrive), forget-me, and a JSON task API with personal API tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|
|
7
7
|
"files": ["dist"],
|