UNPKG

signalk-parquet

Version:

Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.

2,021 lines (1,777 loc) 142 kB
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="referrer" content="origin" /> <title>SignalK Parquet Data Interface</title> <link rel="icon" type="image/png" href="parquet.png" /> <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html { overflow-x: hidden; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background-color: #f5f5f5; color: #333; line-height: 1.6; overflow-x: hidden; max-width: 100vw; } .container { max-width: 1400px; margin: 0 auto; padding: 20px; overflow-x: hidden; } .header { background: linear-gradient(135deg, #2bb5d6 0%, #013542 100%); color: white; padding: 20px 0; margin-bottom: 20px; border-radius: 10px; text-align: center; position: relative; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; margin: 0 0 10px 0; } .header p { font-size: 1.1rem; opacity: 0.9; } .section { background: white; padding: 20px; margin-bottom: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); overflow-x: hidden; max-width: 100%; } .section h2 { color: #667eea; margin-bottom: 15px; font-size: 1.5rem; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: 500; color: #555; } input, select, textarea { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 14px; transition: border-color 0.3s; } input:focus, select:focus, textarea:focus { outline: none; border-color: #667eea; } textarea { resize: vertical; min-height: 100px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; } button { background: #667eea; color: white; padding: 12px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; margin-right: 10px; margin-bottom: 10px; } button:hover { background: #5a6fd8; } button:disabled { background: #ccc; cursor: not-allowed; } .btn-secondary { background: #6c757d; } .btn-secondary:hover { background: #5a6268; } .results { margin-top: 20px; } .table-container { overflow-x: auto; -webkit-overflow-scrolling: touch; margin-top: 15px; max-width: 100%; } table { width: 100%; border-collapse: collapse; table-layout: auto; } th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; word-wrap: break-word; word-break: break-word; } /* Code elements in tables should wrap */ td code { word-break: break-all; white-space: normal; display: inline-block; max-width: 100%; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .edit-command-form-row td { padding: 0; background: transparent; } .loading { text-align: center; padding: 20px; color: #666; } .error { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px; border-radius: 5px; margin-top: 10px; } .success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 10px; border-radius: 5px; margin-top: 10px; } .path-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; margin-top: 15px; } .path-item { background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #667eea; } .path-item h4 { margin-bottom: 5px; color: #495057; } .path-item p { font-size: 0.9rem; color: #666; } .path-item button { margin-top: 10px; padding: 5px 10px; font-size: 12px; } .query-examples { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 15px; } .query-examples h4 { margin-bottom: 10px; color: #495057; } .query-examples ul { list-style: none; padding: 0; } .query-examples li { margin-bottom: 10px; padding: 10px; background: white; border-radius: 3px; cursor: pointer; transition: background-color 0.3s; } .query-examples li:hover { background: #e9ecef; } .query-examples code { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.9rem; } .stats { display: flex; justify-content: space-between; margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; } .stat-item { text-align: center; } .stat-value { font-size: 1.5rem; font-weight: bold; color: #667eea; } .stat-label { font-size: 0.9rem; color: #666; } .collapsible-header { cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; margin: -20px -20px 15px -20px; background: #f8f9fa; border-radius: 10px 10px 0 0; transition: background-color 0.3s; } .collapsible-header:hover { background: #e9ecef; } .collapsible-header h2 { margin: 0; } .collapse-icon { font-size: 1.2rem; transition: transform 0.3s; } .collapse-icon.collapsed { transform: rotate(-90deg); } .collapsible-content { overflow: hidden; transition: max-height 0.3s ease-out; } .collapsible-content.collapsed { max-height: 0; } .collapsible-content.expanded { max-height: 2000px; } /* Tab Navigation Styles */ .tab-navigation { display: flex; background: white; border-radius: 10px 10px 0 0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-bottom: 0; overflow-x: auto; } .tab-button { background: #f8f9fa; border: none; padding: 15px 20px; cursor: pointer; font-size: 14px; font-weight: 500; color: #666; border-radius: 0; transition: all 0.3s; white-space: nowrap; border-bottom: 3px solid transparent; margin: 0; } .tab-button:first-child { border-radius: 10px 0 0 0; } .tab-button:last-child { border-radius: 0 10px 0 0; } .tab-button:hover { background: #e9ecef; color: #333; } .tab-button.active { background: white; color: #667eea; border-bottom-color: #667eea; } .tab-content { background: white; border-radius: 0 0 10px 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-bottom: 20px; overflow-x: hidden; max-width: 100%; } .tab-panel { display: none; } .tab-panel.active { display: block; } .tab-panel .section { margin-bottom: 0; box-shadow: none; border-radius: 0 0 10px 10px; } /* Modal styles for responsiveness */ .modal-overlay { display: flex; align-items: flex-start; justify-content: center; padding: 20px; } .modal-content { position: relative; width: 100%; } /* Button groups for responsive layout */ .btn-group { display: flex; flex-wrap: wrap; gap: 8px; } .btn-group button { flex: 1 1 auto; } /* Responsive utility classes */ .hide-mobile { display: block; } .show-mobile { display: none; } .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ============================================ RESPONSIVE DESIGN - Mobile First Approach ============================================ */ /* Extra small devices (phones, 480px and down) */ @media (max-width: 480px) { .container { padding: 8px; } .header { padding: 15px 10px; border-radius: 8px; } .header img { position: static !important; height: 50px !important; margin-bottom: 10px; } .header h1 { font-size: 1.4rem; } .header p { font-size: 0.9rem; } .section { padding: 12px; border-radius: 8px; } .section h2 { font-size: 1.2rem; } .stats { flex-direction: column; gap: 8px; padding: 8px; } .stat-value { font-size: 1.2rem; } .stat-label { font-size: 0.8rem; } /* Tab navigation - horizontal scroll on very small screens */ .tab-navigation { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } .tab-button { flex: 0 0 auto; min-width: 100px; padding: 10px 12px; font-size: 12px; } .tab-button:first-child, .tab-button:last-child { border-radius: 0; } /* Buttons - full width on mobile */ button { width: 100%; margin-right: 0; padding: 14px 16px; font-size: 15px; min-height: 48px; /* Touch-friendly */ } .btn-group { display: flex; flex-direction: column; gap: 8px; } /* Form inputs - larger touch targets */ input, select, textarea { padding: 12px; font-size: 16px; /* Prevents iOS zoom */ min-height: 48px; } /* Tables - responsive on mobile */ .table-container { margin: 0 -8px; padding: 0; overflow-x: auto; max-width: calc(100% + 16px); } table { min-width: 500px; font-size: 12px; } th, td { padding: 8px 6px; font-size: 12px; } td code { font-size: 11px; } /* Hide less important columns on mobile */ th:nth-child(4), td:nth-child(4), /* Source */ th:nth-child(5), td:nth-child(5), /* Context */ th:nth-child(6), td:nth-child(6) { /* Exclude MMSI */ display: none; } /* Compact action buttons on mobile */ td button { padding: 6px 8px !important; font-size: 11px !important; margin: 2px !important; width: auto !important; min-height: 32px !important; } /* Path list - single column */ .path-list { grid-template-columns: 1fr; gap: 10px; } .path-item { padding: 12px; } /* Collapsible sections */ .collapsible-header { padding: 12px 15px; margin: -12px -12px 12px -12px; } /* Path tree */ .path-tree-container { max-height: 300px; } .path-tree-item { padding: 10px 8px; min-height: 44px; } /* Query examples */ .query-examples { padding: 10px; } .query-examples code { font-size: 0.8rem; word-break: break-all; } /* Utility class toggles for mobile */ .hide-mobile { display: none !important; } .show-mobile { display: block !important; } } /* Small devices (large phones, tablets portrait - 481px to 768px) */ @media (min-width: 481px) and (max-width: 768px) { .container { padding: 12px; } .header { padding: 15px; } .header img { height: 60px !important; left: 15px !important; top: 15px !important; } .header h1 { font-size: 1.8rem; } .section { padding: 15px; } .stats { flex-direction: column; gap: 10px; } .tab-navigation { flex-wrap: wrap; } .tab-button { flex: 1 1 calc(25% - 2px); min-width: 100px; padding: 12px 15px; font-size: 13px; } .tab-button:first-child, .tab-button:last-child { border-radius: 0; } /* Two column grid for path list */ .path-list { grid-template-columns: repeat(2, 1fr); } /* Table responsive for tablets */ .table-container { overflow-x: auto; } table { min-width: 600px; font-size: 13px; } th, td { padding: 8px; } /* Show more table columns on tablet */ th:nth-child(4), td:nth-child(4) { display: table-cell; } /* Hide context and exclude MMSI on tablet */ th:nth-child(5), td:nth-child(5), th:nth-child(6), td:nth-child(6) { display: none; } button { min-height: 44px; } input, select, textarea { font-size: 16px; } } /* Medium devices (tablets landscape, small laptops - 769px to 1024px) */ @media (min-width: 769px) and (max-width: 1024px) { .container { padding: 15px; } .header h1 { font-size: 2.2rem; } .stats { flex-wrap: wrap; justify-content: center; gap: 15px; } .stat-item { flex: 1 1 auto; min-width: 100px; } .path-list { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } /* Table on medium screens */ .table-container { overflow-x: auto; } table { min-width: 700px; } /* Hide exclude MMSI on medium screens */ th:nth-child(6), td:nth-child(6) { display: none; } } /* Large devices (desktops - 1025px to 1440px) */ @media (min-width: 1025px) and (max-width: 1440px) { .container { padding: 20px; } } /* Extra large devices (large desktops, TVs - 1441px and up) */ @media (min-width: 1441px) { .container { max-width: 1600px; } .header h1 { font-size: 3rem; } .section h2 { font-size: 1.8rem; } .stat-value { font-size: 2rem; } .path-list { grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); } } /* Landscape orientation for phones */ @media (max-height: 500px) and (orientation: landscape) { .header { padding: 10px; } .header img { height: 40px !important; } .header h1 { font-size: 1.5rem; } .header p { display: none; } .tab-navigation { position: sticky; top: 0; z-index: 100; } } /* High DPI / Retina display optimizations */ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .header img { image-rendering: -webkit-optimize-contrast; } } /* Print styles */ @media print { .tab-navigation, button, .header img { display: none; } .tab-panel { display: block !important; page-break-inside: avoid; } .section { box-shadow: none; border: 1px solid #ddd; } } /* Responsive modal styles */ .modal-responsive { margin: 20px !important; max-width: calc(100% - 40px) !important; max-height: calc(100vh - 40px) !important; } @media (max-width: 480px) { .modal-responsive, #contextPreviewModal > div, #analysisHistoryModal > div, #analysisViewModal > div, #thresholdModal > div { margin: 0 !important; max-width: 100% !important; width: 100% !important; max-height: 100vh !important; height: 100vh !important; border-radius: 0 !important; } } @media (max-width: 768px) { #contextPreviewModal > div, #analysisHistoryModal > div, #analysisViewModal > div, #thresholdModal > div { margin: 10px !important; max-width: calc(100% - 20px) !important; max-height: calc(100vh - 20px) !important; } } /* Touch-friendly scrollbars */ @media (pointer: coarse) { .table-container, .path-tree-container, .tab-navigation { scrollbar-width: thin; } .table-container::-webkit-scrollbar, .path-tree-container::-webkit-scrollbar, .tab-navigation::-webkit-scrollbar { height: 6px; width: 6px; } .table-container::-webkit-scrollbar-thumb, .path-tree-container::-webkit-scrollbar-thumb, .tab-navigation::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; } } /* Reduce motion for accessibility */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* Hierarchical Path Tree Styles */ .path-tree-container { border: 1px solid #ddd; border-radius: 4px; background: white; max-height: 400px; overflow-y: auto; padding: 8px; } .path-tree-item { padding: 4px 8px; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 4px; } .path-tree-item:hover { background: #f0f8ff; } .path-tree-item.selected { background: #d0e8ff; font-weight: 500; } .path-tree-item.has-value { color: #0066cc; } .path-tree-toggle { width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; } .path-tree-children { margin-left: 20px; display: none; } .path-tree-children.expanded { display: block; } .path-tree-search { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 8px; } .path-tree-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* ========== Map Explorer Styles ========== */ .me-layout { display: flex; gap: 12px; margin-bottom: 10px; } .me-layout-map { flex: 1; min-width: 0; } .me-map-container { height: 450px; width: 100%; border-radius: 8px; border: 2px solid #ddd; margin-bottom: 10px; z-index: 1; } .me-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 10px; padding: 8px; background: #f8f9fa; border-radius: 6px; } .me-toolbar button { padding: 6px 12px; font-size: 13px; margin: 0; } .me-toolbar button.me-active { background: #9c27b0; } .me-status-text { color: #666; font-size: 13px; margin-left: 8px; } .me-legend { display: flex; gap: 8px; overflow-x: auto; padding: 6px 0; margin-bottom: 6px; } .me-legend-chip { display: flex; align-items: center; gap: 4px; padding: 4px 10px; background: #f0f0f0; border-radius: 16px; font-size: 12px; cursor: pointer; white-space: nowrap; border: 2px solid transparent; user-select: none; } .me-legend-chip.me-hidden { opacity: 0.4; text-decoration: line-through; } .me-legend-chip.me-active-series { border-color: #333; } .me-color-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } .me-summary { padding: 6px 10px; background: #e8eaf6; border-radius: 4px; font-size: 12px; color: #333; margin-bottom: 10px; overflow-x: auto; white-space: nowrap; } .me-query-panel { padding: 8px 10px; background: #fafafa; overflow: hidden; border: 1px solid #e0e0e0; border-radius: 8px; display: flex; flex-direction: column; flex: 1; } .me-query-panel h3 { margin: 0 0 6px; font-size: 1rem; color: #555; flex-shrink: 0; } .me-query-grid { display: flex; flex-direction: column; gap: 6px; flex: 1; min-height: 0; } .me-query-col label { font-size: 12px; margin-bottom: 2px; } .me-query-col select, .me-query-col input { font-size: 12px; padding: 4px 6px; } .me-btn-group { display: flex; gap: 4px; flex-wrap: wrap; } .me-btn-group button { padding: 3px 8px; font-size: 11px; margin: 0; background: #e0e0e0; color: #333; } .me-btn-group button.me-active { background: #667eea; color: white; } .me-btn-group button:hover { background: #bbb; } .me-btn-group button.me-active:hover { background: #5a6fd8; } .me-date-range-picker { display: flex; align-items: center; border: 1px solid #ccc; border-radius: 4px; overflow: hidden; background: white; } .me-date-range-picker input[type="date"] { border: none; outline: none; padding: 5px 6px; font-size: 13px; flex: 1; min-width: 0; background: transparent; } .me-date-sep { color: #999; font-size: 14px; padding: 0 2px; user-select: none; } .me-path-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; } .me-path-chip { display: inline-flex; align-items: center; gap: 4px; background: #667eea; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; } .me-path-chip button { background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 0 2px; line-height: 1; } .me-path-filter { width: 100%; box-sizing: border-box; margin-bottom: 4px; } .me-path-list { max-height: 90px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; background: white; } .me-path-row { display: flex; align-items: center; gap: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; font-weight: normal; } .me-path-row:hover { background: #f0f0f0; } .me-path-row input[type="checkbox"], .me-path-row input[type="radio"] { width: auto; margin: 0; flex-shrink: 0; } .me-path-disabled { opacity: 0.4; cursor: default; } .me-loading { text-align: center; padding: 20px; } .me-results-panel { margin-top: 10px; } .me-results-tabs { display: flex; gap: 4px; margin-bottom: 8px; } .me-results-tab-btn { padding: 6px 14px; font-size: 13px; margin: 0; background: #e0e0e0; color: #333; } .me-results-tab-btn.me-active { background: #667eea; color: white; } .me-all-points { max-height: 350px; overflow: auto; } .me-table-scroll { overflow-x: auto; } .me-table-scroll table { font-size: 13px; } .me-table-scroll thead { position: sticky; top: 0; z-index: 1; } .me-selected-row { background: #fff9c4 !important; } .me-table-scroll tr { cursor: pointer; } .me-detail-header { padding: 8px 0; border-bottom: 1px solid #eee; margin-bottom: 8px; } .me-detail-value-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 14px; } .me-sparkline { height: 80px; margin-bottom: 6px; } .me-playback { padding: 8px 0; border-top: 1px solid #eee; margin-top: 8px; } .me-playback-controls { display: flex; gap: 6px; align-items: center; margin-top: 6px; } .me-btn-sm { padding: 4px 10px !important; font-size: 12px !important; margin: 0 !important; } .me-btn-danger { background: #e53935 !important; } .me-export { display: flex; gap: 6px; padding: 10px 0; border-top: 1px solid #eee; margin-top: 10px; } .me-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; } .me-modal { background: white; border-radius: 10px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; } .me-saved-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee; gap: 10px; } .me-badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; color: white; margin-left: 6px; } .me-badge-bbox { background: #7b1fa2; } .me-badge-radius { background: #0288d1; } .me-handle { cursor: grab; } @media (max-width: 600px) { .me-layout { flex-direction: column; } .me-query-panel { flex: none; max-height: none; } .me-map-container { height: 300px; } .me-toolbar { flex-direction: column; align-items: flex-start; } .me-summary { font-size: 11px; } } </style> </head> <body> <div class="container"> <div class="header"> <img src="parquet.png" alt="Parquet" style=" height: 80px; position: absolute; left: 20px; top: 20px; border-radius: 5px; " /> <h1 style="margin: 0 0 10px 0">SignalK Parquet Data Store</h1> <p> Export, query, and explore your SignalK data archived as Parquet files </p> </div> <!-- Tab Navigation --> <div class="tab-navigation"> <button class="tab-button active" onclick="showTab('pathConfig')"> ⚙️ Path Configuration </button> <button class="tab-button" onclick="showTab('commandManager')"> 🎮 Regimen/Commands Manager </button> <button class="tab-button" onclick="showTab('liveConnections')" style="display: none" > 🔴 Live Connections </button> <button id="queryDatabaseTab" class="tab-button" onclick="showTab('dataPaths')" > 🗄️ Query Database </button> <button class="tab-button" onclick="showTab('aiAnalysis')" style="display: none" > 🧠 AI Analysis <small>(EXPERIMENTAL)</small> </button> <button class="tab-button" onclick="showTab('dataValidation')" style="display: none" > 🔍 Data Validation </button> <button class="tab-button" onclick="showTab('mapExplorer')"> 🗺️ Map Explorer </button> <button class="tab-button" onclick="showTab('migration')"> 📊 Status </button> <button class="tab-button" onclick="showTab('s3Config')"> ☁️ Cloud Status </button> </div> <!-- Tab Content Panels --> <div class="tab-content"> <!-- Path Configuration Tab --> <div id="pathConfig" class="tab-panel active"> <div class="section"> <h2>⚙️ Path Configuration</h2> <p>Manage which SignalK paths to collect data from:</p> <!-- Home Port Configuration --> <div id="homePortConfig" style=" margin: 15px 0 20px 0; padding: 15px; background: #e8f5e9; border-radius: 8px; border: 2px solid #4caf50; " > <h3 style=" margin: 0 0 10px 0; color: #2e7d32; display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; " onclick="toggleHomePortCollapse()" > <span id="homePortToggle">▼</span> 🏠 Home Port Location <span style="font-size: 0.8em; font-weight: normal; color: #666" >(Used for proximity-based thresholds)</span > </h3> <div id="homePortDetails"> <div style=" display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end; " > <div> <label for="homePortLat" style=" display: block; margin-bottom: 5px; font-weight: 500; color: #333; " >Latitude:</label > <input type="number" id="homePortLat" step="0.000001" placeholder="e.g., 40.712800" style=" width: 100%; padding: 8px; border: 1px solid #4caf50; border-radius: 4px; " /> </div> <div> <label for="homePortLon" style=" display: block; margin-bottom: 5px; font-weight: 500; color: #333; " >Longitude:</label > <input type="number" id="homePortLon" step="0.000001" placeholder="e.g., -74.006000" style=" width: 100%; padding: 8px; border: 1px solid #4caf50; border-radius: 4px; " /> </div> <div> <button onclick="setCurrentLocationAsHomePort()" style=" background: #2196f3; padding: 8px 12px; white-space: nowrap; " > 📍 Use Current Position </button> </div> </div> <div style=" display: flex; gap: 10px; align-items: center; margin-top: 12px; " > <button onclick="saveHomePort()" style="background: #4caf50; padding: 8px 16px" > 💾 Save Home Port </button> <div id="homePortStatus" style="font-size: 0.9em"></div> </div> </div> </div> <button onclick="loadPathConfigurations()">🔄 Refresh Paths</button> <button onclick="showAddPathForm()">➕ Add New Path</button> <button id="toggleCommandsBtn" onclick="toggleCommandPaths()"> 👁️ Show Commands </button> <!-- Add Path Form (hidden by default) --> <div id="addPathForm" style=" display: none; margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 5px; min-height: 400px; overflow: visible; " > <h4>Add New Path Configuration</h4> <div class="form-group"> <label for="pathSignalK">SignalK Path:</label> <div style=" margin-bottom: 8px; display: flex; align-items: center; gap: 15px; " > <label style=" display: flex; align-items: center; font-weight: normal; cursor: pointer; white-space: nowrap; " > <span style="margin-right: 5px">🏠 Self paths</span> <input type="radio" id="pathFilterSelf" name="pathFilter" value="self" checked onchange="updatePathFilter()" /> </label> <label style=" display: flex; align-items: center; font-weight: normal; cursor: pointer; white-space: nowrap; " > <span style="margin-right: 5px">🚢 Other vessel paths</span> <input type="radio" id="pathFilterOthers" name="pathFilter" value="others" onchange="updatePathFilter()" /> </label> </div> <input type="text" id="pathSignalKSearch" class="path-tree-search" placeholder="🔍 Search paths..." /> <div id="pathSignalKTree" class="path-tree-container"></div> <input type="hidden" id="pathSignalK" /> <button type="button" onclick="showCustomPathInput('pathSignalK')" style=" margin-top: 5px; padding: 6px 12px; background: #666; color: white; border: none; border-radius: 4px; font-size: 0.9em; " > 🖊️ Enter Custom Path </button> <input type="text" id="pathSignalKCustom" placeholder="Enter custom SignalK path (e.g., navigation.position)" style=" width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 4px; display: none; " /> </div> <div class="form-group"> <label for="pathEnabled" style=" display: inline !important; margin-bottom: 0 !important; " >Always Enabled:</label > <input type="checkbox" id="pathEnabled" style=" display: inline !important; width: auto !important; margin-left: 10px; " /> </div> <div class="form-group"> <label for="pathRegimen">Regimen Control:</label> <div style=" border: 1px solid #ddd; border-radius: 4px; padding: 10px; max-height: 150px; overflow-y: auto; background: white; " > <div id="regimenCheckboxes"> <!-- Regimen checkboxes will be populated here --> </div> <div style=" margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee; " > <label for="customRegimen" style="font-size: 0.9em; color: #666" >Custom Regimen:</label > <input type="text" id="customRegimen" placeholder="Enter custom regimen name" style=" width: 100%; padding: 6px; margin-top: 5px; border: 1px solid #ddd; border-radius: 3px; " /> <button type="button" onclick="addCustomRegimen()" style=" margin-top: 5px; padding: 4px 8px; background: #28a745; color: white; border: none; border-radius: 3px; font-size: 0.8em; " > ➕ Add </button> </div> </div> </div> <div class="form-group"> <label for="pathContext">Context:</label> <input type="text" id="pathContext" placeholder="e.g., vessels.self or vessels.*" value="vessels.self" /> </div> <div class="form-group"> <label for="pathSource">Source Filter (optional):</label> <input type="text" id="pathSource" placeholder="e.g., mqtt-weatherflow-udp" /> <small style="color: #666; display: block; margin-top: 5px"> Filter by specific data source. Leave empty to accept all sources. </small> </div> <div class="form-group"> <label for="pathExcludeMMSI" >Exclude MMSI (for vessels.* context):</label > <input type="text" id="pathExcludeMMSI" placeholder="e.g., 123456789, 987654321 (comma-separated)" /> <small style="color: #666; display: block; margin-top: 5px"> Only applicable when context is "vessels.*". Leave empty to include all vessels. </small> </div> <div style=" margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd; min-height: 60px; display: flex; gap: 10px; align-items: center; " > <button onclick="addPathConfiguration()">✅ Add Path</button> <button class="btn-secondary" onclick="hideAddPathForm()"> ❌ Cancel </button> </div> </div> <div id="pathConfigContainer"> <div class="loading">Loading path configurations...</div> </div> </div> </div> <!-- Command Manager Tab --> <div id="commandManager" class="tab-panel"> <div class="section"> <h2>🎮 Regimen/Commands Manager</h2> <p>Register and manage SignalK commands for remote control:</p> <button onclick="showAddCommandForm()">➕ Add New Command</button> <!-- Add Command Form (hidden by default) --> <div id="addCommandForm" style=" display: none; margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 5px; " > <h4>Register New Command</h4> <div class="form-group"> <label for="commandName">Command Name:</label> <input type="text" id="commandName" placeholder="e.g., captureWeather, startLogging" /> </div> <div class="form-group"> <label for="commandDescription">Description (optional):</label> <input type="text" id="commandDescription" placeholder="e.g., Start weather data capture" /> </div> <div class="form-group"> <label for="commandKeywords" >Keywords (for Claude context matching):</label > <input type="text" id="commandKeywords" placeholder="e.g., anchor, anchoring, drag, swing" /> <small style="color: #666; font-size: 11px" >Separate multiple keywords with commas</small > </div> <!-- Default State is now hardcoded to OFF on server side --> <!-- Threshold Configuration --> <div style=" margin-top: 20px; padding: 15px; background: #f0f8ff; border: 1px solid #b3d9ff; border-radius: 5px; " > <h5 style="margin-bottom: 15px; color: #0066cc"> 🎯 Threshold-Based Activation </h5> <div id="addCommandThresholdsList"> <!-- New thresholds will be populated here --> </div> <button type="button" id="addCommandThresholdButton" onclick="addNewCommandThreshold()" style=" background: #4caf50; color: white; border: none; padding: 8px 12px; border-radius: 4px; margin-top: 10px; " > ➕ Add Threshold </button> <!-- Threshold form replaced by unified modal --> </div> <div style=" margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd; " > <button onclick="registerCommand()">✅ Register Command</button> <button class="btn-secondary" onclick="hideAddCommandForm()"> ❌ Cancel </button> </div> </div> <!-- Edit Command Form (hidden by default) --> <div id="editCommandForm" style=" display: none; margin: 20px 0; padding: 20px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; width: 100%; box-sizing: border-box; " > <h4>Edit Command</h4> <div class="form-group"> <label for="editCommandName">Command Name:</label> <input type="text" id="editCommandName" readonly style="background: #f8f9fa" /> </div> <div class="form-group"> <label for="editCommandDescription">Description:</label> <input type="text" id="editCommandDescription" placeholder="e.g., Start weather data capture" /> </div> <div class="form-group"> <label for="editCommandKeywords" >Keywords (for Claude context matching):</label > <input type="text" id="editCommandKeywords" placeholder="e.g., anchor, anchoring, drag, swing" /> <small style="color: #666; font-size: 11px" >Separate multiple keywords with commas</small > </div> <!-- Default State is now hardcoded to OFF on server side --> <!-- Threshold Configuration --> <div style=" margin-top: 20px; padding: 15px; background: #f0f8ff; border: 1px solid #b3d9ff; border-radius: 5px; " > <h5 style="margin-bottom: 15px; color: #0066cc"> 🎯 Threshold-Based Activation </h5> <div id="threshold