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/README.md +98 -0
- package/dist/content-processor.js +122 -22
- package/dist/controller/config-registry.js +277 -0
- package/dist/controller/events.js +29 -0
- package/dist/controller/index.js +139 -0
- package/dist/controller/job-runner.js +314 -0
- package/dist/controller/migrations.js +55 -0
- package/dist/controller/notifier.js +106 -0
- package/dist/controller/scheduler.js +54 -0
- package/dist/controller/server.js +288 -0
- package/dist/controller/store.js +186 -0
- package/dist/controller/types.js +25 -0
- package/dist/doc2vec.js +121 -28
- package/dist/logger.js +89 -0
- package/dist/ui/assets/index-BVg6n7Si.css +1 -0
- package/dist/ui/assets/index-D-QOw3rk.js +126 -0
- package/dist/ui/index.html +14 -0
- package/package.json +7 -2
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}}
|