signalk-ais-navionics-converter 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-ais-navionics-converter",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "SignalK plugin to convert AIS data to NMEA 0183 sentences to TCP clients (e.g. Navionics boating app, OpenCpn) and optional to vesselfinder.com",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -17,7 +17,8 @@
17
17
  "url": "https://github.com/formifan2002/signalk-ais-navionics-converter"
18
18
  },
19
19
  "dependencies": {
20
- "axios": "^1.13.2"
20
+ "axios": "^1.13.2",
21
+ "ws": "^8.18.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@babel/core": "^7.23.0",
@@ -40,16 +41,5 @@
40
41
  "engines": {
41
42
  "node": ">=14.0.0"
42
43
  },
43
- "files": [
44
- "index.js",
45
- "ais-encoder.js",
46
- "package.json",
47
- "public/",
48
- "src/",
49
- "webpack.config.js",
50
- ".babelrc",
51
- "README.md",
52
- "LICENSE"
53
- ],
54
44
  "signalk-plugin-enabled-by-default": false
55
45
  }
@@ -15,7 +15,7 @@
15
15
  \*****************************************************/
16
16
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
17
17
 
18
- eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"webpack/sharing/consume/default/react/react\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nconst PluginConfigurationPanel = ({\n configuration,\n save\n}) => {\n const [config, setConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration || {});\n const [initialConfig, setInitialConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration);\n const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [status, setStatus] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const [showDialog, setShowDialog] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [dialogData, setDialogData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({\n title: '',\n message: '',\n callback: null\n });\n const [ownMMSI, setOwnMMSI] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);\n const translations = {\n de: {\n general: 'Allgemein',\n tcpServer: 'TCP Server',\n filtering: 'Filterung',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',\n tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',\n skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',\n skipStaleData: 'Schiffe mit alten Daten überspringen',\n staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',\n staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen ab (Minuten, 0=deaktiviert):',\n minAlarmSOG: 'Minimale SOG für Alarm (m/s):',\n maxMinutesSOGToZero: 'Maximum Minuten vor SOG auf 0 gesetzt (0=keine Korrektur):',\n logDebugDetails: 'Debug alle Schiff-Details',\n logMMSI: 'Filter Debug-Ausgabe für MMSI:',\n logDebugStale: 'Debug alte Schiffe',\n logDebugJSON: 'Debug JSON-Daten',\n logDebugAIS: 'Debug AIS-Daten',\n logDebugSOG: 'Debug Schiffe mit korrigierter SOG',\n vesselFinderEnabled: 'VesselFinder-Weiterleitung aktivieren',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (Sekunden):',\n cloudVesselsEnabled: 'Schiffe von AISFleet.com einbeziehen',\n cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',\n cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',\n save: 'Speichern',\n cancel: 'Abbruch',\n unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',\n unsavedTitle: 'Ungespeicherte Änderungen',\n yes: 'Ja',\n no: 'Nein'\n },\n en: {\n general: 'General',\n tcpServer: 'TCP Server',\n filtering: 'Filtering',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n updateInterval: 'Update interval for changed vessels (seconds):',\n tcpResendInterval: 'Update interval for unchanged vessels (seconds):',\n skipWithoutCallsign: 'Skip vessels without callsign',\n skipStaleData: 'Skip vessels with stale data',\n staleDataThreshold: 'Stale data threshold (minutes):',\n staleDataShipname: 'Add timestamp to ship name from (minutes, 0=disabled):',\n minAlarmSOG: 'Minimum SOG for alarm (m/s):',\n maxMinutesSOGToZero: 'Maximum minutes before SOG set to 0 (0=no correction):',\n logDebugDetails: 'Debug all vessel details',\n logMMSI: 'Filter Debug MMSI:',\n logDebugStale: 'Debug stale vessels',\n logDebugJSON: 'Debug JSON data',\n logDebugAIS: 'Debug AIS data',\n logDebugSOG: 'Debug vessels with corrected SOG',\n vesselFinderEnabled: 'Enable VesselFinder forwarding',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (seconds):',\n cloudVesselsEnabled: 'Include vessels from AISFleet.com',\n cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',\n cloudVesselsRadius: 'Radius from own vessel (nautical miles):',\n save: 'Save',\n cancel: 'Cancel',\n unsavedWarning: 'There are unsaved changes. Really cancel?',\n unsavedTitle: 'Unsaved changes',\n yes: 'Yes',\n no: 'No'\n }\n };\n const [currentLang, setCurrentLang] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(config.language === 'de' ? 'de' : 'en');\n const t = translations[currentLang];\n\n // Hole eigene MMSI beim Laden\n react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {\n const fetchOwnMMSI = async () => {\n try {\n const protocol = window.location.protocol;\n const hostname = window.location.hostname;\n const port = window.location.port;\n const url = `${protocol}//${hostname}${port ? ':' + port : ''}/signalk/v1/api/self`;\n console.log('Fetching own MMSI from:', url);\n console.log('Protocol:', protocol, 'Hostname:', hostname, 'Port:', port);\n const response = await fetch(url);\n console.log('Response status:', response.status);\n if (response.ok) {\n const data = await response.json();\n console.log('Self data received:', JSON.stringify(data, null, 2));\n\n // Data könnte String oder Objekt sein\n let vesselKey = null;\n if (typeof data === 'string') {\n // String Format: \"vessels.urn:mrn:imo:mmsi:211177520\"\n vesselKey = data.replace('vessels.', '');\n console.log('Parsed vessel key from string:', vesselKey);\n } else if (data.vessels && typeof data.vessels === 'object') {\n // Objekt Format\n const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));\n vesselKey = mmsiMatch;\n console.log('Found vessel key in object:', vesselKey);\n }\n if (vesselKey) {\n const mmsi = vesselKey.match(/mmsi:(\\d+)/)?.[1];\n console.log('Extracted MMSI:', mmsi);\n if (mmsi) {\n setOwnMMSI(mmsi);\n }\n }\n } else {\n console.error('Failed to fetch self data, status:', response.status);\n }\n } catch (err) {\n console.error('Failed to fetch own MMSI:', err);\n // Fehler ignorieren - Validierung wird einfach nicht aktiviert\n }\n };\n\n // Verzögert starten um sicherzustellen dass DOM ready ist\n setTimeout(fetchOwnMMSI, 500);\n }, []);\n const handleConfigChange = (key, value) => {\n setConfig(prev => ({\n ...prev,\n [key]: value\n }));\n };\n const isMMSIInvalid = () => {\n return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;\n };\n const checkUnsavedChanges = () => {\n return JSON.stringify(config) !== JSON.stringify(initialConfig);\n };\n const handleSave = () => {\n // Validiere Debug MMSI\n if (isMMSIInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: Sie können nicht die eigene MMSI zum Filtern verwenden!' : 'Error: You cannot use your own MMSI for filtering!';\n alert(errorMsg);\n return;\n }\n setLoading(true);\n if (save) {\n try {\n const result = save(config);\n if (result && typeof result.then === 'function') {\n // save() gibt ein Promise zurück\n result.then(() => {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n }).catch(err => {\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n }).finally(() => {\n setLoading(false);\n });\n } else {\n // save() gibt kein Promise zurück - assume success\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n } catch (err) {\n console.error('Error in handleSave:', err);\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n }\n };\n const handleLanguageChange = lang => {\n setCurrentLang(lang);\n handleConfigChange('language', lang === 'de');\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.container\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.header\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", {\n style: styles.title\n }, \"AIS to NMEA 0183 Converter\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => window.open('https://github.com/formifan2002/signalk-ais-navionics-converter', '_blank'),\n style: styles.helpButton\n }, \"\\u2139\\uFE0F \", currentLang === 'de' ? 'Hilfe' : 'Help')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.languageSelector\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('de'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'de' ? styles.langButtonActive : {})\n }\n }, \"Deutsch\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('en'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'en' ? styles.langButtonActive : {})\n }\n }, \"English\")), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.tcpServer), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Dieser Port ist später in der Navionics boating app im Menüpunkt \\'Gekoppelte Geräte\\' als TCP Port anzugeben.' : 'This port must be specified later in the Navionics boating app under the menu item \\'Paired Devices\\' as TCP Port.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.tcpPort || 10113,\n onChange: e => handleConfigChange('tcpPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.updateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.updateInterval || 15,\n onChange: e => handleConfigChange('updateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpResendInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.tcpResendInterval || 60,\n onChange: e => handleConfigChange('tcpResendInterval', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.filtering), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipWithoutCallsign || false,\n onChange: e => handleConfigChange('skipWithoutCallsign', e.target.checked)\n }), t.skipWithoutCallsign)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipStaleData !== false,\n onChange: e => handleConfigChange('skipStaleData', e.target.checked)\n }), t.skipStaleData)), config.skipStaleData !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataThreshold), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.staleDataThresholdMinutes || 60,\n onChange: e => handleConfigChange('staleDataThresholdMinutes', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataShipname), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.staleDataShipnameAddTime || 5,\n onChange: e => handleConfigChange('staleDataShipnameAddTime', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.minAlarmSOG), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n step: \"0.1\",\n value: config.minAlarmSOG || 0.2,\n onChange: e => handleConfigChange('minAlarmSOG', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.maxMinutesSOGToZero), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.maxMinutesSOGToZero || 0,\n onChange: e => handleConfigChange('maxMinutesSOGToZero', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.debugging), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugDetails || false,\n onChange: e => handleConfigChange('logDebugDetails', e.target.checked)\n }), t.logDebugDetails)), config.logDebugDetails && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.logMMSI), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Debug Ausgaben werden nur für das Schiff mit dieser MMSI erzeugt. Für das eigene Schiff / die eigene MMSI werden keine AIS Daten erzeugt. Wenn das Feld leer bleibt, werden Debug-Ausgaben für alle Schiffe (außer dem eigenen) erzeugt.' : 'Debug output is only generated for the vessel with this MMSI. No AIS data is generated for your own vessel / own MMSI. If the field is left empty, debug output is generated for all vessels (except your own).'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.logMMSI || '',\n onChange: e => handleConfigChange('logMMSI', e.target.value),\n placeholder: \"e.g. 123456789\",\n style: {\n ...styles.input,\n ...(isMMSIInvalid() ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), isMMSIInvalid() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, currentLang === 'de' ? '❌ Sie können nicht die eigene MMSI verwenden!' : '❌ You cannot use your own MMSI!')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugStale || false,\n onChange: e => handleConfigChange('logDebugStale', e.target.checked)\n }), t.logDebugStale)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugJSON || false,\n onChange: e => handleConfigChange('logDebugJSON', e.target.checked)\n }), t.logDebugJSON)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugAIS || false,\n onChange: e => handleConfigChange('logDebugAIS', e.target.checked)\n }), t.logDebugAIS)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugSOG || false,\n onChange: e => handleConfigChange('logDebugSOG', e.target.checked)\n }), t.logDebugSOG)))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.vesselFinder), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.vesselFinderEnabled || false,\n onChange: e => handleConfigChange('vesselFinderEnabled', e.target.checked)\n }), t.vesselFinderEnabled)), config.vesselFinderEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderHost), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.vesselFinderHost || 'ais.vesselfinder.com',\n onChange: e => handleConfigChange('vesselFinderHost', e.target.value),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.vesselFinderPort || 5500,\n onChange: e => handleConfigChange('vesselFinderPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderUpdateRate), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.vesselFinderUpdateRate || 60,\n onChange: e => handleConfigChange('vesselFinderUpdateRate', Number(e.target.value)),\n style: styles.input\n })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.cloudVessels), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.cloudVesselsEnabled !== false,\n onChange: e => handleConfigChange('cloudVesselsEnabled', e.target.checked)\n }), t.cloudVesselsEnabled)), config.cloudVesselsEnabled !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsUpdateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsUpdateInterval || 60,\n onChange: e => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsRadius), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsRadius || 10,\n onChange: e => handleConfigChange('cloudVesselsRadius', Number(e.target.value)),\n style: styles.input\n })))), status && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n ...styles.statusMessage,\n ...(status === 'error' ? styles.error : styles.success)\n }\n }, status === 'success' ? currentLang === 'de' ? 'Konfiguration gespeichert' : 'Configuration saved' : currentLang === 'de' ? 'Fehler beim Speichern' : 'Error saving'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.buttonGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: handleSave,\n disabled: loading || isMMSIInvalid(),\n style: {\n ...styles.button,\n ...styles.primaryButton,\n ...(isMMSIInvalid() ? {\n opacity: 0.5,\n cursor: 'not-allowed'\n } : {})\n }\n }, t.save), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (checkUnsavedChanges()) {\n setDialogData({\n title: t.unsavedTitle,\n message: t.unsavedWarning,\n callback: () => setConfig(initialConfig)\n });\n setShowDialog(true);\n } else {\n setConfig(initialConfig);\n }\n },\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.cancel)), showDialog && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialog\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogContent\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h4\", {\n style: styles.dialogTitle\n }, dialogData.title), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", null, dialogData.message), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogButtons\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => setShowDialog(false),\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.no), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (dialogData.callback) dialogData.callback();\n setShowDialog(false);\n },\n style: {\n ...styles.button,\n ...styles.primaryButton\n }\n }, t.yes)))));\n};\nconst styles = {\n container: {\n padding: '20px',\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto'\n },\n header: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: '30px',\n paddingBottom: '20px',\n borderBottom: '2px solid #667eea'\n },\n title: {\n margin: 0,\n fontSize: '1.5em',\n fontWeight: '600',\n color: '#333'\n },\n helpButton: {\n padding: '8px 16px',\n backgroundColor: '#667eea',\n color: 'white',\n border: 'none',\n borderRadius: '6px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '0.95em',\n transition: 'background 0.3s'\n },\n languageSelector: {\n display: 'flex',\n gap: '10px',\n marginBottom: '30px'\n },\n langButton: {\n padding: '8px 16px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n backgroundColor: '#f5f5f5'\n },\n langButtonActive: {\n backgroundColor: '#667eea',\n color: 'white',\n borderColor: '#667eea'\n },\n section: {\n marginBottom: '30px'\n },\n sectionTitle: {\n fontSize: '1.2em',\n fontWeight: '600',\n marginBottom: '15px',\n color: '#333',\n borderBottom: '2px solid #667eea',\n paddingBottom: '10px'\n },\n formGroup: {\n marginBottom: '15px'\n },\n label: {\n display: 'block',\n fontWeight: '500',\n marginBottom: '5px',\n color: '#333'\n },\n input: {\n padding: '8px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n fontSize: '1em',\n width: '200px'\n },\n checkbox: {\n display: 'flex',\n alignItems: 'center',\n marginBottom: '8px',\n cursor: 'pointer',\n gap: '8px'\n },\n buttonGroup: {\n display: 'flex',\n gap: '10px',\n marginTop: '30px',\n paddingTop: '20px',\n borderTop: '1px solid #ddd'\n },\n button: {\n padding: '10px 20px',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '1em'\n },\n primaryButton: {\n backgroundColor: '#667eea',\n color: 'white'\n },\n secondaryButton: {\n backgroundColor: '#6c757d',\n color: 'white'\n },\n statusMessage: {\n padding: '12px',\n borderRadius: '4px',\n marginBottom: '15px',\n fontSize: '0.95em'\n },\n success: {\n backgroundColor: '#d4edda',\n color: '#155724'\n },\n error: {\n backgroundColor: '#f8d7da',\n color: '#721c24'\n },\n dialog: {\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 1000\n },\n dialogContent: {\n backgroundColor: 'white',\n padding: '30px',\n borderRadius: '8px',\n maxWidth: '400px',\n boxShadow: '0 4px 6px rgba(0,0,0,0.1)'\n },\n dialogTitle: {\n marginTop: 0,\n marginBottom: '15px',\n fontSize: '1.1em'\n },\n dialogButtons: {\n display: 'flex',\n gap: '10px',\n justifyContent: 'flex-end',\n marginTop: '20px'\n }\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (PluginConfigurationPanel);\n\n//# sourceURL=webpack://signalk-ais-navionics-converter/./src/components/PluginConfigurationPanel.jsx?\n}");
18
+ eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"webpack/sharing/consume/default/react/react\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nconst PluginConfigurationPanel = ({\n configuration,\n save\n}) => {\n const [config, setConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration || {});\n const [initialConfig, setInitialConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration);\n const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [status, setStatus] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const [showDialog, setShowDialog] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [dialogData, setDialogData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({\n title: '',\n message: '',\n callback: null\n });\n const [ownMMSI, setOwnMMSI] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);\n const [aisfleetEnabled, setAisfleetEnabled] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [portError, setPortError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const translations = {\n de: {\n general: 'Allgemein',\n tcpServer: 'TCP Server',\n filtering: 'Filterung',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n wsPort: 'WebSocket Port:',\n updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',\n tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',\n skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',\n skipStaleData: 'Schiffe mit alten Daten überspringen',\n staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',\n staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen wenn die letzte Positionsmeldung älter ist als x Minuten (0=deaktiviert):',\n minAlarmSOG: 'SOG (und COG) wird auf 0 gesetzt, wenn die Geschwindigkeit kleiner als x Knoten ist (0=deaktiviert):',\n maxMinutesSOGToZero: 'SOG wird auf 0 gesetzt wenn die letzte Positionsmeldung älter ist als x Minuten (0=keine Korrektur):',\n logDebugDetails: 'Debug Schiff-Details',\n logMMSI: 'Filter Debug-Ausgabe nur für MMSI:',\n logDebugStale: 'Debug alte Schiffe',\n logDebugJSON: 'Debug JSON-Daten',\n logDebugAIS: 'Debug AIS-Daten',\n logDebugSOG: 'Debug Schiffe mit korrigierter SOG',\n vesselFinderEnabled: 'VesselFinder-Weiterleitung aktivieren',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (Sekunden):',\n cloudVesselsEnabled: 'Schiffe von AISFleet.com einbeziehen',\n cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',\n cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',\n portError: 'TCP Port und WebSocket Port müssen unterschiedlich sein',\n save: 'Speichern',\n cancel: 'Abbruch',\n unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',\n unsavedTitle: 'Ungespeicherte Änderungen',\n yes: 'Ja',\n no: 'Nein'\n },\n en: {\n general: 'General',\n tcpServer: 'TCP Server',\n filtering: 'Filtering',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n wsPort: 'WebSocket Port:',\n updateInterval: 'Update interval for changed vessels (seconds):',\n tcpResendInterval: 'Update interval for unchanged vessels (seconds):',\n skipWithoutCallsign: 'Skip vessels without callsign',\n skipStaleData: 'Skip vessels with stale data',\n staleDataThreshold: 'Stale data threshold (minutes):',\n staleDataShipname: 'Add timestamp to vessel name if the last position report is older than x minutes (0=disabled):',\n minAlarmSOG: 'SOG (and COG) is set to 0 if the speed is less than x knots (0=disabled):',\n maxMinutesSOGToZero: 'SOG is set to 0 if the last position report is older than x minutes (0=no correction):',\n logDebugDetails: 'Debug vessel details',\n logMMSI: 'Filter Debug only for MMSI:',\n logDebugStale: 'Debug stale vessels',\n logDebugJSON: 'Debug JSON data',\n logDebugAIS: 'Debug AIS data',\n logDebugSOG: 'Debug vessels with corrected SOG',\n vesselFinderEnabled: 'Enable VesselFinder forwarding',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (seconds):',\n cloudVesselsEnabled: 'Include vessels from AISFleet.com',\n cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',\n cloudVesselsRadius: 'Radius from own vessel (nautical miles):',\n portError: 'TCP Port and WebSocket Port must be different',\n save: 'Save',\n cancel: 'Cancel',\n unsavedWarning: 'There are unsaved changes. Really cancel?',\n unsavedTitle: 'Unsaved changes',\n yes: 'Yes',\n no: 'No'\n }\n };\n const [currentLang, setCurrentLang] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(config.language === 'de' ? 'de' : 'en');\n const t = translations[currentLang];\n\n // Hole eigene MMSI beim Laden\n (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {\n const fetchOwnMMSI = async () => {\n try {\n const protocol = window.location.protocol;\n const hostname = window.location.hostname;\n const port = window.location.port;\n const baseUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;\n const aisfleetUrl = `${baseUrl}/plugins/aisfleet`;\n const url = `${baseUrl}/signalk/v1/api/self`;\n const response = await fetch(url);\n if (response.ok) {\n const data = await response.json();\n let vesselKey = null;\n if (typeof data === 'string') {\n vesselKey = data.replace('vessels.', '');\n } else if (data.vessels && typeof data.vessels === 'object') {\n const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));\n vesselKey = mmsiMatch;\n }\n if (vesselKey) {\n const mmsi = vesselKey.match(/mmsi:(\\d+)/)?.[1];\n if (mmsi) setOwnMMSI(mmsi);\n }\n }\n\n // AIS Fleet Plugin prüfen\n try {\n const aisResponse = await fetch(aisfleetUrl);\n if (aisResponse.ok) {\n const aisData = await aisResponse.json();\n setAisfleetEnabled(!!aisData.enabled);\n } else {\n setAisfleetEnabled(false);\n }\n } catch (err) {\n setAisfleetEnabled(false);\n }\n } catch (err) {\n console.error('Failed to fetch own MMSI:', err);\n }\n };\n setTimeout(fetchOwnMMSI, 500);\n }, []);\n const handleConfigChange = (key, value) => {\n setConfig(prev => ({\n ...prev,\n [key]: value\n }));\n\n // Überprüfe Port-Nummern\n if (key === 'tcpPort' || key === 'wsPort') {\n const tcpPort = key === 'tcpPort' ? value : config.tcpPort;\n const wsPort = key === 'wsPort' ? value : config.wsPort;\n if (tcpPort && wsPort && tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort) {\n setPortError(t.portError);\n } else {\n setPortError('');\n }\n }\n };\n const isMMSIInvalid = () => {\n return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;\n };\n const isPortInvalid = () => {\n const tcpPort = config.tcpPort || 10113;\n const wsPort = config.wsPort || 10114;\n return tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort;\n };\n const checkUnsavedChanges = () => {\n return JSON.stringify(config) !== JSON.stringify(initialConfig);\n };\n const handleSave = () => {\n // Validiere Debug MMSI\n if (isMMSIInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: Sie können nicht die eigene MMSI zum Filtern verwenden!' : 'Error: You cannot use your own MMSI for filtering!';\n alert(errorMsg);\n return;\n }\n\n // Validiere Ports\n if (isPortInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: TCP Port und WebSocket Port müssen unterschiedlich sein!' : 'Error: TCP Port and WebSocket Port must be different!';\n alert(errorMsg);\n return;\n }\n setLoading(true);\n if (save) {\n try {\n const result = save(config);\n if (result && typeof result.then === 'function') {\n result.then(() => {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n }).catch(err => {\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n }).finally(() => {\n setLoading(false);\n });\n } else {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n } catch (err) {\n console.error('Error in handleSave:', err);\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n }\n };\n const handleLanguageChange = lang => {\n setCurrentLang(lang);\n handleConfigChange('language', lang === 'de');\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.container\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.header\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", {\n style: styles.title\n }, \"AIS to NMEA 0183 Converter\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => window.open('https://github.com/formifan2002/signalk-ais-navionics-converter', '_blank'),\n style: styles.helpButton\n }, \"\\u2139\\uFE0F \", currentLang === 'de' ? 'Hilfe' : 'Help')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.languageSelector\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('de'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'de' ? styles.langButtonActive : {})\n }\n }, \"Deutsch\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('en'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'en' ? styles.langButtonActive : {})\n }\n }, \"English\")), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.tcpServer), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Dieser Port ist später in der Navionics boating app im Menüpunkt \\'Gekoppelte Geräte\\' als TCP Port anzugeben.' : 'This port must be specified later in the Navionics boating app under the menu item \\'Paired Devices\\' as TCP Port.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.tcpPort || 10113,\n onChange: e => handleConfigChange('tcpPort', Number(e.target.value)),\n style: {\n ...styles.input,\n ...(portError ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.wsPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Über diesen Port werden alle AIS Daten als NMEA0183 und alle Schiffsdaten per JOSN als Websocket gesendet (nicht für Navionics relevant). 0=kein Websocket Server.' : 'This port is used to send all AIS data as NMEA0183 and all vessel data as JSON via Websocket (not relevant for Navionics). 0=no Websocket server.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n max: \"65535\",\n value: config.wsPort || 10114,\n onChange: e => handleConfigChange('wsPort', Number(e.target.value)),\n style: {\n ...styles.input,\n ...(portError ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), portError && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, \"\\u26A0\\uFE0F \", portError)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.updateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.updateInterval || 15,\n onChange: e => handleConfigChange('updateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpResendInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.tcpResendInterval || 60,\n onChange: e => handleConfigChange('tcpResendInterval', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.filtering), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipWithoutCallsign || false,\n onChange: e => handleConfigChange('skipWithoutCallsign', e.target.checked)\n }), t.skipWithoutCallsign)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipStaleData !== false,\n onChange: e => handleConfigChange('skipStaleData', e.target.checked)\n }), t.skipStaleData)), config.skipStaleData !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataThreshold), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.staleDataThresholdMinutes || 60,\n onChange: e => handleConfigChange('staleDataThresholdMinutes', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataShipname), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.staleDataShipnameAddTime || 5,\n onChange: e => handleConfigChange('staleDataShipnameAddTime', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.minAlarmSOG), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n step: \"0.1\",\n value: config.minAlarmSOG || 0.2,\n onChange: e => handleConfigChange('minAlarmSOG', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.maxMinutesSOGToZero), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.maxMinutesSOGToZero || 0,\n onChange: e => handleConfigChange('maxMinutesSOGToZero', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.debugging), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugDetails || false,\n onChange: e => handleConfigChange('logDebugDetails', e.target.checked)\n }), t.logDebugDetails)), config.logDebugDetails && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.logMMSI), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Debug Ausgaben werden nur für das Schiff mit dieser MMSI erzeugt. Für das eigene Schiff / die eigene MMSI werden keine AIS Daten erzeugt. Wenn das Feld leer bleibt, werden Debug-Ausgaben für alle Schiffe (außer dem eigenen) erzeugt.' : 'Debug output is only generated for the vessel with this MMSI. No AIS data is generated for your own vessel / own MMSI. If the field is left empty, debug output is generated for all vessels (except your own).'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.logMMSI || '',\n onChange: e => handleConfigChange('logMMSI', e.target.value),\n placeholder: \"e.g. 123456789\",\n style: {\n ...styles.input,\n ...(isMMSIInvalid() ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), isMMSIInvalid() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, currentLang === 'de' ? '⚠️ Sie können nicht die eigene MMSI verwenden!' : '⚠️ You cannot use your own MMSI!')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugStale || false,\n onChange: e => handleConfigChange('logDebugStale', e.target.checked)\n }), t.logDebugStale)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugJSON || false,\n onChange: e => handleConfigChange('logDebugJSON', e.target.checked)\n }), t.logDebugJSON)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugAIS || false,\n onChange: e => handleConfigChange('logDebugAIS', e.target.checked)\n }), t.logDebugAIS)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugSOG || false,\n onChange: e => handleConfigChange('logDebugSOG', e.target.checked)\n }), t.logDebugSOG)))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.vesselFinder), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.vesselFinderEnabled || false,\n onChange: e => handleConfigChange('vesselFinderEnabled', e.target.checked)\n }), t.vesselFinderEnabled)), config.vesselFinderEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderHost), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.vesselFinderHost || 'ais.vesselfinder.com',\n onChange: e => handleConfigChange('vesselFinderHost', e.target.value),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.vesselFinderPort || 5500,\n onChange: e => handleConfigChange('vesselFinderPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderUpdateRate), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.vesselFinderUpdateRate || 60,\n onChange: e => handleConfigChange('vesselFinderUpdateRate', Number(e.target.value)),\n style: styles.input\n })))), !aisfleetEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.cloudVessels), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.cloudVesselsEnabled !== false,\n onChange: e => handleConfigChange('cloudVesselsEnabled', e.target.checked)\n }), t.cloudVesselsEnabled)), config.cloudVesselsEnabled !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsUpdateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsUpdateInterval || 60,\n onChange: e => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsRadius), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsRadius || 10,\n onChange: e => handleConfigChange('cloudVesselsRadius', Number(e.target.value)),\n style: styles.input\n })))), status && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n ...styles.statusMessage,\n ...(status === 'error' ? styles.error : styles.success)\n }\n }, status === 'success' ? currentLang === 'de' ? 'Konfiguration gespeichert' : 'Configuration saved' : currentLang === 'de' ? 'Fehler beim Speichern' : 'Error saving'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.buttonGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: handleSave,\n disabled: loading || isMMSIInvalid() || isPortInvalid(),\n style: {\n ...styles.button,\n ...styles.primaryButton,\n ...(isMMSIInvalid() || isPortInvalid() ? {\n opacity: 0.5,\n cursor: 'not-allowed'\n } : {})\n }\n }, t.save), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (checkUnsavedChanges()) {\n setDialogData({\n title: t.unsavedTitle,\n message: t.unsavedWarning,\n callback: () => setConfig(initialConfig)\n });\n setShowDialog(true);\n } else {\n setConfig(initialConfig);\n }\n },\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.cancel)), showDialog && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialog\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogContent\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h4\", {\n style: styles.dialogTitle\n }, dialogData.title), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", null, dialogData.message), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogButtons\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => setShowDialog(false),\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.no), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (dialogData.callback) dialogData.callback();\n setShowDialog(false);\n },\n style: {\n ...styles.button,\n ...styles.primaryButton\n }\n }, t.yes)))));\n};\nconst styles = {\n container: {\n padding: '20px',\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto'\n },\n header: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: '30px',\n paddingBottom: '20px',\n borderBottom: '2px solid #667eea'\n },\n title: {\n margin: 0,\n fontSize: '1.5em',\n fontWeight: '600',\n color: '#333'\n },\n helpButton: {\n padding: '8px 16px',\n backgroundColor: '#667eea',\n color: 'white',\n border: 'none',\n borderRadius: '6px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '0.95em',\n transition: 'background 0.3s'\n },\n languageSelector: {\n display: 'flex',\n gap: '10px',\n marginBottom: '30px'\n },\n langButton: {\n padding: '8px 16px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n backgroundColor: '#f5f5f5'\n },\n langButtonActive: {\n backgroundColor: '#667eea',\n color: 'white',\n borderColor: '#667eea'\n },\n section: {\n marginBottom: '30px'\n },\n sectionTitle: {\n fontSize: '1.2em',\n fontWeight: '600',\n marginBottom: '15px',\n color: '#333',\n borderBottom: '2px solid #667eea',\n paddingBottom: '10px'\n },\n formGroup: {\n marginBottom: '15px'\n },\n label: {\n display: 'block',\n fontWeight: '500',\n marginBottom: '5px',\n color: '#333'\n },\n input: {\n padding: '8px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n fontSize: '1em',\n width: '200px'\n },\n checkbox: {\n display: 'flex',\n alignItems: 'center',\n marginBottom: '8px',\n cursor: 'pointer',\n gap: '8px'\n },\n buttonGroup: {\n display: 'flex',\n gap: '10px',\n marginTop: '30px',\n paddingTop: '20px',\n borderTop: '1px solid #ddd'\n },\n button: {\n padding: '10px 20px',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '1em'\n },\n primaryButton: {\n backgroundColor: '#667eea',\n color: 'white'\n },\n secondaryButton: {\n backgroundColor: '#6c757d',\n color: 'white'\n },\n statusMessage: {\n padding: '12px',\n borderRadius: '4px',\n marginBottom: '15px',\n fontSize: '0.95em'\n },\n success: {\n backgroundColor: '#d4edda',\n color: '#155724'\n },\n error: {\n backgroundColor: '#f8d7da',\n color: '#721c24'\n },\n dialog: {\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 1000\n },\n dialogContent: {\n backgroundColor: 'white',\n padding: '30px',\n borderRadius: '8px',\n maxWidth: '400px',\n boxShadow: '0 4px 6px rgba(0,0,0,0.1)'\n },\n dialogTitle: {\n marginTop: 0,\n marginBottom: '15px',\n fontSize: '1.1em'\n },\n dialogButtons: {\n display: 'flex',\n gap: '10px',\n justifyContent: 'flex-end',\n marginTop: '20px'\n }\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (PluginConfigurationPanel);\n\n//# sourceURL=webpack://signalk-ais-navionics-converter/./src/components/PluginConfigurationPanel.jsx?\n}");
19
19
 
20
20
  /***/ })
21
21
 
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
 
3
3
  const PluginConfigurationPanel = ({ configuration, save }) => {
4
4
  const [config, setConfig] = useState(configuration || {});
@@ -8,7 +8,9 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
8
8
  const [showDialog, setShowDialog] = useState(false);
9
9
  const [dialogData, setDialogData] = useState({ title: '', message: '', callback: null });
10
10
  const [ownMMSI, setOwnMMSI] = useState(null);
11
-
11
+ const [aisfleetEnabled, setAisfleetEnabled] = useState(false);
12
+ const [portError, setPortError] = useState('');
13
+
12
14
  const translations = {
13
15
  de: {
14
16
  general: 'Allgemein',
@@ -19,19 +21,20 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
19
21
  cloudVessels: 'Cloud Vessels (AISFleet)',
20
22
 
21
23
  tcpPort: 'TCP Port:',
24
+ wsPort: 'WebSocket Port:',
22
25
  updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',
23
26
  tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',
24
27
 
25
28
  skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',
26
29
  skipStaleData: 'Schiffe mit alten Daten überspringen',
27
30
  staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',
28
- staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen ab (Minuten, 0=deaktiviert):',
31
+ staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen wenn die letzte Positionsmeldung älter ist als x Minuten (0=deaktiviert):',
29
32
 
30
- minAlarmSOG: 'Minimale SOG für Alarm (m/s):',
31
- maxMinutesSOGToZero: 'Maximum Minuten vor SOG auf 0 gesetzt (0=keine Korrektur):',
33
+ minAlarmSOG: 'SOG (und COG) wird auf 0 gesetzt, wenn die Geschwindigkeit kleiner als x Knoten ist (0=deaktiviert):',
34
+ maxMinutesSOGToZero: 'SOG wird auf 0 gesetzt wenn die letzte Positionsmeldung älter ist als x Minuten (0=keine Korrektur):',
32
35
 
33
- logDebugDetails: 'Debug alle Schiff-Details',
34
- logMMSI: 'Filter Debug-Ausgabe für MMSI:',
36
+ logDebugDetails: 'Debug Schiff-Details',
37
+ logMMSI: 'Filter Debug-Ausgabe nur für MMSI:',
35
38
  logDebugStale: 'Debug alte Schiffe',
36
39
  logDebugJSON: 'Debug JSON-Daten',
37
40
  logDebugAIS: 'Debug AIS-Daten',
@@ -46,6 +49,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
46
49
  cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',
47
50
  cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',
48
51
 
52
+ portError: 'TCP Port und WebSocket Port müssen unterschiedlich sein',
53
+
49
54
  save: 'Speichern',
50
55
  cancel: 'Abbruch',
51
56
  unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',
@@ -62,19 +67,20 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
62
67
  cloudVessels: 'Cloud Vessels (AISFleet)',
63
68
 
64
69
  tcpPort: 'TCP Port:',
70
+ wsPort: 'WebSocket Port:',
65
71
  updateInterval: 'Update interval for changed vessels (seconds):',
66
72
  tcpResendInterval: 'Update interval for unchanged vessels (seconds):',
67
73
 
68
74
  skipWithoutCallsign: 'Skip vessels without callsign',
69
75
  skipStaleData: 'Skip vessels with stale data',
70
76
  staleDataThreshold: 'Stale data threshold (minutes):',
71
- staleDataShipname: 'Add timestamp to ship name from (minutes, 0=disabled):',
77
+ staleDataShipname: 'Add timestamp to vessel name if the last position report is older than x minutes (0=disabled):',
72
78
 
73
- minAlarmSOG: 'Minimum SOG for alarm (m/s):',
74
- maxMinutesSOGToZero: 'Maximum minutes before SOG set to 0 (0=no correction):',
79
+ minAlarmSOG: 'SOG (and COG) is set to 0 if the speed is less than x knots (0=disabled):',
80
+ maxMinutesSOGToZero: 'SOG is set to 0 if the last position report is older than x minutes (0=no correction):',
75
81
 
76
- logDebugDetails: 'Debug all vessel details',
77
- logMMSI: 'Filter Debug MMSI:',
82
+ logDebugDetails: 'Debug vessel details',
83
+ logMMSI: 'Filter Debug only for MMSI:',
78
84
  logDebugStale: 'Debug stale vessels',
79
85
  logDebugJSON: 'Debug JSON data',
80
86
  logDebugAIS: 'Debug AIS data',
@@ -89,6 +95,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
89
95
  cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',
90
96
  cloudVesselsRadius: 'Radius from own vessel (nautical miles):',
91
97
 
98
+ portError: 'TCP Port and WebSocket Port must be different',
99
+
92
100
  save: 'Save',
93
101
  cancel: 'Cancel',
94
102
  unsavedWarning: 'There are unsaved changes. Really cancel?',
@@ -102,65 +110,80 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
102
110
  const t = translations[currentLang];
103
111
 
104
112
  // Hole eigene MMSI beim Laden
105
- React.useEffect(() => {
113
+ useEffect(() => {
106
114
  const fetchOwnMMSI = async () => {
107
115
  try {
108
116
  const protocol = window.location.protocol;
109
117
  const hostname = window.location.hostname;
110
118
  const port = window.location.port;
111
- const url = `${protocol}//${hostname}${port ? ':' + port : ''}/signalk/v1/api/self`;
112
-
113
- console.log('Fetching own MMSI from:', url);
114
- console.log('Protocol:', protocol, 'Hostname:', hostname, 'Port:', port);
115
-
119
+ const baseUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;
120
+ const aisfleetUrl = `${baseUrl}/plugins/aisfleet`;
121
+ const url = `${baseUrl}/signalk/v1/api/self`;
122
+
116
123
  const response = await fetch(url);
117
- console.log('Response status:', response.status);
118
-
119
124
  if (response.ok) {
120
125
  const data = await response.json();
121
- console.log('Self data received:', JSON.stringify(data, null, 2));
122
-
123
- // Data könnte String oder Objekt sein
124
126
  let vesselKey = null;
127
+
125
128
  if (typeof data === 'string') {
126
- // String Format: "vessels.urn:mrn:imo:mmsi:211177520"
127
129
  vesselKey = data.replace('vessels.', '');
128
- console.log('Parsed vessel key from string:', vesselKey);
129
130
  } else if (data.vessels && typeof data.vessels === 'object') {
130
- // Objekt Format
131
131
  const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));
132
132
  vesselKey = mmsiMatch;
133
- console.log('Found vessel key in object:', vesselKey);
134
133
  }
135
-
134
+
136
135
  if (vesselKey) {
137
136
  const mmsi = vesselKey.match(/mmsi:(\d+)/)?.[1];
138
- console.log('Extracted MMSI:', mmsi);
139
- if (mmsi) {
140
- setOwnMMSI(mmsi);
141
- }
137
+ if (mmsi) setOwnMMSI(mmsi);
142
138
  }
143
- } else {
144
- console.error('Failed to fetch self data, status:', response.status);
139
+ }
140
+
141
+ // AIS Fleet Plugin prüfen
142
+ try {
143
+ const aisResponse = await fetch(aisfleetUrl);
144
+ if (aisResponse.ok) {
145
+ const aisData = await aisResponse.json();
146
+ setAisfleetEnabled(!!aisData.enabled);
147
+ } else {
148
+ setAisfleetEnabled(false);
149
+ }
150
+ } catch (err) {
151
+ setAisfleetEnabled(false);
145
152
  }
146
153
  } catch (err) {
147
154
  console.error('Failed to fetch own MMSI:', err);
148
- // Fehler ignorieren - Validierung wird einfach nicht aktiviert
149
155
  }
150
156
  };
151
-
152
- // Verzögert starten um sicherzustellen dass DOM ready ist
157
+
153
158
  setTimeout(fetchOwnMMSI, 500);
154
159
  }, []);
155
160
 
156
161
  const handleConfigChange = (key, value) => {
157
162
  setConfig(prev => ({ ...prev, [key]: value }));
163
+
164
+ // Überprüfe Port-Nummern
165
+ if (key === 'tcpPort' || key === 'wsPort') {
166
+ const tcpPort = key === 'tcpPort' ? value : config.tcpPort;
167
+ const wsPort = key === 'wsPort' ? value : config.wsPort;
168
+
169
+ if (tcpPort && wsPort && tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort) {
170
+ setPortError(t.portError);
171
+ } else {
172
+ setPortError('');
173
+ }
174
+ }
158
175
  };
159
176
 
160
177
  const isMMSIInvalid = () => {
161
178
  return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;
162
179
  };
163
180
 
181
+ const isPortInvalid = () => {
182
+ const tcpPort = config.tcpPort || 10113;
183
+ const wsPort = config.wsPort || 10114;
184
+ return tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort;
185
+ };
186
+
164
187
  const checkUnsavedChanges = () => {
165
188
  return JSON.stringify(config) !== JSON.stringify(initialConfig);
166
189
  };
@@ -176,13 +199,22 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
176
199
  return;
177
200
  }
178
201
 
202
+ // Validiere Ports
203
+ if (isPortInvalid()) {
204
+ setStatus('error');
205
+ const errorMsg = currentLang === 'de'
206
+ ? 'Fehler: TCP Port und WebSocket Port müssen unterschiedlich sein!'
207
+ : 'Error: TCP Port and WebSocket Port must be different!';
208
+ alert(errorMsg);
209
+ return;
210
+ }
211
+
179
212
  setLoading(true);
180
213
  if (save) {
181
214
  try {
182
215
  const result = save(config);
183
216
 
184
217
  if (result && typeof result.then === 'function') {
185
- // save() gibt ein Promise zurück
186
218
  result
187
219
  .then(() => {
188
220
  setStatus('success');
@@ -197,7 +229,6 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
197
229
  setLoading(false);
198
230
  });
199
231
  } else {
200
- // save() gibt kein Promise zurück - assume success
201
232
  setStatus('success');
202
233
  setInitialConfig(config);
203
234
  setTimeout(() => setStatus(''), 3000);
@@ -269,8 +300,49 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
269
300
  max="65535"
270
301
  value={config.tcpPort || 10113}
271
302
  onChange={(e) => handleConfigChange('tcpPort', Number(e.target.value))}
272
- style={styles.input}
303
+ style={{
304
+ ...styles.input,
305
+ ...(portError ? { borderColor: '#dc3545', backgroundColor: '#fff5f5' } : {})
306
+ }}
307
+ />
308
+ </div>
309
+
310
+ <div style={styles.formGroup}>
311
+ <label style={styles.label}>{t.wsPort}</label>
312
+ <div style={{
313
+ display: 'block',
314
+ fontSize: '0.85em',
315
+ color: '#666',
316
+ marginTop: '8px',
317
+ marginBottom: '12px',
318
+ fontStyle: 'italic',
319
+ lineHeight: '1.4'
320
+ }}>
321
+ {currentLang === 'de'
322
+ ? 'Über diesen Port werden alle AIS Daten als NMEA0183 und alle Schiffsdaten per JOSN als Websocket gesendet (nicht für Navionics relevant). 0=kein Websocket Server.'
323
+ : 'This port is used to send all AIS data as NMEA0183 and all vessel data as JSON via Websocket (not relevant for Navionics). 0=no Websocket server.'}
324
+ </div>
325
+ <input
326
+ type="number"
327
+ min="0"
328
+ max="65535"
329
+ value={config.wsPort || 10114}
330
+ onChange={(e) => handleConfigChange('wsPort', Number(e.target.value))}
331
+ style={{
332
+ ...styles.input,
333
+ ...(portError ? { borderColor: '#dc3545', backgroundColor: '#fff5f5' } : {})
334
+ }}
273
335
  />
336
+ {portError && (
337
+ <div style={{
338
+ color: '#dc3545',
339
+ fontSize: '0.85em',
340
+ marginTop: '8px',
341
+ fontWeight: '500'
342
+ }}>
343
+ ⚠️ {portError}
344
+ </div>
345
+ )}
274
346
  </div>
275
347
 
276
348
  <div style={styles.formGroup}>
@@ -420,8 +492,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
420
492
  fontWeight: '500'
421
493
  }}>
422
494
  {currentLang === 'de'
423
- ? ' Sie können nicht die eigene MMSI verwenden!'
424
- : ' You cannot use your own MMSI!'}
495
+ ? '⚠️ Sie können nicht die eigene MMSI verwenden!'
496
+ : '⚠️ You cannot use your own MMSI!'}
425
497
  </div>
426
498
  )}
427
499
  </div>
@@ -527,46 +599,48 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
527
599
  </div>
528
600
 
529
601
  {/* Cloud Vessels */}
530
- <div style={styles.section}>
531
- <h3 style={styles.sectionTitle}>{t.cloudVessels}</h3>
532
-
533
- <div style={styles.formGroup}>
534
- <label style={styles.checkbox}>
535
- <input
536
- type="checkbox"
537
- checked={config.cloudVesselsEnabled !== false}
538
- onChange={(e) => handleConfigChange('cloudVesselsEnabled', e.target.checked)}
539
- />
540
- {t.cloudVesselsEnabled}
541
- </label>
542
- </div>
602
+ {!aisfleetEnabled && (
603
+ <div style={styles.section}>
604
+ <h3 style={styles.sectionTitle}>{t.cloudVessels}</h3>
543
605
 
544
- {config.cloudVesselsEnabled !== false && (
545
- <>
546
- <div style={styles.formGroup}>
547
- <label style={styles.label}>{t.cloudVesselsUpdateInterval}</label>
606
+ <div style={styles.formGroup}>
607
+ <label style={styles.checkbox}>
548
608
  <input
549
- type="number"
550
- min="1"
551
- value={config.cloudVesselsUpdateInterval || 60}
552
- onChange={(e) => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value))}
553
- style={styles.input}
609
+ type="checkbox"
610
+ checked={config.cloudVesselsEnabled !== false}
611
+ onChange={(e) => handleConfigChange('cloudVesselsEnabled', e.target.checked)}
554
612
  />
555
- </div>
613
+ {t.cloudVesselsEnabled}
614
+ </label>
615
+ </div>
556
616
 
557
- <div style={styles.formGroup}>
558
- <label style={styles.label}>{t.cloudVesselsRadius}</label>
559
- <input
560
- type="number"
561
- min="1"
562
- value={config.cloudVesselsRadius || 10}
563
- onChange={(e) => handleConfigChange('cloudVesselsRadius', Number(e.target.value))}
564
- style={styles.input}
565
- />
566
- </div>
567
- </>
568
- )}
569
- </div>
617
+ {config.cloudVesselsEnabled !== false && (
618
+ <>
619
+ <div style={styles.formGroup}>
620
+ <label style={styles.label}>{t.cloudVesselsUpdateInterval}</label>
621
+ <input
622
+ type="number"
623
+ min="1"
624
+ value={config.cloudVesselsUpdateInterval || 60}
625
+ onChange={(e) => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value))}
626
+ style={styles.input}
627
+ />
628
+ </div>
629
+
630
+ <div style={styles.formGroup}>
631
+ <label style={styles.label}>{t.cloudVesselsRadius}</label>
632
+ <input
633
+ type="number"
634
+ min="1"
635
+ value={config.cloudVesselsRadius || 10}
636
+ onChange={(e) => handleConfigChange('cloudVesselsRadius', Number(e.target.value))}
637
+ style={styles.input}
638
+ />
639
+ </div>
640
+ </>
641
+ )}
642
+ </div>
643
+ )}
570
644
 
571
645
  {status && (
572
646
  <div style={{...styles.statusMessage, ...(status === 'error' ? styles.error : styles.success)}}>
@@ -579,8 +653,12 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
579
653
  <div style={styles.buttonGroup}>
580
654
  <button
581
655
  onClick={handleSave}
582
- disabled={loading || isMMSIInvalid()}
583
- style={{...styles.button, ...styles.primaryButton, ...(isMMSIInvalid() ? { opacity: 0.5, cursor: 'not-allowed' } : {})}}
656
+ disabled={loading || isMMSIInvalid() || isPortInvalid()}
657
+ style={{
658
+ ...styles.button,
659
+ ...styles.primaryButton,
660
+ ...((isMMSIInvalid() || isPortInvalid()) ? { opacity: 0.5, cursor: 'not-allowed' } : {})
661
+ }}
584
662
  >
585
663
  {t.save}
586
664
  </button>