doc2vec 2.10.5 → 2.11.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/dist/doc2vec.js CHANGED
@@ -176,36 +176,59 @@ class Doc2Vec {
176
176
  await this.markdownStore.init();
177
177
  }
178
178
  this.logger.section('PROCESSING SOURCES');
179
+ const runStats = [];
179
180
  for (const sourceConfig of this.config.sources) {
180
181
  const sourceLogger = this.logger.child(`source:${sourceConfig.product_name}`);
181
182
  sourceLogger.info(`Processing ${sourceConfig.type} source for ${sourceConfig.product_name}@${sourceConfig.version}`);
182
- if (sourceConfig.type === 'github') {
183
- await this.processGithubRepo(sourceConfig, sourceLogger);
184
- }
185
- else if (sourceConfig.type === 'website') {
186
- await this.processWebsite(sourceConfig, sourceLogger);
187
- }
188
- else if (sourceConfig.type === 'local_directory') {
189
- await this.processLocalDirectory(sourceConfig, sourceLogger);
190
- }
191
- else if (sourceConfig.type === 'code') {
192
- await this.processCodeSource(sourceConfig, sourceLogger);
193
- }
194
- else if (sourceConfig.type === 'zendesk') {
195
- await this.processZendesk(sourceConfig, sourceLogger);
196
- }
197
- else if (sourceConfig.type === 's3') {
198
- await this.processS3(sourceConfig, sourceLogger);
199
- }
200
- else {
201
- sourceLogger.error(`Unknown source type: ${sourceConfig.type}`);
183
+ const startTime = Date.now();
184
+ let ok = true;
185
+ let errorMessage;
186
+ try {
187
+ if (sourceConfig.type === 'github') {
188
+ await this.processGithubRepo(sourceConfig, sourceLogger);
189
+ }
190
+ else if (sourceConfig.type === 'website') {
191
+ await this.processWebsite(sourceConfig, sourceLogger);
192
+ }
193
+ else if (sourceConfig.type === 'local_directory') {
194
+ await this.processLocalDirectory(sourceConfig, sourceLogger);
195
+ }
196
+ else if (sourceConfig.type === 'code') {
197
+ await this.processCodeSource(sourceConfig, sourceLogger);
198
+ }
199
+ else if (sourceConfig.type === 'zendesk') {
200
+ await this.processZendesk(sourceConfig, sourceLogger);
201
+ }
202
+ else if (sourceConfig.type === 's3') {
203
+ await this.processS3(sourceConfig, sourceLogger);
204
+ }
205
+ else {
206
+ ok = false;
207
+ errorMessage = `Unknown source type: ${sourceConfig.type}`;
208
+ sourceLogger.error(errorMessage);
209
+ }
202
210
  }
211
+ catch (error) {
212
+ ok = false;
213
+ errorMessage = error instanceof Error ? error.message : String(error);
214
+ sourceLogger.error(`Failed to process ${sourceConfig.type} source for ${sourceConfig.product_name}:`, error);
215
+ }
216
+ runStats.push({
217
+ product_name: sourceConfig.product_name,
218
+ type: sourceConfig.type,
219
+ version: sourceConfig.version,
220
+ duration_ms: Date.now() - startTime,
221
+ ok,
222
+ ...(errorMessage && { error: errorMessage })
223
+ });
203
224
  }
204
225
  // Close the Postgres markdown store connection pool
205
226
  if (this.markdownStore) {
206
227
  await this.markdownStore.close();
207
228
  }
208
229
  this.logger.section('PROCESSING COMPLETE');
230
+ this.logger.event('run-summary', { sources: runStats });
231
+ return runStats;
209
232
  }
210
233
  async fetchAndProcessGitHubIssues(repo, sourceConfig, dbConnection, logger) {
211
234
  const [owner, repoName] = repo.split('/');
@@ -287,7 +310,6 @@ class Doc2Vec {
287
310
  let issues = [];
288
311
  let page = 1;
289
312
  const perPage = 100;
290
- const sinceTimestamp = new Date(sinceDate);
291
313
  while (true) {
292
314
  // Log progress every 10 pages to reduce noise
293
315
  if (page === 1 || page % 10 === 0) {
@@ -301,9 +323,14 @@ class Doc2Vec {
301
323
  });
302
324
  if (data.length === 0)
303
325
  break;
304
- const filtered = data.filter((issue) => new Date(issue.created_at) >= sinceTimestamp);
305
- issues = issues.concat(filtered);
306
- if (filtered.length < data.length)
326
+ // The `since` param already scopes results to issues whose
327
+ // updated_at is after sinceDate — i.e. anything created, closed,
328
+ // reopened, edited, or newly commented since the last run. Do NOT
329
+ // re-filter by created_at: an old issue that was just closed or
330
+ // received a new comment has an old created_at and would be
331
+ // dropped, leaving its status and comments permanently stale.
332
+ issues = issues.concat(data);
333
+ if (data.length < perPage)
307
334
  break;
308
335
  page++;
309
336
  }
@@ -347,6 +374,18 @@ class Doc2Vec {
347
374
  };
348
375
  const chunks = await this.contentProcessor.chunkMarkdown(markdown, issueConfig, url);
349
376
  logger.info(`Issue #${issueNumber}: Created ${chunks.length} chunks`);
377
+ // Purge the issue's existing chunks before inserting the fresh set.
378
+ // chunk_id is a content hash, so when an issue changes (closed/reopened,
379
+ // edited, new comments) the regenerated chunks get new ids and the old
380
+ // ones would otherwise linger — leaving stale state ("open") and missing
381
+ // the latest comments in search results. All chunks for an issue share
382
+ // its unique url, so delete-by-url reconciles them exactly.
383
+ if (dbConnection.type === 'sqlite') {
384
+ database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
385
+ }
386
+ else if (dbConnection.type === 'qdrant') {
387
+ await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
388
+ }
350
389
  // Process and store each chunk immediately
351
390
  for (const chunk of chunks) {
352
391
  const chunkHash = utils_1.Utils.generateHash(chunk.content);
@@ -592,7 +631,11 @@ class Doc2Vec {
592
631
  this.brokenLinksByWebsite[baseUrl] = unique;
593
632
  }
594
633
  writeBrokenLinksReport() {
595
- const reportPath = path.join(this.configDir, '404.yaml');
634
+ // The default (next to the config file) breaks when the config lives on
635
+ // a read-only mount (e.g. a Kubernetes ConfigMap in controller mode) —
636
+ // DOC2VEC_REPORT_DIR points the report somewhere writable instead.
637
+ const reportDir = process.env.DOC2VEC_REPORT_DIR || this.configDir;
638
+ const reportPath = path.join(reportDir, '404.yaml');
596
639
  const orderedEntries = Object.entries(this.brokenLinksByWebsite)
597
640
  .sort(([a], [b]) => a.localeCompare(b));
598
641
  const reportPayload = orderedEntries.map(([website, links]) => ({
@@ -1635,14 +1678,64 @@ exports.Doc2Vec = Doc2Vec;
1635
1678
  Doc2Vec.MAX_EMBEDDING_TOKENS = 8191;
1636
1679
  Doc2Vec.CHARS_PER_TOKEN = 4;
1637
1680
  Doc2Vec.MAX_EMBEDDING_CHARS = Doc2Vec.MAX_EMBEDDING_TOKENS * Doc2Vec.CHARS_PER_TOKEN;
1638
- if (require.main === module) {
1639
- const configPath = process.argv[2] || 'config.yaml';
1681
+ function runOneShot(configPath) {
1640
1682
  if (!fs.existsSync(configPath)) {
1641
1683
  console.error('Please provide a valid path to a YAML config file.');
1642
1684
  process.exit(1);
1643
1685
  }
1644
1686
  const doc2Vec = new Doc2Vec(configPath);
1645
1687
  doc2Vec.run()
1646
- .then(() => process.exit(0))
1688
+ .then((stats) => process.exit(stats.some(s => !s.ok) ? 1 : 0))
1647
1689
  .catch((err) => { console.error(err); process.exit(1); });
1648
1690
  }
1691
+ if (require.main === module) {
1692
+ const { Command } = require('commander');
1693
+ const program = new Command();
1694
+ program
1695
+ .name('doc2vec')
1696
+ .description('Crawl documentation sources and store embeddings in vector databases')
1697
+ // Legacy invocation: `doc2vec [config.yaml]` runs a one-shot sync
1698
+ .argument('[config]', 'path to a YAML config file', 'config.yaml')
1699
+ .action((configPath) => runOneShot(configPath));
1700
+ program
1701
+ .command('run <config>')
1702
+ .description('Run a one-shot sync for a config file (what the controller spawns)')
1703
+ .action((configPath) => runOneShot(configPath));
1704
+ program
1705
+ .command('controller [configs...]')
1706
+ .description('Run as a long-lived controller: schedule sync jobs per config file, persist runs in Postgres, serve a web UI')
1707
+ .option('--database-url <url>', 'Postgres connection string (or DATABASE_URL env var)')
1708
+ .option('--port <port>', 'HTTP port for the API/UI (or PORT env var)', (v) => parseInt(v, 10))
1709
+ .option('--read-write', 'allow creating/editing configs from the UI (default: read-only)', false)
1710
+ .option('--config-dir <dir>', 'directory where UI-created configs are written (required with --read-write)')
1711
+ .option('--max-parallel <n>', 'maximum number of sync jobs running at once', (v) => parseInt(v, 10), 1)
1712
+ .option('--reload-interval <seconds>', 'how often to re-poll config files for changes', (v) => parseInt(v, 10), 30)
1713
+ .option('--log-retention-days <days>', 'delete run logs older than this many days', (v) => parseInt(v, 10), 14)
1714
+ .option('--slack-webhook-url <url>', 'Slack incoming webhook for run notifications (or SLACK_WEBHOOK_URL env var)')
1715
+ .option('--slack-notify <mode>', "which runs to notify about: 'all' or 'failures'", 'all')
1716
+ .option('--public-url <url>', 'externally reachable base URL, used for links in notifications (or PUBLIC_URL env var)')
1717
+ .action(async (configs, options) => {
1718
+ // Lazy-require so the one-shot path doesn't load express/pg
1719
+ const { startController } = require('./controller');
1720
+ try {
1721
+ await startController({
1722
+ configArgs: configs,
1723
+ databaseUrl: options.databaseUrl || process.env.DATABASE_URL,
1724
+ port: options.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 8080),
1725
+ readWrite: options.readWrite,
1726
+ configDir: options.configDir,
1727
+ maxParallel: options.maxParallel,
1728
+ reloadIntervalSec: options.reloadInterval,
1729
+ logRetentionDays: options.logRetentionDays,
1730
+ slackWebhookUrl: options.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL,
1731
+ slackNotify: options.slackNotify === 'failures' ? 'failures' : 'all',
1732
+ publicUrl: options.publicUrl || process.env.PUBLIC_URL,
1733
+ });
1734
+ }
1735
+ catch (err) {
1736
+ console.error(err instanceof Error ? err.message : err);
1737
+ process.exit(1);
1738
+ }
1739
+ });
1740
+ program.parse();
1741
+ }
package/dist/logger.js CHANGED
@@ -16,6 +16,35 @@ var LogLevel;
16
16
  LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
17
17
  LogLevel[LogLevel["NONE"] = 100] = "NONE";
18
18
  })(LogLevel || (exports.LogLevel = LogLevel = {}));
19
+ /**
20
+ * When DOC2VEC_STRUCTURED_LOGS=1 (set by the controller when spawning sync jobs),
21
+ * every log line is emitted as a single JSON object so a parent process can parse
22
+ * the stream reliably. Human-formatted output is suppressed in this mode.
23
+ */
24
+ const STRUCTURED = process.env.DOC2VEC_STRUCTURED_LOGS === '1';
25
+ function emitStructured(payload, useStderr = false) {
26
+ const line = JSON.stringify(payload);
27
+ if (useStderr) {
28
+ process.stderr.write(line + '\n');
29
+ }
30
+ else {
31
+ process.stdout.write(line + '\n');
32
+ }
33
+ }
34
+ function stringifyArg(arg) {
35
+ if (arg instanceof Error) {
36
+ return `${arg.message}${arg.stack ? `\n${arg.stack}` : ''}`;
37
+ }
38
+ if (typeof arg === 'object' && arg !== null) {
39
+ try {
40
+ return JSON.stringify(arg);
41
+ }
42
+ catch (e) {
43
+ return '[Unserializable Object]';
44
+ }
45
+ }
46
+ return String(arg);
47
+ }
19
48
  /**
20
49
  * Basic color functions that don't rely on external packages
21
50
  */
@@ -117,8 +146,23 @@ class Logger {
117
146
  * @param message Message to log
118
147
  * @param args Additional arguments
119
148
  */
149
+ emitJson(level, message, args, useStderr = false) {
150
+ const msg = args.length > 0
151
+ ? `${message} ${args.map(stringifyArg).join(' ')}`
152
+ : message;
153
+ emitStructured({
154
+ ts: new Date().toISOString(),
155
+ level,
156
+ module: this.moduleName,
157
+ msg
158
+ }, useStderr);
159
+ }
120
160
  debug(message, ...args) {
121
161
  if (this.config.level <= LogLevel.DEBUG) {
162
+ if (STRUCTURED) {
163
+ this.emitJson('debug', message, args);
164
+ return;
165
+ }
122
166
  const formattedMessage = this.formatMessage('DEBUG', message, args);
123
167
  console.log(this.colorize(LogLevel.DEBUG, formattedMessage));
124
168
  }
@@ -131,6 +175,10 @@ class Logger {
131
175
  */
132
176
  info(message, ...args) {
133
177
  if (this.config.level <= LogLevel.INFO) {
178
+ if (STRUCTURED) {
179
+ this.emitJson('info', message, args);
180
+ return;
181
+ }
134
182
  const formattedMessage = this.formatMessage('INFO', message, args);
135
183
  console.log(this.colorize(LogLevel.INFO, formattedMessage));
136
184
  }
@@ -143,6 +191,10 @@ class Logger {
143
191
  */
144
192
  warn(message, ...args) {
145
193
  if (this.config.level <= LogLevel.WARN) {
194
+ if (STRUCTURED) {
195
+ this.emitJson('warn', message, args, true);
196
+ return;
197
+ }
146
198
  const formattedMessage = this.formatMessage('WARN', message, args);
147
199
  console.warn(this.colorize(LogLevel.WARN, formattedMessage));
148
200
  }
@@ -155,10 +207,28 @@ class Logger {
155
207
  */
156
208
  error(message, ...args) {
157
209
  if (this.config.level <= LogLevel.ERROR) {
210
+ if (STRUCTURED) {
211
+ this.emitJson('error', message, args, true);
212
+ return;
213
+ }
158
214
  const formattedMessage = this.formatMessage('ERROR', message, args);
159
215
  console.error(this.colorize(LogLevel.ERROR, formattedMessage));
160
216
  }
161
217
  }
218
+ /**
219
+ * Emit a structured event (e.g. run-summary). In structured mode this prints a
220
+ * machine-parseable JSON line; otherwise it logs the payload at info level.
221
+ *
222
+ * @param name Event name
223
+ * @param data Event payload
224
+ */
225
+ event(name, data = {}) {
226
+ if (STRUCTURED) {
227
+ emitStructured({ event: name, ts: new Date().toISOString(), ...data });
228
+ return;
229
+ }
230
+ this.info(`[event:${name}]`, data);
231
+ }
162
232
  /**
163
233
  * Create a child logger with a more specific module name
164
234
  *
@@ -176,6 +246,10 @@ class Logger {
176
246
  */
177
247
  section(title) {
178
248
  if (this.config.level <= LogLevel.INFO) {
249
+ if (STRUCTURED) {
250
+ emitStructured({ event: 'section', ts: new Date().toISOString(), module: this.moduleName, title });
251
+ return this;
252
+ }
179
253
  const separator = '='.repeat(Math.max(80 - title.length - 4, 10));
180
254
  const message = `${separator} ${title} ${separator}`;
181
255
  console.log(this.colorize(LogLevel.INFO, message));
@@ -192,10 +266,21 @@ class Logger {
192
266
  progress(title, total) {
193
267
  let current = 0;
194
268
  const startTime = Date.now();
269
+ // In structured mode, throttle progress lines to at most one per second so a
270
+ // large crawl doesn't flood the parent process / database with log rows.
271
+ let lastStructuredEmit = 0;
195
272
  const update = (increment = 1, message) => {
196
273
  if (this.config.level > LogLevel.INFO)
197
274
  return;
198
275
  current += increment;
276
+ if (STRUCTURED) {
277
+ const now = Date.now();
278
+ if (now - lastStructuredEmit >= 1000 || current === total) {
279
+ lastStructuredEmit = now;
280
+ emitStructured({ event: 'progress', ts: new Date().toISOString(), module: this.moduleName, title, current, total, message });
281
+ }
282
+ return;
283
+ }
199
284
  const percentage = Math.min(Math.floor((current / total) * 100), 100);
200
285
  const elapsed = (Date.now() - startTime) / 1000;
201
286
  let rate = current / elapsed;
@@ -213,6 +298,10 @@ class Logger {
213
298
  return;
214
299
  const elapsed = (Date.now() - startTime) / 1000;
215
300
  const rate = total / elapsed;
301
+ if (STRUCTURED) {
302
+ emitStructured({ event: 'progress', ts: new Date().toISOString(), module: this.moduleName, title, current: total, total, message: `${message} in ${elapsed.toFixed(2)}s`, done: true });
303
+ return;
304
+ }
216
305
  console.log(this.colorize(LogLevel.INFO, this.formatMessage('INFO', `${title}: ${this.createProgressBar(100)} 100% (${total}/${total}) - ${message} in ${elapsed.toFixed(2)}s (${rate.toFixed(2)} items/sec)`)));
217
306
  };
218
307
  return { update, complete };
@@ -0,0 +1 @@
1
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:system-ui, -apple-system, "Segoe UI", sans-serif;--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.max-h-\[560px\]{max-height:560px}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing) * 2)}.w-4{width:calc(var(--spacing) * 4)}.w-10{width:calc(var(--spacing) * 10)}.w-44{width:calc(var(--spacing) * 44)}.w-72{width:calc(var(--spacing) * 72)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-md{max-width:var(--container-md)}.min-w-48{min-width:calc(var(--spacing) * 48)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-accent,.border-accent\/30{border-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/30{border-color:color-mix(in oklab,var(--accent) 30%,transparent)}}.border-accent\/40{border-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/40{border-color:color-mix(in oklab,var(--accent) 40%,transparent)}}.border-critical\/40{border-color:var(--status-critical)}@supports (color:color-mix(in lab,red,red)){.border-critical\/40{border-color:color-mix(in oklab,var(--status-critical) 40%,transparent)}}.border-critical\/50{border-color:var(--status-critical)}@supports (color:color-mix(in lab,red,red)){.border-critical\/50{border-color:color-mix(in oklab,var(--status-critical) 50%,transparent)}}.border-current{border-color:currentColor}.border-edge,.border-edge\/60{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-edge\/60{border-color:color-mix(in oklab,var(--border) 60%,transparent)}}.border-good\/40{border-color:var(--status-good)}@supports (color:color-mix(in lab,red,red)){.border-good\/40{border-color:color-mix(in oklab,var(--status-good) 40%,transparent)}}.border-transparent{border-color:#0000}.border-warning\/40{border-color:var(--status-warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--status-warning) 40%,transparent)}}.border-warning\/50{border-color:var(--status-warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/50{border-color:color-mix(in oklab,var(--status-warning) 50%,transparent)}}.bg-accent{background-color:var(--accent)}.bg-page{background-color:var(--page)}.bg-surface{background-color:var(--surface-1)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--accent)}.text-critical{color:var(--status-critical)}.text-good-text{color:var(--good-text)}.text-ink{color:var(--text-primary)}.text-ink-muted{color:var(--text-muted)}.text-ink-secondary{color:var(--text-secondary)}.text-warning{color:var(--status-warning)}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.underline-offset-2{text-underline-offset:2px}.accent-\(--accent\){accent-color:var(--accent)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-ink-muted\/60::placeholder{color:var(--text-muted)}@supports (color:color-mix(in lab,red,red)){.placeholder\:text-ink-muted\/60::placeholder{color:color-mix(in oklab,var(--text-muted) 60%,transparent)}}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media(hover:hover){.hover\:border-accent:hover,.hover\:border-accent\/50:hover{border-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:border-accent\/50:hover{border-color:color-mix(in oklab,var(--accent) 50%,transparent)}}.hover\:bg-critical\/10:hover{background-color:var(--status-critical)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-critical\/10:hover{background-color:color-mix(in oklab,var(--status-critical) 10%,transparent)}}.hover\:bg-ink\/\[0\.03\]:hover{background-color:var(--text-primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-ink\/\[0\.03\]:hover{background-color:color-mix(in oklab,var(--text-primary) 3%,transparent)}}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-critical:hover{color:var(--status-critical)}.hover\:text-ink-secondary:hover{color:var(--text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-accent:focus{border-color:var(--accent)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}@media(min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}:root{--page:#f9f9f7;--surface-1:#fcfcfb;--text-primary:#0b0b0b;--text-secondary:#52514e;--text-muted:#898781;--gridline:#e1e0d9;--baseline:#c3c2b7;--border:#0b0b0b1a;--series-1:#2a78d6;--status-good:#0ca30c;--status-warning:#fab219;--status-critical:#d03b3b;--good-text:#006300;--accent:#2a78d6}@media(prefers-color-scheme:dark){:root{--page:#0d0d0d;--surface-1:#1a1a19;--text-primary:#fff;--text-secondary:#c3c2b7;--text-muted:#898781;--gridline:#2c2c2a;--baseline:#383835;--border:#ffffff1a;--series-1:#3987e5;--status-good:#0ca30c;--status-warning:#fab219;--status-critical:#d03b3b;--good-text:#0ca30c;--accent:#3987e5}}html{background:var(--page);color:var(--text-primary);font-family:system-ui,-apple-system,Segoe UI,sans-serif}.log-scroll::-webkit-scrollbar{width:10px;height:10px}.log-scroll::-webkit-scrollbar-thumb{background:var(--baseline);border-radius:5px}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes pulse{50%{opacity:.5}}