signalk-edge-link 2.3.0 → 2.4.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/lib/connection-config.js +8 -0
- package/lib/delta-sanitizer.js +85 -0
- package/lib/instance.js +17 -2
- package/lib/pipeline-v2-client.js +20 -16
- package/lib/shared/connection-schema.js +10 -1
- package/package.json +1 -1
- package/public/982.cc4f5aca99be921e0171.js +2 -0
- package/public/982.cc4f5aca99be921e0171.js.map +1 -0
- package/public/index.html +1 -1
- package/public/main.0b6f5e3267731da945f0.js.map +1 -1
- package/public/{main.e2b9c98749816ac2e285.css → main.2ae3dd54effad689f0da.css} +16 -1
- package/public/main.2ae3dd54effad689f0da.css.map +1 -0
- package/public/remoteEntry.js +1 -1
- package/public/remoteEntry.js.map +1 -1
- package/public/982.63949a2b2f6c5854e034.js +0 -2
- package/public/982.63949a2b2f6c5854e034.js.map +0 -1
- package/public/main.e2b9c98749816ac2e285.css.map +0 -1
- package/public/main.js +0 -467
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.0b6f5e3267731da945f0.js","mappings":"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","sources":["webpack://signalk-edge-link/webpack/runtime/load script","webpack://signalk-edge-link/./src/webapp/utils/apiFetch.ts","webpack://signalk-edge-link/./src/webapp/index.ts","webpack://signalk-edge-link/webpack/bootstrap","webpack://signalk-edge-link/webpack/runtime/ensure chunk","webpack://signalk-edge-link/webpack/runtime/get javascript chunk filename","webpack://signalk-edge-link/webpack/runtime/get mini-css chunk filename","webpack://signalk-edge-link/webpack/runtime/global","webpack://signalk-edge-link/webpack/runtime/hasOwnProperty shorthand","webpack://signalk-edge-link/webpack/runtime/sharing","webpack://signalk-edge-link/webpack/runtime/publicPath","webpack://signalk-edge-link/webpack/runtime/jsonp chunk loading","webpack://signalk-edge-link/webpack/startup"],"sourcesContent":["var inProgress = {};\nvar dataWebpackPrefix = \"signalk-edge-link:\";\n// loadScript function to load a script via script tag\n__webpack_require__.l = (url, done, key, chunkId) => {\n\tif(inProgress[url]) { inProgress[url].push(done); return; }\n\tvar script, needAttach;\n\tif(key !== undefined) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tfor(var i = 0; i < scripts.length; i++) {\n\t\t\tvar s = scripts[i];\n\t\t\tif(s.getAttribute(\"src\") == url || s.getAttribute(\"data-webpack\") == dataWebpackPrefix + key) { script = s; break; }\n\t\t}\n\t}\n\tif(!script) {\n\t\tneedAttach = true;\n\t\tscript = document.createElement('script');\n\n\t\tscript.charset = 'utf-8';\n\t\tif (__webpack_require__.nc) {\n\t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n\t\t}\n\t\tscript.setAttribute(\"data-webpack\", dataWebpackPrefix + key);\n\n\t\tscript.src = url;\n\t}\n\tinProgress[url] = [done];\n\tvar onScriptComplete = (prev, event) => {\n\t\t// avoid mem leaks in IE.\n\t\tscript.onerror = script.onload = null;\n\t\tclearTimeout(timeout);\n\t\tvar doneFns = inProgress[url];\n\t\tdelete inProgress[url];\n\t\tscript.parentNode && script.parentNode.removeChild(script);\n\t\tdoneFns && doneFns.forEach((fn) => (fn(event)));\n\t\tif(prev) return prev(event);\n\t}\n\tvar timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);\n\tscript.onerror = onScriptComplete.bind(null, script.onerror);\n\tscript.onload = onScriptComplete.bind(null, script.onload);\n\tneedAttach && document.head.appendChild(script);\n};","/// <reference lib=\"dom\" />\r\n\r\nexport const MANAGEMENT_TOKEN_ERROR_MESSAGE = \"Management token required/invalid.\";\r\n\r\ninterface AuthConfig {\r\n token: string | null;\r\n localStorageKey: string;\r\n queryParam: string;\r\n includeTokenInQuery: boolean;\r\n headerMode: string;\r\n}\r\n\r\ndeclare global {\r\n interface Window {\r\n __EDGE_LINK_AUTH__?: Partial<AuthConfig>;\r\n }\r\n}\r\n\r\nconst DEFAULT_AUTH_CONFIG: AuthConfig = {\r\n token: null,\r\n localStorageKey: \"signalkEdgeLinkManagementToken\",\r\n queryParam: \"edgeLinkToken\",\r\n // Default to false: query-parameter tokens leak into browser history, server\r\n // access logs, and Referer headers. Set includeTokenInQuery: true in\r\n // window.__EDGE_LINK_AUTH__ only when you explicitly need URL-based auth.\r\n includeTokenInQuery: false,\r\n headerMode: \"both\"\r\n};\r\n\r\nfunction readRuntimeAuthConfig(): AuthConfig {\r\n if (typeof window === \"undefined\") {\r\n return DEFAULT_AUTH_CONFIG;\r\n }\r\n\r\n const runtime = window.__EDGE_LINK_AUTH__;\r\n if (!runtime || typeof runtime !== \"object\") {\r\n return DEFAULT_AUTH_CONFIG;\r\n }\r\n\r\n return { ...DEFAULT_AUTH_CONFIG, ...runtime };\r\n}\r\n\r\nfunction resolveToken(config: AuthConfig): string {\r\n if (config.token) {\r\n return String(config.token).trim();\r\n }\r\n\r\n if (typeof window === \"undefined\") {\r\n return \"\";\r\n }\r\n\r\n // SECURITY NOTE: Query parameter tokens can leak into browser history, server\r\n // access logs, and Referer headers. Prefer localStorage or\r\n // window.__EDGE_LINK_AUTH__.token for production deployments. Set\r\n // includeTokenInQuery: false in __EDGE_LINK_AUTH__ to disable this path.\r\n if (config.includeTokenInQuery && config.queryParam) {\r\n const tokenFromQuery = new URLSearchParams(window.location.search).get(config.queryParam);\r\n if (tokenFromQuery) {\r\n return tokenFromQuery.trim();\r\n }\r\n }\r\n\r\n if (config.localStorageKey && window.localStorage) {\r\n const tokenFromStorage = window.localStorage.getItem(config.localStorageKey);\r\n if (tokenFromStorage) {\r\n return tokenFromStorage.trim();\r\n }\r\n }\r\n\r\n return \"\";\r\n}\r\n\r\nfunction attachAuthHeaders(headers: Headers, token: string, headerMode: string): Headers {\r\n if (!token) {\r\n return headers;\r\n }\r\n\r\n const normalizedMode = (headerMode || \"both\").toLowerCase();\r\n if (\r\n normalizedMode === \"x-edge-link-token\" ||\r\n normalizedMode === \"token\" ||\r\n normalizedMode === \"both\"\r\n ) {\r\n headers.set(\"X-Edge-Link-Token\", token);\r\n }\r\n if (\r\n normalizedMode === \"authorization\" ||\r\n normalizedMode === \"bearer\" ||\r\n normalizedMode === \"both\"\r\n ) {\r\n headers.set(\"Authorization\", `Bearer ${token}`);\r\n }\r\n return headers;\r\n}\r\n\r\nexport function getAuthToken(): string {\r\n const config = readRuntimeAuthConfig();\r\n return resolveToken(config);\r\n}\r\n\r\nexport function getTokenHelpText(): string {\r\n const config = readRuntimeAuthConfig();\r\n const modeText =\r\n config.headerMode && String(config.headerMode).toLowerCase() === \"authorization\"\r\n ? \"Authorization: Bearer <token>\"\r\n : config.headerMode && String(config.headerMode).toLowerCase() === \"x-edge-link-token\"\r\n ? \"X-Edge-Link-Token\"\r\n : \"X-Edge-Link-Token and Authorization: Bearer <token>\";\r\n\r\n return `The server-side token is configured in plugin settings (managementApiToken) or via the SIGNALK_EDGE_LINK_MANAGEMENT_TOKEN environment variable. To authenticate from the browser, provide the token using window.__EDGE_LINK_AUTH__.token, query parameter \"${config.queryParam}\", or localStorage key \"${config.localStorageKey}\". Requests send ${modeText} when a token is available.`;\r\n}\r\n\r\nexport function apiFetch(input: string | Request, init: RequestInit = {}): Promise<Response> {\r\n const config = readRuntimeAuthConfig();\r\n const token = resolveToken(config);\r\n const headers = new Headers(init.headers || {});\r\n attachAuthHeaders(headers, token, config.headerMode);\r\n\r\n return fetch(input, {\r\n ...init,\r\n headers\r\n });\r\n}\r\n","import \"./styles.css\";\r\nimport { apiFetch, getTokenHelpText, MANAGEMENT_TOKEN_ERROR_MESSAGE } from \"./utils/apiFetch\";\r\n\r\n// Constants\r\nconst API_BASE_PATH = \"/plugins/signalk-edge-link\";\r\nconst DELTA_TIMER_MIN = 100;\r\nconst DELTA_TIMER_MAX = 10000;\r\nconst NOTIFICATION_TIMEOUT = 4000;\r\nconst METRICS_REFRESH_INTERVAL = 15000;\r\nconst JSON_SYNC_DEBOUNCE = 300;\r\n\r\n// ── Types ─────────────────────────────────────────────────────────────────────\r\n\r\ninterface ConnectionInfo {\r\n id: string;\r\n name?: string;\r\n type: string;\r\n healthy?: boolean;\r\n readyToSend?: boolean;\r\n}\r\n\r\ninterface AuthenticatedError extends Error {\r\n isUnauthorized?: boolean;\r\n}\r\n\r\n// Escape HTML special characters to prevent XSS when inserting dynamic values into innerHTML\r\nfunction escapeHtml(str: string | number): string {\r\n return String(str)\r\n .replace(/&/g, \"&\")\r\n .replace(/</g, \"<\")\r\n .replace(/>/g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/'/g, \"'\");\r\n}\r\n\r\n// HTML Template Helpers\r\nconst renderCard = (\r\n title: string,\r\n subtitle: string | null,\r\n contentId: string,\r\n contentClass = \"\"\r\n) => `\r\n <div class=\"config-section\">\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <h2>${escapeHtml(title)}</h2>\r\n ${subtitle ? `<p class=\"subtitle\">${escapeHtml(subtitle)}</p>` : \"\"}\r\n </div>\r\n <div class=\"card-content\">\r\n <div id=\"${contentId}\" class=\"${contentClass || contentId + \"-info\"}\">\r\n <p>Loading ${escapeHtml(title.toLowerCase())}...</p>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n`;\r\n\r\nconst renderStatItem = (label: string, value: string | number, hasError = false) => `\r\n <div class=\"stat-item${hasError ? \" error\" : \"\"}\">\r\n <span class=\"stat-label\">${escapeHtml(label)}:</span>\r\n <span class=\"stat-value\">${escapeHtml(value)}</span>\r\n </div>\r\n`;\r\n\r\nconst renderMetricItem = (label: string, value: string | number, statusClass = \"\") => `\r\n <div class=\"metric-item${statusClass ? \" \" + statusClass : \"\"}\">\r\n <div class=\"metric-label\">${escapeHtml(label)}</div>\r\n <div class=\"metric-value\">${escapeHtml(value)}</div>\r\n </div>\r\n`;\r\n\r\n// Variants that accept a pre-sanitized HTML string as the value (label is still escaped)\r\nconst renderMetricItemHtml = (label: string, htmlValue: string, statusClass = \"\") => `\r\n <div class=\"metric-item${statusClass ? \" \" + statusClass : \"\"}\">\r\n <div class=\"metric-label\">${escapeHtml(label)}</div>\r\n <div class=\"metric-value\">${htmlValue}</div>\r\n </div>\r\n`;\r\n\r\nconst renderStatItemHtml = (label: string, htmlValue: string, hasError = false) => `\r\n <div class=\"stat-item${hasError ? \" error\" : \"\"}\">\r\n <span class=\"stat-label\">${escapeHtml(label)}:</span>\r\n <span class=\"stat-value\">${htmlValue}</span>\r\n </div>\r\n`;\r\n\r\nconst renderBwStat = (\r\n label: string,\r\n value: string | number,\r\n isHighlight = false,\r\n isSuccess = false\r\n) => `\r\n <div class=\"bw-stat${isHighlight ? \" highlight\" : \"\"}\">\r\n <span class=\"bw-label\">${escapeHtml(label)}:</span>\r\n <span class=\"bw-value${isSuccess ? \" success-text\" : \"\"}\">${escapeHtml(value)}</span>\r\n </div>\r\n`;\r\n\r\nconst renderSectionGroup = (\r\n title: string,\r\n description: string | null,\r\n content: string,\r\n id = \"\"\r\n) => `\r\n <section class=\"page-group\"${id ? ` id=\"${id}\"` : \"\"}>\r\n <div class=\"page-group-header\">\r\n <h2>${escapeHtml(title)}</h2>\r\n ${description ? `<p>${escapeHtml(description)}</p>` : \"\"}\r\n </div>\r\n <div class=\"page-group-content\">\r\n ${content}\r\n </div>\r\n </section>\r\n`;\r\n\r\nclass DataConnectorConfig {\r\n connections: ConnectionInfo[];\r\n activeConnectionId: string | null;\r\n pluginConfig: Record<string, unknown> | null;\r\n pluginSchema: Record<string, unknown> | null;\r\n schemaCurrentMode: string | null;\r\n metricsInterval: ReturnType<typeof setInterval> | null;\r\n syncTimeout: ReturnType<typeof setTimeout> | null;\r\n /** True while syncFromJson() is rebuilding the path-input list — prevents\r\n * addPathItem() from triggering updateJsonFromForm() which would drop the\r\n * textarea's meta block on every path added. */\r\n isHydratingFromTextarea: boolean = false;\r\n tokenHelpText: string;\r\n deltaTimerConfig: Record<string, unknown> | null;\r\n subscriptionConfig: Record<string, unknown> | null;\r\n sentenceFilterConfig: Record<string, unknown> | null;\r\n protocolVersion: number;\r\n isServerMode: boolean;\r\n _refreshInFlight: boolean;\r\n _notificationTimer: ReturnType<typeof setTimeout> | null;\r\n\r\n constructor() {\r\n this.connections = [];\r\n this.activeConnectionId = null;\r\n this.pluginConfig = null;\r\n this.pluginSchema = null;\r\n this.schemaCurrentMode = null;\r\n this.metricsInterval = null;\r\n this.syncTimeout = null;\r\n this.tokenHelpText = getTokenHelpText();\r\n\r\n // Per-connection state (loaded for the active tab)\r\n this.deltaTimerConfig = null;\r\n this.subscriptionConfig = null;\r\n this.sentenceFilterConfig = null;\r\n this.protocolVersion = 1;\r\n this.isServerMode = false;\r\n this._refreshInFlight = false;\r\n this._notificationTimer = null;\r\n\r\n this.init();\r\n }\r\n\r\n async init() {\r\n try {\r\n await this.loadPluginConfiguration(false);\r\n await this.fetchConnections();\r\n this.renderPage();\r\n this.startMetricsRefresh();\r\n } catch (error: unknown) {\r\n console.error(\"Initialization error:\", error);\r\n this.showNotification(\r\n \"Failed to initialize application: \" +\r\n (error instanceof Error ? error.message : String(error)),\r\n \"error\"\r\n );\r\n }\r\n }\r\n\r\n // ── Connections list ───────────────────────────────────────────────────────\r\n\r\n async fetchConnections() {\r\n try {\r\n const res = await this.request(`${API_BASE_PATH}/connections`);\r\n if (res.ok) {\r\n this.connections = await res.json();\r\n }\r\n } catch (_e) {\r\n // /connections not available – fall back to legacy single-instance detection\r\n }\r\n\r\n if (!this.connections || this.connections.length === 0) {\r\n // Fallback: detect mode from plugin config and treat as single connection\r\n const type = this.detectModeFromConfig();\r\n this.connections = [{ id: \"_legacy\", name: \"Default\", type }];\r\n }\r\n\r\n if (!this.activeConnectionId) {\r\n this.activeConnectionId = this.connections[0].id;\r\n }\r\n }\r\n\r\n detectModeFromConfig(): string {\r\n if (this.pluginConfig) {\r\n const st = this.normalizeServerType(this.pluginConfig.serverType);\r\n if (st) {\r\n return st;\r\n }\r\n }\r\n if (this.schemaCurrentMode) {\r\n return this.schemaCurrentMode;\r\n }\r\n return \"client\";\r\n }\r\n\r\n getActiveConnection(): ConnectionInfo {\r\n return this.connections.find((c) => c.id === this.activeConnectionId) || this.connections[0];\r\n }\r\n\r\n isLegacyMode(): boolean {\r\n return this.connections.length === 1 && this.connections[0].id === \"_legacy\";\r\n }\r\n\r\n // ── API path helpers ───────────────────────────────────────────────────────\r\n\r\n metricsPath(connId: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/metrics`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/metrics`;\r\n }\r\n\r\n configPath(connId: string, filename: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/config/${filename}`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/config/${filename}`;\r\n }\r\n\r\n monitoringPath(connId: string, sub: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/monitoring/${sub}`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/monitoring/${sub}`;\r\n }\r\n\r\n congestionPath(connId: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/congestion`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/congestion`;\r\n }\r\n\r\n bondingPath(connId: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/bonding`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/bonding`;\r\n }\r\n\r\n bondingFailoverPath(connId: string): string {\r\n if (connId === \"_legacy\") {\r\n return `${API_BASE_PATH}/bonding/failover`;\r\n }\r\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/bonding/failover`;\r\n }\r\n\r\n async request(input: RequestInfo, init: RequestInit = {}): Promise<Response> {\r\n const response = await apiFetch(input, init);\r\n if (response.status === 401) {\r\n const error: AuthenticatedError = new Error(MANAGEMENT_TOKEN_ERROR_MESSAGE);\r\n error.isUnauthorized = true;\r\n throw error;\r\n }\r\n return response;\r\n }\r\n\r\n authFailureMessage(context: string): string {\r\n return `${MANAGEMENT_TOKEN_ERROR_MESSAGE} Failed while ${context}. ${this.tokenHelpText}`;\r\n }\r\n\r\n // ── Page rendering ─────────────────────────────────────────────────────────\r\n\r\n renderPage() {\r\n this.renderTabs();\r\n this.renderConnectionContent();\r\n }\r\n\r\n renderTabs() {\r\n const tabsDiv = document.getElementById(\"connectionTabs\");\r\n if (!tabsDiv) {\r\n return;\r\n }\r\n\r\n if (this.connections.length <= 1) {\r\n tabsDiv.innerHTML = \"\";\r\n tabsDiv.style.display = \"none\";\r\n return;\r\n }\r\n\r\n tabsDiv.style.display = \"\";\r\n tabsDiv.innerHTML = `<div class=\"tabs-container\">${this.connections\r\n .map((c) => {\r\n const icon = c.type === \"server\" ? \"🖥\" : \"📱\";\r\n const statusDot =\r\n c.healthy === false\r\n ? '<span class=\"tab-status-dot error\"></span>'\r\n : c.readyToSend || c.type === \"server\"\r\n ? '<span class=\"tab-status-dot ok\"></span>'\r\n : '<span class=\"tab-status-dot warning\"></span>';\r\n return `<button class=\"connection-tab${c.id === this.activeConnectionId ? \" active\" : \"\"}\"\r\n data-connection-id=\"${this.escapeHtml(c.id)}\">\r\n ${statusDot}\r\n <span class=\"tab-icon\">${icon}</span>\r\n <span class=\"tab-name\">${this.escapeHtml(c.name || c.id)}</span>\r\n <span class=\"tab-type\">${this.escapeHtml(c.type)}</span>\r\n </button>`;\r\n })\r\n .join(\"\")}</div>`;\r\n\r\n // Attach tab click listeners\r\n tabsDiv.querySelectorAll(\".connection-tab\").forEach((btn) => {\r\n btn.addEventListener(\"click\", () => {\r\n const id = (btn as HTMLElement).dataset.connectionId;\r\n if (id !== this.activeConnectionId) {\r\n this.activeConnectionId = id!;\r\n this.renderPage();\r\n this.loadConnectionData();\r\n }\r\n });\r\n });\r\n }\r\n\r\n renderConnectionContent() {\r\n const contentDiv = document.getElementById(\"connectionContent\");\r\n if (!contentDiv) {\r\n return;\r\n }\r\n\r\n const conn = this.getActiveConnection();\r\n this.isServerMode = conn.type === \"server\";\r\n\r\n if (this.isServerMode) {\r\n this.renderServerContent(contentDiv);\r\n } else {\r\n this.renderClientContent(contentDiv);\r\n }\r\n\r\n this.setupEventListeners();\r\n this.loadConnectionData();\r\n }\r\n\r\n renderServerContent(container: HTMLElement) {\r\n const telemetryAndHealth =\r\n renderCard(\r\n \"Performance Metrics\",\r\n \"Real-time reception statistics (auto-refreshes every 15 seconds)\",\r\n \"metrics\"\r\n ) +\r\n '<div id=\"monitoringSection\" style=\"display:none;\">' +\r\n renderCard(\r\n \"Network Quality\",\r\n \"Link quality score and network health indicators\",\r\n \"networkQuality\"\r\n ) +\r\n renderCard(\"Bandwidth Monitor\", \"Network reception statistics\", \"bandwidth\") +\r\n renderCard(\"Path Analytics\", \"Incoming data volume by SignalK path\", \"pathAnalytics\") +\r\n renderCard(\r\n \"Monitoring & Alerts\",\r\n \"Packet loss, retransmission tracking, and alert thresholds\",\r\n \"monitoringAlerts\"\r\n ) +\r\n \"</div>\";\r\n\r\n container.innerHTML =\r\n renderSectionGroup(\r\n \"Operations & Monitoring\",\r\n \"Track reception quality, throughput, and runtime behavior.\",\r\n telemetryAndHealth,\r\n \"operationsGroup\"\r\n ) +\r\n renderSectionGroup(\r\n \"Advanced\",\r\n \"Full plugin configurator (JSON editor).\",\r\n this.renderPluginConfigurationCard(),\r\n \"advancedGroup\"\r\n );\r\n }\r\n\r\n renderClientContent(container: HTMLElement) {\r\n const configuration =\r\n this.renderDeltaTimerCard() + this.renderSubscriptionCard() + this.renderSentenceFilterCard();\r\n\r\n const telemetryAndHealth =\r\n renderCard(\r\n \"Performance Metrics\",\r\n \"Real-time transmission statistics (auto-refreshes every 15 seconds)\",\r\n \"metrics\"\r\n ) +\r\n '<div id=\"congestionSection\" class=\"config-section\" style=\"display:none;\">' +\r\n renderCard(\r\n \"Network Quality\",\r\n \"Link quality score and network health indicators\",\r\n \"networkQuality\"\r\n ) +\r\n renderCard(\"Bandwidth Monitor\", \"Real-time data transmission statistics\", \"bandwidth\") +\r\n renderCard(\"Path Analytics\", \"Data volume by subscription path\", \"pathAnalytics\") +\r\n renderCard(\r\n \"Congestion Control\",\r\n \"AIMD congestion control state and delta timer auto-adjustment\",\r\n \"congestionControl\"\r\n ) +\r\n \"</div>\" +\r\n '<div id=\"bondingSection\" class=\"config-section\" style=\"display:none;\">' +\r\n renderCard(\r\n \"Connection Bonding\",\r\n \"Multi-link bonding status and failover control\",\r\n \"bondingStatus\"\r\n ) +\r\n \"</div>\" +\r\n '<div id=\"monitoringSection\" style=\"display:none;\">' +\r\n renderCard(\r\n \"Monitoring & Alerts\",\r\n \"Packet loss, retransmission tracking, and alert thresholds\",\r\n \"monitoringAlerts\"\r\n ) +\r\n \"</div>\" +\r\n renderCard(\"Status\", null, \"status\", \"status-info\");\r\n\r\n container.innerHTML =\r\n renderSectionGroup(\r\n \"Configuration\",\r\n \"Set up transmission behavior and plugin-level parameters.\",\r\n configuration,\r\n \"configurationGroup\"\r\n ) +\r\n renderSectionGroup(\r\n \"Operations & Monitoring\",\r\n \"Track transmission quality, reliability, and runtime performance.\",\r\n telemetryAndHealth,\r\n \"operationsGroup\"\r\n ) +\r\n renderSectionGroup(\r\n \"Advanced\",\r\n \"Full plugin configurator (JSON editor).\",\r\n this.renderPluginConfigurationCard(),\r\n \"advancedGroup\"\r\n );\r\n }\r\n\r\n renderDeltaTimerCard(): string {\r\n return `\r\n <div class=\"config-section\">\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <h2>Delta Timer Configuration</h2>\r\n <p>Controls how often deltas are collected and sent (in milliseconds)</p>\r\n </div>\r\n <div class=\"card-content\">\r\n <div class=\"form-group\">\r\n <label for=\"deltaTimer\">Delta Timer (ms):</label>\r\n <input type=\"number\" id=\"deltaTimer\" min=\"100\" max=\"10000\" step=\"100\" placeholder=\"1000\" />\r\n <small class=\"help-text\">\r\n Lower values = more frequent updates, higher bandwidth usage<br>\r\n Higher values = better compression ratio, lower bandwidth usage\r\n </small>\r\n </div>\r\n <button id=\"saveDeltaTimer\" class=\"btn btn-primary\">Save Delta Timer</button>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n renderSubscriptionCard(): string {\r\n return `\r\n <div class=\"config-section\">\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <h2>Subscription Configuration</h2>\r\n <p>Define which SignalK data paths to subscribe to</p>\r\n </div>\r\n <div class=\"card-content\">\r\n <div class=\"form-group\">\r\n <label for=\"context\">Context:</label>\r\n <input type=\"text\" id=\"context\" placeholder=\"*\" />\r\n <small class=\"help-text\">\r\n Context for the subscription (e.g., \"vessels.self\", \"*\" for all)\r\n </small>\r\n </div>\r\n <div class=\"subscription-paths\">\r\n <h3>Subscription Paths</h3>\r\n <div id=\"pathsList\" class=\"paths-list\"></div>\r\n <button id=\"addPath\" class=\"btn btn-secondary\">Add Path</button>\r\n </div>\r\n <fieldset class=\"meta-config\">\r\n <legend>Metadata streaming</legend>\r\n <p class=\"help-text\">\r\n Also forward Signal K path metadata (units, descriptions, zones,\r\n display names, ...) to the remote receiver. Disabled by default.\r\n Meta is sent as a full snapshot at startup and re-broadcast on\r\n an interval so a restarted receiver can recover without a\r\n round-trip.\r\n </p>\r\n <div class=\"form-group\">\r\n <label>\r\n <input type=\"checkbox\" id=\"metaEnabled\" />\r\n Include metadata\r\n </label>\r\n </div>\r\n <div class=\"form-group\">\r\n <label for=\"metaIntervalSec\">Snapshot interval (seconds):</label>\r\n <input type=\"number\" id=\"metaIntervalSec\" min=\"30\" max=\"86400\" step=\"1\" placeholder=\"300\" />\r\n <small class=\"help-text\">Between 30 and 86400. Default 300 (5 minutes).</small>\r\n </div>\r\n <div class=\"form-group\">\r\n <label for=\"metaPathsRegex\">Include paths matching (regex, optional):</label>\r\n <input type=\"text\" id=\"metaPathsRegex\" placeholder=\"\" />\r\n <small class=\"help-text\">Leave empty to include every subscribed path.</small>\r\n </div>\r\n <div class=\"form-group\">\r\n <label for=\"metaMaxPerPacket\">Max paths per packet:</label>\r\n <input type=\"number\" id=\"metaMaxPerPacket\" min=\"10\" max=\"5000\" step=\"1\" placeholder=\"500\" />\r\n <small class=\"help-text\">Between 10 and 5000. Default 500.</small>\r\n </div>\r\n </fieldset>\r\n <div class=\"json-editor\">\r\n <h3>JSON Editor</h3>\r\n <textarea id=\"subscriptionJson\" rows=\"10\" placeholder='{\"context\": \"*\", \"subscribe\": [{\"path\": \"*\"}]}'></textarea>\r\n <small class=\"help-text\">Advanced: Edit the raw JSON configuration</small>\r\n </div>\r\n <button id=\"saveSubscription\" class=\"btn btn-primary\">Save Subscription</button>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n renderSentenceFilterCard(): string {\r\n return `\r\n <div class=\"config-section\">\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <h2>Sentence Filter</h2>\r\n <p>Exclude NMEA sentences from transmission (reduces bandwidth)</p>\r\n </div>\r\n <div class=\"card-content\">\r\n <div class=\"form-group\">\r\n <label for=\"sentenceFilter\">Excluded Sentences:</label>\r\n <input type=\"text\" id=\"sentenceFilter\" placeholder=\"\" autocomplete=\"off\" />\r\n <small class=\"help-text\">\r\n Comma-separated list of NMEA sentence types to exclude.\r\n </small>\r\n </div>\r\n <button id=\"saveSentenceFilter\" class=\"btn btn-primary\">Save Sentence Filter</button>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n renderPluginConfigurationCard(): string {\r\n return `\r\n <div class=\"config-section\">\r\n <div class=\"card\">\r\n <div class=\"card-header\">\r\n <h2>Full Plugin Configuration</h2>\r\n <p>All parameters from <code>/plugin-config</code> (advanced JSON editor)</p>\r\n </div>\r\n <div class=\"card-content\">\r\n <div id=\"pluginConfigSummary\" class=\"plugin-config-summary\">\r\n <p>Loading plugin configuration...</p>\r\n </div>\r\n <div class=\"json-editor plugin-config-editor\">\r\n <h3>Plugin Config JSON</h3>\r\n <textarea\r\n id=\"pluginConfigJson\"\r\n rows=\"20\"\r\n placeholder='{\"serverType\":\"client\",\"udpPort\":4446,\"secretKey\":\"...\"}'\r\n ></textarea>\r\n <small class=\"help-text\">\r\n This editor exposes all available plugin fields. Save triggers plugin restart when supported.\r\n </small>\r\n <small class=\"help-text\">${this.escapeHtml(this.tokenHelpText)}</small>\r\n </div>\r\n <div class=\"plugin-config-actions\">\r\n <button id=\"savePluginConfig\" class=\"btn btn-primary\">Save Full Plugin Config</button>\r\n <button id=\"reloadPluginConfig\" class=\"btn btn-secondary\">Reload From Server</button>\r\n <button id=\"loadDefaultPluginConfig\" class=\"btn btn-secondary\">Load Schema Defaults</button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n // ── Data loading ───────────────────────────────────────────────────────────\r\n\r\n async loadConnectionData() {\r\n const conn = this.getActiveConnection();\r\n const connId = conn.id;\r\n\r\n if (!this.isServerMode) {\r\n await this.loadConfigurations(connId);\r\n this.updateUI();\r\n this.updateStatus();\r\n } else {\r\n this.updatePluginConfigUI();\r\n }\r\n\r\n await this.loadMetrics(connId);\r\n }\r\n\r\n async loadPluginConfiguration(showErrors = true): Promise<boolean> {\r\n try {\r\n const [configResponse, schemaResponse] = await Promise.all([\r\n this.request(`${API_BASE_PATH}/plugin-config`),\r\n this.request(`${API_BASE_PATH}/plugin-schema`)\r\n ]);\r\n\r\n if (!configResponse.ok) {\r\n throw new Error(`Failed to load plugin configuration (${configResponse.status})`);\r\n }\r\n\r\n const configData = await configResponse.json();\r\n const configuration =\r\n configData &&\r\n configData.configuration &&\r\n typeof configData.configuration === \"object\" &&\r\n !Array.isArray(configData.configuration)\r\n ? configData.configuration\r\n : {};\r\n\r\n if (schemaResponse.ok) {\r\n const schemaData = await schemaResponse.json();\r\n if (schemaData && schemaData.schema && typeof schemaData.schema === \"object\") {\r\n this.pluginSchema = schemaData.schema;\r\n }\r\n if (\r\n schemaData &&\r\n (schemaData.currentMode === \"server\" || schemaData.currentMode === \"client\")\r\n ) {\r\n this.schemaCurrentMode = schemaData.currentMode;\r\n }\r\n }\r\n\r\n this.pluginConfig = this.buildCompletePluginConfig(configuration);\r\n return true;\r\n } catch (error: unknown) {\r\n if (showErrors) {\r\n const err = error as AuthenticatedError;\r\n this.showNotification(\r\n err.isUnauthorized\r\n ? this.authFailureMessage(\"loading plugin config\")\r\n : \"Error loading plugin config: \" + err.message,\r\n \"warning\"\r\n );\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n async loadConfigurations(connId: string) {\r\n try {\r\n const [deltaResponse, subResponse, filterResponse] = await Promise.all([\r\n this.request(this.configPath(connId, \"delta_timer.json\")),\r\n this.request(this.configPath(connId, \"subscription.json\")),\r\n this.request(this.configPath(connId, \"sentence_filter.json\"))\r\n ]);\r\n\r\n this.deltaTimerConfig = deltaResponse.ok ? await deltaResponse.json() : null;\r\n this.subscriptionConfig = subResponse.ok ? await subResponse.json() : null;\r\n this.sentenceFilterConfig = filterResponse.ok ? await filterResponse.json() : null;\r\n } catch (error: unknown) {\r\n const err = error as AuthenticatedError;\r\n this.showNotification(\r\n err.isUnauthorized\r\n ? this.authFailureMessage(\"loading connection configuration\")\r\n : \"Error loading configurations: \" + err.message,\r\n \"error\"\r\n );\r\n }\r\n }\r\n\r\n async loadMetrics(connId?: string) {\r\n if (!connId) {\r\n connId = this.activeConnectionId!;\r\n }\r\n try {\r\n const response = await this.request(this.metricsPath(connId));\r\n if (response.ok) {\r\n const metrics = await response.json();\r\n this.protocolVersion = metrics.protocolVersion || 1;\r\n this.updateMetricsDisplay(metrics);\r\n\r\n if (this.protocolVersion >= 2) {\r\n this.loadV2Data(connId);\r\n }\r\n }\r\n } catch (error: unknown) {\r\n console.error(\"Error loading metrics:\", (error as Error).message);\r\n }\r\n }\r\n\r\n async loadV2Data(connId: string) {\r\n const isClient = !this.isServerMode;\r\n\r\n const fetches: Promise<Response | null>[] = [\r\n this.request(this.monitoringPath(connId, \"alerts\")).catch(() => null),\r\n this.request(this.monitoringPath(connId, \"packet-loss\")).catch(() => null),\r\n this.request(this.monitoringPath(connId, \"retransmissions\")).catch(() => null)\r\n ];\r\n\r\n if (isClient) {\r\n fetches.push(\r\n this.request(this.congestionPath(connId)).catch(() => null),\r\n this.request(this.bondingPath(connId)).catch(() => null)\r\n );\r\n }\r\n\r\n const results = await Promise.all(fetches);\r\n const [alertsRes, packetLossRes, retransmissionsRes, congestionRes, bondingRes] = results;\r\n\r\n const monitoringData: Record<string, unknown> = {};\r\n if (alertsRes && alertsRes.ok) {\r\n monitoringData.alerts = await alertsRes.json();\r\n }\r\n if (packetLossRes && packetLossRes.ok) {\r\n monitoringData.packetLoss = await packetLossRes.json();\r\n }\r\n if (retransmissionsRes && retransmissionsRes.ok) {\r\n monitoringData.retransmissions = await retransmissionsRes.json();\r\n }\r\n this.updateMonitoringDisplay(monitoringData);\r\n\r\n if (isClient && congestionRes && congestionRes.ok) {\r\n const congestionData = await congestionRes.json();\r\n this.updateCongestionDisplay(congestionData);\r\n }\r\n\r\n if (isClient && bondingRes && bondingRes.ok) {\r\n const bondingData = await bondingRes.json();\r\n this.updateBondingDisplay(bondingData);\r\n }\r\n }\r\n\r\n startMetricsRefresh() {\r\n if (this.metricsInterval) {\r\n clearInterval(this.metricsInterval);\r\n }\r\n\r\n this.metricsInterval = setInterval(() => {\r\n if (this._refreshInFlight) {\r\n return;\r\n }\r\n this._refreshInFlight = true;\r\n this.refreshActiveTab().finally(() => {\r\n this._refreshInFlight = false;\r\n });\r\n }, METRICS_REFRESH_INTERVAL);\r\n }\r\n\r\n async refreshActiveTab() {\r\n // Refresh the connections list to update tab badges\r\n try {\r\n const res = await this.request(`${API_BASE_PATH}/connections`);\r\n if (res.ok) {\r\n const updated = await res.json();\r\n if (updated.length > 0) {\r\n this.connections = updated;\r\n this.renderTabs();\r\n }\r\n }\r\n } catch (_e) {\r\n /* ignore */\r\n }\r\n\r\n await this.loadMetrics(this.activeConnectionId!);\r\n }\r\n\r\n // ── Event listeners ────────────────────────────────────────────────────────\r\n\r\n setupEventListeners() {\r\n const saveDeltaTimerBtn = document.getElementById(\"saveDeltaTimer\");\r\n if (saveDeltaTimerBtn) {\r\n saveDeltaTimerBtn.addEventListener(\"click\", () => this.saveDeltaTimer());\r\n }\r\n\r\n const saveSubscriptionBtn = document.getElementById(\"saveSubscription\");\r\n if (saveSubscriptionBtn) {\r\n saveSubscriptionBtn.addEventListener(\"click\", () => this.saveSubscription());\r\n }\r\n\r\n const saveSentenceFilterBtn = document.getElementById(\"saveSentenceFilter\");\r\n if (saveSentenceFilterBtn) {\r\n saveSentenceFilterBtn.addEventListener(\"click\", () => this.saveSentenceFilter());\r\n }\r\n\r\n const addPathBtn = document.getElementById(\"addPath\");\r\n if (addPathBtn) {\r\n addPathBtn.addEventListener(\"click\", () => this.addPathItem());\r\n }\r\n\r\n const subscriptionJsonEditor = document.getElementById(\"subscriptionJson\");\r\n if (subscriptionJsonEditor) {\r\n subscriptionJsonEditor.addEventListener(\"input\", () => {\r\n if (this.syncTimeout) {\r\n clearTimeout(this.syncTimeout);\r\n }\r\n this.syncTimeout = setTimeout(() => this.syncFromJson(), JSON_SYNC_DEBOUNCE);\r\n });\r\n }\r\n\r\n const contextInput = document.getElementById(\"context\");\r\n if (contextInput) {\r\n contextInput.addEventListener(\"input\", () => this.updateJsonFromForm());\r\n }\r\n\r\n // Meta controls feed the same JSON-from-form sync so the textarea always\r\n // reflects the current state of the structured controls. Without these\r\n // listeners, the textarea would lag behind the form until save and the\r\n // user viewing the raw JSON would see stale content.\r\n for (const metaId of [\"metaEnabled\", \"metaIntervalSec\", \"metaPathsRegex\", \"metaMaxPerPacket\"]) {\r\n const el = document.getElementById(metaId);\r\n if (el) {\r\n el.addEventListener(\"input\", () => this.updateJsonFromForm());\r\n if (metaId === \"metaEnabled\") {\r\n // checkbox emits \"change\" rather than \"input\" on some browsers\r\n el.addEventListener(\"change\", () => this.updateJsonFromForm());\r\n }\r\n }\r\n }\r\n\r\n const savePluginConfigBtn = document.getElementById(\"savePluginConfig\");\r\n if (savePluginConfigBtn) {\r\n savePluginConfigBtn.addEventListener(\"click\", () => this.savePluginConfig());\r\n }\r\n\r\n const reloadPluginConfigBtn = document.getElementById(\"reloadPluginConfig\");\r\n if (reloadPluginConfigBtn) {\r\n reloadPluginConfigBtn.addEventListener(\"click\", () => this.reloadPluginConfiguration());\r\n }\r\n\r\n const loadDefaultPluginConfigBtn = document.getElementById(\"loadDefaultPluginConfig\");\r\n if (loadDefaultPluginConfigBtn) {\r\n loadDefaultPluginConfigBtn.addEventListener(\"click\", () =>\r\n this.loadDefaultPluginConfiguration()\r\n );\r\n }\r\n }\r\n\r\n // ── UI updates ─────────────────────────────────────────────────────────────\r\n\r\n updateUI() {\r\n this.updatePluginConfigUI();\r\n\r\n if (this.deltaTimerConfig && (this.deltaTimerConfig as Record<string, unknown>).deltaTimer) {\r\n const el = document.getElementById(\"deltaTimer\") as HTMLInputElement | null;\r\n if (el) {\r\n el.value = String((this.deltaTimerConfig as Record<string, unknown>).deltaTimer);\r\n }\r\n }\r\n\r\n if (this.subscriptionConfig) {\r\n const cfg = this.subscriptionConfig as Record<string, unknown>;\r\n const ctxEl = document.getElementById(\"context\") as HTMLInputElement | null;\r\n if (ctxEl) {\r\n ctxEl.value = (cfg.context as string) || \"*\";\r\n }\r\n\r\n const pathsList = document.getElementById(\"pathsList\");\r\n if (pathsList) {\r\n pathsList.innerHTML = \"\";\r\n if (cfg.subscribe && Array.isArray(cfg.subscribe)) {\r\n (cfg.subscribe as Array<{ path: string }>).forEach((sub) => this.addPathItem(sub.path));\r\n }\r\n }\r\n\r\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\r\n if (jsonEl) {\r\n jsonEl.value = JSON.stringify(this.subscriptionConfig, null, 2);\r\n }\r\n\r\n // Populate the structured metadata controls from the loaded config.\r\n this.populateMetaControls(cfg.meta as Record<string, unknown> | undefined);\r\n }\r\n\r\n if (\r\n this.sentenceFilterConfig &&\r\n Array.isArray((this.sentenceFilterConfig as Record<string, unknown>).excludedSentences)\r\n ) {\r\n const el = document.getElementById(\"sentenceFilter\") as HTMLInputElement | null;\r\n if (el) {\r\n el.value = (\r\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\r\n ).join(\", \");\r\n }\r\n }\r\n }\r\n\r\n updatePluginConfigUI() {\r\n const editor = document.getElementById(\"pluginConfigJson\") as HTMLTextAreaElement | null;\r\n const summary = document.getElementById(\"pluginConfigSummary\");\r\n\r\n if (!editor) {\r\n return;\r\n }\r\n\r\n if (!this.pluginConfig) {\r\n editor.value = \"{}\";\r\n if (summary) {\r\n summary.innerHTML = \"<p>Plugin config unavailable.</p>\";\r\n }\r\n return;\r\n }\r\n\r\n editor.value = JSON.stringify(this.pluginConfig, null, 2);\r\n\r\n if (summary) {\r\n const hasConnections =\r\n Array.isArray(this.pluginConfig.connections) &&\r\n (this.pluginConfig.connections as unknown[]).length > 0;\r\n\r\n let summaryScope = \"Top-level\";\r\n let keyLabel = \"Top-Level Fields\";\r\n let summaryConfig: Record<string, unknown> = this.pluginConfig;\r\n\r\n if (hasConnections) {\r\n const totalConnections = (this.pluginConfig.connections as unknown[]).length;\r\n const runtimeIndex = this.connections.findIndex((c) => c.id === this.activeConnectionId);\r\n const summaryIndex =\r\n runtimeIndex >= 0 && runtimeIndex < totalConnections ? runtimeIndex : 0;\r\n const candidate = (this.pluginConfig.connections as Record<string, unknown>[])[\r\n summaryIndex\r\n ];\r\n summaryConfig =\r\n candidate && typeof candidate === \"object\" && !Array.isArray(candidate) ? candidate : {};\r\n summaryScope = `Connection ${summaryIndex + 1}/${totalConnections}`;\r\n keyLabel = \"Connection Fields\";\r\n }\r\n\r\n const mode = this.normalizeServerType(summaryConfig.serverType) || \"client\";\r\n const protocol =\r\n Number(summaryConfig.protocolVersion) >= 2 ? Number(summaryConfig.protocolVersion) : 1;\r\n const keyCount = Object.keys(summaryConfig).length;\r\n summary.innerHTML = `\r\n <div class=\"plugin-summary-grid\">\r\n ${renderStatItem(\"Scope\", summaryScope)}\r\n ${renderStatItem(\"Mode\", mode.toUpperCase())}\r\n ${renderStatItem(\"Protocol\", \"v\" + String(protocol))}\r\n ${renderStatItem(keyLabel, String(keyCount))}\r\n </div>\r\n `;\r\n }\r\n }\r\n\r\n // ── Form helpers (subscription paths) ──────────────────────────────────────\r\n\r\n addPathItem(path = \"\") {\r\n const pathsList = document.getElementById(\"pathsList\");\r\n if (!pathsList) {\r\n return;\r\n }\r\n\r\n const pathItem = document.createElement(\"div\");\r\n pathItem.className = \"path-item\";\r\n\r\n const input = document.createElement(\"input\");\r\n input.type = \"text\";\r\n input.value = path;\r\n input.placeholder = \"navigation.position\";\r\n input.className = \"path-input\";\r\n\r\n const button = document.createElement(\"button\");\r\n button.type = \"button\";\r\n button.className = \"btn btn-danger\";\r\n button.textContent = \"Remove\";\r\n\r\n input.addEventListener(\"input\", () => this.updateJsonFromForm());\r\n button.addEventListener(\"click\", () => {\r\n pathItem.remove();\r\n this.updateJsonFromForm();\r\n });\r\n\r\n pathItem.appendChild(input);\r\n pathItem.appendChild(button);\r\n pathsList.appendChild(pathItem);\r\n this.updateJsonFromForm();\r\n }\r\n\r\n updateJsonFromForm() {\r\n // Skip form → JSON syncing while we're hydrating the form from a\r\n // pasted/loaded JSON — otherwise addPathItem() would fire this on every\r\n // added path-input and reserialize the subscription.json as just\r\n // {context, subscribe} without the meta block, silently dropping meta\r\n // from pasted configs.\r\n if (this.isHydratingFromTextarea) {\r\n return;\r\n }\r\n\r\n const contextEl = document.getElementById(\"context\") as HTMLInputElement | null;\r\n const context = contextEl ? contextEl.value || \"*\" : \"*\";\r\n const pathInputs = document.querySelectorAll(\".path-input\") as NodeListOf<HTMLInputElement>;\r\n const subscribe = Array.from(pathInputs)\r\n .map((input) => ({ path: input.value }))\r\n .filter((sub) => sub.path.trim() !== \"\");\r\n\r\n // Build the meta block from the structured controls — they are the\r\n // authoritative source for metadata settings. The textarea becomes a\r\n // read-only mirror of the form state, kept in sync on every input.\r\n const config: Record<string, unknown> = { context, subscribe };\r\n const metaEnabledEl = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\r\n if (metaEnabledEl && metaEnabledEl.checked) {\r\n const intervalEl = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\r\n const pathsEl = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\r\n const maxEl = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\r\n const intervalSec = intervalEl && intervalEl.value ? Number(intervalEl.value) : 300;\r\n const maxPathsPerPacket = maxEl && maxEl.value ? Number(maxEl.value) : 500;\r\n const includePathsMatching = pathsEl && pathsEl.value ? pathsEl.value : null;\r\n config.meta = {\r\n enabled: true,\r\n intervalSec,\r\n includePathsMatching,\r\n maxPathsPerPacket\r\n };\r\n }\r\n\r\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\r\n if (jsonEl) {\r\n jsonEl.value = JSON.stringify(config, null, 2);\r\n }\r\n }\r\n\r\n /** Populate the structured meta controls from a parsed `meta` block.\r\n * Passing undefined/null resets the controls to their empty state so the\r\n * UI always matches what the textarea / loaded config contains. */\r\n populateMetaControls(meta: Record<string, unknown> | undefined | null) {\r\n const metaEnabled = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\r\n if (metaEnabled) {\r\n metaEnabled.checked = !!(meta && meta.enabled === true);\r\n }\r\n const metaIntervalSec = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\r\n if (metaIntervalSec) {\r\n metaIntervalSec.value =\r\n meta && typeof meta.intervalSec === \"number\" ? String(meta.intervalSec) : \"\";\r\n }\r\n const metaPathsRegex = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\r\n if (metaPathsRegex) {\r\n metaPathsRegex.value =\r\n meta && typeof meta.includePathsMatching === \"string\" ? meta.includePathsMatching : \"\";\r\n }\r\n const metaMaxPerPacket = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\r\n if (metaMaxPerPacket) {\r\n metaMaxPerPacket.value =\r\n meta && typeof meta.maxPathsPerPacket === \"number\" ? String(meta.maxPathsPerPacket) : \"\";\r\n }\r\n }\r\n\r\n syncFromJson() {\r\n this.isHydratingFromTextarea = true;\r\n try {\r\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\r\n if (!jsonEl) {\r\n return;\r\n }\r\n const config = JSON.parse(jsonEl.value);\r\n\r\n const ctxEl = document.getElementById(\"context\") as HTMLInputElement | null;\r\n if (ctxEl) {\r\n ctxEl.value = config.context || \"*\";\r\n }\r\n\r\n const pathsList = document.getElementById(\"pathsList\");\r\n if (pathsList) {\r\n pathsList.innerHTML = \"\";\r\n if (config.subscribe && Array.isArray(config.subscribe)) {\r\n config.subscribe.forEach((sub: { path?: string }) => this.addPathItem(sub.path || \"\"));\r\n }\r\n }\r\n\r\n // Keep the meta form in sync with the raw JSON editor. Without this,\r\n // editing the textarea would leave the checkbox/interval/regex/max\r\n // controls showing the previously-loaded values.\r\n this.populateMetaControls(config.meta as Record<string, unknown> | undefined);\r\n } catch (error: unknown) {\r\n console.warn(\"Invalid JSON in editor:\", (error as Error).message);\r\n } finally {\r\n this.isHydratingFromTextarea = false;\r\n }\r\n }\r\n\r\n // ── Save operations ────────────────────────────────────────────────────────\r\n\r\n async saveConfig(filename: string, config: unknown, configKey: string, label: string) {\r\n const connId = this.activeConnectionId!;\r\n try {\r\n const response = await this.request(this.configPath(connId, filename), {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(config)\r\n });\r\n\r\n if (response.ok) {\r\n (this as Record<string, unknown>)[configKey] = config;\r\n this.showNotification(`${label} saved successfully!`, \"success\");\r\n this.updateStatus();\r\n } else {\r\n throw new Error(\"Failed to save configuration\");\r\n }\r\n } catch (error: unknown) {\r\n const err = error as AuthenticatedError;\r\n this.showNotification(\r\n err.isUnauthorized\r\n ? this.authFailureMessage(`saving ${label.toLowerCase()}`)\r\n : `Error saving ${label.toLowerCase()}: ` + err.message,\r\n \"error\"\r\n );\r\n }\r\n }\r\n\r\n async saveDeltaTimer() {\r\n const deltaTimer = parseInt((document.getElementById(\"deltaTimer\") as HTMLInputElement).value);\r\n\r\n if (isNaN(deltaTimer) || deltaTimer < DELTA_TIMER_MIN || deltaTimer > DELTA_TIMER_MAX) {\r\n this.showNotification(\r\n `Delta timer must be between ${DELTA_TIMER_MIN} and ${DELTA_TIMER_MAX} milliseconds`,\r\n \"error\"\r\n );\r\n return;\r\n }\r\n\r\n await this.saveConfig(\r\n \"delta_timer.json\",\r\n { deltaTimer },\r\n \"deltaTimerConfig\",\r\n \"Delta timer configuration\"\r\n );\r\n }\r\n\r\n async saveSubscription() {\r\n try {\r\n // Flush any pending JSON-from-textarea debounce so the structured form\r\n // controls (in particular the meta fieldset) reflect what's currently\r\n // in the textarea before we read either side. Without this, a user\r\n // editing the raw JSON and immediately clicking Save would lose their\r\n // textarea-only edits — saveSubscription writes meta from the form\r\n // controls, which would still be stale.\r\n if (this.syncTimeout) {\r\n clearTimeout(this.syncTimeout);\r\n this.syncTimeout = null;\r\n this.syncFromJson();\r\n }\r\n\r\n const jsonText = (document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement).value;\r\n const config = JSON.parse(jsonText);\r\n\r\n if (!config.context) {\r\n throw new Error(\"Context is required\");\r\n }\r\n if (!config.subscribe || !Array.isArray(config.subscribe)) {\r\n throw new Error(\"Subscribe array is required\");\r\n }\r\n\r\n // The structured meta controls are authoritative for the meta block on\r\n // save. This lets users toggle the checkbox off (removing `meta` from\r\n // the saved config) or edit the interval/regex/max fields even after a\r\n // meta block has been saved before — the previous \"textarea wins\"\r\n // policy made those UI controls one-time-only after first enable.\r\n const metaEnabledEl = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\r\n if (metaEnabledEl && metaEnabledEl.checked) {\r\n const intervalEl = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\r\n const pathsEl = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\r\n const maxEl = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\r\n const intervalSec = intervalEl && intervalEl.value ? Number(intervalEl.value) : 300;\r\n const maxPathsPerPacket = maxEl && maxEl.value ? Number(maxEl.value) : 500;\r\n const includePathsMatching = pathsEl && pathsEl.value ? pathsEl.value : null;\r\n config.meta = {\r\n enabled: true,\r\n intervalSec,\r\n includePathsMatching,\r\n maxPathsPerPacket\r\n };\r\n } else if (config.meta !== undefined) {\r\n // Checkbox unchecked — drop any previously-saved meta block so the\r\n // runtime stops streaming metadata.\r\n delete config.meta;\r\n }\r\n\r\n await this.saveConfig(\r\n \"subscription.json\",\r\n config,\r\n \"subscriptionConfig\",\r\n \"Subscription configuration\"\r\n );\r\n } catch (error: unknown) {\r\n this.showNotification(\"Error saving subscription: \" + (error as Error).message, \"error\");\r\n }\r\n }\r\n\r\n async saveSentenceFilter() {\r\n const filterInput = (document.getElementById(\"sentenceFilter\") as HTMLInputElement).value;\r\n const excludedSentences = filterInput\r\n .split(\",\")\r\n .map((s) => s.trim().toUpperCase())\r\n .filter((s) => s.length > 0);\r\n\r\n await this.saveConfig(\r\n \"sentence_filter.json\",\r\n { excludedSentences },\r\n \"sentenceFilterConfig\",\r\n \"Sentence filter\"\r\n );\r\n }\r\n\r\n async savePluginConfig() {\r\n const editor = document.getElementById(\"pluginConfigJson\") as HTMLTextAreaElement | null;\r\n if (!editor) {\r\n return;\r\n }\r\n\r\n try {\r\n const parsedConfig = JSON.parse(editor.value);\r\n if (!parsedConfig || typeof parsedConfig !== \"object\" || Array.isArray(parsedConfig)) {\r\n throw new Error(\"Plugin configuration must be a JSON object\");\r\n }\r\n\r\n const requestConfig = this.deepClone(parsedConfig) as Record<string, unknown>;\r\n const normalizedServerType = this.normalizeServerType(requestConfig.serverType);\r\n if (normalizedServerType) {\r\n requestConfig.serverType = normalizedServerType;\r\n }\r\n\r\n const response = await this.request(`${API_BASE_PATH}/plugin-config`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(requestConfig)\r\n });\r\n\r\n const result = await response.json().catch(() => ({}) as Record<string, unknown>);\r\n if (!response.ok || !result.success) {\r\n throw new Error(result.error || `Failed to save plugin configuration (${response.status})`);\r\n }\r\n\r\n this.pluginConfig = this.buildCompletePluginConfig(requestConfig);\r\n this.updatePluginConfigUI();\r\n this.showNotification(\r\n result.message || \"Plugin configuration saved. Refresh to apply changes.\",\r\n \"success\"\r\n );\r\n } catch (error: unknown) {\r\n const err = error as AuthenticatedError;\r\n this.showNotification(\r\n err.isUnauthorized\r\n ? this.authFailureMessage(\"saving full plugin config\")\r\n : \"Error saving full plugin config: \" + err.message,\r\n \"error\"\r\n );\r\n }\r\n }\r\n\r\n async reloadPluginConfiguration() {\r\n const loaded = await this.loadPluginConfiguration(true);\r\n if (loaded) {\r\n this.updatePluginConfigUI();\r\n this.showNotification(\"Plugin configuration reloaded.\", \"success\");\r\n }\r\n }\r\n\r\n loadDefaultPluginConfiguration() {\r\n this.pluginConfig = this.buildCompletePluginConfig({});\r\n this.updatePluginConfigUI();\r\n this.showNotification(\"Loaded schema defaults into editor. Save to apply.\", \"warning\");\r\n }\r\n\r\n // ── Metrics display ────────────────────────────────────────────────────────\r\n\r\n updateMetricsDisplay(metrics: Record<string, any>) {\r\n this.updateNetworkQualityDisplay(metrics);\r\n this.updateBandwidthDisplay(metrics);\r\n this.updatePathAnalyticsDisplay(metrics);\r\n\r\n const metricsDiv = document.getElementById(\"metrics\");\r\n if (!metricsDiv) {\r\n return;\r\n }\r\n\r\n const isClient = metrics.mode === \"client\";\r\n const { stats, status, uptime } = metrics;\n\n const cryptoErrors = (stats.errorCounts && stats.errorCounts.crypto) || 0;\n const malformedPackets = stats.malformedPackets || 0;\n const dataPacketsReceived = stats.dataPacketsReceived || 0;\n const rateLimitedPackets = stats.rateLimitedPackets || 0;\n const droppedDeltaBatches = stats.droppedDeltaBatches || 0;\n const droppedDeltaCount = stats.droppedDeltaCount || 0;\n\n const hasErrors =\n stats.udpSendErrors > 0 ||\n stats.compressionErrors > 0 ||\n stats.encryptionErrors > 0 ||\n stats.subscriptionErrors > 0 ||\n cryptoErrors > 0 ||\n malformedPackets > 0 ||\n rateLimitedPackets > 0 ||\n droppedDeltaCount > 0;\n\r\n const protocolVersion = metrics.protocolVersion || 1;\r\n const protocolLabel = protocolVersion >= 2 ? `v${protocolVersion}` : \"v1\";\r\n\r\n const metricsGridItems = [\r\n renderMetricItem(\"Uptime\", uptime.formatted),\r\n renderMetricItem(\"Mode\", isClient ? \"Client\" : \"Server\"),\r\n renderMetricItemHtml(\r\n \"Protocol\",\r\n `<span class=\"protocol-badge protocol-${escapeHtml(protocolLabel)}\">${escapeHtml(protocolLabel.toUpperCase())}</span>`\r\n ),\r\n renderMetricItem(\r\n \"Status\",\r\n status.readyToSend ? \"Ready\" : \"Not Ready\",\r\n status.readyToSend ? \"success\" : \"error\"\r\n ),\r\n isClient ? renderMetricItem(\"Buffered Deltas\", status.deltasBuffered) : \"\"\r\n ].join(\"\");\r\n\r\n const subErrors = stats.subscriptionErrors;\r\n const subscriptionErrorStat = isClient\r\n ? renderStatItem(\"Subscription Errors\", subErrors, subErrors > 0)\r\n : \"\";\r\n\r\n const statsItems = [\r\n isClient\n ? renderStatItem(\"Deltas Sent\", stats.deltasSent.toLocaleString())\n : renderStatItem(\"Deltas Received\", stats.deltasReceived.toLocaleString()),\n !isClient\n ? renderStatItem(\"Data Packets Received\", dataPacketsReceived.toLocaleString())\n : \"\",\n isClient\n ? renderStatItem(\"UDP Send Errors\", stats.udpSendErrors, stats.udpSendErrors > 0)\n : \"\",\n isClient ? renderStatItem(\"UDP Retries\", stats.udpRetries) : \"\",\n !isClient\n ? renderStatItem(\n \"Rate-Limited Packets\",\n rateLimitedPackets.toLocaleString(),\n rateLimitedPackets > 0\n )\n : \"\",\n isClient\n ? renderStatItem(\n \"Dropped Delta Batches\",\n droppedDeltaBatches.toLocaleString(),\n droppedDeltaBatches > 0\n )\n : \"\",\n isClient\n ? renderStatItem(\n \"Dropped Deltas\",\n droppedDeltaCount.toLocaleString(),\n droppedDeltaCount > 0\n )\n : \"\",\n renderStatItem(\"Compression Errors\", stats.compressionErrors, stats.compressionErrors > 0),\n renderStatItem(\"Encryption Errors\", stats.encryptionErrors, stats.encryptionErrors > 0),\n subscriptionErrorStat,\n !isClient && stats.duplicatePackets > 0\r\n ? renderStatItem(\"Duplicate Packets\", stats.duplicatePackets.toLocaleString())\r\n : \"\",\r\n protocolVersion >= 3\r\n ? renderStatItem(\"Auth Failures (V3)\", cryptoErrors, cryptoErrors > 0)\r\n : \"\",\r\n renderStatItem(\"Malformed Packets\", malformedPackets, malformedPackets > 0)\r\n ].join(\"\");\r\n\r\n let metricsHtml = `\r\n <h4>Performance Metrics</h4>\r\n <div class=\"metrics-grid\">${metricsGridItems}</div>\r\n <div class=\"metrics-stats\">\r\n <h5>Transmission Statistics</h5>\r\n <div class=\"stats-grid\">${statsItems}</div>\r\n </div>\r\n `;\r\n\r\n if (isClient && metrics.smartBatching) {\r\n const sb = metrics.smartBatching;\r\n const totalSends = sb.earlySends + sb.timerSends;\r\n const earlyPercent = totalSends > 0 ? Math.round((sb.earlySends / totalSends) * 100) : 0;\r\n\r\n metricsHtml += `\r\n <div class=\"metrics-stats\">\r\n <h5>Smart Batching</h5>\r\n <div class=\"stats-grid\">\r\n ${renderStatItem(\"Avg Bytes/Delta\", sb.avgBytesPerDelta + \" bytes\")}\r\n ${renderStatItem(\"Max Deltas/Batch\", sb.maxDeltasPerBatch)}\r\n ${renderStatItem(\"Early Sends\", sb.earlySends.toLocaleString() + \" (\" + earlyPercent + \"%)\")}\r\n ${renderStatItem(\"Timer Sends\", sb.timerSends.toLocaleString())}\r\n ${renderStatItem(\"Oversized Packets\", sb.oversizedPackets, sb.oversizedPackets > 0)}\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n if (Array.isArray(metrics.recentErrors) && metrics.recentErrors.length > 0) {\r\n const errorItems = [...metrics.recentErrors]\r\n .reverse()\r\n .map((err) => {\r\n const timeAgo = Date.now() - err.timestamp;\r\n const timeStr =\r\n timeAgo < 60000\r\n ? `${Math.floor(timeAgo / 1000)}s ago`\r\n : `${Math.floor(timeAgo / 60000)}m ago`;\r\n return `\r\n <div class=\"recent-error-item\">\r\n <span class=\"error-category-badge\">${this.escapeHtml(err.category)}</span>\r\n <span class=\"recent-error-msg\">${this.escapeHtml(err.message)}</span>\r\n <span class=\"recent-error-time\">${this.escapeHtml(timeStr)}</span>\r\n </div>\r\n `;\r\n })\r\n .join(\"\");\r\n metricsHtml += `\r\n <div class=\"metrics-error\">\r\n <h5>Recent Errors (${metrics.recentErrors.length})</h5>\r\n <div class=\"recent-errors-list\">${errorItems}</div>\r\n </div>\r\n `;\r\n } else if (metrics.lastError) {\r\n const timeAgo = metrics.lastError.timeAgo;\r\n const timeAgoStr =\r\n timeAgo < 60000\r\n ? `${Math.floor(timeAgo / 1000)}s ago`\r\n : `${Math.floor(timeAgo / 60000)}m ago`;\r\n\r\n metricsHtml += `\r\n <div class=\"metrics-error\">\r\n <h5>Last Error</h5>\r\n <div class=\"error-message\">${this.escapeHtml(metrics.lastError.message)}</div>\r\n <div class=\"error-time\">Occurred ${timeAgoStr}</div>\r\n </div>\r\n `;\r\n } else if (!hasErrors) {\r\n metricsHtml += `\r\n <div class=\"metrics-success\">\r\n <div class=\"success-message\">No errors detected</div>\r\n </div>\r\n `;\r\n }\r\n\r\n metricsDiv.innerHTML = metricsHtml;\r\n }\r\n\r\n updateNetworkQualityDisplay(metrics: Record<string, any>) {\r\n const nqDiv = document.getElementById(\"networkQuality\");\r\n if (!nqDiv || !metrics.networkQuality) {\r\n return;\r\n }\r\n\r\n const nq = metrics.networkQuality;\r\n const isClient = metrics.mode === \"client\";\r\n\r\n let qualityLabel = \"N/A\";\r\n let qualityColor = \"#9E9E9E\";\r\n if (nq.linkQuality !== undefined) {\r\n if (nq.linkQuality >= 90) {\r\n qualityLabel = \"Excellent\";\r\n qualityColor = \"#4CAF50\";\r\n } else if (nq.linkQuality >= 70) {\r\n qualityLabel = \"Good\";\r\n qualityColor = \"#FFC107\";\r\n } else if (nq.linkQuality >= 50) {\r\n qualityLabel = \"Fair\";\r\n qualityColor = \"#FF9800\";\r\n } else {\r\n qualityLabel = \"Poor\";\r\n qualityColor = \"#F44336\";\r\n }\r\n }\r\n\r\n const qualityPct = nq.linkQuality !== undefined ? nq.linkQuality : 0;\r\n const gaugeAngle = (qualityPct / 100) * 180;\r\n const radStart = Math.PI;\r\n const radEnd = radStart + (gaugeAngle * Math.PI) / 180;\r\n const cx = 50,\r\n cy = 50,\r\n r = 40;\r\n const x1 = cx + r * Math.cos(radStart);\r\n const y1 = cy + r * Math.sin(radStart);\r\n const x2 = cx + r * Math.cos(radEnd);\r\n const y2 = cy + r * Math.sin(radEnd);\r\n const largeArc = gaugeAngle > 180 ? 1 : 0;\r\n\r\n const gaugeArcPath =\r\n qualityPct > 0\r\n ? `<path d=\"M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2}\"\r\n fill=\"none\" stroke=\"${qualityColor}\" stroke-width=\"8\" stroke-linecap=\"round\"/>`\r\n : \"\";\r\n\r\n const gaugeSvg = `\r\n <svg viewBox=\"0 0 100 55\" class=\"quality-gauge\" preserveAspectRatio=\"xMidYMid meet\">\r\n <path d=\"M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}\"\r\n fill=\"none\" stroke=\"#E0E0E0\" stroke-width=\"8\" stroke-linecap=\"round\"/>\r\n ${gaugeArcPath}\r\n <text x=\"${cx}\" y=\"${cy - 5}\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"bold\" fill=\"${qualityColor}\">\r\n ${qualityPct}\r\n </text>\r\n <text x=\"${cx}\" y=\"${cy + 8}\" text-anchor=\"middle\" font-size=\"7\" fill=\"#666\">\r\n ${qualityLabel}\r\n </text>\r\n </svg>\r\n `;\r\n\n const rttDisplay = nq.rtt !== undefined ? nq.rtt + \" ms\" : \"N/A\";\n const jitterDisplay = nq.jitter !== undefined ? nq.jitter + \" ms\" : \"N/A\";\n const packetLoss = Number.isFinite(nq.packetLoss) ? nq.packetLoss : 0;\n const retransmitRate = Number.isFinite(nq.retransmitRate) ? nq.retransmitRate : 0;\n const packetLossDisplay = this.formatRatioPercent(packetLoss);\n const retransmitRateDisplay = this.formatRatioPercent(retransmitRate);\n const dataSourceDisplay = nq.dataSource || \"local\";\n\n let nqHtml = `\n <div class=\"network-quality-dashboard\">\n <div class=\"nq-hero\">\n <div class=\"nq-gauge-container\">\n ${gaugeSvg}\r\n <div class=\"nq-gauge-label\">Link Quality</div>\r\n </div>\r\n <div class=\"nq-key-metrics\">\n ${renderMetricItem(\"RTT\", rttDisplay, nq.rtt > 500 ? \"error\" : nq.rtt > 200 ? \"warning\" : \"\")}\n ${renderMetricItem(\"Jitter\", jitterDisplay, nq.jitter > 100 ? \"error\" : nq.jitter > 50 ? \"warning\" : \"\")}\n ${renderMetricItem(\n \"Packet Loss\",\n packetLossDisplay,\n packetLoss > 0.1 ? \"error\" : packetLoss > 0.03 ? \"warning\" : \"\"\n )}\n </div>\n </div>\n\n <div class=\"nq-details\">\n <h5>Reliability Statistics</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Data Source\", dataSourceDisplay)}\n ${nq.activeLink ? renderStatItem(\"Active Link\", nq.activeLink) : \"\"}\n ${renderStatItem(\"Retransmit Rate\", retransmitRateDisplay, retransmitRate > 0.1)}\n ${\n nq.lastRemoteUpdate\n ? renderStatItem(\"Last Remote Update\", this.formatTimestampAge(nq.lastRemoteUpdate))\n : \"\"\n }\n `;\n\n if (isClient) {\n nqHtml += `\r\n ${renderStatItem(\"Retransmissions\", (nq.retransmissions || 0).toLocaleString(), nq.retransmissions > 0)}\r\n ${renderStatItem(\"Queue Depth\", (nq.queueDepth || 0).toLocaleString(), nq.queueDepth > 100)}\r\n `;\r\n } else {\r\n nqHtml += `\r\n ${renderStatItem(\"ACKs Sent\", (nq.acksSent || 0).toLocaleString())}\r\n ${renderStatItem(\"NAKs Sent\", (nq.naksSent || 0).toLocaleString(), nq.naksSent > 0)}\r\n `;\r\n }\r\n\r\n nqHtml += `\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n\r\n nqDiv.innerHTML = nqHtml;\r\n }\r\n\r\n updateBandwidthDisplay(metrics: Record<string, any>) {\r\n const bandwidthDiv = document.getElementById(\"bandwidth\");\r\n if (!bandwidthDiv || !metrics.bandwidth) {\r\n return;\r\n }\r\n\r\n const bw = metrics.bandwidth;\n const isClient = metrics.mode === \"client\";\n\n const savedBytes = isClient ? bw.bytesOutRaw - bw.bytesOut : bw.bytesInRaw - bw.bytesIn;\n const savedFormatted = this.formatBytes(savedBytes > 0 ? savedBytes : 0);\n const metaBytesOut = bw.metaBytesOut || 0;\n const metaBytesIn = bw.metaBytesIn || 0;\n const metaPacketsOut = bw.metaPacketsOut || 0;\n const metaPacketsIn = bw.metaPacketsIn || 0;\n const metaSnapshotsSent = bw.metaSnapshotsSent || 0;\n const metaDiffsSent = bw.metaDiffsSent || 0;\n const metaRateLimitedPackets = bw.metaRateLimitedPackets || 0;\n\n let bandwidthStats: string[];\n let metadataStats: string[];\n if (isClient) {\n bandwidthStats = [\n renderBwStat(\"Total Sent (Compressed)\", bw.bytesOutFormatted),\n renderBwStat(\"Total Raw (Before Compression)\", bw.bytesOutRawFormatted),\n renderBwStat(\"Bandwidth Saved\", savedFormatted, true, true),\n renderBwStat(\"Packets Sent\", bw.packetsOut.toLocaleString())\n ];\n metadataStats = [\n renderBwStat(\n \"Metadata Sent\",\n bw.metaBytesOutFormatted || this.formatBytes(metaBytesOut)\n ),\n renderBwStat(\"Metadata Packets Sent\", metaPacketsOut.toLocaleString()),\n renderBwStat(\"Metadata Snapshots Sent\", metaSnapshotsSent.toLocaleString()),\n renderBwStat(\"Metadata Diffs Sent\", metaDiffsSent.toLocaleString())\n ];\n } else {\n bandwidthStats = [\n renderBwStat(\"Total Received (Compressed)\", bw.bytesInFormatted),\n renderBwStat(\n \"Total Raw (After Decompression)\",\n bw.bytesInRawFormatted || this.formatBytes(bw.bytesInRaw || 0)\n ),\n renderBwStat(\"Bandwidth Saved\", savedFormatted, true, true),\n renderBwStat(\"Packets Received\", bw.packetsIn.toLocaleString())\n ];\n metadataStats = [\n renderBwStat(\n \"Metadata Received\",\n bw.metaBytesInFormatted || this.formatBytes(metaBytesIn)\n ),\n renderBwStat(\"Metadata Packets Received\", metaPacketsIn.toLocaleString()),\n renderBwStat(\"Metadata Rate-Limited\", metaRateLimitedPackets.toLocaleString())\n ];\n }\n\n const bandwidthHtml = `\n <div class=\"bandwidth-dashboard\">\r\n <div class=\"bandwidth-hero\">\r\n <div class=\"hero-stat ${isClient ? \"primary\" : \"secondary\"}\">\r\n <div class=\"hero-value\">${isClient ? bw.rateOutFormatted : bw.rateInFormatted}</div>\r\n <div class=\"hero-label\">${isClient ? \"Upload Rate\" : \"Download Rate\"}</div>\r\n </div>\r\n <div class=\"hero-stat success\">\r\n <div class=\"hero-value\">${bw.compressionRatio}%</div>\r\n <div class=\"hero-label\">Compression Ratio</div>\r\n </div>\r\n <div class=\"hero-stat\">\r\n <div class=\"hero-value\">${bw.avgPacketSizeFormatted}</div>\r\n <div class=\"hero-label\">Avg Packet Size</div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"bandwidth-details\">\r\n <h5>Bandwidth Details</h5>\n <div class=\"bandwidth-grid\">${bandwidthStats.join(\"\")}</div>\n </div>\n\n <div class=\"bandwidth-details metadata-details\">\n <h5>Metadata Traffic</h5>\n <div class=\"bandwidth-grid\">${metadataStats.join(\"\")}</div>\n </div>\n\n ${this.renderBandwidthChart(bw.history, isClient)}\n </div>\n `;\n\r\n bandwidthDiv.innerHTML = bandwidthHtml;\r\n }\r\n\r\n renderBandwidthChart(\r\n history: Array<{ rateOut: number; rateIn: number }> | undefined,\r\n isClient: boolean\r\n ): string {\r\n if (!history || history.length < 2) {\r\n return `\r\n <div class=\"bandwidth-chart-placeholder\">\r\n <p>Collecting data for chart... (${history ? history.length : 0}/2 points)</p>\r\n </div>\r\n `;\r\n }\r\n\r\n const width = 100;\r\n const height = 40;\r\n const maxRate = Math.max(...history.map((h) => (isClient ? h.rateOut : h.rateIn)), 1);\r\n const points = history\r\n .map((h, i) => {\r\n const x = (i / (history.length - 1)) * width;\r\n const y = height - ((isClient ? h.rateOut : h.rateIn) / maxRate) * height;\r\n return `${x},${y}`;\r\n })\r\n .join(\" \");\r\n\r\n const maxRateFormatted = this.formatBytes(maxRate);\r\n const intervalSeconds = METRICS_REFRESH_INTERVAL / 1000;\r\n\r\n return `\r\n <div class=\"bandwidth-chart\">\r\n <h5>Rate History (Last ${history.length * intervalSeconds}s)</h5>\r\n <div class=\"chart-container\">\r\n <svg viewBox=\"0 0 ${width} ${height}\" class=\"sparkline\" preserveAspectRatio=\"none\">\r\n <polyline\r\n fill=\"none\"\r\n stroke=\"var(--primary-color)\"\r\n stroke-width=\"1.5\"\r\n points=\"${points}\"\r\n />\r\n </svg>\r\n <div class=\"chart-labels\">\r\n <span class=\"chart-max\">${maxRateFormatted}/s</span>\r\n <span class=\"chart-min\">0</span>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n updatePathAnalyticsDisplay(metrics: Record<string, any>) {\r\n const pathDiv = document.getElementById(\"pathAnalytics\");\r\n if (!pathDiv || !metrics.pathStats) {\r\n return;\r\n }\r\n\r\n const paths: Array<{\r\n path: string;\r\n updatesPerMinute: number;\r\n bytesFormatted: string;\r\n percentage: number;\r\n }> = metrics.pathStats;\r\n\r\n if (paths.length === 0) {\r\n pathDiv.innerHTML = `\r\n <div class=\"path-analytics-empty\">\r\n <p>No path data collected yet. Data will appear once deltas are transmitted.</p>\r\n </div>\r\n `;\r\n return;\r\n }\r\n\r\n const categoryCount = new Set(paths.map((p) => p.path.split(\".\")[0])).size;\r\n\r\n let pathHtml = `\r\n <div class=\"path-analytics-dashboard\">\r\n <div class=\"path-summary\">\r\n <div class=\"summary-stat\">\r\n <span class=\"summary-value\">${paths.length}</span>\r\n <span class=\"summary-label\">Active Paths</span>\r\n </div>\r\n <div class=\"summary-stat\">\r\n <span class=\"summary-value\">${categoryCount}</span>\r\n <span class=\"summary-label\">Categories</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"path-table-container\">\r\n <table class=\"path-table\">\r\n <thead>\r\n <tr>\r\n <th>Path</th>\r\n <th>Updates/min</th>\r\n <th>Data Volume</th>\r\n <th>% of Total</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n `;\r\n\r\n paths.slice(0, 15).forEach((p) => {\r\n const barWidth = Math.max(p.percentage, 2);\r\n pathHtml += `\r\n <tr>\r\n <td class=\"path-name\" title=\"${this.escapeHtml(p.path)}\">${this.escapeHtml(p.path)}</td>\r\n <td class=\"path-rate\">${p.updatesPerMinute}</td>\r\n <td class=\"path-bytes\">${p.bytesFormatted}</td>\r\n <td class=\"path-percentage\">\r\n <div class=\"percentage-bar-container\">\r\n <div class=\"percentage-bar\" style=\"width: ${barWidth}%\"></div>\r\n <span class=\"percentage-text\">${p.percentage}%</span>\r\n </div>\r\n </td>\r\n </tr>\r\n `;\r\n });\r\n\r\n pathHtml += `\r\n </tbody>\r\n </table>\r\n </div>\r\n `;\r\n\r\n if (paths.length > 15) {\r\n pathHtml += `\r\n <div class=\"path-more\">\r\n <p>Showing top 15 of ${paths.length} paths</p>\r\n </div>\r\n `;\r\n }\r\n\r\n pathHtml += \"</div>\";\r\n pathDiv.innerHTML = pathHtml;\r\n }\r\n\r\n updateCongestionDisplay(data: Record<string, any>) {\r\n const section = document.getElementById(\"congestionSection\");\r\n const div = document.getElementById(\"congestionControl\");\r\n if (!section || !div) {\r\n return;\r\n }\r\n\r\n section.style.display = \"\";\r\n\r\n const enabled = !!data.enabled;\r\n const manualMode = !!data.manualMode;\r\n const stateLabel = enabled ? (manualMode ? \"manual\" : \"active\") : \"disabled\";\r\n const stateClass = enabled ? (manualMode ? \"warning\" : \"success\") : \"error\";\r\n const modeLabel = manualMode ? \"Manual Override\" : \"Automatic\";\r\n const currentDeltaTimer = data.currentDeltaTimer || 0;\r\n const nominalDeltaTimer = data.nominalDeltaTimer || 0;\r\n\r\n const html = `\r\n <div class=\"v2-dashboard\">\r\n <div class=\"metrics-grid\">\r\n ${renderMetricItemHtml(\"State\", `<span class=\"congestion-state ${stateClass}\">${stateLabel}</span>`)}\r\n ${renderMetricItem(\"Mode\", modeLabel)}\r\n ${renderMetricItem(\"Current Timer\", currentDeltaTimer + \" ms\")}\r\n ${renderMetricItem(\"Nominal Timer\", nominalDeltaTimer + \" ms\")}\r\n </div>\r\n <div class=\"metrics-stats\">\r\n <h5>Congestion Details</h5>\r\n <div class=\"stats-grid\">\r\n ${renderStatItem(\"Min Delta Timer\", (data.minDeltaTimer || 0) + \" ms\")}\r\n ${renderStatItem(\"Max Delta Timer\", (data.maxDeltaTimer || 0) + \" ms\")}\r\n ${renderStatItem(\"Target RTT\", (data.targetRTT || 0) + \" ms\")}\r\n ${renderStatItem(\"Avg RTT\", (data.avgRTT !== undefined ? Math.round(data.avgRTT) : 0) + \" ms\")}\r\n ${renderStatItem(\"Avg Packet Loss\", (data.avgLoss !== undefined ? (data.avgLoss * 100).toFixed(1) : 0) + \"%\", data.avgLoss > 0.05)}\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n\r\n div.innerHTML = html;\r\n }\r\n\r\n updateBondingDisplay(data: Record<string, any>) {\r\n const section = document.getElementById(\"bondingSection\");\r\n const div = document.getElementById(\"bondingStatus\");\r\n if (!section || !div) {\r\n return;\r\n }\r\n\r\n if (!data.enabled) {\r\n section.style.display = \"none\";\r\n return;\r\n }\r\n\r\n section.style.display = \"\";\r\n\r\n const modeLabel = (data.mode || \"main-backup\").replace(/-/g, \" \");\r\n const activeLink = data.activeLink || \"primary\";\r\n\r\n let linksHtml = \"\";\r\n if (data.links) {\r\n const linkEntries = Object.entries(data.links) as Array<[string, Record<string, any>]>;\r\n linksHtml = linkEntries\r\n .map(([name, link]) => {\r\n const isActive = name === activeLink;\r\n const status = (link.status || \"unknown\").toLowerCase();\r\n const isUp = status !== \"down\";\r\n const aliveClass = isUp ? \"success\" : \"error\";\r\n return `\r\n <div class=\"bonding-link ${isActive ? \"active\" : \"\"}\">\r\n <div class=\"link-header\">\r\n <span class=\"link-name\">${this.escapeHtml(name)}</span>\r\n ${isActive ? '<span class=\"link-badge active-badge\">ACTIVE</span>' : \"\"}\r\n <span class=\"link-badge ${aliveClass}\">${this.escapeHtml(status.toUpperCase())}</span>\r\n </div>\r\n <div class=\"link-stats\">\r\n ${renderStatItem(\"RTT\", (link.rtt || 0) + \" ms\")}\r\n ${renderStatItem(\"Packet Loss\", ((link.loss || 0) * 100).toFixed(1) + \"%\")}\r\n </div>\r\n </div>\r\n `;\r\n })\r\n .join(\"\");\r\n }\r\n\r\n const html = `\r\n <div class=\"v2-dashboard\">\r\n <div class=\"metrics-grid\">\r\n ${renderMetricItem(\"Mode\", modeLabel)}\r\n ${renderMetricItem(\"Active Link\", activeLink)}\r\n </div>\r\n <div class=\"bonding-links\">${linksHtml}</div>\r\n <div style=\"margin-top: 1rem;\">\r\n <button id=\"failoverBtn\" class=\"btn btn-secondary\">Force Failover</button>\r\n </div>\r\n </div>\r\n `;\r\n\r\n div.innerHTML = html;\r\n\r\n const failoverBtn = document.getElementById(\"failoverBtn\");\r\n if (failoverBtn) {\r\n failoverBtn.addEventListener(\"click\", () => this.triggerFailover());\r\n }\r\n }\r\n\r\n async triggerFailover() {\r\n const connId = this.activeConnectionId!;\r\n const btn = document.getElementById(\"failoverBtn\") as HTMLButtonElement | null;\r\n if (btn) {\r\n btn.disabled = true;\r\n }\r\n try {\r\n const response = await this.request(this.bondingFailoverPath(connId), { method: \"POST\" });\r\n if (response.ok) {\r\n const result = await response.json();\r\n this.showNotification(`Failover complete. Active link: ${result.activeLink}`, \"success\");\r\n this.loadMetrics(connId);\r\n } else {\r\n const err = await response.json();\r\n this.showNotification(\r\n response.status === 401\r\n ? this.authFailureMessage(\"triggering failover\")\r\n : \"Failover failed: \" + (err.error || \"Unknown error\"),\r\n \"error\"\r\n );\r\n }\r\n } catch (error: unknown) {\r\n const err = error as AuthenticatedError;\r\n this.showNotification(\r\n err.isUnauthorized\r\n ? this.authFailureMessage(\"triggering failover\")\r\n : \"Failover failed: \" + err.message,\r\n \"error\"\r\n );\r\n } finally {\r\n if (btn) {\r\n btn.disabled = false;\r\n }\r\n }\r\n }\r\n\r\n updateMonitoringDisplay(data: Record<string, any>) {\r\n const section = document.getElementById(\"monitoringSection\");\r\n const div = document.getElementById(\"monitoringAlerts\");\r\n if (!section || !div) {\r\n return;\r\n }\r\n\r\n const hasData = data.alerts || data.packetLoss || data.retransmissions;\r\n if (!hasData) {\r\n section.style.display = \"none\";\r\n return;\r\n }\r\n\r\n section.style.display = \"\";\r\n\r\n let html = '<div class=\"v2-dashboard\">';\r\n\r\n if (data.alerts) {\r\n const activeAlerts: Record<string, any> = data.alerts.activeAlerts || {};\r\n const alertEntries = Object.entries(activeAlerts).map(([metric, alert]: [string, any]) => {\r\n let level = \"warning\";\r\n if (typeof alert === \"string\") {\r\n level = alert.toLowerCase();\r\n } else if (alert && typeof alert === \"object\" && alert.level) {\r\n level = String(alert.level).toLowerCase();\r\n }\r\n\r\n if (level === \"warn\") {\r\n level = \"warning\";\r\n }\r\n if (level === \"alert\") {\r\n level = \"critical\";\r\n }\r\n if (level !== \"warning\" && level !== \"critical\") {\r\n level = \"warning\";\r\n }\r\n\r\n return {\r\n metric,\r\n level,\r\n value: alert && typeof alert === \"object\" ? alert.value : undefined\r\n };\r\n });\r\n const alertCount = alertEntries.length;\r\n\r\n const alertItems = alertEntries\r\n .map((entry) => {\r\n const alertValue =\r\n entry.value !== undefined ? ` (${this.escapeHtml(String(entry.value))})` : \"\";\r\n return renderStatItemHtml(\r\n entry.metric,\r\n `<span class=\"alert-level alert-${entry.level}\">${this.escapeHtml(entry.level.toUpperCase())}${alertValue}</span>`,\r\n entry.level === \"critical\"\r\n );\r\n })\r\n .join(\"\");\r\n\r\n const alertsContent =\r\n alertCount === 0\r\n ? '<div class=\"metrics-success\"><div class=\"success-message\">No active alerts</div></div>'\r\n : `<div class=\"stats-grid\">${alertItems}</div>`;\r\n\r\n html += `\r\n <div class=\"monitoring-subsection\">\r\n <h5>Active Alerts</h5>\r\n ${alertsContent}\r\n </div>\r\n `;\r\n }\r\n\r\n if (data.packetLoss && data.packetLoss.summary) {\r\n const summary = data.packetLoss.summary;\r\n html += `\r\n <div class=\"monitoring-subsection\">\r\n <h5>Packet Loss</h5>\r\n <div class=\"stats-grid\">\r\n ${renderStatItem(\"Overall Loss Rate\", (summary.overallLossRate * 100).toFixed(2) + \"%\", summary.overallLossRate > 0.05)}\r\n ${renderStatItem(\"Max Loss Rate\", (summary.maxLossRate * 100).toFixed(2) + \"%\", summary.maxLossRate > 0.1)}\r\n ${renderStatItem(\"Trend\", summary.trend || \"stable\")}\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n if (data.retransmissions && data.retransmissions.summary) {\r\n const summary = data.retransmissions.summary;\r\n html += `\r\n <div class=\"monitoring-subsection\">\r\n <h5>Retransmission Rates</h5>\r\n <div class=\"stats-grid\">\r\n ${renderStatItem(\"Current Rate\", (summary.currentRate * 100).toFixed(2) + \"%\", summary.currentRate > 0.05)}\r\n ${renderStatItem(\"Average Rate\", (summary.avgRate * 100).toFixed(2) + \"%\")}\r\n ${renderStatItem(\"Max Rate\", (summary.maxRate * 100).toFixed(2) + \"%\", summary.maxRate > 0.1)}\r\n </div>\r\n </div>\r\n `;\r\n }\r\n\r\n html += \"</div>\";\r\n div.innerHTML = html;\r\n }\r\n\r\n // ── Status display ─────────────────────────────────────────────────────────\r\n\r\n updateStatus() {\r\n const statusDiv = document.getElementById(\"status\");\r\n if (!statusDiv) {\r\n return;\r\n }\r\n\r\n let statusHtml = \"<h4>Configuration Status</h4>\";\r\n\r\n if (this.deltaTimerConfig) {\r\n statusHtml += `\r\n <div class=\"status-item\">\r\n <strong>Delta Timer:</strong> ${this.escapeHtml(String((this.deltaTimerConfig as Record<string, unknown>).deltaTimer))}ms\r\n <span class=\"status-indicator success\">Configured</span>\r\n </div>\r\n `;\r\n } else {\r\n statusHtml += `\r\n <div class=\"status-item\">\r\n <strong>Delta Timer:</strong>\r\n <span class=\"status-indicator warning\">Not configured</span>\r\n </div>\r\n `;\r\n }\r\n\r\n if (this.subscriptionConfig && (this.subscriptionConfig as Record<string, unknown>).subscribe) {\r\n const cfg = this.subscriptionConfig as Record<string, unknown>;\r\n const pathCount = (cfg.subscribe as unknown[]).length;\r\n const escapedContext = this.escapeHtml((cfg.context as string) || \"\");\r\n const escapedPaths = (cfg.subscribe as Array<{ path: string }>)\r\n .map((s) => this.escapeHtml(s.path))\r\n .join(\", \");\r\n statusHtml += `\r\n <div class=\"status-item\">\r\n <strong>Subscriptions:</strong> ${pathCount} path(s) configured\r\n <span class=\"status-indicator success\">Configured</span>\r\n </div>\r\n <div class=\"status-details\">\r\n <strong>Context:</strong> ${escapedContext}<br>\r\n <strong>Paths:</strong> ${escapedPaths}\r\n </div>\r\n `;\r\n } else {\r\n statusHtml += `\r\n <div class=\"status-item\">\r\n <strong>Subscriptions:</strong>\r\n <span class=\"status-indicator warning\">Not configured</span>\r\n </div>\r\n `;\r\n }\r\n\r\n if (\r\n this.sentenceFilterConfig &&\r\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences &&\r\n ((this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[])\r\n .length > 0\r\n ) {\r\n const filterCount = (\r\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\r\n ).length;\r\n const escapedFilters = (\r\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\r\n )\r\n .map((s) => this.escapeHtml(s))\r\n .join(\", \");\r\n statusHtml += `\r\n <div class=\"status-item\">\r\n <strong>Sentence Filter:</strong> ${filterCount} sentence(s) excluded\r\n <span class=\"status-indicator success\">Configured</span>\r\n </div>\r\n <div class=\"status-details\">\r\n <strong>Excluded:</strong> ${escapedFilters}\r\n </div>\r\n `;\r\n }\r\n\r\n statusDiv.innerHTML = statusHtml;\r\n }\r\n\r\n // ── Utility methods ────────────────────────────────────────────────────────\r\n\r\n buildCompletePluginConfig(currentConfig: Record<string, unknown>): Record<string, unknown> {\r\n const defaults = this.extractSchemaDefaults(this.pluginSchema);\r\n const merged = this.deepMerge(defaults || {}, currentConfig || {}) as Record<string, unknown>;\r\n if (Array.isArray(merged.connections)) {\r\n return merged;\r\n }\r\n const normalizedServerType = this.normalizeServerType(merged.serverType);\r\n merged.serverType = normalizedServerType || \"client\";\r\n return merged;\r\n }\r\n\r\n normalizeServerType(value: unknown): string | undefined {\r\n if (value === true || value === \"server\") {\r\n return \"server\";\r\n }\r\n if (value === false || value === \"client\") {\r\n return \"client\";\r\n }\r\n return undefined;\r\n }\r\n\r\n extractSchemaDefaults(\r\n schemaNode: Record<string, any> | null\r\n ): Record<string, unknown> | undefined {\r\n if (!schemaNode || typeof schemaNode !== \"object\") {\r\n return undefined;\r\n }\r\n\r\n const isObjectNode = schemaNode.type === \"object\" || !!schemaNode.properties;\r\n const merged: Record<string, unknown> = {};\r\n let hasData = false;\r\n\r\n if (isObjectNode && schemaNode.default && this.isPlainObject(schemaNode.default)) {\r\n Object.assign(merged, this.deepClone(schemaNode.default));\r\n hasData = true;\r\n }\r\n\r\n if (schemaNode.properties && this.isPlainObject(schemaNode.properties)) {\r\n for (const [key, value] of Object.entries(schemaNode.properties)) {\r\n const childDefaults = this.extractSchemaDefaults(value as Record<string, any>);\r\n if (childDefaults !== undefined) {\r\n merged[key] = childDefaults;\r\n hasData = true;\r\n } else if (value && (value as Record<string, any>).type === \"object\") {\r\n merged[key] = {};\r\n hasData = true;\r\n } else if (value && (value as Record<string, any>).type === \"string\") {\r\n merged[key] = \"\";\r\n hasData = true;\r\n }\r\n }\r\n }\r\n\r\n if (schemaNode.dependencies && this.isPlainObject(schemaNode.dependencies)) {\r\n for (const dependencyValue of Object.values(schemaNode.dependencies)) {\r\n const dependencyDefaults = this.extractSchemaDefaults(\r\n dependencyValue as Record<string, any>\r\n );\r\n if (dependencyDefaults && this.isPlainObject(dependencyDefaults)) {\r\n Object.assign(merged, this.deepMerge(merged, dependencyDefaults));\r\n hasData = true;\r\n }\r\n }\r\n }\r\n\r\n for (const compositeKey of [\"oneOf\", \"anyOf\", \"allOf\"]) {\r\n const composite = schemaNode[compositeKey];\r\n if (!Array.isArray(composite)) {\r\n continue;\r\n }\r\n\r\n composite.forEach((item: Record<string, any>) => {\r\n const itemDefaults = this.extractSchemaDefaults(item);\r\n if (itemDefaults === undefined) {\r\n return;\r\n }\r\n\r\n if (this.isPlainObject(itemDefaults)) {\r\n Object.assign(merged, this.deepMerge(merged, itemDefaults));\r\n hasData = true;\r\n return;\r\n }\r\n\r\n if (!hasData && schemaNode.default === undefined) {\r\n return;\r\n }\r\n\r\n hasData = true;\r\n });\r\n }\r\n\r\n if (hasData) {\r\n return merged;\r\n }\r\n\r\n if (schemaNode.default !== undefined) {\r\n return this.deepClone(schemaNode.default) as Record<string, unknown>;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n isPlainObject(value: unknown): value is Record<string, unknown> {\r\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\r\n }\r\n\r\n deepClone(value: unknown): unknown {\r\n if (Array.isArray(value)) {\r\n return value.map((item) => this.deepClone(item));\r\n }\r\n if (this.isPlainObject(value)) {\r\n const clone: Record<string, unknown> = {};\r\n for (const [key, childValue] of Object.entries(value)) {\r\n clone[key] = this.deepClone(childValue);\r\n }\r\n return clone;\r\n }\r\n return value;\r\n }\r\n\r\n deepMerge(baseValue: unknown, overrideValue: unknown): unknown {\r\n if (overrideValue === undefined) {\r\n return this.deepClone(baseValue);\r\n }\r\n if (Array.isArray(overrideValue)) {\r\n return this.deepClone(overrideValue);\r\n }\r\n\r\n if (this.isPlainObject(baseValue) && this.isPlainObject(overrideValue)) {\r\n const merged = this.deepClone(baseValue) as Record<string, unknown>;\r\n for (const [key, value] of Object.entries(overrideValue)) {\r\n merged[key] = this.deepMerge((baseValue as Record<string, unknown>)[key], value);\r\n }\r\n return merged;\r\n }\r\n\r\n return this.deepClone(overrideValue);\r\n }\r\n\r\n formatBytes(bytes: number): string {\n if (!bytes || bytes <= 0) {\n return \"0 B\";\n }\r\n const k = 1024;\r\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\r\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);\r\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n }\n\n formatRatioPercent(value: number): string {\n if (!Number.isFinite(value) || value <= 0) {\n return \"0.0%\";\n }\n return (value * 100).toFixed(1) + \"%\";\n }\n\n formatTimestampAge(timestamp: number): string {\n if (!Number.isFinite(timestamp) || timestamp <= 0) {\n return \"N/A\";\n }\n const ageMs = Math.max(0, Date.now() - timestamp);\n if (ageMs < 1000) {\n return \"just now\";\n }\n if (ageMs < 60000) {\n return `${Math.floor(ageMs / 1000)}s ago`;\n }\n if (ageMs < 3600000) {\n return `${Math.floor(ageMs / 60000)}m ago`;\n }\n return `${Math.floor(ageMs / 3600000)}h ago`;\n }\n\n escapeHtml(text: string): string {\n const div = document.createElement(\"div\");\n div.textContent = text;\r\n return div.innerHTML;\r\n }\r\n\r\n showNotification(message: string, type = \"success\") {\r\n const notification = document.getElementById(\"notification\");\r\n if (!notification) {\r\n return;\r\n }\r\n if (this._notificationTimer) {\r\n clearTimeout(this._notificationTimer);\r\n }\r\n notification.textContent = message;\r\n notification.className = `notification ${type} show`;\r\n\r\n this._notificationTimer = setTimeout(() => {\r\n notification.classList.remove(\"show\");\r\n this._notificationTimer = null;\r\n }, NOTIFICATION_TIMEOUT);\r\n }\r\n}\r\n\r\n// Extend Window interface for the global config instance\r\ndeclare global {\r\n interface Window {\r\n dataConnectorConfig: DataConnectorConfig;\r\n }\r\n}\r\n\r\n// Initialize when DOM is loaded\r\ndocument.addEventListener(\"DOMContentLoaded\", () => {\r\n window.dataConnectorConfig = new DataConnectorConfig();\r\n});\r\n\r\n// Clean up metrics refresh interval when page is hidden or unloaded\r\ndocument.addEventListener(\"visibilitychange\", () => {\r\n if (!window.dataConnectorConfig) {\r\n return;\r\n }\r\n\r\n if (document.hidden) {\r\n if (window.dataConnectorConfig.metricsInterval) {\r\n clearInterval(window.dataConnectorConfig.metricsInterval);\r\n window.dataConnectorConfig.metricsInterval = null;\r\n }\r\n } else if (!window.dataConnectorConfig.metricsInterval) {\r\n window.dataConnectorConfig.loadMetrics();\r\n window.dataConnectorConfig.startMetricsRefresh();\r\n }\r\n});\r\n\r\nwindow.addEventListener(\"beforeunload\", () => {\r\n if (window.dataConnectorConfig && window.dataConnectorConfig.metricsInterval) {\r\n clearInterval(window.dataConnectorConfig.metricsInterval);\r\n window.dataConnectorConfig.metricsInterval = null;\r\n }\r\n});\r\n\r\nexport default DataConnectorConfig;\r\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n// expose the module cache\n__webpack_require__.c = __webpack_module_cache__;\n\n","__webpack_require__.f = {};\n// This file contains only the entry chunk.\n// The chunk loading function for additional chunks\n__webpack_require__.e = (chunkId) => {\n\treturn Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {\n\t\t__webpack_require__.f[key](chunkId, promises);\n\t\treturn promises;\n\t}, []));\n};","// This function allow to reference async chunks\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn \"\" + chunkId + \".\" + \"70a0bc6aadb412703390\" + \".js\";\n};","// This function allow to reference async chunks\n__webpack_require__.miniCssF = (chunkId) => {\n\t// return url for filenames based on template\n\treturn undefined;\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","__webpack_require__.S = {};\nvar initPromises = {};\nvar initTokens = {};\n__webpack_require__.I = (name, initScope) => {\n\tif(!initScope) initScope = [];\n\t// handling circular init calls\n\tvar initToken = initTokens[name];\n\tif(!initToken) initToken = initTokens[name] = {};\n\tif(initScope.indexOf(initToken) >= 0) return;\n\tinitScope.push(initToken);\n\t// only runs once\n\tif(initPromises[name]) return initPromises[name];\n\t// creates a new share scope if needed\n\tif(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};\n\t// runs all init snippets from all modules reachable\n\tvar scope = __webpack_require__.S[name];\n\tvar warn = (msg) => {\n\t\tif (typeof console !== \"undefined\" && console.warn) console.warn(msg);\n\t};\n\tvar uniqueName = \"signalk-edge-link\";\n\tvar register = (name, version, factory, eager) => {\n\t\tvar versions = scope[name] = scope[name] || {};\n\t\tvar activeVersion = versions[version];\n\t\tif(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };\n\t};\n\tvar initExternal = (id) => {\n\t\tvar handleError = (err) => (warn(\"Initialization of sharing external failed: \" + err));\n\t\ttry {\n\t\t\tvar module = __webpack_require__(id);\n\t\t\tif(!module) return;\n\t\t\tvar initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))\n\t\t\tif(module.then) return promises.push(module.then(initFn, handleError));\n\t\t\tvar initResult = initFn(module);\n\t\t\tif(initResult && initResult.then) return promises.push(initResult['catch'](handleError));\n\t\t} catch(err) { handleError(err); }\n\t}\n\tvar promises = [];\n\tswitch(name) {\n\t\tcase \"default\": {\n\t\t\tregister(\"react\", \"16.14.0\", () => (__webpack_require__.e(540).then(() => (() => (__webpack_require__(6540))))));\n\t\t}\n\t\tbreak;\n\t}\n\tif(!promises.length) return initPromises[name] = 1;\n\treturn initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));\n};","var scriptUrl;\nif (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + \"\";\nvar document = __webpack_require__.g.document;\nif (!scriptUrl && document) {\n\tif (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT')\n\t\tscriptUrl = document.currentScript.src;\n\tif (!scriptUrl) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tif(scripts.length) {\n\t\t\tvar i = scripts.length - 1;\n\t\t\twhile (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;\n\t\t}\n\t}\n}\n// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration\n// or pass an empty string (\"\") and set the __webpack_public_path__ variable from your code to use your own logic.\nif (!scriptUrl) throw new Error(\"Automatic publicPath is not supported in this browser\");\nscriptUrl = scriptUrl.replace(/^blob:/, \"\").replace(/#.*$/, \"\").replace(/\\?.*$/, \"\").replace(/\\/[^\\/]+$/, \"/\");\n__webpack_require__.p = scriptUrl;","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t792: 0\n};\n\n__webpack_require__.f.j = (chunkId, promises) => {\n\t\t// JSONP chunk loading for javascript\n\t\tvar installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;\n\t\tif(installedChunkData !== 0) { // 0 means \"already installed\".\n\n\t\t\t// a Promise means \"currently loading\".\n\t\t\tif(installedChunkData) {\n\t\t\t\tpromises.push(installedChunkData[2]);\n\t\t\t} else {\n\t\t\t\tif(true) { // all chunks have JS\n\t\t\t\t\t// setup Promise in chunk cache\n\t\t\t\t\tvar promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));\n\t\t\t\t\tpromises.push(installedChunkData[2] = promise);\n\n\t\t\t\t\t// start chunk loading\n\t\t\t\t\tvar url = __webpack_require__.p + __webpack_require__.u(chunkId);\n\t\t\t\t\t// create error before stack unwound to get useful stacktrace later\n\t\t\t\t\tvar error = new Error();\n\t\t\t\t\tvar loadingEnded = (event) => {\n\t\t\t\t\t\tif(__webpack_require__.o(installedChunks, chunkId)) {\n\t\t\t\t\t\t\tinstalledChunkData = installedChunks[chunkId];\n\t\t\t\t\t\t\tif(installedChunkData !== 0) installedChunks[chunkId] = undefined;\n\t\t\t\t\t\t\tif(installedChunkData) {\n\t\t\t\t\t\t\t\tvar errorType = event && (event.type === 'load' ? 'missing' : event.type);\n\t\t\t\t\t\t\t\tvar realSrc = event && event.target && event.target.src;\n\t\t\t\t\t\t\t\terror.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';\n\t\t\t\t\t\t\t\terror.name = 'ChunkLoadError';\n\t\t\t\t\t\t\t\terror.type = errorType;\n\t\t\t\t\t\t\t\terror.request = realSrc;\n\t\t\t\t\t\t\t\tinstalledChunkData[1](error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t__webpack_require__.l(url, loadingEnded, \"chunk-\" + chunkId, chunkId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n};\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n// no on chunks loaded\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunksignalk_edge_link\"] = self[\"webpackChunksignalk_edge_link\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// module cache are used so entry inlining is disabled\n// startup\n// Load entry module and return exports\nvar __webpack_exports__ = __webpack_require__(9217);\n"],"names":["inProgress","dataWebpackPrefix","MANAGEMENT_TOKEN_ERROR_MESSAGE","DEFAULT_AUTH_CONFIG","token","localStorageKey","queryParam","includeTokenInQuery","headerMode","readRuntimeAuthConfig","window","runtime","__EDGE_LINK_AUTH__","API_BASE_PATH","escapeHtml","str","String","replace","renderCard","title","subtitle","contentId","contentClass","toLowerCase","renderStatItem","label","value","hasError","renderMetricItem","statusClass","renderMetricItemHtml","htmlValue","renderBwStat","isHighlight","isSuccess","renderSectionGroup","description","content","id","DataConnectorConfig","constructor","isHydratingFromTextarea","this","connections","activeConnectionId","pluginConfig","pluginSchema","schemaCurrentMode","metricsInterval","syncTimeout","tokenHelpText","config","modeText","getTokenHelpText","deltaTimerConfig","subscriptionConfig","sentenceFilterConfig","protocolVersion","isServerMode","_refreshInFlight","_notificationTimer","init","loadPluginConfiguration","fetchConnections","renderPage","startMetricsRefresh","error","console","showNotification","Error","message","res","request","ok","json","_e","length","type","detectModeFromConfig","name","st","normalizeServerType","serverType","getActiveConnection","find","c","isLegacyMode","metricsPath","connId","encodeURIComponent","configPath","filename","monitoringPath","sub","congestionPath","bondingPath","bondingFailoverPath","input","response","trim","tokenFromQuery","URLSearchParams","location","search","get","localStorage","tokenFromStorage","getItem","resolveToken","headers","Headers","normalizedMode","set","attachAuthHeaders","fetch","apiFetch","status","isUnauthorized","authFailureMessage","context","renderTabs","renderConnectionContent","tabsDiv","document","getElementById","innerHTML","style","display","map","icon","statusDot","healthy","readyToSend","join","querySelectorAll","forEach","btn","addEventListener","dataset","connectionId","loadConnectionData","contentDiv","conn","renderServerContent","renderClientContent","setupEventListeners","container","telemetryAndHealth","renderPluginConfigurationCard","configuration","renderDeltaTimerCard","renderSubscriptionCard","renderSentenceFilterCard","updatePluginConfigUI","loadConfigurations","updateUI","updateStatus","loadMetrics","showErrors","configResponse","schemaResponse","Promise","all","configData","Array","isArray","schemaData","schema","currentMode","buildCompletePluginConfig","err","deltaResponse","subResponse","filterResponse","metrics","updateMetricsDisplay","loadV2Data","isClient","fetches","catch","push","results","alertsRes","packetLossRes","retransmissionsRes","congestionRes","bondingRes","monitoringData","alerts","packetLoss","retransmissions","updateMonitoringDisplay","congestionData","updateCongestionDisplay","bondingData","updateBondingDisplay","clearInterval","setInterval","refreshActiveTab","finally","updated","saveDeltaTimerBtn","saveDeltaTimer","saveSubscriptionBtn","saveSubscription","saveSentenceFilterBtn","saveSentenceFilter","addPathBtn","addPathItem","subscriptionJsonEditor","clearTimeout","setTimeout","syncFromJson","contextInput","updateJsonFromForm","metaId","el","savePluginConfigBtn","savePluginConfig","reloadPluginConfigBtn","reloadPluginConfiguration","loadDefaultPluginConfigBtn","loadDefaultPluginConfiguration","deltaTimer","cfg","ctxEl","pathsList","subscribe","path","jsonEl","JSON","stringify","populateMetaControls","meta","excludedSentences","editor","summary","hasConnections","summaryScope","keyLabel","summaryConfig","totalConnections","runtimeIndex","findIndex","summaryIndex","candidate","mode","protocol","Number","keyCount","Object","keys","toUpperCase","pathItem","createElement","className","placeholder","button","textContent","remove","appendChild","contextEl","pathInputs","from","filter","metaEnabledEl","checked","intervalEl","pathsEl","maxEl","intervalSec","maxPathsPerPacket","includePathsMatching","enabled","metaEnabled","metaIntervalSec","metaPathsRegex","metaMaxPerPacket","parse","warn","saveConfig","configKey","method","body","parseInt","isNaN","jsonText","undefined","split","s","parsedConfig","requestConfig","deepClone","normalizedServerType","result","success","updateNetworkQualityDisplay","updateBandwidthDisplay","updatePathAnalyticsDisplay","metricsDiv","stats","uptime","cryptoErrors","errorCounts","crypto","malformedPackets","dataPacketsReceived","rateLimitedPackets","droppedDeltaBatches","droppedDeltaCount","hasErrors","udpSendErrors","compressionErrors","encryptionErrors","subscriptionErrors","protocolLabel","metricsGridItems","formatted","deltasBuffered","subErrors","subscriptionErrorStat","metricsHtml","deltasSent","toLocaleString","deltasReceived","udpRetries","duplicatePackets","smartBatching","sb","totalSends","earlySends","timerSends","earlyPercent","Math","round","avgBytesPerDelta","maxDeltasPerBatch","oversizedPackets","recentErrors","errorItems","reverse","timeAgo","Date","now","timestamp","timeStr","floor","category","lastError","timeAgoStr","nqDiv","networkQuality","nq","qualityLabel","qualityColor","linkQuality","qualityPct","gaugeAngle","radStart","PI","radEnd","x1","cos","y1","sin","x2","y2","gaugeSvg","rttDisplay","rtt","jitterDisplay","jitter","isFinite","retransmitRate","packetLossDisplay","formatRatioPercent","retransmitRateDisplay","dataSourceDisplay","dataSource","nqHtml","activeLink","lastRemoteUpdate","formatTimestampAge","queueDepth","acksSent","naksSent","bandwidthDiv","bandwidth","bw","savedBytes","bytesOutRaw","bytesOut","bytesInRaw","bytesIn","savedFormatted","formatBytes","metaBytesOut","metaBytesIn","metaPacketsOut","metaPacketsIn","metaSnapshotsSent","metaDiffsSent","metaRateLimitedPackets","bandwidthStats","metadataStats","bytesOutFormatted","bytesOutRawFormatted","packetsOut","metaBytesOutFormatted","bytesInFormatted","bytesInRawFormatted","packetsIn","metaBytesInFormatted","bandwidthHtml","rateOutFormatted","rateInFormatted","compressionRatio","avgPacketSizeFormatted","renderBandwidthChart","history","maxRate","max","h","rateOut","rateIn","points","i","maxRateFormatted","METRICS_REFRESH_INTERVAL","pathDiv","pathStats","paths","categoryCount","Set","p","size","pathHtml","slice","barWidth","percentage","updatesPerMinute","bytesFormatted","data","section","div","manualMode","stateLabel","stateClass","modeLabel","currentDeltaTimer","nominalDeltaTimer","html","minDeltaTimer","maxDeltaTimer","targetRTT","avgRTT","avgLoss","toFixed","linksHtml","links","entries","link","isActive","aliveClass","loss","failoverBtn","triggerFailover","disabled","activeAlerts","alertEntries","metric","alert","level","alertCount","alertItems","entry","alertValue","renderStatItemHtml","overallLossRate","maxLossRate","trend","currentRate","avgRate","statusDiv","statusHtml","currentConfig","defaults","extractSchemaDefaults","merged","deepMerge","schemaNode","isObjectNode","properties","hasData","default","isPlainObject","assign","key","childDefaults","dependencies","dependencyValue","values","dependencyDefaults","compositeKey","composite","item","itemDefaults","clone","childValue","baseValue","overrideValue","bytes","sizes","min","log","parseFloat","pow","ageMs","text","notification","classList","dataConnectorConfig","hidden","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","__webpack_modules__","m","f","e","chunkId","reduce","promises","u","miniCssF","g","globalThis","Function","o","obj","prop","prototype","hasOwnProperty","call","l","url","done","script","needAttach","scripts","getElementsByTagName","getAttribute","charset","nc","setAttribute","src","onScriptComplete","prev","event","onerror","onload","timeout","doneFns","parentNode","removeChild","fn","bind","target","head","S","initPromises","initTokens","I","initScope","initToken","indexOf","scope","uniqueName","version","factory","eager","versions","activeVersion","loaded","then","register","scriptUrl","importScripts","currentScript","tagName","test","installedChunks","j","installedChunkData","promise","resolve","reject","errorType","realSrc","webpackJsonpCallback","parentChunkLoadingFunction","chunkIds","moreModules","some","chunkLoadingGlobal","self"],"sourceRoot":""}
|
|
1
|
+
{"version":3,"file":"main.0b6f5e3267731da945f0.js","mappings":"uBAAIA,EACAC,E,UCCG,MAAMC,EAAiC,qCAgBxCC,EAAkC,CACtCC,MAAO,KACPC,gBAAiB,iCACjBC,WAAY,gBAIZC,qBAAqB,EACrBC,WAAY,QAGd,SAASC,IACP,GAAsB,oBAAXC,OACT,OAAOP,EAGT,MAAMQ,EAAUD,OAAOE,mBACvB,OAAKD,GAA8B,iBAAZA,EAIhB,IAAKR,KAAwBQ,GAH3BR,CAIX,CCpCA,MAAMU,EAAgB,6BAsBtB,SAASC,EAAWC,GAClB,OAAOC,OAAOD,GACXE,QAAQ,KAAM,SACdA,QAAQ,KAAM,QACdA,QAAQ,KAAM,QACdA,QAAQ,KAAM,UACdA,QAAQ,KAAM,QACnB,CAGA,MAAMC,EAAa,CACjBC,EACAC,EACAC,EACAC,EAAe,KACZ,0GAISR,EAAWK,oBACfC,EAAW,uBAAuBN,EAAWM,SAAkB,wEAGtDC,aAAqBC,GAAgBD,EAAY,mCAC7CP,EAAWK,EAAMI,8EAOlCC,EAAiB,CAACC,EAAeC,EAAwBC,GAAW,IAAU,4BAC3DA,EAAW,SAAW,sCAChBb,EAAWW,4CACXX,EAAWY,wBAIpCE,EAAmB,CAACH,EAAeC,EAAwBG,EAAc,KAAO,8BAC3DA,EAAc,IAAMA,EAAc,uCAC7Bf,EAAWW,2CACXX,EAAWY,uBAKrCI,EAAuB,CAACL,EAAeM,EAAmBF,EAAc,KAAO,8BAC1DA,EAAc,IAAMA,EAAc,uCAC7Bf,EAAWW,2CACXM,sBAW1BC,EAAe,CACnBP,EACAC,EACAO,GAAc,EACdC,GAAY,IACT,0BACkBD,EAAc,aAAe,oCACvBnB,EAAWW,wCACbS,EAAY,gBAAkB,OAAOpB,EAAWY,wBAIrES,EAAqB,CACzBhB,EACAiB,EACAC,EACAC,EAAK,KACF,kCAC0BA,EAAK,QAAQA,KAAQ,uDAExCxB,EAAWK,kBACfiB,EAAc,MAAMtB,EAAWsB,SAAqB,+DAGpDC,gCAKR,MAAME,EAqBJ,WAAAC,GAVA,KAAAC,yBAAmC,EAWjCC,KAAKC,YAAc,GACnBD,KAAKE,mBAAqB,KAC1BF,KAAKG,aAAe,KACpBH,KAAKI,aAAe,KACpBJ,KAAKK,kBAAoB,KACzBL,KAAKM,gBAAkB,KACvBN,KAAKO,YAAc,KACnBP,KAAKQ,cD5CF,WACL,MAAMC,EAAS1C,IACT2C,EACJD,EAAO3C,YAA0D,kBAA5CQ,OAAOmC,EAAO3C,YAAYe,cAC3C,gCACA4B,EAAO3C,YAA0D,sBAA5CQ,OAAOmC,EAAO3C,YAAYe,cAC7C,oBACA,sDAER,MAAO,+PAA+P4B,EAAO7C,qCAAqC6C,EAAO9C,mCAAmC+C,8BAC9V,CCkCyBC,GAGrBX,KAAKY,iBAAmB,KACxBZ,KAAKa,mBAAqB,KAC1Bb,KAAKc,qBAAuB,KAC5Bd,KAAKe,gBAAkB,EACvBf,KAAKgB,cAAe,EACpBhB,KAAKiB,kBAAmB,EACxBjB,KAAKkB,mBAAqB,KAE1BlB,KAAKmB,MACP,CAEA,UAAMA,GACJ,UACQnB,KAAKoB,yBAAwB,SAC7BpB,KAAKqB,mBACXrB,KAAKsB,aACLtB,KAAKuB,qBACP,CAAE,MAAOC,GACPC,QAAQD,MAAM,wBAAyBA,GACvCxB,KAAK0B,iBACH,sCACGF,aAAiBG,MAAQH,EAAMI,QAAUtD,OAAOkD,IACnD,QAEJ,CACF,CAIA,sBAAMH,GACJ,IACE,MAAMQ,QAAY7B,KAAK8B,QAAQ,GAAG3D,iBAC9B0D,EAAIE,KACN/B,KAAKC,kBAAoB4B,EAAIG,OAEjC,CAAE,MAAOC,GAET,CAEA,IAAKjC,KAAKC,aAA2C,IAA5BD,KAAKC,YAAYiC,OAAc,CAEtD,MAAMC,EAAOnC,KAAKoC,uBAClBpC,KAAKC,YAAc,CAAC,CAAEL,GAAI,UAAWyC,KAAM,UAAWF,QACxD,CAEKnC,KAAKE,qBACRF,KAAKE,mBAAqBF,KAAKC,YAAY,GAAGL,GAElD,CAEA,oBAAAwC,GACE,GAAIpC,KAAKG,aAAc,CACrB,MAAMmC,EAAKtC,KAAKuC,oBAAoBvC,KAAKG,aAAaqC,YACtD,GAAIF,EACF,OAAOA,CAEX,CACA,OAAItC,KAAKK,kBACAL,KAAKK,kBAEP,QACT,CAEA,mBAAAoC,GACE,OAAOzC,KAAKC,YAAYyC,KAAMC,GAAMA,EAAE/C,KAAOI,KAAKE,qBAAuBF,KAAKC,YAAY,EAC5F,CAEA,YAAA2C,GACE,OAAmC,IAA5B5C,KAAKC,YAAYiC,QAA2C,YAA3BlC,KAAKC,YAAY,GAAGL,EAC9D,CAIA,WAAAiD,CAAYC,GACV,MAAe,YAAXA,EACK,GAAG3E,YAEL,GAAGA,iBAA6B4E,mBAAmBD,YAC5D,CAEA,UAAAE,CAAWF,EAAgBG,GACzB,MAAe,YAAXH,EACK,GAAG3E,YAAwB8E,IAE7B,GAAG9E,iBAA6B4E,mBAAmBD,aAAkBG,GAC9E,CAEA,cAAAC,CAAeJ,EAAgBK,GAC7B,MAAe,YAAXL,EACK,GAAG3E,gBAA4BgF,IAEjC,GAAGhF,iBAA6B4E,mBAAmBD,iBAAsBK,GAClF,CAEA,cAAAC,CAAeN,GACb,MAAe,YAAXA,EACK,GAAG3E,eAEL,GAAGA,iBAA6B4E,mBAAmBD,eAC5D,CAEA,WAAAO,CAAYP,GACV,MAAe,YAAXA,EACK,GAAG3E,YAEL,GAAGA,iBAA6B4E,mBAAmBD,YAC5D,CAEA,mBAAAQ,CAAoBR,GAClB,MAAe,YAAXA,EACK,GAAG3E,qBAEL,GAAGA,iBAA6B4E,mBAAmBD,qBAC5D,CAEA,aAAMhB,CAAQyB,EAAoBpC,EAAoB,CAAC,GACrD,MAAMqC,QDvJH,SAAkBD,EAAyBpC,EAAoB,CAAC,GACrE,MAAMV,EAAS1C,IACTL,EAxER,SAAsB+C,GACpB,GAAIA,EAAO/C,MACT,OAAOY,OAAOmC,EAAO/C,OAAO+F,OAG9B,GAAsB,oBAAXzF,OACT,MAAO,GAOT,GAAIyC,EAAO5C,qBAAuB4C,EAAO7C,WAAY,CACnD,MAAM8F,EAAiB,IAAIC,gBAAgB3F,OAAO4F,SAASC,QAAQC,IAAIrD,EAAO7C,YAC9E,GAAI8F,EACF,OAAOA,EAAeD,MAE1B,CAEA,GAAIhD,EAAO9C,iBAAmBK,OAAO+F,aAAc,CACjD,MAAMC,EAAmBhG,OAAO+F,aAAaE,QAAQxD,EAAO9C,iBAC5D,GAAIqG,EACF,OAAOA,EAAiBP,MAE5B,CAEA,MAAO,EACT,CA4CgBS,CAAazD,GACrB0D,EAAU,IAAIC,QAAQjD,EAAKgD,SAAW,CAAC,GAG7C,OA9CF,SAA2BA,EAAkBzG,EAAeI,GAC1D,IAAKJ,EACH,OAAOyG,EAGT,MAAME,GAAkBvG,GAAc,QAAQe,cAEzB,sBAAnBwF,GACmB,UAAnBA,GACmB,SAAnBA,GAEAF,EAAQG,IAAI,oBAAqB5G,GAGd,kBAAnB2G,GACmB,WAAnBA,GACmB,SAAnBA,GAEAF,EAAQG,IAAI,gBAAiB,UAAU5G,IAG3C,CAuBE6G,CAAkBJ,EAASzG,EAAO+C,EAAO3C,YAElC0G,MAAMjB,EAAO,IACfpC,EACHgD,WAEJ,CC6I2BM,CAASlB,EAAOpC,GACvC,GAAwB,MAApBqC,EAASkB,OAAgB,CAC3B,MAAMlD,EAA4B,IAAIG,MAAMnE,GAE5C,MADAgE,EAAMmD,gBAAiB,EACjBnD,CACR,CACA,OAAOgC,CACT,CAEA,kBAAAoB,CAAmBC,GACjB,MAAO,GAAGrH,kBAA+CqH,MAAY7E,KAAKQ,eAC5E,CAIA,UAAAc,GACEtB,KAAK8E,aACL9E,KAAK+E,yBACP,CAEA,UAAAD,GACE,MAAME,EAAUC,SAASC,eAAe,kBACxC,GAAKF,EAAL,CAIA,GAAIhF,KAAKC,YAAYiC,QAAU,EAG7B,OAFA8C,EAAQG,UAAY,QACpBH,EAAQI,MAAMC,QAAU,QAI1BL,EAAQI,MAAMC,QAAU,GACxBL,EAAQG,UAAY,+BAA+BnF,KAAKC,YACrDqF,IAAK3C,IACJ,MAAM4C,EAAkB,WAAX5C,EAAER,KAAoB,YAAc,YAC3CqD,GACU,IAAd7C,EAAE8C,QACE,6CACA9C,EAAE+C,aAA0B,WAAX/C,EAAER,KACjB,0CACA,+CACR,MAAO,gCAAgCQ,EAAE/C,KAAOI,KAAKE,mBAAqB,UAAY,4CACxDF,KAAK5B,WAAWuE,EAAE/C,oBAC5C4F,uCACuBD,8CACAvF,KAAK5B,WAAWuE,EAAEN,MAAQM,EAAE/C,gDAC5BI,KAAK5B,WAAWuE,EAAER,oCAG9CwD,KAAK,YAGRX,EAAQY,iBAAiB,mBAAmBC,QAASC,IACnDA,EAAIC,iBAAiB,QAAS,KAC5B,MAAMnG,EAAMkG,EAAoBE,QAAQC,aACpCrG,IAAOI,KAAKE,qBACdF,KAAKE,mBAAqBN,EAC1BI,KAAKsB,aACLtB,KAAKkG,yBAnCX,CAuCF,CAEA,uBAAAnB,GACE,MAAMoB,EAAalB,SAASC,eAAe,qBAC3C,IAAKiB,EACH,OAGF,MAAMC,EAAOpG,KAAKyC,sBAClBzC,KAAKgB,aAA6B,WAAdoF,EAAKjE,KAErBnC,KAAKgB,aACPhB,KAAKqG,oBAAoBF,GAEzBnG,KAAKsG,oBAAoBH,GAG3BnG,KAAKuG,sBACLvG,KAAKkG,oBACP,CAEA,mBAAAG,CAAoBG,GAClB,MAAMC,EACJjI,EACE,sBACA,mEACA,WAEF,qDACAA,EACE,kBACA,mDACA,kBAEFA,EAAW,oBAAqB,+BAAgC,aAChEA,EAAW,iBAAkB,uCAAwC,iBACrEA,EACE,sBACA,6DACA,oBAEF,SAEFgI,EAAUrB,UACR1F,EACE,0BACA,6DACAgH,EACA,mBAEFhH,EACE,WACA,0CACAO,KAAK0G,gCACL,gBAEN,CAEA,mBAAAJ,CAAoBE,GAClB,MAAMG,EACJ3G,KAAK4G,uBAAyB5G,KAAK6G,yBAA2B7G,KAAK8G,2BAE/DL,EACJjI,EACE,sBACA,sEACA,WAEF,4EACAA,EACE,kBACA,mDACA,kBAEFA,EAAW,oBAAqB,yCAA0C,aAC1EA,EAAW,iBAAkB,mCAAoC,iBACjEA,EACE,qBACA,gEACA,qBAhBFA,+EAoBAA,EACE,qBACA,iDACA,iBAvBFA,2DA2BAA,EACE,sBACA,6DACA,oBAEF,SACAA,EAAW,SAAU,KAAM,SAAU,eAEvCgI,EAAUrB,UACR1F,EACE,gBACA,4DACAkH,EACA,sBAEFlH,EACE,0BACA,oEACAgH,EACA,mBAEFhH,EACE,WACA,0CACAO,KAAK0G,gCACL,gBAEN,CAEA,oBAAAE,GACE,MAAO,i4BAqBT,CAEA,sBAAAC,GACE,MAAO,u/FA6DT,CAEA,wBAAAC,GACE,MAAO,qxBAoBT,CAEA,6BAAAJ,GACE,MAAO,88BAqB8B1G,KAAK5B,WAAW4B,KAAKQ,8dAW5D,CAIA,wBAAM0F,GACJ,MACMpD,EADO9C,KAAKyC,sBACE7C,GAEfI,KAAKgB,aAKRhB,KAAK+G,8BAJC/G,KAAKgH,mBAAmBlE,GAC9B9C,KAAKiH,WACLjH,KAAKkH,sBAKDlH,KAAKmH,YAAYrE,EACzB,CAEA,6BAAM1B,CAAwBgG,GAAa,GACzC,IACE,MAAOC,EAAgBC,SAAwBC,QAAQC,IAAI,CACzDxH,KAAK8B,QAAQ,GAAG3D,mBAChB6B,KAAK8B,QAAQ,GAAG3D,qBAGlB,IAAKkJ,EAAetF,GAClB,MAAM,IAAIJ,MAAM,wCAAwC0F,EAAe3C,WAGzE,MAAM+C,QAAmBJ,EAAerF,OAClC2E,EACJc,GACAA,EAAWd,eACyB,iBAA7Bc,EAAWd,gBACjBe,MAAMC,QAAQF,EAAWd,eACtBc,EAAWd,cACX,CAAC,EAEP,GAAIW,EAAevF,GAAI,CACrB,MAAM6F,QAAmBN,EAAetF,OACpC4F,GAAcA,EAAWC,QAAuC,iBAAtBD,EAAWC,SACvD7H,KAAKI,aAAewH,EAAWC,SAG/BD,GAC4B,WAA3BA,EAAWE,aAAuD,WAA3BF,EAAWE,cAEnD9H,KAAKK,kBAAoBuH,EAAWE,YAExC,CAGA,OADA9H,KAAKG,aAAeH,KAAK+H,0BAA0BpB,IAC5C,CACT,CAAE,MAAOnF,GACP,GAAI4F,EAAY,CACd,MAAMY,EAAMxG,EACZxB,KAAK0B,iBACHsG,EAAIrD,eACA3E,KAAK4E,mBAAmB,yBACxB,gCAAkCoD,EAAIpG,QAC1C,UAEJ,CACA,OAAO,CACT,CACF,CAEA,wBAAMoF,CAAmBlE,GACvB,IACE,MAAOmF,EAAeC,EAAaC,SAAwBZ,QAAQC,IAAI,CACrExH,KAAK8B,QAAQ9B,KAAKgD,WAAWF,EAAQ,qBACrC9C,KAAK8B,QAAQ9B,KAAKgD,WAAWF,EAAQ,sBACrC9C,KAAK8B,QAAQ9B,KAAKgD,WAAWF,EAAQ,2BAGvC9C,KAAKY,iBAAmBqH,EAAclG,SAAWkG,EAAcjG,OAAS,KACxEhC,KAAKa,mBAAqBqH,EAAYnG,SAAWmG,EAAYlG,OAAS,KACtEhC,KAAKc,qBAAuBqH,EAAepG,SAAWoG,EAAenG,OAAS,IAChF,CAAE,MAAOR,GACP,MAAMwG,EAAMxG,EACZxB,KAAK0B,iBACHsG,EAAIrD,eACA3E,KAAK4E,mBAAmB,oCACxB,iCAAmCoD,EAAIpG,QAC3C,QAEJ,CACF,CAEA,iBAAMuF,CAAYrE,GACXA,IACHA,EAAS9C,KAAKE,oBAEhB,IACE,MAAMsD,QAAiBxD,KAAK8B,QAAQ9B,KAAK6C,YAAYC,IACrD,GAAIU,EAASzB,GAAI,CACf,MAAMqG,QAAgB5E,EAASxB,OAC/BhC,KAAKe,gBAAkBqH,EAAQrH,iBAAmB,EAClDf,KAAKqI,qBAAqBD,GAEtBpI,KAAKe,iBAAmB,GAC1Bf,KAAKsI,WAAWxF,EAEpB,CACF,CAAE,MAAOtB,GACPC,QAAQD,MAAM,yBAA2BA,EAAgBI,QAC3D,CACF,CAEA,gBAAM0G,CAAWxF,GACf,MAAMyF,GAAYvI,KAAKgB,aAEjBwH,EAAsC,CAC1CxI,KAAK8B,QAAQ9B,KAAKkD,eAAeJ,EAAQ,WAAW2F,MAAM,IAAM,MAChEzI,KAAK8B,QAAQ9B,KAAKkD,eAAeJ,EAAQ,gBAAgB2F,MAAM,IAAM,MACrEzI,KAAK8B,QAAQ9B,KAAKkD,eAAeJ,EAAQ,oBAAoB2F,MAAM,IAAM,OAGvEF,GACFC,EAAQE,KACN1I,KAAK8B,QAAQ9B,KAAKoD,eAAeN,IAAS2F,MAAM,IAAM,MACtDzI,KAAK8B,QAAQ9B,KAAKqD,YAAYP,IAAS2F,MAAM,IAAM,OAIvD,MAAME,QAAgBpB,QAAQC,IAAIgB,IAC3BI,EAAWC,EAAeC,EAAoBC,EAAeC,GAAcL,EAE5EM,EAA0C,CAAC,EAYjD,GAXIL,GAAaA,EAAU7G,KACzBkH,EAAeC,aAAeN,EAAU5G,QAEtC6G,GAAiBA,EAAc9G,KACjCkH,EAAeE,iBAAmBN,EAAc7G,QAE9C8G,GAAsBA,EAAmB/G,KAC3CkH,EAAeG,sBAAwBN,EAAmB9G,QAE5DhC,KAAKqJ,wBAAwBJ,GAEzBV,GAAYQ,GAAiBA,EAAchH,GAAI,CACjD,MAAMuH,QAAuBP,EAAc/G,OAC3ChC,KAAKuJ,wBAAwBD,EAC/B,CAEA,GAAIf,GAAYS,GAAcA,EAAWjH,GAAI,CAC3C,MAAMyH,QAAoBR,EAAWhH,OACrChC,KAAKyJ,qBAAqBD,EAC5B,CACF,CAEA,mBAAAjI,GACMvB,KAAKM,iBACPoJ,cAAc1J,KAAKM,iBAGrBN,KAAKM,gBAAkBqJ,YAAY,KAC7B3J,KAAKiB,mBAGTjB,KAAKiB,kBAAmB,EACxBjB,KAAK4J,mBAAmBC,QAAQ,KAC9B7J,KAAKiB,kBAAmB,MAxuBC,KA2uB/B,CAEA,sBAAM2I,GAEJ,IACE,MAAM/H,QAAY7B,KAAK8B,QAAQ,GAAG3D,iBAClC,GAAI0D,EAAIE,GAAI,CACV,MAAM+H,QAAgBjI,EAAIG,OACtB8H,EAAQ5H,OAAS,IACnBlC,KAAKC,YAAc6J,EACnB9J,KAAK8E,aAET,CACF,CAAE,MAAO7C,GAET,OAEMjC,KAAKmH,YAAYnH,KAAKE,mBAC9B,CAIA,mBAAAqG,GACE,MAAMwD,EAAoB9E,SAASC,eAAe,kBAC9C6E,GACFA,EAAkBhE,iBAAiB,QAAS,IAAM/F,KAAKgK,kBAGzD,MAAMC,EAAsBhF,SAASC,eAAe,oBAChD+E,GACFA,EAAoBlE,iBAAiB,QAAS,IAAM/F,KAAKkK,oBAG3D,MAAMC,EAAwBlF,SAASC,eAAe,sBAClDiF,GACFA,EAAsBpE,iBAAiB,QAAS,IAAM/F,KAAKoK,sBAG7D,MAAMC,EAAapF,SAASC,eAAe,WACvCmF,GACFA,EAAWtE,iBAAiB,QAAS,IAAM/F,KAAKsK,eAGlD,MAAMC,EAAyBtF,SAASC,eAAe,oBACnDqF,GACFA,EAAuBxE,iBAAiB,QAAS,KAC3C/F,KAAKO,aACPiK,aAAaxK,KAAKO,aAEpBP,KAAKO,YAAckK,WAAW,IAAMzK,KAAK0K,eA3xBtB,OA+xBvB,MAAMC,EAAe1F,SAASC,eAAe,WACzCyF,GACFA,EAAa5E,iBAAiB,QAAS,IAAM/F,KAAK4K,sBAOpD,IAAK,MAAMC,IAAU,CAAC,cAAe,kBAAmB,iBAAkB,oBAAqB,CAC7F,MAAMC,EAAK7F,SAASC,eAAe2F,GAC/BC,IACFA,EAAG/E,iBAAiB,QAAS,IAAM/F,KAAK4K,sBACzB,gBAAXC,GAEFC,EAAG/E,iBAAiB,SAAU,IAAM/F,KAAK4K,sBAG/C,CAEA,MAAMG,EAAsB9F,SAASC,eAAe,oBAChD6F,GACFA,EAAoBhF,iBAAiB,QAAS,IAAM/F,KAAKgL,oBAG3D,MAAMC,EAAwBhG,SAASC,eAAe,sBAClD+F,GACFA,EAAsBlF,iBAAiB,QAAS,IAAM/F,KAAKkL,6BAG7D,MAAMC,EAA6BlG,SAASC,eAAe,2BACvDiG,GACFA,EAA2BpF,iBAAiB,QAAS,IACnD/F,KAAKoL,iCAGX,CAIA,QAAAnE,GAGE,GAFAjH,KAAK+G,uBAED/G,KAAKY,kBAAqBZ,KAAKY,iBAA6CyK,WAAY,CAC1F,MAAMP,EAAK7F,SAASC,eAAe,cAC/B4F,IACFA,EAAG9L,MAAQV,OAAQ0B,KAAKY,iBAA6CyK,YAEzE,CAEA,GAAIrL,KAAKa,mBAAoB,CAC3B,MAAMyK,EAAMtL,KAAKa,mBACX0K,EAAQtG,SAASC,eAAe,WAClCqG,IACFA,EAAMvM,MAASsM,EAAIzG,SAAsB,KAG3C,MAAM2G,EAAYvG,SAASC,eAAe,aACtCsG,IACFA,EAAUrG,UAAY,GAClBmG,EAAIG,WAAa/D,MAAMC,QAAQ2D,EAAIG,YACpCH,EAAIG,UAAsC5F,QAAS1C,GAAQnD,KAAKsK,YAAYnH,EAAIuI,QAIrF,MAAMC,EAAS1G,SAASC,eAAe,oBACnCyG,IACFA,EAAO3M,MAAQ4M,KAAKC,UAAU7L,KAAKa,mBAAoB,KAAM,IAI/Db,KAAK8L,qBAAqBR,EAAIS,KAChC,CAEA,GACE/L,KAAKc,sBACL4G,MAAMC,QAAS3H,KAAKc,qBAAiDkL,mBACrE,CACA,MAAMlB,EAAK7F,SAASC,eAAe,kBAC/B4F,IACFA,EAAG9L,MACAgB,KAAKc,qBAAiDkL,kBACvDrG,KAAK,MAEX,CACF,CAEA,oBAAAoB,GACE,MAAMkF,EAAShH,SAASC,eAAe,oBACjCgH,EAAUjH,SAASC,eAAe,uBAExC,GAAK+G,EAAL,CAIA,IAAKjM,KAAKG,aAKR,OAJA8L,EAAOjN,MAAQ,UACXkN,IACFA,EAAQ/G,UAAY,sCAOxB,GAFA8G,EAAOjN,MAAQ4M,KAAKC,UAAU7L,KAAKG,aAAc,KAAM,GAEnD+L,EAAS,CACX,MAAMC,EACJzE,MAAMC,QAAQ3H,KAAKG,aAAaF,cAC/BD,KAAKG,aAAaF,YAA0BiC,OAAS,EAExD,IAAIkK,EAAe,YACfC,EAAW,mBACXC,EAAyCtM,KAAKG,aAElD,GAAIgM,EAAgB,CAClB,MAAMI,EAAoBvM,KAAKG,aAAaF,YAA0BiC,OAChEsK,EAAexM,KAAKC,YAAYwM,UAAW9J,GAAMA,EAAE/C,KAAOI,KAAKE,oBAC/DwM,EACJF,GAAgB,GAAKA,EAAeD,EAAmBC,EAAe,EAClEG,EAAa3M,KAAKG,aAAaF,YACnCyM,GAEFJ,EACEK,GAAkC,iBAAdA,IAA2BjF,MAAMC,QAAQgF,GAAaA,EAAY,CAAC,EACzFP,EAAe,cAAcM,EAAe,KAAKH,IACjDF,EAAW,mBACb,CAEA,MAAMO,EAAO5M,KAAKuC,oBAAoB+J,EAAc9J,aAAe,SAC7DqK,EACJC,OAAOR,EAAcvL,kBAAoB,EAAI+L,OAAOR,EAAcvL,iBAAmB,EACjFgM,EAAWC,OAAOC,KAAKX,GAAepK,OAC5CgK,EAAQ/G,UAAY,0DAEdrG,EAAe,QAASsN,iBACxBtN,EAAe,OAAQ8N,EAAKM,6BAC5BpO,EAAe,WAAY,IAAMR,OAAOuO,kBACxC/N,EAAeuN,EAAU/N,OAAOyO,6BAGxC,CA/CA,CAgDF,CAIA,WAAAzC,CAAYoB,EAAO,IACjB,MAAMF,EAAYvG,SAASC,eAAe,aAC1C,IAAKsG,EACH,OAGF,MAAM2B,EAAWlI,SAASmI,cAAc,OACxCD,EAASE,UAAY,YAErB,MAAM9J,EAAQ0B,SAASmI,cAAc,SACrC7J,EAAMpB,KAAO,OACboB,EAAMvE,MAAQ0M,EACdnI,EAAM+J,YAAc,sBACpB/J,EAAM8J,UAAY,aAElB,MAAME,EAAStI,SAASmI,cAAc,UACtCG,EAAOpL,KAAO,SACdoL,EAAOF,UAAY,iBACnBE,EAAOC,YAAc,SAErBjK,EAAMwC,iBAAiB,QAAS,IAAM/F,KAAK4K,sBAC3C2C,EAAOxH,iBAAiB,QAAS,KAC/BoH,EAASM,SACTzN,KAAK4K,uBAGPuC,EAASO,YAAYnK,GACrB4J,EAASO,YAAYH,GACrB/B,EAAUkC,YAAYP,GACtBnN,KAAK4K,oBACP,CAEA,kBAAAA,GAME,GAAI5K,KAAKD,wBACP,OAGF,MAAM4N,EAAY1I,SAASC,eAAe,WACpCL,EAAU8I,GAAYA,EAAU3O,OAAe,IAC/C4O,EAAa3I,SAASW,iBAAiB,eAQvCnF,EAAkC,CAAEoE,UAAS4G,UAPjC/D,MAAMmG,KAAKD,GAC1BtI,IAAK/B,IAAU,CAAGmI,KAAMnI,EAAMvE,SAC9B8O,OAAQ3K,GAA4B,KAApBA,EAAIuI,KAAKjI,SAMtBsK,EAAgB9I,SAASC,eAAe,eAC9C,GAAI6I,GAAiBA,EAAcC,QAAS,CAC1C,MAAMC,EAAahJ,SAASC,eAAe,mBACrCgJ,EAAUjJ,SAASC,eAAe,kBAClCiJ,EAAQlJ,SAASC,eAAe,oBAChCkJ,EAAcH,GAAcA,EAAWjP,MAAQ8N,OAAOmB,EAAWjP,OAAS,IAC1EqP,EAAoBF,GAASA,EAAMnP,MAAQ8N,OAAOqB,EAAMnP,OAAS,IACjEsP,EAAuBJ,GAAWA,EAAQlP,MAAQkP,EAAQlP,MAAQ,KACxEyB,EAAOsL,KAAO,CACZwC,SAAS,EACTH,cACAE,uBACAD,oBAEJ,CAEA,MAAM1C,EAAS1G,SAASC,eAAe,oBACnCyG,IACFA,EAAO3M,MAAQ4M,KAAKC,UAAUpL,EAAQ,KAAM,GAEhD,CAKA,oBAAAqL,CAAqBC,GACnB,MAAMyC,EAAcvJ,SAASC,eAAe,eACxCsJ,IACFA,EAAYR,WAAajC,IAAyB,IAAjBA,EAAKwC,UAExC,MAAME,EAAkBxJ,SAASC,eAAe,mBAC5CuJ,IACFA,EAAgBzP,MACd+M,GAAoC,iBAArBA,EAAKqC,YAA2B9P,OAAOyN,EAAKqC,aAAe,IAE9E,MAAMM,EAAiBzJ,SAASC,eAAe,kBAC3CwJ,IACFA,EAAe1P,MACb+M,GAA6C,iBAA9BA,EAAKuC,qBAAoCvC,EAAKuC,qBAAuB,IAExF,MAAMK,EAAmB1J,SAASC,eAAe,oBAC7CyJ,IACFA,EAAiB3P,MACf+M,GAA0C,iBAA3BA,EAAKsC,kBAAiC/P,OAAOyN,EAAKsC,mBAAqB,GAE5F,CAEA,YAAA3D,GACE1K,KAAKD,yBAA0B,EAC/B,IACE,MAAM4L,EAAS1G,SAASC,eAAe,oBACvC,IAAKyG,EACH,OAEF,MAAMlL,EAASmL,KAAKgD,MAAMjD,EAAO3M,OAE3BuM,EAAQtG,SAASC,eAAe,WAClCqG,IACFA,EAAMvM,MAAQyB,EAAOoE,SAAW,KAGlC,MAAM2G,EAAYvG,SAASC,eAAe,aACtCsG,IACFA,EAAUrG,UAAY,GAClB1E,EAAOgL,WAAa/D,MAAMC,QAAQlH,EAAOgL,YAC3ChL,EAAOgL,UAAU5F,QAAS1C,GAA2BnD,KAAKsK,YAAYnH,EAAIuI,MAAQ,MAOtF1L,KAAK8L,qBAAqBrL,EAAOsL,KACnC,CAAE,MAAOvK,GACPC,QAAQoN,KAAK,0BAA4BrN,EAAgBI,QAC3D,C,QACE5B,KAAKD,yBAA0B,CACjC,CACF,CAIA,gBAAM+O,CAAW7L,EAAkBxC,EAAiBsO,EAAmBhQ,GACrE,MAAM+D,EAAS9C,KAAKE,mBACpB,IAOE,WANuBF,KAAK8B,QAAQ9B,KAAKgD,WAAWF,EAAQG,GAAW,CACrE+L,OAAQ,OACR7K,QAAS,CAAE,eAAgB,oBAC3B8K,KAAMrD,KAAKC,UAAUpL,MAGVsB,GAKX,MAAM,IAAIJ,MAAM,gCAJf3B,KAAiC+O,GAAatO,EAC/CT,KAAK0B,iBAAiB,GAAG3C,wBAA6B,WACtDiB,KAAKkH,cAIT,CAAE,MAAO1F,GACP,MAAMwG,EAAMxG,EACZxB,KAAK0B,iBACHsG,EAAIrD,eACA3E,KAAK4E,mBAAmB,UAAU7F,EAAMF,iBACxC,gBAAgBE,EAAMF,kBAAoBmJ,EAAIpG,QAClD,QAEJ,CACF,CAEA,oBAAMoI,GACJ,MAAMqB,EAAa6D,SAAUjK,SAASC,eAAe,cAAmClG,OAEpFmQ,MAAM9D,IAAeA,EAzlCL,KAylCqCA,EAxlCrC,IAylClBrL,KAAK0B,iBACH,yDACA,eAKE1B,KAAK8O,WACT,mBACA,CAAEzD,cACF,mBACA,4BAEJ,CAEA,sBAAMnB,GACJ,IAOMlK,KAAKO,cACPiK,aAAaxK,KAAKO,aAClBP,KAAKO,YAAc,KACnBP,KAAK0K,gBAGP,MAAM0E,EAAYnK,SAASC,eAAe,oBAA4ClG,MAChFyB,EAASmL,KAAKgD,MAAMQ,GAE1B,IAAK3O,EAAOoE,QACV,MAAM,IAAIlD,MAAM,uBAElB,IAAKlB,EAAOgL,YAAc/D,MAAMC,QAAQlH,EAAOgL,WAC7C,MAAM,IAAI9J,MAAM,+BAQlB,MAAMoM,EAAgB9I,SAASC,eAAe,eAC9C,GAAI6I,GAAiBA,EAAcC,QAAS,CAC1C,MAAMC,EAAahJ,SAASC,eAAe,mBACrCgJ,EAAUjJ,SAASC,eAAe,kBAClCiJ,EAAQlJ,SAASC,eAAe,oBAChCkJ,EAAcH,GAAcA,EAAWjP,MAAQ8N,OAAOmB,EAAWjP,OAAS,IAC1EqP,EAAoBF,GAASA,EAAMnP,MAAQ8N,OAAOqB,EAAMnP,OAAS,IACjEsP,EAAuBJ,GAAWA,EAAQlP,MAAQkP,EAAQlP,MAAQ,KACxEyB,EAAOsL,KAAO,CACZwC,SAAS,EACTH,cACAE,uBACAD,oBAEJ,WAA2BgB,IAAhB5O,EAAOsL,aAGTtL,EAAOsL,WAGV/L,KAAK8O,WACT,oBACArO,EACA,qBACA,6BAEJ,CAAE,MAAOe,GACPxB,KAAK0B,iBAAiB,8BAAiCF,EAAgBI,QAAS,QAClF,CACF,CAEA,wBAAMwI,GACJ,MACM4B,EADe/G,SAASC,eAAe,kBAAuClG,MAEjFsQ,MAAM,KACNhK,IAAKiK,GAAMA,EAAE9L,OAAOyJ,eACpBY,OAAQyB,GAAMA,EAAErN,OAAS,SAEtBlC,KAAK8O,WACT,uBACA,CAAE9C,qBACF,uBACA,kBAEJ,CAEA,sBAAMhB,GACJ,MAAMiB,EAAShH,SAASC,eAAe,oBACvC,GAAK+G,EAIL,IACE,MAAMuD,EAAe5D,KAAKgD,MAAM3C,EAAOjN,OACvC,IAAKwQ,GAAwC,iBAAjBA,GAA6B9H,MAAMC,QAAQ6H,GACrE,MAAM,IAAI7N,MAAM,8CAGlB,MAAM8N,EAAgBzP,KAAK0P,UAAUF,GAC/BG,EAAuB3P,KAAKuC,oBAAoBkN,EAAcjN,YAChEmN,IACFF,EAAcjN,WAAamN,GAG7B,MAAMnM,QAAiBxD,KAAK8B,QAAQ,GAAG3D,kBAA+B,CACpE6Q,OAAQ,OACR7K,QAAS,CAAE,eAAgB,oBAC3B8K,KAAMrD,KAAKC,UAAU4D,KAGjBG,QAAepM,EAASxB,OAAOyG,MAAM,KAAM,CAAG,IACpD,IAAKjF,EAASzB,KAAO6N,EAAOC,QAC1B,MAAM,IAAIlO,MAAMiO,EAAOpO,OAAS,wCAAwCgC,EAASkB,WAGnF1E,KAAKG,aAAeH,KAAK+H,0BAA0B0H,GACnDzP,KAAK+G,uBACL/G,KAAK0B,iBACHkO,EAAOhO,SAAW,wDAClB,UAEJ,CAAE,MAAOJ,GACP,MAAMwG,EAAMxG,EACZxB,KAAK0B,iBACHsG,EAAIrD,eACA3E,KAAK4E,mBAAmB,6BACxB,oCAAsCoD,EAAIpG,QAC9C,QAEJ,CACF,CAEA,+BAAMsJ,SACiBlL,KAAKoB,yBAAwB,KAEhDpB,KAAK+G,uBACL/G,KAAK0B,iBAAiB,iCAAkC,WAE5D,CAEA,8BAAA0J,GACEpL,KAAKG,aAAeH,KAAK+H,0BAA0B,CAAC,GACpD/H,KAAK+G,uBACL/G,KAAK0B,iBAAiB,qDAAsD,UAC9E,CAIA,oBAAA2G,CAAqBD,GACnBpI,KAAK8P,4BAA4B1H,GACjCpI,KAAK+P,uBAAuB3H,GAC5BpI,KAAKgQ,2BAA2B5H,GAEhC,MAAM6H,EAAahL,SAASC,eAAe,WAC3C,IAAK+K,EACH,OAGF,MAAM1H,EAA4B,WAAjBH,EAAQwE,MACnB,MAAEsD,EAAK,OAAExL,EAAM,OAAEyL,GAAW/H,EAE5BgI,EAAgBF,EAAMG,aAAeH,EAAMG,YAAYC,QAAW,EAClEC,EAAmBL,EAAMK,kBAAoB,EAC7CC,EAAsBN,EAAMM,qBAAuB,EACnDC,EAAqBP,EAAMO,oBAAsB,EACjDC,EAAsBR,EAAMQ,qBAAuB,EACnDC,EAAoBT,EAAMS,mBAAqB,EAE/CC,EACJV,EAAMW,cAAgB,GACtBX,EAAMY,kBAAoB,GAC1BZ,EAAMa,iBAAmB,GACzBb,EAAMc,mBAAqB,GAC3BZ,EAAe,GACfG,EAAmB,GACnBE,EAAqB,GACrBE,EAAoB,EAEhB5P,EAAkBqH,EAAQrH,iBAAmB,EAC7CkQ,EAAgBlQ,GAAmB,EAAI,IAAIA,IAAoB,KAE/DmQ,EAAmB,CACvBhS,EAAiB,SAAUiR,EAAOgB,WAClCjS,EAAiB,OAAQqJ,EAAW,SAAW,UAC/CnJ,EACE,WACA,wCAAwChB,EAAW6S,OAAmB7S,EAAW6S,EAAc/D,yBAEjGhO,EACE,SACAwF,EAAOgB,YAAc,QAAU,YAC/BhB,EAAOgB,YAAc,UAAY,SAEnC6C,EAAWrJ,EAAiB,kBAAmBwF,EAAO0M,gBAAkB,IACxEzL,KAAK,IAED0L,EAAYnB,EAAMc,mBAClBM,EAAwB/I,EAC1BzJ,EAAe,sBAAuBuS,EAAWA,EAAY,GAC7D,GA8CJ,IAAIE,EAAc,yEAEYL,yHA9CX,CACjB3I,EACIzJ,EAAe,cAAeoR,EAAMsB,WAAWC,kBAC/C3S,EAAe,kBAAmBoR,EAAMwB,eAAeD,kBAC1DlJ,EAEG,GADAzJ,EAAe,wBAAyB0R,EAAoBiB,kBAEhElJ,EACIzJ,EAAe,kBAAmBoR,EAAMW,cAAeX,EAAMW,cAAgB,GAC7E,GACJtI,EAAWzJ,EAAe,cAAeoR,EAAMyB,YAAc,GAC5DpJ,EAMG,GALAzJ,EACE,uBACA2R,EAAmBgB,iBACnBhB,EAAqB,GAG3BlI,EACIzJ,EACE,wBACA4R,EAAoBe,iBACpBf,EAAsB,GAExB,GACJnI,EACIzJ,EACE,iBACA6R,EAAkBc,iBAClBd,EAAoB,GAEtB,GACJ7R,EAAe,qBAAsBoR,EAAMY,kBAAmBZ,EAAMY,kBAAoB,GACxFhS,EAAe,oBAAqBoR,EAAMa,iBAAkBb,EAAMa,iBAAmB,GACrFO,GACC/I,GAAY2H,EAAM0B,iBAAmB,EAClC9S,EAAe,oBAAqBoR,EAAM0B,iBAAiBH,kBAC3D,GACJ1Q,GAAmB,EACfjC,EAAe,qBAAsBsR,EAAcA,EAAe,GAClE,GACJtR,EAAe,oBAAqByR,EAAkBA,EAAmB,IACzE5K,KAAK,gCAWP,GAAI4C,GAAYH,EAAQyJ,cAAe,CACrC,MAAMC,EAAK1J,EAAQyJ,cACbE,EAAaD,EAAGE,WAAaF,EAAGG,WAChCC,EAAeH,EAAa,EAAII,KAAKC,MAAON,EAAGE,WAAaD,EAAc,KAAO,EAEvFR,GAAe,6HAIPzS,EAAe,kBAAmBgT,EAAGO,iBAAmB,0BACxDvT,EAAe,mBAAoBgT,EAAGQ,mCACtCxT,EAAe,cAAegT,EAAGE,WAAWP,iBAAmB,KAAOS,EAAe,sBACrFpT,EAAe,cAAegT,EAAGG,WAAWR,kCAC5C3S,EAAe,oBAAqBgT,EAAGS,iBAAkBT,EAAGS,iBAAmB,8CAIzF,CAEA,GAAI7K,MAAMC,QAAQS,EAAQoK,eAAiBpK,EAAQoK,aAAatQ,OAAS,EAAG,CAC1E,MAAMuQ,EAAa,IAAIrK,EAAQoK,cAC5BE,UACApN,IAAK0C,IACJ,MAAM2K,EAAUC,KAAKC,MAAQ7K,EAAI8K,UAC3BC,EACJJ,EAAU,IACN,GAAGR,KAAKa,MAAML,EAAU,YACxB,GAAGR,KAAKa,MAAML,EAAU,YAC9B,MAAO,mGAEkC3S,KAAK5B,WAAW4J,EAAIiL,kEACxBjT,KAAK5B,WAAW4J,EAAIpG,kEACnB5B,KAAK5B,WAAW2U,8CAIvDpN,KAAK,IACR4L,GAAe,uEAEUnJ,EAAQoK,aAAatQ,2DACRuQ,iCAGxC,MAAO,GAAIrK,EAAQ8K,UAAW,CAC5B,MAAMP,EAAUvK,EAAQ8K,UAAUP,QAC5BQ,EACJR,EAAU,IACN,GAAGR,KAAKa,MAAML,EAAU,YACxB,GAAGR,KAAKa,MAAML,EAAU,YAE9BpB,GAAe,8GAGkBvR,KAAK5B,WAAWgK,EAAQ8K,UAAUtR,8DAC5BuR,iCAGzC,MAAYvC,IACVW,GAAe,oIAOjBtB,EAAW9K,UAAYoM,CACzB,CAEA,2BAAAzB,CAA4B1H,GAC1B,MAAMgL,EAAQnO,SAASC,eAAe,kBACtC,IAAKkO,IAAUhL,EAAQiL,eACrB,OAGF,MAAMC,EAAKlL,EAAQiL,eACb9K,EAA4B,WAAjBH,EAAQwE,KAEzB,IAAI2G,EAAe,MACfC,EAAe,eACInE,IAAnBiE,EAAGG,cACDH,EAAGG,aAAe,IACpBF,EAAe,YACfC,EAAe,WACNF,EAAGG,aAAe,IAC3BF,EAAe,OACfC,EAAe,WACNF,EAAGG,aAAe,IAC3BF,EAAe,OACfC,EAAe,YAEfD,EAAe,OACfC,EAAe,YAInB,MAAME,OAAgCrE,IAAnBiE,EAAGG,YAA4BH,EAAGG,YAAc,EAC7DE,EAAcD,EAAa,IAAO,IAClCE,EAAWzB,KAAK0B,GAChBC,EAASF,EAAYD,EAAaxB,KAAK0B,GAAM,IAI7CE,EAHK,GAEL,GACc5B,KAAK6B,IAAIJ,GACvBK,EAHC,GACD,GAEc9B,KAAK+B,IAAIN,GACvBO,EALK,GAEL,GAGchC,KAAK6B,IAAIF,GACvBM,EALC,GACD,GAIcjC,KAAK+B,IAAIJ,GASvBO,EAAW,8OALfX,EAAa,EACT,cAAcK,KAAME,eAJTN,EAAa,IAAM,EAAI,OAIoBQ,KAAMC,yCAClCZ,+CAC1B,gGAO2FA,kBACzFE,8GAGAH,yCAKFe,OAAwBjF,IAAXiE,EAAGiB,IAAoBjB,EAAGiB,IAAM,MAAQ,MACrDC,OAA8BnF,IAAdiE,EAAGmB,OAAuBnB,EAAGmB,OAAS,MAAQ,MAC9DtL,EAAa2D,OAAO4H,SAASpB,EAAGnK,YAAcmK,EAAGnK,WAAa,EAC9DwL,EAAiB7H,OAAO4H,SAASpB,EAAGqB,gBAAkBrB,EAAGqB,eAAiB,EAC1EC,EAAoB5U,KAAK6U,mBAAmB1L,GAC5C2L,EAAwB9U,KAAK6U,mBAAmBF,GAChDI,EAAoBzB,EAAG0B,YAAc,QAE3C,IAAIC,EAAS,2IAIHZ,wIAIAnV,EAAiB,MAAOoV,EAAYhB,EAAGiB,IAAM,IAAM,QAAUjB,EAAGiB,IAAM,IAAM,UAAY,oBACxFrV,EAAiB,SAAUsV,EAAelB,EAAGmB,OAAS,IAAM,QAAUnB,EAAGmB,OAAS,GAAK,UAAY,oBACnGvV,EACA,cACA0V,EACAzL,EAAa,GAAM,QAAUA,EAAa,IAAO,UAAY,yKAQ7DrK,EAAe,cAAeiW,mBAC9BzB,EAAG4B,WAAapW,EAAe,cAAewU,EAAG4B,YAAc,mBAC/DpW,EAAe,kBAAmBgW,EAAuBH,EAAiB,oBAE1ErB,EAAG6B,iBACCrW,EAAe,qBAAsBkB,KAAKoV,mBAAmB9B,EAAG6B,mBAChE,WAKZF,GADE1M,EACQ,iBACFzJ,EAAe,mBAAoBwU,EAAGlK,iBAAmB,GAAGqI,iBAAkB6B,EAAGlK,gBAAkB,mBACnGtK,EAAe,eAAgBwU,EAAG+B,YAAc,GAAG5D,iBAAkB6B,EAAG+B,WAAa,eAGnF,iBACFvW,EAAe,aAAcwU,EAAGgC,UAAY,GAAG7D,kCAC/C3S,EAAe,aAAcwU,EAAGiC,UAAY,GAAG9D,iBAAkB6B,EAAGiC,SAAW,aAIzFN,GAAU,yDAMV7B,EAAMjO,UAAY8P,CACpB,CAEA,sBAAAlF,CAAuB3H,GACrB,MAAMoN,EAAevQ,SAASC,eAAe,aAC7C,IAAKsQ,IAAiBpN,EAAQqN,UAC5B,OAGF,MAAMC,EAAKtN,EAAQqN,UACblN,EAA4B,WAAjBH,EAAQwE,KAEnB+I,EAAapN,EAAWmN,EAAGE,YAAcF,EAAGG,SAAWH,EAAGI,WAAaJ,EAAGK,QAC1EC,EAAiBhW,KAAKiW,YAAYN,EAAa,EAAIA,EAAa,GAChEO,EAAeR,EAAGQ,cAAgB,EAClCC,EAAcT,EAAGS,aAAe,EAChCC,EAAiBV,EAAGU,gBAAkB,EACtCC,EAAgBX,EAAGW,eAAiB,EACpCC,EAAoBZ,EAAGY,mBAAqB,EAC5CC,EAAgBb,EAAGa,eAAiB,EACpCC,EAAyBd,EAAGc,wBAA0B,EAE5D,IAAIC,EACAC,EACAnO,GACFkO,EAAiB,CACfnX,EAAa,0BAA2BoW,EAAGiB,mBAC3CrX,EAAa,iCAAkCoW,EAAGkB,sBAClDtX,EAAa,kBAAmB0W,GAAgB,GAAM,GACtD1W,EAAa,eAAgBoW,EAAGmB,WAAWpF,mBAE7CiF,EAAgB,CACdpX,EAAa,gBAAiBoW,EAAGoB,uBAAyB9W,KAAKiW,YAAYC,IAC3E5W,EAAa,wBAAyB8W,EAAe3E,kBACrDnS,EAAa,0BAA2BgX,EAAkB7E,kBAC1DnS,EAAa,sBAAuBiX,EAAc9E,qBAGpDgF,EAAiB,CACfnX,EAAa,8BAA+BoW,EAAGqB,kBAC/CzX,EACE,kCACAoW,EAAGsB,qBAAuBhX,KAAKiW,YAAYP,EAAGI,YAAc,IAE9DxW,EAAa,kBAAmB0W,GAAgB,GAAM,GACtD1W,EAAa,mBAAoBoW,EAAGuB,UAAUxF,mBAEhDiF,EAAgB,CACdpX,EAAa,oBAAqBoW,EAAGwB,sBAAwBlX,KAAKiW,YAAYE,IAC9E7W,EAAa,4BAA6B+W,EAAc5E,kBACxDnS,EAAa,wBAAyBkX,EAAuB/E,oBAIjE,MAAM0F,EAAgB,oHAGQ5O,EAAW,UAAY,sDACnBA,EAAWmN,EAAG0B,iBAAmB1B,EAAG2B,8DACpC9O,EAAW,cAAgB,2HAG3BmN,EAAG4B,kLAIH5B,EAAG6B,qPAODd,EAAe9Q,KAAK,qKAKpB+Q,EAAc/Q,KAAK,wCAGjD3F,KAAKwX,qBAAqB9B,EAAG+B,QAASlP,yBAI5CiN,EAAarQ,UAAYgS,CAC3B,CAEA,oBAAAK,CACEC,EACAlP,GAEA,IAAKkP,GAAWA,EAAQvV,OAAS,EAC/B,MAAO,mGAEgCuV,EAAUA,EAAQvV,OAAS,0CAKpE,MAEMwV,EAAUvF,KAAKwF,OAAOF,EAAQnS,IAAKsS,GAAOrP,EAAWqP,EAAEC,QAAUD,EAAEE,QAAU,GAC7EC,EAASN,EACZnS,IAAI,CAACsS,EAAGI,IAGA,GAFIA,GAAKP,EAAQvV,OAAS,GALvB,OACC,IAKUqG,EAAWqP,EAAEC,QAAUD,EAAEE,QAAUJ,EAL7C,MAQZ/R,KAAK,KAEFsS,EAAmBjY,KAAKiW,YAAYyB,GAG1C,MAAO,yEAFiBQ,GAIKT,EAAQvV,mSAOjB6V,mHAIcE,gHAMpC,CAEA,0BAAAjI,CAA2B5H,GACzB,MAAM+P,EAAUlT,SAASC,eAAe,iBACxC,IAAKiT,IAAY/P,EAAQgQ,UACvB,OAGF,MAAMC,EAKDjQ,EAAQgQ,UAEb,GAAqB,IAAjBC,EAAMnW,OAMR,YALAiW,EAAQhT,UAAY,oKAQtB,MAAMmT,EAAgB,IAAIC,IAAIF,EAAM/S,IAAKkT,GAAMA,EAAE9M,KAAK4D,MAAM,KAAK,KAAKmJ,KAEtE,IAAIC,EAAW,qKAIuBL,EAAMnW,+KAINoW,2bAkBtCD,EAAMM,MAAM,EAAG,IAAI9S,QAAS2S,IAC1B,MAAMI,EAAWzG,KAAKwF,IAAIa,EAAEK,WAAY,GACxCH,GAAY,0DAEuB1Y,KAAK5B,WAAWoa,EAAE9M,UAAU1L,KAAK5B,WAAWoa,EAAE9M,+CACrD8M,EAAEM,2DACDN,EAAEO,4KAGqBH,2DACZJ,EAAEK,mFAO5CH,GAAY,mEAMRL,EAAMnW,OAAS,KACjBwW,GAAY,qEAEeL,EAAMnW,4CAKnCwW,GAAY,SACZP,EAAQhT,UAAYuT,CACtB,CAEA,uBAAAnP,CAAwByP,GACtB,MAAMC,EAAUhU,SAASC,eAAe,qBAClCgU,EAAMjU,SAASC,eAAe,qBACpC,IAAK+T,IAAYC,EACf,OAGFD,EAAQ7T,MAAMC,QAAU,GAExB,MAAMkJ,IAAYyK,EAAKzK,QACjB4K,IAAeH,EAAKG,WACpBC,EAAa7K,EAAW4K,EAAa,SAAW,SAAY,WAC5DE,EAAa9K,EAAW4K,EAAa,UAAY,UAAa,QAC9DG,EAAYH,EAAa,kBAAoB,YAC7CI,EAAoBP,EAAKO,mBAAqB,EAC9CC,EAAoBR,EAAKQ,mBAAqB,EAE9CC,EAAO,qFAGLra,EAAqB,QAAS,iCAAiCia,MAAeD,0BAC9Ela,EAAiB,OAAQoa,iBACzBpa,EAAiB,gBAAiBqa,EAAoB,qBACtDra,EAAiB,gBAAiBsa,EAAoB,uJAKpD1a,EAAe,mBAAoBka,EAAKU,eAAiB,GAAK,uBAC9D5a,EAAe,mBAAoBka,EAAKW,eAAiB,GAAK,uBAC9D7a,EAAe,cAAeka,EAAKY,WAAa,GAAK,uBACrD9a,EAAe,gBAA4BuQ,IAAhB2J,EAAKa,OAAuB1H,KAAKC,MAAM4G,EAAKa,QAAU,GAAK,uBACtF/a,EAAe,wBAAqCuQ,IAAjB2J,EAAKc,SAAwC,IAAfd,EAAKc,SAAeC,QAAQ,GAAK,GAAK,IAAKf,EAAKc,QAAU,6DAMrIZ,EAAI/T,UAAYsU,CAClB,CAEA,oBAAAhQ,CAAqBuP,GACnB,MAAMC,EAAUhU,SAASC,eAAe,kBAClCgU,EAAMjU,SAASC,eAAe,iBACpC,IAAK+T,IAAYC,EACf,OAGF,IAAKF,EAAKzK,QAER,YADA0K,EAAQ7T,MAAMC,QAAU,QAI1B4T,EAAQ7T,MAAMC,QAAU,GAExB,MAAMiU,GAAaN,EAAKpM,MAAQ,eAAerO,QAAQ,KAAM,KACvD2W,EAAa8D,EAAK9D,YAAc,UAEtC,IAAI8E,EAAY,GACZhB,EAAKiB,QAEPD,EADoBhN,OAAOkN,QAAQlB,EAAKiB,OAErC3U,IAAI,EAAEjD,EAAM8X,MACX,MAAMC,EAAW/X,IAAS6S,EACpBxQ,GAAUyV,EAAKzV,QAAU,WAAW7F,cAEpCwb,EADkB,SAAX3V,EACa,UAAY,QACtC,MAAO,wCACoB0V,EAAW,SAAW,sFAEnBpa,KAAK5B,WAAWiE,4BACxC+X,EAAW,sDAAwD,6CAC3CC,MAAera,KAAK5B,WAAWsG,EAAOwI,kGAG9DpO,EAAe,OAAQqb,EAAK5F,KAAO,GAAK,yBACxCzV,EAAe,eAAmC,KAAlBqb,EAAKG,MAAQ,IAAUP,QAAQ,GAAK,yDAK3EpU,KAAK,KAGV,MAAM8T,EAAO,qFAGLva,EAAiB,OAAQoa,iBACzBpa,EAAiB,cAAegW,0DAEP8E,6KAOjCd,EAAI/T,UAAYsU,EAEhB,MAAMc,EAActV,SAASC,eAAe,eACxCqV,GACFA,EAAYxU,iBAAiB,QAAS,IAAM/F,KAAKwa,kBAErD,CAEA,qBAAMA,GACJ,MAAM1X,EAAS9C,KAAKE,mBACd4F,EAAMb,SAASC,eAAe,eAChCY,IACFA,EAAI2U,UAAW,GAEjB,IACE,MAAMjX,QAAiBxD,KAAK8B,QAAQ9B,KAAKsD,oBAAoBR,GAAS,CAAEkM,OAAQ,SAChF,GAAIxL,EAASzB,GAAI,CACf,MAAM6N,QAAepM,EAASxB,OAC9BhC,KAAK0B,iBAAiB,mCAAmCkO,EAAOsF,aAAc,WAC9ElV,KAAKmH,YAAYrE,EACnB,KAAO,CACL,MAAMkF,QAAYxE,EAASxB,OAC3BhC,KAAK0B,iBACiB,MAApB8B,EAASkB,OACL1E,KAAK4E,mBAAmB,uBACxB,qBAAuBoD,EAAIxG,OAAS,iBACxC,QAEJ,CACF,CAAE,MAAOA,GACP,MAAMwG,EAAMxG,EACZxB,KAAK0B,iBACHsG,EAAIrD,eACA3E,KAAK4E,mBAAmB,uBACxB,oBAAsBoD,EAAIpG,QAC9B,QAEJ,C,QACMkE,IACFA,EAAI2U,UAAW,EAEnB,CACF,CAEA,uBAAApR,CAAwB2P,GACtB,MAAMC,EAAUhU,SAASC,eAAe,qBAClCgU,EAAMjU,SAASC,eAAe,oBACpC,IAAK+T,IAAYC,EACf,OAIF,KADgBF,EAAK9P,QAAU8P,EAAK7P,YAAc6P,EAAK5P,iBAGrD,YADA6P,EAAQ7T,MAAMC,QAAU,QAI1B4T,EAAQ7T,MAAMC,QAAU,GAExB,IAAIoU,EAAO,6BAEX,GAAIT,EAAK9P,OAAQ,CACf,MAAMwR,EAAoC1B,EAAK9P,OAAOwR,cAAgB,CAAC,EACjEC,EAAe3N,OAAOkN,QAAQQ,GAAcpV,IAAI,EAAEsV,EAAQC,MAC9D,IAAIC,EAAQ,UAiBZ,MAhBqB,iBAAVD,EACTC,EAAQD,EAAMhc,cACLgc,GAA0B,iBAAVA,GAAsBA,EAAMC,QACrDA,EAAQxc,OAAOuc,EAAMC,OAAOjc,eAGhB,SAAVic,IACFA,EAAQ,WAEI,UAAVA,IACFA,EAAQ,YAEI,YAAVA,GAAiC,aAAVA,IACzBA,EAAQ,WAGH,CACLF,SACAE,QACA9b,MAAO6b,GAA0B,iBAAVA,EAAqBA,EAAM7b,WAAQqQ,KAGxD0L,EAAaJ,EAAazY,OAE1B8Y,EAAaL,EAChBrV,IAAK2V,IACJ,MAAMC,OACY7L,IAAhB4L,EAAMjc,MAAsB,KAAKgB,KAAK5B,WAAWE,OAAO2c,EAAMjc,WAAa,GAC7E,MAr2DiB,EAACD,EAAeM,EAAmBJ,GAAW,IAAU,4BAC1DA,EAAW,SAAW,sCAChBb,EAAWW,4CACXM,uBAk2Dd8b,CACLF,EAAML,OACN,kCAAkCK,EAAMH,UAAU9a,KAAK5B,WAAW6c,EAAMH,MAAM5N,iBAAiBgO,WAC/E,aAAhBD,EAAMH,SAGTnV,KAAK,IAOR8T,GAAQ,8FAJS,IAAfsB,EACI,yFACA,2BAA2BC,mCAQnC,CAEA,GAAIhC,EAAK7P,YAAc6P,EAAK7P,WAAW+C,QAAS,CAC9C,MAAMA,EAAU8M,EAAK7P,WAAW+C,QAChCuN,GAAQ,kIAIA3a,EAAe,qBAAgD,IAA1BoN,EAAQkP,iBAAuBrB,QAAQ,GAAK,IAAK7N,EAAQkP,gBAAkB,qBAChHtc,EAAe,iBAAwC,IAAtBoN,EAAQmP,aAAmBtB,QAAQ,GAAK,IAAK7N,EAAQmP,YAAc,oBACpGvc,EAAe,QAASoN,EAAQoP,OAAS,qDAInD,CAEA,GAAItC,EAAK5P,iBAAmB4P,EAAK5P,gBAAgB8C,QAAS,CACxD,MAAMA,EAAU8M,EAAK5P,gBAAgB8C,QACrCuN,GAAQ,2IAIA3a,EAAe,gBAAuC,IAAtBoN,EAAQqP,aAAmBxB,QAAQ,GAAK,IAAK7N,EAAQqP,YAAc,qBACnGzc,EAAe,gBAAmC,IAAlBoN,EAAQsP,SAAezB,QAAQ,GAAK,qBACpEjb,EAAe,YAA+B,IAAlBoN,EAAQwL,SAAeqC,QAAQ,GAAK,IAAK7N,EAAQwL,QAAU,+CAIjG,CAEA+B,GAAQ,SACRP,EAAI/T,UAAYsU,CAClB,CAIA,YAAAvS,GACE,MAAMuU,EAAYxW,SAASC,eAAe,UAC1C,IAAKuW,EACH,OAGF,IAAIC,EAAa,gCAkBjB,GAhBI1b,KAAKY,iBACP8a,GAAc,gFAEsB1b,KAAK5B,WAAWE,OAAQ0B,KAAKY,iBAA6CyK,6GAK9GqQ,GAAc,+KAQZ1b,KAAKa,oBAAuBb,KAAKa,mBAA+C4K,UAAW,CAC7F,MAAMH,EAAMtL,KAAKa,mBAMjB6a,GAAc,kFALKpQ,EAAIG,UAAwBvJ,4LACxBlC,KAAK5B,WAAYkN,EAAIzG,SAAsB,8CAC5CyG,EAAIG,UACvBnG,IAAKiK,GAAMvP,KAAK5B,WAAWmR,EAAE7D,OAC7B/F,KAAK,+BAWV,MACE+V,GAAc,iLASd1b,KAAKc,sBACJd,KAAKc,qBAAiDkL,mBACrDhM,KAAKc,qBAAiDkL,kBACrD9J,OAAS,IAUZwZ,GAAc,oFAPX1b,KAAKc,qBAAiDkL,kBACvD9J,+LAEClC,KAAKc,qBAAiDkL,kBAEtD1G,IAAKiK,GAAMvP,KAAK5B,WAAWmR,IAC3B5J,KAAK,iCAYV8V,EAAUtW,UAAYuW,CACxB,CAIA,yBAAA3T,CAA0B4T,GACxB,MAAMC,EAAW5b,KAAK6b,sBAAsB7b,KAAKI,cAC3C0b,EAAS9b,KAAK+b,UAAUH,GAAY,CAAC,EAAGD,GAAiB,CAAC,GAChE,GAAIjU,MAAMC,QAAQmU,EAAO7b,aACvB,OAAO6b,EAET,MAAMnM,EAAuB3P,KAAKuC,oBAAoBuZ,EAAOtZ,YAE7D,OADAsZ,EAAOtZ,WAAamN,GAAwB,SACrCmM,CACT,CAEA,mBAAAvZ,CAAoBvD,GAClB,OAAc,IAAVA,GAA4B,WAAVA,EACb,UAEK,IAAVA,GAA6B,WAAVA,EACd,cADT,CAIF,CAEA,qBAAA6c,CACEG,GAEA,IAAKA,GAAoC,iBAAfA,EACxB,OAGF,MAAMC,EAAmC,WAApBD,EAAW7Z,QAAuB6Z,EAAWE,WAC5DJ,EAAkC,CAAC,EACzC,IAAIK,GAAU,EAOd,GALIF,GAAgBD,EAAWI,SAAWpc,KAAKqc,cAAcL,EAAWI,WACtEpP,OAAOsP,OAAOR,EAAQ9b,KAAK0P,UAAUsM,EAAWI,UAChDD,GAAU,GAGRH,EAAWE,YAAclc,KAAKqc,cAAcL,EAAWE,YACzD,IAAK,MAAOK,EAAKvd,KAAUgO,OAAOkN,QAAQ8B,EAAWE,YAAa,CAChE,MAAMM,EAAgBxc,KAAK6b,sBAAsB7c,QAC3BqQ,IAAlBmN,GACFV,EAAOS,GAAOC,EACdL,GAAU,GACDnd,GAAiD,WAAvCA,EAA8BmD,MACjD2Z,EAAOS,GAAO,CAAC,EACfJ,GAAU,GACDnd,GAAiD,WAAvCA,EAA8BmD,OACjD2Z,EAAOS,GAAO,GACdJ,GAAU,EAEd,CAGF,GAAIH,EAAWS,cAAgBzc,KAAKqc,cAAcL,EAAWS,cAC3D,IAAK,MAAMC,KAAmB1P,OAAO2P,OAAOX,EAAWS,cAAe,CACpE,MAAMG,EAAqB5c,KAAK6b,sBAC9Ba,GAEEE,GAAsB5c,KAAKqc,cAAcO,KAC3C5P,OAAOsP,OAAOR,EAAQ9b,KAAK+b,UAAUD,EAAQc,IAC7CT,GAAU,EAEd,CAGF,IAAK,MAAMU,IAAgB,CAAC,QAAS,QAAS,SAAU,CACtD,MAAMC,EAAYd,EAAWa,GACxBnV,MAAMC,QAAQmV,IAInBA,EAAUjX,QAASkX,IACjB,MAAMC,EAAehd,KAAK6b,sBAAsBkB,GAChD,QAAqB1N,IAAjB2N,EAIJ,OAAIhd,KAAKqc,cAAcW,IACrBhQ,OAAOsP,OAAOR,EAAQ9b,KAAK+b,UAAUD,EAAQkB,SAC7Cb,GAAU,UAIPA,QAAkC9M,IAAvB2M,EAAWI,WAI3BD,GAAU,KAEd,CAEA,OAAIA,EACKL,OAGkBzM,IAAvB2M,EAAWI,QACNpc,KAAK0P,UAAUsM,EAAWI,cADnC,CAKF,CAEA,aAAAC,CAAcrd,GACZ,OAAiB,OAAVA,GAAmC,iBAAVA,IAAuB0I,MAAMC,QAAQ3I,EACvE,CAEA,SAAA0Q,CAAU1Q,GACR,GAAI0I,MAAMC,QAAQ3I,GAChB,OAAOA,EAAMsG,IAAKyX,GAAS/c,KAAK0P,UAAUqN,IAE5C,GAAI/c,KAAKqc,cAAcrd,GAAQ,CAC7B,MAAMie,EAAiC,CAAC,EACxC,IAAK,MAAOV,EAAKW,KAAelQ,OAAOkN,QAAQlb,GAC7Cie,EAAMV,GAAOvc,KAAK0P,UAAUwN,GAE9B,OAAOD,CACT,CACA,OAAOje,CACT,CAEA,SAAA+c,CAAUoB,EAAoBC,GAC5B,QAAsB/N,IAAlB+N,EACF,OAAOpd,KAAK0P,UAAUyN,GAExB,GAAIzV,MAAMC,QAAQyV,GAChB,OAAOpd,KAAK0P,UAAU0N,GAGxB,GAAIpd,KAAKqc,cAAcc,IAAcnd,KAAKqc,cAAce,GAAgB,CACtE,MAAMtB,EAAS9b,KAAK0P,UAAUyN,GAC9B,IAAK,MAAOZ,EAAKvd,KAAUgO,OAAOkN,QAAQkD,GACxCtB,EAAOS,GAAOvc,KAAK+b,UAAWoB,EAAsCZ,GAAMvd,GAE5E,OAAO8c,CACT,CAEA,OAAO9b,KAAK0P,UAAU0N,EACxB,CAEA,WAAAnH,CAAYoH,GACV,IAAKA,GAASA,GAAS,EACrB,MAAO,MAET,MACMC,EAAQ,CAAC,IAAK,KAAM,KAAM,MAC1BtF,EAAI7F,KAAKoL,IAAIpL,KAAKa,MAAMb,KAAKqL,IAAIH,GAASlL,KAAKqL,IAF3C,OAEoDF,EAAMpb,OAAS,GAC7E,OAAOub,YAAYJ,EAAQlL,KAAKuL,IAHtB,KAG6B1F,IAAI+B,QAAQ,IAAM,IAAMuD,EAAMtF,EACvE,CAEA,kBAAAnD,CAAmB7V,GACjB,OAAK8N,OAAO4H,SAAS1V,IAAUA,GAAS,EAC/B,QAEO,IAARA,GAAa+a,QAAQ,GAAK,GACpC,CAEA,kBAAA3E,CAAmBtC,GACjB,IAAKhG,OAAO4H,SAAS5B,IAAcA,GAAa,EAC9C,MAAO,MAET,MAAM6K,EAAQxL,KAAKwF,IAAI,EAAG/E,KAAKC,MAAQC,GACvC,OAAI6K,EAAQ,IACH,WAELA,EAAQ,IACH,GAAGxL,KAAKa,MAAM2K,EAAQ,YAE3BA,EAAQ,KACH,GAAGxL,KAAKa,MAAM2K,EAAQ,YAExB,GAAGxL,KAAKa,MAAM2K,EAAQ,YAC/B,CAEA,UAAAvf,CAAWwf,GACT,MAAM1E,EAAMjU,SAASmI,cAAc,OAEnC,OADA8L,EAAI1L,YAAcoQ,EACX1E,EAAI/T,SACb,CAEA,gBAAAzD,CAAiBE,EAAiBO,EAAO,WACvC,MAAM0b,EAAe5Y,SAASC,eAAe,gBACxC2Y,IAGD7d,KAAKkB,oBACPsJ,aAAaxK,KAAKkB,oBAEpB2c,EAAarQ,YAAc5L,EAC3Bic,EAAaxQ,UAAY,gBAAgBlL,SAEzCnC,KAAKkB,mBAAqBuJ,WAAW,KACnCoT,EAAaC,UAAUrQ,OAAO,QAC9BzN,KAAKkB,mBAAqB,MApvEH,KAsvE3B,EAWF+D,SAASc,iBAAiB,mBAAoB,KAC5C/H,OAAO+f,oBAAsB,IAAIle,IAInCoF,SAASc,iBAAiB,mBAAoB,KACvC/H,OAAO+f,sBAIR9Y,SAAS+Y,OACPhgB,OAAO+f,oBAAoBzd,kBAC7BoJ,cAAc1L,OAAO+f,oBAAoBzd,iBACzCtC,OAAO+f,oBAAoBzd,gBAAkB,MAErCtC,OAAO+f,oBAAoBzd,kBACrCtC,OAAO+f,oBAAoB5W,cAC3BnJ,OAAO+f,oBAAoBxc,0BAI/BvD,OAAO+H,iBAAiB,eAAgB,KAClC/H,OAAO+f,qBAAuB/f,OAAO+f,oBAAoBzd,kBAC3DoJ,cAAc1L,OAAO+f,oBAAoBzd,iBACzCtC,OAAO+f,oBAAoBzd,gBAAkB,O,GC/xE7C2d,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqB9O,IAAjB+O,EACH,OAAOA,EAAaC,QAGrB,IAAIC,EAASL,EAAyBE,GAAY,CAGjDE,QAAS,CAAC,GAOX,OAHAE,EAAoBJ,GAAUG,EAAQA,EAAOD,QAASH,GAG/CI,EAAOD,OACf,CAGAH,EAAoBM,EAAID,EAGxBL,EAAoBvb,EAAIsb,EC5BxBC,EAAoBO,EAAI,CAAC,EAGzBP,EAAoBQ,EAAKC,GACjBpX,QAAQC,IAAIwF,OAAOC,KAAKiR,EAAoBO,GAAGG,OAAO,CAACC,EAAUtC,KACvE2B,EAAoBO,EAAElC,GAAKoC,EAASE,GAC7BA,GACL,KCNJX,EAAoBY,EAAKH,GAEZA,EAAL,2BCFRT,EAAoBa,SAAYJ,MCDhCT,EAAoBc,EAAI,WACvB,GAA0B,iBAAfC,WAAyB,OAAOA,WAC3C,IACC,OAAOjf,MAAQ,IAAIkf,SAAS,cAAb,EAChB,CAAE,MAAOR,GACR,GAAsB,iBAAX1gB,OAAqB,OAAOA,MACxC,CACA,CAPuB,GCAxBkgB,EAAoBiB,EAAI,CAACC,EAAKC,IAAUrS,OAAOsS,UAAUC,eAAeC,KAAKJ,EAAKC,GRA9E/hB,EAAa,CAAC,EACdC,EAAoB,qBAExB2gB,EAAoBuB,EAAI,CAACC,EAAKC,EAAMpD,EAAKoC,KACxC,GAAGrhB,EAAWoiB,GAAQpiB,EAAWoiB,GAAKhX,KAAKiX,OAA3C,CACA,IAAIC,EAAQC,EACZ,QAAWxQ,IAARkN,EAEF,IADA,IAAIuD,EAAU7a,SAAS8a,qBAAqB,UACpC/H,EAAI,EAAGA,EAAI8H,EAAQ5d,OAAQ8V,IAAK,CACvC,IAAIzI,EAAIuQ,EAAQ9H,GAChB,GAAGzI,EAAEyQ,aAAa,QAAUN,GAAOnQ,EAAEyQ,aAAa,iBAAmBziB,EAAoBgf,EAAK,CAAEqD,EAASrQ,EAAG,KAAO,CACpH,CAEGqQ,IACHC,GAAa,GACbD,EAAS3a,SAASmI,cAAc,WAEzB6S,QAAU,QACb/B,EAAoBgC,IACvBN,EAAOO,aAAa,QAASjC,EAAoBgC,IAElDN,EAAOO,aAAa,eAAgB5iB,EAAoBgf,GAExDqD,EAAOQ,IAAMV,GAEdpiB,EAAWoiB,GAAO,CAACC,GACnB,IAAIU,EAAmB,CAACC,EAAMC,KAE7BX,EAAOY,QAAUZ,EAAOa,OAAS,KACjCjW,aAAakW,GACb,IAAIC,EAAUrjB,EAAWoiB,GAIzB,UAHOpiB,EAAWoiB,GAClBE,EAAOgB,YAAchB,EAAOgB,WAAWC,YAAYjB,GACnDe,GAAWA,EAAQ9a,QAASib,GAAQA,EAAGP,IACpCD,EAAM,OAAOA,EAAKC,IAElBG,EAAUjW,WAAW4V,EAAiBU,KAAK,UAAM1R,EAAW,CAAElN,KAAM,UAAW6e,OAAQpB,IAAW,MACtGA,EAAOY,QAAUH,EAAiBU,KAAK,KAAMnB,EAAOY,SACpDZ,EAAOa,OAASJ,EAAiBU,KAAK,KAAMnB,EAAOa,QACnDZ,GAAc5a,SAASgc,KAAKvT,YAAYkS,EAnCkB,G,MSJ3D1B,EAAoBgD,EAAI,CAAC,EACzB,IAAIC,EAAe,CAAC,EAChBC,EAAa,CAAC,EAClBlD,EAAoBmD,EAAI,CAAChf,EAAMif,KAC1BA,IAAWA,EAAY,IAE3B,IAAIC,EAAYH,EAAW/e,GAE3B,GADIkf,IAAWA,EAAYH,EAAW/e,GAAQ,CAAC,KAC5Cif,EAAUE,QAAQD,IAAc,GAAnC,CAGA,GAFAD,EAAU5Y,KAAK6Y,GAEZJ,EAAa9e,GAAO,OAAO8e,EAAa9e,GAEvC6b,EAAoBiB,EAAEjB,EAAoBgD,EAAG7e,KAAO6b,EAAoBgD,EAAE7e,GAAQ,CAAC,GAEvF,IAAIof,EAAQvD,EAAoBgD,EAAE7e,GAI9Bqf,EAAa,oBAiBb7C,EAAW,GAOf,MALM,YADCxc,GAjBQ,EAACA,EAAMsf,EAASC,EAASC,KACvC,IAAIC,EAAWL,EAAMpf,GAAQof,EAAMpf,IAAS,CAAC,EACzC0f,EAAgBD,EAASH,KACzBI,IAAmBA,EAAcC,SAAW,IAAWD,EAAcF,MAAQA,EAAQH,EAAaK,EAAclU,SAAQiU,EAASH,GAAW,CAAE7d,IAgBpH,IAAOoa,EAAoBQ,EAAE,KAAKuD,KAAK,IAAM,IAAQ/D,EAAoB,OAhByDrQ,KAAM6T,EAAYG,OAAO,KAgBxLK,CAAS,QAAS,WAKbf,EAAa9e,GADhBwc,EAAS3c,OACeqF,QAAQC,IAAIqX,GAAUoD,KAAK,IAAOd,EAAa9e,GAAQ,GADlC,CAnCL,E,WCR7C,IAAI8f,EACAjE,EAAoBc,EAAEoD,gBAAeD,EAAYjE,EAAoBc,EAAEpb,SAAW,IACtF,IAAIqB,EAAWiZ,EAAoBc,EAAE/Z,SACrC,IAAKkd,GAAald,IACbA,EAASod,eAAkE,WAAjDpd,EAASod,cAAcC,QAAQpV,gBAC5DiV,EAAYld,EAASod,cAAcjC,MAC/B+B,GAAW,CACf,IAAIrC,EAAU7a,EAAS8a,qBAAqB,UAC5C,GAAGD,EAAQ5d,OAEV,IADA,IAAI8V,EAAI8H,EAAQ5d,OAAS,EAClB8V,GAAK,KAAOmK,IAAc,aAAaI,KAAKJ,KAAaA,EAAYrC,EAAQ9H,KAAKoI,GAE3F,CAID,IAAK+B,EAAW,MAAM,IAAIxgB,MAAM,yDAChCwgB,EAAYA,EAAU5jB,QAAQ,SAAU,IAAIA,QAAQ,OAAQ,IAAIA,QAAQ,QAAS,IAAIA,QAAQ,YAAa,KAC1G2f,EAAoB1F,EAAI2J,C,WCbxB,IAAIK,EAAkB,CACrB,IAAK,GAGNtE,EAAoBO,EAAEgE,EAAI,CAAC9D,EAASE,KAElC,IAAI6D,EAAqBxE,EAAoBiB,EAAEqD,EAAiB7D,GAAW6D,EAAgB7D,QAAWtP,EACtG,GAA0B,IAAvBqT,EAGF,GAAGA,EACF7D,EAASnW,KAAKga,EAAmB,QAC3B,CAGL,IAAIC,EAAU,IAAIpb,QAAQ,CAACqb,EAASC,IAAYH,EAAqBF,EAAgB7D,GAAW,CAACiE,EAASC,IAC1GhE,EAASnW,KAAKga,EAAmB,GAAKC,GAGtC,IAAIjD,EAAMxB,EAAoB1F,EAAI0F,EAAoBY,EAAEH,GAEpDnd,EAAQ,IAAIG,MAgBhBuc,EAAoBuB,EAAEC,EAfFa,IACnB,GAAGrC,EAAoBiB,EAAEqD,EAAiB7D,KAEf,KAD1B+D,EAAqBF,EAAgB7D,MACR6D,EAAgB7D,QAAWtP,GACrDqT,GAAoB,CACtB,IAAII,EAAYvC,IAAyB,SAAfA,EAAMpe,KAAkB,UAAYoe,EAAMpe,MAChE4gB,EAAUxC,GAASA,EAAMS,QAAUT,EAAMS,OAAOZ,IACpD5e,EAAMI,QAAU,iBAAmB+c,EAAU,cAAgBmE,EAAY,KAAOC,EAAU,IAC1FvhB,EAAMa,KAAO,iBACbb,EAAMW,KAAO2gB,EACbthB,EAAMM,QAAUihB,EAChBL,EAAmB,GAAGlhB,EACvB,GAGuC,SAAWmd,EAASA,EAE/D,GAeH,IAAIqE,EAAuB,CAACC,EAA4BjK,KACvD,IAGImF,EAAUQ,GAHTuE,EAAUC,EAAallB,GAAW+a,EAGhBhB,EAAI,EAC3B,GAAGkL,EAASE,KAAMxjB,GAAgC,IAAxB4iB,EAAgB5iB,IAAa,CACtD,IAAIue,KAAYgF,EACZjF,EAAoBiB,EAAEgE,EAAahF,KACrCD,EAAoBM,EAAEL,GAAYgF,EAAYhF,IAG7ClgB,GAAsBA,EAAQigB,EAClC,CAEA,IADG+E,GAA4BA,EAA2BjK,GACrDhB,EAAIkL,EAAShhB,OAAQ8V,IACzB2G,EAAUuE,EAASlL,GAChBkG,EAAoBiB,EAAEqD,EAAiB7D,IAAY6D,EAAgB7D,IACrE6D,EAAgB7D,GAAS,KAE1B6D,EAAgB7D,GAAW,GAKzB0E,EAAqBC,KAAoC,8BAAIA,KAAoC,+BAAK,GAC1GD,EAAmBxd,QAAQmd,EAAqBjC,KAAK,KAAM,IAC3DsC,EAAmB3a,KAAOsa,EAAqBjC,KAAK,KAAMsC,EAAmB3a,KAAKqY,KAAKsC,G,KClF7DnF,EAAoB,K","sources":["webpack://signalk-edge-link/webpack/runtime/load script","webpack://signalk-edge-link/./src/webapp/utils/apiFetch.ts","webpack://signalk-edge-link/./src/webapp/index.ts","webpack://signalk-edge-link/webpack/bootstrap","webpack://signalk-edge-link/webpack/runtime/ensure chunk","webpack://signalk-edge-link/webpack/runtime/get javascript chunk filename","webpack://signalk-edge-link/webpack/runtime/get mini-css chunk filename","webpack://signalk-edge-link/webpack/runtime/global","webpack://signalk-edge-link/webpack/runtime/hasOwnProperty shorthand","webpack://signalk-edge-link/webpack/runtime/sharing","webpack://signalk-edge-link/webpack/runtime/publicPath","webpack://signalk-edge-link/webpack/runtime/jsonp chunk loading","webpack://signalk-edge-link/webpack/startup"],"sourcesContent":["var inProgress = {};\nvar dataWebpackPrefix = \"signalk-edge-link:\";\n// loadScript function to load a script via script tag\n__webpack_require__.l = (url, done, key, chunkId) => {\n\tif(inProgress[url]) { inProgress[url].push(done); return; }\n\tvar script, needAttach;\n\tif(key !== undefined) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tfor(var i = 0; i < scripts.length; i++) {\n\t\t\tvar s = scripts[i];\n\t\t\tif(s.getAttribute(\"src\") == url || s.getAttribute(\"data-webpack\") == dataWebpackPrefix + key) { script = s; break; }\n\t\t}\n\t}\n\tif(!script) {\n\t\tneedAttach = true;\n\t\tscript = document.createElement('script');\n\n\t\tscript.charset = 'utf-8';\n\t\tif (__webpack_require__.nc) {\n\t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n\t\t}\n\t\tscript.setAttribute(\"data-webpack\", dataWebpackPrefix + key);\n\n\t\tscript.src = url;\n\t}\n\tinProgress[url] = [done];\n\tvar onScriptComplete = (prev, event) => {\n\t\t// avoid mem leaks in IE.\n\t\tscript.onerror = script.onload = null;\n\t\tclearTimeout(timeout);\n\t\tvar doneFns = inProgress[url];\n\t\tdelete inProgress[url];\n\t\tscript.parentNode && script.parentNode.removeChild(script);\n\t\tdoneFns && doneFns.forEach((fn) => (fn(event)));\n\t\tif(prev) return prev(event);\n\t}\n\tvar timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);\n\tscript.onerror = onScriptComplete.bind(null, script.onerror);\n\tscript.onload = onScriptComplete.bind(null, script.onload);\n\tneedAttach && document.head.appendChild(script);\n};","/// <reference lib=\"dom\" />\r\n\r\nexport const MANAGEMENT_TOKEN_ERROR_MESSAGE = \"Management token required/invalid.\";\r\n\r\ninterface AuthConfig {\r\n token: string | null;\r\n localStorageKey: string;\r\n queryParam: string;\r\n includeTokenInQuery: boolean;\r\n headerMode: string;\r\n}\r\n\r\ndeclare global {\r\n interface Window {\r\n __EDGE_LINK_AUTH__?: Partial<AuthConfig>;\r\n }\r\n}\r\n\r\nconst DEFAULT_AUTH_CONFIG: AuthConfig = {\r\n token: null,\r\n localStorageKey: \"signalkEdgeLinkManagementToken\",\r\n queryParam: \"edgeLinkToken\",\r\n // Default to false: query-parameter tokens leak into browser history, server\r\n // access logs, and Referer headers. Set includeTokenInQuery: true in\r\n // window.__EDGE_LINK_AUTH__ only when you explicitly need URL-based auth.\r\n includeTokenInQuery: false,\r\n headerMode: \"both\"\r\n};\r\n\r\nfunction readRuntimeAuthConfig(): AuthConfig {\r\n if (typeof window === \"undefined\") {\r\n return DEFAULT_AUTH_CONFIG;\r\n }\r\n\r\n const runtime = window.__EDGE_LINK_AUTH__;\r\n if (!runtime || typeof runtime !== \"object\") {\r\n return DEFAULT_AUTH_CONFIG;\r\n }\r\n\r\n return { ...DEFAULT_AUTH_CONFIG, ...runtime };\r\n}\r\n\r\nfunction resolveToken(config: AuthConfig): string {\r\n if (config.token) {\r\n return String(config.token).trim();\r\n }\r\n\r\n if (typeof window === \"undefined\") {\r\n return \"\";\r\n }\r\n\r\n // SECURITY NOTE: Query parameter tokens can leak into browser history, server\r\n // access logs, and Referer headers. Prefer localStorage or\r\n // window.__EDGE_LINK_AUTH__.token for production deployments. Set\r\n // includeTokenInQuery: false in __EDGE_LINK_AUTH__ to disable this path.\r\n if (config.includeTokenInQuery && config.queryParam) {\r\n const tokenFromQuery = new URLSearchParams(window.location.search).get(config.queryParam);\r\n if (tokenFromQuery) {\r\n return tokenFromQuery.trim();\r\n }\r\n }\r\n\r\n if (config.localStorageKey && window.localStorage) {\r\n const tokenFromStorage = window.localStorage.getItem(config.localStorageKey);\r\n if (tokenFromStorage) {\r\n return tokenFromStorage.trim();\r\n }\r\n }\r\n\r\n return \"\";\r\n}\r\n\r\nfunction attachAuthHeaders(headers: Headers, token: string, headerMode: string): Headers {\r\n if (!token) {\r\n return headers;\r\n }\r\n\r\n const normalizedMode = (headerMode || \"both\").toLowerCase();\r\n if (\r\n normalizedMode === \"x-edge-link-token\" ||\r\n normalizedMode === \"token\" ||\r\n normalizedMode === \"both\"\r\n ) {\r\n headers.set(\"X-Edge-Link-Token\", token);\r\n }\r\n if (\r\n normalizedMode === \"authorization\" ||\r\n normalizedMode === \"bearer\" ||\r\n normalizedMode === \"both\"\r\n ) {\r\n headers.set(\"Authorization\", `Bearer ${token}`);\r\n }\r\n return headers;\r\n}\r\n\r\nexport function getAuthToken(): string {\r\n const config = readRuntimeAuthConfig();\r\n return resolveToken(config);\r\n}\r\n\r\nexport function getTokenHelpText(): string {\r\n const config = readRuntimeAuthConfig();\r\n const modeText =\r\n config.headerMode && String(config.headerMode).toLowerCase() === \"authorization\"\r\n ? \"Authorization: Bearer <token>\"\r\n : config.headerMode && String(config.headerMode).toLowerCase() === \"x-edge-link-token\"\r\n ? \"X-Edge-Link-Token\"\r\n : \"X-Edge-Link-Token and Authorization: Bearer <token>\";\r\n\r\n return `The server-side token is configured in plugin settings (managementApiToken) or via the SIGNALK_EDGE_LINK_MANAGEMENT_TOKEN environment variable. To authenticate from the browser, provide the token using window.__EDGE_LINK_AUTH__.token, query parameter \"${config.queryParam}\", or localStorage key \"${config.localStorageKey}\". Requests send ${modeText} when a token is available.`;\r\n}\r\n\r\nexport function apiFetch(input: string | Request, init: RequestInit = {}): Promise<Response> {\r\n const config = readRuntimeAuthConfig();\r\n const token = resolveToken(config);\r\n const headers = new Headers(init.headers || {});\r\n attachAuthHeaders(headers, token, config.headerMode);\r\n\r\n return fetch(input, {\r\n ...init,\r\n headers\r\n });\r\n}\r\n","import \"./styles.css\";\nimport { apiFetch, getTokenHelpText, MANAGEMENT_TOKEN_ERROR_MESSAGE } from \"./utils/apiFetch\";\n\n// Constants\nconst API_BASE_PATH = \"/plugins/signalk-edge-link\";\nconst DELTA_TIMER_MIN = 100;\nconst DELTA_TIMER_MAX = 10000;\nconst NOTIFICATION_TIMEOUT = 4000;\nconst METRICS_REFRESH_INTERVAL = 15000;\nconst JSON_SYNC_DEBOUNCE = 300;\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\ninterface ConnectionInfo {\n id: string;\n name?: string;\n type: string;\n healthy?: boolean;\n readyToSend?: boolean;\n}\n\ninterface AuthenticatedError extends Error {\n isUnauthorized?: boolean;\n}\n\n// Escape HTML special characters to prevent XSS when inserting dynamic values into innerHTML\nfunction escapeHtml(str: string | number): string {\n return String(str)\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n// HTML Template Helpers\nconst renderCard = (\n title: string,\n subtitle: string | null,\n contentId: string,\n contentClass = \"\"\n) => `\n <div class=\"config-section\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h2>${escapeHtml(title)}</h2>\n ${subtitle ? `<p class=\"subtitle\">${escapeHtml(subtitle)}</p>` : \"\"}\n </div>\n <div class=\"card-content\">\n <div id=\"${contentId}\" class=\"${contentClass || contentId + \"-info\"}\">\n <p>Loading ${escapeHtml(title.toLowerCase())}...</p>\n </div>\n </div>\n </div>\n </div>\n`;\n\nconst renderStatItem = (label: string, value: string | number, hasError = false) => `\n <div class=\"stat-item${hasError ? \" error\" : \"\"}\">\n <span class=\"stat-label\">${escapeHtml(label)}:</span>\n <span class=\"stat-value\">${escapeHtml(value)}</span>\n </div>\n`;\n\nconst renderMetricItem = (label: string, value: string | number, statusClass = \"\") => `\n <div class=\"metric-item${statusClass ? \" \" + statusClass : \"\"}\">\n <div class=\"metric-label\">${escapeHtml(label)}</div>\n <div class=\"metric-value\">${escapeHtml(value)}</div>\n </div>\n`;\n\n// Variants that accept a pre-sanitized HTML string as the value (label is still escaped)\nconst renderMetricItemHtml = (label: string, htmlValue: string, statusClass = \"\") => `\n <div class=\"metric-item${statusClass ? \" \" + statusClass : \"\"}\">\n <div class=\"metric-label\">${escapeHtml(label)}</div>\n <div class=\"metric-value\">${htmlValue}</div>\n </div>\n`;\n\nconst renderStatItemHtml = (label: string, htmlValue: string, hasError = false) => `\n <div class=\"stat-item${hasError ? \" error\" : \"\"}\">\n <span class=\"stat-label\">${escapeHtml(label)}:</span>\n <span class=\"stat-value\">${htmlValue}</span>\n </div>\n`;\n\nconst renderBwStat = (\n label: string,\n value: string | number,\n isHighlight = false,\n isSuccess = false\n) => `\n <div class=\"bw-stat${isHighlight ? \" highlight\" : \"\"}\">\n <span class=\"bw-label\">${escapeHtml(label)}:</span>\n <span class=\"bw-value${isSuccess ? \" success-text\" : \"\"}\">${escapeHtml(value)}</span>\n </div>\n`;\n\nconst renderSectionGroup = (\n title: string,\n description: string | null,\n content: string,\n id = \"\"\n) => `\n <section class=\"page-group\"${id ? ` id=\"${id}\"` : \"\"}>\n <div class=\"page-group-header\">\n <h2>${escapeHtml(title)}</h2>\n ${description ? `<p>${escapeHtml(description)}</p>` : \"\"}\n </div>\n <div class=\"page-group-content\">\n ${content}\n </div>\n </section>\n`;\n\nclass DataConnectorConfig {\n connections: ConnectionInfo[];\n activeConnectionId: string | null;\n pluginConfig: Record<string, unknown> | null;\n pluginSchema: Record<string, unknown> | null;\n schemaCurrentMode: string | null;\n metricsInterval: ReturnType<typeof setInterval> | null;\n syncTimeout: ReturnType<typeof setTimeout> | null;\n /** True while syncFromJson() is rebuilding the path-input list — prevents\n * addPathItem() from triggering updateJsonFromForm() which would drop the\n * textarea's meta block on every path added. */\n isHydratingFromTextarea: boolean = false;\n tokenHelpText: string;\n deltaTimerConfig: Record<string, unknown> | null;\n subscriptionConfig: Record<string, unknown> | null;\n sentenceFilterConfig: Record<string, unknown> | null;\n protocolVersion: number;\n isServerMode: boolean;\n _refreshInFlight: boolean;\n _notificationTimer: ReturnType<typeof setTimeout> | null;\n\n constructor() {\n this.connections = [];\n this.activeConnectionId = null;\n this.pluginConfig = null;\n this.pluginSchema = null;\n this.schemaCurrentMode = null;\n this.metricsInterval = null;\n this.syncTimeout = null;\n this.tokenHelpText = getTokenHelpText();\n\n // Per-connection state (loaded for the active tab)\n this.deltaTimerConfig = null;\n this.subscriptionConfig = null;\n this.sentenceFilterConfig = null;\n this.protocolVersion = 1;\n this.isServerMode = false;\n this._refreshInFlight = false;\n this._notificationTimer = null;\n\n this.init();\n }\n\n async init() {\n try {\n await this.loadPluginConfiguration(false);\n await this.fetchConnections();\n this.renderPage();\n this.startMetricsRefresh();\n } catch (error: unknown) {\n console.error(\"Initialization error:\", error);\n this.showNotification(\n \"Failed to initialize application: \" +\n (error instanceof Error ? error.message : String(error)),\n \"error\"\n );\n }\n }\n\n // ── Connections list ───────────────────────────────────────────────────────\n\n async fetchConnections() {\n try {\n const res = await this.request(`${API_BASE_PATH}/connections`);\n if (res.ok) {\n this.connections = await res.json();\n }\n } catch (_e) {\n // /connections not available – fall back to legacy single-instance detection\n }\n\n if (!this.connections || this.connections.length === 0) {\n // Fallback: detect mode from plugin config and treat as single connection\n const type = this.detectModeFromConfig();\n this.connections = [{ id: \"_legacy\", name: \"Default\", type }];\n }\n\n if (!this.activeConnectionId) {\n this.activeConnectionId = this.connections[0].id;\n }\n }\n\n detectModeFromConfig(): string {\n if (this.pluginConfig) {\n const st = this.normalizeServerType(this.pluginConfig.serverType);\n if (st) {\n return st;\n }\n }\n if (this.schemaCurrentMode) {\n return this.schemaCurrentMode;\n }\n return \"client\";\n }\n\n getActiveConnection(): ConnectionInfo {\n return this.connections.find((c) => c.id === this.activeConnectionId) || this.connections[0];\n }\n\n isLegacyMode(): boolean {\n return this.connections.length === 1 && this.connections[0].id === \"_legacy\";\n }\n\n // ── API path helpers ───────────────────────────────────────────────────────\n\n metricsPath(connId: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/metrics`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/metrics`;\n }\n\n configPath(connId: string, filename: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/config/${filename}`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/config/${filename}`;\n }\n\n monitoringPath(connId: string, sub: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/monitoring/${sub}`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/monitoring/${sub}`;\n }\n\n congestionPath(connId: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/congestion`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/congestion`;\n }\n\n bondingPath(connId: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/bonding`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/bonding`;\n }\n\n bondingFailoverPath(connId: string): string {\n if (connId === \"_legacy\") {\n return `${API_BASE_PATH}/bonding/failover`;\n }\n return `${API_BASE_PATH}/connections/${encodeURIComponent(connId)}/bonding/failover`;\n }\n\n async request(input: RequestInfo, init: RequestInit = {}): Promise<Response> {\n const response = await apiFetch(input, init);\n if (response.status === 401) {\n const error: AuthenticatedError = new Error(MANAGEMENT_TOKEN_ERROR_MESSAGE);\n error.isUnauthorized = true;\n throw error;\n }\n return response;\n }\n\n authFailureMessage(context: string): string {\n return `${MANAGEMENT_TOKEN_ERROR_MESSAGE} Failed while ${context}. ${this.tokenHelpText}`;\n }\n\n // ── Page rendering ─────────────────────────────────────────────────────────\n\n renderPage() {\n this.renderTabs();\n this.renderConnectionContent();\n }\n\n renderTabs() {\n const tabsDiv = document.getElementById(\"connectionTabs\");\n if (!tabsDiv) {\n return;\n }\n\n if (this.connections.length <= 1) {\n tabsDiv.innerHTML = \"\";\n tabsDiv.style.display = \"none\";\n return;\n }\n\n tabsDiv.style.display = \"\";\n tabsDiv.innerHTML = `<div class=\"tabs-container\">${this.connections\n .map((c) => {\n const icon = c.type === \"server\" ? \"🖥\" : \"📱\";\n const statusDot =\n c.healthy === false\n ? '<span class=\"tab-status-dot error\"></span>'\n : c.readyToSend || c.type === \"server\"\n ? '<span class=\"tab-status-dot ok\"></span>'\n : '<span class=\"tab-status-dot warning\"></span>';\n return `<button class=\"connection-tab${c.id === this.activeConnectionId ? \" active\" : \"\"}\"\n data-connection-id=\"${this.escapeHtml(c.id)}\">\n ${statusDot}\n <span class=\"tab-icon\">${icon}</span>\n <span class=\"tab-name\">${this.escapeHtml(c.name || c.id)}</span>\n <span class=\"tab-type\">${this.escapeHtml(c.type)}</span>\n </button>`;\n })\n .join(\"\")}</div>`;\n\n // Attach tab click listeners\n tabsDiv.querySelectorAll(\".connection-tab\").forEach((btn) => {\n btn.addEventListener(\"click\", () => {\n const id = (btn as HTMLElement).dataset.connectionId;\n if (id !== this.activeConnectionId) {\n this.activeConnectionId = id!;\n this.renderPage();\n this.loadConnectionData();\n }\n });\n });\n }\n\n renderConnectionContent() {\n const contentDiv = document.getElementById(\"connectionContent\");\n if (!contentDiv) {\n return;\n }\n\n const conn = this.getActiveConnection();\n this.isServerMode = conn.type === \"server\";\n\n if (this.isServerMode) {\n this.renderServerContent(contentDiv);\n } else {\n this.renderClientContent(contentDiv);\n }\n\n this.setupEventListeners();\n this.loadConnectionData();\n }\n\n renderServerContent(container: HTMLElement) {\n const telemetryAndHealth =\n renderCard(\n \"Performance Metrics\",\n \"Real-time reception statistics (auto-refreshes every 15 seconds)\",\n \"metrics\"\n ) +\n '<div id=\"monitoringSection\" style=\"display:none;\">' +\n renderCard(\n \"Network Quality\",\n \"Link quality score and network health indicators\",\n \"networkQuality\"\n ) +\n renderCard(\"Bandwidth Monitor\", \"Network reception statistics\", \"bandwidth\") +\n renderCard(\"Path Analytics\", \"Incoming data volume by SignalK path\", \"pathAnalytics\") +\n renderCard(\n \"Monitoring & Alerts\",\n \"Packet loss, retransmission tracking, and alert thresholds\",\n \"monitoringAlerts\"\n ) +\n \"</div>\";\n\n container.innerHTML =\n renderSectionGroup(\n \"Operations & Monitoring\",\n \"Track reception quality, throughput, and runtime behavior.\",\n telemetryAndHealth,\n \"operationsGroup\"\n ) +\n renderSectionGroup(\n \"Advanced\",\n \"Full plugin configurator (JSON editor).\",\n this.renderPluginConfigurationCard(),\n \"advancedGroup\"\n );\n }\n\n renderClientContent(container: HTMLElement) {\n const configuration =\n this.renderDeltaTimerCard() + this.renderSubscriptionCard() + this.renderSentenceFilterCard();\n\n const telemetryAndHealth =\n renderCard(\n \"Performance Metrics\",\n \"Real-time transmission statistics (auto-refreshes every 15 seconds)\",\n \"metrics\"\n ) +\n '<div id=\"congestionSection\" class=\"config-section\" style=\"display:none;\">' +\n renderCard(\n \"Network Quality\",\n \"Link quality score and network health indicators\",\n \"networkQuality\"\n ) +\n renderCard(\"Bandwidth Monitor\", \"Real-time data transmission statistics\", \"bandwidth\") +\n renderCard(\"Path Analytics\", \"Data volume by subscription path\", \"pathAnalytics\") +\n renderCard(\n \"Congestion Control\",\n \"AIMD congestion control state and delta timer auto-adjustment\",\n \"congestionControl\"\n ) +\n \"</div>\" +\n '<div id=\"bondingSection\" class=\"config-section\" style=\"display:none;\">' +\n renderCard(\n \"Connection Bonding\",\n \"Multi-link bonding status and failover control\",\n \"bondingStatus\"\n ) +\n \"</div>\" +\n '<div id=\"monitoringSection\" style=\"display:none;\">' +\n renderCard(\n \"Monitoring & Alerts\",\n \"Packet loss, retransmission tracking, and alert thresholds\",\n \"monitoringAlerts\"\n ) +\n \"</div>\" +\n renderCard(\"Status\", null, \"status\", \"status-info\");\n\n container.innerHTML =\n renderSectionGroup(\n \"Configuration\",\n \"Set up transmission behavior and plugin-level parameters.\",\n configuration,\n \"configurationGroup\"\n ) +\n renderSectionGroup(\n \"Operations & Monitoring\",\n \"Track transmission quality, reliability, and runtime performance.\",\n telemetryAndHealth,\n \"operationsGroup\"\n ) +\n renderSectionGroup(\n \"Advanced\",\n \"Full plugin configurator (JSON editor).\",\n this.renderPluginConfigurationCard(),\n \"advancedGroup\"\n );\n }\n\n renderDeltaTimerCard(): string {\n return `\n <div class=\"config-section\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h2>Delta Timer Configuration</h2>\n <p>Controls how often deltas are collected and sent (in milliseconds)</p>\n </div>\n <div class=\"card-content\">\n <div class=\"form-group\">\n <label for=\"deltaTimer\">Delta Timer (ms):</label>\n <input type=\"number\" id=\"deltaTimer\" min=\"100\" max=\"10000\" step=\"100\" placeholder=\"1000\" />\n <small class=\"help-text\">\n Lower values = more frequent updates, higher bandwidth usage<br>\n Higher values = better compression ratio, lower bandwidth usage\n </small>\n </div>\n <button id=\"saveDeltaTimer\" class=\"btn btn-primary\">Save Delta Timer</button>\n </div>\n </div>\n </div>\n `;\n }\n\n renderSubscriptionCard(): string {\n return `\n <div class=\"config-section\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h2>Subscription Configuration</h2>\n <p>Define which SignalK data paths to subscribe to</p>\n </div>\n <div class=\"card-content\">\n <div class=\"form-group\">\n <label for=\"context\">Context:</label>\n <input type=\"text\" id=\"context\" placeholder=\"*\" />\n <small class=\"help-text\">\n Context for the subscription (e.g., \"vessels.self\", \"*\" for all)\n </small>\n </div>\n <div class=\"subscription-paths\">\n <h3>Subscription Paths</h3>\n <div id=\"pathsList\" class=\"paths-list\"></div>\n <button id=\"addPath\" class=\"btn btn-secondary\">Add Path</button>\n </div>\n <fieldset class=\"meta-config\">\n <legend>Metadata streaming</legend>\n <p class=\"help-text\">\n Also forward Signal K path metadata (units, descriptions, zones,\n display names, ...) to the remote receiver. Disabled by default.\n Meta is sent as a full snapshot at startup and re-broadcast on\n an interval so a restarted receiver can recover without a\n round-trip.\n </p>\n <div class=\"form-group\">\n <label>\n <input type=\"checkbox\" id=\"metaEnabled\" />\n Include metadata\n </label>\n </div>\n <div class=\"form-group\">\n <label for=\"metaIntervalSec\">Snapshot interval (seconds):</label>\n <input type=\"number\" id=\"metaIntervalSec\" min=\"30\" max=\"86400\" step=\"1\" placeholder=\"300\" />\n <small class=\"help-text\">Between 30 and 86400. Default 300 (5 minutes).</small>\n </div>\n <div class=\"form-group\">\n <label for=\"metaPathsRegex\">Include paths matching (regex, optional):</label>\n <input type=\"text\" id=\"metaPathsRegex\" placeholder=\"\" />\n <small class=\"help-text\">Leave empty to include every subscribed path.</small>\n </div>\n <div class=\"form-group\">\n <label for=\"metaMaxPerPacket\">Max paths per packet:</label>\n <input type=\"number\" id=\"metaMaxPerPacket\" min=\"10\" max=\"5000\" step=\"1\" placeholder=\"500\" />\n <small class=\"help-text\">Between 10 and 5000. Default 500.</small>\n </div>\n </fieldset>\n <div class=\"json-editor\">\n <h3>JSON Editor</h3>\n <textarea id=\"subscriptionJson\" rows=\"10\" placeholder='{\"context\": \"*\", \"subscribe\": [{\"path\": \"*\"}]}'></textarea>\n <small class=\"help-text\">Advanced: Edit the raw JSON configuration</small>\n </div>\n <button id=\"saveSubscription\" class=\"btn btn-primary\">Save Subscription</button>\n </div>\n </div>\n </div>\n `;\n }\n\n renderSentenceFilterCard(): string {\n return `\n <div class=\"config-section\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h2>Sentence Filter</h2>\n <p>Exclude NMEA sentences from transmission (reduces bandwidth)</p>\n </div>\n <div class=\"card-content\">\n <div class=\"form-group\">\n <label for=\"sentenceFilter\">Excluded Sentences:</label>\n <input type=\"text\" id=\"sentenceFilter\" placeholder=\"\" autocomplete=\"off\" />\n <small class=\"help-text\">\n Comma-separated list of NMEA sentence types to exclude.\n </small>\n </div>\n <button id=\"saveSentenceFilter\" class=\"btn btn-primary\">Save Sentence Filter</button>\n </div>\n </div>\n </div>\n `;\n }\n\n renderPluginConfigurationCard(): string {\n return `\n <div class=\"config-section\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h2>Full Plugin Configuration</h2>\n <p>All parameters from <code>/plugin-config</code> (advanced JSON editor)</p>\n </div>\n <div class=\"card-content\">\n <div id=\"pluginConfigSummary\" class=\"plugin-config-summary\">\n <p>Loading plugin configuration...</p>\n </div>\n <div class=\"json-editor plugin-config-editor\">\n <h3>Plugin Config JSON</h3>\n <textarea\n id=\"pluginConfigJson\"\n rows=\"20\"\n placeholder='{\"serverType\":\"client\",\"udpPort\":4446,\"secretKey\":\"...\"}'\n ></textarea>\n <small class=\"help-text\">\n This editor exposes all available plugin fields. Save triggers plugin restart when supported.\n </small>\n <small class=\"help-text\">${this.escapeHtml(this.tokenHelpText)}</small>\n </div>\n <div class=\"plugin-config-actions\">\n <button id=\"savePluginConfig\" class=\"btn btn-primary\">Save Full Plugin Config</button>\n <button id=\"reloadPluginConfig\" class=\"btn btn-secondary\">Reload From Server</button>\n <button id=\"loadDefaultPluginConfig\" class=\"btn btn-secondary\">Load Schema Defaults</button>\n </div>\n </div>\n </div>\n </div>\n `;\n }\n\n // ── Data loading ───────────────────────────────────────────────────────────\n\n async loadConnectionData() {\n const conn = this.getActiveConnection();\n const connId = conn.id;\n\n if (!this.isServerMode) {\n await this.loadConfigurations(connId);\n this.updateUI();\n this.updateStatus();\n } else {\n this.updatePluginConfigUI();\n }\n\n await this.loadMetrics(connId);\n }\n\n async loadPluginConfiguration(showErrors = true): Promise<boolean> {\n try {\n const [configResponse, schemaResponse] = await Promise.all([\n this.request(`${API_BASE_PATH}/plugin-config`),\n this.request(`${API_BASE_PATH}/plugin-schema`)\n ]);\n\n if (!configResponse.ok) {\n throw new Error(`Failed to load plugin configuration (${configResponse.status})`);\n }\n\n const configData = await configResponse.json();\n const configuration =\n configData &&\n configData.configuration &&\n typeof configData.configuration === \"object\" &&\n !Array.isArray(configData.configuration)\n ? configData.configuration\n : {};\n\n if (schemaResponse.ok) {\n const schemaData = await schemaResponse.json();\n if (schemaData && schemaData.schema && typeof schemaData.schema === \"object\") {\n this.pluginSchema = schemaData.schema;\n }\n if (\n schemaData &&\n (schemaData.currentMode === \"server\" || schemaData.currentMode === \"client\")\n ) {\n this.schemaCurrentMode = schemaData.currentMode;\n }\n }\n\n this.pluginConfig = this.buildCompletePluginConfig(configuration);\n return true;\n } catch (error: unknown) {\n if (showErrors) {\n const err = error as AuthenticatedError;\n this.showNotification(\n err.isUnauthorized\n ? this.authFailureMessage(\"loading plugin config\")\n : \"Error loading plugin config: \" + err.message,\n \"warning\"\n );\n }\n return false;\n }\n }\n\n async loadConfigurations(connId: string) {\n try {\n const [deltaResponse, subResponse, filterResponse] = await Promise.all([\n this.request(this.configPath(connId, \"delta_timer.json\")),\n this.request(this.configPath(connId, \"subscription.json\")),\n this.request(this.configPath(connId, \"sentence_filter.json\"))\n ]);\n\n this.deltaTimerConfig = deltaResponse.ok ? await deltaResponse.json() : null;\n this.subscriptionConfig = subResponse.ok ? await subResponse.json() : null;\n this.sentenceFilterConfig = filterResponse.ok ? await filterResponse.json() : null;\n } catch (error: unknown) {\n const err = error as AuthenticatedError;\n this.showNotification(\n err.isUnauthorized\n ? this.authFailureMessage(\"loading connection configuration\")\n : \"Error loading configurations: \" + err.message,\n \"error\"\n );\n }\n }\n\n async loadMetrics(connId?: string) {\n if (!connId) {\n connId = this.activeConnectionId!;\n }\n try {\n const response = await this.request(this.metricsPath(connId));\n if (response.ok) {\n const metrics = await response.json();\n this.protocolVersion = metrics.protocolVersion || 1;\n this.updateMetricsDisplay(metrics);\n\n if (this.protocolVersion >= 2) {\n this.loadV2Data(connId);\n }\n }\n } catch (error: unknown) {\n console.error(\"Error loading metrics:\", (error as Error).message);\n }\n }\n\n async loadV2Data(connId: string) {\n const isClient = !this.isServerMode;\n\n const fetches: Promise<Response | null>[] = [\n this.request(this.monitoringPath(connId, \"alerts\")).catch(() => null),\n this.request(this.monitoringPath(connId, \"packet-loss\")).catch(() => null),\n this.request(this.monitoringPath(connId, \"retransmissions\")).catch(() => null)\n ];\n\n if (isClient) {\n fetches.push(\n this.request(this.congestionPath(connId)).catch(() => null),\n this.request(this.bondingPath(connId)).catch(() => null)\n );\n }\n\n const results = await Promise.all(fetches);\n const [alertsRes, packetLossRes, retransmissionsRes, congestionRes, bondingRes] = results;\n\n const monitoringData: Record<string, unknown> = {};\n if (alertsRes && alertsRes.ok) {\n monitoringData.alerts = await alertsRes.json();\n }\n if (packetLossRes && packetLossRes.ok) {\n monitoringData.packetLoss = await packetLossRes.json();\n }\n if (retransmissionsRes && retransmissionsRes.ok) {\n monitoringData.retransmissions = await retransmissionsRes.json();\n }\n this.updateMonitoringDisplay(monitoringData);\n\n if (isClient && congestionRes && congestionRes.ok) {\n const congestionData = await congestionRes.json();\n this.updateCongestionDisplay(congestionData);\n }\n\n if (isClient && bondingRes && bondingRes.ok) {\n const bondingData = await bondingRes.json();\n this.updateBondingDisplay(bondingData);\n }\n }\n\n startMetricsRefresh() {\n if (this.metricsInterval) {\n clearInterval(this.metricsInterval);\n }\n\n this.metricsInterval = setInterval(() => {\n if (this._refreshInFlight) {\n return;\n }\n this._refreshInFlight = true;\n this.refreshActiveTab().finally(() => {\n this._refreshInFlight = false;\n });\n }, METRICS_REFRESH_INTERVAL);\n }\n\n async refreshActiveTab() {\n // Refresh the connections list to update tab badges\n try {\n const res = await this.request(`${API_BASE_PATH}/connections`);\n if (res.ok) {\n const updated = await res.json();\n if (updated.length > 0) {\n this.connections = updated;\n this.renderTabs();\n }\n }\n } catch (_e) {\n /* ignore */\n }\n\n await this.loadMetrics(this.activeConnectionId!);\n }\n\n // ── Event listeners ────────────────────────────────────────────────────────\n\n setupEventListeners() {\n const saveDeltaTimerBtn = document.getElementById(\"saveDeltaTimer\");\n if (saveDeltaTimerBtn) {\n saveDeltaTimerBtn.addEventListener(\"click\", () => this.saveDeltaTimer());\n }\n\n const saveSubscriptionBtn = document.getElementById(\"saveSubscription\");\n if (saveSubscriptionBtn) {\n saveSubscriptionBtn.addEventListener(\"click\", () => this.saveSubscription());\n }\n\n const saveSentenceFilterBtn = document.getElementById(\"saveSentenceFilter\");\n if (saveSentenceFilterBtn) {\n saveSentenceFilterBtn.addEventListener(\"click\", () => this.saveSentenceFilter());\n }\n\n const addPathBtn = document.getElementById(\"addPath\");\n if (addPathBtn) {\n addPathBtn.addEventListener(\"click\", () => this.addPathItem());\n }\n\n const subscriptionJsonEditor = document.getElementById(\"subscriptionJson\");\n if (subscriptionJsonEditor) {\n subscriptionJsonEditor.addEventListener(\"input\", () => {\n if (this.syncTimeout) {\n clearTimeout(this.syncTimeout);\n }\n this.syncTimeout = setTimeout(() => this.syncFromJson(), JSON_SYNC_DEBOUNCE);\n });\n }\n\n const contextInput = document.getElementById(\"context\");\n if (contextInput) {\n contextInput.addEventListener(\"input\", () => this.updateJsonFromForm());\n }\n\n // Meta controls feed the same JSON-from-form sync so the textarea always\n // reflects the current state of the structured controls. Without these\n // listeners, the textarea would lag behind the form until save and the\n // user viewing the raw JSON would see stale content.\n for (const metaId of [\"metaEnabled\", \"metaIntervalSec\", \"metaPathsRegex\", \"metaMaxPerPacket\"]) {\n const el = document.getElementById(metaId);\n if (el) {\n el.addEventListener(\"input\", () => this.updateJsonFromForm());\n if (metaId === \"metaEnabled\") {\n // checkbox emits \"change\" rather than \"input\" on some browsers\n el.addEventListener(\"change\", () => this.updateJsonFromForm());\n }\n }\n }\n\n const savePluginConfigBtn = document.getElementById(\"savePluginConfig\");\n if (savePluginConfigBtn) {\n savePluginConfigBtn.addEventListener(\"click\", () => this.savePluginConfig());\n }\n\n const reloadPluginConfigBtn = document.getElementById(\"reloadPluginConfig\");\n if (reloadPluginConfigBtn) {\n reloadPluginConfigBtn.addEventListener(\"click\", () => this.reloadPluginConfiguration());\n }\n\n const loadDefaultPluginConfigBtn = document.getElementById(\"loadDefaultPluginConfig\");\n if (loadDefaultPluginConfigBtn) {\n loadDefaultPluginConfigBtn.addEventListener(\"click\", () =>\n this.loadDefaultPluginConfiguration()\n );\n }\n }\n\n // ── UI updates ─────────────────────────────────────────────────────────────\n\n updateUI() {\n this.updatePluginConfigUI();\n\n if (this.deltaTimerConfig && (this.deltaTimerConfig as Record<string, unknown>).deltaTimer) {\n const el = document.getElementById(\"deltaTimer\") as HTMLInputElement | null;\n if (el) {\n el.value = String((this.deltaTimerConfig as Record<string, unknown>).deltaTimer);\n }\n }\n\n if (this.subscriptionConfig) {\n const cfg = this.subscriptionConfig as Record<string, unknown>;\n const ctxEl = document.getElementById(\"context\") as HTMLInputElement | null;\n if (ctxEl) {\n ctxEl.value = (cfg.context as string) || \"*\";\n }\n\n const pathsList = document.getElementById(\"pathsList\");\n if (pathsList) {\n pathsList.innerHTML = \"\";\n if (cfg.subscribe && Array.isArray(cfg.subscribe)) {\n (cfg.subscribe as Array<{ path: string }>).forEach((sub) => this.addPathItem(sub.path));\n }\n }\n\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\n if (jsonEl) {\n jsonEl.value = JSON.stringify(this.subscriptionConfig, null, 2);\n }\n\n // Populate the structured metadata controls from the loaded config.\n this.populateMetaControls(cfg.meta as Record<string, unknown> | undefined);\n }\n\n if (\n this.sentenceFilterConfig &&\n Array.isArray((this.sentenceFilterConfig as Record<string, unknown>).excludedSentences)\n ) {\n const el = document.getElementById(\"sentenceFilter\") as HTMLInputElement | null;\n if (el) {\n el.value = (\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\n ).join(\", \");\n }\n }\n }\n\n updatePluginConfigUI() {\n const editor = document.getElementById(\"pluginConfigJson\") as HTMLTextAreaElement | null;\n const summary = document.getElementById(\"pluginConfigSummary\");\n\n if (!editor) {\n return;\n }\n\n if (!this.pluginConfig) {\n editor.value = \"{}\";\n if (summary) {\n summary.innerHTML = \"<p>Plugin config unavailable.</p>\";\n }\n return;\n }\n\n editor.value = JSON.stringify(this.pluginConfig, null, 2);\n\n if (summary) {\n const hasConnections =\n Array.isArray(this.pluginConfig.connections) &&\n (this.pluginConfig.connections as unknown[]).length > 0;\n\n let summaryScope = \"Top-level\";\n let keyLabel = \"Top-Level Fields\";\n let summaryConfig: Record<string, unknown> = this.pluginConfig;\n\n if (hasConnections) {\n const totalConnections = (this.pluginConfig.connections as unknown[]).length;\n const runtimeIndex = this.connections.findIndex((c) => c.id === this.activeConnectionId);\n const summaryIndex =\n runtimeIndex >= 0 && runtimeIndex < totalConnections ? runtimeIndex : 0;\n const candidate = (this.pluginConfig.connections as Record<string, unknown>[])[\n summaryIndex\n ];\n summaryConfig =\n candidate && typeof candidate === \"object\" && !Array.isArray(candidate) ? candidate : {};\n summaryScope = `Connection ${summaryIndex + 1}/${totalConnections}`;\n keyLabel = \"Connection Fields\";\n }\n\n const mode = this.normalizeServerType(summaryConfig.serverType) || \"client\";\n const protocol =\n Number(summaryConfig.protocolVersion) >= 2 ? Number(summaryConfig.protocolVersion) : 1;\n const keyCount = Object.keys(summaryConfig).length;\n summary.innerHTML = `\n <div class=\"plugin-summary-grid\">\n ${renderStatItem(\"Scope\", summaryScope)}\n ${renderStatItem(\"Mode\", mode.toUpperCase())}\n ${renderStatItem(\"Protocol\", \"v\" + String(protocol))}\n ${renderStatItem(keyLabel, String(keyCount))}\n </div>\n `;\n }\n }\n\n // ── Form helpers (subscription paths) ──────────────────────────────────────\n\n addPathItem(path = \"\") {\n const pathsList = document.getElementById(\"pathsList\");\n if (!pathsList) {\n return;\n }\n\n const pathItem = document.createElement(\"div\");\n pathItem.className = \"path-item\";\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.value = path;\n input.placeholder = \"navigation.position\";\n input.className = \"path-input\";\n\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = \"btn btn-danger\";\n button.textContent = \"Remove\";\n\n input.addEventListener(\"input\", () => this.updateJsonFromForm());\n button.addEventListener(\"click\", () => {\n pathItem.remove();\n this.updateJsonFromForm();\n });\n\n pathItem.appendChild(input);\n pathItem.appendChild(button);\n pathsList.appendChild(pathItem);\n this.updateJsonFromForm();\n }\n\n updateJsonFromForm() {\n // Skip form → JSON syncing while we're hydrating the form from a\n // pasted/loaded JSON — otherwise addPathItem() would fire this on every\n // added path-input and reserialize the subscription.json as just\n // {context, subscribe} without the meta block, silently dropping meta\n // from pasted configs.\n if (this.isHydratingFromTextarea) {\n return;\n }\n\n const contextEl = document.getElementById(\"context\") as HTMLInputElement | null;\n const context = contextEl ? contextEl.value || \"*\" : \"*\";\n const pathInputs = document.querySelectorAll(\".path-input\") as NodeListOf<HTMLInputElement>;\n const subscribe = Array.from(pathInputs)\n .map((input) => ({ path: input.value }))\n .filter((sub) => sub.path.trim() !== \"\");\n\n // Build the meta block from the structured controls — they are the\n // authoritative source for metadata settings. The textarea becomes a\n // read-only mirror of the form state, kept in sync on every input.\n const config: Record<string, unknown> = { context, subscribe };\n const metaEnabledEl = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\n if (metaEnabledEl && metaEnabledEl.checked) {\n const intervalEl = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\n const pathsEl = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\n const maxEl = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\n const intervalSec = intervalEl && intervalEl.value ? Number(intervalEl.value) : 300;\n const maxPathsPerPacket = maxEl && maxEl.value ? Number(maxEl.value) : 500;\n const includePathsMatching = pathsEl && pathsEl.value ? pathsEl.value : null;\n config.meta = {\n enabled: true,\n intervalSec,\n includePathsMatching,\n maxPathsPerPacket\n };\n }\n\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\n if (jsonEl) {\n jsonEl.value = JSON.stringify(config, null, 2);\n }\n }\n\n /** Populate the structured meta controls from a parsed `meta` block.\n * Passing undefined/null resets the controls to their empty state so the\n * UI always matches what the textarea / loaded config contains. */\n populateMetaControls(meta: Record<string, unknown> | undefined | null) {\n const metaEnabled = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\n if (metaEnabled) {\n metaEnabled.checked = !!(meta && meta.enabled === true);\n }\n const metaIntervalSec = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\n if (metaIntervalSec) {\n metaIntervalSec.value =\n meta && typeof meta.intervalSec === \"number\" ? String(meta.intervalSec) : \"\";\n }\n const metaPathsRegex = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\n if (metaPathsRegex) {\n metaPathsRegex.value =\n meta && typeof meta.includePathsMatching === \"string\" ? meta.includePathsMatching : \"\";\n }\n const metaMaxPerPacket = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\n if (metaMaxPerPacket) {\n metaMaxPerPacket.value =\n meta && typeof meta.maxPathsPerPacket === \"number\" ? String(meta.maxPathsPerPacket) : \"\";\n }\n }\n\n syncFromJson() {\n this.isHydratingFromTextarea = true;\n try {\n const jsonEl = document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement | null;\n if (!jsonEl) {\n return;\n }\n const config = JSON.parse(jsonEl.value);\n\n const ctxEl = document.getElementById(\"context\") as HTMLInputElement | null;\n if (ctxEl) {\n ctxEl.value = config.context || \"*\";\n }\n\n const pathsList = document.getElementById(\"pathsList\");\n if (pathsList) {\n pathsList.innerHTML = \"\";\n if (config.subscribe && Array.isArray(config.subscribe)) {\n config.subscribe.forEach((sub: { path?: string }) => this.addPathItem(sub.path || \"\"));\n }\n }\n\n // Keep the meta form in sync with the raw JSON editor. Without this,\n // editing the textarea would leave the checkbox/interval/regex/max\n // controls showing the previously-loaded values.\n this.populateMetaControls(config.meta as Record<string, unknown> | undefined);\n } catch (error: unknown) {\n console.warn(\"Invalid JSON in editor:\", (error as Error).message);\n } finally {\n this.isHydratingFromTextarea = false;\n }\n }\n\n // ── Save operations ────────────────────────────────────────────────────────\n\n async saveConfig(filename: string, config: unknown, configKey: string, label: string) {\n const connId = this.activeConnectionId!;\n try {\n const response = await this.request(this.configPath(connId, filename), {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(config)\n });\n\n if (response.ok) {\n (this as Record<string, unknown>)[configKey] = config;\n this.showNotification(`${label} saved successfully!`, \"success\");\n this.updateStatus();\n } else {\n throw new Error(\"Failed to save configuration\");\n }\n } catch (error: unknown) {\n const err = error as AuthenticatedError;\n this.showNotification(\n err.isUnauthorized\n ? this.authFailureMessage(`saving ${label.toLowerCase()}`)\n : `Error saving ${label.toLowerCase()}: ` + err.message,\n \"error\"\n );\n }\n }\n\n async saveDeltaTimer() {\n const deltaTimer = parseInt((document.getElementById(\"deltaTimer\") as HTMLInputElement).value);\n\n if (isNaN(deltaTimer) || deltaTimer < DELTA_TIMER_MIN || deltaTimer > DELTA_TIMER_MAX) {\n this.showNotification(\n `Delta timer must be between ${DELTA_TIMER_MIN} and ${DELTA_TIMER_MAX} milliseconds`,\n \"error\"\n );\n return;\n }\n\n await this.saveConfig(\n \"delta_timer.json\",\n { deltaTimer },\n \"deltaTimerConfig\",\n \"Delta timer configuration\"\n );\n }\n\n async saveSubscription() {\n try {\n // Flush any pending JSON-from-textarea debounce so the structured form\n // controls (in particular the meta fieldset) reflect what's currently\n // in the textarea before we read either side. Without this, a user\n // editing the raw JSON and immediately clicking Save would lose their\n // textarea-only edits — saveSubscription writes meta from the form\n // controls, which would still be stale.\n if (this.syncTimeout) {\n clearTimeout(this.syncTimeout);\n this.syncTimeout = null;\n this.syncFromJson();\n }\n\n const jsonText = (document.getElementById(\"subscriptionJson\") as HTMLTextAreaElement).value;\n const config = JSON.parse(jsonText);\n\n if (!config.context) {\n throw new Error(\"Context is required\");\n }\n if (!config.subscribe || !Array.isArray(config.subscribe)) {\n throw new Error(\"Subscribe array is required\");\n }\n\n // The structured meta controls are authoritative for the meta block on\n // save. This lets users toggle the checkbox off (removing `meta` from\n // the saved config) or edit the interval/regex/max fields even after a\n // meta block has been saved before — the previous \"textarea wins\"\n // policy made those UI controls one-time-only after first enable.\n const metaEnabledEl = document.getElementById(\"metaEnabled\") as HTMLInputElement | null;\n if (metaEnabledEl && metaEnabledEl.checked) {\n const intervalEl = document.getElementById(\"metaIntervalSec\") as HTMLInputElement | null;\n const pathsEl = document.getElementById(\"metaPathsRegex\") as HTMLInputElement | null;\n const maxEl = document.getElementById(\"metaMaxPerPacket\") as HTMLInputElement | null;\n const intervalSec = intervalEl && intervalEl.value ? Number(intervalEl.value) : 300;\n const maxPathsPerPacket = maxEl && maxEl.value ? Number(maxEl.value) : 500;\n const includePathsMatching = pathsEl && pathsEl.value ? pathsEl.value : null;\n config.meta = {\n enabled: true,\n intervalSec,\n includePathsMatching,\n maxPathsPerPacket\n };\n } else if (config.meta !== undefined) {\n // Checkbox unchecked — drop any previously-saved meta block so the\n // runtime stops streaming metadata.\n delete config.meta;\n }\n\n await this.saveConfig(\n \"subscription.json\",\n config,\n \"subscriptionConfig\",\n \"Subscription configuration\"\n );\n } catch (error: unknown) {\n this.showNotification(\"Error saving subscription: \" + (error as Error).message, \"error\");\n }\n }\n\n async saveSentenceFilter() {\n const filterInput = (document.getElementById(\"sentenceFilter\") as HTMLInputElement).value;\n const excludedSentences = filterInput\n .split(\",\")\n .map((s) => s.trim().toUpperCase())\n .filter((s) => s.length > 0);\n\n await this.saveConfig(\n \"sentence_filter.json\",\n { excludedSentences },\n \"sentenceFilterConfig\",\n \"Sentence filter\"\n );\n }\n\n async savePluginConfig() {\n const editor = document.getElementById(\"pluginConfigJson\") as HTMLTextAreaElement | null;\n if (!editor) {\n return;\n }\n\n try {\n const parsedConfig = JSON.parse(editor.value);\n if (!parsedConfig || typeof parsedConfig !== \"object\" || Array.isArray(parsedConfig)) {\n throw new Error(\"Plugin configuration must be a JSON object\");\n }\n\n const requestConfig = this.deepClone(parsedConfig) as Record<string, unknown>;\n const normalizedServerType = this.normalizeServerType(requestConfig.serverType);\n if (normalizedServerType) {\n requestConfig.serverType = normalizedServerType;\n }\n\n const response = await this.request(`${API_BASE_PATH}/plugin-config`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(requestConfig)\n });\n\n const result = await response.json().catch(() => ({}) as Record<string, unknown>);\n if (!response.ok || !result.success) {\n throw new Error(result.error || `Failed to save plugin configuration (${response.status})`);\n }\n\n this.pluginConfig = this.buildCompletePluginConfig(requestConfig);\n this.updatePluginConfigUI();\n this.showNotification(\n result.message || \"Plugin configuration saved. Refresh to apply changes.\",\n \"success\"\n );\n } catch (error: unknown) {\n const err = error as AuthenticatedError;\n this.showNotification(\n err.isUnauthorized\n ? this.authFailureMessage(\"saving full plugin config\")\n : \"Error saving full plugin config: \" + err.message,\n \"error\"\n );\n }\n }\n\n async reloadPluginConfiguration() {\n const loaded = await this.loadPluginConfiguration(true);\n if (loaded) {\n this.updatePluginConfigUI();\n this.showNotification(\"Plugin configuration reloaded.\", \"success\");\n }\n }\n\n loadDefaultPluginConfiguration() {\n this.pluginConfig = this.buildCompletePluginConfig({});\n this.updatePluginConfigUI();\n this.showNotification(\"Loaded schema defaults into editor. Save to apply.\", \"warning\");\n }\n\n // ── Metrics display ────────────────────────────────────────────────────────\n\n updateMetricsDisplay(metrics: Record<string, any>) {\n this.updateNetworkQualityDisplay(metrics);\n this.updateBandwidthDisplay(metrics);\n this.updatePathAnalyticsDisplay(metrics);\n\n const metricsDiv = document.getElementById(\"metrics\");\n if (!metricsDiv) {\n return;\n }\n\n const isClient = metrics.mode === \"client\";\n const { stats, status, uptime } = metrics;\n\n const cryptoErrors = (stats.errorCounts && stats.errorCounts.crypto) || 0;\n const malformedPackets = stats.malformedPackets || 0;\n const dataPacketsReceived = stats.dataPacketsReceived || 0;\n const rateLimitedPackets = stats.rateLimitedPackets || 0;\n const droppedDeltaBatches = stats.droppedDeltaBatches || 0;\n const droppedDeltaCount = stats.droppedDeltaCount || 0;\n\n const hasErrors =\n stats.udpSendErrors > 0 ||\n stats.compressionErrors > 0 ||\n stats.encryptionErrors > 0 ||\n stats.subscriptionErrors > 0 ||\n cryptoErrors > 0 ||\n malformedPackets > 0 ||\n rateLimitedPackets > 0 ||\n droppedDeltaCount > 0;\n\n const protocolVersion = metrics.protocolVersion || 1;\n const protocolLabel = protocolVersion >= 2 ? `v${protocolVersion}` : \"v1\";\n\n const metricsGridItems = [\n renderMetricItem(\"Uptime\", uptime.formatted),\n renderMetricItem(\"Mode\", isClient ? \"Client\" : \"Server\"),\n renderMetricItemHtml(\n \"Protocol\",\n `<span class=\"protocol-badge protocol-${escapeHtml(protocolLabel)}\">${escapeHtml(protocolLabel.toUpperCase())}</span>`\n ),\n renderMetricItem(\n \"Status\",\n status.readyToSend ? \"Ready\" : \"Not Ready\",\n status.readyToSend ? \"success\" : \"error\"\n ),\n isClient ? renderMetricItem(\"Buffered Deltas\", status.deltasBuffered) : \"\"\n ].join(\"\");\n\n const subErrors = stats.subscriptionErrors;\n const subscriptionErrorStat = isClient\n ? renderStatItem(\"Subscription Errors\", subErrors, subErrors > 0)\n : \"\";\n\n const statsItems = [\n isClient\n ? renderStatItem(\"Deltas Sent\", stats.deltasSent.toLocaleString())\n : renderStatItem(\"Deltas Received\", stats.deltasReceived.toLocaleString()),\n !isClient\n ? renderStatItem(\"Data Packets Received\", dataPacketsReceived.toLocaleString())\n : \"\",\n isClient\n ? renderStatItem(\"UDP Send Errors\", stats.udpSendErrors, stats.udpSendErrors > 0)\n : \"\",\n isClient ? renderStatItem(\"UDP Retries\", stats.udpRetries) : \"\",\n !isClient\n ? renderStatItem(\n \"Rate-Limited Packets\",\n rateLimitedPackets.toLocaleString(),\n rateLimitedPackets > 0\n )\n : \"\",\n isClient\n ? renderStatItem(\n \"Dropped Delta Batches\",\n droppedDeltaBatches.toLocaleString(),\n droppedDeltaBatches > 0\n )\n : \"\",\n isClient\n ? renderStatItem(\n \"Dropped Deltas\",\n droppedDeltaCount.toLocaleString(),\n droppedDeltaCount > 0\n )\n : \"\",\n renderStatItem(\"Compression Errors\", stats.compressionErrors, stats.compressionErrors > 0),\n renderStatItem(\"Encryption Errors\", stats.encryptionErrors, stats.encryptionErrors > 0),\n subscriptionErrorStat,\n !isClient && stats.duplicatePackets > 0\n ? renderStatItem(\"Duplicate Packets\", stats.duplicatePackets.toLocaleString())\n : \"\",\n protocolVersion >= 3\n ? renderStatItem(\"Auth Failures (V3)\", cryptoErrors, cryptoErrors > 0)\n : \"\",\n renderStatItem(\"Malformed Packets\", malformedPackets, malformedPackets > 0)\n ].join(\"\");\n\n let metricsHtml = `\n <h4>Performance Metrics</h4>\n <div class=\"metrics-grid\">${metricsGridItems}</div>\n <div class=\"metrics-stats\">\n <h5>Transmission Statistics</h5>\n <div class=\"stats-grid\">${statsItems}</div>\n </div>\n `;\n\n if (isClient && metrics.smartBatching) {\n const sb = metrics.smartBatching;\n const totalSends = sb.earlySends + sb.timerSends;\n const earlyPercent = totalSends > 0 ? Math.round((sb.earlySends / totalSends) * 100) : 0;\n\n metricsHtml += `\n <div class=\"metrics-stats\">\n <h5>Smart Batching</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Avg Bytes/Delta\", sb.avgBytesPerDelta + \" bytes\")}\n ${renderStatItem(\"Max Deltas/Batch\", sb.maxDeltasPerBatch)}\n ${renderStatItem(\"Early Sends\", sb.earlySends.toLocaleString() + \" (\" + earlyPercent + \"%)\")}\n ${renderStatItem(\"Timer Sends\", sb.timerSends.toLocaleString())}\n ${renderStatItem(\"Oversized Packets\", sb.oversizedPackets, sb.oversizedPackets > 0)}\n </div>\n </div>\n `;\n }\n\n if (Array.isArray(metrics.recentErrors) && metrics.recentErrors.length > 0) {\n const errorItems = [...metrics.recentErrors]\n .reverse()\n .map((err) => {\n const timeAgo = Date.now() - err.timestamp;\n const timeStr =\n timeAgo < 60000\n ? `${Math.floor(timeAgo / 1000)}s ago`\n : `${Math.floor(timeAgo / 60000)}m ago`;\n return `\n <div class=\"recent-error-item\">\n <span class=\"error-category-badge\">${this.escapeHtml(err.category)}</span>\n <span class=\"recent-error-msg\">${this.escapeHtml(err.message)}</span>\n <span class=\"recent-error-time\">${this.escapeHtml(timeStr)}</span>\n </div>\n `;\n })\n .join(\"\");\n metricsHtml += `\n <div class=\"metrics-error\">\n <h5>Recent Errors (${metrics.recentErrors.length})</h5>\n <div class=\"recent-errors-list\">${errorItems}</div>\n </div>\n `;\n } else if (metrics.lastError) {\n const timeAgo = metrics.lastError.timeAgo;\n const timeAgoStr =\n timeAgo < 60000\n ? `${Math.floor(timeAgo / 1000)}s ago`\n : `${Math.floor(timeAgo / 60000)}m ago`;\n\n metricsHtml += `\n <div class=\"metrics-error\">\n <h5>Last Error</h5>\n <div class=\"error-message\">${this.escapeHtml(metrics.lastError.message)}</div>\n <div class=\"error-time\">Occurred ${timeAgoStr}</div>\n </div>\n `;\n } else if (!hasErrors) {\n metricsHtml += `\n <div class=\"metrics-success\">\n <div class=\"success-message\">No errors detected</div>\n </div>\n `;\n }\n\n metricsDiv.innerHTML = metricsHtml;\n }\n\n updateNetworkQualityDisplay(metrics: Record<string, any>) {\n const nqDiv = document.getElementById(\"networkQuality\");\n if (!nqDiv || !metrics.networkQuality) {\n return;\n }\n\n const nq = metrics.networkQuality;\n const isClient = metrics.mode === \"client\";\n\n let qualityLabel = \"N/A\";\n let qualityColor = \"#9E9E9E\";\n if (nq.linkQuality !== undefined) {\n if (nq.linkQuality >= 90) {\n qualityLabel = \"Excellent\";\n qualityColor = \"#4CAF50\";\n } else if (nq.linkQuality >= 70) {\n qualityLabel = \"Good\";\n qualityColor = \"#FFC107\";\n } else if (nq.linkQuality >= 50) {\n qualityLabel = \"Fair\";\n qualityColor = \"#FF9800\";\n } else {\n qualityLabel = \"Poor\";\n qualityColor = \"#F44336\";\n }\n }\n\n const qualityPct = nq.linkQuality !== undefined ? nq.linkQuality : 0;\n const gaugeAngle = (qualityPct / 100) * 180;\n const radStart = Math.PI;\n const radEnd = radStart + (gaugeAngle * Math.PI) / 180;\n const cx = 50,\n cy = 50,\n r = 40;\n const x1 = cx + r * Math.cos(radStart);\n const y1 = cy + r * Math.sin(radStart);\n const x2 = cx + r * Math.cos(radEnd);\n const y2 = cy + r * Math.sin(radEnd);\n const largeArc = gaugeAngle > 180 ? 1 : 0;\n\n const gaugeArcPath =\n qualityPct > 0\n ? `<path d=\"M ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2}\"\n fill=\"none\" stroke=\"${qualityColor}\" stroke-width=\"8\" stroke-linecap=\"round\"/>`\n : \"\";\n\n const gaugeSvg = `\n <svg viewBox=\"0 0 100 55\" class=\"quality-gauge\" preserveAspectRatio=\"xMidYMid meet\">\n <path d=\"M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}\"\n fill=\"none\" stroke=\"#E0E0E0\" stroke-width=\"8\" stroke-linecap=\"round\"/>\n ${gaugeArcPath}\n <text x=\"${cx}\" y=\"${cy - 5}\" text-anchor=\"middle\" font-size=\"16\" font-weight=\"bold\" fill=\"${qualityColor}\">\n ${qualityPct}\n </text>\n <text x=\"${cx}\" y=\"${cy + 8}\" text-anchor=\"middle\" font-size=\"7\" fill=\"#666\">\n ${qualityLabel}\n </text>\n </svg>\n `;\n\n const rttDisplay = nq.rtt !== undefined ? nq.rtt + \" ms\" : \"N/A\";\n const jitterDisplay = nq.jitter !== undefined ? nq.jitter + \" ms\" : \"N/A\";\n const packetLoss = Number.isFinite(nq.packetLoss) ? nq.packetLoss : 0;\n const retransmitRate = Number.isFinite(nq.retransmitRate) ? nq.retransmitRate : 0;\n const packetLossDisplay = this.formatRatioPercent(packetLoss);\n const retransmitRateDisplay = this.formatRatioPercent(retransmitRate);\n const dataSourceDisplay = nq.dataSource || \"local\";\n\n let nqHtml = `\n <div class=\"network-quality-dashboard\">\n <div class=\"nq-hero\">\n <div class=\"nq-gauge-container\">\n ${gaugeSvg}\n <div class=\"nq-gauge-label\">Link Quality</div>\n </div>\n <div class=\"nq-key-metrics\">\n ${renderMetricItem(\"RTT\", rttDisplay, nq.rtt > 500 ? \"error\" : nq.rtt > 200 ? \"warning\" : \"\")}\n ${renderMetricItem(\"Jitter\", jitterDisplay, nq.jitter > 100 ? \"error\" : nq.jitter > 50 ? \"warning\" : \"\")}\n ${renderMetricItem(\n \"Packet Loss\",\n packetLossDisplay,\n packetLoss > 0.1 ? \"error\" : packetLoss > 0.03 ? \"warning\" : \"\"\n )}\n </div>\n </div>\n\n <div class=\"nq-details\">\n <h5>Reliability Statistics</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Data Source\", dataSourceDisplay)}\n ${nq.activeLink ? renderStatItem(\"Active Link\", nq.activeLink) : \"\"}\n ${renderStatItem(\"Retransmit Rate\", retransmitRateDisplay, retransmitRate > 0.1)}\n ${\n nq.lastRemoteUpdate\n ? renderStatItem(\"Last Remote Update\", this.formatTimestampAge(nq.lastRemoteUpdate))\n : \"\"\n }\n `;\n\n if (isClient) {\n nqHtml += `\n ${renderStatItem(\"Retransmissions\", (nq.retransmissions || 0).toLocaleString(), nq.retransmissions > 0)}\n ${renderStatItem(\"Queue Depth\", (nq.queueDepth || 0).toLocaleString(), nq.queueDepth > 100)}\n `;\n } else {\n nqHtml += `\n ${renderStatItem(\"ACKs Sent\", (nq.acksSent || 0).toLocaleString())}\n ${renderStatItem(\"NAKs Sent\", (nq.naksSent || 0).toLocaleString(), nq.naksSent > 0)}\n `;\n }\n\n nqHtml += `\n </div>\n </div>\n </div>\n `;\n\n nqDiv.innerHTML = nqHtml;\n }\n\n updateBandwidthDisplay(metrics: Record<string, any>) {\n const bandwidthDiv = document.getElementById(\"bandwidth\");\n if (!bandwidthDiv || !metrics.bandwidth) {\n return;\n }\n\n const bw = metrics.bandwidth;\n const isClient = metrics.mode === \"client\";\n\n const savedBytes = isClient ? bw.bytesOutRaw - bw.bytesOut : bw.bytesInRaw - bw.bytesIn;\n const savedFormatted = this.formatBytes(savedBytes > 0 ? savedBytes : 0);\n const metaBytesOut = bw.metaBytesOut || 0;\n const metaBytesIn = bw.metaBytesIn || 0;\n const metaPacketsOut = bw.metaPacketsOut || 0;\n const metaPacketsIn = bw.metaPacketsIn || 0;\n const metaSnapshotsSent = bw.metaSnapshotsSent || 0;\n const metaDiffsSent = bw.metaDiffsSent || 0;\n const metaRateLimitedPackets = bw.metaRateLimitedPackets || 0;\n\n let bandwidthStats: string[];\n let metadataStats: string[];\n if (isClient) {\n bandwidthStats = [\n renderBwStat(\"Total Sent (Compressed)\", bw.bytesOutFormatted),\n renderBwStat(\"Total Raw (Before Compression)\", bw.bytesOutRawFormatted),\n renderBwStat(\"Bandwidth Saved\", savedFormatted, true, true),\n renderBwStat(\"Packets Sent\", bw.packetsOut.toLocaleString())\n ];\n metadataStats = [\n renderBwStat(\"Metadata Sent\", bw.metaBytesOutFormatted || this.formatBytes(metaBytesOut)),\n renderBwStat(\"Metadata Packets Sent\", metaPacketsOut.toLocaleString()),\n renderBwStat(\"Metadata Snapshots Sent\", metaSnapshotsSent.toLocaleString()),\n renderBwStat(\"Metadata Diffs Sent\", metaDiffsSent.toLocaleString())\n ];\n } else {\n bandwidthStats = [\n renderBwStat(\"Total Received (Compressed)\", bw.bytesInFormatted),\n renderBwStat(\n \"Total Raw (After Decompression)\",\n bw.bytesInRawFormatted || this.formatBytes(bw.bytesInRaw || 0)\n ),\n renderBwStat(\"Bandwidth Saved\", savedFormatted, true, true),\n renderBwStat(\"Packets Received\", bw.packetsIn.toLocaleString())\n ];\n metadataStats = [\n renderBwStat(\"Metadata Received\", bw.metaBytesInFormatted || this.formatBytes(metaBytesIn)),\n renderBwStat(\"Metadata Packets Received\", metaPacketsIn.toLocaleString()),\n renderBwStat(\"Metadata Rate-Limited\", metaRateLimitedPackets.toLocaleString())\n ];\n }\n\n const bandwidthHtml = `\n <div class=\"bandwidth-dashboard\">\n <div class=\"bandwidth-hero\">\n <div class=\"hero-stat ${isClient ? \"primary\" : \"secondary\"}\">\n <div class=\"hero-value\">${isClient ? bw.rateOutFormatted : bw.rateInFormatted}</div>\n <div class=\"hero-label\">${isClient ? \"Upload Rate\" : \"Download Rate\"}</div>\n </div>\n <div class=\"hero-stat success\">\n <div class=\"hero-value\">${bw.compressionRatio}%</div>\n <div class=\"hero-label\">Compression Ratio</div>\n </div>\n <div class=\"hero-stat\">\n <div class=\"hero-value\">${bw.avgPacketSizeFormatted}</div>\n <div class=\"hero-label\">Avg Packet Size</div>\n </div>\n </div>\n\n <div class=\"bandwidth-details\">\n <h5>Bandwidth Details</h5>\n <div class=\"bandwidth-grid\">${bandwidthStats.join(\"\")}</div>\n </div>\n\n <div class=\"bandwidth-details metadata-details\">\n <h5>Metadata Traffic</h5>\n <div class=\"bandwidth-grid\">${metadataStats.join(\"\")}</div>\n </div>\n\n ${this.renderBandwidthChart(bw.history, isClient)}\n </div>\n `;\n\n bandwidthDiv.innerHTML = bandwidthHtml;\n }\n\n renderBandwidthChart(\n history: Array<{ rateOut: number; rateIn: number }> | undefined,\n isClient: boolean\n ): string {\n if (!history || history.length < 2) {\n return `\n <div class=\"bandwidth-chart-placeholder\">\n <p>Collecting data for chart... (${history ? history.length : 0}/2 points)</p>\n </div>\n `;\n }\n\n const width = 100;\n const height = 40;\n const maxRate = Math.max(...history.map((h) => (isClient ? h.rateOut : h.rateIn)), 1);\n const points = history\n .map((h, i) => {\n const x = (i / (history.length - 1)) * width;\n const y = height - ((isClient ? h.rateOut : h.rateIn) / maxRate) * height;\n return `${x},${y}`;\n })\n .join(\" \");\n\n const maxRateFormatted = this.formatBytes(maxRate);\n const intervalSeconds = METRICS_REFRESH_INTERVAL / 1000;\n\n return `\n <div class=\"bandwidth-chart\">\n <h5>Rate History (Last ${history.length * intervalSeconds}s)</h5>\n <div class=\"chart-container\">\n <svg viewBox=\"0 0 ${width} ${height}\" class=\"sparkline\" preserveAspectRatio=\"none\">\n <polyline\n fill=\"none\"\n stroke=\"var(--primary-color)\"\n stroke-width=\"1.5\"\n points=\"${points}\"\n />\n </svg>\n <div class=\"chart-labels\">\n <span class=\"chart-max\">${maxRateFormatted}/s</span>\n <span class=\"chart-min\">0</span>\n </div>\n </div>\n </div>\n `;\n }\n\n updatePathAnalyticsDisplay(metrics: Record<string, any>) {\n const pathDiv = document.getElementById(\"pathAnalytics\");\n if (!pathDiv || !metrics.pathStats) {\n return;\n }\n\n const paths: Array<{\n path: string;\n updatesPerMinute: number;\n bytesFormatted: string;\n percentage: number;\n }> = metrics.pathStats;\n\n if (paths.length === 0) {\n pathDiv.innerHTML = `\n <div class=\"path-analytics-empty\">\n <p>No path data collected yet. Data will appear once deltas are transmitted.</p>\n </div>\n `;\n return;\n }\n\n const categoryCount = new Set(paths.map((p) => p.path.split(\".\")[0])).size;\n\n let pathHtml = `\n <div class=\"path-analytics-dashboard\">\n <div class=\"path-summary\">\n <div class=\"summary-stat\">\n <span class=\"summary-value\">${paths.length}</span>\n <span class=\"summary-label\">Active Paths</span>\n </div>\n <div class=\"summary-stat\">\n <span class=\"summary-value\">${categoryCount}</span>\n <span class=\"summary-label\">Categories</span>\n </div>\n </div>\n\n <div class=\"path-table-container\">\n <table class=\"path-table\">\n <thead>\n <tr>\n <th>Path</th>\n <th>Updates/min</th>\n <th>Data Volume</th>\n <th>% of Total</th>\n </tr>\n </thead>\n <tbody>\n `;\n\n paths.slice(0, 15).forEach((p) => {\n const barWidth = Math.max(p.percentage, 2);\n pathHtml += `\n <tr>\n <td class=\"path-name\" title=\"${this.escapeHtml(p.path)}\">${this.escapeHtml(p.path)}</td>\n <td class=\"path-rate\">${p.updatesPerMinute}</td>\n <td class=\"path-bytes\">${p.bytesFormatted}</td>\n <td class=\"path-percentage\">\n <div class=\"percentage-bar-container\">\n <div class=\"percentage-bar\" style=\"width: ${barWidth}%\"></div>\n <span class=\"percentage-text\">${p.percentage}%</span>\n </div>\n </td>\n </tr>\n `;\n });\n\n pathHtml += `\n </tbody>\n </table>\n </div>\n `;\n\n if (paths.length > 15) {\n pathHtml += `\n <div class=\"path-more\">\n <p>Showing top 15 of ${paths.length} paths</p>\n </div>\n `;\n }\n\n pathHtml += \"</div>\";\n pathDiv.innerHTML = pathHtml;\n }\n\n updateCongestionDisplay(data: Record<string, any>) {\n const section = document.getElementById(\"congestionSection\");\n const div = document.getElementById(\"congestionControl\");\n if (!section || !div) {\n return;\n }\n\n section.style.display = \"\";\n\n const enabled = !!data.enabled;\n const manualMode = !!data.manualMode;\n const stateLabel = enabled ? (manualMode ? \"manual\" : \"active\") : \"disabled\";\n const stateClass = enabled ? (manualMode ? \"warning\" : \"success\") : \"error\";\n const modeLabel = manualMode ? \"Manual Override\" : \"Automatic\";\n const currentDeltaTimer = data.currentDeltaTimer || 0;\n const nominalDeltaTimer = data.nominalDeltaTimer || 0;\n\n const html = `\n <div class=\"v2-dashboard\">\n <div class=\"metrics-grid\">\n ${renderMetricItemHtml(\"State\", `<span class=\"congestion-state ${stateClass}\">${stateLabel}</span>`)}\n ${renderMetricItem(\"Mode\", modeLabel)}\n ${renderMetricItem(\"Current Timer\", currentDeltaTimer + \" ms\")}\n ${renderMetricItem(\"Nominal Timer\", nominalDeltaTimer + \" ms\")}\n </div>\n <div class=\"metrics-stats\">\n <h5>Congestion Details</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Min Delta Timer\", (data.minDeltaTimer || 0) + \" ms\")}\n ${renderStatItem(\"Max Delta Timer\", (data.maxDeltaTimer || 0) + \" ms\")}\n ${renderStatItem(\"Target RTT\", (data.targetRTT || 0) + \" ms\")}\n ${renderStatItem(\"Avg RTT\", (data.avgRTT !== undefined ? Math.round(data.avgRTT) : 0) + \" ms\")}\n ${renderStatItem(\"Avg Packet Loss\", (data.avgLoss !== undefined ? (data.avgLoss * 100).toFixed(1) : 0) + \"%\", data.avgLoss > 0.05)}\n </div>\n </div>\n </div>\n `;\n\n div.innerHTML = html;\n }\n\n updateBondingDisplay(data: Record<string, any>) {\n const section = document.getElementById(\"bondingSection\");\n const div = document.getElementById(\"bondingStatus\");\n if (!section || !div) {\n return;\n }\n\n if (!data.enabled) {\n section.style.display = \"none\";\n return;\n }\n\n section.style.display = \"\";\n\n const modeLabel = (data.mode || \"main-backup\").replace(/-/g, \" \");\n const activeLink = data.activeLink || \"primary\";\n\n let linksHtml = \"\";\n if (data.links) {\n const linkEntries = Object.entries(data.links) as Array<[string, Record<string, any>]>;\n linksHtml = linkEntries\n .map(([name, link]) => {\n const isActive = name === activeLink;\n const status = (link.status || \"unknown\").toLowerCase();\n const isUp = status !== \"down\";\n const aliveClass = isUp ? \"success\" : \"error\";\n return `\n <div class=\"bonding-link ${isActive ? \"active\" : \"\"}\">\n <div class=\"link-header\">\n <span class=\"link-name\">${this.escapeHtml(name)}</span>\n ${isActive ? '<span class=\"link-badge active-badge\">ACTIVE</span>' : \"\"}\n <span class=\"link-badge ${aliveClass}\">${this.escapeHtml(status.toUpperCase())}</span>\n </div>\n <div class=\"link-stats\">\n ${renderStatItem(\"RTT\", (link.rtt || 0) + \" ms\")}\n ${renderStatItem(\"Packet Loss\", ((link.loss || 0) * 100).toFixed(1) + \"%\")}\n </div>\n </div>\n `;\n })\n .join(\"\");\n }\n\n const html = `\n <div class=\"v2-dashboard\">\n <div class=\"metrics-grid\">\n ${renderMetricItem(\"Mode\", modeLabel)}\n ${renderMetricItem(\"Active Link\", activeLink)}\n </div>\n <div class=\"bonding-links\">${linksHtml}</div>\n <div style=\"margin-top: 1rem;\">\n <button id=\"failoverBtn\" class=\"btn btn-secondary\">Force Failover</button>\n </div>\n </div>\n `;\n\n div.innerHTML = html;\n\n const failoverBtn = document.getElementById(\"failoverBtn\");\n if (failoverBtn) {\n failoverBtn.addEventListener(\"click\", () => this.triggerFailover());\n }\n }\n\n async triggerFailover() {\n const connId = this.activeConnectionId!;\n const btn = document.getElementById(\"failoverBtn\") as HTMLButtonElement | null;\n if (btn) {\n btn.disabled = true;\n }\n try {\n const response = await this.request(this.bondingFailoverPath(connId), { method: \"POST\" });\n if (response.ok) {\n const result = await response.json();\n this.showNotification(`Failover complete. Active link: ${result.activeLink}`, \"success\");\n this.loadMetrics(connId);\n } else {\n const err = await response.json();\n this.showNotification(\n response.status === 401\n ? this.authFailureMessage(\"triggering failover\")\n : \"Failover failed: \" + (err.error || \"Unknown error\"),\n \"error\"\n );\n }\n } catch (error: unknown) {\n const err = error as AuthenticatedError;\n this.showNotification(\n err.isUnauthorized\n ? this.authFailureMessage(\"triggering failover\")\n : \"Failover failed: \" + err.message,\n \"error\"\n );\n } finally {\n if (btn) {\n btn.disabled = false;\n }\n }\n }\n\n updateMonitoringDisplay(data: Record<string, any>) {\n const section = document.getElementById(\"monitoringSection\");\n const div = document.getElementById(\"monitoringAlerts\");\n if (!section || !div) {\n return;\n }\n\n const hasData = data.alerts || data.packetLoss || data.retransmissions;\n if (!hasData) {\n section.style.display = \"none\";\n return;\n }\n\n section.style.display = \"\";\n\n let html = '<div class=\"v2-dashboard\">';\n\n if (data.alerts) {\n const activeAlerts: Record<string, any> = data.alerts.activeAlerts || {};\n const alertEntries = Object.entries(activeAlerts).map(([metric, alert]: [string, any]) => {\n let level = \"warning\";\n if (typeof alert === \"string\") {\n level = alert.toLowerCase();\n } else if (alert && typeof alert === \"object\" && alert.level) {\n level = String(alert.level).toLowerCase();\n }\n\n if (level === \"warn\") {\n level = \"warning\";\n }\n if (level === \"alert\") {\n level = \"critical\";\n }\n if (level !== \"warning\" && level !== \"critical\") {\n level = \"warning\";\n }\n\n return {\n metric,\n level,\n value: alert && typeof alert === \"object\" ? alert.value : undefined\n };\n });\n const alertCount = alertEntries.length;\n\n const alertItems = alertEntries\n .map((entry) => {\n const alertValue =\n entry.value !== undefined ? ` (${this.escapeHtml(String(entry.value))})` : \"\";\n return renderStatItemHtml(\n entry.metric,\n `<span class=\"alert-level alert-${entry.level}\">${this.escapeHtml(entry.level.toUpperCase())}${alertValue}</span>`,\n entry.level === \"critical\"\n );\n })\n .join(\"\");\n\n const alertsContent =\n alertCount === 0\n ? '<div class=\"metrics-success\"><div class=\"success-message\">No active alerts</div></div>'\n : `<div class=\"stats-grid\">${alertItems}</div>`;\n\n html += `\n <div class=\"monitoring-subsection\">\n <h5>Active Alerts</h5>\n ${alertsContent}\n </div>\n `;\n }\n\n if (data.packetLoss && data.packetLoss.summary) {\n const summary = data.packetLoss.summary;\n html += `\n <div class=\"monitoring-subsection\">\n <h5>Packet Loss</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Overall Loss Rate\", (summary.overallLossRate * 100).toFixed(2) + \"%\", summary.overallLossRate > 0.05)}\n ${renderStatItem(\"Max Loss Rate\", (summary.maxLossRate * 100).toFixed(2) + \"%\", summary.maxLossRate > 0.1)}\n ${renderStatItem(\"Trend\", summary.trend || \"stable\")}\n </div>\n </div>\n `;\n }\n\n if (data.retransmissions && data.retransmissions.summary) {\n const summary = data.retransmissions.summary;\n html += `\n <div class=\"monitoring-subsection\">\n <h5>Retransmission Rates</h5>\n <div class=\"stats-grid\">\n ${renderStatItem(\"Current Rate\", (summary.currentRate * 100).toFixed(2) + \"%\", summary.currentRate > 0.05)}\n ${renderStatItem(\"Average Rate\", (summary.avgRate * 100).toFixed(2) + \"%\")}\n ${renderStatItem(\"Max Rate\", (summary.maxRate * 100).toFixed(2) + \"%\", summary.maxRate > 0.1)}\n </div>\n </div>\n `;\n }\n\n html += \"</div>\";\n div.innerHTML = html;\n }\n\n // ── Status display ─────────────────────────────────────────────────────────\n\n updateStatus() {\n const statusDiv = document.getElementById(\"status\");\n if (!statusDiv) {\n return;\n }\n\n let statusHtml = \"<h4>Configuration Status</h4>\";\n\n if (this.deltaTimerConfig) {\n statusHtml += `\n <div class=\"status-item\">\n <strong>Delta Timer:</strong> ${this.escapeHtml(String((this.deltaTimerConfig as Record<string, unknown>).deltaTimer))}ms\n <span class=\"status-indicator success\">Configured</span>\n </div>\n `;\n } else {\n statusHtml += `\n <div class=\"status-item\">\n <strong>Delta Timer:</strong>\n <span class=\"status-indicator warning\">Not configured</span>\n </div>\n `;\n }\n\n if (this.subscriptionConfig && (this.subscriptionConfig as Record<string, unknown>).subscribe) {\n const cfg = this.subscriptionConfig as Record<string, unknown>;\n const pathCount = (cfg.subscribe as unknown[]).length;\n const escapedContext = this.escapeHtml((cfg.context as string) || \"\");\n const escapedPaths = (cfg.subscribe as Array<{ path: string }>)\n .map((s) => this.escapeHtml(s.path))\n .join(\", \");\n statusHtml += `\n <div class=\"status-item\">\n <strong>Subscriptions:</strong> ${pathCount} path(s) configured\n <span class=\"status-indicator success\">Configured</span>\n </div>\n <div class=\"status-details\">\n <strong>Context:</strong> ${escapedContext}<br>\n <strong>Paths:</strong> ${escapedPaths}\n </div>\n `;\n } else {\n statusHtml += `\n <div class=\"status-item\">\n <strong>Subscriptions:</strong>\n <span class=\"status-indicator warning\">Not configured</span>\n </div>\n `;\n }\n\n if (\n this.sentenceFilterConfig &&\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences &&\n ((this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[])\n .length > 0\n ) {\n const filterCount = (\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\n ).length;\n const escapedFilters = (\n (this.sentenceFilterConfig as Record<string, unknown>).excludedSentences as string[]\n )\n .map((s) => this.escapeHtml(s))\n .join(\", \");\n statusHtml += `\n <div class=\"status-item\">\n <strong>Sentence Filter:</strong> ${filterCount} sentence(s) excluded\n <span class=\"status-indicator success\">Configured</span>\n </div>\n <div class=\"status-details\">\n <strong>Excluded:</strong> ${escapedFilters}\n </div>\n `;\n }\n\n statusDiv.innerHTML = statusHtml;\n }\n\n // ── Utility methods ────────────────────────────────────────────────────────\n\n buildCompletePluginConfig(currentConfig: Record<string, unknown>): Record<string, unknown> {\n const defaults = this.extractSchemaDefaults(this.pluginSchema);\n const merged = this.deepMerge(defaults || {}, currentConfig || {}) as Record<string, unknown>;\n if (Array.isArray(merged.connections)) {\n return merged;\n }\n const normalizedServerType = this.normalizeServerType(merged.serverType);\n merged.serverType = normalizedServerType || \"client\";\n return merged;\n }\n\n normalizeServerType(value: unknown): string | undefined {\n if (value === true || value === \"server\") {\n return \"server\";\n }\n if (value === false || value === \"client\") {\n return \"client\";\n }\n return undefined;\n }\n\n extractSchemaDefaults(\n schemaNode: Record<string, any> | null\n ): Record<string, unknown> | undefined {\n if (!schemaNode || typeof schemaNode !== \"object\") {\n return undefined;\n }\n\n const isObjectNode = schemaNode.type === \"object\" || !!schemaNode.properties;\n const merged: Record<string, unknown> = {};\n let hasData = false;\n\n if (isObjectNode && schemaNode.default && this.isPlainObject(schemaNode.default)) {\n Object.assign(merged, this.deepClone(schemaNode.default));\n hasData = true;\n }\n\n if (schemaNode.properties && this.isPlainObject(schemaNode.properties)) {\n for (const [key, value] of Object.entries(schemaNode.properties)) {\n const childDefaults = this.extractSchemaDefaults(value as Record<string, any>);\n if (childDefaults !== undefined) {\n merged[key] = childDefaults;\n hasData = true;\n } else if (value && (value as Record<string, any>).type === \"object\") {\n merged[key] = {};\n hasData = true;\n } else if (value && (value as Record<string, any>).type === \"string\") {\n merged[key] = \"\";\n hasData = true;\n }\n }\n }\n\n if (schemaNode.dependencies && this.isPlainObject(schemaNode.dependencies)) {\n for (const dependencyValue of Object.values(schemaNode.dependencies)) {\n const dependencyDefaults = this.extractSchemaDefaults(\n dependencyValue as Record<string, any>\n );\n if (dependencyDefaults && this.isPlainObject(dependencyDefaults)) {\n Object.assign(merged, this.deepMerge(merged, dependencyDefaults));\n hasData = true;\n }\n }\n }\n\n for (const compositeKey of [\"oneOf\", \"anyOf\", \"allOf\"]) {\n const composite = schemaNode[compositeKey];\n if (!Array.isArray(composite)) {\n continue;\n }\n\n composite.forEach((item: Record<string, any>) => {\n const itemDefaults = this.extractSchemaDefaults(item);\n if (itemDefaults === undefined) {\n return;\n }\n\n if (this.isPlainObject(itemDefaults)) {\n Object.assign(merged, this.deepMerge(merged, itemDefaults));\n hasData = true;\n return;\n }\n\n if (!hasData && schemaNode.default === undefined) {\n return;\n }\n\n hasData = true;\n });\n }\n\n if (hasData) {\n return merged;\n }\n\n if (schemaNode.default !== undefined) {\n return this.deepClone(schemaNode.default) as Record<string, unknown>;\n }\n\n return undefined;\n }\n\n isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n }\n\n deepClone(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((item) => this.deepClone(item));\n }\n if (this.isPlainObject(value)) {\n const clone: Record<string, unknown> = {};\n for (const [key, childValue] of Object.entries(value)) {\n clone[key] = this.deepClone(childValue);\n }\n return clone;\n }\n return value;\n }\n\n deepMerge(baseValue: unknown, overrideValue: unknown): unknown {\n if (overrideValue === undefined) {\n return this.deepClone(baseValue);\n }\n if (Array.isArray(overrideValue)) {\n return this.deepClone(overrideValue);\n }\n\n if (this.isPlainObject(baseValue) && this.isPlainObject(overrideValue)) {\n const merged = this.deepClone(baseValue) as Record<string, unknown>;\n for (const [key, value] of Object.entries(overrideValue)) {\n merged[key] = this.deepMerge((baseValue as Record<string, unknown>)[key], value);\n }\n return merged;\n }\n\n return this.deepClone(overrideValue);\n }\n\n formatBytes(bytes: number): string {\n if (!bytes || bytes <= 0) {\n return \"0 B\";\n }\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n }\n\n formatRatioPercent(value: number): string {\n if (!Number.isFinite(value) || value <= 0) {\n return \"0.0%\";\n }\n return (value * 100).toFixed(1) + \"%\";\n }\n\n formatTimestampAge(timestamp: number): string {\n if (!Number.isFinite(timestamp) || timestamp <= 0) {\n return \"N/A\";\n }\n const ageMs = Math.max(0, Date.now() - timestamp);\n if (ageMs < 1000) {\n return \"just now\";\n }\n if (ageMs < 60000) {\n return `${Math.floor(ageMs / 1000)}s ago`;\n }\n if (ageMs < 3600000) {\n return `${Math.floor(ageMs / 60000)}m ago`;\n }\n return `${Math.floor(ageMs / 3600000)}h ago`;\n }\n\n escapeHtml(text: string): string {\n const div = document.createElement(\"div\");\n div.textContent = text;\n return div.innerHTML;\n }\n\n showNotification(message: string, type = \"success\") {\n const notification = document.getElementById(\"notification\");\n if (!notification) {\n return;\n }\n if (this._notificationTimer) {\n clearTimeout(this._notificationTimer);\n }\n notification.textContent = message;\n notification.className = `notification ${type} show`;\n\n this._notificationTimer = setTimeout(() => {\n notification.classList.remove(\"show\");\n this._notificationTimer = null;\n }, NOTIFICATION_TIMEOUT);\n }\n}\n\n// Extend Window interface for the global config instance\ndeclare global {\n interface Window {\n dataConnectorConfig: DataConnectorConfig;\n }\n}\n\n// Initialize when DOM is loaded\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n window.dataConnectorConfig = new DataConnectorConfig();\n});\n\n// Clean up metrics refresh interval when page is hidden or unloaded\ndocument.addEventListener(\"visibilitychange\", () => {\n if (!window.dataConnectorConfig) {\n return;\n }\n\n if (document.hidden) {\n if (window.dataConnectorConfig.metricsInterval) {\n clearInterval(window.dataConnectorConfig.metricsInterval);\n window.dataConnectorConfig.metricsInterval = null;\n }\n } else if (!window.dataConnectorConfig.metricsInterval) {\n window.dataConnectorConfig.loadMetrics();\n window.dataConnectorConfig.startMetricsRefresh();\n }\n});\n\nwindow.addEventListener(\"beforeunload\", () => {\n if (window.dataConnectorConfig && window.dataConnectorConfig.metricsInterval) {\n clearInterval(window.dataConnectorConfig.metricsInterval);\n window.dataConnectorConfig.metricsInterval = null;\n }\n});\n\nexport default DataConnectorConfig;\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n// expose the module cache\n__webpack_require__.c = __webpack_module_cache__;\n\n","__webpack_require__.f = {};\n// This file contains only the entry chunk.\n// The chunk loading function for additional chunks\n__webpack_require__.e = (chunkId) => {\n\treturn Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {\n\t\t__webpack_require__.f[key](chunkId, promises);\n\t\treturn promises;\n\t}, []));\n};","// This function allow to reference async chunks\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn \"\" + chunkId + \".\" + \"70a0bc6aadb412703390\" + \".js\";\n};","// This function allow to reference async chunks\n__webpack_require__.miniCssF = (chunkId) => {\n\t// return url for filenames based on template\n\treturn undefined;\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","__webpack_require__.S = {};\nvar initPromises = {};\nvar initTokens = {};\n__webpack_require__.I = (name, initScope) => {\n\tif(!initScope) initScope = [];\n\t// handling circular init calls\n\tvar initToken = initTokens[name];\n\tif(!initToken) initToken = initTokens[name] = {};\n\tif(initScope.indexOf(initToken) >= 0) return;\n\tinitScope.push(initToken);\n\t// only runs once\n\tif(initPromises[name]) return initPromises[name];\n\t// creates a new share scope if needed\n\tif(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};\n\t// runs all init snippets from all modules reachable\n\tvar scope = __webpack_require__.S[name];\n\tvar warn = (msg) => {\n\t\tif (typeof console !== \"undefined\" && console.warn) console.warn(msg);\n\t};\n\tvar uniqueName = \"signalk-edge-link\";\n\tvar register = (name, version, factory, eager) => {\n\t\tvar versions = scope[name] = scope[name] || {};\n\t\tvar activeVersion = versions[version];\n\t\tif(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };\n\t};\n\tvar initExternal = (id) => {\n\t\tvar handleError = (err) => (warn(\"Initialization of sharing external failed: \" + err));\n\t\ttry {\n\t\t\tvar module = __webpack_require__(id);\n\t\t\tif(!module) return;\n\t\t\tvar initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))\n\t\t\tif(module.then) return promises.push(module.then(initFn, handleError));\n\t\t\tvar initResult = initFn(module);\n\t\t\tif(initResult && initResult.then) return promises.push(initResult['catch'](handleError));\n\t\t} catch(err) { handleError(err); }\n\t}\n\tvar promises = [];\n\tswitch(name) {\n\t\tcase \"default\": {\n\t\t\tregister(\"react\", \"16.14.0\", () => (__webpack_require__.e(540).then(() => (() => (__webpack_require__(6540))))));\n\t\t}\n\t\tbreak;\n\t}\n\tif(!promises.length) return initPromises[name] = 1;\n\treturn initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));\n};","var scriptUrl;\nif (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + \"\";\nvar document = __webpack_require__.g.document;\nif (!scriptUrl && document) {\n\tif (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT')\n\t\tscriptUrl = document.currentScript.src;\n\tif (!scriptUrl) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tif(scripts.length) {\n\t\t\tvar i = scripts.length - 1;\n\t\t\twhile (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;\n\t\t}\n\t}\n}\n// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration\n// or pass an empty string (\"\") and set the __webpack_public_path__ variable from your code to use your own logic.\nif (!scriptUrl) throw new Error(\"Automatic publicPath is not supported in this browser\");\nscriptUrl = scriptUrl.replace(/^blob:/, \"\").replace(/#.*$/, \"\").replace(/\\?.*$/, \"\").replace(/\\/[^\\/]+$/, \"/\");\n__webpack_require__.p = scriptUrl;","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t792: 0\n};\n\n__webpack_require__.f.j = (chunkId, promises) => {\n\t\t// JSONP chunk loading for javascript\n\t\tvar installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;\n\t\tif(installedChunkData !== 0) { // 0 means \"already installed\".\n\n\t\t\t// a Promise means \"currently loading\".\n\t\t\tif(installedChunkData) {\n\t\t\t\tpromises.push(installedChunkData[2]);\n\t\t\t} else {\n\t\t\t\tif(true) { // all chunks have JS\n\t\t\t\t\t// setup Promise in chunk cache\n\t\t\t\t\tvar promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));\n\t\t\t\t\tpromises.push(installedChunkData[2] = promise);\n\n\t\t\t\t\t// start chunk loading\n\t\t\t\t\tvar url = __webpack_require__.p + __webpack_require__.u(chunkId);\n\t\t\t\t\t// create error before stack unwound to get useful stacktrace later\n\t\t\t\t\tvar error = new Error();\n\t\t\t\t\tvar loadingEnded = (event) => {\n\t\t\t\t\t\tif(__webpack_require__.o(installedChunks, chunkId)) {\n\t\t\t\t\t\t\tinstalledChunkData = installedChunks[chunkId];\n\t\t\t\t\t\t\tif(installedChunkData !== 0) installedChunks[chunkId] = undefined;\n\t\t\t\t\t\t\tif(installedChunkData) {\n\t\t\t\t\t\t\t\tvar errorType = event && (event.type === 'load' ? 'missing' : event.type);\n\t\t\t\t\t\t\t\tvar realSrc = event && event.target && event.target.src;\n\t\t\t\t\t\t\t\terror.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';\n\t\t\t\t\t\t\t\terror.name = 'ChunkLoadError';\n\t\t\t\t\t\t\t\terror.type = errorType;\n\t\t\t\t\t\t\t\terror.request = realSrc;\n\t\t\t\t\t\t\t\tinstalledChunkData[1](error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t__webpack_require__.l(url, loadingEnded, \"chunk-\" + chunkId, chunkId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n};\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n// no on chunks loaded\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunksignalk_edge_link\"] = self[\"webpackChunksignalk_edge_link\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// module cache are used so entry inlining is disabled\n// startup\n// Load entry module and return exports\nvar __webpack_exports__ = __webpack_require__(9217);\n"],"names":["inProgress","dataWebpackPrefix","MANAGEMENT_TOKEN_ERROR_MESSAGE","DEFAULT_AUTH_CONFIG","token","localStorageKey","queryParam","includeTokenInQuery","headerMode","readRuntimeAuthConfig","window","runtime","__EDGE_LINK_AUTH__","API_BASE_PATH","escapeHtml","str","String","replace","renderCard","title","subtitle","contentId","contentClass","toLowerCase","renderStatItem","label","value","hasError","renderMetricItem","statusClass","renderMetricItemHtml","htmlValue","renderBwStat","isHighlight","isSuccess","renderSectionGroup","description","content","id","DataConnectorConfig","constructor","isHydratingFromTextarea","this","connections","activeConnectionId","pluginConfig","pluginSchema","schemaCurrentMode","metricsInterval","syncTimeout","tokenHelpText","config","modeText","getTokenHelpText","deltaTimerConfig","subscriptionConfig","sentenceFilterConfig","protocolVersion","isServerMode","_refreshInFlight","_notificationTimer","init","loadPluginConfiguration","fetchConnections","renderPage","startMetricsRefresh","error","console","showNotification","Error","message","res","request","ok","json","_e","length","type","detectModeFromConfig","name","st","normalizeServerType","serverType","getActiveConnection","find","c","isLegacyMode","metricsPath","connId","encodeURIComponent","configPath","filename","monitoringPath","sub","congestionPath","bondingPath","bondingFailoverPath","input","response","trim","tokenFromQuery","URLSearchParams","location","search","get","localStorage","tokenFromStorage","getItem","resolveToken","headers","Headers","normalizedMode","set","attachAuthHeaders","fetch","apiFetch","status","isUnauthorized","authFailureMessage","context","renderTabs","renderConnectionContent","tabsDiv","document","getElementById","innerHTML","style","display","map","icon","statusDot","healthy","readyToSend","join","querySelectorAll","forEach","btn","addEventListener","dataset","connectionId","loadConnectionData","contentDiv","conn","renderServerContent","renderClientContent","setupEventListeners","container","telemetryAndHealth","renderPluginConfigurationCard","configuration","renderDeltaTimerCard","renderSubscriptionCard","renderSentenceFilterCard","updatePluginConfigUI","loadConfigurations","updateUI","updateStatus","loadMetrics","showErrors","configResponse","schemaResponse","Promise","all","configData","Array","isArray","schemaData","schema","currentMode","buildCompletePluginConfig","err","deltaResponse","subResponse","filterResponse","metrics","updateMetricsDisplay","loadV2Data","isClient","fetches","catch","push","results","alertsRes","packetLossRes","retransmissionsRes","congestionRes","bondingRes","monitoringData","alerts","packetLoss","retransmissions","updateMonitoringDisplay","congestionData","updateCongestionDisplay","bondingData","updateBondingDisplay","clearInterval","setInterval","refreshActiveTab","finally","updated","saveDeltaTimerBtn","saveDeltaTimer","saveSubscriptionBtn","saveSubscription","saveSentenceFilterBtn","saveSentenceFilter","addPathBtn","addPathItem","subscriptionJsonEditor","clearTimeout","setTimeout","syncFromJson","contextInput","updateJsonFromForm","metaId","el","savePluginConfigBtn","savePluginConfig","reloadPluginConfigBtn","reloadPluginConfiguration","loadDefaultPluginConfigBtn","loadDefaultPluginConfiguration","deltaTimer","cfg","ctxEl","pathsList","subscribe","path","jsonEl","JSON","stringify","populateMetaControls","meta","excludedSentences","editor","summary","hasConnections","summaryScope","keyLabel","summaryConfig","totalConnections","runtimeIndex","findIndex","summaryIndex","candidate","mode","protocol","Number","keyCount","Object","keys","toUpperCase","pathItem","createElement","className","placeholder","button","textContent","remove","appendChild","contextEl","pathInputs","from","filter","metaEnabledEl","checked","intervalEl","pathsEl","maxEl","intervalSec","maxPathsPerPacket","includePathsMatching","enabled","metaEnabled","metaIntervalSec","metaPathsRegex","metaMaxPerPacket","parse","warn","saveConfig","configKey","method","body","parseInt","isNaN","jsonText","undefined","split","s","parsedConfig","requestConfig","deepClone","normalizedServerType","result","success","updateNetworkQualityDisplay","updateBandwidthDisplay","updatePathAnalyticsDisplay","metricsDiv","stats","uptime","cryptoErrors","errorCounts","crypto","malformedPackets","dataPacketsReceived","rateLimitedPackets","droppedDeltaBatches","droppedDeltaCount","hasErrors","udpSendErrors","compressionErrors","encryptionErrors","subscriptionErrors","protocolLabel","metricsGridItems","formatted","deltasBuffered","subErrors","subscriptionErrorStat","metricsHtml","deltasSent","toLocaleString","deltasReceived","udpRetries","duplicatePackets","smartBatching","sb","totalSends","earlySends","timerSends","earlyPercent","Math","round","avgBytesPerDelta","maxDeltasPerBatch","oversizedPackets","recentErrors","errorItems","reverse","timeAgo","Date","now","timestamp","timeStr","floor","category","lastError","timeAgoStr","nqDiv","networkQuality","nq","qualityLabel","qualityColor","linkQuality","qualityPct","gaugeAngle","radStart","PI","radEnd","x1","cos","y1","sin","x2","y2","gaugeSvg","rttDisplay","rtt","jitterDisplay","jitter","isFinite","retransmitRate","packetLossDisplay","formatRatioPercent","retransmitRateDisplay","dataSourceDisplay","dataSource","nqHtml","activeLink","lastRemoteUpdate","formatTimestampAge","queueDepth","acksSent","naksSent","bandwidthDiv","bandwidth","bw","savedBytes","bytesOutRaw","bytesOut","bytesInRaw","bytesIn","savedFormatted","formatBytes","metaBytesOut","metaBytesIn","metaPacketsOut","metaPacketsIn","metaSnapshotsSent","metaDiffsSent","metaRateLimitedPackets","bandwidthStats","metadataStats","bytesOutFormatted","bytesOutRawFormatted","packetsOut","metaBytesOutFormatted","bytesInFormatted","bytesInRawFormatted","packetsIn","metaBytesInFormatted","bandwidthHtml","rateOutFormatted","rateInFormatted","compressionRatio","avgPacketSizeFormatted","renderBandwidthChart","history","maxRate","max","h","rateOut","rateIn","points","i","maxRateFormatted","METRICS_REFRESH_INTERVAL","pathDiv","pathStats","paths","categoryCount","Set","p","size","pathHtml","slice","barWidth","percentage","updatesPerMinute","bytesFormatted","data","section","div","manualMode","stateLabel","stateClass","modeLabel","currentDeltaTimer","nominalDeltaTimer","html","minDeltaTimer","maxDeltaTimer","targetRTT","avgRTT","avgLoss","toFixed","linksHtml","links","entries","link","isActive","aliveClass","loss","failoverBtn","triggerFailover","disabled","activeAlerts","alertEntries","metric","alert","level","alertCount","alertItems","entry","alertValue","renderStatItemHtml","overallLossRate","maxLossRate","trend","currentRate","avgRate","statusDiv","statusHtml","currentConfig","defaults","extractSchemaDefaults","merged","deepMerge","schemaNode","isObjectNode","properties","hasData","default","isPlainObject","assign","key","childDefaults","dependencies","dependencyValue","values","dependencyDefaults","compositeKey","composite","item","itemDefaults","clone","childValue","baseValue","overrideValue","bytes","sizes","min","log","parseFloat","pow","ageMs","text","notification","classList","dataConnectorConfig","hidden","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","__webpack_modules__","m","f","e","chunkId","reduce","promises","u","miniCssF","g","globalThis","Function","o","obj","prop","prototype","hasOwnProperty","call","l","url","done","script","needAttach","scripts","getElementsByTagName","getAttribute","charset","nc","setAttribute","src","onScriptComplete","prev","event","onerror","onload","timeout","doneFns","parentNode","removeChild","fn","bind","target","head","S","initPromises","initTokens","I","initScope","initToken","indexOf","scope","uniqueName","version","factory","eager","versions","activeVersion","loaded","then","register","scriptUrl","importScripts","currentScript","tagName","test","installedChunks","j","installedChunkData","promise","resolve","reject","errorType","realSrc","webpackJsonpCallback","parentChunkLoadingFunction","chunkIds","moreModules","some","chunkLoadingGlobal","self"],"sourceRoot":""}
|