@houtini/better-search-console 1.0.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 (71) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +334 -0
  3. package/better-search-console.jpg +0 -0
  4. package/dist/core/DataRetention.d.ts +49 -0
  5. package/dist/core/DataRetention.d.ts.map +1 -0
  6. package/dist/core/DataRetention.js +165 -0
  7. package/dist/core/DataRetention.js.map +1 -0
  8. package/dist/core/DataSync.d.ts +24 -0
  9. package/dist/core/DataSync.d.ts.map +1 -0
  10. package/dist/core/DataSync.js +247 -0
  11. package/dist/core/DataSync.js.map +1 -0
  12. package/dist/core/Database.d.ts +29 -0
  13. package/dist/core/Database.d.ts.map +1 -0
  14. package/dist/core/Database.js +205 -0
  15. package/dist/core/Database.js.map +1 -0
  16. package/dist/core/GscClient.d.ts +23 -0
  17. package/dist/core/GscClient.d.ts.map +1 -0
  18. package/dist/core/GscClient.js +92 -0
  19. package/dist/core/GscClient.js.map +1 -0
  20. package/dist/core/SyncManager.d.ts +66 -0
  21. package/dist/core/SyncManager.d.ts.map +1 -0
  22. package/dist/core/SyncManager.js +368 -0
  23. package/dist/core/SyncManager.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +14 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/overview/src/ui/overview.html +185 -0
  29. package/dist/server.d.ts +6 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +441 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/src/ui/dashboard.html +437 -0
  34. package/dist/sync-progress/src/ui/sync-progress.html +147 -0
  35. package/dist/tools/compare-periods.d.ts +3 -0
  36. package/dist/tools/compare-periods.d.ts.map +1 -0
  37. package/dist/tools/compare-periods.js +127 -0
  38. package/dist/tools/compare-periods.js.map +1 -0
  39. package/dist/tools/get-dashboard.d.ts +10 -0
  40. package/dist/tools/get-dashboard.d.ts.map +1 -0
  41. package/dist/tools/get-dashboard.js +310 -0
  42. package/dist/tools/get-dashboard.js.map +1 -0
  43. package/dist/tools/get-insights.d.ts +3 -0
  44. package/dist/tools/get-insights.d.ts.map +1 -0
  45. package/dist/tools/get-insights.js +509 -0
  46. package/dist/tools/get-insights.js.map +1 -0
  47. package/dist/tools/get-overview.d.ts +36 -0
  48. package/dist/tools/get-overview.d.ts.map +1 -0
  49. package/dist/tools/get-overview.js +111 -0
  50. package/dist/tools/get-overview.js.map +1 -0
  51. package/dist/tools/helpers.d.ts +67 -0
  52. package/dist/tools/helpers.d.ts.map +1 -0
  53. package/dist/tools/helpers.js +239 -0
  54. package/dist/tools/helpers.js.map +1 -0
  55. package/dist/tools/list-properties.d.ts +4 -0
  56. package/dist/tools/list-properties.d.ts.map +1 -0
  57. package/dist/tools/list-properties.js +35 -0
  58. package/dist/tools/list-properties.js.map +1 -0
  59. package/dist/tools/query-data.d.ts +12 -0
  60. package/dist/tools/query-data.d.ts.map +1 -0
  61. package/dist/tools/query-data.js +20 -0
  62. package/dist/tools/query-data.js.map +1 -0
  63. package/dist/tools/sync-data.d.ts +11 -0
  64. package/dist/tools/sync-data.d.ts.map +1 -0
  65. package/dist/tools/sync-data.js +62 -0
  66. package/dist/tools/sync-data.js.map +1 -0
  67. package/dist/types/index.d.ts +101 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +3 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/package.json +57 -0
@@ -0,0 +1,205 @@
1
+ import BetterSqlite3 from 'better-sqlite3';
2
+ export class Database {
3
+ db;
4
+ insertStmt = null;
5
+ constructor(dbPath) {
6
+ this.db = new BetterSqlite3(dbPath);
7
+ this.db.pragma('journal_mode = WAL');
8
+ this.db.pragma('busy_timeout = 5000'); // wait up to 5s for concurrent writers
9
+ this.db.pragma('cache_size = -65536'); // 64MB cache (default is ~2MB)
10
+ this.db.pragma('temp_store = MEMORY'); // temp tables in RAM
11
+ this.db.pragma('mmap_size = 4294967296'); // 4GB mmap for large DBs
12
+ this.initializeTables();
13
+ }
14
+ initializeTables() {
15
+ this.db.exec(`
16
+ CREATE TABLE IF NOT EXISTS property_meta (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ site_url TEXT UNIQUE NOT NULL,
19
+ permission_level TEXT,
20
+ last_synced_at TEXT,
21
+ created_at TEXT DEFAULT (datetime('now'))
22
+ );
23
+ `);
24
+ this.db.exec(`
25
+ CREATE TABLE IF NOT EXISTS search_analytics (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ date TEXT NOT NULL,
28
+ query TEXT,
29
+ page TEXT,
30
+ device TEXT,
31
+ country TEXT,
32
+ search_appearance TEXT,
33
+ clicks INTEGER NOT NULL DEFAULT 0,
34
+ impressions INTEGER NOT NULL DEFAULT 0,
35
+ ctr REAL NOT NULL DEFAULT 0,
36
+ position REAL NOT NULL DEFAULT 0,
37
+ created_at TEXT DEFAULT (datetime('now'))
38
+ );
39
+ `);
40
+ this.db.exec(`
41
+ -- Primary uniqueness constraint (also serves as composite index for date-first lookups)
42
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sa_unique
43
+ ON search_analytics(date, query, page, device, country);
44
+
45
+ -- Single-column indexes for standalone filtering
46
+ CREATE INDEX IF NOT EXISTS idx_sa_date ON search_analytics(date);
47
+ CREATE INDEX IF NOT EXISTS idx_sa_query ON search_analytics(query);
48
+ CREATE INDEX IF NOT EXISTS idx_sa_page ON search_analytics(page);
49
+ CREATE INDEX IF NOT EXISTS idx_sa_clicks ON search_analytics(clicks DESC);
50
+ CREATE INDEX IF NOT EXISTS idx_sa_impressions ON search_analytics(impressions DESC);
51
+
52
+ -- Composite indexes for dashboard query patterns
53
+ CREATE INDEX IF NOT EXISTS idx_sa_date_query ON search_analytics(date, query, clicks, impressions, position);
54
+ CREATE INDEX IF NOT EXISTS idx_sa_date_page ON search_analytics(date, page, clicks, impressions, position);
55
+ CREATE INDEX IF NOT EXISTS idx_sa_date_country ON search_analytics(date, country, clicks, impressions);
56
+ CREATE INDEX IF NOT EXISTS idx_sa_query_date ON search_analytics(query, date);
57
+
58
+ -- Covering index for summary aggregations
59
+ CREATE INDEX IF NOT EXISTS idx_sa_date_metrics ON search_analytics(date, clicks, impressions, ctr, position);
60
+ `);
61
+ // Update query planner statistics only when needed
62
+ // sqlite_stat1 may not exist if ANALYZE has never run, so check safely
63
+ const statExists = this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1' LIMIT 1`).get();
64
+ const hasStats = statExists && this.db.prepare(`SELECT 1 FROM sqlite_stat1 WHERE tbl = 'search_analytics' AND idx = 'idx_sa_date_metrics' LIMIT 1`).get();
65
+ if (!hasStats) {
66
+ this.db.exec('ANALYZE;');
67
+ }
68
+ this.db.exec(`
69
+ CREATE TABLE IF NOT EXISTS sync_log (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ sync_type TEXT NOT NULL,
72
+ dimensions TEXT NOT NULL,
73
+ date_from TEXT,
74
+ date_to TEXT,
75
+ rows_fetched INTEGER DEFAULT 0,
76
+ rows_inserted INTEGER DEFAULT 0,
77
+ status TEXT DEFAULT 'running',
78
+ error_message TEXT,
79
+ started_at TEXT DEFAULT (datetime('now')),
80
+ completed_at TEXT
81
+ );
82
+ `);
83
+ }
84
+ // --- Property Meta ---
85
+ upsertPropertyMeta(siteUrl, permissionLevel) {
86
+ this.db.prepare(`
87
+ INSERT INTO property_meta (site_url, permission_level)
88
+ VALUES (?, ?)
89
+ ON CONFLICT(site_url) DO UPDATE SET permission_level = excluded.permission_level
90
+ `).run(siteUrl, permissionLevel);
91
+ }
92
+ updateLastSynced(siteUrl) {
93
+ this.db.prepare(`
94
+ UPDATE property_meta SET last_synced_at = datetime('now') WHERE site_url = ?
95
+ `).run(siteUrl);
96
+ }
97
+ getPropertyMeta(siteUrl) {
98
+ const row = this.db.prepare(`
99
+ SELECT site_url, permission_level, last_synced_at FROM property_meta WHERE site_url = ?
100
+ `).get(siteUrl);
101
+ if (!row)
102
+ return null;
103
+ return {
104
+ siteUrl: row.site_url,
105
+ permissionLevel: row.permission_level,
106
+ lastSyncedAt: row.last_synced_at,
107
+ };
108
+ }
109
+ getLastSyncDate(siteUrl) {
110
+ const row = this.db.prepare(`
111
+ SELECT MAX(date) as max_date FROM search_analytics
112
+ `).get();
113
+ return row?.max_date ?? null;
114
+ }
115
+ // --- Search Analytics ---
116
+ getInsertStmt() {
117
+ if (!this.insertStmt) {
118
+ this.insertStmt = this.db.prepare(`
119
+ INSERT OR REPLACE INTO search_analytics
120
+ (date, query, page, device, country, search_appearance, clicks, impressions, ctr, position)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
122
+ `);
123
+ }
124
+ return this.insertStmt;
125
+ }
126
+ insertSearchAnalyticsBatch(rows) {
127
+ let inserted = 0;
128
+ const stmt = this.getInsertStmt();
129
+ const transaction = this.db.transaction((rows) => {
130
+ for (const row of rows) {
131
+ stmt.run(row.date, row.query, row.page, row.device, row.country, row.searchAppearance, row.clicks, row.impressions, row.ctr, row.position);
132
+ inserted++;
133
+ }
134
+ });
135
+ transaction(rows);
136
+ return inserted;
137
+ }
138
+ getRowCount() {
139
+ const result = this.db.prepare('SELECT COUNT(*) as count FROM search_analytics').get();
140
+ return result.count;
141
+ }
142
+ getDateRange() {
143
+ const result = this.db.prepare('SELECT MIN(date) as min_date, MAX(date) as max_date FROM search_analytics').get();
144
+ if (!result || !result.min_date)
145
+ return null;
146
+ return { minDate: result.min_date, maxDate: result.max_date };
147
+ }
148
+ // --- Sync Log ---
149
+ createSyncLog(entry) {
150
+ const result = this.db.prepare(`
151
+ INSERT INTO sync_log (sync_type, dimensions, date_from, date_to, rows_fetched, rows_inserted, status, error_message)
152
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
153
+ `).run(entry.syncType, entry.dimensions, entry.dateFrom, entry.dateTo, entry.rowsFetched, entry.rowsInserted, entry.status, entry.errorMessage);
154
+ return Number(result.lastInsertRowid);
155
+ }
156
+ updateSyncLog(id, updates) {
157
+ const fields = [];
158
+ const values = [];
159
+ if (updates.rowsFetched !== undefined) {
160
+ fields.push('rows_fetched = ?');
161
+ values.push(updates.rowsFetched);
162
+ }
163
+ if (updates.rowsInserted !== undefined) {
164
+ fields.push('rows_inserted = ?');
165
+ values.push(updates.rowsInserted);
166
+ }
167
+ if (updates.status !== undefined) {
168
+ fields.push('status = ?');
169
+ values.push(updates.status);
170
+ }
171
+ if (updates.errorMessage !== undefined) {
172
+ fields.push('error_message = ?');
173
+ values.push(updates.errorMessage);
174
+ }
175
+ if (updates.status === 'completed' || updates.status === 'error') {
176
+ fields.push("completed_at = datetime('now')");
177
+ }
178
+ if (fields.length === 0)
179
+ return;
180
+ values.push(id);
181
+ this.db.prepare(`UPDATE sync_log SET ${fields.join(', ')} WHERE id = ?`).run(...values);
182
+ }
183
+ // --- Raw Query (read-only) ---
184
+ executeReadOnlyQuery(sql, params = [], maxRows = 10000) {
185
+ const forbidden = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE|ATTACH|DETACH|REINDEX|VACUUM|PRAGMA)\b/i;
186
+ if (forbidden.test(sql)) {
187
+ throw new Error('Only SELECT queries are allowed. Write operations and PRAGMA are blocked.');
188
+ }
189
+ // Enforce a result size limit to prevent memory exhaustion
190
+ const hasLimit = /\bLIMIT\b/i.test(sql);
191
+ const safeSql = hasLimit ? sql : `${sql} LIMIT ${maxRows}`;
192
+ return this.db.prepare(safeSql).all(...params);
193
+ }
194
+ // --- Generic query helper for insights ---
195
+ query(sql, params = []) {
196
+ return this.db.prepare(sql).all(...params);
197
+ }
198
+ queryOne(sql, params = []) {
199
+ return this.db.prepare(sql).get(...params);
200
+ }
201
+ close() {
202
+ this.db.close();
203
+ }
204
+ }
205
+ //# sourceMappingURL=Database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Database.js","sourceRoot":"","sources":["../../src/core/Database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { GscProperty, GscApiRow, FetchOptions } from '../types/index.js';
2
+ export interface PageResult {
3
+ rows: GscApiRow[];
4
+ totalSoFar: number;
5
+ startRow: number;
6
+ }
7
+ export declare class GscClient {
8
+ private auth;
9
+ private searchconsole;
10
+ constructor(credentialsPath: string);
11
+ listProperties(): Promise<GscProperty[]>;
12
+ /**
13
+ * Fetch search analytics with streaming page callback.
14
+ *
15
+ * Each 25k-row page from the API triggers the onPage callback
16
+ * so callers can commit to the database immediately rather than
17
+ * accumulating everything in memory.
18
+ *
19
+ * Returns the total number of rows fetched across all pages.
20
+ */
21
+ fetchSearchAnalytics(siteUrl: string, options: FetchOptions, signal?: AbortSignal, onPage?: (page: PageResult) => void): Promise<number>;
22
+ }
23
+ //# sourceMappingURL=GscClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GscClient.d.ts","sourceRoot":"","sources":["../../src/core/GscClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAuB9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,aAAa,CAAM;gBAEf,eAAe,EAAE,MAAM;IAQ7B,cAAc,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAS9C;;;;;;;;OAQG;IACG,oBAAoB,CACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,YAAY,EACrB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAClC,OAAO,CAAC,MAAM,CAAC;CAiDnB"}
@@ -0,0 +1,92 @@
1
+ import { google } from 'googleapis';
2
+ const ROW_LIMIT = 25000; // GSC API max per request
3
+ const DEFAULT_DIMENSIONS = ['query', 'page', 'date', 'device', 'country'];
4
+ const MAX_RETRIES = 3;
5
+ const RETRYABLE_STATUS_CODES = [429, 500, 502, 503];
6
+ async function withRetry(fn, retries = MAX_RETRIES) {
7
+ for (let attempt = 0; attempt <= retries; attempt++) {
8
+ try {
9
+ return await fn();
10
+ }
11
+ catch (err) {
12
+ const status = err?.code || err?.response?.status || err?.status;
13
+ const isRetryable = RETRYABLE_STATUS_CODES.includes(Number(status));
14
+ if (!isRetryable || attempt === retries)
15
+ throw err;
16
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
17
+ console.error(`[GSC] Retryable error (${status}), attempt ${attempt + 1}/${retries}, waiting ${delay}ms...`);
18
+ await new Promise(resolve => setTimeout(resolve, delay));
19
+ }
20
+ }
21
+ throw new Error('Unreachable');
22
+ }
23
+ export class GscClient {
24
+ auth;
25
+ searchconsole;
26
+ constructor(credentialsPath) {
27
+ this.auth = new google.auth.GoogleAuth({
28
+ keyFile: credentialsPath,
29
+ scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
30
+ });
31
+ this.searchconsole = google.searchconsole({ version: 'v1', auth: this.auth });
32
+ }
33
+ async listProperties() {
34
+ const response = await withRetry(() => this.searchconsole.sites.list());
35
+ const sites = response.data.siteEntry || [];
36
+ return sites.map((site) => ({
37
+ siteUrl: site.siteUrl,
38
+ permissionLevel: site.permissionLevel,
39
+ }));
40
+ }
41
+ /**
42
+ * Fetch search analytics with streaming page callback.
43
+ *
44
+ * Each 25k-row page from the API triggers the onPage callback
45
+ * so callers can commit to the database immediately rather than
46
+ * accumulating everything in memory.
47
+ *
48
+ * Returns the total number of rows fetched across all pages.
49
+ */
50
+ async fetchSearchAnalytics(siteUrl, options, signal, onPage) {
51
+ const dimensions = options.dimensions || DEFAULT_DIMENSIONS;
52
+ let totalRows = 0;
53
+ let startRow = 0;
54
+ while (true) {
55
+ if (signal?.aborted) {
56
+ console.error(`[GSC] Sync aborted after ${totalRows} rows`);
57
+ break;
58
+ }
59
+ const response = await withRetry(() => this.searchconsole.searchanalytics.query({
60
+ siteUrl,
61
+ requestBody: {
62
+ startDate: options.startDate,
63
+ endDate: options.endDate,
64
+ dimensions,
65
+ rowLimit: options.rowLimit || ROW_LIMIT,
66
+ startRow,
67
+ dataState: options.dataState || 'all',
68
+ ...(options.searchType ? { type: options.searchType } : {}),
69
+ },
70
+ }));
71
+ const rows = (response.data.rows || []).map((row) => ({
72
+ keys: row.keys,
73
+ clicks: row.clicks,
74
+ impressions: row.impressions,
75
+ ctr: row.ctr,
76
+ position: row.position,
77
+ }));
78
+ totalRows += rows.length;
79
+ // Fire callback so caller can commit this page immediately
80
+ if (onPage && rows.length > 0) {
81
+ onPage({ rows, totalSoFar: totalRows, startRow });
82
+ }
83
+ if (rows.length < (options.rowLimit || ROW_LIMIT)) {
84
+ break;
85
+ }
86
+ startRow += rows.length;
87
+ console.error(`[GSC] Fetched ${totalRows} rows so far (page at startRow=${startRow})...`);
88
+ }
89
+ return totalRows;
90
+ }
91
+ }
92
+ //# sourceMappingURL=GscClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GscClient.js","sourceRoot":"","sources":["../../src/core/GscClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { GscClient } from './GscClient.js';
2
+ export type SyncJobStatus = 'queued' | 'syncing' | 'completed' | 'failed' | 'cancelled';
3
+ export interface SyncJobResult {
4
+ siteUrl: string;
5
+ status: 'completed' | 'failed' | 'skipped' | 'cancelled';
6
+ rowsFetched: number;
7
+ rowsInserted: number;
8
+ durationMs: number;
9
+ error?: string;
10
+ pruned?: {
11
+ rowsDeleted: number;
12
+ rowsAfter: number;
13
+ spaceSavedMB: number;
14
+ };
15
+ }
16
+ export interface SyncStatus {
17
+ jobId: string;
18
+ status: SyncJobStatus;
19
+ totalProperties: number;
20
+ completedProperties: number;
21
+ currentProperty: string | null;
22
+ rowsFetched: number;
23
+ estimatedTotalRows: number | null;
24
+ apiCallsMade: number;
25
+ startedAt: string;
26
+ elapsedMs: number;
27
+ results: SyncJobResult[];
28
+ error?: string;
29
+ }
30
+ export declare class SyncManager {
31
+ private gscClient;
32
+ private jobs;
33
+ private jobOrder;
34
+ constructor(gscClient: GscClient);
35
+ startSync(args: {
36
+ siteUrl: string;
37
+ startDate?: string;
38
+ endDate?: string;
39
+ dimensions?: string[];
40
+ searchType?: 'web' | 'discover' | 'googleNews' | 'image' | 'video';
41
+ }): string;
42
+ startSyncAll(args: {
43
+ startDate?: string;
44
+ endDate?: string;
45
+ dimensions?: string[];
46
+ searchType?: 'web' | 'discover' | 'googleNews' | 'image' | 'video';
47
+ }): Promise<string>;
48
+ getStatus(jobId?: string): SyncStatus | SyncStatus[];
49
+ cancelJob(jobId: string): boolean;
50
+ private createJob;
51
+ private pruneHistory;
52
+ /**
53
+ * Run the job in the background with parallel property syncing.
54
+ * Up to PROPERTY_CONCURRENCY properties sync concurrently.
55
+ */
56
+ private runJob;
57
+ /**
58
+ * Sync a single property with parallel chunk fetching.
59
+ * Up to CHUNK_CONCURRENCY date-range chunks fetch concurrently.
60
+ * Each chunk's DB writes are serialized (SQLite is single-writer)
61
+ * but API fetches overlap.
62
+ */
63
+ private syncOneProperty;
64
+ private jobToStatus;
65
+ }
66
+ //# sourceMappingURL=SyncManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncManager.d.ts","sourceRoot":"","sources":["../../src/core/SyncManager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAa3C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAExF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE;QACP,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,qBAAa,WAAW;IAIV,OAAO,CAAC,SAAS;IAH7B,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,QAAQ,CAAgB;gBAEZ,SAAS,EAAE,SAAS;IAExC,SAAS,CAAC,IAAI,EAAE;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;KACpE,GAAG,MAAM;IAMJ,YAAY,CAAC,IAAI,EAAE;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;KACpE,GAAG,OAAO,CAAC,MAAM,CAAC;IAcnB,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,EAAE;IAwBpD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAajC,OAAO,CAAC,SAAS;IAuBjB,OAAO,CAAC,YAAY;IAapB;;;OAGG;YACW,MAAM;IAiDpB;;;;;OAKG;YACW,eAAe;IA6J7B,OAAO,CAAC,WAAW;CAgBpB"}