@rackbops/plugin-tracker 0.13.0 → 0.14.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 +23 -4
- package/dist/plugin.js +136 -6
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ slice 3) -- the JSON task API with personal API tokens (#80, slice 4), for a pro
|
|
|
13
13
|
you, such as a later intake agent (plan E10) -- and the one-off research request (#82, plan E8,
|
|
14
14
|
category 5): a question looked into on the web by a model run, checked by a second run against its
|
|
15
15
|
sources, and DMed once, with its claims kept as findings -- and an admin raising one person's
|
|
16
|
-
daily model ceiling from the web (#82, plan 5.7)
|
|
16
|
+
daily model ceiling from the web (#82, plan 5.7), with an operator switch that turns the daily
|
|
17
|
+
ceilings off for the alpha and an admin page of daily model usage to evaluate it by. The model runs never happen here: they go
|
|
17
18
|
through city-hall to Rackbops/docket-runner on roshne's own host (plan 5.12), and the whole execute
|
|
18
19
|
lane stays off until the city-hall Executor is configured (Configuration below).
|
|
19
20
|
|
|
@@ -127,7 +128,11 @@ says research is not available; it does not list the waiting ones (`/tasks` does
|
|
|
127
128
|
calls in all. At a ceiling the person's research waits until midnight Eastern; the person gets one
|
|
128
129
|
DM and the admins one each. Reminders, renewals and prices never count. An admin can raise one
|
|
129
130
|
person's ceiling from their admin page (0.13.0, below); the global ceiling still applies to everyone
|
|
130
|
-
together.
|
|
131
|
+
together. **For the alpha the ceilings can be switched off** (0.14.0; roshne, 2026-10-02, choosing option
|
|
132
|
+
A: "A. but lets build in an unlimited budget "flag" during alpha, evaluate usage during alpha, then
|
|
133
|
+
test budgets during beta"): with `TRACKER_BUDGET_UNLIMITED` on (Configuration, below) no daily ceiling holds a run,
|
|
134
|
+
while each Job's own caps (15 turns and 1 USD for a research run) still bound it and every run is
|
|
135
|
+
still charged, so `/admin/usage` (below) shows what the defaults would have held.
|
|
131
136
|
|
|
132
137
|
**Findings.** A passed research answer's claims are stored with their first source (docket's
|
|
133
138
|
`findings`), shown on the task's page to its owner, accepted recipients and admins (docket's
|
|
@@ -284,6 +289,16 @@ the function the command path runs:
|
|
|
284
289
|
when) and a log line (`u1 set u2's daily ceiling to 4 USD / 40 calls (c1)`).
|
|
285
290
|
- **Remove from the tracker**: forget-me for that person (below), with the same confirmation.
|
|
286
291
|
|
|
292
|
+
**Usage** (0.14.0). `/admin/usage`, linked from every admin page, shows the model spend per budget
|
|
293
|
+
day (midnight Eastern) for the last 14 days, newest first: dollars and calls for everyone together,
|
|
294
|
+
then per person. Each row is measured against the default ceilings, not a person's raise -- a
|
|
295
|
+
"would have hit the 20 call limit" (or "2.00 USD") marker on a day that reached one, and how many
|
|
296
|
+
calls were charged after it had, roughly what that ceiling would have held (docket checks before a
|
|
297
|
+
run and charges at its end). It reads docket's `usage` table, which every run is charged to whether
|
|
298
|
+
budgets are on or off -- the way to evaluate the alpha with `TRACKER_BUDGET_UNLIMITED` on
|
|
299
|
+
(Configuration, below). Read-only, and like every admin page answers anyone who is not an admin
|
|
300
|
+
with the unknown page's 404.
|
|
301
|
+
|
|
287
302
|
**Deliveries** (0.10.0). `/admin/deliveries`, linked from every admin page, lists the DMs that
|
|
288
303
|
settled `failed`, `unconfirmed` or `deferred` in the last 30 days, newest first, at most 200 rows
|
|
289
304
|
(the page says how many matched when the bound cuts it). Each row: the task (its title, linking to
|
|
@@ -294,8 +309,10 @@ say). Before 0.10.0 these showed only in the bot log, where the warnings still g
|
|
|
294
309
|
read-only, and like every admin page answers anyone who is not an admin with the unknown page's 404.
|
|
295
310
|
|
|
296
311
|
Not here yet, though plan 5.10 lists them: grants and retry, and an admin pause of someone else's
|
|
297
|
-
task. Of budgets, raising one person's ceiling
|
|
298
|
-
docket's constants
|
|
312
|
+
task. Of budgets, raising one person's ceiling and the usage page are here; the global ceiling and
|
|
313
|
+
the defaults are docket's constants, and turning budgets off is the operator's
|
|
314
|
+
`TRACKER_BUDGET_UNLIMITED`, not a web control. While it is on, the ceiling section says the ceiling
|
|
315
|
+
is not enforced; a change is still recorded and applies once it is off.
|
|
299
316
|
|
|
300
317
|
**Forget me** (plan 5.8). A signed-in person erases themselves from `/forget` (web only: the plan
|
|
301
318
|
asks for no Discord command, so without `TRACKER_WEB_URL` the only path is an admin with the web
|
|
@@ -499,6 +516,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
499
516
|
| 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. |
|
|
500
517
|
| 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`) 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` and `research`. |
|
|
501
518
|
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts` | 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 rules. |
|
|
519
|
+
| 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. |
|
|
502
520
|
| 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. |
|
|
503
521
|
| 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. |
|
|
504
522
|
| 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. |
|
|
@@ -519,6 +537,7 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
519
537
|
| `TRACKER_ADMIN_DISCORD_IDS` | no | Comma-separated Discord user ids (spaces around commas allowed; an empty entry, as from a trailing comma, is refused and the plugin does not load) made admin at start. Unset = none. Removing an id does not revoke it. |
|
|
520
538
|
| `TRACKER_GUILD_ID` | no | The Discord server whose members may use the tracker, or a comma-separated list of them (spaces around commas allowed; a repeated id counts once): a member of any listed server passes. Checked through the interaction's client with a single-member lookup per server (no privileged intent), all asked at once; a yes from any server is a member, a no from every server is not, and otherwise the answer is unknown (refused, never revoked). Inside a listed server, the person running a command needs no lookup. Unset = no membership gate, and a warning is logged each time the plugin activates; a malformed or empty entry anywhere in the list refuses to load, naming it. One store and one admission list serve every listed server; there are no per-server admins. |
|
|
521
539
|
| `TRACKER_WEB_URL` | no | The https origin the bot's HTTP is reached at through its tunnel, e.g. `https://clerk.example.com` (no path). `/web` links and the allowed `Origin` come from it, never from a request's `Host`. Unset = no web area (`/web` says so, the pages answer 404); anything but a bare https origin refuses to load. |
|
|
540
|
+
| `TRACKER_BUDGET_UNLIMITED` | no | Budgets off for the alpha (0.14.0): `true` or `1` means no daily ceiling -- a person's or the global one -- holds a model run, bot-wide. Each Job's own caps still apply, usage is still recorded (see `/admin/usage`), the bot logs one line at start saying so (or, with the execute lane off, that no model work runs), the People and Usage admin pages show a "Budgets off (alpha): unlimited" banner, and the person page's ceiling section shows a "not enforced" note. Unset, empty, `false` or `0` = the ceilings apply (the default); anything else refuses to load. |
|
|
522
541
|
| `TRACKER_CITY_HALL_URL` | no | The https origin of the city-hall that queues the tracker's model Jobs (no path). |
|
|
523
542
|
| `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. |
|
|
524
543
|
| `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. |
|
package/dist/plugin.js
CHANGED
|
@@ -3331,6 +3331,7 @@ function isRaised(latest) {
|
|
|
3331
3331
|
function budgetNoticeKey(userId, at) {
|
|
3332
3332
|
return `budget:person:${userId}:${budgetDay(at).day}`;
|
|
3333
3333
|
}
|
|
3334
|
+
var NOT_ENFORCED = "Budgets are off (alpha): TRACKER_BUDGET_UNLIMITED is on, so no ceiling is enforced; this one applies once it is turned off.";
|
|
3334
3335
|
var HISTORY_SHOWN = 20;
|
|
3335
3336
|
function toChange(r) {
|
|
3336
3337
|
return { id: `c${r.seq}`, userId: r.user_id, usd: r.usd, calls: r.calls, setBy: r.set_by, at: r.at };
|
|
@@ -3385,8 +3386,9 @@ async function setCeiling(d, admin, targetId, input) {
|
|
|
3385
3386
|
return { ok: true, text: `${who2(target)} is already on the default ceiling.` };
|
|
3386
3387
|
const change = d.ceilings.record(target.id, decided.limits, admin.id, d.clock.now().toISOString());
|
|
3387
3388
|
const now = limitsOf(change);
|
|
3389
|
+
const note = d.budgetUnlimited ? ` ${NOT_ENFORCED}` : "";
|
|
3388
3390
|
d.log.info(`${admin.id} set ${target.id}'s daily ceiling to ${decided.limits === null ? "the default" : `${now.usd} USD / ${now.calls} calls`} (${change.id})`);
|
|
3389
|
-
return decided.limits === null ? { ok: true, text: `${who2(target)} is back on the default ceiling: ${describeLimits(now)}
|
|
3391
|
+
return decided.limits === null ? { ok: true, text: `${who2(target)} is back on the default ceiling: ${describeLimits(now)}.${note}` } : { ok: true, text: `${who2(target)}'s ceiling is now ${describeLimits(now)}, until an admin changes it.${note}` };
|
|
3390
3392
|
}
|
|
3391
3393
|
|
|
3392
3394
|
// plugins/tracker/src/delivery-health.ts
|
|
@@ -8869,7 +8871,8 @@ function ceilingSection(v, personId, view, now) {
|
|
|
8869
8871
|
const { min, max } = view.bounds;
|
|
8870
8872
|
return html`<section>
|
|
8871
8873
|
<h2>Daily model budget</h2>
|
|
8872
|
-
|
|
8874
|
+
${view.unlimited ? html`<div class="rb-alert rb-alert--warning" role="status"><p>${NOT_ENFORCED}</p></div>` : null}
|
|
8875
|
+
<p>Today: ${view.today.usd.toFixed(2)} USD and ${view.today.calls} model call(s). Ceiling: ${describeLimits(limits)}${raised ? html` <span class="rb-badge">raised</span>` : " (the default)"}${view.unlimited ? " -- not enforced" : null}.</p>
|
|
8873
8876
|
<p class="rb-muted">A raise stands until an admin changes it. Setting exactly the default puts them back on it. It may not go below the default (${describeLimits(view.defaults)}) or above the global ceiling (${describeLimits(max)}), which still applies to everyone together. A day ends at midnight Eastern.</p>
|
|
8874
8877
|
<form method="post" action="${action}" class="tr-stack">
|
|
8875
8878
|
${csrf}
|
|
@@ -8887,6 +8890,74 @@ ${view.history.length === 0 ? html`<p class="rb-muted">No admin has changed thei
|
|
|
8887
8890
|
</section>`;
|
|
8888
8891
|
}
|
|
8889
8892
|
|
|
8893
|
+
// plugins/tracker/src/usage.ts
|
|
8894
|
+
var BUDGET_UNLIMITED_KEY = "TRACKER_BUDGET_UNLIMITED";
|
|
8895
|
+
function parseBudgetUnlimited(raw) {
|
|
8896
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
8897
|
+
if (value === "" || value === "false" || value === "0")
|
|
8898
|
+
return false;
|
|
8899
|
+
if (value === "true" || value === "1")
|
|
8900
|
+
return true;
|
|
8901
|
+
throw new Error(`${BUDGET_UNLIMITED_KEY} must be true, 1, false or 0 (or unset)`);
|
|
8902
|
+
}
|
|
8903
|
+
var NO_LIMITS = Object.freeze({ usd: null, calls: null });
|
|
8904
|
+
var UNLIMITED_BUDGET = Object.freeze({ person: NO_LIMITS, global: NO_LIMITS });
|
|
8905
|
+
function executeBudget(ceilings, unlimited) {
|
|
8906
|
+
return unlimited ? UNLIMITED_BUDGET : budgetPolicy(ceilings);
|
|
8907
|
+
}
|
|
8908
|
+
var UNLIMITED_LOG = `budgets off (alpha): ${BUDGET_UNLIMITED_KEY} is on, so no daily ceiling holds a model run; each Job's own caps still apply and usage is still recorded`;
|
|
8909
|
+
var UNLIMITED_IDLE_LOG = `${BUDGET_UNLIMITED_KEY} is on, but the execute lane is off, so no model work runs for it to leave unbudgeted`;
|
|
8910
|
+
var UNLIMITED_BANNER = "Budgets off (alpha): unlimited. No daily ceiling holds a model run while TRACKER_BUDGET_UNLIMITED is on; usage is still recorded.";
|
|
8911
|
+
var USAGE_DAYS = 14;
|
|
8912
|
+
function usageWindow(now, days = USAGE_DAYS) {
|
|
8913
|
+
const out = [budgetDay(now)];
|
|
8914
|
+
while (out.length < days)
|
|
8915
|
+
out.unshift(budgetDay(new Date(Date.parse(out[0].start) - 1)));
|
|
8916
|
+
return out;
|
|
8917
|
+
}
|
|
8918
|
+
function measure(rows, limits) {
|
|
8919
|
+
const total = { usd: 0, calls: 0 };
|
|
8920
|
+
let past = 0;
|
|
8921
|
+
for (const r of rows) {
|
|
8922
|
+
if (reached(total, limits) !== null)
|
|
8923
|
+
past += r.calls;
|
|
8924
|
+
total.usd += r.costUsd;
|
|
8925
|
+
total.calls += r.calls;
|
|
8926
|
+
}
|
|
8927
|
+
return { ...total, reached: reached(total, limits), past };
|
|
8928
|
+
}
|
|
8929
|
+
function byAt(a, b) {
|
|
8930
|
+
return a.at < b.at ? -1 : a.at > b.at ? 1 : 0;
|
|
8931
|
+
}
|
|
8932
|
+
function usageReport(rows, now, days = USAGE_DAYS, policy = DEFAULT_BUDGET) {
|
|
8933
|
+
const window = usageWindow(now, days);
|
|
8934
|
+
const index = new Map(window.map((w, i) => [w.day, i]));
|
|
8935
|
+
const buckets = window.map(() => []);
|
|
8936
|
+
for (const r of [...rows].sort(byAt)) {
|
|
8937
|
+
const i = index.get(budgetDay(new Date(r.at)).day);
|
|
8938
|
+
if (i !== undefined)
|
|
8939
|
+
buckets[i]?.push(r);
|
|
8940
|
+
}
|
|
8941
|
+
const out = window.map((w, i) => {
|
|
8942
|
+
const dayRows = buckets[i] ?? [];
|
|
8943
|
+
const users = [...new Set(dayRows.map((r) => r.userId))];
|
|
8944
|
+
const people = users.map((userId) => ({ userId, ...measure(dayRows.filter((r) => r.userId === userId), policy.person) })).sort((a, b) => b.usd - a.usd || b.calls - a.calls || (a.userId < b.userId ? -1 : 1));
|
|
8945
|
+
return { day: w.day, everyone: measure(dayRows, policy.global), people };
|
|
8946
|
+
});
|
|
8947
|
+
return {
|
|
8948
|
+
days: out.reverse(),
|
|
8949
|
+
person: policy.person,
|
|
8950
|
+
global: policy.global,
|
|
8951
|
+
since: window[0].start,
|
|
8952
|
+
before: window.at(-1).end
|
|
8953
|
+
};
|
|
8954
|
+
}
|
|
8955
|
+
async function loadUsage(store, now, days = USAGE_DAYS) {
|
|
8956
|
+
const window = usageWindow(now, days);
|
|
8957
|
+
const rows = await store.listUsage({ since: window[0].start, before: window.at(-1).end });
|
|
8958
|
+
return usageReport(rows, now, days);
|
|
8959
|
+
}
|
|
8960
|
+
|
|
8890
8961
|
// plugins/tracker/src/web/admin-pages.ts
|
|
8891
8962
|
var CONFIGURED_NOTE = "Named in TRACKER_ADMIN_DISCORD_IDS: made an admin again at every start, so they cannot be revoked or removed here. Remove them from the configuration first.";
|
|
8892
8963
|
var CONFIRM_WORD = "forget";
|
|
@@ -8908,7 +8979,10 @@ function hidden(v) {
|
|
|
8908
8979
|
return html`<input type="hidden" name="csrf" value="${v.csrf}">`;
|
|
8909
8980
|
}
|
|
8910
8981
|
function adminNav(v) {
|
|
8911
|
-
return html`<p class="tr-row"><a class="rb-link" href="${v.base}/admin">People</a> <a class="rb-link" href="${v.base}/admin/tasks">All tasks</a> <a class="rb-link" href="${v.base}/admin/deliveries">Deliveries</a></p>`;
|
|
8982
|
+
return html`<p class="tr-row"><a class="rb-link" href="${v.base}/admin">People</a> <a class="rb-link" href="${v.base}/admin/tasks">All tasks</a> <a class="rb-link" href="${v.base}/admin/deliveries">Deliveries</a> <a class="rb-link" href="${v.base}/admin/usage">Usage</a></p>`;
|
|
8983
|
+
}
|
|
8984
|
+
function budgetsOffBanner(unlimited) {
|
|
8985
|
+
return unlimited ? html`<div class="rb-alert rb-alert--warning" role="status"><p><strong>${UNLIMITED_BANNER}</strong></p></div>` : null;
|
|
8912
8986
|
}
|
|
8913
8987
|
function personHref(v, id, action = "") {
|
|
8914
8988
|
return `${v.base}/admin/people/${encodeURIComponent(id)}${action ? `/${action}` : ""}`;
|
|
@@ -8943,6 +9017,7 @@ function adminPage(v, data, o = {}) {
|
|
|
8943
9017
|
return framed(v, "Admin", html`<section>
|
|
8944
9018
|
<h1>People</h1>
|
|
8945
9019
|
${adminNav(v)}
|
|
9020
|
+
${budgetsOffBanner(o.unlimited === true)}
|
|
8946
9021
|
${flash(o.result ?? null)}
|
|
8947
9022
|
<div class="rb-table-scroll"><table class="rb-table">
|
|
8948
9023
|
<thead><tr><th scope="col">Person</th><th scope="col">Status</th><th scope="col">Zone, hour</th><th scope="col">Delivery</th><th scope="col">Tasks</th></tr></thead>
|
|
@@ -9078,6 +9153,48 @@ ${hidden(v)}
|
|
|
9078
9153
|
</section>`);
|
|
9079
9154
|
}
|
|
9080
9155
|
|
|
9156
|
+
// plugins/tracker/src/web/usage-pages.ts
|
|
9157
|
+
function marker(m, limits) {
|
|
9158
|
+
if (m.reached === null)
|
|
9159
|
+
return html`<span class="rb-muted">under</span>`;
|
|
9160
|
+
const which = m.reached === "calls" ? `${limits.calls} call` : `${(limits.usd ?? 0).toFixed(2)} USD`;
|
|
9161
|
+
const past = m.past > 0 ? `; ${m.past} call(s) after it` : "";
|
|
9162
|
+
return html`<span class="rb-badge">would have hit the ${which} limit</span>${past}`;
|
|
9163
|
+
}
|
|
9164
|
+
function row(day, who, m, limits) {
|
|
9165
|
+
return html`<tr>
|
|
9166
|
+
<td>${day ?? ""}</td>
|
|
9167
|
+
<td>${who}</td>
|
|
9168
|
+
<td>${m.usd.toFixed(2)}</td>
|
|
9169
|
+
<td>${m.calls}</td>
|
|
9170
|
+
<td>${marker(m, limits)}</td>
|
|
9171
|
+
</tr>`;
|
|
9172
|
+
}
|
|
9173
|
+
function adminUsagePage(v, data) {
|
|
9174
|
+
const { report, names } = data;
|
|
9175
|
+
const rows = report.days.map((d) => {
|
|
9176
|
+
if (d.everyone.calls === 0 && d.everyone.usd === 0) {
|
|
9177
|
+
return html`<tr><td>${d.day}</td><td colspan="4" class="rb-muted">No model use.</td></tr>`;
|
|
9178
|
+
}
|
|
9179
|
+
return [
|
|
9180
|
+
row(d.day, html`<strong>Everyone</strong>`, d.everyone, report.global),
|
|
9181
|
+
...d.people.map((p) => row(null, html`<a class="rb-link" href="${personHref(v, p.userId)}">${names.get(p.userId) ?? p.userId}</a>`, p, report.person))
|
|
9182
|
+
];
|
|
9183
|
+
});
|
|
9184
|
+
const total = report.days.reduce((s, d) => ({ usd: s.usd + d.everyone.usd, calls: s.calls + d.everyone.calls }), { usd: 0, calls: 0 });
|
|
9185
|
+
return framed(v, "Usage", html`<section>
|
|
9186
|
+
<h1>Model usage</h1>
|
|
9187
|
+
${adminNav(v)}
|
|
9188
|
+
${budgetsOffBanner(data.unlimited)}
|
|
9189
|
+
<p>The last ${report.days.length} days, newest first: ${total.usd.toFixed(2)} USD and ${total.calls} model call(s) in all. A day ends at midnight Eastern. Dollars are the CLI's list-price estimate, not a bill.</p>
|
|
9190
|
+
<p class="rb-muted">Each row is measured against the default ceilings -- ${describeLimits(report.person)} a person, ${describeLimits(report.global)} for everyone together -- not a person's raised one. "Would have hit" means that day's spend reached the ceiling; "calls after it" are the calls charged once it had, roughly what it would have held (the ceiling is checked before a run and the run charged at its end).${data.unlimited ? " While budgets are off, nothing is held." : ""}</p>
|
|
9191
|
+
<div class="rb-table-scroll"><table class="rb-table">
|
|
9192
|
+
<thead><tr><th scope="col">Day</th><th scope="col">Who</th><th scope="col">USD</th><th scope="col">Calls</th><th scope="col">Default ceiling</th></tr></thead>
|
|
9193
|
+
<tbody>${rows}</tbody>
|
|
9194
|
+
</table></div>
|
|
9195
|
+
</section>`);
|
|
9196
|
+
}
|
|
9197
|
+
|
|
9081
9198
|
// plugins/tracker/src/web/admin.ts
|
|
9082
9199
|
function unknownPage(base) {
|
|
9083
9200
|
return htmlResponse(notFoundPage(base), 404);
|
|
@@ -9101,7 +9218,7 @@ function confirmed(form) {
|
|
|
9101
9218
|
var WRONG_WORD = `Type ${CONFIRM_WORD} to confirm. Nothing was deleted.`;
|
|
9102
9219
|
async function peoplePage(a, result, status, allow) {
|
|
9103
9220
|
const data = { people: a.d.roster.people(), blocks: a.d.roster.activeBlocks(a.d.clock.now()), now: a.d.clock.now() };
|
|
9104
|
-
return htmlResponse(adminPage(a.v, data, { result, ...allow !== undefined ? { allow } : {} }), status);
|
|
9221
|
+
return htmlResponse(adminPage(a.v, data, { result, unlimited: a.d.budgetUnlimited === true, ...allow !== undefined ? { allow } : {} }), status);
|
|
9105
9222
|
}
|
|
9106
9223
|
async function onePerson(a, id, result = null) {
|
|
9107
9224
|
const everyone = a.d.roster.people();
|
|
@@ -9117,7 +9234,8 @@ async function onePerson(a, id, result = null) {
|
|
|
9117
9234
|
today: await spentToday(a.d.store, p.id, now),
|
|
9118
9235
|
defaults: DEFAULT_BUDGET.person,
|
|
9119
9236
|
bounds: boundsOf(),
|
|
9120
|
-
names: new Map(everyone.map((x) => [x.id, nameOf(x)]))
|
|
9237
|
+
names: new Map(everyone.map((x) => [x.id, nameOf(x)])),
|
|
9238
|
+
unlimited: a.d.budgetUnlimited === true
|
|
9121
9239
|
};
|
|
9122
9240
|
return htmlResponse(personPage(a.v, p, tasks, now, result, isConfiguredAdmin(a.d, p), tokens, budget), result && !result.ok ? 400 : 200);
|
|
9123
9241
|
}
|
|
@@ -9126,6 +9244,11 @@ async function adminGet(a, r) {
|
|
|
9126
9244
|
return peoplePage(a, null, 200);
|
|
9127
9245
|
if (r.kind === "admin-person")
|
|
9128
9246
|
return onePerson(a, r.id);
|
|
9247
|
+
if (r.kind === "admin-usage") {
|
|
9248
|
+
const now = a.d.clock.now();
|
|
9249
|
+
const people = new Map(a.d.roster.people().map((p) => [p.id, nameOf(p)]));
|
|
9250
|
+
return htmlResponse(adminUsagePage(a.v, { report: await loadUsage(a.d.store, now), names: people, unlimited: a.d.budgetUnlimited === true }));
|
|
9251
|
+
}
|
|
9129
9252
|
const people = new Map(a.d.roster.people().map((p) => [p.id, nameOf(p)]));
|
|
9130
9253
|
if (r.kind === "admin-deliveries") {
|
|
9131
9254
|
const now = a.d.clock.now();
|
|
@@ -10131,6 +10254,8 @@ function route2(path) {
|
|
|
10131
10254
|
return { kind: "admin-tasks" };
|
|
10132
10255
|
if (path === "/admin/deliveries")
|
|
10133
10256
|
return { kind: "admin-deliveries" };
|
|
10257
|
+
if (path === "/admin/usage")
|
|
10258
|
+
return { kind: "admin-usage" };
|
|
10134
10259
|
if (path === "/admin/allow")
|
|
10135
10260
|
return { kind: "admin-allow" };
|
|
10136
10261
|
const ceiling = PERSON_CEILING.exec(path);
|
|
@@ -10484,6 +10609,7 @@ function createWebHandler(w) {
|
|
|
10484
10609
|
case "admin":
|
|
10485
10610
|
case "admin-tasks":
|
|
10486
10611
|
case "admin-deliveries":
|
|
10612
|
+
case "admin-usage":
|
|
10487
10613
|
case "admin-person":
|
|
10488
10614
|
return adminGet(a, r);
|
|
10489
10615
|
default:
|
|
@@ -10534,6 +10660,7 @@ function createPlugin(host, options = {}) {
|
|
|
10534
10660
|
const guildIds = parseGuildIds(host.env.TRACKER_GUILD_ID);
|
|
10535
10661
|
const webOrigin = parseWebUrl(host.env.TRACKER_WEB_URL);
|
|
10536
10662
|
const cityHall = parseCityHallConfig(host.env);
|
|
10663
|
+
const budgetUnlimited = parseBudgetUnlimited(host.env[BUDGET_UNLIMITED_KEY]);
|
|
10537
10664
|
const cityHallConfig = cityHall.config;
|
|
10538
10665
|
const executeOn = cityHallConfig !== null || options.executor !== undefined;
|
|
10539
10666
|
const clock = options.clock ?? { now: () => new Date };
|
|
@@ -10740,7 +10867,9 @@ function createPlugin(host, options = {}) {
|
|
|
10740
10867
|
});
|
|
10741
10868
|
}
|
|
10742
10869
|
const ceilings = new Ceilings(opened);
|
|
10743
|
-
executeLane = executor ? new ExecuteLane({ store: openedStore, clock, types, notifier, executor, locks, budget:
|
|
10870
|
+
executeLane = executor ? new ExecuteLane({ store: openedStore, clock, types, notifier, executor, locks, budget: executeBudget(ceilings, budgetUnlimited) }) : null;
|
|
10871
|
+
if (budgetUnlimited)
|
|
10872
|
+
host.log.info(executeLane ? UNLIMITED_LOG : UNLIMITED_IDLE_LOG);
|
|
10744
10873
|
if (cityHallConfig)
|
|
10745
10874
|
host.log.info(`execute lane on: city-hall ${cityHallConfig.url}, capability ${cityHallConfig.capability}`);
|
|
10746
10875
|
else if ("missing" in cityHall && cityHall.missing.length > 0)
|
|
@@ -10761,6 +10890,7 @@ function createPlugin(host, options = {}) {
|
|
|
10761
10890
|
apiTokens: new ApiTokens(opened),
|
|
10762
10891
|
roster: new Roster(opened),
|
|
10763
10892
|
ceilings,
|
|
10893
|
+
budgetUnlimited,
|
|
10764
10894
|
configuredAdmins: new Set(adminIds),
|
|
10765
10895
|
lanes,
|
|
10766
10896
|
webEditor: webOrigin !== null,
|
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 (research requests too, made but not changed), an admin view (with the DMs that did not arrive,
|
|
3
|
+
"version": "0.14.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, raising a person's daily model ceiling, and daily model usage per person), an operator switch that turns the daily budgets off for the alpha, forget-me, and a JSON task API with personal API tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|
|
7
7
|
"files": ["dist"],
|
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
"secret": false,
|
|
45
45
|
"description": "The https origin the bot's HTTP is reached at through its tunnel, e.g. https://clerk.example.com (no path). /web links to <this>/tracker/login, and a form post from any other origin is refused. Unset = no web area: /web says so and its pages answer 404."
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
"key": "TRACKER_BUDGET_UNLIMITED",
|
|
49
|
+
"format": "^ *([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee]|1|0)? *$",
|
|
50
|
+
"required": false,
|
|
51
|
+
"secret": false,
|
|
52
|
+
"description": "Budgets off for the alpha: true or 1 means no daily ceiling (per person or for everyone) holds a model run, bot-wide. Each Job's own caps still apply, usage is still recorded, the People and Usage admin pages show a budgets-off banner, and the person page shows a not-enforced note. Unset, empty, false or 0 = the daily ceilings apply (the default); anything else refuses to load."
|
|
53
|
+
},
|
|
47
54
|
{
|
|
48
55
|
"key": "TRACKER_CITY_HALL_URL",
|
|
49
56
|
"format": "^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?/?$",
|