@rackbops/plugin-tracker 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +72 -7
  2. package/dist/plugin.js +1950 -106
  3. package/package.json +12 -4
package/dist/plugin.js CHANGED
@@ -2063,6 +2063,40 @@ function resumedNotice(r) {
2063
2063
  return `I could not DM you ${PAUSE_AFTER} times in a row, so your reminders were paused. ${what} ` + "Check that you accept DMs from members of a server we share, or they will pause again.";
2064
2064
  }
2065
2065
 
2066
+ // plugins/tracker/src/discord-common.ts
2067
+ import { MessageFlags } from "discord.js";
2068
+ var STARTING = "The tracker is starting up; try again in a minute.";
2069
+ var FAILED = "Something went wrong on my side; try again in a minute.";
2070
+ var EPHEMERAL = { flags: MessageFlags.Ephemeral };
2071
+ var NO_MENTIONS = { allowedMentions: { parse: [] } };
2072
+ var UNKNOWN_MEMBER = 10007;
2073
+ var UNKNOWN_USER = 10013;
2074
+ async function lookupMembership(interaction, guildId, discordId, log) {
2075
+ if (guildId === null)
2076
+ return "not-checked";
2077
+ if (interaction.guildId === guildId && interaction.user.id === discordId)
2078
+ return "member";
2079
+ try {
2080
+ const guild = await interaction.client.guilds.fetch(guildId);
2081
+ await guild.members.fetch({ user: discordId, force: true });
2082
+ return "member";
2083
+ } catch (err) {
2084
+ const code = err.code;
2085
+ if (code === UNKNOWN_MEMBER || code === UNKNOWN_USER)
2086
+ return "not-member";
2087
+ log.warn(`could not check membership of ${guildId}: ${err instanceof Error ? err.message : String(err)}`);
2088
+ return "unknown";
2089
+ }
2090
+ }
2091
+ function serial() {
2092
+ let chain = Promise.resolve();
2093
+ return (fn) => {
2094
+ const next = chain.then(fn, fn);
2095
+ chain = next.catch(() => {});
2096
+ return next;
2097
+ };
2098
+ }
2099
+
2066
2100
  // plugins/tracker/src/people.ts
2067
2101
  var DEFAULT_TIME_ZONE = "America/New_York";
2068
2102
  var DEFAULT_PREFERRED_HOUR = 9;
@@ -2190,6 +2224,20 @@ async function registerPerson(d, user, input) {
2190
2224
  await rescheduleOwned(d, updated, zoneChanged);
2191
2225
  return registeredText(updated, first);
2192
2226
  }
2227
+ async function saveSettings(d, user, input) {
2228
+ let updated;
2229
+ try {
2230
+ updated = await setPreferences(d.store, user.id, { preferredHour: input.hour, timeZone: input.zone });
2231
+ } catch (err) {
2232
+ if (err instanceof PeopleError)
2233
+ return err.message;
2234
+ throw err;
2235
+ }
2236
+ const zoneChanged = updated.timeZone !== user.timeZone;
2237
+ if (zoneChanged || updated.preferredHour !== user.preferredHour)
2238
+ await rescheduleOwned(d, updated, zoneChanged);
2239
+ return null;
2240
+ }
2193
2241
  async function setHour(d, user, hour) {
2194
2242
  if (hour === user.preferredHour)
2195
2243
  return `Your preferred hour is already ${String(hour).padStart(2, "0")}:00, ${user.timeZone} time.`;
@@ -2269,19 +2317,25 @@ async function remind(d, user, input) {
2269
2317
  return clip(`Reminder \`${task.id}\` set: ${text}
2270
2318
  Next: ${at}${cadence}.`);
2271
2319
  }
2272
- async function listTasks(d, user) {
2273
- const now = d.clock.now();
2274
- const lines = [formatTaskList(await taskList(d.store, { userId: user.id, admin: user.admin }), user, now)];
2320
+ async function loadTaskList(d, user) {
2321
+ const entries = await taskList(d.store, { userId: user.id, admin: user.admin });
2322
+ const paused = [];
2275
2323
  for (const task of await d.store.listTasks({ ownerId: user.id, status: "paused" })) {
2276
- const held = d.health.pausesFor(task.id).filter((u) => u !== user.id);
2277
- if (held.length === 0) {
2324
+ const held = [];
2325
+ for (const id of d.health.pausesFor(task.id).filter((u) => u !== user.id))
2326
+ held.push((await d.store.getUser(id))?.displayName ?? id);
2327
+ paused.push({ task, held });
2328
+ }
2329
+ return { entries, paused };
2330
+ }
2331
+ async function listTasks(d, user) {
2332
+ const { entries, paused } = await loadTaskList(d, user);
2333
+ const lines = [formatTaskList(entries, user, d.clock.now())];
2334
+ for (const { task, held } of paused) {
2335
+ if (held.length === 0)
2278
2336
  lines.push(`Paused: \`${task.id}\` ${task.title}`);
2279
- continue;
2280
- }
2281
- const who = [];
2282
- for (const id of held)
2283
- who.push((await d.store.getUser(id))?.displayName ?? id);
2284
- lines.push(`Paused: \`${task.id}\` ${task.title} -- I could not DM ${who.join(", ")}; \`/task resume ${task.id}\` goes on without them.`);
2337
+ else
2338
+ lines.push(`Paused: \`${task.id}\` ${task.title} -- I could not DM ${held.join(", ")}; \`/task resume ${task.id}\` goes on without them.`);
2285
2339
  }
2286
2340
  return clip(lines.join(`
2287
2341
  `));
@@ -2338,40 +2392,6 @@ async function resumeTask(d, user, taskId) {
2338
2392
  return `Resumed \`${task.id}\`${without}.`;
2339
2393
  }
2340
2394
 
2341
- // plugins/tracker/src/discord-common.ts
2342
- import { MessageFlags } from "discord.js";
2343
- var STARTING = "The tracker is starting up; try again in a minute.";
2344
- var FAILED = "Something went wrong on my side; try again in a minute.";
2345
- var EPHEMERAL = { flags: MessageFlags.Ephemeral };
2346
- var NO_MENTIONS = { allowedMentions: { parse: [] } };
2347
- var UNKNOWN_MEMBER = 10007;
2348
- var UNKNOWN_USER = 10013;
2349
- async function lookupMembership(interaction, guildId, discordId, log) {
2350
- if (guildId === null)
2351
- return "not-checked";
2352
- if (interaction.guildId === guildId && interaction.user.id === discordId)
2353
- return "member";
2354
- try {
2355
- const guild = await interaction.client.guilds.fetch(guildId);
2356
- await guild.members.fetch({ user: discordId });
2357
- return "member";
2358
- } catch (err) {
2359
- const code = err.code;
2360
- if (code === UNKNOWN_MEMBER || code === UNKNOWN_USER)
2361
- return "not-member";
2362
- log.warn(`could not check membership of ${guildId}: ${err instanceof Error ? err.message : String(err)}`);
2363
- return "unknown";
2364
- }
2365
- }
2366
- function serial() {
2367
- let chain = Promise.resolve();
2368
- return (fn) => {
2369
- const next = chain.then(fn, fn);
2370
- chain = next.catch(() => {});
2371
- return next;
2372
- };
2373
- }
2374
-
2375
2395
  // plugins/tracker/src/history.ts
2376
2396
  var HISTORY_RUNS = 10;
2377
2397
  var HISTORY_CHANGES = 5;
@@ -2384,23 +2404,36 @@ function replyText(r) {
2384
2404
  return `chose ${String(r.payload?.choice ?? "?")}`;
2385
2405
  return r.kind === "opt_out" ? "opted out" : r.kind;
2386
2406
  }
2387
- function formatRun(o, replies, people, viewer, now) {
2388
- const late = o.late ? " (late)" : "";
2389
- const error = o.status === "failed" && o.error ? `: ${clip(o.error, 120)}` : "";
2390
- const answers = replies.filter((r) => r.occurrenceId === o.id).map((r) => `${people.get(r.userId)?.displayName ?? r.userId} ${replyText(r)}`);
2391
- const answered = answers.length > 0 ? `; ${answers.join(", ")}` : "";
2392
- return `- ${formatInstant(o.dueAt, viewer.timeZone, now)} -- ${o.status}${late}${error}${answered}`;
2407
+ function runView(o, replies, people, viewer, now) {
2408
+ return {
2409
+ due: formatInstant(o.dueAt, viewer.timeZone, now),
2410
+ status: o.status,
2411
+ late: o.late,
2412
+ error: o.status === "failed" && o.error ? clip(o.error, 120) : null,
2413
+ answers: replies.filter((r) => r.occurrenceId === o.id).map((r) => `${people.get(r.userId)?.displayName ?? r.userId} ${replyText(r)}`)
2414
+ };
2415
+ }
2416
+ function changeView(e, viewer, now) {
2417
+ return {
2418
+ at: formatInstant(e.at, viewer.timeZone, now),
2419
+ kind: e.kind.replaceAll("_", " "),
2420
+ detail: e.kind === "paused" || e.kind === "resumed" ? e.detail : null
2421
+ };
2393
2422
  }
2394
- function formatChange(e, viewer, now) {
2395
- const detail = e.kind === "paused" || e.kind === "resumed" ? `: ${e.detail}` : "";
2396
- return `- ${formatInstant(e.at, viewer.timeZone, now)} ${e.kind.replaceAll("_", " ")}${detail}`;
2423
+ function runLine(v) {
2424
+ const late = v.late ? " (late)" : "";
2425
+ const error = v.error ? `: ${v.error}` : "";
2426
+ const answered = v.answers.length > 0 ? `; ${v.answers.join(", ")}` : "";
2427
+ return `- ${v.due} -- ${v.status}${late}${error}${answered}`;
2397
2428
  }
2398
- async function taskHistory(d, user, taskId) {
2429
+ function changeLine(v) {
2430
+ return `- ${v.at} ${v.kind}${v.detail !== null ? `: ${v.detail}` : ""}`;
2431
+ }
2432
+ async function loadHistory(d, user, taskId) {
2399
2433
  const actor = { userId: user.id, admin: user.admin };
2400
- const id = taskId.trim();
2401
- const task = await visibleTask(d.store, actor, id);
2434
+ const task = await visibleTask(d.store, actor, taskId.trim());
2402
2435
  if (!task)
2403
- return NO_SUCH_TASK;
2436
+ return null;
2404
2437
  const now = d.clock.now();
2405
2438
  const all = await visibleOccurrences(d.store, actor, task.id) ?? [];
2406
2439
  const upcoming = (o) => o.status === "queued" && Date.parse(o.dueAt) > now.getTime();
@@ -2417,19 +2450,33 @@ async function taskHistory(d, user, taskId) {
2417
2450
  }
2418
2451
  }
2419
2452
  const owner = task.ownerId === user.id ? user : await d.store.getUser(task.ownerId);
2420
- const cadence = task.schedule && owner ? describeSchedule(task.schedule, owner, user.timeZone, now) : "no schedule";
2421
- const lines = [`\`${task.id}\` ${task.title} -- ${task.type}, ${task.status}, ${cadence}`];
2422
- lines.push(next ? `Next: ${formatInstant(next.dueAt, user.timeZone, now)}` : "Next: nothing scheduled");
2423
- lines.push(runs.length === 0 ? "Runs: none yet" : "Runs:");
2424
- if (runs.length > HISTORY_RUNS)
2425
- lines.push(`- (${runs.length - HISTORY_RUNS} earlier)`);
2426
- for (const o of runs.slice(-HISTORY_RUNS))
2427
- lines.push(formatRun(o, replies, people, user, now));
2453
+ return {
2454
+ task,
2455
+ cadence: task.schedule && owner ? describeSchedule(task.schedule, owner, user.timeZone, now) : "no schedule",
2456
+ next: next ? formatInstant(next.dueAt, user.timeZone, now) : null,
2457
+ runs: runs.slice(-HISTORY_RUNS).map((o) => runView(o, replies, people, user, now)),
2458
+ earlierRuns: Math.max(0, runs.length - HISTORY_RUNS),
2459
+ changes: changes.slice(-HISTORY_CHANGES).map((e) => changeView(e, user, now)),
2460
+ earlierChanges: Math.max(0, changes.length - HISTORY_CHANGES)
2461
+ };
2462
+ }
2463
+ async function taskHistory(d, user, taskId) {
2464
+ const h = await loadHistory(d, user, taskId);
2465
+ if (!h)
2466
+ return NO_SUCH_TASK;
2467
+ const { task } = h;
2468
+ const lines = [`\`${task.id}\` ${task.title} -- ${task.type}, ${task.status}, ${h.cadence}`];
2469
+ lines.push(h.next ? `Next: ${h.next}` : "Next: nothing scheduled");
2470
+ lines.push(h.runs.length === 0 ? "Runs: none yet" : "Runs:");
2471
+ if (h.earlierRuns > 0)
2472
+ lines.push(`- (${h.earlierRuns} earlier)`);
2473
+ for (const v of h.runs)
2474
+ lines.push(runLine(v));
2428
2475
  lines.push("Changes:");
2429
- if (changes.length > HISTORY_CHANGES)
2430
- lines.push(`- (${changes.length - HISTORY_CHANGES} earlier)`);
2431
- for (const e of changes.slice(-HISTORY_CHANGES))
2432
- lines.push(formatChange(e, user, now));
2476
+ if (h.earlierChanges > 0)
2477
+ lines.push(`- (${h.earlierChanges} earlier)`);
2478
+ for (const c of h.changes)
2479
+ lines.push(changeLine(c));
2433
2480
  return clip(lines.join(`
2434
2481
  `));
2435
2482
  }
@@ -2815,6 +2862,68 @@ ${body}` : body;
2815
2862
  };
2816
2863
  }
2817
2864
 
2865
+ // plugins/tracker/src/web/secrets.ts
2866
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
2867
+ function randomToken() {
2868
+ return randomBytes(32).toString("base64url");
2869
+ }
2870
+ function sha256(value) {
2871
+ return createHash("sha256").update(value).digest("hex");
2872
+ }
2873
+ function safeEqual(a, b) {
2874
+ const x = Buffer.from(a);
2875
+ const y = Buffer.from(b);
2876
+ return x.length === y.length && timingSafeEqual(x, y);
2877
+ }
2878
+ function isTokenShape(value) {
2879
+ return /^[A-Za-z0-9_-]{43}$/.test(value);
2880
+ }
2881
+
2882
+ // plugins/tracker/src/web/signin-link.ts
2883
+ var LINK_TTL_MS = 10 * 60 * 1000;
2884
+
2885
+ class LoginLinks {
2886
+ db;
2887
+ constructor(db) {
2888
+ this.db = db;
2889
+ }
2890
+ issue(userId, now, memberCheckedAt = null) {
2891
+ const token = randomToken();
2892
+ const at = now.toISOString();
2893
+ this.db.query("DELETE FROM web_login_tokens WHERE expires_at <= ?").run(at);
2894
+ this.db.query("INSERT INTO web_login_tokens (token_hash, user_id, created_at, expires_at, member_checked_at) VALUES (?, ?, ?, ?, ?)").run(sha256(token), userId, at, new Date(now.getTime() + LINK_TTL_MS).toISOString(), memberCheckedAt);
2895
+ return token;
2896
+ }
2897
+ peek(token, now) {
2898
+ if (!isTokenShape(token))
2899
+ return false;
2900
+ const row = this.db.query("SELECT 1 AS ok FROM web_login_tokens WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?").get(sha256(token), now.toISOString());
2901
+ return row !== null;
2902
+ }
2903
+ consume(token, now) {
2904
+ if (!isTokenShape(token))
2905
+ return null;
2906
+ const at = now.toISOString();
2907
+ const row = this.db.query("UPDATE web_login_tokens SET used_at = ? WHERE token_hash = ? AND used_at IS NULL AND expires_at > ? RETURNING user_id, member_checked_at").get(at, sha256(token), at);
2908
+ return row ? { userId: row.user_id, memberCheckedAt: row.member_checked_at } : null;
2909
+ }
2910
+ }
2911
+
2912
+ // plugins/tracker/src/web/command.ts
2913
+ var WEB_NOT_CONFIGURED = "The web area is not set up on this bot.";
2914
+ function webLink(d, user, web) {
2915
+ if (!web)
2916
+ return WEB_NOT_CONFIGURED;
2917
+ const now = d.clock.now();
2918
+ const token = d.logins.issue(user.id, now, web.gated ? now.toISOString() : null);
2919
+ const minutes = LINK_TTL_MS / 60000;
2920
+ return [
2921
+ `Your sign-in link for the tracker's web area. It works once, within ${minutes} minutes; do not share it.`,
2922
+ `<${web.origin}/${web.name}/login?t=${token}>`
2923
+ ].join(`
2924
+ `);
2925
+ }
2926
+
2818
2927
  // plugins/tracker/src/discord.ts
2819
2928
  function displayName(user) {
2820
2929
  return user.globalName ?? user.username;
@@ -2822,7 +2931,7 @@ function displayName(user) {
2822
2931
  function createSurface(w) {
2823
2932
  const ctx = {
2824
2933
  deps: w.deps,
2825
- queue: serial(),
2934
+ queue: w.queue ?? serial(),
2826
2935
  membershipOf: (interaction, discordId) => w.membership ? w.membership(interaction, discordId) : lookupMembership(interaction, w.guildId, discordId, w.log),
2827
2936
  log: w.log
2828
2937
  };
@@ -2937,6 +3046,11 @@ ${body}` : body);
2937
3046
  name: "settings",
2938
3047
  build: (b) => b.setDescription("Your tracker settings").addSubcommand((s) => s.setName("hour").setDescription("The hour (0-23) a reminder without a time reaches you").addIntegerOption((o) => o.setName("hour").setDescription("0-23, in your time zone").setRequired(true).setMinValue(0).setMaxValue(23))),
2939
3048
  handle: (interaction) => run(interaction, "registered", (d, user) => setHour(d, user, interaction.options.getInteger("hour", true)))
3049
+ },
3050
+ {
3051
+ name: "web",
3052
+ build: (b) => b.setDescription("Get a one-time link to sign in to the tracker's web area"),
3053
+ handle: (interaction) => run(interaction, "registered", async (d, user) => webLink(d, user, w.web))
2940
3054
  }
2941
3055
  ];
2942
3056
  return { commands, interactions: createInteractionHandler(ctx) };
@@ -3150,6 +3264,28 @@ var MIGRATIONS = [
3150
3264
  at TEXT NOT NULL,
3151
3265
  PRIMARY KEY (task_id, user_id)
3152
3266
  );
3267
+ `,
3268
+ `
3269
+ CREATE TABLE web_login_tokens (
3270
+ token_hash TEXT PRIMARY KEY,
3271
+ user_id TEXT NOT NULL,
3272
+ created_at TEXT NOT NULL,
3273
+ expires_at TEXT NOT NULL,
3274
+ used_at TEXT,
3275
+ member_checked_at TEXT
3276
+ );
3277
+ CREATE INDEX web_login_tokens_expiry ON web_login_tokens (expires_at);
3278
+
3279
+ CREATE TABLE web_sessions (
3280
+ id_hash TEXT PRIMARY KEY,
3281
+ user_id TEXT NOT NULL,
3282
+ csrf TEXT NOT NULL,
3283
+ created_at TEXT NOT NULL,
3284
+ expires_at TEXT NOT NULL,
3285
+ member_checked_at TEXT
3286
+ );
3287
+ CREATE INDEX web_sessions_user ON web_sessions (user_id);
3288
+ CREATE INDEX web_sessions_expiry ON web_sessions (expires_at);
3153
3289
  `
3154
3290
  ];
3155
3291
  function migrate(db) {
@@ -3493,40 +3629,1746 @@ class SqliteStore {
3493
3629
  }
3494
3630
  }
3495
3631
 
3496
- // plugins/tracker/src/index.ts
3497
- var DB_DIR = "tracker";
3498
- var DB_FILE = "tracker.sqlite";
3499
- var TRACKER_TYPES = Object.freeze({
3500
- reminder,
3501
- renewal
3502
- });
3503
- var NO_DM = {
3504
- async sendDm() {
3505
- throw new Error("this bot has no host.dm (rackbops-discord-bot#736)");
3632
+ // node_modules/@rackbops/styles/rackbops-noir/bundle.css
3633
+ var bundle_default = `:where([data-rb-style], [data-rb-style] *), :where([data-rb-style], [data-rb-style] *):before, :where([data-rb-style], [data-rb-style] *):after {
3634
+ box-sizing: border-box;
3635
+ }
3636
+
3637
+ :where(html[data-rb-style]) body {
3638
+ min-height: 100vh;
3639
+ margin: 0;
3640
+ }
3641
+
3642
+ :where([data-rb-style="rackbops-noir"]) {
3643
+ color-scheme: dark;
3644
+ --rb-bg: #0d141d;
3645
+ --rb-surface: #141f2a;
3646
+ --rb-surface-2: #19242f;
3647
+ --rb-surface-sunken: #0f1720;
3648
+ --rb-text: #e9eff4;
3649
+ --rb-text-soft: #a4b4c3;
3650
+ --rb-text-faint: #697a8c;
3651
+ --rb-border: #243140;
3652
+ --rb-border-strong: #31404f;
3653
+ --rb-accent: #ff5d3d;
3654
+ --rb-accent-fg: #0d141d;
3655
+ --rb-accent-wash: #ff5d3d21;
3656
+ --rb-accent-grad: linear-gradient(180deg, #ff5d3d, #ff7a5f);
3657
+ --rb-info: #4c9df0;
3658
+ --rb-success: #43c97e;
3659
+ --rb-warning: #f2a93b;
3660
+ --rb-danger: #f0616d;
3661
+ --rb-font-display: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3662
+ --rb-font-body: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
3663
+ --rb-font-mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
3664
+ --rb-font-weight: 400;
3665
+ --rb-font-weight-medium: 600;
3666
+ --rb-font-weight-bold: 800;
3667
+ --rb-heading-tracking: -.02em;
3668
+ --rb-label-tracking: .18em;
3669
+ --rb-text-sm: 13.5px;
3670
+ --rb-radius: .6875rem;
3671
+ --rb-radius-lg: .9375rem;
3672
+ --rb-radius-pill: 99px;
3673
+ --rb-shadow-sm: 0 1px 2px #00000059;
3674
+ --rb-shadow-lg: 0 2px 6px #0006, 0 24px 50px -24px #000000b3;
3675
+ --rb-blur: 10px;
3676
+ --rb-transition: .15s;
3677
+ --rb-focus-ring: 2.5px solid var(--rb-accent);
3678
+ --rb-ease: ease;
3679
+ --rb-space-1: .5rem;
3680
+ --rb-space-2: .75rem;
3681
+ --rb-space-3: 1rem;
3682
+ --rb-space-4: 1.5rem;
3683
+ --rb-space-5: 2.25rem;
3684
+ }
3685
+
3686
+ @media (prefers-reduced-motion: reduce) {
3687
+ :where([data-rb-style="rackbops-noir"]) {
3688
+ --rb-transition: 0s;
3506
3689
  }
3507
- };
3508
- function createPlugin(host, options = {}) {
3509
- const adminIds = parseAdminIds(host.env.TRACKER_ADMIN_DISCORD_IDS);
3510
- const guildId = parseGuildId(host.env.TRACKER_GUILD_ID);
3511
- const clock = options.clock ?? { now: () => new Date };
3512
- const types = options.types ?? TRACKER_TYPES;
3513
- const health = { activatedAt: null, lastTickAt: null, blocked: null };
3514
- let db = null;
3515
- let store = null;
3516
- let claims = null;
3517
- let delivery = null;
3518
- let deps = null;
3519
- let warnedNoDm = false;
3520
- const dm = host.dm?.bind(host);
3521
- const surface = createSurface({
3522
- deps: () => deps,
3523
- guildId,
3524
- log: host.log,
3525
- ...options.membership ? { membership: options.membership } : {}
3526
- });
3527
- return {
3528
- commands: surface.commands,
3529
- interactions: surface.interactions,
3690
+ }
3691
+
3692
+ :where([data-rb-style="rackbops-noir"]) {
3693
+ background-color: var(--rb-bg);
3694
+ color: var(--rb-text);
3695
+ font-family: var(--rb-font-body);
3696
+ font-weight: var(--rb-font-weight);
3697
+ font-size: 17px;
3698
+ line-height: 1.6;
3699
+ }
3700
+
3701
+ :where(html[data-rb-style="rackbops-noir"]) body {
3702
+ background-color: var(--rb-bg);
3703
+ color: var(--rb-text);
3704
+ font-family: var(--rb-font-body);
3705
+ font-weight: var(--rb-font-weight);
3706
+ }
3707
+
3708
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(h1, h2, h3, h4, h5, h6) {
3709
+ margin: 0 0 var(--rb-space-3);
3710
+ font-family: var(--rb-font-display);
3711
+ font-weight: var(--rb-font-weight-bold);
3712
+ letter-spacing: var(--rb-heading-tracking);
3713
+ text-wrap: balance;
3714
+ line-height: 1.08;
3715
+ }
3716
+
3717
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(p) {
3718
+ margin: 0 0 var(--rb-space-3);
3719
+ }
3720
+
3721
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(ul, ol) {
3722
+ margin: 0 0 var(--rb-space-3);
3723
+ padding-inline-start: var(--rb-space-4);
3724
+ }
3725
+
3726
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(a) {
3727
+ color: var(--rb-accent);
3728
+ }
3729
+
3730
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *):where(:focus-visible) {
3731
+ outline: var(--rb-focus-ring);
3732
+ outline-offset: 3px;
3733
+ border-radius: 3px;
3734
+ }
3735
+
3736
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *)::selection {
3737
+ background: var(--rb-accent);
3738
+ color: #fff;
3739
+ }
3740
+
3741
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn {
3742
+ align-items: center;
3743
+ gap: var(--rb-space-2);
3744
+ border-radius: var(--rb-radius);
3745
+ font-weight: var(--rb-font-weight-medium);
3746
+ cursor: pointer;
3747
+ color: var(--rb-text);
3748
+ transition: transform var(--rb-transition) var(--rb-ease),
3749
+ background var(--rb-transition) var(--rb-ease),
3750
+ border-color var(--rb-transition) var(--rb-ease),
3751
+ color var(--rb-transition) var(--rb-ease);
3752
+ background: none;
3753
+ border: 1px solid #0000;
3754
+ padding: 13px 22px;
3755
+ font-size: 16px;
3756
+ text-decoration: none;
3757
+ display: inline-flex;
3758
+ }
3759
+
3760
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--sm {
3761
+ font-size: var(--rb-text-sm);
3762
+ padding: 7px 14px;
3763
+ }
3764
+
3765
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-icon-btn {
3766
+ padding: var(--rb-space-2);
3767
+ justify-content: center;
3768
+ gap: 0;
3769
+ min-width: 2rem;
3770
+ min-height: 2rem;
3771
+ line-height: 1;
3772
+ }
3773
+
3774
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:active {
3775
+ transform: translateY(1px);
3776
+ }
3777
+
3778
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary {
3779
+ background: var(--rb-text);
3780
+ color: var(--rb-bg);
3781
+ }
3782
+
3783
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--primary:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent {
3784
+ background: var(--rb-accent);
3785
+ color: var(--rb-accent-fg);
3786
+ }
3787
+
3788
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--accent:focus-visible {
3789
+ background: color-mix(in srgb, var(--rb-accent), var(--rb-text) 15%);
3790
+ }
3791
+
3792
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost {
3793
+ color: var(--rb-text);
3794
+ border-color: var(--rb-border-strong);
3795
+ background: none;
3796
+ }
3797
+
3798
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--ghost:focus-visible {
3799
+ border-color: var(--rb-text);
3800
+ background: var(--rb-surface-2);
3801
+ }
3802
+
3803
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger {
3804
+ color: var(--rb-danger);
3805
+ border-color: color-mix(in srgb, var(--rb-danger) 45%, var(--rb-border));
3806
+ background: none;
3807
+ }
3808
+
3809
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger:not(:disabled):hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn--danger:focus-visible {
3810
+ border-color: var(--rb-danger);
3811
+ }
3812
+
3813
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:disabled {
3814
+ opacity: .5;
3815
+ cursor: not-allowed;
3816
+ }
3817
+
3818
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn__arrow {
3819
+ transition: transform var(--rb-transition) var(--rb-ease);
3820
+ }
3821
+
3822
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-btn:not(:disabled):hover .rb-btn__arrow {
3823
+ transform: translateX(3px);
3824
+ }
3825
+
3826
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card {
3827
+ background: var(--rb-surface);
3828
+ border: 1px solid var(--rb-border);
3829
+ border-radius: var(--rb-radius-lg);
3830
+ box-shadow: var(--rb-shadow-sm);
3831
+ transition: transform var(--rb-transition) var(--rb-ease),
3832
+ border-color var(--rb-transition) var(--rb-ease),
3833
+ box-shadow var(--rb-transition) var(--rb-ease);
3834
+ flex-direction: column;
3835
+ padding: 26px 24px 28px;
3836
+ display: flex;
3837
+ }
3838
+
3839
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card:hover, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card--raised {
3840
+ border-color: var(--rb-border-strong);
3841
+ box-shadow: var(--rb-shadow-lg);
3842
+ transform: translateY(-4px);
3843
+ }
3844
+
3845
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card--raised:hover {
3846
+ transform: translateY(-6px);
3847
+ }
3848
+
3849
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-card__tag {
3850
+ font-family: var(--rb-font-mono);
3851
+ letter-spacing: .12em;
3852
+ text-transform: uppercase;
3853
+ color: var(--rb-accent);
3854
+ justify-content: space-between;
3855
+ align-items: center;
3856
+ font-size: 12px;
3857
+ display: flex;
3858
+ }
3859
+
3860
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link {
3861
+ align-items: center;
3862
+ gap: var(--rb-space-1);
3863
+ font-family: var(--rb-font-mono);
3864
+ font-size: var(--rb-text-sm);
3865
+ color: var(--rb-text-soft);
3866
+ cursor: pointer;
3867
+ transition: color var(--rb-transition) var(--rb-ease),
3868
+ background var(--rb-transition) var(--rb-ease);
3869
+ border-radius: 8px;
3870
+ padding: 8px 12px;
3871
+ text-decoration: none;
3872
+ display: inline-flex;
3873
+ }
3874
+
3875
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link:hover {
3876
+ color: var(--rb-text);
3877
+ background: var(--rb-surface-2);
3878
+ }
3879
+
3880
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link.rb-link--active, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-link[aria-current="page"] {
3881
+ color: var(--rb-accent);
3882
+ }
3883
+
3884
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-nav-rail {
3885
+ gap: var(--rb-space-1);
3886
+ width: 14rem;
3887
+ padding: var(--rb-space-3);
3888
+ background: var(--rb-surface);
3889
+ border-right: 1px solid var(--rb-border);
3890
+ flex-direction: column;
3891
+ flex-shrink: 0;
3892
+ display: flex;
3893
+ }
3894
+
3895
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field {
3896
+ gap: var(--rb-space-1);
3897
+ flex-direction: column;
3898
+ display: flex;
3899
+ }
3900
+
3901
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-label {
3902
+ font-size: var(--rb-text-sm);
3903
+ font-weight: var(--rb-font-weight-medium);
3904
+ color: var(--rb-text-soft);
3905
+ }
3906
+
3907
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-label__required {
3908
+ color: var(--rb-danger);
3909
+ margin-left: var(--rb-space-1);
3910
+ }
3911
+
3912
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field__help {
3913
+ color: var(--rb-text-soft);
3914
+ font-size: var(--rb-text-sm);
3915
+ margin-top: var(--rb-space-1);
3916
+ }
3917
+
3918
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-field__error {
3919
+ color: var(--rb-text);
3920
+ font-size: var(--rb-text-sm);
3921
+ padding-left: var(--rb-space-2);
3922
+ border-left: 3px solid var(--rb-danger);
3923
+ margin-top: var(--rb-space-1);
3924
+ }
3925
+
3926
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select {
3927
+ background: var(--rb-surface);
3928
+ border: 1px solid var(--rb-border-strong);
3929
+ border-radius: var(--rb-radius);
3930
+ width: 100%;
3931
+ color: var(--rb-text);
3932
+ font-family: var(--rb-font-body);
3933
+ transition: border-color var(--rb-transition) var(--rb-ease),
3934
+ box-shadow var(--rb-transition) var(--rb-ease);
3935
+ padding: 12px 14px;
3936
+ font-size: 16px;
3937
+ }
3938
+
3939
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea {
3940
+ resize: vertical;
3941
+ min-height: 6rem;
3942
+ }
3943
+
3944
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select:focus-visible {
3945
+ border-color: var(--rb-accent);
3946
+ box-shadow: 0 0 0 3px var(--rb-accent-wash);
3947
+ outline: none;
3948
+ }
3949
+
3950
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input[aria-invalid="true"], :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea[aria-invalid="true"], :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-select[aria-invalid="true"] {
3951
+ border-color: var(--rb-danger);
3952
+ }
3953
+
3954
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-input::placeholder, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-textarea::placeholder {
3955
+ color: var(--rb-text-faint);
3956
+ }
3957
+
3958
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-choice {
3959
+ align-items: center;
3960
+ gap: var(--rb-space-1);
3961
+ color: var(--rb-text);
3962
+ cursor: pointer;
3963
+ font-size: 16px;
3964
+ display: inline-flex;
3965
+ }
3966
+
3967
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-checkbox, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-radio {
3968
+ accent-color: var(--rb-accent);
3969
+ cursor: pointer;
3970
+ width: 1.1rem;
3971
+ height: 1.1rem;
3972
+ }
3973
+
3974
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch {
3975
+ appearance: none;
3976
+ vertical-align: text-bottom;
3977
+ cursor: pointer;
3978
+ background: var(--rb-surface-sunken);
3979
+ border: 1px solid var(--rb-border-strong);
3980
+ border-radius: var(--rb-radius-pill);
3981
+ width: 2.25rem;
3982
+ height: 1.25rem;
3983
+ transition: background var(--rb-transition) var(--rb-ease), border-color var(--rb-transition) var(--rb-ease);
3984
+ margin: 0;
3985
+ position: relative;
3986
+ }
3987
+
3988
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:after {
3989
+ content: "";
3990
+ background: var(--rb-text-soft);
3991
+ width: calc(1.25rem - 6px);
3992
+ height: calc(1.25rem - 6px);
3993
+ transition: transform var(--rb-transition) var(--rb-ease), background var(--rb-transition) var(--rb-ease);
3994
+ border-radius: 50%;
3995
+ position: absolute;
3996
+ top: 2px;
3997
+ left: 2px;
3998
+ }
3999
+
4000
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:checked {
4001
+ background: var(--rb-accent);
4002
+ border-color: var(--rb-accent);
4003
+ }
4004
+
4005
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:checked:after {
4006
+ background: var(--rb-accent-fg);
4007
+ transform: translateX(1rem);
4008
+ }
4009
+
4010
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-switch:disabled {
4011
+ cursor: not-allowed;
4012
+ opacity: .5;
4013
+ }
4014
+
4015
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge {
4016
+ align-items: center;
4017
+ gap: var(--rb-space-1);
4018
+ border: 1px solid var(--rb-border);
4019
+ border-radius: var(--rb-radius-pill);
4020
+ font-family: var(--rb-font-mono);
4021
+ letter-spacing: .02em;
4022
+ color: var(--rb-text-soft);
4023
+ background: var(--rb-surface);
4024
+ padding: 4px 10px;
4025
+ font-size: 11.5px;
4026
+ display: inline-flex;
4027
+ }
4028
+
4029
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--md {
4030
+ font-size: var(--rb-text-sm);
4031
+ padding: var(--rb-space-1) var(--rb-space-3);
4032
+ }
4033
+
4034
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--info {
4035
+ background: color-mix(in srgb, var(--rb-info) 14%, var(--rb-surface));
4036
+ border-color: color-mix(in srgb, var(--rb-info) 45%, var(--rb-border));
4037
+ color: var(--rb-text);
4038
+ }
4039
+
4040
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--success {
4041
+ background: color-mix(in srgb, var(--rb-success) 14%, var(--rb-surface));
4042
+ border-color: color-mix(in srgb, var(--rb-success) 45%, var(--rb-border));
4043
+ color: var(--rb-text);
4044
+ }
4045
+
4046
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--warning {
4047
+ background: color-mix(in srgb, var(--rb-warning) 16%, var(--rb-surface));
4048
+ border-color: color-mix(in srgb, var(--rb-warning) 45%, var(--rb-border));
4049
+ color: var(--rb-text);
4050
+ }
4051
+
4052
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-badge--danger {
4053
+ background: color-mix(in srgb, var(--rb-danger) 14%, var(--rb-surface));
4054
+ border-color: color-mix(in srgb, var(--rb-danger) 45%, var(--rb-border));
4055
+ color: var(--rb-text);
4056
+ }
4057
+
4058
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert {
4059
+ gap: var(--rb-space-1);
4060
+ padding: var(--rb-space-3) var(--rb-space-4);
4061
+ background: var(--rb-surface);
4062
+ border: 1px solid var(--rb-border);
4063
+ border-left: 4px solid var(--rb-accent);
4064
+ border-radius: var(--rb-radius);
4065
+ box-shadow: var(--rb-shadow-sm);
4066
+ color: var(--rb-text);
4067
+ flex-direction: column;
4068
+ display: flex;
4069
+ }
4070
+
4071
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert__title {
4072
+ font-weight: var(--rb-font-weight-medium);
4073
+ }
4074
+
4075
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--info {
4076
+ border-left-color: var(--rb-info);
4077
+ }
4078
+
4079
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--success {
4080
+ border-left-color: var(--rb-success);
4081
+ }
4082
+
4083
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--warning {
4084
+ border-left-color: var(--rb-warning);
4085
+ }
4086
+
4087
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-alert--danger {
4088
+ border-left-color: var(--rb-danger);
4089
+ }
4090
+
4091
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast-region {
4092
+ top: var(--rb-space-4);
4093
+ right: var(--rb-space-4);
4094
+ z-index: 1000;
4095
+ gap: var(--rb-space-2);
4096
+ width: fit-content;
4097
+ max-width: min(28rem, calc(100vw - 2 * var(--rb-space-4)));
4098
+ pointer-events: none;
4099
+ flex-direction: column;
4100
+ display: flex;
4101
+ position: fixed;
4102
+ }
4103
+
4104
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast {
4105
+ pointer-events: auto;
4106
+ align-items: flex-start;
4107
+ gap: var(--rb-space-3);
4108
+ padding: var(--rb-space-3) var(--rb-space-4);
4109
+ background: var(--rb-surface);
4110
+ border: 1px solid var(--rb-border);
4111
+ border-left: 4px solid var(--rb-accent);
4112
+ border-radius: var(--rb-radius);
4113
+ box-shadow: var(--rb-shadow-lg);
4114
+ color: var(--rb-text);
4115
+ transition: opacity var(--rb-transition) var(--rb-ease),
4116
+ transform var(--rb-transition) var(--rb-ease);
4117
+ display: flex;
4118
+ }
4119
+
4120
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast[data-rb-enter] {
4121
+ opacity: 0;
4122
+ transform: translateY(calc(-1 * var(--rb-space-2)));
4123
+ }
4124
+
4125
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--info {
4126
+ border-left-color: var(--rb-info);
4127
+ }
4128
+
4129
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--success {
4130
+ border-left-color: var(--rb-success);
4131
+ }
4132
+
4133
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--warning {
4134
+ border-left-color: var(--rb-warning);
4135
+ }
4136
+
4137
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast--danger {
4138
+ border-left-color: var(--rb-danger);
4139
+ }
4140
+
4141
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close {
4142
+ border-radius: var(--rb-radius);
4143
+ width: 1.25rem;
4144
+ height: 1.25rem;
4145
+ color: var(--rb-text-soft);
4146
+ cursor: pointer;
4147
+ transition: color var(--rb-transition) var(--rb-ease);
4148
+ background: none;
4149
+ border: none;
4150
+ flex: none;
4151
+ justify-content: center;
4152
+ align-items: center;
4153
+ margin-inline-start: auto;
4154
+ padding: 0;
4155
+ font-size: 1.125rem;
4156
+ line-height: 1;
4157
+ display: inline-flex;
4158
+ }
4159
+
4160
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close:hover {
4161
+ color: var(--rb-text);
4162
+ }
4163
+
4164
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-toast__close:focus-visible {
4165
+ outline: var(--rb-focus-ring);
4166
+ outline-offset: 2px;
4167
+ }
4168
+
4169
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog {
4170
+ border: 1px solid var(--rb-border);
4171
+ border-radius: var(--rb-radius-lg);
4172
+ background: var(--rb-surface);
4173
+ color: var(--rb-text);
4174
+ box-shadow: var(--rb-shadow-lg);
4175
+ max-width: min(92vw, 34rem);
4176
+ padding: 0;
4177
+ }
4178
+
4179
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog::backdrop {
4180
+ -webkit-backdrop-filter: blur(var(--rb-blur));
4181
+ background: #131e2966;
4182
+ }
4183
+
4184
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__body {
4185
+ padding: var(--rb-space-5);
4186
+ }
4187
+
4188
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__title {
4189
+ margin: 0 0 var(--rb-space-3);
4190
+ font-family: var(--rb-font-display);
4191
+ font-weight: var(--rb-font-weight-bold);
4192
+ letter-spacing: var(--rb-heading-tracking);
4193
+ font-size: 1.4rem;
4194
+ }
4195
+
4196
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-dialog__actions {
4197
+ justify-content: flex-end;
4198
+ gap: var(--rb-space-2);
4199
+ padding: var(--rb-space-3) var(--rb-space-5);
4200
+ border-top: 1px solid var(--rb-border);
4201
+ display: flex;
4202
+ }
4203
+
4204
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabs {
4205
+ gap: var(--rb-space-4);
4206
+ border-bottom: 1px solid var(--rb-border);
4207
+ display: flex;
4208
+ }
4209
+
4210
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab {
4211
+ padding: var(--rb-space-2) 0;
4212
+ color: var(--rb-text-soft);
4213
+ font-family: var(--rb-font-body);
4214
+ font-weight: var(--rb-font-weight-medium);
4215
+ cursor: pointer;
4216
+ transition: color var(--rb-transition) var(--rb-ease);
4217
+ background: none;
4218
+ border: 0;
4219
+ font-size: 15px;
4220
+ position: relative;
4221
+ }
4222
+
4223
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab:not(:disabled):hover {
4224
+ color: var(--rb-text);
4225
+ }
4226
+
4227
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab:disabled {
4228
+ opacity: .5;
4229
+ cursor: not-allowed;
4230
+ }
4231
+
4232
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab.rb-tab--active, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab[aria-selected="true"] {
4233
+ color: var(--rb-text);
4234
+ }
4235
+
4236
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab.rb-tab--active:after, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tab[aria-selected="true"]:after {
4237
+ content: "";
4238
+ background: var(--rb-accent);
4239
+ border-radius: 2px 2px 0 0;
4240
+ height: 2px;
4241
+ position: absolute;
4242
+ bottom: -1px;
4243
+ left: 0;
4244
+ right: 0;
4245
+ }
4246
+
4247
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabpanel {
4248
+ padding: var(--rb-space-4) 0;
4249
+ }
4250
+
4251
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip {
4252
+ gap: var(--rb-space-2);
4253
+ padding: var(--rb-space-2) 0;
4254
+ display: flex;
4255
+ }
4256
+
4257
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab {
4258
+ padding: var(--rb-space-2) var(--rb-space-4);
4259
+ background: var(--rb-surface-2);
4260
+ border: 1px solid var(--rb-border);
4261
+ border-radius: var(--rb-radius);
4262
+ color: var(--rb-text);
4263
+ font-size: var(--rb-text-sm);
4264
+ font-weight: var(--rb-font-weight-medium);
4265
+ cursor: pointer;
4266
+ transition: border-color var(--rb-transition) var(--rb-ease);
4267
+ position: relative;
4268
+ }
4269
+
4270
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab:not(:disabled):hover {
4271
+ border-color: var(--rb-accent);
4272
+ }
4273
+
4274
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab:disabled {
4275
+ opacity: .5;
4276
+ cursor: not-allowed;
4277
+ }
4278
+
4279
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__tab.rb-tabstrip__tab--active {
4280
+ color: var(--rb-accent);
4281
+ border-color: color-mix(in srgb, var(--rb-accent) 40%, var(--rb-border));
4282
+ background: var(--rb-accent-wash);
4283
+ }
4284
+
4285
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tabstrip__badge {
4286
+ color: var(--rb-text-faint);
4287
+ font-size: var(--rb-text-sm);
4288
+ font-variant-numeric: tabular-nums;
4289
+ }
4290
+
4291
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-wordmark {
4292
+ width: fit-content;
4293
+ font-family: var(--rb-font-display);
4294
+ font-weight: var(--rb-font-weight-medium);
4295
+ letter-spacing: var(--rb-heading-tracking);
4296
+ color: var(--rb-text);
4297
+ align-items: baseline;
4298
+ gap: .4em;
4299
+ margin: 0;
4300
+ font-size: 1rem;
4301
+ display: inline-flex;
4302
+ }
4303
+
4304
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-wordmark__spark {
4305
+ color: var(--rb-accent);
4306
+ }
4307
+
4308
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table {
4309
+ border-collapse: collapse;
4310
+ width: 100%;
4311
+ font-size: 15px;
4312
+ }
4313
+
4314
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table th {
4315
+ text-align: left;
4316
+ padding: var(--rb-space-2) var(--rb-space-3);
4317
+ border-bottom: 2px solid var(--rb-border-strong);
4318
+ color: var(--rb-text-soft);
4319
+ font-family: var(--rb-font-mono);
4320
+ letter-spacing: .06em;
4321
+ text-transform: uppercase;
4322
+ font-size: 12px;
4323
+ font-weight: var(--rb-font-weight-medium);
4324
+ }
4325
+
4326
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table td {
4327
+ padding: var(--rb-space-2) var(--rb-space-3);
4328
+ border-bottom: 1px solid var(--rb-border);
4329
+ color: var(--rb-text);
4330
+ }
4331
+
4332
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table tbody tr:hover td {
4333
+ background: var(--rb-surface-2);
4334
+ }
4335
+
4336
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table .rb-num {
4337
+ text-align: right;
4338
+ font-variant-numeric: tabular-nums;
4339
+ }
4340
+
4341
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__group-row td {
4342
+ padding-top: var(--rb-space-3);
4343
+ padding-bottom: var(--rb-space-1);
4344
+ color: var(--rb-text-faint);
4345
+ font-size: var(--rb-text-sm);
4346
+ font-weight: var(--rb-font-weight-medium);
4347
+ text-transform: uppercase;
4348
+ letter-spacing: .04em;
4349
+ border-bottom: none;
4350
+ }
4351
+
4352
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort {
4353
+ align-items: center;
4354
+ gap: var(--rb-space-1);
4355
+ font: inherit;
4356
+ color: inherit;
4357
+ cursor: pointer;
4358
+ background: none;
4359
+ border: none;
4360
+ margin: 0;
4361
+ padding: 0;
4362
+ display: inline-flex;
4363
+ }
4364
+
4365
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort:hover {
4366
+ color: var(--rb-text);
4367
+ }
4368
+
4369
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort-icon {
4370
+ color: var(--rb-text-faint);
4371
+ }
4372
+
4373
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table__sort:hover .rb-table__sort-icon {
4374
+ color: var(--rb-text);
4375
+ }
4376
+
4377
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table-scroll {
4378
+ max-height: 70vh;
4379
+ overflow: auto;
4380
+ }
4381
+
4382
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table-scroll thead th {
4383
+ background: var(--rb-surface);
4384
+ z-index: 1;
4385
+ position: sticky;
4386
+ top: 0;
4387
+ }
4388
+
4389
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr {
4390
+ cursor: pointer;
4391
+ }
4392
+
4393
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:focus-visible, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:has(:focus-visible) {
4394
+ outline: var(--rb-focus-ring);
4395
+ outline-offset: -3px;
4396
+ border-radius: 0;
4397
+ }
4398
+
4399
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:focus-visible td, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-table--interactive tbody tr:has(:focus-visible) td {
4400
+ background: var(--rb-surface-2);
4401
+ }
4402
+
4403
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress {
4404
+ appearance: none;
4405
+ border-radius: var(--rb-radius-pill);
4406
+ background: var(--rb-surface-sunken);
4407
+ border: 0;
4408
+ width: 100%;
4409
+ height: 8px;
4410
+ overflow: hidden;
4411
+ }
4412
+
4413
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-webkit-progress-bar {
4414
+ background: var(--rb-surface-sunken);
4415
+ }
4416
+
4417
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-webkit-progress-value {
4418
+ background: var(--rb-accent);
4419
+ }
4420
+
4421
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress::-moz-progress-bar {
4422
+ background: var(--rb-accent);
4423
+ }
4424
+
4425
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-webkit-progress-bar {
4426
+ background-color: var(--rb-surface-sunken);
4427
+ background-image: linear-gradient(90deg, transparent 0%, var(--rb-accent) 50%, transparent 100%);
4428
+ background-repeat: no-repeat;
4429
+ background-size: 50% 100%;
4430
+ animation: 1.4s ease-in-out infinite rb-noir-indeterminate;
4431
+ }
4432
+
4433
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-moz-progress-bar {
4434
+ background-color: var(--rb-surface-sunken);
4435
+ background-image: linear-gradient(90deg, transparent 0%, var(--rb-accent) 50%, transparent 100%);
4436
+ background-repeat: no-repeat;
4437
+ background-size: 50% 100%;
4438
+ animation: 1.4s ease-in-out infinite rb-noir-indeterminate;
4439
+ }
4440
+
4441
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-spinner {
4442
+ border: 2px solid var(--rb-border-strong);
4443
+ border-top-color: var(--rb-accent);
4444
+ border-radius: 50%;
4445
+ width: 1.25rem;
4446
+ height: 1.25rem;
4447
+ animation: .7s linear infinite rb-noir-spin;
4448
+ display: inline-block;
4449
+ }
4450
+
4451
+ @keyframes rb-noir-spin {
4452
+ to {
4453
+ transform: rotate(360deg);
4454
+ }
4455
+ }
4456
+
4457
+ @keyframes rb-noir-indeterminate {
4458
+ 0% {
4459
+ background-position: -50% 0;
4460
+ }
4461
+
4462
+ 100% {
4463
+ background-position: 150% 0;
4464
+ }
4465
+ }
4466
+
4467
+ @media (prefers-reduced-motion: reduce) {
4468
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-webkit-progress-bar {
4469
+ animation-duration: 2s;
4470
+ }
4471
+
4472
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-progress:indeterminate::-moz-progress-bar {
4473
+ animation-duration: 2s;
4474
+ }
4475
+
4476
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-spinner {
4477
+ animation-duration: 2s;
4478
+ }
4479
+ }
4480
+
4481
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper {
4482
+ isolation: isolate;
4483
+ margin: 0;
4484
+ padding: 0;
4485
+ list-style: none;
4486
+ display: flex;
4487
+ }
4488
+
4489
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step {
4490
+ align-items: center;
4491
+ gap: var(--rb-space-2);
4492
+ text-align: center;
4493
+ flex-direction: column;
4494
+ flex: 1;
4495
+ min-width: 0;
4496
+ display: flex;
4497
+ position: relative;
4498
+ }
4499
+
4500
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child):before {
4501
+ content: "";
4502
+ background: var(--rb-surface-sunken);
4503
+ z-index: -1;
4504
+ width: 100%;
4505
+ height: 2px;
4506
+ transition: background-color var(--rb-transition) var(--rb-ease);
4507
+ position: absolute;
4508
+ top: 1rem;
4509
+ right: 50%;
4510
+ }
4511
+
4512
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child).rb-stepper--complete:before, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step:not(:first-child).rb-stepper--current:before, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"]:before {
4513
+ background: var(--rb-accent);
4514
+ }
4515
+
4516
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__node {
4517
+ border-radius: var(--rb-radius-pill);
4518
+ background: var(--rb-surface-2);
4519
+ width: 2rem;
4520
+ height: 2rem;
4521
+ color: var(--rb-text-faint);
4522
+ font-size: var(--rb-text-sm);
4523
+ font-weight: var(--rb-font-weight-medium);
4524
+ border: 2px solid #0000;
4525
+ justify-content: center;
4526
+ align-items: center;
4527
+ display: flex;
4528
+ }
4529
+
4530
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__label {
4531
+ font-size: var(--rb-text-sm);
4532
+ color: var(--rb-text-faint);
4533
+ }
4534
+
4535
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--complete .rb-stepper__node {
4536
+ background: var(--rb-accent);
4537
+ color: var(--rb-accent-fg);
4538
+ }
4539
+
4540
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--complete .rb-stepper__label {
4541
+ color: var(--rb-text);
4542
+ }
4543
+
4544
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--current .rb-stepper__node, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"] .rb-stepper__node {
4545
+ background: var(--rb-surface);
4546
+ border-color: var(--rb-accent);
4547
+ color: var(--rb-accent);
4548
+ box-shadow: var(--rb-shadow-lg);
4549
+ }
4550
+
4551
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper--current .rb-stepper__label, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-stepper__step[aria-current="step"] .rb-stepper__label {
4552
+ color: var(--rb-accent);
4553
+ font-weight: var(--rb-font-weight-medium);
4554
+ }
4555
+
4556
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-muted {
4557
+ color: var(--rb-text-faint);
4558
+ font-style: italic;
4559
+ }
4560
+
4561
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-pre {
4562
+ padding: var(--rb-space-3) var(--rb-space-4);
4563
+ background: var(--rb-surface-sunken);
4564
+ border: 1px solid var(--rb-border);
4565
+ border-radius: var(--rb-radius);
4566
+ font-family: var(--rb-font-mono);
4567
+ font-size: var(--rb-text-sm);
4568
+ margin: 0;
4569
+ overflow-x: auto;
4570
+ }
4571
+
4572
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-log {
4573
+ white-space: pre-wrap;
4574
+ overflow-wrap: anywhere;
4575
+ max-height: 16rem;
4576
+ line-height: 1.5;
4577
+ overflow-y: auto;
4578
+ }
4579
+
4580
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-pre.rb-log {
4581
+ overflow-x: hidden;
4582
+ }
4583
+
4584
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-eyebrow {
4585
+ font-family: var(--rb-font-mono);
4586
+ font-size: 12.5px;
4587
+ font-weight: var(--rb-font-weight-medium);
4588
+ letter-spacing: var(--rb-label-tracking);
4589
+ text-transform: uppercase;
4590
+ color: var(--rb-accent);
4591
+ align-items: center;
4592
+ gap: 10px;
4593
+ display: inline-flex;
4594
+ }
4595
+
4596
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-eyebrow:before {
4597
+ content: "";
4598
+ background: var(--rb-accent);
4599
+ width: 22px;
4600
+ height: 1.5px;
4601
+ display: inline-block;
4602
+ }
4603
+
4604
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack {
4605
+ --rb-rack-panel: #0a121a;
4606
+ --rb-rack-ink: #e7eef4;
4607
+ --rb-rack-faint: #7d90a2;
4608
+ --rb-rack-line: #1e2b37;
4609
+ background: var(--rb-rack-panel);
4610
+ border: 1px solid var(--rb-rack-line);
4611
+ box-shadow: var(--rb-shadow-lg);
4612
+ color: var(--rb-rack-ink);
4613
+ border-radius: 16px;
4614
+ padding: 24px;
4615
+ overflow: hidden;
4616
+ }
4617
+
4618
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__top {
4619
+ font-family: var(--rb-font-mono);
4620
+ letter-spacing: .14em;
4621
+ text-transform: uppercase;
4622
+ color: var(--rb-rack-faint);
4623
+ justify-content: space-between;
4624
+ align-items: center;
4625
+ font-size: 11.5px;
4626
+ display: flex;
4627
+ }
4628
+
4629
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live {
4630
+ color: var(--rb-rack-ink);
4631
+ align-items: center;
4632
+ gap: 7px;
4633
+ display: inline-flex;
4634
+ }
4635
+
4636
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live:before {
4637
+ content: "";
4638
+ background: var(--rb-accent);
4639
+ border-radius: 50%;
4640
+ width: 7px;
4641
+ height: 7px;
4642
+ animation: 1.8s ease-in-out infinite rb-noir-pulse;
4643
+ }
4644
+
4645
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars {
4646
+ align-items: flex-end;
4647
+ gap: 7px;
4648
+ height: 132px;
4649
+ margin: 22px 2px 16px;
4650
+ display: flex;
4651
+ }
4652
+
4653
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i {
4654
+ background: var(--rb-accent-grad);
4655
+ transform-origin: bottom;
4656
+ opacity: .92;
4657
+ border-radius: 4px 4px 2px 2px;
4658
+ flex: 1;
4659
+ animation: 1.4s ease-in-out infinite rb-noir-bop;
4660
+ }
4661
+
4662
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:first-child {
4663
+ height: 52%;
4664
+ animation-delay: -.2s;
4665
+ }
4666
+
4667
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(2) {
4668
+ height: 88%;
4669
+ animation-delay: -1.1s;
4670
+ }
4671
+
4672
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(3) {
4673
+ height: 34%;
4674
+ animation-delay: -.6s;
4675
+ }
4676
+
4677
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(4) {
4678
+ height: 70%;
4679
+ animation-delay: -.9s;
4680
+ }
4681
+
4682
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(5) {
4683
+ height: 100%;
4684
+ animation-delay: -.35s;
4685
+ }
4686
+
4687
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i:nth-child(6) {
4688
+ height: 46%;
4689
+ animation-delay: -1.3s;
4690
+ }
4691
+
4692
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__foot {
4693
+ border-top: 1px solid var(--rb-rack-line);
4694
+ font-family: var(--rb-font-mono);
4695
+ color: var(--rb-rack-faint);
4696
+ letter-spacing: .04em;
4697
+ justify-content: space-between;
4698
+ gap: 12px;
4699
+ padding-top: 14px;
4700
+ font-size: 11.5px;
4701
+ display: flex;
4702
+ }
4703
+
4704
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__foot b {
4705
+ color: var(--rb-rack-ink);
4706
+ font-weight: var(--rb-font-weight-medium);
4707
+ }
4708
+
4709
+ @keyframes rb-noir-bop {
4710
+ 0%, 100% {
4711
+ transform: scaleY(.5);
4712
+ }
4713
+
4714
+ 50% {
4715
+ transform: scaleY(1);
4716
+ }
4717
+ }
4718
+
4719
+ @keyframes rb-noir-pulse {
4720
+ 0%, 100% {
4721
+ opacity: 1;
4722
+ }
4723
+
4724
+ 50% {
4725
+ opacity: .35;
4726
+ }
4727
+ }
4728
+
4729
+ @media (prefers-reduced-motion: reduce) {
4730
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__bars > i, :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-rack__live:before {
4731
+ animation: none;
4732
+ }
4733
+ }
4734
+
4735
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principles {
4736
+ flex-direction: column;
4737
+ display: flex;
4738
+ }
4739
+
4740
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle {
4741
+ border-top: 1px solid var(--rb-border);
4742
+ grid-template-columns: 56px 1fr;
4743
+ gap: 18px;
4744
+ padding: 22px 0;
4745
+ display: grid;
4746
+ }
4747
+
4748
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principles > .rb-principle:first-child {
4749
+ border-top: 0;
4750
+ }
4751
+
4752
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__n {
4753
+ font-family: var(--rb-font-mono);
4754
+ font-size: 13px;
4755
+ font-weight: var(--rb-font-weight-medium);
4756
+ color: var(--rb-accent);
4757
+ padding-top: 3px;
4758
+ }
4759
+
4760
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__body h3 {
4761
+ margin: 0;
4762
+ font-size: 1.16rem;
4763
+ }
4764
+
4765
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-principle__body p {
4766
+ color: var(--rb-text-soft);
4767
+ margin: 7px 0 0;
4768
+ }
4769
+
4770
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tags {
4771
+ flex-wrap: wrap;
4772
+ gap: 8px;
4773
+ margin: 0;
4774
+ padding: 0;
4775
+ list-style: none;
4776
+ display: flex;
4777
+ }
4778
+
4779
+ :where([data-rb-style="rackbops-noir"], [data-rb-style="rackbops-noir"] *).rb-tag {
4780
+ font-family: var(--rb-font-mono);
4781
+ color: var(--rb-text-soft);
4782
+ border: 1px solid var(--rb-border);
4783
+ border-radius: var(--rb-radius-pill);
4784
+ letter-spacing: .02em;
4785
+ padding: 4px 10px;
4786
+ font-size: 11.5px;
4787
+ }
4788
+ `;
4789
+
4790
+ // plugins/tracker/src/web/theme.ts
4791
+ var THEME_CSS = bundle_default;
4792
+ var LAYOUT_CSS = `
4793
+ /* tracker web area layout (not part of the theme) */
4794
+ .tr-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rb-space-3); padding: var(--rb-space-3) var(--rb-space-4); border-bottom: 1px solid var(--rb-border); }
4795
+ .tr-top nav { display: flex; flex-wrap: wrap; align-items: center; gap: var(--rb-space-2); margin-left: auto; }
4796
+ .tr-top form { margin: 0; }
4797
+ .tr-main { max-width: 60rem; margin: 0 auto; padding: var(--rb-space-4); }
4798
+ .tr-main > section { margin-bottom: var(--rb-space-5); }
4799
+ .tr-stack { display: grid; gap: var(--rb-space-3); max-width: 28rem; }
4800
+ .tr-foot { margin-top: var(--rb-space-5); }
4801
+ `;
4802
+ var STYLESHEET = `${THEME_CSS}
4803
+ ${LAYOUT_CSS}`;
4804
+ var STYLESHEET_PATH = `/assets/theme.${sha256(STYLESHEET).slice(0, 16)}.css`;
4805
+
4806
+ // plugins/tracker/src/web/html.ts
4807
+ function esc(value) {
4808
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4809
+ }
4810
+
4811
+ class Html {
4812
+ value;
4813
+ constructor(value) {
4814
+ this.value = value;
4815
+ }
4816
+ toString() {
4817
+ return this.value;
4818
+ }
4819
+ }
4820
+ function render(part) {
4821
+ if (part === null || part === undefined || part === false)
4822
+ return "";
4823
+ if (part instanceof Html)
4824
+ return part.value;
4825
+ if (Array.isArray(part))
4826
+ return part.map(render).join("");
4827
+ return esc(part);
4828
+ }
4829
+ function html(strings, ...values) {
4830
+ let out = strings[0] ?? "";
4831
+ values.forEach((v, i) => {
4832
+ out += render(v) + (strings[i + 1] ?? "");
4833
+ });
4834
+ return new Html(out);
4835
+ }
4836
+ var CSP = "default-src 'none'; style-src 'self'; img-src 'none'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
4837
+ var REFERRER_POLICY = "same-origin";
4838
+ function pageHeaders(extra = {}) {
4839
+ const h = new Headers({
4840
+ "Content-Type": "text/html; charset=utf-8",
4841
+ "Content-Security-Policy": CSP,
4842
+ "X-Content-Type-Options": "nosniff",
4843
+ "X-Frame-Options": "DENY",
4844
+ "Referrer-Policy": REFERRER_POLICY,
4845
+ "Cache-Control": "no-store"
4846
+ });
4847
+ for (const [k, v] of Object.entries(extra))
4848
+ h.set(k, v);
4849
+ return h;
4850
+ }
4851
+ function page(f) {
4852
+ const nav = f.signedIn ? html`<nav aria-label="Tracker">
4853
+ <span class="rb-muted">${f.signedIn.name}</span>
4854
+ <a class="rb-link" href="${f.base}/">My tasks</a>
4855
+ <a class="rb-link" href="${f.base}/settings">Settings</a>
4856
+ <form method="post" action="${f.base}/logout"><input type="hidden" name="csrf" value="${f.signedIn.csrf}"><button class="rb-btn rb-btn--ghost rb-btn--sm" type="submit">Sign out</button></form>
4857
+ </nav>` : null;
4858
+ return html`<!doctype html>
4859
+ <html lang="en" data-rb-style="rackbops-noir">
4860
+ <head>
4861
+ <meta charset="utf-8">
4862
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4863
+ <meta name="referrer" content="${REFERRER_POLICY}">
4864
+ <title>${f.title} -- Tracker</title>
4865
+ <link rel="stylesheet" href="${f.base}${STYLESHEET_PATH}">
4866
+ </head>
4867
+ <body>
4868
+ <header class="tr-top"><span class="rb-wordmark">Tracker<span class="rb-wordmark__spark">.</span></span>${nav}</header>
4869
+ <main class="tr-main">
4870
+ ${f.body}
4871
+ </main>
4872
+ </body>
4873
+ </html>
4874
+ `.value;
4875
+ }
4876
+ function htmlResponse(body, status = 200, extra = {}) {
4877
+ return new Response(body, { status, headers: pageHeaders(extra) });
4878
+ }
4879
+ function redirect(location, cookies = []) {
4880
+ const h = new Headers({ Location: location, "Cache-Control": "no-store", "Referrer-Policy": REFERRER_POLICY });
4881
+ for (const c of cookies)
4882
+ h.append("Set-Cookie", c);
4883
+ return new Response(null, { status: 303, headers: h });
4884
+ }
4885
+ function readCookie(request, name) {
4886
+ const header = request.headers.get("cookie");
4887
+ if (!header)
4888
+ return null;
4889
+ for (const part of header.split(";")) {
4890
+ const eq = part.indexOf("=");
4891
+ if (eq < 0)
4892
+ continue;
4893
+ if (part.slice(0, eq).trim() === name)
4894
+ return part.slice(eq + 1).trim();
4895
+ }
4896
+ return null;
4897
+ }
4898
+ function cookie(name, value, o) {
4899
+ return `${name}=${value}; Path=${o.base}/; Max-Age=${o.maxAgeSeconds}; HttpOnly; Secure; SameSite=${o.sameSite}`;
4900
+ }
4901
+
4902
+ // plugins/tracker/src/web/pages.ts
4903
+ function name3(u) {
4904
+ return u.displayName ?? "you";
4905
+ }
4906
+ function framed(v, title, body) {
4907
+ return page({ base: v.base, title, signedIn: { name: name3(v.user), csrf: v.csrf }, body });
4908
+ }
4909
+ function signInHelpPage(base, note) {
4910
+ return page({
4911
+ base,
4912
+ title: "Sign in",
4913
+ body: html`<section class="rb-card">
4914
+ <h1>Sign in</h1>
4915
+ ${note ? html`<p class="rb-muted">${note}</p>` : null}
4916
+ <p>Run <code>/web</code> in Discord: the bot answers you alone with a sign-in link, good for ten minutes and one use.</p>
4917
+ </section>`
4918
+ });
4919
+ }
4920
+ function loginPage(base, input) {
4921
+ if (!input.valid)
4922
+ return signInHelpPage(base, "That sign-in link has expired or was already used.");
4923
+ return page({
4924
+ base,
4925
+ title: "Sign in",
4926
+ body: html`<section class="rb-card">
4927
+ <h1>Sign in to the tracker</h1>
4928
+ <p>This link works once. Press the button to sign in on this browser.</p>
4929
+ <form method="post" action="${base}/login">
4930
+ <input type="hidden" name="t" value="${input.token}">
4931
+ <input type="hidden" name="csrf" value="${input.loginCsrf}">
4932
+ <button class="rb-btn rb-btn--primary" type="submit">Sign in</button>
4933
+ </form>
4934
+ </section>`
4935
+ });
4936
+ }
4937
+ function when2(entry, viewer, now) {
4938
+ return entry.next ? formatInstant(entry.next.dueAt, viewer.timeZone, now) : "nothing due";
4939
+ }
4940
+ function cadence(entry, viewer, now) {
4941
+ const s = entry.task.schedule;
4942
+ if (!s || s.kind === "once")
4943
+ return "once";
4944
+ return describeSchedule(s, entry.from ?? viewer, viewer.timeZone, now);
4945
+ }
4946
+ function tasksPage(v, data, now) {
4947
+ const rows = data.entries.map((e) => html`<tr>
4948
+ <td><a class="rb-link" href="${v.base}/tasks/${e.task.id}">${e.task.title}</a></td>
4949
+ <td>${e.from ? name3(e.from) : "you"}</td>
4950
+ <td>${when2(e, v.user, now)}</td>
4951
+ <td>${cadence(e, v.user, now)}</td>
4952
+ </tr>`);
4953
+ const active = data.entries.length === 0 ? html`<p class="rb-muted">You have no active tasks. <code>/remind</code> in Discord sets one.</p>` : html`<div class="rb-table-scroll"><table class="rb-table">
4954
+ <thead><tr><th scope="col">Task</th><th scope="col">From</th><th scope="col">Next</th><th scope="col">Repeats</th></tr></thead>
4955
+ <tbody>${rows}</tbody>
4956
+ </table></div>`;
4957
+ const paused = data.paused.length === 0 ? null : html`<section>
4958
+ <h2>Paused</h2>
4959
+ <ul>${data.paused.map((p) => html`<li><a class="rb-link" href="${v.base}/tasks/${p.task.id}">${p.task.title}</a>${p.held.length > 0 ? html` -- I could not DM ${p.held.join(", ")}; <code>/task resume ${p.task.id}</code> goes on without them.` : null}</li>`)}</ul>
4960
+ </section>`;
4961
+ return framed(v, "My tasks", html`<section>
4962
+ <h1>My tasks</h1>
4963
+ <p class="rb-muted">Times are in ${v.user.timeZone}.</p>
4964
+ ${active}
4965
+ </section>
4966
+ ${paused}
4967
+ <p class="rb-muted tr-foot">${ADMIN_DISCLOSURE}</p>`);
4968
+ }
4969
+ function historyPage(v, h) {
4970
+ const runs = h.runs.length === 0 ? html`<p class="rb-muted">No runs yet.</p>` : html`<div class="rb-table-scroll"><table class="rb-table">
4971
+ <thead><tr><th scope="col">Due</th><th scope="col">Status</th><th scope="col">Answers</th></tr></thead>
4972
+ <tbody>${h.runs.map((r) => html`<tr>
4973
+ <td>${r.due}</td>
4974
+ <td>${r.status}${r.late ? " (late)" : ""}${r.error ? html`<br><span class="rb-muted">${r.error}</span>` : null}</td>
4975
+ <td>${r.answers.join("; ")}</td>
4976
+ </tr>`)}</tbody>
4977
+ </table></div>`;
4978
+ return framed(v, h.task.title, html`<section>
4979
+ <p><a class="rb-link" href="${v.base}/">My tasks</a></p>
4980
+ <h1>${h.task.title}</h1>
4981
+ <p>${h.task.type}, ${h.task.status}, ${h.cadence}. Next: ${h.next ?? "nothing scheduled"}.</p>
4982
+ </section>
4983
+ <section>
4984
+ <h2>Runs</h2>
4985
+ ${h.earlierRuns > 0 ? html`<p class="rb-muted">${h.earlierRuns} earlier run(s) not shown.</p>` : null}
4986
+ ${runs}
4987
+ </section>
4988
+ <section>
4989
+ <h2>Changes</h2>
4990
+ ${h.earlierChanges > 0 ? html`<p class="rb-muted">${h.earlierChanges} earlier change(s) not shown.</p>` : null}
4991
+ <ul>${h.changes.map((c) => html`<li>${c.at} ${c.kind}${c.detail !== null ? `: ${c.detail}` : ""}</li>`)}</ul>
4992
+ </section>`);
4993
+ }
4994
+ function settingsPage(v, form) {
4995
+ const hours = Array.from({ length: 24 }, (_, h) => h);
4996
+ return framed(v, "Settings", html`<section>
4997
+ <h1>Settings</h1>
4998
+ ${form.saved ? html`<div class="rb-alert rb-alert--success" role="status"><p class="rb-alert__title">Saved.</p></div>` : null}
4999
+ ${form.error ? html`<div class="rb-alert rb-alert--danger" role="alert"><p class="rb-alert__title">Not saved</p><p>${form.error}</p></div>` : null}
5000
+ <form method="post" action="${v.base}/settings" class="tr-stack">
5001
+ <input type="hidden" name="csrf" value="${v.csrf}">
5002
+ <div class="rb-field">
5003
+ <label class="rb-label" for="hour">Preferred hour</label>
5004
+ <select class="rb-select" id="hour" name="hour">${hours.map((h) => html`<option value="${h}"${String(h) === form.hour ? html` selected` : null}>${String(h).padStart(2, "0")}:00</option>`)}</select>
5005
+ <p class="rb-field__help">A reminder without a time of day arrives at this hour.</p>
5006
+ </div>
5007
+ <div class="rb-field">
5008
+ <label class="rb-label" for="zone">Time zone</label>
5009
+ <input class="rb-input" id="zone" name="zone" value="${form.zone}" maxlength="64" required${form.error ? html` aria-invalid="true"` : null}>
5010
+ <p class="rb-field__help">An IANA name, such as America/New_York or Europe/London.</p>
5011
+ </div>
5012
+ <div><button class="rb-btn rb-btn--primary" type="submit">Save</button></div>
5013
+ </form>
5014
+ </section>`);
5015
+ }
5016
+ function notFoundPage(base, v) {
5017
+ const body = html`<section><h1>Not found</h1><p>There is nothing here, or nothing you can see.</p><p><a class="rb-link" href="${base}/">My tasks</a></p></section>`;
5018
+ return v ? framed(v, "Not found", body) : page({ base, title: "Not found", body });
5019
+ }
5020
+ function errorPage(base, title, message) {
5021
+ return page({ base, title, body: html`<section class="rb-card"><h1>${title}</h1><p>${message}</p></section>` });
5022
+ }
5023
+
5024
+ // plugins/tracker/src/web/sessions.ts
5025
+ var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
5026
+
5027
+ class Sessions {
5028
+ db;
5029
+ constructor(db) {
5030
+ this.db = db;
5031
+ }
5032
+ create(userId, now, memberCheckedAt = null) {
5033
+ const id = randomToken();
5034
+ const at = now.toISOString();
5035
+ const session = { userId, csrf: randomToken(), expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString(), memberCheckedAt };
5036
+ this.db.query("DELETE FROM web_sessions WHERE expires_at <= ?").run(at);
5037
+ this.db.query("INSERT INTO web_sessions (id_hash, user_id, csrf, created_at, expires_at, member_checked_at) VALUES (?, ?, ?, ?, ?, ?)").run(sha256(id), userId, session.csrf, at, session.expiresAt, memberCheckedAt);
5038
+ return { id, session };
5039
+ }
5040
+ find(id, now) {
5041
+ if (!isTokenShape(id))
5042
+ return null;
5043
+ const row = this.db.query("SELECT user_id, csrf, expires_at, member_checked_at FROM web_sessions WHERE id_hash = ? AND expires_at > ?").get(sha256(id), now.toISOString());
5044
+ return row ? { userId: row.user_id, csrf: row.csrf, expiresAt: row.expires_at, memberCheckedAt: row.member_checked_at } : null;
5045
+ }
5046
+ delete(id) {
5047
+ if (isTokenShape(id))
5048
+ this.db.query("DELETE FROM web_sessions WHERE id_hash = ?").run(sha256(id));
5049
+ }
5050
+ confirmMember(userId, at) {
5051
+ this.db.query("UPDATE web_sessions SET member_checked_at = ? WHERE user_id = ?").run(at, userId);
5052
+ }
5053
+ deleteForUser(userId) {
5054
+ this.db.query("DELETE FROM web_sessions WHERE user_id = ?").run(userId);
5055
+ }
5056
+ }
5057
+
5058
+ // plugins/tracker/src/web/app.ts
5059
+ var MEMBER_RECHECK_MS = 15 * 60 * 1000;
5060
+ var MEMBER_GRACE_MS = 24 * 60 * 60 * 1000;
5061
+ var MEMBER_CHECK_TIMEOUT_MS = 3000;
5062
+ var MEMBER_RETRY_MS = 60 * 1000;
5063
+ var LEFT_SERVER = `You are signed out. ${NOT_MEMBER}`;
5064
+ var RECHECK_FAILED = "You are signed out: I could not check that you are still a member of this tracker's server.";
5065
+ var MAX_FORM_BYTES = 8 * 1024;
5066
+ var TASK_PATH = /^\/tasks\/([^/]+)$/;
5067
+ function plain(status, text, extra = {}) {
5068
+ return new Response(text, {
5069
+ status,
5070
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...extra }
5071
+ });
5072
+ }
5073
+ async function lookupWithin(lookup, ms) {
5074
+ let timer;
5075
+ const late = new Promise((resolve) => {
5076
+ timer = setTimeout(() => resolve("unknown"), ms);
5077
+ });
5078
+ try {
5079
+ return await Promise.race([lookup().catch(() => "unknown"), late]);
5080
+ } finally {
5081
+ clearTimeout(timer);
5082
+ }
5083
+ }
5084
+ function createWebHandler(w) {
5085
+ const base = `/${w.name}`;
5086
+ const SESSION_COOKIE = `__Secure-${w.name}-session`;
5087
+ const LOGIN_COOKIE = `__Secure-${w.name}-login`;
5088
+ const clear = (name) => cookie(name, "", { base, maxAgeSeconds: 0, sameSite: "Lax" });
5089
+ const inFlight = new Map;
5090
+ const failedAt = new Map;
5091
+ async function authenticate(d, request) {
5092
+ const id = readCookie(request, SESSION_COOKIE);
5093
+ if (!id)
5094
+ return { kind: "none" };
5095
+ const session = d.sessions.find(id, d.clock.now());
5096
+ if (!session)
5097
+ return { kind: "stale" };
5098
+ const user = await d.store.getUser(session.userId);
5099
+ if (!user || !d.admissions.isRegistered(user.id)) {
5100
+ d.sessions.deleteForUser(session.userId);
5101
+ return { kind: "stale" };
5102
+ }
5103
+ const refused = await recheckMembership(d, session, user);
5104
+ if (refused)
5105
+ return refused;
5106
+ const current = d.sessions.find(id, d.clock.now());
5107
+ if (!current)
5108
+ return { kind: "stale" };
5109
+ return { kind: "ok", id, session: current, user };
5110
+ }
5111
+ function lookupShared(userId, discordId) {
5112
+ let pending = inFlight.get(userId);
5113
+ if (!pending) {
5114
+ const started = w.membership(discordId).catch(() => "unknown");
5115
+ pending = started;
5116
+ inFlight.set(userId, started);
5117
+ started.finally(() => {
5118
+ if (inFlight.get(userId) === started)
5119
+ inFlight.delete(userId);
5120
+ });
5121
+ }
5122
+ const shared = pending;
5123
+ return lookupWithin(() => shared, MEMBER_CHECK_TIMEOUT_MS);
5124
+ }
5125
+ async function recheckMembership(d, session, user) {
5126
+ if (w.guildId === null)
5127
+ return null;
5128
+ const now = d.clock.now().getTime();
5129
+ const age = session.memberCheckedAt ? now - Date.parse(session.memberCheckedAt) : Number.POSITIVE_INFINITY;
5130
+ const fresh = (limit) => age >= 0 && age < limit;
5131
+ if (fresh(MEMBER_RECHECK_MS))
5132
+ return null;
5133
+ const lastFailed = failedAt.get(user.id);
5134
+ const backingOff = lastFailed !== undefined && now - lastFailed >= 0 && now - lastFailed < MEMBER_RETRY_MS;
5135
+ let found = "unknown";
5136
+ if (!backingOff && user.discordId) {
5137
+ found = await lookupShared(user.id, user.discordId);
5138
+ if (found === "member" || found === "not-member")
5139
+ failedAt.delete(user.id);
5140
+ else
5141
+ failedAt.set(user.id, d.clock.now().getTime());
5142
+ }
5143
+ if (found === "not-member") {
5144
+ d.sessions.deleteForUser(user.id);
5145
+ return { kind: "signed-out", note: LEFT_SERVER };
5146
+ }
5147
+ if (found === "member") {
5148
+ d.sessions.confirmMember(user.id, d.clock.now().toISOString());
5149
+ return null;
5150
+ }
5151
+ if (fresh(MEMBER_GRACE_MS))
5152
+ return null;
5153
+ d.sessions.deleteForUser(user.id);
5154
+ return { kind: "signed-out", note: RECHECK_FAILED };
5155
+ }
5156
+ async function readForm(request) {
5157
+ const type = request.headers.get("content-type") ?? "";
5158
+ if (!type.toLowerCase().startsWith("application/x-www-form-urlencoded"))
5159
+ return null;
5160
+ const body = await request.text();
5161
+ if (Buffer.byteLength(body) > MAX_FORM_BYTES)
5162
+ return null;
5163
+ return new URLSearchParams(body);
5164
+ }
5165
+ function forbidden() {
5166
+ return htmlResponse(errorPage(base, "Refused", "That form was stale or came from somewhere else. Go back, reload the page and try again."), 403);
5167
+ }
5168
+ async function loginGet(d, url) {
5169
+ const token = url.searchParams.get("t") ?? "";
5170
+ const valid = d.logins.peek(token, d.clock.now());
5171
+ if (!valid)
5172
+ return htmlResponse(loginPage(base, { token: "", loginCsrf: "", valid: false }));
5173
+ const loginCsrf = randomToken();
5174
+ return htmlResponse(loginPage(base, { token, loginCsrf, valid: true }), 200, {
5175
+ "Set-Cookie": cookie(LOGIN_COOKIE, loginCsrf, { base, maxAgeSeconds: LINK_TTL_MS / 1000, sameSite: "Strict" })
5176
+ });
5177
+ }
5178
+ async function loginPost(d, request) {
5179
+ const form = await readForm(request);
5180
+ if (!form)
5181
+ return plain(400, "Bad request");
5182
+ const expected = readCookie(request, LOGIN_COOKIE);
5183
+ const sent = form.get("csrf") ?? "";
5184
+ if (!expected || !safeEqual(expected, sent))
5185
+ return forbidden();
5186
+ const now = d.clock.now();
5187
+ const link = d.logins.consume(form.get("t") ?? "", now);
5188
+ const user = link ? await d.store.getUser(link.userId) : null;
5189
+ if (!user || !d.admissions.isRegistered(user.id)) {
5190
+ return htmlResponse(signInHelpPage(base, "That sign-in link has expired or was already used."), 400, { "Set-Cookie": clear(LOGIN_COOKIE) });
5191
+ }
5192
+ const { id } = d.sessions.create(user.id, now, link?.memberCheckedAt ?? null);
5193
+ return redirect(`${base}/`, [cookie(SESSION_COOKIE, id, { base, maxAgeSeconds: SESSION_TTL_MS / 1000, sameSite: "Lax" }), clear(LOGIN_COOKIE)]);
5194
+ }
5195
+ async function settingsPost(d, v, form) {
5196
+ const hour = (form.get("hour") ?? "").trim();
5197
+ const zone = (form.get("zone") ?? "").trim();
5198
+ let error = null;
5199
+ if (!/^([0-9]|1[0-9]|2[0-3])$/.test(hour))
5200
+ error = "The preferred hour must be a whole hour from 0 to 23.";
5201
+ else if (zone === "" || zone.length > 64)
5202
+ error = "Give a time zone, such as America/New_York.";
5203
+ else {
5204
+ error = await w.queue(async () => {
5205
+ const fresh = await d.store.getUser(v.user.id);
5206
+ return fresh ? saveSettings(d, fresh, { hour: Number(hour), zone }) : "You are no longer on this tracker's list.";
5207
+ });
5208
+ }
5209
+ if (error !== null)
5210
+ return htmlResponse(settingsPage(v, { hour, zone, error }), 400);
5211
+ return redirect(`${base}/settings?saved=1`);
5212
+ }
5213
+ return async (request, info) => {
5214
+ const method = request.method;
5215
+ if (method !== "GET" && method !== "POST")
5216
+ return plain(405, "Method not allowed", { Allow: "GET, POST" });
5217
+ if (w.origin === null)
5218
+ return plain(404, "Not found");
5219
+ const d = w.deps();
5220
+ if (!d)
5221
+ return plain(503, "The tracker is starting up; try again in a minute.");
5222
+ if (method === "POST") {
5223
+ const origin = request.headers.get("origin");
5224
+ if (origin !== null && origin !== w.origin)
5225
+ return forbidden();
5226
+ }
5227
+ const path = info.path;
5228
+ const url = new URL(request.url);
5229
+ const only = (allowed) => method === allowed ? null : plain(405, "Method not allowed", { Allow: allowed });
5230
+ if (path === STYLESHEET_PATH) {
5231
+ return only("GET") ?? new Response(STYLESHEET, {
5232
+ headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "public, max-age=31536000, immutable", "X-Content-Type-Options": "nosniff" }
5233
+ });
5234
+ }
5235
+ if (path === "/signin")
5236
+ return only("GET") ?? htmlResponse(signInHelpPage(base, url.searchParams.get("out") === "1" ? "You are signed out." : undefined));
5237
+ if (path === "/login")
5238
+ return method === "GET" ? loginGet(d, url) : loginPost(d, request);
5239
+ const task = TASK_PATH.exec(path);
5240
+ const route = path === "/" ? "tasks" : path === "/settings" ? "settings" : path === "/logout" ? "logout" : task ? "history" : null;
5241
+ if (route === null)
5242
+ return htmlResponse(notFoundPage(base), 404);
5243
+ const wrong = route === "logout" ? only("POST") : route === "settings" ? null : only("GET");
5244
+ if (wrong)
5245
+ return wrong;
5246
+ const auth = await authenticate(d, request);
5247
+ if (auth.kind === "signed-out")
5248
+ return htmlResponse(signInHelpPage(base, auth.note), 403, { "Set-Cookie": clear(SESSION_COOKIE) });
5249
+ if (auth.kind !== "ok")
5250
+ return redirect(`${base}/signin`, auth.kind === "stale" ? [clear(SESSION_COOKIE)] : []);
5251
+ const v = { base, user: auth.user, csrf: auth.session.csrf };
5252
+ if (method === "POST") {
5253
+ const form = await readForm(request);
5254
+ if (!form)
5255
+ return plain(400, "Bad request");
5256
+ if (!safeEqual(auth.session.csrf, form.get("csrf") ?? ""))
5257
+ return forbidden();
5258
+ if (route === "logout") {
5259
+ d.sessions.delete(auth.id);
5260
+ return redirect(`${base}/signin?out=1`, [clear(SESSION_COOKIE)]);
5261
+ }
5262
+ return settingsPost(d, v, form);
5263
+ }
5264
+ const now = d.clock.now();
5265
+ switch (route) {
5266
+ case "tasks":
5267
+ return htmlResponse(tasksPage(v, await loadTaskList(d, auth.user), now));
5268
+ case "history": {
5269
+ let id;
5270
+ try {
5271
+ id = decodeURIComponent(task?.[1] ?? "");
5272
+ } catch {
5273
+ return htmlResponse(notFoundPage(base, v), 404);
5274
+ }
5275
+ const view = await loadHistory(d, auth.user, id);
5276
+ return view ? htmlResponse(historyPage(v, view)) : htmlResponse(notFoundPage(base, v), 404);
5277
+ }
5278
+ default:
5279
+ return htmlResponse(settingsPage(v, { hour: String(auth.user.preferredHour), zone: auth.user.timeZone, saved: url.searchParams.get("saved") === "1" }));
5280
+ }
5281
+ };
5282
+ }
5283
+
5284
+ // plugins/tracker/src/web/config.ts
5285
+ var WEB_URL_FORMAT = "^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?/?$";
5286
+ function parseWebUrl(raw) {
5287
+ if (raw === undefined || raw.trim() === "")
5288
+ return null;
5289
+ const value = raw.trim();
5290
+ if (!new RegExp(WEB_URL_FORMAT).test(value)) {
5291
+ throw new Error(`TRACKER_WEB_URL: "${value}" is not an https origin such as https://clerk.example.com (no path, query or credentials)`);
5292
+ }
5293
+ let url;
5294
+ try {
5295
+ url = new URL(value);
5296
+ } catch {
5297
+ throw new Error(`TRACKER_WEB_URL: "${value}" is not a URL`);
5298
+ }
5299
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
5300
+ throw new Error(`TRACKER_WEB_URL: "${value}" must be a bare https origin`);
5301
+ }
5302
+ return url.origin;
5303
+ }
5304
+
5305
+ // plugins/tracker/src/index.ts
5306
+ var DB_DIR = "tracker";
5307
+ var DB_FILE = "tracker.sqlite";
5308
+ var TRACKER_TYPES = Object.freeze({
5309
+ reminder,
5310
+ renewal
5311
+ });
5312
+ var NO_DM = {
5313
+ async sendDm() {
5314
+ throw new Error("this bot has no host.dm (rackbops-discord-bot#736)");
5315
+ }
5316
+ };
5317
+ function createPlugin(host, options = {}) {
5318
+ const adminIds = parseAdminIds(host.env.TRACKER_ADMIN_DISCORD_IDS);
5319
+ const guildId = parseGuildId(host.env.TRACKER_GUILD_ID);
5320
+ const webOrigin = parseWebUrl(host.env.TRACKER_WEB_URL);
5321
+ const clock = options.clock ?? { now: () => new Date };
5322
+ const types = options.types ?? TRACKER_TYPES;
5323
+ const health = { activatedAt: null, lastTickAt: null, blocked: null };
5324
+ let db = null;
5325
+ let store = null;
5326
+ let claims = null;
5327
+ let delivery = null;
5328
+ let deps = null;
5329
+ let warnedNoDm = false;
5330
+ const dm = host.dm?.bind(host);
5331
+ const queue = serial();
5332
+ const surface = createSurface({
5333
+ deps: () => deps,
5334
+ guildId,
5335
+ log: host.log,
5336
+ queue,
5337
+ web: webOrigin ? { origin: webOrigin, name: host.name, gated: guildId !== null } : null,
5338
+ ...options.membership ? { membership: options.membership } : {}
5339
+ });
5340
+ let discordClient = null;
5341
+ const capture = (interaction) => {
5342
+ const client = interaction.client;
5343
+ if (client)
5344
+ discordClient = client;
5345
+ };
5346
+ const clientMembership = async (discordId) => {
5347
+ if (!discordClient)
5348
+ return null;
5349
+ return lookupMembership({ guildId: null, user: { id: discordId }, client: discordClient }, guildId, discordId, host.log);
5350
+ };
5351
+ const web = createWebHandler({
5352
+ name: host.name,
5353
+ origin: webOrigin,
5354
+ deps: () => deps,
5355
+ queue,
5356
+ guildId,
5357
+ membership: options.webMembership ?? clientMembership
5358
+ });
5359
+ const interactions = surface.interactions;
5360
+ return {
5361
+ commands: surface.commands.map((c) => ({
5362
+ ...c,
5363
+ handle: (interaction) => {
5364
+ capture(interaction);
5365
+ return c.handle(interaction);
5366
+ }
5367
+ })),
5368
+ interactions: (interaction) => {
5369
+ capture(interaction);
5370
+ return interactions(interaction);
5371
+ },
3530
5372
  async activate() {
3531
5373
  let path = options.dbPath;
3532
5374
  if (path === undefined) {
@@ -3576,7 +5418,9 @@ function createPlugin(host, options = {}) {
3576
5418
  types,
3577
5419
  dm,
3578
5420
  log: host.log,
3579
- notifier: dm ? createDmNotifier({ store: openedStore, claims: openedClaims, dm, clock, log: host.log, health: delivery }) : NO_DM
5421
+ notifier: dm ? createDmNotifier({ store: openedStore, claims: openedClaims, dm, clock, log: host.log, health: delivery }) : NO_DM,
5422
+ logins: new LoginLinks(opened),
5423
+ sessions: new Sessions(opened)
3580
5424
  };
3581
5425
  if (guildId === null)
3582
5426
  host.log.warn("TRACKER_GUILD_ID is unset: no membership gate, only the admission list");
@@ -3619,7 +5463,7 @@ function createPlugin(host, options = {}) {
3619
5463
  if (info.path === "/healthz" && (request.method === "GET" || request.method === "HEAD")) {
3620
5464
  return healthResponse(decideHealth(health, clock.now()));
3621
5465
  }
3622
- return new Response("Not found", { status: 404 });
5466
+ return web(request, info);
3623
5467
  }
3624
5468
  };
3625
5469
  }