signalk-parquet
Version:
Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.
4,533 lines • 142 kB
HTML
<!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="thresholdsList">
<!-- Existing thresholds will be populated here -->
</div>
<button
type="button"
id="editCommandAddThresholdButton"
onclick="addNewThreshold()"
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;
display: flex;
flex-direction: column;
gap: 10px;
"
>
<div style="display: flex; gap: 10px; align-items: center">
<button
id="updateCommandButton"
onclick="updateCommand()"
disabled
style="opacity: 0.7"
>
✅ Update Command
</button>
<button class="btn-secondary" onclick="hideEditCommandForm()">
❌ Cancel
</button>
</div>
<div
id="editCommandStatus"
style="font-size: 0.85em; color: #0066cc; display: none"
></div>
</div>
</div>
<div id="commandContainer">
<div class="loading">Loading commands...</div>
</div>
<!-- Command History -->
<div style="margin-top: 30px">
<h3>📋 Command History</h3>
<button onclick="loadCommandHistory()">🔄 Refresh History</button>
<div id="commandHistoryContainer" style="margin-top: 15px">
<div class="loading">Loading command history...</div>
</div>
</div>
</div>
</div>
<!-- Live Connections Tab -->
<div id="liveConnections" class="tab-panel">
<div class="section" style="display: none">
<h2>🔴 Live Connections - DISABLED</h2>
<p>Streaming functionality has been disabled.</p>
<div style="margin-bottom: 20px">
<button onclick="loadStreams()">🔄 Refresh Streams</button>
<button onclick="showAddStreamForm()">
➕ Create New Stream
</button>
</div>
<!-- Add Stream Form (hidden by default) -->
<div
id="addStreamForm"
style="
display: none;
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 5px;
"
>
<h4>Create New Historical Stream</h4>
<div class="form-group">
<label for="streamName">Stream Name:</label>
<input
type="text"
id="streamName"
placeholder="e.g., Navigation History"
/>
</div>
<div class="form-group">
<label for="streamPath">SignalK Path:</label>
<div style="display: flex; gap: 10px; align-items: center">
<select id="streamPath" style="flex: 1">
<option value="">Loading available paths...</option>
</select>
<button
type="button"
onclick="refreshSignalKPaths()"
style="
padding: 8px;
background: #007bff;
color: white;
border: none;
border-radius: 3px;
"
title="Refresh path list"
>
🔄
</button>
</div>
<small style="color: #666; display: block; margin-top: 5px">
Available paths from historical data. Click refresh if you
don't see your path.
</small>
</div>
<div class="form-group">
<label for="streamTimeRange">Time Range:</label>
<select id="streamTimeRange">
<option value="1h">Last 1 Hour</option>
<option value="6h">Last 6 Hours</option>
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div
class="form-group"
id="customTimeRange"
style="display: none"
>
<label>Custom Time Range:</label>
<div
style="
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
"
>
<input
type="datetime-local"
id="streamStartTime"
placeholder="Start time"
/>
<input
type="datetime-local"
id="streamEndTime"
placeholder="End time"
/>
</div>
</div>
<div class="form-group">
<label for="streamResolution">Data Resolution:</label>
<select id="streamResolution">
<option value="1000">1 second</option>
<option value="5000">5 seconds</option>
<option value="15000">15 seconds</option>
<option value="30000" selected>30 seconds</option>
<option value="60000">1 minute</option>
<option value="300000">5 minutes</option>
</select>
</div>
<div class="form-group">
<label for="streamRate">Streaming Rate:</label>
<select id="streamRate">
<option value="100">Fast (100ms intervals)</option>
<option value="500" selected>Normal (500ms intervals)</option>
<option value="1000">Slow (1 second intervals)</option>
<option value="5000">Very Slow (5 second intervals)</option>
</select>
</div>
<div class="form-group">
<label for="streamAggregateMethod">Statistical Function:</label>
<select id="streamAggregateMethod">
<option value="average" selected>
Average - arithmetic mean of values
</option>
<option value="min">
Minimum - lowest value in time window
</option>
<option value="max">
Maximum - highest value in time window
</option>
<option value="first">
First - earliest value in time window
</option>
<option value="last">
Last - most recent value in time window
</option>
<option value="mid">Median - middle value when sorted</option>
<option value="middle_index">
Middle Index - middle value by position
</option>
</select>
<small style="color: #666; display: block; margin-top: 5px">
Statistical function applied to data within each time window
</small>
</div>
<div class="form-group">
<label for="streamWindowSize">Rolling Window Size:</label>
<select id="streamWindowSize">
<option value="5">5 data points</option>
<option value="10" selected>10 data points</option>
<option value="20">20 data points</option>
<option value="50">50 data points</option>
<option value="100">100 data points</option>
</select>
<small style="color: #666; display: block; margin-top: 5px">
Number of recent values used for real-time statistical
calculations
</small>
</div>
<div class="form-group">
<label for="streamAutoStart">Auto-start stream:</label>
<input type="checkbox" id="streamAutoStart" checked />
<small style="color: #666; display: block; margin-top: 5px">
Start streaming immediately after creation
</small>
</div>
<div
style="
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #ddd;
"
>
<button onclick="createStream()">✅ Create Stream</button>
<button class="btn-secondary" onclick="hideAddStreamForm()">
❌ Cancel
</button>
</div>
</div>
<!-- Active Streams Display -->
<div id="streamsContainer">
<div class="loading">Loading streams...</div>
</div>
<!-- Stream Statistics -->
<div
style="
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
"
>
<h3>📊 Stream Statistics</h3>
<div id="streamStats" style="margin-top: 15px">
<div class="loading">Loading statistics...</div>
</div>
</div>
<!-- Live Data Display -->
<div
style="
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
"
>
<h3>📡 Live Data Stream</h3>
<p style="margin-bottom: 15px">
Real-time time-bucketed statistical streaming data (newest
first, max 100 entries):
</p>
<!-- Data Interpretation Guide -->
<div
style="
background: #f0f8ff;
padding: 15px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 13px;
"
>
<strong>📊 How to Read This Data:</strong><br />
• <strong>📊 INITIAL</strong>: Full time window data (first load
with many buckets)<br />
• <strong>📈 INCREMENTAL</strong>: New data points since last
update (sliding window - fewer buckets)<br />
• <strong>Statistical Method</strong>: MAX, AVG, MIN, FIRST,
LAST, MID applied to each time bucket<br />
• <strong>Buckets</strong>: Number of time intervals processed
(e.g., 30-second time buckets)<br />
• <strong>Value</strong>: Result of statistical calculation for
that time bucket<br />
• <strong>EMA/SMA</strong>: Moving averages (10-period)
calculated for numeric values - EMA responds faster to
changes<br />
• <strong>Time</strong>: Timestamp of the time bucket (not
delivery time)
</div>
<div
style="
max-height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 5px;
"
>
<table
id="liveDataTable"
style="
width: 100%;
border-collapse: collapse;
font-size: 12px;
"
>
<thead
style="
position: sticky;
top: 0;
background: #f8f9fa;
z-index: 1;
"
>
<tr>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
Bucket Time
</th>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
Stream Type & Statistical Method
</th>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
Path
</th>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
Statistical Value
</th>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
EMA (10)
</th>
<th
style="
padding: 8px;
border-bottom: 1px solid #ddd;
text-align: left;
"
>
SMA (10)
</th>
</tr>
</thead>
<tbody id="liveDataBody">
<tr>
<td
colspan="6"
style="padding: 20px; text-align: center; color: #666"
>
No data streaming yet. Start a stream to see live data.
</td>
</tr>
</tbody>
</table>
</div>
<div style="margin-top: 10px; font-size: 11px; color: #666">
<span id="liveDataCount">0</span> time-bucketed entries •
<button
onclick="clearLiveData()"
style="font-size: 11px; padding: 2px 8px"
>
🗑️ Clear
</button>
<button
onclick="toggleLiveDataPause()"
id="pauseDataBtn"
style="font-size: 11px; padding: 2px 8px; margin-left: 5px"
>
⏸️ Pause
</button>
<button
onclick="showDataSummary()"
style="font-size: 11px; padding: 2px 8px; margin-left: 5px"
>
📊 Summary
</button>
</div>
<!-- Data Summary Panel -->
<div
id="dataSummaryPanel"
style="
display: none;
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
"
>
<h4>📊 Data Summary & Trends</h4>
<div id="dataSummaryContent"></div>
</div>
</div>
</div>
</div>
<!-- AI Analysis Tab -->
<div id="aiAnalysis" class="tab-panel">
<div class="section">
<h2>🧠 AI Analysis with Claude</h2>
<small>EXPERIMENTAL</small>
<p>
Analyze your maritime data using Claude AI for insights, patterns,
and recommendations.
</p>
<!-- Connection Test -->
<div style="margin-bottom: 20px">
<button onclick="testClaudeConnection()" id="testConnectionBtn">
🔗 Test Claude Connection
</button>
<div id="claudeConnectionResult" style="margin-top: 10px"></div>
</div>
<!-- Vessel Context Section -->
<div
style="
margin-bottom: 30px;
border: 2px solid #e3f2fd;
border-radius: 8px;
background: #fafbfc;
"
>
<div
onclick="toggleVesselContext()"
style="
padding: 15px 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
"
>
<div>
<h3 style="margin: 0">⚓ Vessel Context Document</h3>
<p style="margin: 5px 0 0 0; color: #555; font-size: 0.9em">
Click to expand - context automatically included with every
AI analysis
</p>
</div>
<span
id="vesselContextToggleIcon"
style="font-size: 1.2em; color: #667eea"
>▶</span
>
</div>
<div
id="vesselContextContent"
style="
display: none;
padding: 0 20px 20px 20px;
border-top: 1px solid #e3f2fd;
"
>
<!-- Auto-extracted vessel info -->
<div style="margin-bottom: 20px">
<h4
style="
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<span>📊 Auto-Extracted Vessel Information</span>
<button
onclick="refreshVesselInfo()"
style="font-size: 0.9em; padding: 6px 12px"
>
🔄 Refresh from SignalK
</button>
</h4>
<div
id="autoVesselInfo"
style="
background: white;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<div style="color: #666; font-style: italic">
Loading vessel information...
</div>
</div>
</div>
<!-- Custom context -->
<div style="margin-bottom: 20px">
<h4 style="margin-bottom: 10px">✏️ Custom Context & Notes</h4>
<p style="font-size: 0.9em; color: #666; margin-bottom: 10px">
Add any additional context about your vessel, operations, or
specific information you want Claude to consider during
analysis:
</p>
<textarea
id="customVesselContext"
placeholder="e.g., 'This is a racing sailboat primarily used for coastal sailing. The vessel has recently had engine maintenance. We're particularly interested in wind performance analysis...'"
style="
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
resize: vertical;
font-family: inherit;
"
></textarea>
</div>
<!-- Context preview and actions -->
<div
style="
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
"
>
<button
onclick="saveVesselContext()"
style="background: #4caf50; padding: 8px 16px"
>
💾 Save Context
</button>
<button
onclick="previewClaudeContext()"
style="background: #2196f3; padding: 8px 16px"
>
👁️ Preview Claude Context
</button>
<div
id="vesselContextStatus"
style="margin-left: 10px; font-size: 0.9em"
></div>
</div>
<!-- Context preview modal -->
<div
id="contextPreviewModal"
class="modal-overlay"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow-y: auto;
"
>
<div
class="modal-content"
style="
max-width: 800px;
margin: 50px auto;
background: white;
border-radius: 10px;
padding: 20px;
max-height: 80vh;
overflow-y: auto;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
"
>
<h3>👁️ Claude Context Preview</h3>
<button
onclick="closeContextPreview()"
style="
background: #f44336;
color: white;
padding: 5px 10px;
border-radius: 3px;
"
>
×
</button>
</div>
<p style="color: #666; margin-bottom: 15px">
This is the context that will be sent to Claude with every
analysis:
</p>
<pre
id="contextPreviewContent"
style="
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
white-space: pre-wrap;
font-family:
'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9em;
line-height: 1.4;
max-height: 400px;
overflow-y: auto;
"
></pre>
<div
style="
margin-top: 15px;
padding: 10px;
background: #e8f5e8;
border-radius: 5px;
border-left: 4px solid #4caf50;
"
>
<small
><strong>💡 Tip:</strong> A good vessel context helps
Claude provide more accurate and relevant insights for
your specific vessel and operations.</small
>
</div>
</div>
</div>
</div>
</div>
<!-- Custom Analysis -->
<div style="margin-bottom: 30px">
<h3>🎯 Custom Analysis</h3>
<div style="margin-top: 15px">
<!-- Analysis Mode Toggle with Model Selector -->
<div
style="
margin-bottom: 20px;
padding: 15px;
border: 2px solid #e3f2fd;
border-radius: 8px;
background: #f8f9ff;
"
>
<div style="margin-bottom: 10px">
<label
style="
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
font-weight: bold;
"
>
<span>🚀 Direct Database Access</span>
<input
type="checkbox"
id="enableDatabaseAccess"
checked
onchange="toggleAnalysisMode()"
style="transform: scale(1.2)"
/>
</label>
</div>
<div style="font-size: 0.9em; color: #555">
<div id="legacyModeDesc" style="color: #666; display: none">
<strong>Legacy:</strong> Pre-load data samples for
analysis (faster, limited scope)
</div>
<div id="databaseModeDesc" style="color: #1976d2">
<strong>Enhanced:</strong> Claude explores your database
interactively (more powerful, complete historical access)
</div>
</div>
<!-- Claude Model Selection - moved here -->
<div style="margin-top: 15px">
<label for="claudeModelMain">Claude Model:</label>
<select
id="claudeModelMain"
style="width: 100%; padding: 8px; margin-top: 5px"
>
<option value="claude-sonnet-4-20250514" selected>
Claude Sonnet 4 - Latest Sonnet model (recommended)
</option>
<option value="claude-opus-4-1-20250805">
Claude Opus 4.1 - Most powerful model (premium)
</option>
<option value="claude-opus-4-20250514">
Claude Opus 4 - High performance model (premium)
</option>
</select>
<p style="font-size: 0.9em; color: #666; margin: 5px 0 0 0">
Choose the Claude model for analysis. Sonnet provides the
most detailed insights.
</p>
</div>
</div>
<!-- Collapsible Selection Options -->
<div id="selectionOptionsSection" style="margin-bottom: 20px">
<div
onclick="toggleSelectionOptions()"
style="
cursor: pointer;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
user-select: none;
"
>
<span id="selectionToggleIcon" style="margin-right: 8px"
>▶</span
>
<strong>Selection Options</strong>
<span
id="selectionToggleSubtext"
style="font-size: 0.9em; color: #666; margin-left: 10px"
>(Advanced options for legacy mode)</span
>
</div>
<div
id="selectionOptionsContainer"
style="
display: none;
border: 1px solid #dee2e6;
border-top: none;
border-radius: 0 0 5px 5px;
padding: 15px;
background: #f9f9f9;
"
>
<!-- Data Path Selection -->
<div style="margin-bottom: 15px">
<label>Data Paths (select one or more):</label>
<div
id="analysisDataPathContainer"
style="
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-top: 5px;
background: white;
"
>
<div style="color: #666; font-style: italic">
Loading available paths...
</div>
</div>
<style>
.path-checkbox-row {
display: flex;
align-items: center;
padding: 8px 8px 8px 5px;
margin: 2px 0;
border-radius: 4px;
}
.path-checkbox-row:nth-child(odd) {
background-color: #f8f9fa;
}
.path-checkbox-row:nth-child(even) {
background-color: white;
}
.path-checkbox-row:hover {
background-color: #e3f2fd;
}
.path-info {
display: flex;
align-items: center;
flex-grow: 1;
}
.path-icon {
margin-right: 8px;
font-size: 16px;
}
.path-checkbox {
margin-left: auto;
width: 16px;
height: 16px;
flex-shrink: 0;
}
</style>
<div
style="
margin-top: 5px;
font-size: 0.9em;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
"
>
<span
><span id="selectedPathCount">0</span> path(s)
selected</span
>
<div>
<button
onclick="selectAllPaths()"
style="
font-size: 0.8em;
padding: 2px 6px;
margin-right: 5px;
"
>
Select All
</button>
<button
onclick="clearAllPaths()"
style="font-size: 0.8em; padding: 2px 6px"
>
Clear All
</button>
</div>
</div>
</div>
<!-- Date Range Selection -->
<div
style="
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
"
>
<div>
<label for="analysisStartDate">Start Date:</label>
<input
type="datetime-local"
id="analysisStartDate"
style="width: 100%; padding: 8px; margin-top: 5px"
/>
</div>
<div>
<label for="analysisEndDate">End Date:</label>
<input
type="datetime-local"
id="analysisEndDate"
style="width: 100%; padding: 8px; margin-top: 5px"
/>
</div>
</div>
<!-- Aggregation Method -->
<div style="margin-bottom: 15px">
<label for="aggregationMethod">Aggregation Method:</label>
<select
id="aggregationMethod"
style="width: 100%; padding: 8px; margin-top: 5px"
>
<option value="average" selected>
Average - Mean values over time buckets
</option>
<option value="min">
Min - Minimum values over time buckets
</option>
<option value="max">
Max - Maximum values over time buckets
</option>
<option value="first">
First - First value in each time bucket
</option>
<option value="last">
Last - Last value in each time bucket
</option>
<option value="mid">
Median - Middle values over time buckets
</option>
<option value="multiple">
Multiple - Get min, max, and average
</option>
</select>
</div>
<!-- Time Resolution -->
<div style="margin-bottom: 15px">
<label for="resolution">Time Resolution:</label>
<select
id="resolution"
style="width: 100%; padding: 8px; margin-top: 5px"
>
<option value="" selected>
Auto - Automatically determine best resolution
</option>
<option value="1m">1 minute - Fine-grained data</option>
<option value="5m">5 minutes - Detailed data</option>
<option value="15m">
15 minutes - Regular sampling
</option>
<option value="1h">1 hour - Hourly summaries</option>
<option value="1d">1 day - Daily summaries</option>
</select>
</div>
</div>
</div>
<div style="margin-bottom: 15px">
<label for="customPrompt"
>Custom Analysis Request (optional):</label
>
<textarea
id="customPrompt"
placeholder="What specific insights are you looking for? Leave blank for general analysis..."
style="
width: 100%;
padding: 8px;
margin-top: 5px;
min-height: 180px;
resize: vertical;
"
></textarea>
</div>
<button
onclick="runCustomAnalysis()"
style="padding: 12px 25px; margin-top: 15px"
>
🚀 Run Custom Analysis
</button>
</div>
</div>
<!-- Analysis Results -->
<div id="analysisResults" style="display: none">
<h3>📊 Analysis Results</h3>
<div
id="analysisContent"
style="
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 15px;
"
>
<!-- Results will appear here -->
</div>
<!-- Follow-up Questions -->
<div
id="followUpSection"
style="
display: none;
margin-top: 20px;
padding: 15px;
border: 2px solid #e8f4f8;
border-radius: 8px;
background: #f8fdff;
"
>
<h4 style="margin: 0 0 10px 0; color: #1976d2">
💬 Ask Follow-up Questions
</h4>
<p style="font-size: 0.9em; color: #666; margin: 0 0 15px 0">
Continue the conversation to explore your data deeper.
</p>
<div style="display: flex; gap: 10px; align-items: flex-end">
<div style="flex: 1">
<textarea
id="followUpQuestion"
placeholder="Ask Claude to explore further... (e.g., 'Show me wind patterns during high waves' or 'What happened around 3 PM?')"
style="
width: 100%;
padding: 8px;
min-height: 40px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
"
></textarea>
</div>
<button
id="askFollowUpBtn"
onclick="askFollowUpQuestion()"
style="
padding: 8px 15px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
"
>
💬 Ask
</button>
</div>
</div>
</div>
<!-- Analysis History -->
<div style="margin-top: 30px">
<h3>📈 Analysis History</h3>
<button onclick="openAnalysisHistoryModal()" id="loadHistoryBtn">
📈 View Analysis History
</button>
<p style="font-size: 0.9em; color: #666; margin-top: 10px">
View and manage your previous Claude AI analyses.
</p>
</div>
</div>
</div>
<!-- Data Validation Tab -->
<div id="dataValidation" class="tab-panel">
<div class="section">
<h2>🔍 Data Validation</h2>
<p>
Validate parquet file schemas against SignalK metadata and schema
guidelines:
</p>
<div style="margin-bottom: 10px">
<button onclick="runDataValidation()" id="validateBtn">
🔍 Run Schema Validation
</button>
<button
onclick="repairSchemas()"
id="repairBtn"
style="margin-left: 10px"
>
🔧 Repair Schema Violations
</button>
</div>
<div
id="processTimer"
style="
background: rgb(227, 242, 253);
border: 1px solid rgb(33, 150, 243);
border-radius: 5px;
padding: 10px;
margin-bottom: 15px;
text-align: center;
font-family: monospace;
display: none;
"
>
<strong id="processIcon">🔍 Validation Timer</strong><br />
<span id="processStartTime">Process started: --:--:--</span><br />
<span id="processElapsed">Elapsed: 0.0s</span><br />
<span id="processProgress">Files processed: 0 of 0 (0%)</span>
</div>
<div id="validationStatus" style="margin-top: 10px"></div>
<div id="validationResults" style="margin-top: 15px; display: none">
<h3 id="validationSummaryHeader">Validation Summary</h3>
<div
id="validationSummary"
style="
margin-bottom: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
"
></div>
<h3 id="validationDetailsHeader">Schema Violations</h3>
<div
id="validationDetails"
style="
max-height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
background: white;
font-family: monospace;
font-size: 12px;
"
></div>
</div>
</div>
</div>
<!-- Cloud Configuration Tab -->
<div id="s3Config" class="tab-panel">
<div class="section">
<h2>☁️ Cloud Status</h2>
<p>
Test your cloud storage connection (Amazon S3 or Cloudflare R2)
and sync files:
</p>
<button onclick="testCloudConnection()">
🔗 Test Cloud Connection
</button>
<div id="cloudTestResult" style="margin-top: 10px"></div>
<div
style="
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
"
>
<h4>📋 Key Prefix Explanation</h4>
<p>
<strong>Key Prefix</strong> organizes your uploaded files in the
bucket with a directory structure:
</p>
<ul style="margin: 10px 0; padding-left: 20px">
<li>
<strong>Without prefix:</strong> Files upload as
<code
>tier=raw/context=vessels__urn.../path=navigation__position/year=2025/day=193/file.parquet</code
>
</li>
<li>
<strong>With prefix "marine-data":</strong> Files upload as
<code
>marine-data/tier=raw/context=vessels__urn.../path=navigation__position/...</code
>
</li>
<li>
<strong>With prefix "boat-123/":</strong> Files upload as
<code
>boat-123/tier=raw/context=vessels__urn.../path=navigation__position/...</code
>
</li>
</ul>
<p>
This allows organizing multiple vessels or data sources in the
same bucket. Works identically for S3 and R2.
</p>
</div>
<!-- Cloud Sync Section -->
<div
style="
margin-top: 25px;
padding: 20px;
background: #e8f5e9;
border-radius: 8px;
border: 2px solid #4caf50;
"
>
<h3 style="margin-top: 0; color: #2e7d32">
🔄 Cloud Sync Status
</h3>
<p style="color: #666; margin-bottom: 15px">
Compare local files with cloud storage and sync missing files.
</p>
<button onclick="compareCloudFiles()" style="background: #2196f3">
📊 Compare Local vs Cloud
</button>
<button
onclick="syncToCloud()"
style="background: #4caf50; margin-left: 10px"
id="syncToCloudBtn"
disabled
>
📤 Sync Missing Files
</button>
<div id="cloudCompareResult" style="margin-top: 15px"></div>
</div>
</div>
</div>
<!-- Data Paths Tab -->
<div id="dataPaths" class="tab-panel">
<div class="section">
<h2>🗄️ Query Database</h2>
<p>Available SignalK data paths with Parquet files:</p>
<div id="availablePaths">
<div class="loading">Loading available paths...</div>
</div>
<!-- Query Section for Data Paths -->
<div
style="
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
"
>
<h3>Query Data</h3>
<div class="form-group">
<label for="dataPathsQueryInput">DuckDB SQL Query:</label>
<textarea
id="dataPathsQueryInput"
placeholder="SELECT * FROM read_parquet('path/to/file.parquet', union_by_name=true) LIMIT 10"
></textarea>
</div>
<button onclick="executeDataPathsQuery()">
▶️ Execute Query
</button>
<button class="btn-secondary" onclick="clearDataPathsQuery()">
🗑️ Clear
</button>
<div id="dataPathsResultsContainer" class="results">
<p>Run a query to see results here...</p>
</div>
</div>
</div>
</div>
<!-- Map Explorer Tab -->
<div id="mapExplorer" class="tab-panel">
<div class="section">
<h2>🗺️ Map Explorer</h2>
<p>
Spatial querying and visualization of historical SignalK data.
Draw a search area, configure your query, and explore results on
the map.
</p>
<!-- Toolbar -->
<div class="me-toolbar">
<button onclick="meStartBboxDraw()" title="Draw bounding box">
◻ Draw Bbox
</button>
<button onclick="meStartRadiusDraw()" title="Draw radius circle">
◯ Draw Radius
</button>
<button onclick="meShowSavedAreasModal()">📁 Saved Areas</button>
<button onclick="meShowSaveAreaDialog()">💾 Save Area</button>
<button
id="me-osm-toggle"
onclick="meToggleOpenSeaMap()"
title="Toggle OpenSeaMap nautical overlay"
>
⚓ OpenSeaMap
</button>
<span id="me-status" class="me-status-text"></span>
<div id="me-export-btns" class="me-export" style="display: none; margin-left: auto;">
<button onclick="meExportCSV()" class="me-btn-sm">📄 CSV</button>
<button onclick="meExportGeoJSON()" class="me-btn-sm">🌍 GeoJSON</button>
<button onclick="meExportKML()" class="me-btn-sm">📍 KML</button>
<span style="border-left: 1px solid #ccc; margin: 0 4px"></span>
<button onclick="meSaveAsWaypoint()" class="me-btn-sm">📌 Waypoint</button>
<button onclick="meSaveAsTrack()" class="me-btn-sm">🛤️ Track</button>
<button onclick="meSaveAsRoute()" class="me-btn-sm">🧭 Route</button>
</div>
</div>
<!-- Map + Query Config side by side -->
<div class="me-layout">
<!-- Left: Map -->
<div class="me-layout-map">
<div id="me-map" class="me-map-container"></div>
<!-- Legend -->
<div id="me-legend" class="me-legend"></div>
<!-- Summary Bar -->
<div id="me-summary" class="me-summary"></div>
</div>
<!-- Right: Query Configuration -->
<div style="display: flex; flex-direction: column; flex: 0 0 340px;">
<div class="me-query-panel">
<h3>Query Configuration</h3>
<div class="me-query-grid">
<!-- Time Range -->
<div class="me-query-col">
<label>Time Range</label>
<div id="me-time-toggle" class="me-btn-group">
<button
class="me-active"
data-mode="lookback"
onclick="meSetTimeMode('lookback')"
>
Lookback
</button>
<button
data-mode="range"
onclick="meSetTimeMode('range')"
>
Date Range
</button>
</div>
<div
id="me-lookback-btns"
class="me-btn-group"
style="margin-top: 4px"
>
<button data-days="1" onclick="meSetLookback(1)">1d</button>
<button data-days="3" onclick="meSetLookback(3)">3d</button>
<button
data-days="7"
onclick="meSetLookback(7)"
class="me-active"
>
7d
</button>
<button data-days="14" onclick="meSetLookback(14)">
14d
</button>
<button data-days="30" onclick="meSetLookback(30)">
30d
</button>
</div>
<div id="me-date-range" style="display: none; margin-top: 4px;">
<div class="me-date-range-picker">
<input type="date" id="me-from" onchange="meOnDateRangeChange()" />
<span class="me-date-sep">–</span>
<input type="date" id="me-to" onchange="meOnDateRangeChange()" />
</div>
</div>
</div>
<!-- Context -->
<div class="me-query-col">
<label>Context</label>
<div>
<label style="display: inline; font-weight: normal">
<input
type="checkbox"
id="me-lookup-contexts"
onchange="meLookupContexts()"
style="width: auto"
/>
Look up other vessels
</label>
</div>
<div id="me-context-select" style="display: none">
<div id="me-context-count" style="font-size: 11px; color: #666; margin-bottom: 2px;"></div>
<input
type="text"
id="me-context-filter"
class="me-path-filter"
placeholder="Filter vessels..."
oninput="meFilterContexts(this.value)"
/>
<div id="me-context-list" class="me-path-list" style="max-height: 90px;"></div>
</div>
</div>
<!-- Paths -->
<div class="me-query-col" id="me-path-selector">
<label>Paths (up to 3)</label>
<div class="me-path-chips"></div>
<input
type="text"
class="me-path-filter"
placeholder="Filter paths..."
oninput="meSetPathFilter(this.value)"
/>
<div class="me-path-list"></div>
</div>
<!-- Aggregation + Smoothing -->
<div class="me-query-col">
<label>Aggregation</label>
<select id="me-aggregation">
<option value="average">Average</option>
<option value="min">Min</option>
<option value="max">Max</option>
</select>
<label style="margin-top: 4px">Smoothing</label>
<select id="me-smoothing" onchange="meShowSmoothingParam()">
<option value="none">None</option>
<option value="sma">SMA</option>
<option value="ema">EMA</option>
</select>
<div id="me-smooth-param-group" style="display: none">
<label id="me-smooth-param-label">Window size:</label>
<input type="number" id="me-smooth-param" value="5" />
</div>
</div>
</div>
</div><!-- /me-query-panel -->
<button
onclick="meExecuteMapQuery()"
style="margin-top: 6px; background: #4caf50; width: 100%;"
>
▶ Run Query
</button>
</div><!-- /query column wrapper -->
</div><!-- /me-layout -->
<!-- Loading -->
<div id="me-loading" class="me-loading" style="display: none">
<span class="loading">Loading results...</span>
</div>
<!-- Results Panel -->
<div
id="me-results-panel"
class="me-results-panel"
style="display: none"
>
<!-- Results Tab Buttons -->
<div class="me-results-tabs">
<button
class="me-results-tab-btn me-active"
data-tab="allpoints"
onclick="meShowResultsTab('allpoints')"
>
All Points
</button>
<button
class="me-results-tab-btn"
data-tab="detail"
onclick="meShowResultsTab('detail')"
>
Detail
</button>
</div>
<!-- All Points Table -->
<div id="me-all-points" class="me-all-points">
<div class="me-table-scroll">
<table>
<thead id="me-table-head"></thead>
<tbody id="me-table-body"></tbody>
</table>
</div>
</div>
<!-- Detail Panel -->
<div id="me-detail" style="display: none">
<div id="me-detail-content">
<p>Click a point to see details.</p>
</div>
<!-- Playback Controls -->
<div class="me-playback">
<input
type="range"
id="me-playback-slider"
min="0"
max="0"
value="0"
oninput="meOnPlaybackSliderChange(this.value)"
style="width: 100%"
/>
<div class="me-playback-controls">
<button onclick="meSkipPlayback(-Infinity)" class="me-btn-sm" title="First">|◀</button>
<button onclick="meSkipPlayback(-10)" class="me-btn-sm" title="Back 10">◀◀</button>
<button onclick="meSkipPlayback(-1)" class="me-btn-sm" title="Back 1">◀</button>
<button id="me-play-btn" onclick="meTogglePlayback()" class="me-btn-sm" style="min-width: 50px" title="Play/Pause">▶</button>
<button onclick="meSkipPlayback(1)" class="me-btn-sm" title="Forward 1">▶</button>
<button onclick="meSkipPlayback(10)" class="me-btn-sm" title="Forward 10">▶▶</button>
<button onclick="meSkipPlayback(Infinity)" class="me-btn-sm" title="Last">▶|</button>
<select onchange="meSetPlaySpeed(this.value)" style="width: auto; padding: 4px">
<option value="1">1x</option>
<option value="2">2x</option>
<option value="5" selected>5x</option>
<option value="10">10x</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Save Area Modal -->
<div id="me-save-modal" class="me-modal-overlay" style="display: none">
<div class="me-modal">
<h3>Save Search Area</h3>
<div class="form-group">
<label for="me-save-name">Name</label>
<input type="text" id="me-save-name" placeholder="Area name" />
</div>
<div class="form-group">
<label for="me-save-desc">Description (optional)</label>
<input type="text" id="me-save-desc" placeholder="Description" />
</div>
<div>
<button onclick="meSaveArea()" style="background: #4caf50">
Save
</button>
<button onclick="meCloseSaveModal()" class="btn-secondary">
Cancel
</button>
</div>
</div>
</div>
<!-- Saved Areas Modal -->
<div
id="me-saved-modal"
class="me-modal-overlay"
style="display: none"
>
<div class="me-modal">
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
>
<h3>Saved Areas</h3>
<button
onclick="meCloseSavedModal()"
class="me-btn-sm btn-secondary"
>
Close
</button>
</div>
<div id="me-saved-list"></div>
</div>
</div>
</div>
<!-- Route Save Modal -->
<div
id="me-route-modal"
class="me-modal-overlay"
style="display: none"
>
<div class="me-modal">
<h3>Save as Route</h3>
<div class="form-group">
<label for="me-route-name">Route name</label>
<input type="text" id="me-route-name" placeholder="Route name" />
</div>
<div class="form-group">
<label for="me-route-desc">Description (optional)</label>
<input type="text" id="me-route-desc" placeholder="Description" />
</div>
<div class="form-group">
<label style="display: inline; font-weight: normal; cursor: pointer;">
<input type="checkbox" id="me-route-hires" style="width: auto;" onchange="meToggleRouteHiRes()" />
High-resolution track (better turns, slower)
</label>
<div id="me-route-hires-status" style="font-size: 11px; color: #666; margin: 4px 0;"></div>
</div>
<div class="form-group">
<label>
Turn detail: <span id="me-route-tolerance-val">5°</span>
</label>
<input
type="range"
id="me-route-tolerance"
min="2"
max="15"
value="5"
oninput="meUpdateRoutePreview()"
style="width: 100%"
/>
<small id="me-route-preview" style="color: #666"></small>
</div>
<div>
<button onclick="meConfirmSaveRoute()" style="background: #4caf50">
Save Route
</button>
<button onclick="meCloseRouteModal()" class="btn-secondary">
Cancel
</button>
</div>
</div>
</div>
</div>
<!-- Migration Tab -->
<div id="migration" class="tab-panel">
<div class="section">
<h2>📊 Store Status & Migration</h2>
<p>
Parquet store overview, SQLite buffer status, and data migration
tools.
</p>
<!-- Parquet Store Stats Section -->
<div
style="
margin-bottom: 30px;
padding: 20px;
background: #e8f5e9;
border-radius: 8px;
border: 2px solid #4caf50;
"
>
<h3 style="margin-top: 0; color: #2e7d32">📊 Parquet Store</h3>
<div id="storeStats">
<p><span class="loading">Loading store stats...</span></p>
</div>
<div style="margin-top: 15px">
<button
onclick="refreshStoreStats()"
style="background: #4caf50"
>
🔄 Refresh
</button>
</div>
</div>
<!-- Buffer Status Section -->
<div
style="
margin-bottom: 30px;
padding: 20px;
background: #e3f2fd;
border-radius: 8px;
border: 2px solid #2196f3;
"
>
<h3 style="margin-top: 0; color: #1565c0">
💾 SQLite Buffer Status
</h3>
<div id="bufferStatus">
<p><span class="loading">Loading buffer status...</span></p>
</div>
<div style="margin-top: 15px">
<button
onclick="refreshBufferStatus()"
style="background: #2196f3"
>
🔄 Refresh
</button>
<button
onclick="forceBufferExport()"
style="background: #4caf50; margin-left: 10px"
>
📤 Force Export
</button>
</div>
</div>
<!-- Migration Section -->
<div
style="
padding: 20px;
background: #fff3e0;
border-radius: 8px;
border: 2px solid #ff9800;
"
>
<h3 style="margin-top: 0; color: #e65100">
📁 Migrate to Hive Partitioning
</h3>
<p style="color: #666; margin-bottom: 15px">
Convert existing flat-structure Parquet files to Hive-style
partitioned structure for better query performance.
</p>
<input type="hidden" id="migrationTier" value="raw" />
<div
class="form-group"
style="display: flex; align-items: center; gap: 10px"
>
<input
type="checkbox"
id="migrationDeleteSource"
style="width: auto"
/>
<label
for="migrationDeleteSource"
style="margin: 0; font-weight: normal"
>Delete source files after migration</label
>
</div>
<div style="margin-top: 20px; display: flex; gap: 10px">
<button
onclick="scanForMigration()"
style="background: #ff9800"
>
🔍 Scan Files
</button>
<button
onclick="startMigration()"
style="background: #4caf50"
id="startMigrationBtn"
disabled
>
▶️ Start Migration
</button>
<button
onclick="cancelMigration()"
style="background: #f44336"
id="cancelMigrationBtn"
disabled
>
❌ Cancel
</button>
</div>
<!-- Scan Results -->
<div
id="migrationScanResults"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Scan Results</h4>
<div id="migrationScanContent"></div>
</div>
<!-- Migration Progress -->
<div
id="migrationProgress"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Migration Progress</h4>
<div
style="
background: #e0e0e0;
border-radius: 5px;
height: 20px;
overflow: hidden;
margin-bottom: 10px;
"
>
<div
id="migrationProgressBar"
style="
background: linear-gradient(90deg, #4caf50, #8bc34a);
height: 100%;
width: 0%;
transition: width 0.3s;
"
></div>
</div>
<div id="migrationProgressText">0% complete</div>
<div
id="migrationCurrentFile"
style="color: #666; font-size: 0.9em; margin-top: 5px"
></div>
</div>
</div>
<!-- GPX Import Section -->
<div
id="gpxImportPanel"
style="
margin-top: 30px;
padding: 20px;
background: #f3e5f5;
border-radius: 8px;
border: 2px solid #8e24aa;
"
>
<h3 style="margin-top: 0; color: #4a148c">
📥 Import GPX Files
</h3>
<p style="color: #666; margin-bottom: 15px">
Import historical GPS tracks from .gpx files into the parquet
store. Each track point is expanded into SignalK records for
the selected paths and written as raw-tier parquet files.
</p>
<div class="form-group">
<label for="gpxFileInput">GPX files to import</label>
<div
id="gpxDropZone"
role="button"
tabindex="0"
aria-label="Click to pick .gpx files or drop them here"
style="
border: 2px dashed #8e24aa;
border-radius: 6px;
padding: 25px;
text-align: center;
background: #fafafa;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
"
>
<div style="color: #6a1b9a; font-size: 1.05em">
<strong>Click to pick files</strong> or drag & drop
.gpx files here
</div>
<div
id="gpxSelectedFiles"
style="
margin-top: 12px;
color: #666;
font-size: 0.9em;
text-align: left;
"
>
No files selected
</div>
</div>
<input
type="file"
id="gpxFileInput"
accept=".gpx,application/gpx+xml"
multiple
style="display: none"
/>
<div
id="gpxRejectedNote"
style="
margin-top: 6px;
color: #c62828;
font-size: 0.85em;
display: none;
"
></div>
</div>
<details id="gpxAdvancedBlock" style="margin-bottom: 15px">
<summary
style="cursor: pointer; color: #6a1b9a; font-weight: 500"
>
Advanced: import from a server-side directory instead
</summary>
<div class="form-group" style="margin-top: 10px">
<label for="gpxSourceDirectory"
>Server directory (searched recursively)</label
>
<input
type="text"
id="gpxSourceDirectory"
placeholder="/home/pi/tracks"
style="width: 100%"
/>
<small style="color: #666">
Path is evaluated on the Signal K host — useful for bulk
imports from mounted USB drives.
</small>
</div>
<div
class="form-group"
style="display: flex; align-items: center; gap: 10px; margin-top: 10px"
>
<input
type="checkbox"
id="gpxDeleteSource"
style="width: auto"
/>
<label
for="gpxDeleteSource"
style="margin: 0; font-weight: normal"
>Delete source .gpx files from the server after successful
import</label
>
</div>
<div style="margin-top: 10px">
<button
onclick="scanGpxImport()"
style="background: #8e24aa"
>
🔍 Scan Files
</button>
</div>
</details>
<div class="form-group">
<label for="gpxContext">Target vessel</label>
<input
type="text"
id="gpxContext"
placeholder="(default: this vessel)"
style="width: 100%"
/>
<small style="color: #666"
>Leave blank to import for this vessel. Only change this if
the data belongs to another vessel already known to Signal K
(advanced).</small
>
</div>
<div class="form-group">
<label>Paths to emit per track point</label>
<div style="display: flex; flex-direction: column; gap: 5px">
<label style="font-weight: normal">
<input
type="checkbox"
class="gpxPath"
value="navigation.position"
checked
/>
navigation.position
</label>
<label style="font-weight: normal">
<input
type="checkbox"
class="gpxPath"
value="navigation.speedOverGround"
checked
/>
navigation.speedOverGround
</label>
<label style="font-weight: normal">
<input
type="checkbox"
class="gpxPath"
value="navigation.courseOverGroundTrue"
checked
/>
navigation.courseOverGroundTrue
</label>
<label style="font-weight: normal">
<input
type="checkbox"
class="gpxPath"
value="navigation.gnss.antennaAltitude"
checked
/>
navigation.gnss.antennaAltitude
</label>
</div>
<small style="color: #666; display: block; margin-top: 6px">
Values are stored in SignalK's canonical units (m/s, rad, m).
Tools that read the parquet files will display them in your
configured display units, just like live data.
</small>
</div>
<div
style="
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
"
>
<button
onclick="startGpxImport()"
style="background: #4caf50"
id="startGpxImportBtn"
disabled
>
▶️ Start Import
</button>
<button
onclick="cancelGpxImport()"
style="background: #f44336"
id="cancelGpxImportBtn"
disabled
>
❌ Cancel
</button>
</div>
<div
id="gpxScanResults"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Scan Results</h4>
<div id="gpxScanContent"></div>
</div>
<div
id="gpxImportProgress"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Import Progress</h4>
<div
style="
background: #e0e0e0;
border-radius: 5px;
height: 20px;
overflow: hidden;
margin-bottom: 10px;
"
>
<div
id="gpxImportProgressBar"
style="
background: linear-gradient(90deg, #8e24aa, #ba68c8);
height: 100%;
width: 0%;
transition: width 0.3s;
"
></div>
</div>
<div id="gpxImportProgressText" aria-live="polite">
0% complete
</div>
<div
id="gpxImportCurrentFile"
style="color: #666; font-size: 0.9em; margin-top: 5px"
></div>
<div
id="gpxImportStats"
style="color: #666; font-size: 0.9em; margin-top: 5px"
></div>
</div>
</div>
<!-- Compaction Section -->
<div
id="compactionPanel"
style="
margin-top: 30px;
padding: 20px;
background: #e8eaf6;
border-radius: 8px;
border: 2px solid #3949ab;
"
>
<h3 style="margin-top: 0; color: #1a237e">
🗜️ Compact old years
</h3>
<p style="color: #666; margin-bottom: 15px">
Merges per-day parquet files within each
(tier, context, path, year) group into a single per-year
file, then deletes the source day-files. Run this on a
long-running store to keep DuckDB queries fast and the
filesystem light. Only completed years (strictly before
the cutoff) are touched.
</p>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
"
>
<div class="form-group">
<label for="compactTier">Tier</label>
<select id="compactTier" style="width: 100%">
<option value="raw" selected>raw</option>
<option value="5s">5s</option>
<option value="60s">60s</option>
<option value="1h">1h</option>
</select>
</div>
<div class="form-group">
<label for="compactBeforeYear">Compact years before</label>
<input
type="number"
id="compactBeforeYear"
placeholder="(default: this year)"
style="width: 100%"
min="2000"
max="2099"
/>
</div>
<div class="form-group">
<label for="compactPathFilter"
>Path filter (substring)</label
>
<input
type="text"
id="compactPathFilter"
placeholder="(optional, e.g. navigation.)"
style="width: 100%"
/>
</div>
</div>
<div
style="
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
"
>
<button
onclick="scanCompaction()"
style="background: #3949ab"
>
🔍 Scan
</button>
<button
onclick="startCompaction()"
style="background: #4caf50"
id="startCompactionBtn"
disabled
>
▶️ Start Compaction
</button>
<button
onclick="cancelCompaction()"
style="background: #f44336"
id="cancelCompactionBtn"
disabled
>
❌ Cancel
</button>
</div>
<div
id="compactScanResults"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Scan results</h4>
<div id="compactScanContent"></div>
</div>
<div
id="compactionProgress"
style="
margin-top: 20px;
display: none;
padding: 15px;
background: white;
border-radius: 5px;
border: 1px solid #ddd;
"
>
<h4 style="margin-top: 0">Compaction progress</h4>
<div
style="
background: #e0e0e0;
border-radius: 5px;
height: 20px;
overflow: hidden;
margin-bottom: 10px;
"
>
<div
id="compactionProgressBar"
style="
background: linear-gradient(90deg, #3949ab, #7986cb);
height: 100%;
width: 0%;
transition: width 0.3s;
"
></div>
</div>
<div id="compactionProgressText" aria-live="polite">
0% complete
</div>
<div
id="compactionCurrentGroup"
style="color: #666; font-size: 0.9em; margin-top: 5px"
></div>
<div
id="compactionStats"
style="color: #666; font-size: 0.9em; margin-top: 5px"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="js/main.js"></script>
<script src="js/migration.js"></script>
<script src="js/import.js"></script>
<script src="js/compaction.js"></script>
<!-- Analysis History Modal -->
<div
id="analysisHistoryModal"
class="modal-overlay"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1001;
overflow-y: auto;
"
>
<div
class="modal-content"
style="
max-width: 900px;
margin: 30px auto;
background: white;
border-radius: 10px;
max-height: 90vh;
overflow-y: auto;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #ddd;
position: sticky;
top: 0;
background: white;
border-radius: 10px 10px 0 0;
"
>
<h3 style="margin: 0">📈 Analysis History</h3>
<button
onclick="closeAnalysisHistoryModal()"
style="
background: #f44336;
color: white;
padding: 5px 10px;
border-radius: 3px;
font-size: 16px;
"
>
×
</button>
</div>
<div id="analysisHistoryModalContent" style="padding: 20px">
<!-- Analysis history content will be loaded here -->
</div>
</div>
</div>
<!-- Individual Analysis View Modal -->
<div
id="analysisViewModal"
class="modal-overlay"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1002;
overflow-y: auto;
"
>
<div
class="modal-content"
style="
max-width: 1000px;
margin: 20px auto;
background: white;
border-radius: 10px;
max-height: 95vh;
overflow-y: auto;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #ddd;
position: sticky;
top: 0;
background: white;
border-radius: 10px 10px 0 0;
"
>
<h3 style="margin: 0" id="analysisViewTitle">🧠 Analysis Details</h3>
<button
onclick="closeAnalysisViewModal()"
style="
background: #f44336;
color: white;
padding: 5px 10px;
border-radius: 3px;
font-size: 16px;
"
>
×
</button>
</div>
<div id="analysisViewContent" style="padding: 20px">
<!-- Individual analysis content will be loaded here -->
</div>
</div>
</div>
<!-- Unified Threshold Modal -->
<div
id="thresholdModal"
class="modal-overlay"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
overflow-y: auto;
"
>
<div
class="modal-content"
style="
max-width: 800px;
margin: 50px auto;
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
"
>
<h3 id="thresholdModalTitle" style="margin: 0; color: #333">
Add Threshold
</h3>
<button
onclick="closeThresholdModal()"
style="
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
"
>
×
</button>
</div>
<div class="form-group">
<label for="thresholdPath">SignalK Path to Monitor:</label>
<input
type="text"
id="thresholdPathSearch"
class="path-tree-search"
placeholder="🔍 Search paths..."
/>
<div id="thresholdPathTree" class="path-tree-container"></div>
<input type="hidden" id="thresholdPath" />
<button
type="button"
onclick="showCustomPathInput('thresholdPath')"
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="thresholdPathCustom"
placeholder="Enter custom SignalK path (e.g., navigation.speedOverGround)"
style="
width: 100%;
padding: 8px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 4px;
display: none;
"
/>
</div>
<div style="display: flex; gap: 10px">
<div class="form-group" style="flex: 1">
<label for="thresholdOperator">Condition:</label>
<select
id="thresholdOperator"
onchange="toggleThresholdValueField()"
>
<option value="gt">Greater than (>)</option>
<option value="lt">Less than (<)</option>
<option value="eq">Equal to (=)</option>
<option value="ne">Not equal to (≠)</option>
<option value="true">Is true</option>
<option value="false">Is false</option>
</select>
</div>
<div class="form-group" style="flex: 1" id="thresholdValueGroup">
<label for="thresholdValue">Value:</label>
<input type="text" id="thresholdValue" placeholder="e.g., 5.0" />
</div>
</div>
<div style="display: flex; gap: 10px">
<div class="form-group" style="flex: 1">
<label for="thresholdAction">Action when condition met:</label>
<select id="thresholdAction">
<option value="true">Turn command ON</option>
<option value="false">Turn command OFF</option>
</select>
</div>
<div class="form-group" style="flex: 1">
<label for="thresholdHysteresis">Hysteresis (seconds):</label>
<input
type="number"
id="thresholdHysteresis"
placeholder="2.0"
step="0.1"
/>
<small style="color: #666; font-size: 11px"
>Prevent rapid switching</small
>
</div>
</div>
<div
style="
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
"
>
<button
type="button"
onclick="cancelThresholdModal()"
style="
background: #666;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
"
>
❌ Cancel
</button>
<button
type="button"
id="thresholdModalSaveButton"
onclick="saveThresholdModal()"
style="
background: #4caf50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
"
>
✅ Add Threshold
</button>
</div>
</div>
</div>
</body>
</html>