From ee9a30f231fb926b1195f65e731f5016e386d034 Mon Sep 17 00:00:00 2001 From: oaquique <30644106+oaquique@users.noreply.github.com> Date: Sat, 13 Dec 2025 22:09:36 -0800 Subject: [PATCH 1/4] Add React UI with real-time music sync progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new React-based web interface accessible at /react/ that provides a modern, responsive dashboard for TeslaUSB. Key features: - Real-time music sync progress with percentage, speed, and ETA - Device model display in sidebar (Pi, Radxa, etc.) - Per-drive storage info (TeslaCam, Music, LightShow, Boombox) - Improved temperature sensor support for non-Pi devices - Mobile-responsive design with ~58KB JS footprint New files: - teslausb-www-react/: Full source code and pre-built dist/ - cgi-bin/storage.sh: Per-drive storage API endpoint - cgi-bin/music_sync_progress.sh: Real-time rsync progress endpoint Modified: - configure-web.sh: Install React UI from local files - copy-music.sh: Add progress2 to rsync for real-time output - status.sh: Add device_model field and multi-device temp support ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- run/cifs_archive/copy-music.sh | 2 +- setup/pi/configure-web.sh | 3 + teslausb-www-react/.gitignore | 30 + teslausb-www-react/README.md | 106 + .../dist/assets/index-Cyod02f8.css | 1 + .../dist/assets/index-DxhX7Us7.js | 5 + teslausb-www-react/dist/filebrowser.css | 592 +++++ teslausb-www-react/dist/filebrowser.js | 1315 ++++++++++ teslausb-www-react/dist/fonts/lato-bold.woff2 | Bin 0 -> 23040 bytes .../dist/fonts/lato-italic.woff2 | Bin 0 -> 24408 bytes .../dist/fonts/lato-regular.woff2 | Bin 0 -> 23580 bytes .../dist/icons/android-chrome-192x192.png | Bin 0 -> 8624 bytes .../dist/icons/android-chrome-512x512.png | Bin 0 -> 24869 bytes .../dist/icons/apple-touch-icon.png | Bin 0 -> 4647 bytes .../dist/icons/browserconfig.xml | 9 + teslausb-www-react/dist/icons/download.svg | 9 + .../dist/icons/favicon-16x16.png | Bin 0 -> 875 bytes .../dist/icons/favicon-32x32.png | Bin 0 -> 1375 bytes teslausb-www-react/dist/icons/favicon.ico | Bin 0 -> 15086 bytes teslausb-www-react/dist/icons/hamburger.svg | 9 + teslausb-www-react/dist/icons/locksound.svg | 4 + .../dist/icons/mstile-144x144.png | Bin 0 -> 3221 bytes .../dist/icons/mstile-150x150.png | Bin 0 -> 3200 bytes .../dist/icons/mstile-310x150.png | Bin 0 -> 3615 bytes .../dist/icons/mstile-310x310.png | Bin 0 -> 7161 bytes .../dist/icons/mstile-70x70.png | Bin 0 -> 2569 bytes teslausb-www-react/dist/icons/newfolder.svg | 4 + teslausb-www-react/dist/icons/pencil.svg | 4 + .../dist/icons/safari-pinned-tab.svg | 38 + .../dist/icons/site.webmanifest | 19 + teslausb-www-react/dist/icons/trash.svg | 8 + teslausb-www-react/dist/icons/upload.svg | 9 + teslausb-www-react/dist/index.html | 51 + teslausb-www-react/dist/manifest.json | 22 + teslausb-www-react/index.html | 50 + teslausb-www-react/package-lock.json | 2006 +++++++++++++++ teslausb-www-react/package.json | 19 + teslausb-www-react/public/filebrowser.css | 592 +++++ teslausb-www-react/public/filebrowser.js | 1315 ++++++++++ .../public/fonts/lato-bold.woff2 | Bin 0 -> 23040 bytes .../public/fonts/lato-italic.woff2 | Bin 0 -> 24408 bytes .../public/fonts/lato-regular.woff2 | Bin 0 -> 23580 bytes .../public/icons/android-chrome-192x192.png | Bin 0 -> 8624 bytes .../public/icons/android-chrome-512x512.png | Bin 0 -> 24869 bytes .../public/icons/apple-touch-icon.png | Bin 0 -> 4647 bytes .../public/icons/browserconfig.xml | 9 + teslausb-www-react/public/icons/download.svg | 9 + .../public/icons/favicon-16x16.png | Bin 0 -> 875 bytes .../public/icons/favicon-32x32.png | Bin 0 -> 1375 bytes teslausb-www-react/public/icons/favicon.ico | Bin 0 -> 15086 bytes teslausb-www-react/public/icons/hamburger.svg | 9 + teslausb-www-react/public/icons/locksound.svg | 4 + .../public/icons/mstile-144x144.png | Bin 0 -> 3221 bytes .../public/icons/mstile-150x150.png | Bin 0 -> 3200 bytes .../public/icons/mstile-310x150.png | Bin 0 -> 3615 bytes .../public/icons/mstile-310x310.png | Bin 0 -> 7161 bytes .../public/icons/mstile-70x70.png | Bin 0 -> 2569 bytes teslausb-www-react/public/icons/newfolder.svg | 4 + teslausb-www-react/public/icons/pencil.svg | 4 + .../public/icons/safari-pinned-tab.svg | 38 + .../public/icons/site.webmanifest | 19 + teslausb-www-react/public/icons/trash.svg | 8 + teslausb-www-react/public/icons/upload.svg | 9 + teslausb-www-react/public/manifest.json | 22 + teslausb-www-react/src/App.jsx | 139 + .../src/components/Dashboard.jsx | 98 + .../src/components/FileBrowser.jsx | 140 ++ teslausb-www-react/src/components/Header.jsx | 49 + teslausb-www-react/src/components/Icons.jsx | 373 +++ .../src/components/LoadingScreen.jsx | 10 + .../src/components/LogViewer.jsx | 227 ++ teslausb-www-react/src/components/Sidebar.jsx | 310 +++ .../src/components/StorageBar.jsx | 123 + .../src/components/SyncStatus.jsx | 139 + .../src/components/VideoViewer.jsx | 386 +++ teslausb-www-react/src/hooks/useLogTail.js | 347 +++ .../src/hooks/useMusicSyncProgress.js | 100 + teslausb-www-react/src/hooks/useStatus.js | 113 + teslausb-www-react/src/main.jsx | 5 + teslausb-www-react/src/services/api.js | 434 ++++ teslausb-www-react/src/styles/fonts.css | 52 + teslausb-www-react/src/styles/index.css | 2226 +++++++++++++++++ teslausb-www-react/vite.config.js | 22 + .../html/cgi-bin/music_sync_progress.sh | 57 + teslausb-www/html/cgi-bin/status.sh | 16 +- teslausb-www/html/cgi-bin/storage.sh | 94 + 86 files changed, 11815 insertions(+), 2 deletions(-) create mode 100644 teslausb-www-react/.gitignore create mode 100644 teslausb-www-react/README.md create mode 100644 teslausb-www-react/dist/assets/index-Cyod02f8.css create mode 100644 teslausb-www-react/dist/assets/index-DxhX7Us7.js create mode 100644 teslausb-www-react/dist/filebrowser.css create mode 100644 teslausb-www-react/dist/filebrowser.js create mode 100644 teslausb-www-react/dist/fonts/lato-bold.woff2 create mode 100644 teslausb-www-react/dist/fonts/lato-italic.woff2 create mode 100644 teslausb-www-react/dist/fonts/lato-regular.woff2 create mode 100644 teslausb-www-react/dist/icons/android-chrome-192x192.png create mode 100644 teslausb-www-react/dist/icons/android-chrome-512x512.png create mode 100644 teslausb-www-react/dist/icons/apple-touch-icon.png create mode 100644 teslausb-www-react/dist/icons/browserconfig.xml create mode 100644 teslausb-www-react/dist/icons/download.svg create mode 100644 teslausb-www-react/dist/icons/favicon-16x16.png create mode 100644 teslausb-www-react/dist/icons/favicon-32x32.png create mode 100644 teslausb-www-react/dist/icons/favicon.ico create mode 100644 teslausb-www-react/dist/icons/hamburger.svg create mode 100644 teslausb-www-react/dist/icons/locksound.svg create mode 100644 teslausb-www-react/dist/icons/mstile-144x144.png create mode 100644 teslausb-www-react/dist/icons/mstile-150x150.png create mode 100644 teslausb-www-react/dist/icons/mstile-310x150.png create mode 100644 teslausb-www-react/dist/icons/mstile-310x310.png create mode 100644 teslausb-www-react/dist/icons/mstile-70x70.png create mode 100644 teslausb-www-react/dist/icons/newfolder.svg create mode 100644 teslausb-www-react/dist/icons/pencil.svg create mode 100644 teslausb-www-react/dist/icons/safari-pinned-tab.svg create mode 100644 teslausb-www-react/dist/icons/site.webmanifest create mode 100644 teslausb-www-react/dist/icons/trash.svg create mode 100644 teslausb-www-react/dist/icons/upload.svg create mode 100644 teslausb-www-react/dist/index.html create mode 100644 teslausb-www-react/dist/manifest.json create mode 100644 teslausb-www-react/index.html create mode 100644 teslausb-www-react/package-lock.json create mode 100644 teslausb-www-react/package.json create mode 100644 teslausb-www-react/public/filebrowser.css create mode 100644 teslausb-www-react/public/filebrowser.js create mode 100644 teslausb-www-react/public/fonts/lato-bold.woff2 create mode 100644 teslausb-www-react/public/fonts/lato-italic.woff2 create mode 100644 teslausb-www-react/public/fonts/lato-regular.woff2 create mode 100644 teslausb-www-react/public/icons/android-chrome-192x192.png create mode 100644 teslausb-www-react/public/icons/android-chrome-512x512.png create mode 100644 teslausb-www-react/public/icons/apple-touch-icon.png create mode 100644 teslausb-www-react/public/icons/browserconfig.xml create mode 100644 teslausb-www-react/public/icons/download.svg create mode 100644 teslausb-www-react/public/icons/favicon-16x16.png create mode 100644 teslausb-www-react/public/icons/favicon-32x32.png create mode 100644 teslausb-www-react/public/icons/favicon.ico create mode 100644 teslausb-www-react/public/icons/hamburger.svg create mode 100644 teslausb-www-react/public/icons/locksound.svg create mode 100644 teslausb-www-react/public/icons/mstile-144x144.png create mode 100644 teslausb-www-react/public/icons/mstile-150x150.png create mode 100644 teslausb-www-react/public/icons/mstile-310x150.png create mode 100644 teslausb-www-react/public/icons/mstile-310x310.png create mode 100644 teslausb-www-react/public/icons/mstile-70x70.png create mode 100644 teslausb-www-react/public/icons/newfolder.svg create mode 100644 teslausb-www-react/public/icons/pencil.svg create mode 100644 teslausb-www-react/public/icons/safari-pinned-tab.svg create mode 100644 teslausb-www-react/public/icons/site.webmanifest create mode 100644 teslausb-www-react/public/icons/trash.svg create mode 100644 teslausb-www-react/public/icons/upload.svg create mode 100644 teslausb-www-react/public/manifest.json create mode 100644 teslausb-www-react/src/App.jsx create mode 100644 teslausb-www-react/src/components/Dashboard.jsx create mode 100644 teslausb-www-react/src/components/FileBrowser.jsx create mode 100644 teslausb-www-react/src/components/Header.jsx create mode 100644 teslausb-www-react/src/components/Icons.jsx create mode 100644 teslausb-www-react/src/components/LoadingScreen.jsx create mode 100644 teslausb-www-react/src/components/LogViewer.jsx create mode 100644 teslausb-www-react/src/components/Sidebar.jsx create mode 100644 teslausb-www-react/src/components/StorageBar.jsx create mode 100644 teslausb-www-react/src/components/SyncStatus.jsx create mode 100644 teslausb-www-react/src/components/VideoViewer.jsx create mode 100644 teslausb-www-react/src/hooks/useLogTail.js create mode 100644 teslausb-www-react/src/hooks/useMusicSyncProgress.js create mode 100644 teslausb-www-react/src/hooks/useStatus.js create mode 100644 teslausb-www-react/src/main.jsx create mode 100644 teslausb-www-react/src/services/api.js create mode 100644 teslausb-www-react/src/styles/fonts.css create mode 100644 teslausb-www-react/src/styles/index.css create mode 100644 teslausb-www-react/vite.config.js create mode 100755 teslausb-www/html/cgi-bin/music_sync_progress.sh create mode 100755 teslausb-www/html/cgi-bin/storage.sh diff --git a/run/cifs_archive/copy-music.sh b/run/cifs_archive/copy-music.sh index 03960f7e..22fc2729 100755 --- a/run/cifs_archive/copy-music.sh +++ b/run/cifs_archive/copy-music.sh @@ -56,7 +56,7 @@ function do_music_sync { if ! rsync -rum --no-human-readable --exclude=.fseventsd/*** --exclude=*.DS_Store --exclude=.metadata_never_index \ --exclude="System Volume Information/***" \ - --delete --modify-window=2 --info=stats2 "$SRC/" "$DST" &> "$LOG" + --delete --modify-window=2 --info=progress2,stats2 "$SRC/" "$DST" &> "$LOG" then log "rsync failed with error $?" fi diff --git a/setup/pi/configure-web.sh b/setup/pi/configure-web.sh index f4341f44..ec30ad4f 100644 --- a/setup/pi/configure-web.sh +++ b/setup/pi/configure-web.sh @@ -53,6 +53,9 @@ then ln -s /var/www/html/favicon.ico /var/www/html/new/favicon.ico fi +# install React UI at /react/ - alternative interface with real-time sync progress +mkdir -p /var/www/html/react +cp -r "$SOURCE_DIR/teslausb-www-react/dist/"* /var/www/html/react/ cat > /sbin/mount.ctts << EOF #!/bin/bash -eu diff --git a/teslausb-www-react/.gitignore b/teslausb-www-react/.gitignore new file mode 100644 index 00000000..b7ecdd84 --- /dev/null +++ b/teslausb-www-react/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build output - included in repo for teslausb integration +# dist/ + +# Editor +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Environment +.env +.env.local +.env.*.local + +# Session notes (local development only) +SESSION-NOTES.md + +# Release artifacts +teslausb-react-ui.tgz diff --git a/teslausb-www-react/README.md b/teslausb-www-react/README.md new file mode 100644 index 00000000..3dcb4646 --- /dev/null +++ b/teslausb-www-react/README.md @@ -0,0 +1,106 @@ +# TeslaUSB React Web UI + +A modern, React-based web interface for TeslaUSB. Built with Preact for minimal bundle size and optimized for Raspberry Pi. + +## Features + +- **Dashboard** - System status, storage visualization, real-time sync progress +- **Video Viewer** - 6-camera synchronized playback with multiple layout options +- **File Browser** - Drag-and-drop file management for Music, LightShow, and Boombox drives +- **Log Viewer** - Real-time log tailing with download option +- **Music Sync Progress** - Live progress display (percentage, speed, ETA) during music sync +- **UI Switcher** - Easy switching between Standard, Vue, and React UIs + +## Installation + +### Option 1: TeslaUSB Integration (Recommended) + +If TeslaUSB includes this UI, it will be automatically installed at `/react/` during setup. Access it at: + +``` +http:///react/ +``` + +### Option 2: Manual Tarball Installation + +Download the latest release and extract to your TeslaUSB: + +```bash +# On your Raspberry Pi +sudo /root/bin/remountfs_rw +curl -L -o /tmp/reactui.tgz https://github.com/oaquique/teslausb-www-react/releases/latest/download/teslausb-react-ui.tgz +sudo tar -C /var/www/html -xf /tmp/reactui.tgz +``` + +Then access at `http:///react/` + +### Option 3: Full Replacement via Deploy Script + +Replace the entire web UI (serves from root `/`): + +```bash +# Build and deploy to your Raspberry Pi +./deploy.sh pi@192.168.1.100 +``` + +## Switching Between UIs + +TeslaUSB supports multiple web interfaces: + +| UI | Path | Description | +|----|------|-------------| +| Standard | `/` | Original TeslaUSB interface | +| Vue | `/new/` | Vue-based alternative | +| React | `/react/` | This interface | + +Use the "Switch UI" section in the sidebar to navigate between them. + +## Development + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# Build release tarball (for /react/ path) +./build-release.sh +``` + +## Tech Stack + +- **Preact** - 3KB React alternative for minimal bundle size +- **Vite** - Fast build tool with excellent tree-shaking +- **Custom CSS** - No external UI framework for maximum performance + +## Project Structure + +``` +teslausb-www-react/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ components/ # UI components +โ”‚ โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ”œโ”€โ”€ services/ # API layer +โ”‚ โ””โ”€โ”€ styles/ # CSS +โ”œโ”€โ”€ cgi-bin/ # CGI scripts for this UI +โ”œโ”€โ”€ public/ # Static assets +โ”œโ”€โ”€ deploy.sh # Full deployment script +โ”œโ”€โ”€ rollback.sh # Restore previous UI +โ””โ”€โ”€ build-release.sh # Create release tarball +``` + +## Rollback + +If deployed via `deploy.sh`, you can restore the previous UI: + +```bash +./rollback.sh pi@192.168.1.100 +``` + +## License + +Same as TeslaUSB project. diff --git a/teslausb-www-react/dist/assets/index-Cyod02f8.css b/teslausb-www-react/dist/assets/index-Cyod02f8.css new file mode 100644 index 00000000..5b8edd68 --- /dev/null +++ b/teslausb-www-react/dist/assets/index-Cyod02f8.css @@ -0,0 +1 @@ +*{margin:0;padding:0;box-sizing:border-box}body{font-family:Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background-color:#f5f5f5;color:#333;overflow:hidden}@media(max-width:767px){body{overflow:visible;height:auto}}.app-shell{display:flex;flex-direction:column;min-height:100vh;overflow:hidden}@media(max-width:767px){.app-shell{overflow:visible;height:auto;min-height:100vh}}.app-topbar{display:flex;justify-content:space-between;align-items:center;background-color:#1f2937;color:#fff;padding:.75rem 1rem;border-bottom:1px solid #374151}.app-topbar-title{font-size:1.1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}.app-topbar-title svg{width:24px;height:24px}.app-topbar-actions{display:flex;gap:1rem;align-items:center}.app-dashboard{display:flex;flex-direction:column;height:100vh;background-color:#f5f5f5}.app-header{background-color:#fff;border-bottom:1px solid #e1e5e9;padding:.75rem 1rem;display:flex;justify-content:space-between;align-items:center;height:60px;flex-shrink:0;position:sticky;top:0;z-index:40;backdrop-filter:saturate(180%) blur(4px)}.app-header-left{display:flex;align-items:center;gap:.5rem}.app-header-icon{width:20px;height:20px;color:#06c}.app-header-title{font-size:16px;font-weight:600;color:#333;letter-spacing:.1px}.app-header-subtitle{font-size:14px;color:#666;margin-left:.5rem}.app-header-right{display:flex;align-items:center;gap:1rem}.dashboard-nav{display:flex;gap:.5rem;align-items:center}.nav-link{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:6px;font-size:14px;font-weight:500;color:#6b7280;text-decoration:none;transition:all .15s ease;border:1px solid transparent;position:relative;cursor:pointer;background:none}.nav-link:hover{background-color:#f9fafb;color:#374151}.nav-link.active{background-color:#0095f6;color:#fff;border-color:#007dd1}.nav-link.active svg{color:#fff}.nav-link svg{color:#6b7280;transition:color .15s ease;width:16px;height:16px}.nav-link:hover svg{color:#374151}.nav-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background-color:#ef4444;color:#fff;font-size:11px;font-weight:700;border-radius:9px;margin-left:4px;line-height:1}.nav-link.active .nav-badge{background-color:#fff;color:#ef4444}.app-last-update{font-size:13px;color:#6b7280}.app-status{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.3px}.app-status.healthy{color:#065f46;background:#d1fae5;border:1px solid #a7f3d0}.app-status.unhealthy{color:#7f1d1d;background:#fee2e2;border:1px solid #fecaca}.app-status.warning{color:#92400e;background:#fef3c7;border:1px solid #fde68a}.app-body{display:flex;flex:1;overflow:hidden}.app-sidebar{width:280px;background-color:#fff;border-right:1px solid #e1e5e9;padding:1rem;overflow-y:auto;overflow-x:hidden;flex-shrink:0;max-height:calc(100vh - 60px);-webkit-overflow-scrolling:touch}.device-info{display:flex;flex-direction:column;border-radius:10px;border:1px solid #e5e7eb;background:#fff;margin-bottom:1rem}.device-header{display:flex;align-items:center;gap:.5rem;padding:10px 12px;border-bottom:1px solid #eef2f7;font-size:14px;font-weight:600;color:#1f2937}.device-header svg{width:16px;height:16px;color:#6b7280}.info-list{display:flex;flex-direction:column;padding:8px 12px}.info-item{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f0f0f0;font-size:13px}.info-item:last-child{border-bottom:none}.info-item.clickable{cursor:pointer;transition:background-color .15s ease;margin:0 -12px;padding:6px 12px;border-radius:4px}.info-item.clickable:hover{background-color:#f3f4f6}.info-item.clickable:active{background-color:#e5e7eb}.toggle-btn{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:600;line-height:1;border:1px solid #007dd1;background-color:#0095f6;color:#fff;cursor:pointer;transition:all .15s ease}.toggle-btn svg{flex-shrink:0;vertical-align:middle;margin-right:4px;color:#fff}.toggle-btn:hover:not(:disabled){background-color:#007dd1;border-color:#006bbd}.toggle-btn:disabled{opacity:.6;cursor:not-allowed}.toggle-btn.active{background-color:#dcfce7;border-color:#22c55e;color:#166534}.toggle-btn.danger{background-color:#fee2e2;border-color:#f87171;color:#b91c1c}.ui-switch-link{color:#0095f6;text-decoration:none;font-size:13px;padding:4px 0;display:block;width:100%}.ui-switch-link:hover{text-decoration:underline}.toggle-btn.danger:hover:not(:disabled){background-color:#fecaca;border-color:#ef4444}.toggle-btn.primary{background-color:#eff6ff;border-color:#3b82f6;color:#1d4ed8}.toggle-btn.primary:hover:not(:disabled){background-color:#dbeafe;border-color:#2563eb}.sidebar-action{margin-top:8px;padding:0}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;border-radius:6px;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .15s ease}.action-btn:disabled{opacity:.6;cursor:not-allowed}.action-btn.connected{background-color:#dcfce7;color:#166534;border:1px solid #22c55e}.action-btn.connected:hover:not(:disabled){background-color:#bbf7d0}.action-btn.disconnected{background-color:#fef2f2;color:#991b1b;border:1px solid #f87171}.action-btn.disconnected:hover:not(:disabled){background-color:#fee2e2}.action-btn.restart{background-color:#f3f4f6;color:#374151;border:1px solid #d1d5db}.action-btn.restart:hover:not(:disabled){background-color:#e5e7eb;border-color:#9ca3af}.info-label{color:#6b7280;font-weight:500}.info-value{color:#111827;font-weight:600;text-align:right}.info-value-with-action{display:flex;align-items:center;gap:8px}.speed-result{color:#111827;font-weight:600;font-size:12px}.small-text{font-size:11px!important;line-height:1.2}.compact-date{font-size:10px!important;line-height:1.1;white-space:nowrap;color:#4b5563}.progress-container{margin-top:.5rem}.progress-bar{width:100%;height:8px;background-color:#eef2f7;border-radius:999px;overflow:hidden;margin-bottom:.25rem}.progress-fill{height:100%;border-radius:999px;transition:width .3s ease}.progress-fill.blue{background-color:#3b82f6}.progress-fill.green{background-color:#10b981}.progress-fill.yellow{background-color:#f59e0b}.progress-fill.red{background-color:#ef4444}.progress-text{font-size:10px;color:#6b7280;font-weight:500;margin-top:6px;text-align:right}.status-healthy{color:#00b04f!important}.status-degraded{color:#ff9800!important}.status-unhealthy{color:#e74c3c!important}.app-main{flex:1;background-color:#fff;padding:1rem;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;max-height:calc(100vh - 60px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1px;background-color:#e1e5e9;border:1px solid #e1e5e9;border-radius:4px;overflow:visible;margin-bottom:1rem}.stats-section{background-color:#fff;padding:1rem}.stats-section h3{font-size:12px;font-weight:600;color:#666;text-transform:uppercase;letter-spacing:.5px;margin-bottom:.75rem}.stat-data{display:flex;flex-direction:column;gap:.5rem}.primary-stat{font-size:24px;font-weight:700;color:#333;line-height:1}.stat-details{display:flex;flex-direction:column;gap:.25rem}.stat-details span{font-size:11px;color:#666}.stat-details span:first-child{color:#333;font-weight:500;font-size:12px}.mono-value{font-family:Monaco,Menlo,Consolas,monospace;color:#333;font-weight:600;font-size:12px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:12px;box-shadow:0 1px #00000005;transition:transform .08s ease,box-shadow .12s ease,border-color .12s ease}.card:hover{transform:translateY(-1px);box-shadow:0 6px 18px #0000000f;border-color:#dee3ea}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.card-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.card-value{font-size:22px;font-weight:700;color:#111827;line-height:1}.card-sub{font-size:11px;color:#666;margin-top:4px}.dashboard-section{margin-bottom:1rem}.section-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px;margin-bottom:.5rem}.features-row{display:flex;flex-wrap:wrap;gap:8px}.feature-badge{display:inline-block;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:500;line-height:1;text-align:center}.feature-badge.enabled{background:#ecfdf5;color:#059669;border:1px solid #a7f3d0}.feature-badge.disabled{background:#f3f4f6;color:#9ca3af;border:1px solid #e5e7eb}.actions-row{display:flex;flex-wrap:wrap;gap:10px}.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:14px}.chart-container{background-color:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:1rem;margin-bottom:1rem}.chart-header{padding:10px 12px;border-bottom:1px solid #eef2f7;margin:-1rem -1rem 1rem}.chart-title{font-size:14px;font-weight:600;color:#1f2937}.loading-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#666}.spinner{width:20px;height:20px;border:2px solid #e5e7eb;border-top-color:#0095f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.spinning{animation:spin 1s linear infinite}.error-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#e74c3c}.retry-btn{background-color:#e74c3c;border:none;color:#fff;padding:.375rem .75rem;border-radius:4px;font-size:12px;cursor:pointer;margin-left:.5rem}.retry-btn:hover{background-color:#c0392b}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 12px;font-size:13px;font-weight:500;line-height:1.4;border-radius:6px;border:1px solid #cfd8e3;background:#fff;color:#374151;cursor:pointer;transition:all .15s ease-in-out;box-shadow:0 1px 2px #00000008}.btn svg{color:#374151;transition:color .15s ease-in-out;width:14px;height:14px}.btn:hover:not(:disabled){background:#0095f6;border-color:#007dd1;color:#fff;box-shadow:0 2px 8px #0000000f}.btn:hover:not(:disabled) svg{color:#fff}.btn:active:not(:disabled){background:#007dd1;border-color:#006bbd}.btn:disabled{opacity:.6;cursor:not-allowed;background:#f9fafb;color:#9ca3af;border-color:#e5e7eb}.btn-primary{background:#0095f6;border-color:#007dd1;color:#fff}.btn-primary svg{color:#fff}.btn-primary:hover:not(:disabled){background:#007dd1;border-color:#006bbd}.btn-danger{background:#ef4444;border-color:#dc2626;color:#fff}.btn-danger svg{color:#fff}.btn-danger:hover:not(:disabled){background:#dc2626;border-color:#b91c1c}.btn-sm{padding:4px 8px;font-size:12px}.btn-lg{padding:10px 20px;font-size:15px}.storage-bar-container{margin:1rem 0}.storage-bar{display:flex;height:24px;border-radius:6px;overflow:hidden;background-color:#e5e7eb}.storage-segment{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#fff;transition:width .3s ease;min-width:0}.storage-segment.teslacam{background-color:#3b82f6}.storage-segment.music{background-color:#8b5cf6}.storage-segment.lightshow{background-color:#ec4899}.storage-segment.boombox{background-color:#f97316}.storage-segment.free{background-color:#d1d5db}.storage-legend{display:flex;flex-wrap:wrap;gap:1rem;margin-top:.75rem;font-size:12px}.storage-legend-item{display:flex;align-items:center;gap:6px}.storage-legend-dot{width:10px;height:10px;border-radius:2px}.storage-legend-dot.teslacam{background-color:#3b82f6}.storage-legend-dot.music{background-color:#8b5cf6}.storage-legend-dot.lightshow{background-color:#ec4899}.storage-legend-dot.boombox{background-color:#f97316}.storage-legend-dot.free{background-color:#d1d5db}.storage-legend-label{color:#374151;font-weight:500}.storage-legend-value{color:#6b7280}.storage-legend-used{color:#9ca3af;font-size:11px}.storage-legend-percent{margin-left:4px;color:#9ca3af;font-size:11px}.storage-note{margin-top:.5rem;font-size:10px;color:#9ca3af;font-style:italic}.sync-status-card{border:1px solid #e5e7eb;border-radius:10px;padding:1rem;background:#fff}.sync-status-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.sync-status-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.sync-status-indicator{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600}.sync-status-dot{width:8px;height:8px;border-radius:50%}.sync-status-dot.idle{background-color:#10b981}.sync-status-dot.connecting{background-color:#f59e0b;animation:pulse 1.5s ease-in-out infinite}.sync-status-dot.archiving{background-color:#3b82f6;animation:pulse 1s ease-in-out infinite}.sync-status-dot.error{background-color:#ef4444}.sync-status-description{font-size:13px;color:#6b7280;margin-bottom:.5rem}.sync-details{font-size:12px;color:#9ca3af}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.sync-progress-bar{height:8px;background-color:#e5e7eb;border-radius:4px;overflow:hidden;margin:.75rem 0}.sync-progress-fill{height:100%;background-color:#3b82f6;border-radius:4px;transition:width .3s ease}.sync-details{display:flex;flex-direction:column;gap:2px;font-size:11px;color:#6b7280}.sync-progress-main{font-weight:500}.sync-progress-secondary{font-size:10px;color:#9ca3af}.sync-file{max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:Monaco,Menlo,monospace}.log-viewer{background-color:#1e1e1e;border-radius:8px;font-family:Monaco,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;overflow:hidden;display:flex;flex-direction:column;height:400px}.log-viewer-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background-color:#2d2d2d;border-bottom:1px solid #3d3d3d}.log-viewer-title{color:#e0e0e0;font-size:13px;font-weight:600}.log-viewer-actions{display:flex;gap:8px}.log-action-btn{background:#4a4a4a!important;border-color:#666!important;color:#e0e0e0!important}.log-action-btn svg{color:#e0e0e0!important}.log-action-btn:hover:not(:disabled){background:#5a5a5a!important;border-color:#888!important}.log-action-btn:hover:not(:disabled) svg{color:#fff!important}.log-action-btn:disabled{opacity:.4}.log-viewer-content{flex:1;overflow-y:auto;padding:12px;color:#d4d4d4}.log-viewer-content::-webkit-scrollbar{width:8px}.log-viewer-content::-webkit-scrollbar-track{background:#1e1e1e}.log-viewer-content::-webkit-scrollbar-thumb{background:#4d4d4d;border-radius:4px}.log-line{white-space:pre-wrap;word-break:break-all}.log-line.error{color:#f87171}.log-line.warning{color:#fbbf24}.log-line.success{color:#34d399}.video-viewer{display:flex;flex-direction:column;height:100%;background:#000;border-radius:8px;overflow:hidden}.video-grid{display:grid;flex:1;gap:2px;background:#1a1a1a;padding:2px}.video-grid.layout-6{grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-4{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.video-cell{position:relative;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden}.video-cell video{width:100%;height:100%;object-fit:contain}.video-cell-label{position:absolute;top:8px;left:8px;background:#000000b3;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500}.video-controls{display:flex;align-items:center;gap:1rem;padding:12px 16px;background:#1a1a1a;border-top:1px solid #333}.video-timeline{flex:1;height:6px;background:#333;border-radius:3px;cursor:pointer;position:relative}.video-timeline-progress{height:100%;background:#0095f6;border-radius:3px;transition:width .1s linear}.video-timeline-markers{position:absolute;inset:0;pointer-events:none}.video-timeline-marker{position:absolute;top:-2px;width:2px;height:10px;background:#ef4444;border-radius:1px}.video-play-btn{background:#0095f6;border:none;color:#fff;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s}.video-play-btn:hover{background:#007dd1}.video-time{color:#fff;font-size:13px;font-family:Monaco,Menlo,monospace;min-width:100px}.file-browser{display:flex;height:100%;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}.file-tree{width:250px;background:#f9fafb;border-right:1px solid #e5e7eb;overflow-y:auto}.file-tree-item{display:flex;align-items:center;gap:6px;padding:8px 12px;cursor:pointer;font-size:13px;color:#374151;border-bottom:1px solid #f0f0f0}.file-tree-item:hover{background:#f3f4f6}.file-tree-item.active{background:#e0f2fe;color:#0369a1}.file-tree-item svg{width:16px;height:16px;color:#6b7280;flex-shrink:0}.file-tree-item.active svg{color:#0369a1}.file-list{flex:1;overflow-y:auto;padding:1rem}.file-list-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #e5e7eb}.file-list-title{font-size:14px;font-weight:600;color:#1f2937}.file-list-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px}.file-item{display:flex;flex-direction:column;align-items:center;padding:12px;border:1px solid transparent;border-radius:8px;cursor:pointer;transition:all .15s}.file-item:hover{background:#f9fafb;border-color:#e5e7eb}.file-item.selected{background:#e0f2fe;border-color:#0ea5e9}.file-item-icon{width:48px;height:48px;margin-bottom:8px;color:#6b7280}.file-item-icon.folder{color:#f59e0b}.file-item-icon.video{color:#8b5cf6}.file-item-icon.audio{color:#ec4899}.file-item-name{font-size:12px;text-align:center;word-break:break-word;color:#374151}.file-item-size{font-size:10px;color:#9ca3af;margin-top:2px}.dropzone{border:2px dashed #d1d5db;border-radius:8px;padding:2rem;text-align:center;color:#6b7280;transition:all .15s}.dropzone svg{width:24px;height:24px}.dropzone.active{border-color:#0095f6;background:#f0f9ff;color:#0369a1}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:#fff;border-radius:12px;max-width:500px;width:90%;max-height:90vh;overflow:hidden;box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid #e5e7eb}.modal-title{font-size:16px;font-weight:600;color:#1f2937}.modal-close{background:none;border:none;cursor:pointer;color:#6b7280;padding:4px}.modal-close:hover{color:#374151}.modal-body{padding:1.5rem;overflow-y:auto}.modal-footer{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid #e5e7eb;background:#f9fafb}.toast-container{position:fixed;bottom:1rem;right:1rem;z-index:1100;display:flex;flex-direction:column;gap:.5rem}.toast{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;background:#1f2937;color:#fff;border-radius:8px;font-size:13px;box-shadow:0 10px 15px -3px #0000001a;animation:slideIn .2s ease}.toast.success{background:#059669}.toast.error{background:#dc2626}.toast.warning{background:#d97706}@keyframes slideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@media(min-width:1024px)and (max-width:1366px){.app-sidebar{width:260px}.kpi-row{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.app-header-title{font-size:15px}}@media(min-width:768px)and (max-width:1023px){.app-body{flex-direction:row}.app-sidebar{width:220px;max-height:calc(100vh - 60px);overflow-y:auto}.kpi-row{grid-template-columns:repeat(2,1fr);gap:8px}.card{padding:10px}.card-value{font-size:18px}.card-title{font-size:11px}.card-sub{font-size:10px}.app-header-right{gap:8px}.app-last-update{font-size:11px}.chart-container{padding:12px;margin-bottom:8px}.chart-title{font-size:13px}}.mobile-quick-status{display:none}.desktop-status-bar{display:block}.sidebar-toggle-btn{display:none;background:none;border:none;color:#6b7280;cursor:pointer;padding:4px;margin-left:auto;transition:color .2s ease}.sidebar-toggle-btn:hover{color:#374151}@media(max-width:767px){.desktop-status-bar{display:none!important}.mobile-quick-status{display:flex;align-items:center;justify-content:space-around;padding:6px 8px;background-color:#f9fafb;border-bottom:1px solid #e5e7eb;gap:4px;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.quick-status-item{display:flex;flex-direction:column;align-items:center;gap:3px;flex-shrink:0}.status-dot-mini{width:8px;height:8px;border-radius:50%;flex-shrink:0}.status-dot-mini.healthy{background-color:#10b981;box-shadow:0 0 4px #10b98180}.status-dot-mini.unhealthy{background-color:#ef4444;box-shadow:0 0 4px #ef444480}.quick-status-label{font-size:9px;color:#6b7280;font-weight:500;white-space:nowrap;text-align:center}.app-body{flex-direction:column;height:auto;overflow:visible}.app-sidebar{width:100%;height:auto;max-height:none;border-right:none;border-bottom:1px solid #e1e5e9;padding:8px;overflow:visible;overflow-x:hidden;flex-shrink:0;-webkit-overflow-scrolling:touch;transition:max-height .3s ease}.sidebar-toggle-btn{display:inline-flex}.app-sidebar:not(.expanded){max-height:60px;overflow:hidden}.app-sidebar:not(.expanded) .device-info:not(:first-child){display:none}.app-sidebar:not(.expanded) .info-item:nth-child(n+4){display:none}.app-sidebar.expanded{max-height:600px;overflow-y:auto}.device-info{margin-top:8px}.device-info:first-child{margin-top:0}.info-item{padding:4px 0;font-size:12px}.app-main{flex:1;padding:12px 8px 150px;overflow-y:auto;overflow-x:hidden;height:auto;min-height:0;max-height:none;-webkit-overflow-scrolling:touch}.kpi-row{grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:10px}.card{padding:6px 8px;min-height:75px}.card-header{margin-bottom:3px}.card-value{font-size:18px;line-height:1.1;margin-bottom:2px}.card-title{font-size:10px;letter-spacing:.3px}.card-sub{font-size:9px;line-height:1.3;margin-top:2px}.app-header{padding:8px 10px 10px!important;height:auto!important;flex-direction:column!important;gap:10px!important;align-items:stretch!important;position:static!important;top:auto!important;z-index:auto!important;backdrop-filter:none!important}.app-header-left{justify-content:center;padding-bottom:4px}.app-header-title{font-size:14px}.app-header-right{flex-direction:row;gap:6px;align-items:center;justify-content:space-between;width:100%;padding-bottom:2px}.dashboard-nav{gap:4px;flex:1;overflow-x:auto;-webkit-overflow-scrolling:touch}.nav-link{padding:7px 8px;font-size:11px;gap:4px;flex-shrink:0;justify-content:center;border-radius:4px}.nav-link svg{width:14px;height:14px}.nav-badge{min-width:16px;height:16px;font-size:10px;padding:0 4px;margin-left:2px}.app-last-update{font-size:10px}.app-status{font-size:9px;padding:1px 6px}.btn{padding:7px 8px;font-size:10px;gap:3px;white-space:nowrap;border-radius:4px;flex-shrink:0}.btn svg{width:12px;height:12px}.chart-container{padding:8px;margin-bottom:10px}.chart-title{font-size:11px;font-weight:600}.chart-header{margin-bottom:6px;padding-bottom:4px}.chart-container:last-child{margin-bottom:100px}.file-browser{flex-direction:column}.file-tree{width:100%;max-height:150px;border-right:none;border-bottom:1px solid #e5e7eb}.file-list-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.video-grid.layout-6{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(3,1fr)}.video-controls{flex-wrap:wrap;gap:.5rem;padding:8px 12px}.video-time{font-size:11px;min-width:80px}.log-viewer{height:300px}.log-viewer-content{font-size:10px}}@media(min-width:393px)and (max-width:430px){.kpi-row{grid-template-columns:1fr}.card{min-height:80px}.app-main{padding-bottom:140px}}@media(max-width:392px){.kpi-row{grid-template-columns:1fr}.app-header{padding:6px 8px;height:45px}.app-header-title{font-size:13px}.app-main{padding:6px 6px 160px}.card{padding:6px;min-height:70px}.card-value{font-size:14px}.chart-title{font-size:11px}}@media(pointer:coarse){.btn{min-height:44px;min-width:44px}.info-item{padding:8px 0;min-height:44px;align-items:center}.card{min-height:100px}.nav-link{min-height:44px}}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.card,.chart-container{border-width:.5px}.app-header{border-bottom-width:.5px}}@media(min-width:768px)and (max-width:1024px)and (orientation:landscape){.kpi-row{grid-template-columns:repeat(3,1fr)}.app-sidebar{width:200px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}@media(min-width:768px)and (max-width:1024px)and (orientation:portrait){.kpi-row{grid-template-columns:repeat(2,1fr)}.app-sidebar{width:240px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}.app-dashboard{max-height:100vh;overflow:hidden}.dashboard-nav::-webkit-scrollbar{display:none}.dashboard-nav{-ms-overflow-style:none;scrollbar-width:none}.tab-content{display:none;flex:1;overflow:hidden}.tab-content.active{display:flex;flex-direction:column}.hidden{display:none!important}.flex{display:flex}.flex-1{flex:1}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.text-sm{font-size:12px}.text-xs{font-size:10px}.text-muted{color:#6b7280}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700} diff --git a/teslausb-www-react/dist/assets/index-DxhX7Us7.js b/teslausb-www-react/dist/assets/index-DxhX7Us7.js new file mode 100644 index 00000000..f27d85d6 --- /dev/null +++ b/teslausb-www-react/dist/assets/index-DxhX7Us7.js @@ -0,0 +1,5 @@ +(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function r(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(o){if(o.ep)return;o.ep=!0;const i=r(o);fetch(o.href,i)}})();var te,N,Ae,P,ye,Ee,Re,De,ue,oe,ae,G={},je=[],an=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,re=Array.isArray;function j(e,n){for(var r in n)e[r]=n[r];return e}function fe(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function ln(e,n,r){var s,o,i,a={};for(i in n)i=="key"?s=n[i]:i=="ref"?o=n[i]:a[i]=n[i];if(arguments.length>2&&(a.children=arguments.length>3?te.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return X(e,a,s,o,null)}function X(e,n,r,s,o){var i={type:e,props:n,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++Ae:o,__i:-1,__u:0};return o==null&&N.vnode!=null&&N.vnode(i),i}function R(e){return e.children}function Q(e,n){this.props=e,this.context=n}function O(e,n){if(n==null)return e.__?O(e.__,e.__i+1):null;for(var r;nl&&P.sort(Re),e=P.shift(),l=P.length,e.__d&&(r=void 0,s=void 0,o=(s=(n=e).__v).__e,i=[],a=[],n.__P&&((r=j({},s)).__v=s.__v+1,N.vnode&&N.vnode(r),_e(n.__P,r,s,n.__n,n.__P.namespaceURI,32&s.__u?[o]:null,i,o==null?O(s):o,!!(32&s.__u),a),r.__v=s.__v,r.__.__k[r.__i]=r,Pe(i,r,a),s.__e=s.__=null,r.__e!=o&&We(r)));ne.__r=0}function He(e,n,r,s,o,i,a,l,u,d,_){var c,h,f,v,y,b,m,p=s&&s.__k||je,w=n.length;for(u=cn(r,n,p,u,w),c=0;c0?a=e.__k[i]=X(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,u=i+h,a.__=e,a.__b=e.__b+1,(d=a.__i=dn(a,r,u,c))!=-1&&(c--,(l=r[d])&&(l.__u|=2)),l==null||l.__v==null?(d==-1&&(o>_?h--:o<_&&h++),typeof a.type!="function"&&(a.__u|=4)):d!=u&&(d==u-1?h--:d==u+1?h++:(d>u?h--:h++,a.__u|=4))):e.__k[i]=null;if(c)for(i=0;i<_;i++)(l=r[i])!=null&&(2&l.__u)==0&&(l.__e==s&&(s=O(l)),Ve(l,l));return s}function Ue(e,n,r,s){var o,i;if(typeof e.type=="function"){for(o=e.__k,i=0;o&&i(_?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&(2&d.__u)==0&&l==d.key&&u==d.type)return a}return-1}function ke(e,n,r){n[0]=="-"?e.setProperty(n,r==null?"":r):e[n]=r==null?"":typeof r!="number"||an.test(n)?r:r+"px"}function Z(e,n,r,s,o){var i,a;e:if(n=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof s=="string"&&(e.style.cssText=s=""),s)for(n in s)r&&n in r||ke(e.style,n,"");if(r)for(n in r)s&&r[n]==s[n]||ke(e.style,n,r[n])}else if(n[0]=="o"&&n[1]=="n")i=n!=(n=n.replace(De,"$1")),a=n.toLowerCase(),n=a in e||n=="onFocusOut"||n=="onFocusIn"?a.slice(2):n.slice(2),e.l||(e.l={}),e.l[n+i]=r,r?s?r.u=s.u:(r.u=ue,e.addEventListener(n,i?ae:oe,i)):e.removeEventListener(n,i?ae:oe,i);else{if(o=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in e)try{e[n]=r==null?"":r;break e}catch(l){}typeof r=="function"||(r==null||r===!1&&n[4]!="-"?e.removeAttribute(n):e.setAttribute(n,n=="popover"&&r==1?"":r))}}function we(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n.t==null)n.t=ue++;else if(n.t0?e:re(e)?e.map(Oe):j({},e)}function hn(e,n,r,s,o,i,a,l,u){var d,_,c,h,f,v,y,b=r.props||G,m=n.props,p=n.type;if(p=="svg"?o="http://www.w3.org/2000/svg":p=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(d=0;d=r.__.length&&r.__.push({}),r.__[e]}function k(e){return Y=1,pn(Ge,e)}function pn(e,n,r){var s=me(K++,2);if(s.t=e,!s.__c&&(s.__=[Ge(void 0,n),function(l){var u=s.__N?s.__N[0]:s.__[0],d=s.t(u,l);u!==d&&(s.__N=[d,s.__[1]],s.__c.setState({}))}],s.__c=S,!S.__f)){var o=function(l,u,d){if(!s.__c.__H)return!0;var _=s.__c.__H.__.filter(function(h){return!!h.__c});if(_.every(function(h){return!h.__N}))return!i||i.call(this,l,u,d);var c=s.__c.props!==l;return _.forEach(function(h){if(h.__N){var f=h.__[0];h.__=h.__N,h.__N=void 0,f!==h.__[0]&&(c=!0)}}),i&&i.call(this,l,u,d)||c};S.__f=!0;var i=S.shouldComponentUpdate,a=S.componentWillUpdate;S.componentWillUpdate=function(l,u,d){if(this.__e){var _=i;i=void 0,o(l,u,d),i=_}a&&a.call(this,l,u,d)},S.shouldComponentUpdate=o}return s.__N||s.__}function F(e,n){var r=me(K++,3);!$.__s&&qe(r.__H,n)&&(r.__=e,r.u=n,S.__H.__h.push(r))}function H(e){return Y=5,ve(function(){return{current:e}},[])}function ve(e,n){var r=me(K++,7);return qe(r.__H,n)&&(r.__=e(),r.__H=n,r.__h=e),r.__}function B(e,n){return Y=8,ve(function(){return e},n)}function mn(){for(var e;e=ze.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(ee),e.__H.__h.forEach(ce),e.__H.__h=[]}catch(n){e.__H.__h=[],$.__e(n,e.__v)}}$.__b=function(e){S=null,xe&&xe(e)},$.__=function(e,n){e&&n.__k&&n.__k.__m&&(e.__m=n.__k.__m),Te&&Te(e,n)},$.__r=function(e){Ce&&Ce(e),K=0;var n=(S=e.__c).__H;n&&(se===S?(n.__h=[],S.__h=[],n.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(n.__h.forEach(ee),n.__h.forEach(ce),n.__h=[],K=0)),se=S},$.diffed=function(e){Le&&Le(e);var n=e.__c;n&&n.__H&&(n.__H.__h.length&&(ze.push(n)!==1&&Ne===$.requestAnimationFrame||((Ne=$.requestAnimationFrame)||vn)(mn)),n.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),se=S=null},$.__c=function(e,n){n.some(function(r){try{r.__h.forEach(ee),r.__h=r.__h.filter(function(s){return!s.__||ce(s)})}catch(s){n.some(function(o){o.__h&&(o.__h=[])}),n=[],$.__e(s,r.__v)}}),Se&&Se(e,n)},$.unmount=function(e){$e&&$e(e);var n,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(s){try{ee(s)}catch(o){n=o}}),r.__H=void 0,n&&$.__e(n,r.__v))};var Be=typeof requestAnimationFrame=="function";function vn(e){var n,r=function(){clearTimeout(s),Be&&cancelAnimationFrame(n),setTimeout(e)},s=setTimeout(r,35);Be&&(n=requestAnimationFrame(r))}function ee(e){var n=S,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),S=n}function ce(e){var n=S;e.__c=e.__(),S=n}function qe(e,n){return!e||e.length!==n.length||n.some(function(r,s){return r!==e[s]})}function Ge(e,n){return typeof n=="function"?n(e):n}const A="/cgi-bin";async function gn(){const e=await fetch(`${A}/status.sh`);if(!e.ok)throw new Error("Failed to fetch status");return e.json()}async function yn(){const e=await fetch(`${A}/config.sh`);if(!e.ok)throw new Error("Failed to fetch config");return e.json()}async function bn(){const e=await fetch(`${A}/storage.sh`);if(!e.ok)throw new Error("Failed to fetch storage");return e.json()}async function kn(){const e=await fetch(`${A}/videolist.sh`);if(!e.ok)throw new Error("Failed to fetch video list");const n=await e.text();return wn(n)}function wn(e){const n=e.trim().split(` +`).filter(Boolean),r={RecentClips:{},SavedClips:{},SentryClips:{}};for(const s of n){const o=s.split("/");if(o.length>=2){const[i,...a]=o;if(r[i]){const l=a[0];r[i][l]||(r[i][l]=[]),a.length>1&&r[i][l].push(a.slice(1).join("/"))}}}return r}async function Nn(){if(!(await fetch(`${A}/trigger_sync.sh`)).ok)throw new Error("Failed to trigger sync")}async function xn(){const e=await fetch(`${A}/music_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch music sync progress");return e.json()}async function Cn(){if(!(await fetch(`${A}/toggledrives.sh`)).ok)throw new Error("Failed to toggle drives")}async function Ln(){if(!(await fetch(`${A}/reboot.sh`)).ok)throw new Error("Failed to reboot")}async function Sn(){return(await fetch(`${A}/pairBLEkey.sh`)).status===202}async function $n(){return(await(await fetch(`${A}/checkBLEstatus.sh`)).text()).includes("

paired

")}async function Tn(){await fetch(`${A}/diagnose.sh`)}async function Bn(){const e=await fetch("/diagnostics.txt");if(!e.ok)throw new Error("Failed to fetch diagnostics");return e.text()}async function Mn(e,n=0){const r={};n>0&&(r.Range=`bytes=${n}-`);const s=await fetch(`/${e}`,{headers:r});if(s.status===416)return{content:"",size:n,truncated:!1};if(s.status===404)throw new Error("Log file not found");if(!s.ok&&s.status!==206)throw new Error(`Failed to fetch log: ${s.status}`);const o=await s.text();let i=n;if(s.status===206){const a=s.headers.get("Content-Range");if(a){const l=a.match(/bytes \d+-(\d+)\/(\d+)/);l?i=parseInt(l[1],10)+1:i=n+o.length}else i=n+o.length}else i=o.length;return{content:o,size:i,truncated:!1}}async function In(e,n){const r=await fetch(`${A}/randomdata.sh`,{signal:n});if(!r.ok)throw new Error("Failed to start speed test");const s=r.body.getReader();let o=0;const i=performance.now();try{for(;;){const{done:l,value:u}=await s.read();if(l)break;o+=u.length;const d=(performance.now()-i)/1e3,_=o*8/(d*1e6);e&&e(_)}}catch(l){if(l.name!=="AbortError")throw l}const a=(performance.now()-i)/1e3;return o*8/(a*1e6)}function Fn(e,n,r){return`/TeslaCam/${e}/${n}/${r}`}function An(e){return`/TeslaCam/SentryClips/${e}/event.json`}async function En(e){try{const n=await fetch(An(e));return n.ok?n.json():null}catch(n){return null}}function Rn(e=5e3){const[n,r]=k(null),[s,o]=k(null),[i,a]=k(null),[l,u]=k(!0),[d,_]=k(null),[c,h]=k(null),f=B(async()=>{try{const[y,b,m]=await Promise.all([gn(),yn(),bn().catch(()=>null)]);r(y),o(b),a(m),h(new Date),_(null)}catch(y){_(y.message)}finally{u(!1)}},[]);F(()=>{f();const y=setInterval(f,e);return()=>clearInterval(y)},[f,e]);const v=n?{cpuTempC:n.cpu_temp?(parseInt(n.cpu_temp,10)/1e3).toFixed(1):null,uptimeFormatted:Dn(parseInt(n.uptime||"0",10)),diskUsedPercent:n.total_space&&n.free_space?Math.round((parseInt(n.total_space,10)-parseInt(n.free_space,10))/parseInt(n.total_space,10)*100):0,diskUsedGB:n.total_space&&n.free_space?((parseInt(n.total_space,10)-parseInt(n.free_space,10))/(1024*1024*1024)).toFixed(1):"0",diskTotalGB:n.total_space?(parseInt(n.total_space,10)/(1024*1024*1024)).toFixed(1):"0",diskFreeGB:n.free_space?(parseInt(n.free_space,10)/(1024*1024*1024)).toFixed(1):"0",drivesActive:n.drives_active==="yes",wifiConnected:!!n.wifi_ssid&&n.wifi_ssid!=="",wifiSignalPercent:jn(n.wifi_strength),ethernetConnected:!!n.ether_ip&&n.ether_ip!=="",snapshotCount:parseInt(n.num_snapshots||"0",10)}:null;return{status:n,config:s,storage:i,computed:v,loading:l,error:d,lastUpdate:c,refresh:f}}function Dn(e){if(!e||isNaN(e))return"0s";const n=Math.floor(e/86400),r=Math.floor(e%86400/3600),s=Math.floor(e%3600/60),o=e%60,i=[];return n>0&&i.push(`${n}d`),r>0&&i.push(`${r}h`),s>0&&i.push(`${s}m`),(o>0||i.length===0)&&i.push(`${o}s`),i.join(" ")}function jn(e){if(!e)return 0;const n=e.split("/");if(n.length!==2)return 0;const[r,s]=n.map(Number);return isNaN(r)||isNaN(s)||s===0?0:Math.round(r/s*100)}function Wn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2-4H8L6 10l-2.5 1.1C2.7 11.3 2 12.1 2 13v3c0 .6.4 1 1 1h2"}),t("circle",{cx:"7",cy:"17",r:"2"}),t("circle",{cx:"17",cy:"17",r:"2"})]})}function Ke({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"})]})}function Hn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"23 7 16 12 23 17 23 7"}),t("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2",ry:"2"})]})}function Un({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}function de({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"23 4 23 10 17 10"}),t("path",{d:"M20.49 15a9 9 0 1 1-2.12-9.36L23 10"})]})}function Pn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:t("polygon",{points:"5 3 19 12 5 21 5 3"})})}function On({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:[t("rect",{x:"6",y:"4",width:"4",height:"16"}),t("rect",{x:"14",y:"4",width:"4",height:"16"})]})}function Vn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"19 20 9 12 19 4 19 20"}),t("line",{x1:"5",y1:"19",x2:"5",y2:"5"})]})}function zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"5 4 15 12 5 20 5 4"}),t("line",{x1:"19",y1:"5",x2:"19",y2:"19"})]})}function qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),t("polyline",{points:"7 10 12 15 17 10"}),t("line",{x1:"12",y1:"15",x2:"12",y2:"3"})]})}function Gn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"3 6 5 6 21 6"}),t("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}function Kn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M5 12.55a11 11 0 0 1 14.08 0"}),t("path",{d:"M1.42 9a16 16 0 0 1 21.16 0"}),t("path",{d:"M8.53 16.11a6 6 0 0 1 6.95 0"}),t("line",{x1:"12",y1:"20",x2:"12.01",y2:"20"})]})}function Me({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("line",{x1:"22",y1:"12",x2:"2",y2:"12"}),t("path",{d:"M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}),t("line",{x1:"6",y1:"16",x2:"6.01",y2:"16"}),t("line",{x1:"10",y1:"16",x2:"10.01",y2:"16"})]})}function Yn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",ry:"2"}),t("rect",{x:"9",y:"9",width:"6",height:"6"}),t("line",{x1:"9",y1:"1",x2:"9",y2:"4"}),t("line",{x1:"15",y1:"1",x2:"15",y2:"4"}),t("line",{x1:"9",y1:"20",x2:"9",y2:"23"}),t("line",{x1:"15",y1:"20",x2:"15",y2:"23"}),t("line",{x1:"20",y1:"9",x2:"23",y2:"9"}),t("line",{x1:"20",y1:"14",x2:"23",y2:"14"}),t("line",{x1:"1",y1:"9",x2:"4",y2:"9"}),t("line",{x1:"1",y1:"14",x2:"4",y2:"14"})]})}function Zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"4",cy:"20",r:"1"}),t("circle",{cx:"20",cy:"20",r:"1"}),t("circle",{cx:"12",cy:"10",r:"1"}),t("path",{d:"M12 3v7"}),t("path",{d:"M4 20v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"}),t("path",{d:"M12 10v9"}),t("path",{d:"M7 10l5-7 5 7"})]})}function Jn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 2v6h-6"}),t("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"}),t("path",{d:"M3 22v-6h6"}),t("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"})]})}function Xn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M18.36 6.64a9 9 0 1 1-12.73 0"}),t("line",{x1:"12",y1:"2",x2:"12",y2:"12"})]})}function Ye({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"})})}function Ze({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"4 17 10 11 4 5"}),t("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function Qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"12",cy:"12",r:"10"}),t("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),t("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]})}function Je({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6 9 12 15 18 9"})})}function et({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"}),t("circle",{cx:"12",cy:"10",r:"3"})]})}function nt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M9 18V5l12-2v13"}),t("circle",{cx:"6",cy:"18",r:"3"}),t("circle",{cx:"18",cy:"16",r:"3"})]})}function Xe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})]})}function tt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19.4 14.9C20.2 13.4 20.6 11.7 20.6 10c0-5-4-9-9-9s-9 4-9 9 4 9 9 9c1.7 0 3.4-.4 4.9-1.2"}),t("path",{d:"M11.6 10l6.4 6.4"})]})}function rt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2",ry:"2"}),t("line",{x1:"3",y1:"9",x2:"21",y2:"9"}),t("line",{x1:"9",y1:"21",x2:"9",y2:"9"})]})}function it({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"17 1 21 5 17 9"}),t("path",{d:"M3 11V9a4 4 0 0 1 4-4h14"}),t("polyline",{points:"7 23 3 19 7 15"}),t("path",{d:"M21 13v2a4 4 0 0 1-4 4H3"})]})}const st={dashboard:Ke,viewer:Hn,files:Un,logs:Ze};function ot({tabs:e,activeTab:n,onTabChange:r,lastUpdate:s,onRefresh:o}){const i=a=>a?a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"";return t("header",{className:"app-header",children:[t("div",{className:"app-header-left",children:t("nav",{className:"dashboard-nav",children:e.map(a=>{const l=st[a.id]||Ke;return t("button",{className:`nav-link ${n===a.id?"active":""}`,onClick:()=>r(a.id),children:[t(l,{}),t("span",{children:a.label})]},a.id)})})}),t("div",{className:"app-header-right desktop-status-bar",children:[t("span",{className:"app-last-update",style:{marginRight:"12px"},children:s&&`Updated ${i(s)}`}),t("button",{className:"btn",onClick:o,children:[t(de,{}),t("span",{children:"Refresh"})]})]})]})}function at({status:e,computed:n,config:r,expanded:s,onToggle:o,onRefresh:i}){const[a,l]=k(!1),[u,d]=k(!1),[_,c]=k(!1),[h,f]=k(null),[v,y]=k(null),b=async()=>{l(!0);try{await Cn(),setTimeout(i,1e3)}catch(x){console.error("Toggle drives failed:",x)}finally{l(!1)}},m=async()=>{if(confirm("Are you sure you want to restart TeslaUSB?")){d(!0);try{await Ln()}catch(x){console.error("Reboot failed:",x)}}},p=async()=>{c(!0),f(null);const x=new AbortController,L=setTimeout(()=>x.abort(),1e4);try{await In(C=>f(C.toFixed(1)),x.signal)}catch(C){C.name!=="AbortError"&&console.error("Speed test failed:",C)}finally{clearTimeout(L),c(!1)}},w=async()=>{y("pairing");try{if(await Sn()){for(let L=0;L<60;L++)if(await new Promise(U=>setTimeout(U,2e3)),await $n()){y("paired");return}y("timeout")}else y("error")}catch(x){console.error("BLE pairing failed:",x),y("error")}};return t("aside",{className:`app-sidebar ${s?"expanded":""}`,children:[t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Yn,{}),t("span",{children:"System"}),t("button",{className:"sidebar-toggle-btn",onClick:o,children:t(Je,{className:s?"rotate-180":""})})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.device_model)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Model"}),t("span",{className:"info-value small-text",children:e.device_model})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Uptime"}),t("span",{className:"info-value",children:(n==null?void 0:n.uptimeFormatted)||"-"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"CPU Temp"}),t("span",{className:`info-value ${lt(n==null?void 0:n.cpuTempC)}`,children:n!=null&&n.cpuTempC?`${n.cpuTempC}ยฐC`:"-"})]}),t("div",{className:"info-item clickable",onClick:b,children:[t("span",{className:"info-label",children:"USB Drives"}),t("button",{className:`toggle-btn ${n!=null&&n.drivesActive?"active":"danger"}`,disabled:a,children:[a&&t(Zn,{style:{width:12,height:12},className:"spinning"}),n!=null&&n.drivesActive?"Disconnect from host":"Connect to host"]})]}),t("div",{className:"info-item clickable",onClick:m,children:[t("span",{className:"info-label",children:"Power"}),t("button",{className:"toggle-btn danger",disabled:u,children:[u&&t(Xn,{style:{width:12,height:12},className:"spinning"}),"Restart"]})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Kn,{}),t("span",{children:"Network"})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.wifi_ssid)&&t(R,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"WiFi SSID"}),t("span",{className:"info-value",children:e.wifi_ssid})]}),(e==null?void 0:e.wifi_freq)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Frequency"}),t("span",{className:"info-value",children:ct(e.wifi_freq)})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Signal"}),t("span",{className:"info-value",children:[(n==null?void 0:n.wifiSignalPercent)||0,"%"]})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.wifi_ip||"-"})]})]}),(e==null?void 0:e.ether_ip)&&t(R,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Ethernet"}),t("span",{className:"info-value",children:e.ether_speed||"Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.ether_ip})]})]}),!(e!=null&&e.wifi_ssid)&&!(e!=null&&e.ether_ip)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Status"}),t("span",{className:"info-value status-unhealthy",children:"Not Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Speed Test"}),t("span",{className:"info-value-with-action",children:[h&&t("span",{className:"speed-result",children:[h," Mbps"]}),t("button",{className:"toggle-btn",onClick:p,disabled:_,children:[_&&t(tt,{style:{width:12,height:12},className:"spinning"}),_?"Testing...":"Run"]})]})]}),(r==null?void 0:r.uses_ble)==="yes"&&t("div",{className:"info-item clickable",onClick:w,children:[t("span",{className:"info-label",children:"Bluetooth"}),t("button",{className:"toggle-btn",disabled:v==="pairing",children:[v==="pairing"&&t(Ye,{style:{width:12,height:12},className:"spinning"}),v==="pairing"?"Pairing...":v==="paired"?"Paired":v==="error"?"Failed":"Pair"]})]})]})]}),(n==null?void 0:n.snapshotCount)>0&&t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Xe,{}),t("span",{children:"Snapshots"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Count"}),t("span",{className:"info-value",children:n.snapshotCount})]}),(e==null?void 0:e.snapshot_oldest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Oldest"}),t("span",{className:"info-value compact-date",children:Ie(e.snapshot_oldest)})]}),(e==null?void 0:e.snapshot_newest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Newest"}),t("span",{className:"info-value compact-date",children:Ie(e.snapshot_newest)})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(it,{}),t("span",{children:"Switch UI"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:t("a",{href:"/",className:"ui-switch-link",children:"Standard UI"})}),t("div",{className:"info-item",children:t("a",{href:"/new/",className:"ui-switch-link",children:"Vue UI"})})]})]})]})}function lt(e){if(!e)return"";const n=parseFloat(e);return n>=80?"status-unhealthy":n>=70?"status-degraded":"status-healthy"}function Ie(e){if(!e)return"-";try{return new Date(parseInt(e,10)*1e3).toLocaleDateString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch(n){return e}}function ct(e){if(!e)return"-";const n=e.match(/(\d+\.?\d*)/);if(n){const r=parseFloat(n[1]),s=t("span",{style:{fontWeight:"normal"},children:[" (",r.toFixed(3),")"]});return r>=2.4&&r<2.5?t(R,{children:["2.4 GHz",s]}):r>=5&&r<6?t(R,{children:["5 GHz",s]}):r>=6?t(R,{children:["6 GHz",s]}):`${r} GHz`}return e}function Qe(e,n=2e3,r=!0){const[s,o]=k([]),[i,a]=k(!0),[l,u]=k(null),d=H(0),_=H(!0),c=H(e),h=B(async(y=!1)=>{if(e)try{y&&(d.current=0);const b=await Mn(e,d.current);if(b.truncated){d.current=0,o([]);return}if(b.content){const m=b.content.split(` +`).filter(Boolean);o(y?m.slice(-1e3):p=>[...p,...m].slice(-1e3))}d.current=b.size,u(null)}catch(b){u(b.message)}finally{a(!1)}},[e]);F(()=>{e!==c.current&&(c.current=e,d.current=0,o([]),a(!0),u(null))},[e]),F(()=>{if(!r||!e)return;h(!0);const y=setInterval(()=>h(!1),n);return()=>clearInterval(y)},[e,n,r]);const f=B(y=>{_.current=y},[]),v=B(()=>{o([]),d.current=0},[]);return{lines:s,loading:i,error:l,autoScroll:_.current,setAutoScroll:f,refresh:()=>h(!1),clear:v}}function dt(e){const n={state:"idle",totalFiles:0,archivedFiles:0,currentFile:null,startTime:null,elapsedTime:null,lastActivity:null,message:null};if(!e||e.length===0)return n;const r=e.slice(-100);r.length>0&&(n.lastActivity=ht(r[r.length-1]));for(let o=r.length-1;o>=0;o--){const i=r[o],a=i.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/);if(a){const u=parseInt(a[2],10),d=a[3]?parseInt(a[3],10):0;n.totalFiles=u+d;break}const l=i.match(/Archiving (\d+)(?: track mode)? file\(s\)/);l&&!i.includes("completed")&&(n.totalFiles=parseInt(l[1],10))}let s=!1;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Finished copying music")||i.includes("Copying music failed"))break;if(i.includes("Syncing music from archive")||i.includes("Starting music sync")){s=!0;break}if(i.includes("Connected usb to host")||i.includes("Waiting for archive to be unreachable"))break}if(s)return n.state="archiving",n.message="Syncing music...",n;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Archiving completed successfully")){n.state="complete",n.message="Archive completed";break}if(i.includes("Finished copying music")){n.state="complete",n.message="Music sync complete";break}if(i.includes("Copied")&&i.includes("file(s)")){const a=i.match(/Copied (\d+)/);if(a){n.state="complete",n.archivedFiles=parseInt(a[1],10),n.message=`Copied ${n.archivedFiles} files`;break}}if(i.includes("Starting recording archiving")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving recordings...";break}if(i.includes("Archiving...")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving files...";break}if(i.includes("Syncing music from archive")){n.state="archiving",n.message="Syncing music...";break}if(i.includes("Copying music")){n.state="archiving",n.message="Copying music...";break}if(i.includes("Finished archiving")){n.state="complete",n.message="Archive complete";break}if(i.includes("Running fsck")){n.state="archiving",n.message="Checking filesystem...";break}if(i.includes("Checking saved folder count")){n.state="archiving",n.message="Scanning files...";break}if(i.includes("Waiting for archive to be reachable")){n.state="connecting",n.message="Connecting to archive server...";break}if(i.includes("Archive is reachable")){n.state="archiving",n.message="Connected, preparing...";break}if(i.includes("Disconnecting usb from host")){n.state="archiving",n.message="Disconnecting from vehicle...";break}if(i.includes("Connected usb to host")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("Waiting for archive to be unreachable")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("snapshot")){n.state="idle",n.message="Managing snapshots...";break}if(i.includes("low space, deleting")){n.state="idle",n.message="Cleaning up old snapshots...";break}if(i.includes("waiting up to")&&i.includes("idle interval")){n.state="idle",n.message="Waiting for idle...";break}if(i.includes("mass storage process")){n.state="idle",n.message="Ready";break}if((i.includes("error")||i.includes("failed"))&&!i.includes("sntp failed")){n.state="error",n.message="Error occurred";break}}return n}function ht(e){const n=e.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/);if(n){const r=new Date(n[1]);if(!isNaN(r.getTime()))return r}return null}function ut(e=!1,n=1500){const[r,s]=k({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""}),[o,i]=k(!1),[a,l]=k(null),u=H(e),d=B(async()=>{try{const _=await xn();s(_),l(null)}catch(_){l(_.message)}finally{i(!1)}},[]);return F(()=>{u.current=e},[e]),F(()=>{if(!e){s({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""});return}i(!0),d();const _=setInterval(()=>{u.current&&d()},n);return()=>clearInterval(_)},[e,n,d]),{...r,loading:o,error:a,refresh:d}}function ft(e){if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],r=1024,s=Math.floor(Math.log(e)/Math.log(r));return`${(e/Math.pow(r,s)).toFixed(s>0?2:0)} ${n[s]}`}function _t(e){return!e||e==="0:00:00"?"":`~${e.replace(/^0:/,"")} remaining`}function pt({storage:e,total:n,free:r,config:s}){var c;const o=h=>{if(h===0||h===null||h===void 0)return"0 B";const f=h/(1024*1024*1024);return f>=1?`${f.toFixed(1)} GB`:`${(h/(1024*1024)).toFixed(0)} MB`},i=((c=e==null?void 0:e.total)==null?void 0:c.free)||r,a=[],l=(h,f,v,y)=>{(s==null?void 0:s[y])!=="yes"||!h||a.push({type:f,label:v,allocated:h.total,used:h.used,mounted:h.mounted,isCached:h.cached||!1})};if(l(e==null?void 0:e.cam,"teslacam","TeslaCam","has_cam"),l(e==null?void 0:e.music,"music","Music","has_music"),l(e==null?void 0:e.lightshow,"lightshow","LightShow","has_lightshow"),l(e==null?void 0:e.boombox,"boombox","Boombox","has_boombox"),a.length===0&&!i)return t("div",{className:"storage-bar-container",children:t("div",{className:"storage-info",children:"No storage data available"})});const d=a.reduce((h,f)=>h+(f.allocated||0),0)+i,_=a.map(h=>({type:h.type,label:h.label,bytes:h.allocated,percent:Math.max(1,Math.round(h.allocated/d*100))}));return i>0&&_.push({type:"free",label:"Free",bytes:i,percent:Math.max(1,Math.round(i/d*100))}),t("div",{className:"storage-bar-container",children:[t("div",{className:"storage-bar",children:_.map((h,f)=>t("div",{className:`storage-segment ${h.type}`,style:{width:`${h.percent}%`},title:`${h.label}: ${o(h.bytes)}`},f))}),t("div",{className:"storage-legend",children:[a.map((h,f)=>{const v=h.used!==null&&h.allocated>0?Math.round(h.used/h.allocated*100):null;return t("div",{className:"storage-legend-item",children:[t("div",{className:`storage-legend-dot ${h.type}`}),t("span",{className:"storage-legend-label",children:h.label}),t("span",{className:"storage-legend-value",children:[o(h.allocated),h.used!==null&&t("span",{className:"storage-legend-used",children:[" ","(",o(h.used)," used",h.isCached?"~":"",v!==null&&`, ${v}%`,")"]})]})]},f)}),t("div",{className:"storage-legend-item",children:[t("div",{className:"storage-legend-dot free"}),t("span",{className:"storage-legend-label",children:"Free"}),t("span",{className:"storage-legend-value",children:o(i)})]})]}),a.some(h=>h.isCached)&&t("div",{className:"storage-note",children:"~ Last known (drive not currently mounted)"})]})}function mt({syncStatus:e,onTriggerSync:n,loading:r,musicProgress:s}){const{state:o,totalFiles:i,archivedFiles:a,message:l,elapsedTime:u,lastActivity:d}=e,_=(l==null?void 0:l.toLowerCase().includes("music"))&&o==="archiving",c=(s==null?void 0:s.active)&&(s==null?void 0:s.percentage)>0,f=(()=>{switch(o){case"idle":return{label:"Idle",color:"idle",description:"Ready to archive"};case"connecting":return{label:"Connecting",color:"connecting",description:l||"Connecting to server..."};case"archiving":return{label:"Archiving",color:"archiving",description:l||"Syncing files..."};case"complete":return{label:"Complete",color:"idle",description:l||"Archive complete"};case"error":return{label:"Error",color:"error",description:l||"Archive failed"};default:return{label:"Unknown",color:"idle",description:"Status unknown"}}})(),v=o==="archiving"||o==="connecting",y=_&&c,b=o==="archiving"&&i>0&&!y;let m=null;y?m=s.percentage:b&&a>0&&(m=Math.min(Math.round(a/i*100),100));const p=w=>{if(!w)return null;const L=Math.floor((new Date-w)/1e3);return L<60?"just now":L<3600?`${Math.floor(L/60)}m ago`:L<86400?`${Math.floor(L/3600)}h ago`:w.toLocaleDateString()};return t("div",{className:"sync-status-card",children:[t("div",{className:"sync-status-header",children:[t("span",{className:"sync-status-title",children:"Sync Status"}),t("div",{className:"sync-status-indicator",children:[t("div",{className:`sync-status-dot ${f.color}`}),t("span",{children:f.label})]})]}),t("div",{className:"sync-status-description",children:f.description}),(y||b||v)&&t("div",{className:"sync-progress-bar",children:t("div",{className:"sync-progress-fill",style:{width:m!==null?`${m}%`:"100%",animation:m===null?"pulse 1.5s ease-in-out infinite":"none",opacity:m===null?.6:1}})}),y&&t("div",{className:"sync-details",children:[t("div",{className:"sync-progress-main",children:[ft(s.bytesTransferred)," transferred",m!==null&&` (${m}%)`]}),(s.speed||s.eta)&&t("div",{className:"sync-progress-secondary",children:[s.speed&&t("span",{children:s.speed}),s.speed&&s.eta&&t("span",{children:" ยท "}),s.eta&&t("span",{children:_t(s.eta)})]})]}),b&&t("div",{className:"sync-details",children:[a," / ",i," files",m!==null&&` (${m}%)`]}),!v&&!b&&!y&&d&&t("div",{className:"sync-details",children:["Last sync: ",p(d)]}),u&&o==="complete"&&t("div",{className:"sync-details",children:["Completed in ",u]}),(o==="idle"||o==="complete"||o==="error")&&t("button",{className:"btn btn-primary btn-sm",onClick:n,disabled:r,style:{marginTop:"0.75rem"},children:[t(Jn,{style:{width:14,height:14},className:r?"spinning":""}),t("span",{children:r?"Starting...":"Sync Now"})]})]})}function vt({status:e,computed:n,config:r,storage:s,onRefresh:o}){const[i,a]=k(!1),{lines:l}=Qe("archiveloop.log",3e3,!0),u=dt(l),d=ve(()=>{var f;return u.state==="archiving"&&((f=u.message)==null?void 0:f.toLowerCase().includes("music"))},[u.state,u.message]),_=ut(d,1500),c=B(async()=>{a(!0);try{await Nn(),setTimeout(o,1e3)}catch(f){console.error("Trigger sync failed:",f)}finally{a(!1)}},[o]),h=[{key:"cam",label:"TeslaCam",icon:Xe,enabled:(r==null?void 0:r.has_cam)==="yes"},{key:"music",label:"Music",icon:nt,enabled:(r==null?void 0:r.has_music)==="yes"},{key:"lightshow",label:"LightShow",icon:Me,enabled:(r==null?void 0:r.has_lightshow)==="yes"},{key:"boombox",label:"Boombox",icon:Me,enabled:(r==null?void 0:r.has_boombox)==="yes"}];return(r==null?void 0:r.uses_ble)==="yes"&&h.push({key:"ble",label:"BLE",icon:Ye,enabled:!0}),t("div",{className:"dashboard-content",children:[t("div",{className:"dashboard-section",children:[t("div",{className:"section-title",children:"Configured Features"}),t("div",{className:"features-row",children:h.map(({key:f,label:v,enabled:y})=>t("div",{className:`feature-badge ${y?"enabled":"disabled"}`,children:v},f))})]}),t("div",{className:"dashboard-section",children:t("div",{className:"card",children:[t("div",{className:"card-header",children:[t("span",{className:"card-title",children:"Storage"}),t("span",{className:"card-value",children:[(n==null?void 0:n.diskTotalGB)||"0"," GB"]})]}),t(pt,{storage:s,total:e!=null&&e.total_space?parseInt(e.total_space,10):0,free:e!=null&&e.free_space?parseInt(e.free_space,10):0,config:r})]})}),t("div",{className:"dashboard-section",children:t(mt,{syncStatus:u,onTriggerSync:c,loading:i,musicProgress:_})})]})}const he={front:"Front",back:"Back",left_repeater:"Left Repeater",right_repeater:"Right Repeater",left_pillar:"Left Pillar",right_pillar:"Right Pillar"},Fe=[{id:"6",name:"All Cameras",cameras:Object.keys(he),cols:3},{id:"4-front",name:"Front Focus",cameras:["front","left_repeater","right_repeater","back"],cols:2},{id:"4-rear",name:"Rear Focus",cameras:["back","left_repeater","right_repeater","front"],cols:2},{id:"2-side",name:"Side View",cameras:["left_repeater","right_repeater"],cols:2},{id:"1-front",name:"Front Only",cameras:["front"],cols:1},{id:"1-back",name:"Rear Only",cameras:["back"],cols:1}];function gt(){const[e,n]=k(null),[r,s]=k(!0),[o,i]=k(null),[a,l]=k("SentryClips"),[u,d]=k(null),[_,c]=k(!1),[h,f]=k(0),[v,y]=k(0),[b,m]=k(Fe[0]),[p,w]=k(!1),[x,L]=k(null),C=H({});F(()=>{U()},[]);const U=async()=>{try{s(!0);const g=await kn();n(g);for(const T of["SentryClips","SavedClips","RecentClips"]){const W=Object.keys(g[T]||{});if(W.length>0){l(T),d(W[0]);break}}}catch(g){i(g.message)}finally{s(!1)}};F(()=>{a==="SentryClips"&&u?En(u).then(L):L(null)},[a,u]);const D=B(()=>{var W;if(!e||!u)return{};const g=((W=e[a])==null?void 0:W[u])||[],T={};for(const q of g)for(const ie of Object.keys(he))if(q.includes(`-${ie}.mp4`)||q.includes(`_${ie}.mp4`)){T[ie]=Fn(a,u,q);break}return T},[e,a,u])(),z=B(()=>{Object.values(C.current).forEach(g=>{g&&g.play()}),c(!0)},[]),M=B(()=>{Object.values(C.current).forEach(g=>{g&&g.pause()}),c(!1)},[]),I=B(g=>{Object.values(C.current).forEach(T=>{T&&(T.currentTime=g)}),f(g)},[]),en=B(()=>{I(Math.max(0,h-10))},[h,I]),nn=B(()=>{I(Math.min(v,h+30))},[h,v,I]),tn=B(g=>{f(g.target.currentTime)},[]),rn=B(g=>{g.target.duration&&g.target.duration!==1/0&&y(g.target.duration)},[]),sn=B(g=>{const T=g.currentTarget.getBoundingClientRect(),q=(g.clientX-T.left)/T.width*v;I(q)},[v,I]),ge=g=>{if(!g||isNaN(g))return"0:00";const T=Math.floor(g/60),W=Math.floor(g%60);return`${T}:${W.toString().padStart(2,"0")}`},on=e!=null&&e[a]?Object.keys(e[a]).sort().reverse():[];return r?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"Loading recordings..."})}):o?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:[t("span",{className:"status-unhealthy",children:["Error: ",o]}),t("button",{className:"btn",onClick:U,style:{marginTop:"1rem"},children:"Retry"})]}):!u||Object.keys(D).length===0?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"No recordings available"})}):t("div",{className:"video-viewer",children:[t("div",{className:"video-selector",style:{display:"flex",gap:"0.5rem",padding:"8px 12px",background:"#1a1a1a",borderBottom:"1px solid #333",alignItems:"center",flexWrap:"wrap"},children:[t("select",{value:a,onChange:g=>{l(g.target.value);const T=Object.keys(e[g.target.value]||{});d(T[0]||null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px"},children:[t("option",{value:"SentryClips",children:"Sentry Clips"}),t("option",{value:"SavedClips",children:"Saved Clips"}),t("option",{value:"RecentClips",children:"Recent Clips"})]}),t("select",{value:u||"",onChange:g=>d(g.target.value),style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",flex:1,maxWidth:"250px"},children:on.map(g=>t("option",{value:g,children:yt(g)},g))}),t("div",{style:{position:"relative"},children:[t("button",{className:"btn btn-sm",onClick:()=>w(!p),style:{background:"#333",borderColor:"#444"},children:[t(rt,{}),t("span",{children:b.name}),t(Je,{})]}),p&&t("div",{style:{position:"absolute",top:"100%",right:0,background:"#2a2a2a",border:"1px solid #444",borderRadius:"6px",marginTop:"4px",zIndex:100,minWidth:"150px"},children:Fe.map(g=>t("button",{onClick:()=>{m(g),w(!1)},style:{display:"block",width:"100%",padding:"8px 12px",background:b.id===g.id?"#0095f6":"transparent",color:"#fff",border:"none",textAlign:"left",cursor:"pointer",fontSize:"13px"},children:g.name},g.id))})]}),x&&x.city&&t("div",{style:{display:"flex",alignItems:"center",gap:"4px",color:"#888",fontSize:"12px",marginLeft:"auto"},children:[t(et,{style:{width:14,height:14}}),t("span",{children:x.city})]})]}),t("div",{className:`video-grid layout-${b.cameras.length}`,style:{gridTemplateColumns:`repeat(${b.cols}, 1fr)`},children:b.cameras.map(g=>t("div",{className:"video-cell",children:[D[g]?t("video",{ref:T=>{C.current[g]=T},src:D[g],onTimeUpdate:tn,onDurationChange:rn,onEnded:M,muted:!0,playsInline:!0}):t("div",{style:{color:"#666",fontSize:"12px"},children:"No video"}),t("div",{className:"video-cell-label",children:he[g]})]},g))}),t("div",{className:"video-controls",children:[t("button",{className:"btn btn-sm",onClick:en,title:"Skip back 10s",children:t(Vn,{})}),t("button",{className:"video-play-btn",onClick:_?M:z,children:_?t(On,{}):t(Pn,{})}),t("button",{className:"btn btn-sm",onClick:nn,title:"Skip forward 30s",children:t(zn,{})}),t("div",{className:"video-time",children:[ge(h)," / ",ge(v)]}),t("div",{className:"video-timeline",onClick:sn,children:t("div",{className:"video-timeline-progress",style:{width:v>0?`${h/v*100}%`:"0%"}})})]})]})}function yt(e){const n=e.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/);if(n){const[,r,s,o,i,a,l]=n,u=new Date(r,s-1,o,i||0,a||0,l||0);return i?u.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):u.toLocaleDateString([],{month:"short",day:"numeric",year:"numeric"})}return e}function bt({config:e}){const n=H(null),r=H(null),[s,o]=k(!!window.FileBrowser);F(()=>{if(!document.querySelector('link[href="/filebrowser.css"]')){const d=document.createElement("link");d.rel="stylesheet",d.href="/filebrowser.css",document.head.appendChild(d)}},[]),F(()=>{if(window.FileBrowser){o(!0);return}const d=document.createElement("script");d.src="/filebrowser.js",d.onload=()=>o(!0),d.onerror=()=>console.error("Failed to load filebrowser.js"),document.head.appendChild(d)},[]);const i=(e==null?void 0:e.has_music)==="yes",a=(e==null?void 0:e.has_lightshow)==="yes",l=(e==null?void 0:e.has_boombox)==="yes";return F(()=>{if(!s||!n.current)return;const d=[];if(i&&d.push({path:"fs/Music",label:"Music"}),a&&d.push({path:"fs/LightShow",label:"LightShow"}),l&&d.push({path:"fs/Boombox",label:"Boombox"}),d.length!==0&&!r.current){try{r.current=new window.FileBrowser(n.current,d)}catch(_){console.error("Failed to initialize FileBrowser:",_)}return()=>{n.current&&(n.current.innerHTML=""),r.current=null}}},[s,i,a,l]),(e==null?void 0:e.has_music)==="yes"||(e==null?void 0:e.has_lightshow)==="yes"||(e==null?void 0:e.has_boombox)==="yes"?s?t("div",{ref:n,style:{width:"100%",height:"calc(100% - 4px)",minHeight:"400px",flex:1,display:"flex",position:"relative"}}):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",minHeight:"400px",color:"#9ca3af",fontSize:"14px"},children:"Loading file browser..."}):t("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:"#9ca3af",fontSize:"14px"},children:[t("svg",{style:{width:32,height:32,marginBottom:8,color:"#d1d5db"},viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})}),t("span",{children:"No file drives configured"})]})}const J={archiveloop:{label:"Archive Log",file:"archiveloop.log",description:"Sync and archive operations"},setup:{label:"Setup Log",file:"teslausb-headless-setup.log",description:"Initial setup and configuration"},diagnostics:{label:"Diagnostics",file:"diagnostics.txt",description:"System diagnostics report"}};function kt(){const[e,n]=k("archiveloop"),[r,s]=k(!1),[o,i]=k(null),a=H(null),l=e!=="diagnostics",{lines:u,loading:d,error:_,refresh:c,clear:h}=Qe(l?J[e].file:"",2e3,l);F(()=>{a.current&&l&&(a.current.scrollTop=a.current.scrollHeight)},[u,l]);const f=B(async()=>{s(!0);try{await Tn(),await new Promise(w=>setTimeout(w,2e3));const p=await Bn();i(p)}catch(p){i(`Error generating diagnostics: ${p.message}`)}finally{s(!1)}},[]);F(()=>{e==="diagnostics"&&!o&&f()},[e,o,f]);const v=B(()=>{const p=e==="diagnostics"?o:u.join(` +`),w=J[e].file,x=new Blob([p],{type:"text/plain"}),L=URL.createObjectURL(x),C=document.createElement("a");C.href=L,C.download=w,document.body.appendChild(C),C.click(),document.body.removeChild(C),URL.revokeObjectURL(L)},[e,u,o]),y=p=>{const w=p.toLowerCase();return w.includes("error")||w.includes("failed")||w.includes("fatal")?"error":w.includes("warning")||w.includes("warn")?"warning":w.includes("success")||w.includes("completed")||w.includes("finished")?"success":""},b=J[e],m=e==="diagnostics"?(o==null?void 0:o.split(` +`))||[]:u;return t("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[t("div",{style:{display:"flex",gap:"0.5rem",marginBottom:"1rem",flexWrap:"wrap"},children:Object.entries(J).map(([p,w])=>t("button",{className:`btn ${e===p?"btn-primary":""}`,onClick:()=>n(p),children:[p==="diagnostics"?t(Qn,{}):t(Ze,{}),t("span",{children:w.label})]},p))}),t("div",{className:"log-viewer",style:{flex:1},children:[t("div",{className:"log-viewer-header",children:[t("div",{className:"log-viewer-title",children:[b.label,t("span",{style:{fontWeight:400,marginLeft:"8px",opacity:.7},children:["โ€” ",b.description]})]}),t("div",{className:"log-viewer-actions",children:[e==="diagnostics"?t("button",{className:"btn btn-sm log-action-btn",onClick:f,disabled:r,children:[t(de,{className:r?"spinning":""}),t("span",{children:"Regenerate"})]}):t(R,{children:[t("button",{className:"btn btn-sm log-action-btn",onClick:c,disabled:d,title:"Refresh",children:t(de,{className:d?"spinning":""})}),t("button",{className:"btn btn-sm log-action-btn",onClick:h,title:"Clear",children:t(Gn,{})})]}),t("button",{className:"btn btn-sm log-action-btn",onClick:v,disabled:m.length===0,title:"Download",children:t(qn,{})})]})]}),t("div",{className:"log-viewer-content",ref:a,children:d&&m.length===0||r?t("div",{style:{color:"#888",fontStyle:"italic"},children:"Loading..."}):_?_.includes("not found")?t("div",{style:{color:"#888",fontStyle:"italic"},children:["Log file not available. ",e==="setup"&&"The setup log is only present during initial setup."]}):t("div",{style:{color:"#f87171"},children:["Error: ",_]}):m.length===0?t("div",{style:{color:"#888",fontStyle:"italic"},children:"No log entries"}):m.map((p,w)=>t("div",{className:`log-line ${y(p)}`,children:p},w))})]}),l&&u.length>0&&t("div",{style:{marginTop:"0.5rem",fontSize:"11px",color:"#666",display:"flex",gap:"1rem"},children:[t("span",{children:[u.length," lines"]}),t("span",{children:"Auto-refreshing every 2s"})]})]})}function wt(){return t("div",{className:"loading-container",children:[t("div",{className:"spinner"}),t("span",{children:"Loading..."})]})}const E={DASHBOARD:"dashboard",VIEWER:"viewer",FILES:"files",LOGS:"logs"};function Nt(){const[e,n]=k(E.DASHBOARD),[r,s]=k(!1),{status:o,config:i,storage:a,computed:l,loading:u,error:d,lastUpdate:_,refresh:c}=Rn(5e3),h=[];return h.push({id:E.DASHBOARD,label:"Dashboard"}),(i==null?void 0:i.has_cam)==="yes"&&h.push({id:E.VIEWER,label:"Viewer"}),((i==null?void 0:i.has_music)==="yes"||(i==null?void 0:i.has_lightshow)==="yes"||(i==null?void 0:i.has_boombox)==="yes")&&h.push({id:E.FILES,label:"Files"}),h.push({id:E.LOGS,label:"Logs"}),u&&!o?t(wt,{}):d&&!o?t("div",{className:"error-container",children:[t("span",{children:["Failed to load: ",d]}),t("button",{className:"retry-btn",onClick:c,children:"Retry"})]}):t("div",{className:"app-shell",children:[t("div",{className:"app-topbar",children:[t("div",{className:"app-topbar-title",children:[t(Wn,{}),t("span",{children:"TeslaUSB"})]}),t("div",{className:"app-topbar-actions",children:t("span",{className:`app-status ${l!=null&&l.drivesActive?"healthy":"warning"}`,children:l!=null&&l.drivesActive?"Connected":"Disconnected"})})]}),t(ot,{tabs:h,activeTab:e,onTabChange:n,lastUpdate:_,onRefresh:c}),t("div",{className:"mobile-quick-status",children:[t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.drivesActive?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"USB"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.wifiConnected?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"WiFi"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.cpuTempC&&parseFloat(l.cpuTempC)<70?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:[l==null?void 0:l.cpuTempC,"ยฐC"]})]})]}),t("div",{className:"app-body",children:[e===E.DASHBOARD&&t(at,{status:o,computed:l,config:i,expanded:r,onToggle:()=>s(!r),onRefresh:c}),t("main",{className:"app-main",children:[e===E.DASHBOARD&&t(vt,{status:o,computed:l,config:i,storage:a,onRefresh:c}),e===E.VIEWER&&(i==null?void 0:i.has_cam)==="yes"&&t(gt,{}),e===E.FILES&&t(bt,{config:i}),e===E.LOGS&&t(kt,{})]})]})]})}fn(t(Nt,{}),document.getElementById("app")); diff --git a/teslausb-www-react/dist/filebrowser.css b/teslausb-www-react/dist/filebrowser.css new file mode 100644 index 00000000..76e10734 --- /dev/null +++ b/teslausb-www-react/dist/filebrowser.css @@ -0,0 +1,592 @@ +/* FileBrowser CSS - Adapted for TeslaUSB React UI */ +/* Uses Lato font and consistent color scheme with main UI */ + +.fb-dragimage { + width: 300px; + height: 150px; + top: -500px; + background: #0095f6; + position: fixed; +} + +.fb-splitter { + width: 3px; + background: #e5e7eb; + border-style: solid; + border-color: #d1d5db; + border-width: 0 1px; + cursor: col-resize; + margin-left: 1px; + margin-right: 1px; + touch-action: none; +} + +.fb-splitterflag { + position: fixed; + width: 40px; + height: 64px; + top: 50%; + background: #e5e7eb; + border-style: solid; + border-color: #d1d5db; + border-width: 1px 0px 1px 1px; + cursor: col-resize; + touch-action: none; +} + +@media(hover: hover) { + .fb-splitterflag { + visibility: hidden; + } +} + +.fb-selection-rect { + position: absolute; + background-color: rgba(0, 149, 246, 0.3); +} + +.fb-tree { + overflow-y: auto; + flex-grow: 1; + padding-left: 30px; + margin-top: 4px; + margin-bottom: 0; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-tree ul { + padding-left: 20px; +} + +.fb-tree li { + display: block; + position: relative; +} + +details summary::-webkit-details-marker { + display: none; +} + +.fb-tree summary { + display: block; + cursor: pointer; + width: max-content; + padding: 4px 8px; + margin-left: 2px; + color: #1f2937; + font-weight: 500; + font-size: 13px; + border-radius: 4px; +} + +.fb-tree summary:hover { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-tree summary::before { + content: ''; + top: 4px; + left: -18px; + width: 14px; + height: 14px; + display: block; + position: absolute; + background: #e5e7eb; + border-radius: 50%; +} + +.fb-tree summary:has(+ ul:not(:empty))::before { + background: #93c5fd; +} + +.fb-tree details[open] > summary:has(+ ul:not(:empty))::before { + background: #0095f6; +} + +summary.fb-droptarget, +.fb-direntry.fb-droptarget, +.fb-fileentry.fb-droptarget { + color: #fff; + background: #22c55e; + border-radius: 4px; + box-shadow: 0 0 0 2px #22c55e; +} + +summary.fb-droptarget.fb-selected, +.fb-direntry.fb-droptarget.fb-selected, +.fb-fileentry.fb-droptarget.fb-selected { + color: #fff; + background: #22c55e; + border-radius: 4px; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-fileslist.fb-droptarget { + box-shadow: inset 0 0 0 3px #22c55e; +} + +.fb-treediv > ul.fb-droptarget, +details > ul.fb-droptarget { + box-shadow: inset 0 0 0 3px #22c55e; +} + +.fb-treerootpath { + position: relative; + padding: 8px 12px; + font-weight: 600; + font-size: 14px; + border-bottom: 1px solid #e5e7eb; + overflow: hidden; + white-space: nowrap; + min-height: 36px; + max-height: 36px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + color: #1f2937; + display: flex; + align-items: center; +} + +.fb-diskinfo-outer { + position: absolute; + right: 4px; + bottom: 8px; + width: 20px; + height: 20px; + background: white; +} + +.fb-diskinfo-inner { + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + border-color: #9ca3af; + border: 1px solid; + border-radius: 50%; + font-weight: normal; + color: #9ca3af; + text-align: center; + font-size: 11px; + line-height: 14px; +} + +.fb-diskinfo-inner::before { + content: 'i'; +} + +.fb-diskinfo-outer:hover .fb-diskinfo-inner { + color: #0095f6; + background: #dbeafe; + border-color: #0095f6; +} + +.fb-diskinfo { + visibility: hidden; + position: fixed; + margin-top: 24px; + height: auto; + width: auto; + z-index: 1; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 8px 12px; + text-align: center; + background: #fff; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + font-size: 12px; + color: #6b7280; +} + +.fb-diskinfo-outer:hover .fb-diskinfo { + visibility: visible; +} + +.fb-treerootpath:has(.fb-treerootpathsinglelabel) { + padding: 8px 12px; + min-height: 36px; + max-height: 36px; +} + +.fb-dirpath { + padding: 8px 12px; + font-weight: 600; + font-size: 14px; + border-bottom: 1px solid #e5e7eb; + overflow: hidden; + white-space: nowrap; + min-height: 36px; + max-height: 36px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + color: #1f2937; +} + +.fb-crumb { + color: #0095f6; + cursor: pointer; + display: inline-block; +} + +.fb-crumb:hover { + text-decoration: underline; +} + +.fb-crumb.fb-droptarget { + text-decoration: underline; + text-decoration-thickness: 2px; +} + +.fb-fileentry { + cursor: pointer; + padding: 4px 8px; + width: max-content; + margin-left: 1px; + font-size: 13px; + color: #374151; + border-radius: 4px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-fileentry:hover:not([contenteditable="true"]) { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-fileentry.fb-selected { + color: #1e40af; + background: #dbeafe; +} + +.fb-fileentry.fb-selected:hover:not([contenteditable="true"]) { + color: #fff; + background: #2563eb; + border-radius: 4px; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-direntry { + cursor: pointer; + padding: 4px 8px; + font-weight: 500; + width: max-content; + margin-left: 1px; + font-size: 13px; + color: #1f2937; + border-radius: 4px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-direntry:hover:not([contenteditable="true"]) { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-direntry.fb-selected { + color: #1e40af; + background: #dbeafe; +} + +.fb-direntry.fb-selected:hover:not([contenteditable="true"]) { + color: #fff; + background: #2563eb; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-treediv { + display: flex; + flex-direction: column; + float: left; + background: #fafafa; + min-width: 15%; + max-width: 85%; + width: 30%; + height: 100%; + overflow-y: auto; + user-select: none; + border-right: 1px solid #e5e7eb; +} + +.fb-filesdiv { + flex: 1; + display: flex; + flex-direction: column; + float: left; + background: #fff; + height: 100%; + margin-left: 0; + overflow-x: auto; + user-select: none; +} + +.fb-fileslist { + position: relative; + height: 100%; + margin-top: 4px; + overflow-y: auto; + flex-grow: 1; + padding: 4px; +} + +.fb-playertitle { + color: #fff; + height: 30px; + padding: 10px 8px 6px 8px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-playertitle::-webkit-scrollbar { + display: none; +} + +.fb-player { + position: absolute; + width: 300px; + height: 100px; + left: 50%; + top: 75%; + background: #1f2937; + border: none; + border-radius: 12px; + padding: 10px; + transform: translate(-50%, -50%); +} + +.fb-dropinfo-holder { + position: fixed; + visibility: hidden; + z-index: 98; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: rgba(0, 0, 0, 0); +} + +.fb-dropinfo { + width: 350px; + height: 150px; + top: calc(100% / 2 - 75px); + left: calc(100% / 2 - 175px); + border-radius: 8px; + position: relative; + visibility: inherit; + background: white; + border: 1px solid #e5e7eb; + z-index: 99; + padding: 1px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + user-select: none; + outline: none !important; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-dropinfo-line1 { + position: absolute; + top: 16px; + left: 20px; + font-weight: 600; + color: #1f2937; +} + +.fb-dropinfo-line2 { + position: absolute; + left: 20px; + right: 20px; + top: 40px; + height: 30px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + color: #6b7280; + font-size: 13px; +} + +.fb-dropinfo-line2::-webkit-scrollbar { + display: none; +} + +.fb-dropinfo-line3 { + position: absolute; + left: 20px; + right: 60px; + bottom: 5px; + height: 30px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + color: #6b7280; + font-size: 12px; +} + +.fb-dropinfo-line3::-webkit-scrollbar { + display: none; +} + +.fb-dropinfo-closebutton { + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + background: white; + text-align: center; + cursor: pointer; + border-radius: 4px; + color: #9ca3af; + line-height: 24px; +} + +.fb-dropinfo-closebutton:hover { + background: #f3f4f6; + color: #1f2937; +} + +.fb-dropinfo-cancel { + position: absolute; + bottom: 12px; + right: 12px; + padding: 6px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 13px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-dropinfo-cancel:hover { + background: #f3f4f6; +} + +.fb-dropinfo-progress { + position: absolute; + left: 20px; + width: calc(100% - 40px); + top: 90px; + height: 8px; + border-radius: 4px; + overflow: hidden; + background: #e5e7eb; + -webkit-appearance: none; + appearance: none; +} + +.fb-dropinfo-progress::-webkit-progress-bar { + background: #e5e7eb; + border-radius: 4px; +} + +.fb-dropinfo-progress::-webkit-progress-value { + background: #0095f6; + border-radius: 4px; +} + +.fb-dropinfo-progress::-moz-progress-bar { + background: #0095f6; + border-radius: 4px; +} + +.fb-barbutton { + float: left; + width: 40px; + height: 40px; + border: none; + padding: 0; + z-index: 3; + display: none; + cursor: pointer; + border-radius: 8px; + margin: 4px; + background-size: 24px 24px; + background-position: center; + background-repeat: no-repeat; +} + +.fb-barbutton:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.fb-barbutton.fb-visiblebarbutton { + display: block; +} + +.fb-trashbutton { + background-image: url(icons/trash.svg); +} + +.fb-pencilbutton { + background-image: url(icons/pencil.svg); +} + +.fb-uploadbutton { + background-image: url(icons/upload.svg); +} + +.fb-downloadbutton { + background-image: url(icons/download.svg); +} + +.fb-newfolderbutton { + background-image: url(icons/newfolder.svg); +} + +.fb-locksoundbutton { + background-image: url(icons/locksound.svg); +} + +.fb-buttonbar { + position: fixed; + right: 24px; + bottom: 24px; + height: 48px; + padding: 0 4px; + background: #fff; + z-index: 2; + border-radius: 12px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + border: 1px solid #e5e7eb; + display: flex; + align-items: center; +} + +@media(hover: hover) { + .uploadbutton { + display: none; + } +} + +.fb-driveselector { + font-size: 14px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + background: transparent; + border-width: 1px; + border-style: solid; + border-color: #d1d5db; + border-radius: 4px; + padding: 4px 8px; + color: #1f2937; +} + +.fb-driveselector:focus { + outline: none; + border-color: #0095f6; +} diff --git a/teslausb-www-react/dist/filebrowser.js b/teslausb-www-react/dist/filebrowser.js new file mode 100644 index 00000000..c8b513fc --- /dev/null +++ b/teslausb-www-react/dist/filebrowser.js @@ -0,0 +1,1315 @@ +class FileBrowser { + DEBUG = false; + splitter_active = false; + splitter_clickoffset = 0; + root_path = ''; + root_label = ''; + anchor_elem = undefined; + dragged_path = undefined; + cancelUpload = false; + uploading = false; + drives = []; + curdrive = 0; + + constructor(anchor, drives) { + this.anchor_elem = anchor; + this.drives = drives; + this.root_path = drives[this.curdrive].path; + this.root_label = drives[this.curdrive].label; + + this.anchor_elem.style.position = "relative"; + this.anchor_elem.style.display = "flex"; + this.anchor_elem.spellcheck = false; + this.anchor_elem.innerHTML = + ` +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
    +
    +
    +
    +
    +
    +
    +
    + `; + + this.dragStart = this.dragStart.bind(this); + this.dragEnd = this.dragEnd.bind(this); + this.allowDrop = this.allowDrop.bind(this); + this.dragEnter = this.dragEnter.bind(this); + this.drop = this.drop.bind(this); + this.readPaths = this.readPaths.bind(this); + this.dirClicked = this.dirClicked.bind(this); + this.fileClicked = this.fileClicked.bind(this); + this.listPointerDown = this.listPointerDown.bind(this); + this.splitterPointerDown = this.splitterPointerDown.bind(this); + this.splitterPointerMove = this.splitterPointerMove.bind(this); + this.splitterPointerUp = this.splitterPointerUp.bind(this); + this.showContextMenu = this.showContextMenu.bind(this); + this.hideContextMenu = this.hideContextMenu.bind(this); + + const splitter = this.anchor_elem.querySelector(".fb-splitter"); + splitter.onpointerdown = (e) => { this.splitterPointerDown(e); }; + const splitterflag = this.anchor_elem.querySelector(".fb-splitterflag"); + splitterflag.onpointerdown = (e) => { this.splitterPointerDown(e); }; + this.splitterSetFlagPos(); + + const fileList = this.anchor_elem.querySelector('.fb-fileslist'); + fileList.onpointerdown = (e) => { this.listPointerDown(e); }; + fileList.oncontextmenu = (e) => { this.showContextMenu(e); }; + fileList.addEventListener("dragstart", this.dragStart); + this.anchor_elem.addEventListener("dragend", this.dragEnd); + + const rootlabel = this.anchor_elem.querySelector(".fb-treerootpath"); + if (this.drives.length > 1) { + var rootlabeldropdown = '
    ' + rootlabel.innerHTML = rootlabeldropdown; + const selector = this.anchor_elem.querySelector(".fb-driveselector"); + selector.onchange = (e) => { + this.curdrive = selector.value; + this.root_path = this.drives[this.curdrive].path; + this.root_label = this.drives[this.curdrive].label; + this.ls(".", false); + this.ls(".", true); + this.updateButtonBar(); + }; + } else { + rootlabel.innerHTML = `${this.drives[0].label}
    `; + } + + this.buttonbar = this.anchor_elem.querySelector(".fb-buttonbar"); + this.buttonbar.querySelector(".fb-uploadbutton").onclick = (e) => { this.pickFile(); }; + this.buttonbar.querySelector(".fb-downloadbutton").onclick = (e) => { this.downloadSelection(); }; + this.buttonbar.querySelector(".fb-newfolderbutton").onclick = (e) => { this.newFolder(); }; + this.buttonbar.querySelector(".fb-trashbutton").onclick = (e) => { this.deleteItems(this.selection()); }; + this.buttonbar.querySelector(".fb-pencilbutton").onclick = (e) => { + const item = this.selection()[0]; + item.scrollIntoView({block: "nearest"}); + this.renameItem(item); + }; + this.buttonbar.querySelector(".fb-locksoundbutton").onclick = (e) => { + const item = this.selection()[0]; + this.makeLockChime(item); + }; + + this.ls(".", true); + this.updateButtonBar(); + } + + log(msg) { + if (this.DEBUG) { + console.log(msg); + } + } + + async refreshLists(callback) { + var tree = this.anchor_elem.querySelector(".fb-tree"); + var openPaths = []; + tree.querySelectorAll("details[open] > summary").forEach((s) => { openPaths.push(s.dataset.fullpath); }); + await this.ls(".", false); + openPaths.forEach((path) => { + var detail = tree.querySelector(`details:has(summary[data-fullpath="${path}"])`); + if (detail != null) { + detail.open = true; + } + }); + await this.ls(this.current_path, true); + if (callback) { + callback(); + } + } + + newFolder() { + var fl = this.anchor_elem.querySelector(".fb-fileslist"); + for (var i = 1; i < 100; i++) { + var str = `${this.current_path == "." ? "" : this.current_path + "/"}New folder${i == 1 ? '' : " (" + i + ")"}`; + this.log(str); + var item = fl.querySelector(`[data-fullpath="${this.stringEncode(str)}"]`); + if (item == null) { + this.readfile( + {url:`cgi-bin/mkdir.sh?${encodeURIComponent(this.root_path)}&${encodeURIComponent(str)}`, + callback:(response, data) => { + this.refreshLists(() => { + var item = fl.querySelector(`[data-fullpath="${this.stringEncode(str)}"]`); + if (item) { + item.scrollIntoView({block: "nearest"}); + this.selectItem(item); + this.renameItem(item); + } + }); + } + }); + return; + } + } + this.log("could not find empty new folder name"); + } + + deleteItems(items) { + var pathsList = ""; + items.forEach((item) => { + const fullpath = this.stringDecode(item.dataset.fullpath) + pathsList += "&"; + pathsList += encodeURIComponent(fullpath); + }); + this.readfile( + {url:`cgi-bin/rm.sh?${this.root_path}${pathsList}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + deleteItem(item) { + this.deleteItems([item]); + } + + downloadSelection() { + const url = this.downloadURLForSelection().substr(1); + const name = url.substr(0, url.indexOf(":")); + const url2 = url.substr(url.indexOf(":") + 1); + this.log(`name: ${name}, url: ${url2}`); + + var elem = document.createElement('a'); + elem.setAttribute('href', url2); + elem.setAttribute('download', name); + elem.style.display = 'none'; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); + } + + selectItemContent(item) { + var range,selection; + if(document.createRange) + { + range = document.createRange(); + range.selectNodeContents(item); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + applyRename(item) { + const fullpath = this.stringDecode(item.dataset.fullpath); + const idx = fullpath.lastIndexOf("/"); + const oldname = fullpath.substr(idx + 1); + var newname = item.textContent; + this.log('renaming "' + oldname + '" to "' + newname + '"'); + this.readfile( + {url:`cgi-bin/mv.sh?${this.root_path}/${this.current_path}&${encodeURIComponent(oldname)}&${encodeURIComponent(newname)}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + stopEditingItem(item, oldValue) { + // clear onblur first because setting contentEditable to false immediately triggers a blur + item.onblur = undefined; + item.contentEditable = false; + item.onkeydown = undefined; + item.oncontextmenu = undefined; + if (item.textContent != oldValue) { + this.applyRename(item); + } + } + + renameItem(item) { + const oldValue = item.textContent; + item.contentEditable = true; + this.selectItemContent(item); + item.onkeydown = (e) => { + if (e.key == 'Enter') { + this.stopEditingItem(item, oldValue); + } else if (e.key == 'Escape') { + item.textContent = oldValue; + this.stopEditingItem(item, oldValue); + } + }; + item.onblur = (e) => { + this.stopEditingItem(item, oldValue); + }; + // Tapping on the selected text to position the cursor + // on mobile results in a context menu event, so intercept + // that while editing. + item.oncontextmenu = (e) => { e.stopPropagation(); }; + item.focus(); + } + + makeLockChime(item) { + // copy the selected item + this.readfile( + {url:`cgi-bin/cp.sh?${this.root_path}&${encodeURIComponent(item.dataset.fullpath)}&LockChime.wav`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + showButton(name, show) { + if (show) { + this.buttonbar.querySelector(name).classList.add("fb-visiblebarbutton"); + } else { + this.buttonbar.querySelector(name).classList.remove("fb-visiblebarbutton"); + } + } + + updateButtonBar() { + const numsel = this.numSelected(); + const enabled = this.valid; + this.showButton(".fb-trashbutton", enabled && numsel > 0); + this.showButton(".fb-pencilbutton", enabled && numsel == 1); + this.showButton(".fb-uploadbutton", enabled && numsel == 0); + this.showButton(".fb-downloadbutton", enabled && numsel > 0); + this.showButton(".fb-newfolderbutton", enabled && numsel == 0); + this.showButton(".fb-locksoundbutton", enabled && numsel == 1 && this.isPotentialLockChime(this.selection()[0])); + if (this.buttonbar.querySelector(".fb-visiblebarbutton") == null) { + this.buttonbar.style.display = "none"; + } else { + this.buttonbar.style.display = "block"; + } + } + + eventCoordinates(e) { + if (e.targetTouches && e.targetTouches.length > 1) { + const t = e.targetTouches[0] + return [t.clientX, t.clientY]; + } + return [e.x, e.y]; + } + + makeMultiSelectContextMenu(event) { + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("Download selected items", + () => { + this.downloadSelection(); + }, null), + new ContextMenuItem("Delete selected items", + () => { + this.deleteItems(this.selection()); + }, null) + + ]).show(...this.eventCoordinates(event)); + } + + makeListContextMenu(event) { + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("New folder", () => { this.newFolder(); }, null) + ]).show(...this.eventCoordinates(event)); + } + + makeDirContextMenu(event) { + const e = event; + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("Rename", () => { this.renameItem(e.target); }, null), + new ContextMenuItem("Download", () => { this.downloadSelection(); }, null), + new ContextMenuItem("Delete", () => { this.deleteItem(e.target); }, null) + ]).show(...this.eventCoordinates(event)); + } + + makeFileContextMenu(event) { + const filename = event.target.innerText; + const e = event; + new ContextMenu(this.anchor_elem, + [ + ...this.isPotentialLockChime(event.target) ? [ new ContextMenuItem("Use as lock sound", () => { this.makeLockChime(e.target); }, null) ] : [], + new ContextMenuItem("Rename", () => { this.renameItem(e.target); }, null), + new ContextMenuItem("Download", () => { this.downloadSelection(); }, null), + new ContextMenuItem("Delete", () => { this.deleteItem(e.target); }, null) + ]).show(...this.eventCoordinates(event)); + } + + hideContextMenu() { + var contextmenu = this.anchor_elem.querySelector(".cm-holder"); + if (contextmenu == null) { + return; + } + contextmenu.style.visibility = "hidden"; + } + + showContextMenu(event) { + this.log("context menu"); + if (!this.valid || event.ctrlKey) { + this.hideContextMenu(); + return; + } + if (event.pointerType == "touch") { + // long press triggered context menu + this.log("touch context menu"); + event.preventDefault(); + return; + } + var target = event.target; + try { + if (target.classList.contains("fb-fileslist")) { + this.unselectAll(); + this.makeListContextMenu(event); + } else if (this.numSelected() > 1 && this.isSelected(target)) { + this.makeMultiSelectContextMenu(event); + } else { + this.unselectAll(); + this.selectItem(target); + if (target.classList.contains("fb-direntry")) { + this.makeDirContextMenu(event); + } else if (target.classList.contains("fb-fileentry")) { + this.makeFileContextMenu(event); + } + } + event.preventDefault(); + } catch (e) { + this.log("error creating context menu"); + this.log(e); + } + } + + splitterSetFlagPos() { + const splitter = this.anchor_elem.querySelector(".fb-splitter"); + const splitterflag = this.anchor_elem.querySelector(".fb-splitterflag"); + splitterflag.style.left = (splitter.getBoundingClientRect().x - + splitterflag.getBoundingClientRect().width + 1) + "px"; + } + + splitterPointerMove(event) { + const treediv = this.anchor_elem.querySelector(".fb-splitter").previousElementSibling; + const treedivrect = treediv.getBoundingClientRect(); + const newwidth = event.clientX + this.splitter_clickoffset - treedivrect.x; + treediv.style.width = `${newwidth}px`; + this.splitterSetFlagPos(); + } + + splitterPointerUp(event) { + const splitter = event.target; + splitter.removeEventListener("pointermove", this.splitterPointerMove); + splitter.removeEventListener("pointerup", this.splitterPointerUp); + splitter.releasePointerCapture(event.pointerId); + this.splitter_active = false; + } + + splitterPointerDown(event) { + var splitter = event.target; + splitter.addEventListener("pointermove", this.splitterPointerMove); + splitter.addEventListener("pointerup", this.splitterPointerUp); + splitter.setPointerCapture(event.pointerId); + const treediv = this.anchor_elem.querySelector(".fb-splitter").previousElementSibling; + const treedivrect = treediv.getBoundingClientRect(); + this.splitter_clickoffset = treedivrect.x + treedivrect.width - event.clientX; + this.splitter_active = true; + } + + intersects(r1, r2) { + return !(r1.x + r1.width < r2.x || + r2.x + r2.width < r1.x || + r1.y + r1.height < r2.y || + r2.y + r2.height < r1.y); + } + + updateSelection(selectRectElem) { + const select = selectRectElem.getBoundingClientRect(); + + const {x, y, height, width} = select; + + selectRectElem.parentElement.querySelectorAll(".fb-direntry, .fb-fileentry").forEach((item) => { + if (this.intersects({x: x + window.scrollX, y: y + window.scrollY, height, width}, item.getBoundingClientRect())){ + item.classList.add("fb-selected"); + } else { + item.classList.remove("fb-selected"); + } + } ); + this.updateButtonBar(); + } + + unselectAll() { + this.selection().forEach((e) => { e.classList.remove("fb-selected");}); + this.updateButtonBar(); + } + + isSelected(elem) { + return elem.classList.contains("fb-selected") + } + + selectItem(elem) { + elem.classList.add("fb-selected"); + this.updateButtonBar(); + } + + selection() { + var fl = this.anchor_elem.querySelector(".fb-fileslist"); + return fl.querySelectorAll(".fb-selected"); + } + + numSelected() { + return this.selection().length; + } + + async createSelectionRectangle(thiz, event) { + const fileList = event.target; + const x = event.offsetX + fileList.scrollLeft; + const y = event.offsetY + fileList.scrollTop; + + const div = document.createElement("div"); + div.style.width = "0"; + div.style.height = "0"; + div.style.left = x + "px"; + div.style.top = y + "px"; + div.classList.add("fb-selection-rect"); + fileList.append(div); + + function resize(event) { + thiz.log("resize"); + if (event.buttons == 0) { + cancelSelectionRectangle(event); + return; + } + if (event.target != fileList) { + return; + } + + const rect = fileList.getBoundingClientRect(); + const offX = (event.touches ? event.touches[0].clientX - rect.left : + event.offsetX) + fileList.scrollLeft; + const offY = (event.touches ? event.touches[0].clientY - rect.top : + event.offsetY) + fileList.scrollTop; + const maxX = fileList.clientWidth + fileList.scrollLeft; + const maxY = fileList.clientHeight + fileList.scrollTop; + const curX = offX > maxX ? maxX : offX; + const curY = offY > maxY ? maxY : offY; + const dX = curX - x; + const dY = curY - y; + div.style.left = dX < 0 ? x + dX + "px" : x + "px"; + div.style.top = dY < 0 ? y + dY + "px" : y + "px"; + div.style.width = Math.abs(dX) + "px"; + div.style.height = Math.abs(dY) + "px"; + thiz.updateSelection(div); + if (event.cancelable) { + event.preventDefault(); + } + } + + if (! event.ctrlKey) { + this.unselectAll(); + } + + function cancelSelectionRectangle(event) { + thiz.log("cancelSelectionRectangle"); + event.target.removeEventListener("pointermove", resize); + try { + fileList.releasePointerCapture(event.pointerId); + } catch(error) { + thiz.log("release error"); + } + fileList.removeEventListener("touchmove", resize); + fileList.removeEventListener("touchend", pointerup); + fileList.removeEventListener("pointermove", resize); + fileList.removeEventListener("pointerup", pointerup); + fileList.removeEventListener("cancelselectionrect", cancelSelectionRectangle); + div.remove(); + } + + function pointerup(event) { + thiz.log("pointerup"); + cancelSelectionRectangle(event); + } + + fileList.setPointerCapture(event.pointerId); + if (event.pointerType == "touch") { + fileList.addEventListener("touchmove", resize); + fileList.addEventListener("touchend", pointerup); + } else { + fileList.addEventListener("pointermove", resize); + fileList.addEventListener("pointerup", pointerup); + } + fileList.addEventListener("cancelselectionrect", cancelSelectionRectangle); + } + + listPointerDown(event) { + /* only respond to left mouse button */ + if (!this.valid || event.button != 0) { + event.preventDefault(); + return; + } + event.target.dispatchEvent( + new Event("cancelselectionrect", { + bubbles: true, + cancelable: true, + composed: false + }) + ); + this.createSelectionRectangle(this, event); + } + + dirClicked(event, path) { + var expanderclicked = (event && event.offsetX < 0); + this.ls(path, !expanderclicked); + } + + isPlayable(filename) { + const lower = filename.toLowerCase(); + for (const ext of [".mp3", ".m4a", ".flac", ".ogg", ".wav"]) { + if (lower.endsWith(ext)) { + return true; + } + } + return false; + } + + isPotentialLockChime(item) { + const lower = item.dataset.fullpath.toLowerCase(); + if (lower == "lockchime.wav") { + return false; + } + if (!lower.endsWith(".wav")) { + return false; + } + if (item.dataset.filesize > 1024 * 1024) { + return false; + } + return true; + } + + fileClicked(event, path) { + this.log(`clicked: ${path}`); + + var displaypath = path; + if (this.isPlayable(path)) { + const div = document.createElement("div"); + div.style.position = "absolute"; + div.style.width = "100%"; + div.style.height = "100%"; + div.style.left = "0"; + div.style.top = "0"; + div.style.background = "#0008"; + div.onclick = (e) => { if (e.target === div) div.remove(); }; + document.firstElementChild.append(div); + div.innerHTML = `
    ${displaypath}
    `; + div.querySelector(".fb-playertitle").scrollLeft=1000; + } + } + + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. + base64ToBytes(base64) { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); + } + + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. + bytesToBase64(bytes) { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); + } + + stringEncode(str) { + return str; // this.bytesToBase64((new TextEncoder()).encode(str)); + } + + stringDecode(encstr) { + return encstr; // new TextDecoder().decode(this.base64ToBytes(encstr)); + } + + addCommonDragHooks(item) { + item.ondragover = this.allowDrop; + item.ondragenter = this.dragEnter; + item.ondragleave = this.dragLeave; + item.ondrop = this.drop; + } + + createTreeItem(label, fullPath) { + var li = document.createElement("li"); + li.innerHTML = '
    ' + + '' + label + '' + + '
      '; + const s = li.querySelector("summary"); + s.onclick = (e) => { this.dirClicked(e, this.stringDecode(e.target.dataset.fullpath)); }; + s.ondragstart = this.dragStart; + const u = li.querySelector("ul"); + u.dataset.fullpath = fullPath; + this.addCommonDragHooks(u); + return li; + } + + addDir(root, path) { + var pathParts = path.split("/"); + var pathSoFar = null; + for (var i = 0; i < pathParts.length; i++) { + if (pathSoFar) { + pathSoFar += "/" + pathParts[i]; + } else { + pathSoFar = pathParts[i]; + } + var node = root.querySelector(`[data-fullpath="${this.stringEncode(pathSoFar)}"]`); + if (node == null) { + /* level 'i' doesn't exist yet, add it */ + var newPath = this.createTreeItem(pathParts[i], pathSoFar); + root.appendChild(newPath); + root = newPath.querySelector("ul"); + } else { + /* level 'i' already exists, check the next level */ + root = node.nextElementSibling; + } + } + return root; + } + + readfile({url, callback, callbackarg}) { + var request = new XMLHttpRequest(); + request.open('GET', url); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status === 200) { + var type = request.getResponseHeader('Content-Type'); + if (type.indexOf("text") !== 1) { + if (callback != null) { + callback(request.responseText, callbackarg); + } + } + } else if (request.status > 400) { + if (callback != null) { + callback(null, null); + } + } + } + } + request.send(); + } + + selectFileEntry(ev) { + if (ev.button != 0) return; + ev.stopPropagation(); + if (! ev.ctrlKey) { + /* not multi-select, so deselect everything that was previously selected */ + this.unselectAll(); + } + ev.target.classList.toggle("fb-selected"); + this.updateButtonBar(); + } + + createFileEntry(isdir, name, path, size) { + var div = document.createElement("div"); + div.className = isdir ? "fb-direntry" : "fb-fileentry" + div.textContent = name; + div.draggable = true; + if (isdir) { + div.ondblclick = (e) => { this.dirClicked(null, path); }; + this.addCommonDragHooks(div); + div.dataset.fullpath = this.stringEncode(path); + } else { + const justThePath = path.substring(0, path.lastIndexOf(":")); + div.ondblclick = (e) => { this.fileClicked(null, justThePath); }; + div.dataset.fullpath = this.stringEncode(justThePath); + div.dataset.filesize = path.substring(path.lastIndexOf(":") + 1); + } + div.onclick = (e) => { this.selectFileEntry(e); }; + div.onpointerdown = (e) => { e.stopPropagation(); }; + return div; + } + + addFileEntry(path) { + var isDir = path.indexOf("d:") == 0; + path = path.substring(2); + var lastSlash = path.lastIndexOf("/"); + var name = path.substring(lastSlash + 1); + var lastColon = name.lastIndexOf(":"); + var size = 0; + if (lastColon > 0) { + size = name.substring(lastColon + 1); + name = name.substring(0, lastColon); + } + var newFile = this.createFileEntry(isDir, name, path, size); + var listDiv = this.anchor_elem.querySelector('.fb-fileslist'); + listDiv.appendChild(newFile); + } + + setDiskStats(freebytes, totalbytes) { + var diskinfospan = this.anchor_elem.querySelector('.fb-diskinfo'); + diskinfospan.innerText = `${this.niceNumber(freebytes)} free of ${this.niceNumber(totalbytes)}`; + } + + /* + switchtopath=false: only update the left-hand side tree view + switchtopath=true: update the right-hand side + */ + readPaths(path, paths, switchtopath) { + this.valid = (paths != null && switchtopath != null); + if (!this.valid) { + var pathdiv = this.anchor_elem.querySelector(".fb-dirpath"); + pathdiv.innerText = "<< error retrieving file list >>"; + } + paths = this.valid ? paths.trimEnd() : ""; + var root = this.anchor_elem.querySelector(".fb-tree"); + root.dataset.fullpath="."; + this.addCommonDragHooks(root); + if (path == "." && !switchtopath) { + root.innerHTML = ''; + } + var lines = paths.split('\n'); + if (! this.valid || switchtopath) { + this.anchor_elem.querySelector('.fb-fileslist').querySelectorAll(".fb-direntry,.fb-fileentry").forEach((entry) => entry.remove()); + } + for (var line of lines) { + if (line.indexOf("d:") == 0 || line.indexOf("D:") == 0) { + this.addDir(root, line.substring(2)); + } + if (line.indexOf("s:") == 0) { + let [freebytes, totalbytes] = line.substring(2).split(":"); + this.setDiskStats(freebytes, totalbytes); + } else if (switchtopath && ! line.indexOf("D:") == 0) { + this.addFileEntry(line); + } + } + this.updateButtonBar(); + } + + makeOnClick(thiz, path) { + return function() { thiz.dirClicked(null, path); }; + + } + + setClickablePath(container, path) { + var pathParts = []; + if (!path) { + path = "."; + } else if (path != ".") { + pathParts = path.split("/"); + } + this.current_path = path; + const fileList = this.anchor_elem.querySelector('.fb-fileslist'); + fileList.dataset.fullpath = this.stringEncode(path); + this.addCommonDragHooks(fileList); + + container.innerHTML = ""; + var pathSoFar = null; + + var a = document.createElement("a"); + if (pathParts.length > 0) { + a.className = "fb-crumb"; + a.onclick = this.makeOnClick(this, "."); + this.addCommonDragHooks(a); + a.dataset.fullpath = this.stringEncode("."); + } + a.innerText = '[' + this.root_label + ']'; + container.appendChild(a); + + for (var i = 0; i < pathParts.length; i++) { + if (pathSoFar) { + pathSoFar += "/" + pathParts[i]; + } else{ + pathSoFar = pathParts[i]; + } + var a = document.createElement("a"); + if (i < pathParts.length - 1) { + a.className = "fb-crumb"; + a.onclick = this.makeOnClick(this, pathSoFar); + this.addCommonDragHooks(a); + a.dataset.fullpath = this.stringEncode(pathSoFar); + } + a.innerText = pathParts[i]; + container.append("/"); + container.appendChild(a); + } + } + + async ls(path, switchtopath) { + return new Promise((resolve, reject) => { + + if (switchtopath) { + var pathdiv = this.anchor_elem.querySelector(".fb-dirpath"); + this.setClickablePath(pathdiv, path); + } + this.readfile({url:`cgi-bin/ls.sh?${encodeURIComponent(this.root_path)}&${encodeURIComponent(path)}`, callback:(paths,switchto) => { this.readPaths(path, paths, switchto); resolve(); }, callbackarg:switchtopath}); + }); + } + + isDropAllowedForTarget(ev) { + var destPath = this.stringDecode(ev.target.dataset.fullpath); + if (destPath == undefined) { + return false; + } + if (ev.target.classList.contains("fb-fileentry")) { + /* files are not drop targets */ + return false; + } + if (!this.dragged_path) { + /* external files are not subject to path checks */ + return true; + } + var pathList = []; + const selection = this.selection(); + if (selection.length == 0) { + // single item from the left side tree view + pathList.push(this.dragged_path); + } else { + // one or more items from the right side file/folder view + selection.forEach((srcItem) => { + pathList.push(this.stringDecode(srcItem.dataset.fullpath)); + }); + } + //this.log(`${pathList.toString()} => ${destPath}`); + for (var srcPath of pathList) { + if (destPath.startsWith(srcPath)) { + /* can't drop parent in child */ + return false; + } + const idx = srcPath.lastIndexOf("/"); + const srcDir = idx > 0 ? srcPath.substr(0, idx) : "."; + if (srcDir == destPath) { + //this.log(`not OK: ${srcDir} => ${destPath}`); + return; + } + //this.log(`OK: ${srcPath} => ${destPath}`); + } + return true; + } + + allowDrop(ev) { + if (!this.isDropAllowedForTarget(ev)) { + return; + } + ev.preventDefault(); + } + + dragEnter(ev) { + if (this.isDropAllowedForTarget(ev)) { + ev.target.classList.add("fb-droptarget"); + } + } + + dragLeave(ev) { + ev.target.classList.remove("fb-droptarget"); + } + + hasExternalFiles(ev) { + this.log(`${ev.dataTransfer.items.length} items dropped`); + this.log(ev.dataTransfer.items.length); + this.log(...ev.dataTransfer.items); + this.log(ev.dataTransfer); + this.log(ev); + if (ev.dataTransfer.items.length > 0) { + return true; + } + return false; + } + + handleInternalDrop(ev) { + const selection = this.selection(); + var pathList = []; + if (selection.length == 0) { + pathList.push(this.dragged_path); + } else { + selection.forEach((srcItem) => { + pathList.push(this.stringDecode(srcItem.dataset.fullpath)); + }); + } + var pathString = ""; + pathList.forEach((path) => { + pathString += `&${encodeURIComponent(path)}`; + }); + this.log(`${pathList} => ${this.stringDecode(ev.target.dataset.fullpath)}`); + this.readfile( + {url:`cgi-bin/mv.sh?${this.root_path}${pathString}&${this.stringDecode(ev.target.dataset.fullpath)}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + + } + + async cancelDrop() { + if (!this.cancelUpload) { + this.cancelUpload = true; + const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); + while (this.uploading) { + await sleep(50); + } + } + this.hideDropInfo(); + } + + showDropInfo() { + var di = this.anchor_elem.querySelector(".fb-dropinfo-holder"); + di.style.visibility = "visible"; + var cb = this.anchor_elem.querySelector(".fb-dropinfo-closebutton"); + cb.onmousedown = (e) => { this.cancelDrop(); }; + var cb = this.anchor_elem.querySelector(".fb-dropinfo-cancel"); + cb.onclick = (e) => { this.cancelDrop(); }; + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.innerText = "Building file list..."; + l1.style.visibility="inherit"; + var p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.style.visibility="hidden"; + } + + niceNumber(totalsize) { + var str = ""; + if (totalsize < 100000) { + str += `${totalsize} bytes` + } else if (totalsize < 2000000) { + str += `${(totalsize / 1024).toFixed(0)} KB` + } else if (totalsize < 1100000000) { + str += `${(totalsize / (1024 * 1024)).toFixed(0)} MB` + } else { + str += `${(totalsize / (1024 * 1024 * 1024)).toFixed(2)} GB` + } + return str; + } + + updateDropInfo(numfiles, totalsize) { + var l2 = this.anchor_elem.querySelector(".fb-dropinfo-line2"); + var str = `${numfiles} file`; + if (numfiles != 1) { + str += "s"; + } + str += ", " + this.niceNumber(totalsize); + l2.innerText = str; + + } + + hideDropInfo() { + var di = this.anchor_elem.querySelector(".fb-dropinfo-holder"); + di.style.visibility = "hidden"; + this.refreshLists(); + } + + async getFilePromise(entry) { + try { + if (entry instanceof File) { + return entry; + } + return await new Promise((resolve, reject) => { + entry.file(resolve, reject); + }); + } catch (err) { + this.log(err); + } + } + + async readEntriesPromise(reader) { + try { + return await new Promise((resolve, reject) => { + reader.readEntries(resolve, reject); + }); + } catch (err) { + this.log(err); + } + } + + async readAllDirectoryEntries(reader) { + var entries = []; + var readEntries = await this.readEntriesPromise(reader); + while (readEntries.length > 0) { + entries.push(...readEntries); + readEntries = await this.readEntriesPromise(reader); + } + return entries; + } + + async handleExternalDrop(targetpath, datatransferitems, files) { + this.cancelUpload = false; + this.uploading = true; + var totalBytes = 0; + var fileList = []; + var queue = []; + if (datatransferitems) { + // Use DataTransferItemList interface to access the file(s) + [...datatransferitems].forEach((item, i) => { + var entry = item.webkitGetAsEntry(); + this.log(entry); + if (entry != null) { + queue.push(entry); + } + }); + } else if (files) { + queue.push(...files); + } + + while (queue.length > 0) { + if (this.cancelUpload) { + this.uploading = false; + return; + } + //this.log(`processing... (${fileList.length})`); + var entry = queue.shift(); + if (entry.isDirectory) { + queue.push(...await this.readAllDirectoryEntries(entry.createReader())); + } else { + fileList.push(entry); + if (fileList.length == 1) { + this.showDropInfo(); + } + var file = await this.getFilePromise(entry); + totalBytes += file.size; + this.updateDropInfo(fileList.length, totalBytes); + } + } + this.log(`total size: ${totalBytes}`); + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.numitems = fileList.length; + + var p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.style.visibility="inherit"; + p.max = totalBytes; + p.value = 0; + this.uploadFiles(targetpath, fileList); + } + + pickFile() { + var input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.onchange = (e) => { + if (input.files.length > 0) { + this.handleExternalDrop(this.current_path, null, input.files); + } + }; + input.click(); + } + + uploadFiles(destpath, fileList) { + var lastLoaded = 0; + if (fileList.length > 0 && ! this.cancelUpload) { + var f = fileList.shift(); + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.innerText = `File ${l1.numitems - fileList.length} / ${l1.numitems}`; + var l2 = this.anchor_elem.querySelector(".fb-dropinfo-line2"); + l2.innerText = f.name; + this.uploadFile(destpath, f, + (status) => { + // completion function + if (status === 200) { + this.uploadFiles(destpath, fileList); + } else { + this.log(`status: ${status}`); + this.uploading = false; + } + }, + (e, request) => { + // progress function + const p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.value += (e.loaded - lastLoaded); + const size1 = this.niceNumber(p.value); + const size2 = this.niceNumber(p.max); + const s = `${size1} / ${size2}`; + const l3 = this.anchor_elem.querySelector(".fb-dropinfo-line3"); + if (l3.innerText != s) { + l3.innerText = s; + } + lastLoaded = e.loaded; + if (this.cancelUpload) { + this.log("cancelling upload"); + request.abort(); + } + }); + } else { + this.hideDropInfo(); + } + } + + async uploadFile(destpath, entry, completionCallback, progressCallback) { + var sent = 0; + var file = await this.getFilePromise(entry); + var relpath = (entry instanceof File) ? file.name : entry.fullPath.substr(1); + + const request = new XMLHttpRequest(); + request.open("POST", `cgi-bin/upload.sh?${encodeURIComponent(this.root_path + "/" + destpath)}&${encodeURIComponent(relpath)}`); + request.setRequestHeader("Content-Type", "application/octet-stream"); + request.onreadystatechange = () => { + // Call a function when the state changes. + if (request.readyState === XMLHttpRequest.DONE) { + completionCallback(request.status); + } + }; + request.upload.onprogress = (e) => { + progressCallback(e, request); + }; + this.log(`uploading with progress ${file.name}`); + + request.send(file); + } + + dragColor(counter) { + if (counter > 4) { + return "#00000000"; + } + return "#000000" + ["ff", "cc", "99", "66", "33"][counter]; + } + + createDragImage(selection) { + var img = this.anchor_elem.querySelector(".fb-dragimage"); + var ctx = img.getContext("2d"); + ctx.font = "16px Arial"; + ctx.clearRect(0, 0, 300, 150); + var counter = 0; + for (var item of selection) { + ctx.fillStyle = this.dragColor(counter); + ctx.fillText(item.innerText, 20 + counter * 4, 32 + counter * 6); + counter += 1; + if (counter > 4) { + break; + } + }; + if (selection.length > 1) { + const textwidth = ctx.measureText(selection.length); + ctx.fillStyle = "#ff0000"; + ctx.beginPath(); + ctx.roundRect(0, 0, textwidth.width + 8, 20, [10]); + ctx.fill(); + ctx.fillStyle = "#ffffff"; + ctx.fillText(selection.length, 4, 16); + } + return img; + } + + drop(ev) { + this.log(ev); + ev.preventDefault(); + ev.target.classList.remove("fb-droptarget"); + if (this.dragged_path == ev.dataTransfer.getData("text/plain")) { + this.handleInternalDrop(ev); + return; + } + if (this.hasExternalFiles(ev)) { + this.handleExternalDrop(this.stringDecode(ev.target.dataset.fullpath), ev.dataTransfer.items, null); + return; + } + this.log("internal path inconsistency"); + this.dragged_path = undefined; + } + + dragStart(ev) { + /* pointer capture on the splitter element doesn't seem to + prevent drag&drop being initiated on the tree view, so + check here if the splitter is being dragged and cancel + drag&drop if so. */ + if (this.splitter_active) { + ev.preventDefault(); + return; + } + /* sometimes the browser will initiate a drag on filelist, + even though its draggable attribute is not set. Check + specifically if the thing being dragged is actually + draggable. + */ + if (!ev.target.draggable) { + ev.preventDefault(); + return; + } + ev.dataTransfer.effectAllowed = "move"; + ev.dataTransfer.dropEffect = "move"; + + const leftSideDrag = ev.target.classList.contains("fb-treedirentry"); + + if (leftSideDrag) { + /* dragging from the left side tree view, so deselect everything on the right side */ + this.unselectAll(); + } else if (!this.isSelected(ev.target)) { + /* the item being dragged was not selected, so deselect everything else and then select it */ + this.unselectAll(); + this.selectItem(ev.target); + } + if (ev.target.classList.contains("fb-fileentry") || ev.target.classList.contains("fb-direntry")) { + ev.dataTransfer.setDragImage(this.createDragImage(this.selection()), 22, 18); + } + const num = this.numSelected(); + if (num > 1) { + this.log("multi-drag"); + } else if (num == 1) { + this.log("single-drag"); + } else if (leftSideDrag) { + this.log("single left side drag"); + } else { + this.log("no-drag"); + } + + /* DragEvent.dataTransfer's data is only available in ondrop, but will be + empty in ondragover/enter/leave. Since the source path is needed during + drag to determine whether something can actually be dropped, store it + elsewhere */ + this.dragged_path = this.stringDecode(ev.target.dataset.fullpath); + ev.dataTransfer.setData("text/plain", this.dragged_path); + if (ev.target.classList.contains("fb-treedirentry")) { + ev.dataTransfer.setData("DownloadURL", this.downloadURLForTreeItem(ev.target)); + } else { + ev.dataTransfer.setData("DownloadURL", this.downloadURLForSelection()); + } + } + + dragEnd(ev) { + this.dragged_path = undefined; + } + + downloadURLForTreeItem(item) { + const downloadName = item.innerText + ".zip"; + const fullpath = this.stringDecode(item.dataset.fullpath); + const idx = fullpath.lastIndexOf("/") + 1; + const root = encodeURIComponent(`${this.root_path}${idx?"/":""}${fullpath.substr(0,idx)}`); + const relpath = encodeURIComponent(fullpath.substr(idx)); + this.log(`:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${root}&${relpath}`); + return `:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${root}&${relpath}`; + } + + downloadURLForSelection() { + var filesOnly = true; + var downloadName = "TeslaUSB-download"; + const selection = this.selection(); + selection.forEach((e) => { if (e.classList.contains("fb-direntry")) filesOnly = false;}); + + if (this.numSelected() == 1) { + const selected = selection[0]; + downloadName = selected.innerText; + if (filesOnly) { + const fullpath = encodeURIComponent(this.stringDecode(selected.dataset.fullpath)); + const root = encodeURIComponent(this.root_path); + return `:${downloadName}:${document.location.href}cgi-bin/download.sh?${root}&${fullpath}`; + } + } + + // the user is dragging multiple entries, or a single directory-entry. + downloadName += ".zip"; + var pathsList = encodeURIComponent(`${this.root_path}`); + if (this.current_path != ".") { + pathsList += encodeURIComponent(`/${this.current_path}`); + } + selection.forEach((e) => { + const fullpath = this.stringDecode(e.dataset.fullpath) + const relpath = this.current_path != "." ? fullpath.substr(this.current_path.length + 1) : fullpath; + pathsList += "&"; + pathsList += encodeURIComponent(relpath); + }); + return `:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${pathsList}`; + } +} + +// Export to window for dynamic loading +window.FileBrowser = FileBrowser; diff --git a/teslausb-www-react/dist/fonts/lato-bold.woff2 b/teslausb-www-react/dist/fonts/lato-bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..11de83feaf0fb872c91fce89534fd4e2d8bbda28 GIT binary patch literal 23040 zcmY(pV{k4^ur(Un_Kt1awr$%^c5M5Jv18k|ZQHh!`=0aNx?kP?Gd0sST~n*q>gt*q z4+U{%AYh>XOdALY>3_ykP9Pwyg8#GjKlA^;;g$&FIYJl$1&Tt^L&usS!@&gom2t=u-WRGUAYJNsgVASCtl;k9$Z_?LT3vVo1Xpo zbwMeLF|sn$0CKDNvh=IdeqHFFnBP}`;m&?kUS{9zxrBud*6KD2}f{%qS?W){G1O&^-D^I z+Fm{bO;;C+S}%!LX)fT##Ga`&%+p{g;m&6jsYV!rVqmGLq6OD6)qzwpe(sjRA6q#X zdb9ch>i z?4W4ww?zrmp=VJ$mk8$ftVvw?q_^v(H?v6ePpafef`{?s;0x}6gaz) zd~g8%`Cz_EB<0^7h80Qgu>>9cv?UbDdOwQjy*rqbYB7(&>VxDp@e9fV;uTZNYpat2 zAb@^2Hb0GpqXxI=vDwXsA~sRXOm@4h&MF3dmJM_#kK&SFgwCWsWlg*ilFC_OpUa0+5fVjaNIe||GRn*X+{E&!5qu{-CjWN{9OwzTU(gWEvzD|8e( zDyUP*07Yxm6asO0b@&bV4UgseQZo#J=sd33tYq&?Kzna0Wu3{az*P{)9$GWTT$N;t z#_j#vwBcu;-QTVvX;56D+V3Hs>OXKIlJ-JnDrIy_Zlrd(x{>uLXBG!US* z$~V2sWDv4Tg~t6oUz-MvKVsX~T)H&SvgdUq($IyE@xPG85OpyEMQrX*Fds_OtB0$CoB&z93K4}o4cM{i)D1J(Q!b>)m@zjqfs)m zf|thzw0XTxr@c!HP;LxfOdO`g8OGGw`~m_plg$)xX|E!Dhqxa1FKuYr%!DgotS18H z0WA~53C*o+3ELGit*$jWJ}Dd}DKsDD&BXaOw0UyJsemffSzvzo{0o1m$a(&hSRYO8in7}NrQaD0%EafbOxmcRf`;t zc{2??4^%u99Q6*_b?0Lm)v6Uc59DQ=r7E>f`EBoKTvl5&z6Sm(_%UGy?d;v+z%ap+V-w>v zl*9|>%b%Pp4-u|8`&*$h8a3NnCV34>Z^WT+mZ)H1f<}(mT3lY>VnakU%9l!tBCVm= za+lYqbpqVRQSHx}+IFwp9oM1r=dFovN~|Wc8KS`D6oxK?< zi>|9BO&db1XLps7H;Rfhy&pAq3Sw6`$_V&OOr<3!^l~2B+#TYl`a^=#tmrx@7W*hj z;{Rm+J9Yc^i$skOo3+%2hnuPbyngfrvcWeL6ig)80=P&%V_QsBc2|(GHO|e4U?i`b zs>laKn2)~%Ohk;>y|UIhCmdFCgFH(dM}ZDeJV<`p;qk^DJdz@jPNEn}dW!95)G%aM zJh%&o>7sf|_2xFfMdePVL3y}>d=9KAN42$b_T{whJ??D0D@;Xp+M!ue<7QQat!T5s ziGP04%m6A<=<-kUo|s245;HY5Zd7sbsX=}fhbsSXj|(vYir9i2d+l4GYqFb85V`(F z=f#*`7XtorLPokxWIeYb>i_0&#xuLq@qD5=OEAh13Jn5~n3}AiC|Xogl4#zgy*P52 zU%C3z4vOur`g)W&UC3e>&~BE&fslpOf-6WrI=NhJ*Wn~mC=pNKjkN-L8Ccy9w_@s}+=UCL&W*_P6um{w7qU7xXg1M(9j|Cus5McScMk{{F{%I1^_WhSJ>*?)+F z4#quD@g#vfw4u$Wqg>35r>n1=(-AJcF=D%jZhzIo>EsTa@h-2W+SaqzRvd2V;`>%}) zkQpJFXnh9<3l()X-)x5k)aZ5s2mZ%U-vwUgG4}BP%7ZlT1a(FEyZfroi4fYn!ggYq zpLT>U9@D|t{9s}Cxg=0V#6dB&D;NtcvEd08E!-?ag%+_^Shb{9<+5*w!&LKtIuBm9 z?$(?nJ;q=@0wW59QT_;J;i5$%Bc<>*Gm!ig zKQc+c`M<>fWR&=#jD#zwyn#yp2?K}Js0A6I@<2s0C(@5@#GAvZj1V1AfWe+SQLh$+ zJ*z+rYsHWgK>`z+BYT#;*!|p1Ifv?1L%_>gHZOpT%O!xEOg0W|7-MV{bK#!LHjRi4PI$b?AiRq{EMnE& zF2++O1tl4=+D*Bw!ePgxBN=4OsUxq_uVhRu<_UI-Pz?u8&lU6=y14J5-Xk-Ft(*IO*Yb6Pv!>R%!>N|rEQ z(LP{no`L4utPMqoOGh{ll8E`WnH$IkJ6TII)9{>)=WW-MH1wBNXjPX!U9Y$@Vl|# z%(n>fZUkxObG3GG)&I`xB`y=VMdb*&0X!rEv5Df(^lx}99MV?Z*az6*xWHR2JuM-Z}!K!MQ9$S5k1vE<=14^gn2?2 zTa_a4FDq3o9-aRjXM`j#EY;&(Hpc&&@I|%CqF6V|+pQea%mp)taeIPIkC+CGlXDs3 zaxu2@%-I5t7;G6_+!MHXCbDyK$<{Jx*V<{WgaeO3HGYaS#~sdebB0Yp1(F;W4<^$Z8WJi0)}uY+@rs38V{o9R zgnlmoQXz$eH54Kqw*xUuybewT7KNh)3a$^WH-hldjM-!8!EU5pG<`Y6b^UWH2vo7G z&4@DCukB+f7E-1Hgka-aOp|1R5OBB(kxuxYBW}J2%^G44&ri)7SZH0`Xj9&oPmHfm zp3vY|88Hj~+*qn?p_E~4eck>wZe=|+&bH%yzB72I5ZDywEif6}@X!JJ3}SbFvpJkv z64Ec{wO`Z)Zq;LT$J0e@Iys4={)(+N$m)!6n9iok`i;YST4J?D^3y zsr3)8jkYH*i0)AQ>a(z*dw1=^?mh_LCk}`dw&Yp213W zZxfVAm@%S6i54=1!1Ha64AE4v7cNADxXE}8L&<3)KRwbgk|+dnzDovKNpgLK-EoV2 z8C?=M5`d2qc^Kg@FM)Av1XD2BBO7wati%3jr95yil@| zed5oh9#6MG;tV&H%~~^^`A7ttNl?k++wYCmuaq#Mrzr9;eyZlg{7}O}M?DmN2lOe( z@`pOR#dI7_&ho6~TtBeY>qQ2W8Bx!Xm+z!D5<4{7U7I6e)YI-cpX5WWj2ij@AF>vn zg@sWGjn#y-t1j0Jv$1n#N@7HbjK@`KvhV(;i5&jIwQ3*_<|NKuc#wCMD>ENXa@kYY z2UYK$ps8I!z17PbEd_5bC|R();N}1aRkE-VRfpT|iZh8jfidWtdbk}jI2F&Q3Y?r7 z%fv2WryEU+`2K z9)LR`_~}{1VBFCq+-cmTuMj5Wh2^w|(BYqMJw;TD)W<=%=7cDuzYoutPVLx2)vopt*2KYjxyVvjwXd-1KnHSB_oLWe9Q_B}Qm=DRS_EqtlxTXy?d}?PiT+a5S|9xaajt ztA~cG6h4AD)@c1Kk!wAhv;`BP^X=+Zef*xYv%m^;a&odX&%TV2rLiMJE@;Sd-4V%6 z&pG*{01aVIMHmfkUsW?^l758^LQ3mRc9R(_>|dAmQwH8>-LiQvd-c5WlG=J3N$d9+g?i?^xeY+|KdZnFq%^Wl z=%skUib!lp?g?{D(z453_Efxkw7C+yck6bz3iVSb(M{1eR$KodEKwAivGA?`E+2-} zX?5^fkYgzp2k(59nIOg$lLtyBE}3fXpCSb%`Fpwz+z*uV(F4d^C;#v-9wm!n%lWt; z^?Q{|CB5aiBIxBWa&^<$l|_zGcr^PeM|@nf0V{yGorSs?T1XlHp-{#(k>E$1kZ1u* zEW8OP70^BKJ`@BsRAk1U*$fEWvnxVEKSsE%24%4+7#G_0t%>n)&q`<5ZB&ijZgc-T zi`0HCUqr9(fK}w=pGV(}j{bFHUCiood9Qw6^Z*wm1r-A`x_N%b4&EA7HjIg&Pa=U)A5@qP+Om)Ud z-oU_Cz(p=Ga%2YAX_J<;4`Y=KTp{og;e8qbO==}*c&f0)<$?azzFT>J4pOnxP0-Jm z(|@!AZ0M7JA*>_Oj&E?3qe!A6a_;y%3+H3GbR%zQh}X~udT$jot~PiD9LAJhrH#yA zmYj@Vw1bZrL@*b_Nc0+@o!jF{!^71+Q?0!w!7JU9E{wnpBuf&S4Yk0&U!>Y?;v!q2_iIuG_`Oq58Th~gBKg_Lb@E_vrB?CWehOp2;*4Zo~$*Em+E3ibZyGPZ^`Km&ow z!IyeBU+4zFAl`xurw7d25-F^2dVF390--vrL7x9Am}&2hp~LxK6u$oL>HmS$d^F9EjfEa?3y*=#5)l#HIu-l6|#p7y_9?Oz5B^xnPiQuAk z;!Y9hPfF)0`HC&H^NAf0qe6gRWo5m4Sx2veKafqBw>Em*(7S+ocw4!8cQnYd;x-cj z&6l^C({8qSAmY%>v?IM>VK(S*@M^?$W20|hyevyUlHVQ-nd{LFx{^i~7@a_#>(O%` zQcU@yhjd%wpMtIpfD(Dr}^72qJt~sF4nCASuWvam( zQ+xmo>de#(*?T$l_q!iQFlu%^R)-l=;zq1!gnmlwv*3GQ_#FYW%3$RQoJa% zj3f!24+@f>@JEl+>QG_PsjQOql6Vn8_Zw5pg;a56SaepZ^-ARWL6riieD&*;;%i*#cLYTiBUZa3X7#Ev7JI(^nt4#4nPkjIte9kpg+kpkD^|Fl&fY}*NRmhcxc_PPxXlGAy zvb_Bn6pXR6c%KxSuL(bim_*kU+=G;@H@)EqJT2QUH$FTd`ufG`#vfwK(ql==#%y^d zK4T(6qk zgF^Glfp~`8dkN!F!WUP*FaX(6g&aOEpILSviZKJG{Okk7@*9&{1wNf<34)|3gJ0OF z7d52^JcXcCRTRsDWZQYujgn(~<1}pHqUn%B%x4C7%$#q+%FQo^!#)S30cZIANqC0b z{|2*$T8v;gbD9I5L=P_Z>ib-YQd2EX_(0I|GM&V9Gz)=i{TCcvV!^eNPa2Gd9!dkv zh+>;oj-+t5QGykI3dT`424r8zl1Ay_Ci-o(4?Ot2_#M3kQx&fz-!!K_81=!TN(23e za7Fql$6%cm^BS&|c4T=oYnQYw|IJDyU-rQ}tN&a)oJEe_aid;;ZtCp3C z(k`UKLru-zs(pbLqDWiqvTH5oLL*qKxQUj+=YPn5cOwJSz$y{qyhuw%dpZKnMw6mO z%H0}8s+N>_h$4BtkWv9e-^wkRCTuN$@&^JLoh4B+3WgR27Wh$ROp>$^X~4k1l@zNP z5a_@=5h5>jjjAaq7!7|ut) z3|i9x=PQZ@J)D=%YsPZlI6u4tAnaJ&Z z;D!+)&%}sZWjmd-0PaH=2_oFNY#={w9tas1`8Z%Ta<3}hE`zMHcH`-6;347yA_o$~S}vG`qP_BM^Gl{HBHXjQ39I`oo` zq<8a<(CbS-lbXuHl#!K!pB6a%% zOvJzmoi$`ytl!_KMkL1twB)W=%)Xv73@Sdv)Pmg%XO0+SiF{^X=d@a4wUDXoPls+T zTfw`T(Ct{~b?rBBddJf%{7FkKMwA&m<8v~**ysIIyrc|og(6lW8L!BTU*6Z{AprF-8IH;a82RZd3>XVsu4v$|@cIBfPDiIUZJQ^bw(yVY^1` zPml10@qO%P;>$$X^bDPoe_(p&@Nt!q&BHz1M(yMUeD!yN005 zq87)KD>kr63~vxUhas&4M}qX_-c=E3zdFx}O@B(>i(Q?y9YcR&+28*hc^;Yr^|j#{ zhCEllt2`-Q8?hg$c?gUpBvCRYm*RA=&nDS-B*K_i^8YAA*nI4|aa>(TN-P)273ZI1 zS5^_F7lbUEsQA4fOz~FF{~@TcJD6FqlF-_@JQ3y4A2rF52$naN?GiQ|&l=zoKDVan znz*XZi#IG+n_{ttiqH(*&AL^Z*B1N4+(SO#rvHd_BYub0 z7}Ru=TiOm}_WbN&Nblv1d;(_0o6yCdX}oq~QJU3h9be4Ki45x0G5i3u+I*8GtuQio zy4>o>x=R}E#-m|#$Cn&`K!X$Ey6~{kQ6T?&gsGLzP(l!AcrYQPkH+K*br5x?e4@;dSk&YDj32by|u7Kkn2lhHo7L znW4IY{|@f9u$ze!pzLwuDap)EIM~F=GfJMv^6+@>fLyq-&%x0JeB!X!E*!3VT0f^2 zq8Xmw8hSVcGfvPZa<%46%#9;CYi+_$;p5^sXWu1q)RNE%>+EGv2O?d|tdBzAGI&tn{#Z3^xb5-0$E~b2R!z;xLA?;iUMOC*3OkBaX)~-(1WxfK z1aR_vz`*DD?~p^~Va3>X*c*x?SOM9@?4`d+SQc)Q0)UR8v_RsI+p|bY%t1xMFATQ{ zB+?OPe;135`$J}cmX?cINBTbR^V++;bzS&g@^{XZ0U?tdj(u(;(CM56*g%EqldeJY~9#TR$2VU2#`eGi@ImgZME@T9jk_=x#$W5y{39 zHs%!vJs3ZDPnLq2E=N+%y%K%Y2xB2h*>`ZT-4U{Y|l94=jkwZk)(tSdmWxT05#mDaRt~P(|w9?~YK)$*7fKNNtaiB4^r`%`gOetQ;y#kA48d zz^Dh8LD$#z#OGT9NX?zYN=iBG!M zf!XGo3lx5hhl6XsVi{p!Ocl1W3&7)f-sl47i5NvaHRru&m`%!>nPnJ8)kE}u$23nm zdLgY?VHL!B{MI=*m56w_Eumu|AQZ8;wtSZZ+{+;E8f*>IjZ}W^uEU6jpKB7Or|V~h z%TgaYqC#6CDI2qBrAb}MoxkrSC#ECc%DaIZ^8V)j;cIuIst=Tw-X28^F$i)`ikEFb z%Da+0-(Iq)%fg#Lyvy9=qNu14sc2_WP}28HSX~(BQ_>4nmEp-{J2{#PySVoL`)nzq zGCeb$W#ew^5C=SEtkEvG7FV8lRd5s(LT@8O(E9mE#*qKD38u|&dv9q?{Bdy|)nN~3 zdi6@B!8jzld9}>X%wIAKl`aV0MB|>= zVPB9?O%I6IvqVBZ99*Hf#?LI4l@QSPH3jm9#H9-^X&dKVSK1j@`s&-z_3{@qH%tpg z!53-6O=5OkUT+mEGV~kc3_{@8IB=OEg^dXFJmpN`cYZ6)A7-0 z5kCo>6=q=<8lO4BE`@-wggd(Y?Ja%G@+}Kp-nPz_UzcM{=QhJ;P-cf^>O~w=GKPnX zLZSdqM#T|Ma{2Rnpyyg+H`6@)MjQj+3T}2&etP1h+L|$95z$i^3xGLHcEmQJdO@O8 zYMds!|AFG?Un)kz!08>n#nW7*&dnS{fDMv3;R+^2Pbo1^)zU#OX<1{`xLEo%;iT?1--+*=Z{;46ZStuqW{c z?6~2**OU!A^qwisxbUkXH95hfv3)n(uOhGNe)^X<@bKfeEYbamfI}946%U6Z`L0TJ zr&=i|El{Y|T{+If(KK!BhcUMAKOu__s&1%KsYdg=F%;Lau+)^`Ne_H&RMsrjHUo$C z8n=aYiC7lT8t7Z1_itY+RhMj#Z{g;bJr+eKqpnJV%f+)oHC`Abjm+C}#ssnd4 zG4B|$?SHDo#Z6}&{NiorbCaCEc)TPuUA$6BI!veZDGh5Cj?;lStdX%%)sFHg>*9-H z(wIPio72RO0@soE{_JyxlnhTn8l3pcl*D%^f_{@>*zjK?+`=&(7;bl-b##}|&#kjv zgW$neGF!Q)XMMM_@NwM*Bh_sI5uMe+Ofs<-l{dm70d=-@)dGGC$*k+VeGtF+?b7mi z<@v=Mek#GN8&~7r^Lvv3Sf3AX6bYPLn)y*{R-;jmDH;r>q_+I6P+MN6(Lfkj3VO4~~{b7(5bovh$AuZRH~YEs+hM!b5*$we~o zSh~gm3ph%pgE_y=aApl^K0P z4073~=qqElGBXBo%F9<+n59?lk5sLko4UW7)eOdwscuy1xd9_XN8$~rj=)9nhY5A< zJX|Rv6|r`9xN4A8TwLLlxN3HG6|pHIT*hlrOK1`%Cu6KXUd|Wcz8WY_eo0{cI zETu#+wtTx8_E7q3Ja#sQYlOa;nWy92(U0B1nv&BUI1x1`irl0;lcQu2iq*@@3hz9h zQ_#c=>B7-9lmAsA9^GGnq)JOqyL0<}hjVy-)goTr-_$I{_ujv?{?RMH-_i15)-KgX zoh4;gV6$jTu&9;QM;6vk2^*iU-8PQUr;j069}bO8Rz4Mf^U+`H8-`56{7!1z&6ya5a&J&d%AK8b)4d-^O%5e^m6+ow#*J8I*#V2J(m~4Yn9bS`dzu zaTU-@@vHxWEBqD{s>gD9S93@2=>!+F>S>58o;lg4u5gqOJ_M#-2iGxbGd#qhLW|tf zbvk)zt?)4QAUgV&(-B~QOi(kR+&U|B|Dh^p1?_1PA+=T2>x-Jc?ahPbz)(6mzA3zn zp#&JFDWhq6*BN+pK6WA~^hMYjOon!Nx)YombX0)w4=p^T$W0d*w%8~*{+p&G_ z%L3ZZcS9_~5w)4~Ns=;5<^EgN85X24)gz;bOJ|Tm-R#s9jit{YtF8ELh1{zppc{kuWD-qA|9r;kL&XDFD&M0!ITdK2P1J_*+*2*MM{2K7^$Y& z44Z-}gipi|*PHOz@8KyL)S{kVKwjd9hy#%?^XaQ+_T#7?;a4q1|8g=jIT1YC6k%&>8DKvOXTRa&4G8)<)T2A$L*m? zdq-K*tU1pT=!MfNS|2jIGJwy78TB9w=im?pB^MM0b^U5YoF}87)q;}%aJ8*Zj3~5Y z6`9p}(x#?m&gUIt=XnubB318aR6lAO?ZEAJ{Vu@K2sXtq4+xKgTy$LRaw){9N7Nv= zsGTIUZ;vq{RDo9Wx#19ONzZ&1+*)B5m~kSUJz(Q!k)YvVac=yk9yhwFFN7PA1O~Yt zAt6on@=elC!CFqp>sr$gPT?9V@G3TIyS*>~0gcEDu|yUaU=TaV6HXBYpU^DOum9r( zJiF>$$To+yx_8o2u&mLuyW;54%0_VLcVfmxTAPo#Bz8j=$nxrRoot)t?uO2Us*5>6 zh`_eo^tR(#G15JkrtC?{NBk<@ASk!MB?i;<=2_zaOhNnapTsk}btyDEhZO!FB5`}s zuqBC9=@PPyD6{dK0i+*EW&Xim#{gM!s5x3C*5s4`&xmPLy)1P=vnIouoU{5{Be=f5 z9N47XX>?RA>Za?kS!>7*4RpwB2sLc+AY-XdIMw=$M*1JtOpd>U?3me~-abbbJ+FV* zMHkJ3nks`ROxN)vVjg$ZE*Ne=1NC>3kTTJlo6ys@+b7Q$aKht8R!^==I9BdoQ)$Yx znwxbzy(*fgpl~#E*7J~<8|S_%6*N0ACCA8}rz8qeiZ@Y#G_2${9n`ie z3jV5%NCi+Qf0AGR0}sJ7sn`Ye{3mUM&EvX}$@g-j60uc{ul7sLR*-!Z=mZMmkpFtJ z@h_G-AHf7!TIg6@l~dPi)BftQfduV(Udn@bx9dCL-+Xd;{`2LoyR|F1y!Crh>qXJh z_n4O69kSadbsV)_z~MHLge!OPQnhq(d4=)BJ2AY%sP@ziweV1#NpDVpJf4=Wfg|O0 zsj=sVtG&n!yS^O7qo^DDrKsJfd~U~!puDDaf-UnOg_)%^`_)SsiDp=75Bn8>Z7J3* zQ?r7T>^9k#+03y+4E(I@fHB;1G=Y5R`#1?E!?mAnBgxs>wc> z0n-<0+UmvS&(P{)bmuFD!freFnK{$KjD;DLGJ)i=2~-;oRc$|%$!(xNt*3Pr!;RZS z4g;?H$JImsZyJRz@Q=YP=P>glbzvDRjg2rF5{gF3RIBD#hik_V4-6h^KZl~upa*OXxl%ul<6HR z3~6H-HZ;vP*v{|XVKhtt-4R_Ct#Z>Ac73Vi2!Dqi?hjUK3k{^!wug_pR)nDH1>U!G zL;2jH->`qUqlnGiYG%1t*LbI=9yVRG}yhw}dS-v|x_C^+=#nH`tIAw3A1V$Sw z-q{#F)dk19%5iC7M?&Q;7R)aHF+~l2*v^gd%xYdokT`#w;_+@>yzXu@dWs_kU!%^_+ z_1k^v9}t!{+G*G=MGY-mr$m*W98{@xji_S!B^~p~Cse zqKz1-kGaqgq!O-z28K*gIj2Gr?>Uec;UG8)761P2#oo}->AGW`yGOM^j4Jl&gSu5= zhqMcBHJz7Q2YyL?qWwrS+zigU^%KdO)j0_^Pkk`d=LcCsVVxYsoz^7|`0%{XFSax< z28@~*%+3<*Kq&0So2L7bz?p7h?BRSShZ66dB{b4PT_xK=jvzz2#k2FgfDb+?=KUK>O);{eQf~3Xp8T=bvV81pn=Y zwx%UZ`-KOIeWmKX?TNSUg|~LCl?!lMHEjbk7_Z&6ZK;ZHd?J~+rU?GB901Txd#(syB8Gt-uvkQw^Z1?Iu2;Ftl>sM!kkfS4B%v|G7}u}6g*;0 zMeA~J$W>hQ8-6bDeHCobH;a2e2z5L>^FaT2B`GW%bp_IKx55UKO54=hIXM_bKT2X@9&4FZPw`?E&-3pD!(3ma1IA8<; zqf8p8VC!@K+HITcu&fQaA91=^6+XCZN~wT^B~TQ{sVfcxcjKc66CemzbRLos9r3x7 zXZ&kNJp8~bZpN4g?KAKM+Ra{u7l4Z-)2!ftLJzQ<;d-9?3@ zNp;a(8P8~cvkjX2Aie-?&B_VJlFBAr6MIg}HZ$x{z2@?XX+XBw#AIDV-PcZ>^qbtb zr5jWvwM&*p=hA>AHH!?S%B5PU$`_Gxz+D)rc~!hp3q6$qne)4yp1Ag%n*93@ulz@7 zUQYUtEk+paVuD~?`c)9DbDSXbGdeN~vK6L6Gr);XrzhM{)Q)84yu-V(P|n1OQ7pc|9b%XElszmGGEz3NF3f!8{^c&&s)j`P(2 z>WVL4v`+F}heG^lnc}ksQ`oa23VcwC;?MZ$DU_FbVQAvZiyPZIvX~+?25nLp;}ex7 zR;Q@o&mf}CcgIGJ#&QPD_*B($D}=1dF?yFXp7WhFPuV*2c#op>I>t}6O@fE_omr8y zlSu;-Kzrb{CX+#8U`N2n%V6uHFu{|K_%yFbqzc<+EB)JO!Y?qd$OnIKh|^gi+dg`= zVM3C=Pf*TDxg<+gIUsuno_mL4=cI{GNP>%N^1Hg$Sk~Mr#zdM;2o_>1rYei)i{SMGgYOd%Wdk}7GWlixH=s~R>U!;EH1*jqcFOGfVAV#p_nl%)^cFUai3 zzXr0I#l9KI^`NVDuT9LZn;>v9PnrcE8*CvN$JxBlaozwHM4w^Tn1SWn4E~VJ4~j2n zjB?i3`^UqPn#IRmO&sgHQY_@aDF~$&;ryzsb43JI|8ILr=e97UAy&-rWR^UF;vLNv zCW__#GN)r9qdlHg=j1T0xit#X-CYbS{78J0bi;EEbzf-<-dSgygtL3yCuuT1G^Ux` zjRJAEs5o=~b4$Z!CNb=iplr6iBE9PN4o;p)BvW(0Bm@ZWD5v=ulf7RX=cav0{glWt zOP?%#SXMZ`(o)$NBy(0)(}Z=qoM(F8c(LPBA&}knZM!Vv+GxU}(7!ILbP3}#?|O?s zz3bNT#w2Jd>jasb=Q;u0ZIv>+qUy(bbc|vXaqM;7v2Y5ckU$7U=kgJ@fdL1Me?x#B z3WQry)kTAGCi2S+dmqb7)|G`qn$zfl#dC)lwrBY?j)gmtT*f$5vc-gCpngAvka5Pf z9qY0R`)_jaGDnqv6zm4x&43_VnntAm%0gTRmxSl-VJC8+R>nC$t%9s-^$R&@*rB>l z{Zt5ul&42XKVTxH$cqD`G|CdHA@Qmf1$fT96~&=7C)*H39jRjDNKHIU9*pDj2vqg9BfV+EnMoK==yO2L5win0rk z#oUax^N=#7$SIa+7+k*8G-W%p=l8eKYOC754>EHbHSbPfR@wL$3;n=mn7T~C*dxLm zC>!(pD31A1NPH++z~og@Shaa&ugt-QIbD&-z$E*YHoM{sT9dJkqs?B0 z(Z}yp?f#R^`^3mew0Gw2*H;I?9flRWPMEh{A&?C^OG5r+h}v0Ev^w)EiQaY*6xW9i z%e6v6(Y)TrOyRlTUmfNAHR|`k0@HNg=Y{yT1dj;2RTF_Ihni#aDb1V0;~6zkW}PNS z!6%%uXqKh<)+X041t5{?S-SA-s^dc&D~*YQA*eeUImDZOyWvUyw2-VXOJtGX7;^v? z!0MPLxTxK)KPN=y)1ey%pC~HWQx@&=>f3w)-)3sT{6sX^K}8V7NA7u=7k_+P;tb2) zSq0ufXka0`eRtD{;j4gg7$r8Ot{4Ori)6asb4LpkiI{wFAjVqxbfg>Mnb7}@4 zfJP&cQyPFSAf$%cQW+#J#tVRlnTlI;QMy6f-oH5!Jx(ICtGKR5vUuUqT{acx6=9zT zUoAMpVKa|nOYr>zOn`|NGB4c4Uqz;((GK<4onOVXN+Tt$yTu5P&A@T=XQu zC#l@LuQC;AiDHBH#C)_S9k9l@dR%+Hw~rxecsA~Q2U;U4=$yR`4SnFnOok*!s#Ge{ z7t62InqJOAQLgXM&%Lo!1?CK?NTJzmJsFSw7^5AgC4}QpkK}>h!zd33;~7V961u@0 z4JkWl{X6!hg=U=r{zmuDmez3~UnksnkTe792;B?J+}V@H!H6L%Lll!5(g<#KxgFEM zh&vUC2C*k4Xok2xpND9i6$zNAJ^}7<`xdh2ZlTCW6jR}Zl)?#O7}Kl76Oc_P5-l*u z*8PhIq~WSWQ+KF?6d|vt?;uP6{{(p%hUVRY?rAM?C_(OLl=9gS$`OIEJk;fR z3WhMAH9H9~wHYDZ-UqmQSLRB%^Hoev(cI!8Gg8oUzDPu{N+dzumT~4MPJw?lHUbxP z5RfI|etgkEd=Y8%h=3Ae?L`M558Xi4E!PoN(?qwJyp@LSYM%=^8}-)8uaIJdDt3HOl>sfDj!}pQ8fy<N93-l1XS8CZn|)bgbvwW{ zp$wP2U+4m}I#Hiy?|os=f#q>)`H|aK?ljX*K&u2u9a2W!4a^B_6_l&>WbgcMGi>u+ zmL?H%27-Pp@|9)k37ewQfx{GZai~E!iTX;FTD@&|7V6zhA2WAOu2n5Bd0w7Tg&?%&^TrKs}76KYq2Y|a**G|%8 z-vHf1fg?2aAi|Px44>#~>Xj~}F1z&pp#8C*W!A@OqFeob*Ix`5RgpwW zfNxgAc~CyapvIP8D7O^G$5NPS-v9NJvX*t9R975ME7m+f9XY&D%`$l%UDK2lSTCt& z^xGtnNO$c_9pzGZSyIgo`QFB;ASFg$sF!vrvZ~}aZkmZktljD%97@W@of|22kX#_p zqkc9^F-y~rfa27cosB~~9jvR&rd+7nR!Q;nx$TTPMtYMMng zk1Z7V)`m{P*v5)9ww7J*w^{Xoz+8L6vPK>`9kMNd`PT*E*1If`+-GipB(RfuEE0fO zb7Kc>W`>yF2h$$#~EL5ux!0=w(T|!!E@&{u+sZrP%GjA_OKWCPW_ph z;kBxh`KC)jo!ZWFVp%2xRis|9i(R8#cx%r6 zAKve5<>Oc==}}@_>Cd#K?6F>3q}D%Q&$Rv4;^clDX|T7ru}ha)mZUd~l@u#0yv0Bpk+qmIQg5l#%}Sk@71 z`I+~a_tYLO=aL22A>%gREW^Fn&)jW|(~UDV%|&Myj$&H~@GZ)kKx_%&>!HG)4O(0p zl;#=4Spv?J{Lpt%?TvyU0njGap7kks{9^Q9jv6G+j@R(Badn5TesgjbaZA(!C&mA47UwD#}$z5spHJA8cK zR%H??=`*vs-1SGSYl3ZA*l|thSX&?MF&2!%PFyCxIKIxpeJa&LQl3+^;;ZRh|?vgGD49 zB7H$3G{C&t#mZ1Rd7O8*OQ3oNL-xm^N|Hm2QqRG2CKS0Hf9>oY*`N%yKF&dLRAUTAIf#j!+lyREx;8A}GES z(3b1D@4s_J7N;=n5gY3Etjg7TXmz4d3uW&Dp;FRGx|E$Kdf+NGyryR(bt5u8+f9k zKOQSY8rebh+j%RBO^O+__dk`nyxts#69p$eJyDKd$0)+>9mtt!Wr{>LBKZgX=7q?mAgkDRFxonB27MaDkOoD!g=h3vG8$KfHF!x)>$I_ zTtV2NtHk@Mk5WcVkM$3^W&Z$OUIV&Vr z1iSt!1N?-WY%*Iy3O_;+wZ#KI9s1|x{%(=QM_n@}iW*4Q6e+lSy3r)JP>L-J#0`bc ztq%kg*aal-&jev5&u5Ts*%<@}*vhpgg|iw8G8GwJCVemaJaEbdS+YQ`CifT9_OROy z=i_-@j)ucT>bCZi*++*nhI}Y?Y=gUp!mIl<0_{ES?8Soyu^09yK!f+nT>tIt%({dl zIFpeaUiTi@%0%Ei218V5yf`x5FWXK!z7BbEHVwcu@t62xwdkH-oL8N(^4KD4-;Tn^$|qe5+#@`xBBnYD9DL26KyROe=+{(D>r`+yQxeOO?Y zi5cn9n5w-C8wysE!C3GH64(m6J=B|Ph~rJ>>XpM=#90f6?29;g ziZ5;F+*<%$_L?phqnBWut2g>*ABN`IE}GnJskTbxv}-9uWk;FDXap+^#aT;;%9nNz zW_tizcbW_C9D?r#KT^!D+yK9m8xUD_6B}q_3n9rjP>^g)i(*zxZwjatZs^tUuJK2?N3{hQ@Q-U1E&x4uggZ z9=e)8J)5;Zt*@=F6r-D~OWQ^dG>Nq^eoQ|EIcq2HU0(2_oU=q+mqs=h$OQhOhP|BeqvXEOcv_>-N2=5Czq{kZ-R-V7 z?_ZuB=@8*R`uAg5+ClH9%)=@0>zC@QuhfSB1MYETvtF$?t24tiPQ!R+MK-J^(J*3d z?4sGGzP(<*pTbv5??W9%z>a>2LgVbL*KIwA!qIjRCSXgi?P^zcx;{rY;tB`FrmfBB z)-0ZUm+82dzbMkxPpP7#qAPhSr2j5k9lS>ADb>k9TB)$2$<(WjX-x#8 zn%5EtrErlZhDTmeC|j8|5m0fCo;Fp>OqW-3b{YFD5rN$uXdb^^tC`#9bJvm-n{G%H zbwT&?q+XLaNYH41RUqwg^9z0P0NDBj_k^5u!zBi4Gos<`N21+{MwR?QnLY6tx?OKCD)7yQ~LR z!1=jVr_M%h0wJ9AeMA%aXyO8&9sI~YG3k!vr-&(}C=eL+HSAmW*)ZC8_69C<4TF^i z2aORtG{c%A^%~dpCu7>nm#2`C?A`fVW}1dzaIUpRXt?Mel01%&_Lblpzk=6AX(jdD z#eWV0{X38#jOOGICtMQSmeu;xSMAu|(RDWR4cI)kS|zI&(gM3lND5#8EYNkWQXy7R z>_b!}YIw**tUkP1YtT6Fj^~5y;WlUYPovYX^kdRRV-7&(at^TR2j*Xd%c=61lbW8Y zEO=W;*?t64Z_k!CE^jvykvO%W0SBNp8EkI5X2N30t7rnK3V9(T;n4c-G@WhE)J1UE z<(joMkgm=Y73y_u^B(OcVUO=}L0x3~)B1YXV@Lsk*bQCRC?n$6yDt2%E_5gmi)W&h zExO@Ge^*N0druyV((ce|W`u=R)&(#+5J0f-Uats>^q|1EHX=~ilvkv<8$k{f-iz0y ziP(6V#m`r%`3?y(=RA7KQ;RFEP8ef?P!30voS9U*C*jgY!iI=AhRHKz)^vop<*%Wt zkFT{VF$nOhaKfD$X>DRbb(fm&0EP}}Uy&t5s;}fXC@SnChGp~)OAzz}_rowiVDpvr zg~b>V8h9?oMw$m3>}uam91a*Fhw0jZ7$2Ow$DP;6clXCb1^yAej$S)whr8q5G!$8+ zYqB>!(D!Cg?o#VV3fz!JE{xK+WL-_4(y4B@`&wc3S7K9VJz^(5C>iS7yK4XTY|M9j?P5$t;Q7^FYk+( zfTnU{kE0dILG!1QPn$bhX$0Q4R#=^3ffU8a%i;#WVJ&Gu$y)nYt(L|-*mSg+Lj*K3mZ$?8?Toh)=-2Bm8 zc=i#DPN?X23SmyI3NK<$h}xo8H;XA6J* zfOi+kb+0n00S_KpZ$m%ZP%(H#O=^Spl1sj3@8gPb+>f2=e`xs0^O>gGc&5PtwsKo& zLKMMS;D$x-4m@F$9Y@tkX?*`pQKlt3phpEmJQJF*Im=o(tfuc3Y#&QZ{P>ERpZl4&`L^DmzY_ zX3qO4a;giqUL}lAfGs z7tr*Am>P0e(s1vk0lKEqxzlmP033I8MinILkeddBWjV;gM1{o>Kx;-^a?g!pKtslr z%{7m0q2S$uY1nE+^si{mP{8AER8Peh`te0lT8C-P5Q6W3a7CfDIBAxr1Y^;XL2DfMl2YQS}<&WbwLGKx<@69hd+o9 z>%U1hZ*bIhGax>9wF@<82ao0GtcfjhEJtnkHHVbu(I@CALVl#89VdxPvQi(f&#Mom z9-v_}B!C>8K?uo^1~E5n3J0Yspc^`#nCG1bKNVt(Mdpo7EFF=e#w|;;3VP-d^z7A1 zM1{Rw!+lg(otBo)OyHtzrf7_OVs#iE$Wu%bT=I-ZB;*WR#Zrp zcbj?^ms8FT)g26o>&uWrY*uLR2Sls-$RwO{9Oj%>P+6)akSYRNr&6pv_bs26YvJ%Y z7n%7=m8Uu>jp!QQeI5@SzBqk=roEZxK3w!j^Zpb@NFf4TSkKE28DdwJRRi=Q7*?wv zjsC@b)Yq@cPhd=m?H3%Q!_|IlS`XZ@g8LLPh!u1`j6>_~9aYd#2Zi3kUB0T%7uhf9 z8-0Y&Lzf8dJpLjt2=en$^(-B-$Y;tW9EoYQQK3Bnw3S(R5a+QA%rpA zc1h1&($AjUYYxoZ@PLOr;xSKn$}^6fxaB!7&WOo;J=5R0wI8_Sn>GK=d}-51 z^J=_(SgZ%PW_^15KW%E?tZhnv`S+4DVwY|y4{tt`fFXgJHnULqV9OUSvobz9RL6*=65mx literal 0 HcmV?d00001 diff --git a/teslausb-www-react/dist/fonts/lato-italic.woff2 b/teslausb-www-react/dist/fonts/lato-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..851630ff85699de633f37170f29cbadce3d322ab GIT binary patch literal 24408 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 QOFh<%JiLFgmWQ+r07g396aWAK literal 0 HcmV?d00001 diff --git a/teslausb-www-react/dist/icons/android-chrome-192x192.png b/teslausb-www-react/dist/icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..6af01ed243af1f1ad5075167bf225650a7aa10cd GIT binary patch literal 8624 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 LyLQ~$&N}rUojuXs literal 0 HcmV?d00001 diff --git a/teslausb-www-react/dist/icons/apple-touch-icon.png b/teslausb-www-react/dist/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2eb8cd9ae34d91c26a91aa82a4ce28cc65e70baf GIT binary patch literal 4647 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 + + + + + #00aba9 + + + diff --git a/teslausb-www-react/dist/icons/download.svg b/teslausb-www-react/dist/icons/download.svg new file mode 100644 index 00000000..d15e4615 --- /dev/null +++ b/teslausb-www-react/dist/icons/download.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/teslausb-www-react/dist/icons/favicon-16x16.png b/teslausb-www-react/dist/icons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..c3e4773827050232712c9762968c9544df8aed63 GIT binary patch literal 875 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 + + + + + + + + diff --git a/teslausb-www-react/dist/icons/locksound.svg b/teslausb-www-react/dist/icons/locksound.svg new file mode 100644 index 00000000..5361095c --- /dev/null +++ b/teslausb-www-react/dist/icons/locksound.svg @@ -0,0 +1,4 @@ + + + + diff --git a/teslausb-www-react/dist/icons/mstile-144x144.png b/teslausb-www-react/dist/icons/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..355ae85c39fa7705d6ba31e229793dd1f61b0d7e GIT binary patch literal 3221 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 DGZDy% literal 0 HcmV?d00001 diff --git a/teslausb-www-react/dist/icons/mstile-150x150.png b/teslausb-www-react/dist/icons/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c144a8c1f6fd60aab99c4dc40933bbe9920e40 GIT binary patch literal 3200 zcmcgvdpOg58~@pyM-eG3Ce4z=92PT&RO+p83g?Q4Whoq1ag~zEP zq5NX0SfsS=ah!#oAw?xmN$d=6|ZBO*VS3&t3p%&WEiXl#|W~swc?@08SVI04oy!zDXh0 z-vDsT0s!VH0D!v;09zQP?S2l@h5R9cha2$oS5!Q_^@kKuNW%NLE4-7D(L`G$RPwe1 z0L%dIwl^SkI%YtK1MgNDK2WL&|KEflhdBL04ToF!ltf&Snf{6{Yd zyb4Eytz|EhF(Ybtav+4K$Vg{-Kr(G}1&}c<5ts5Ba;$l;=v`qKWpq=KvIi>xTDpbu zUQ4+*b&0|+dRpX5}{C) zlweG6?=gdkANcxK!;dO6=;78US}+fmL`JW!Ifj-;FLpA%Y4+gq@|IZLw?5LvW1wGW zr;hGh>ndNB*rSJK<|PY|liI85%|H80zfEg9g5C1x_9=c0V{P-I-@=p=JkeX{bMFtQ z3Ft_08t}CV!@A`7{HO{laiOsaNtkT5D}nB8EJN)34dcRkcLweC#)^!17{fCzJB61y z@+<)B!s;Q=8!`O)0JKgc6y}ABAhE7r;6Lj}o0=U&-A{4_PikEHuJzGbG7Y-tM|oCJTvX(kM^-Duj1PE0KE^cOo4TF!0FOIpiz!*_4=C5rW3$;8; z6bBtTx_Usi&6qlct({FnBkK-ev*QnZ02t<994-c&Zvms{g*gD37k~*;5Z2C8(MS_N zY%*l=cH4xJ*b3LV73!_jWaT>5R#Btj>{ZagSde!JW0v{!l5jNcW>H@Dx!zi}bM?bI zcCe@3qNU&G`_;P?S_$K~fl#^B+mE}M2VSGt0f7zOmPN_qu{50&Na(FauG&+bv}*PZ zX4KngRw2S;YOCyGZCm*!CLU#P1zxYr-+XNIdF10QH{xpGS#RSv3;e8bI@M^f6v(K% z9uuZTQ?J*GZ)dlL!!d(`k;a;CX5y2{ZMqv@PMys5Q-J)OL*$=H)K{qq#ROv0 zLzhx)dw&eu!zU*||J(;_h{=N&+6fw2RGZjOgHEcv7>e#YXd`m^)Anr`W&iJXvoRfT zj}g-&F!np$XYbIUol?IKloNx{Z^=}R8bL;l<&f2`rrAbWE}0B=W+ptSeB9YvwQFN!~yI+)AYK@pKFA}MYe)}i0bG^`@(MNSZ&+_TkCdcO{f^0|OLiD)`dOsuC%4|U!U8F6y`af^u1z;EK>;4ZEI9^R zmn{1q*5a(BUYyG}`NWpfPPAvwk<-?_X#9G;U#itbDN6liql^ew@GqtQ@z;!qT~dkW zDJauVN}@BWv!4TBUFn&8Lm`p`afcAs(mZvaMQ;_huoE0|?y*lrRPdagRyZ%X_MGI9 zy(4e?7QuvJ`y`PQ7Z#t`eP<7pFX*HXZ4d^oW0;2#$8LnUA3`>AO27FHjQJBWFt_3%PP$N++oQSYy>?((Pt*gN@19P3@%M=x z3Hx6EK}1({7?nl1Ta%M(KXME;$edOI6twk1`8}@Tel?kcYH^b7^@9)Ka+QL;ORM*$ zb$1UPF$9;mL%h*AB}R~BM`3YR<&Ox6ld?kuu7EiPEA>3JrPArp)ZZI=z>Zi!*;A5O zaCtugbz{vGKK?f3P)8oQm?+z(xvCi3v#nMk7~9^jBbGa96Q#G254k2kfEehgb&8sL zcB?_625nMI)ji(PFewlBTmbn&M7tV(M|s@&^L)&Oo*m+CVDC`tm7J5eooE!}K-By7 zSV`uajE0JD?%0;=Ve%|$u9<-VHQf^12?_I|aVE?rE;f}F7ZKY-|JsiX_cD1dhk0EQ zFWYt;lWdUw$Wa*!5ynIM6~vnPDH^EaM`c(c!`5!c!IF0b)OfuqTteE1+><>UdioKt z7KuSj%<$w^jMhy)05rrz;GYJq8$*b2N!`^d7376Pl`0P0`+!Fi&O<`N6ba|=&n=~% zlyKi-#V8P0p zLr9BTNz0BD^Je#iK1*y((()YHKj%e*KXEKt(lA(-a1^`LAJq4VOep1(b{)2qyhly| z=8)>5i5zpQZL50B$*RS#=Lq!7d$wh^?I*HU|M{Fsv3Yb)H$JD$mk2a$U#ugpe92%r z66l4pOZrW(`B(Opw>5o!{tC9B8IZq?)df9SNOYseA)EFrZ99vjd=Q@KR5_KqnqQr) zBcpX_=yulNt1rhjh2>aM@kq4#$DUVs&$rsLM=I8ib}s#7TCd}4XN)xL?-V@>&f7}x z$j==={nIAiSOcok3TmpHl?|OEX>V!Xa+%H3HfD$0lFk13K-c z)U7ClIl}|%K4-nDYv!b$jn>!vQmV}zJdGx;yUZ7*vV#j#jMErZ)d@cExy3k9Y#;g5 zxR$T__jJG!XEXJB-y9(17W5r;qL2p1`ps$72gp!(B&X_>2fuKyqN_rJMvGyBKJ z-tasNJI5n@seeW$xd$gj9!!eDQ4^!24Y0DXvNp4@lxBdH1rB42!(dG;EN~VUo)tDt z|3OHgN5&pL{@)39KR7BwkQf04CrA)5+{ecG+mHRXz38cVWSH zi!?O8XtlQHQk*wmjic`z-AWwwPp*-cSo1#w<UO^MVT#o;GzPb|A^eTUpLLVP7>=dOrPOA}p>W8>^RJ{o@Ab3V2MJuEGEaTh?FwI+ zNEb@w&R-XK`uX{xNl!LkULf)&t)$!GElBb1>|Pnpu={s(5=97y5z9Wh;F79&2v0pQ)NZS%zFc2B2UPS;m?frnN^T}TtQHTn>a11Q&75n{%y zu|}NlWcbvn5xvaKPIFDjXkW?Tj*}Fy=5Zf#&W&Y9Kkp2x zy#Ix%Q%d66Cb|}gON(zvc(u$IULzQut|(nq8aB^g$xsUiStmMeRuOh)rTh|veu?!) ze$mKPt$||mH8~kUMbx##!dI+R?%$zLcf&>9@f3B`q`;XF0Q*dG%gRigjJGeaFePuc zo@a&?MzfM*3!-AXa;m^6lXHw z-8=XA^?K!!s?%c$&%}ukW_$A$xtS=3z53~0fayy_-Zbv_`@(xYHG#CHMb~|1s4v4| ztLB3$u0CCa`b>CYh9V)WbQkM9qXNQaTp?ac=*D#2IHY^c>3R(GW_K3VDFXM}LC<#N z?Lkebu}|liOuMfYvqyn)li9RqLd`qzpv!AoFSk!E{ATIf#?HBtAEgf+*b;$lLt<%NKL3AG5}r6L3U?V z1y=ZvcrXtM@3@>m7uS8VlLp>eRX?I{<8cEGx`!5PZWqIEIPi&;bVzAP;=t=YX@JSC zE{huXW8*u>vp4SJgAB5gKJSjg-yj~Al(+eF^kl3@jybR|vo?mF&~=259c+iIz5|vG z65%25&rDU##YmegOO;(8@$NI>J%){Ab8WrTA(xaGVTVRJHp!KC-wNqj@Z`!lW${R< z+a#?GV1^-8QR5d$*bYG(7LB$|nl@XY7LO-9pB~v2mTeblxm3RN-`xG5s?3Y8$c781CSMXID>mEcivGG zqS5Bd4?QrN@DHLmJ*ADWY5qjU{0*&19W)NiShw~x$iwS5XTMqA$P*^2PZW9BrX1J+ zI_>{(skQpns&wFz4wD~nuU>}}11EJdT5;Kldi_g~)uBlS4|#l04FuQm={a$z`^M`9 z4r0nd^gQ0}`MkjE=h16WUsxXj_gw9Eg8#veV9TcFg|vv%Jk|yNGxn3_m=5!`h>0*` zOqZlmnoK6!7sOW^F&7bw0ks1)RV_6}c9>iv38&DebfC}cfxyYI!73tIsHt>wRevm5HUH(T%kh3yZ*>K&F{P5Cm$R>q zzgQR(swp;(Um``Em6+ftQ9Q+k>^6VACA@b0`9d~imp5FY!{o`c)G`#qGf6x<_mH*i zALbYpNuu;PeRPa}`$JDw`|Nsqx_Mp=IWpU7hdg}yNP^%Sf;7-<;8!@H`1R4VL9gyS zFs6!H)5BU*|B4k-kNs;)RG_2+nPGb6%=@gMgA+_QXWV1xf(50 z97-^5zc)7!@6!6CG^OBKb>|b|pNhMy9nX8qlcstm-;ULgY$o zaZ}kQL+Ha}5LEZo7E9EBtAdk%({&PoL)v+=@fjPO`sRQcsT^xwT<%E_UTDD>3&!1t ze3$iD8~4*`_SaG@uM=>g<~moawUJX~jRoGnp%@8jQQN=9jDIcdhGStz-xD!tB%8Km zNg*ydsMA&oo9<*D`K3Mw^Xm91l_pxnl-oiHMQx&PIh(-Ea zP1hlq6$#UeU|o7q+q##urMto@2t~WH$cMjE+0o{|kBh<7Bo>JM~j&VV(CE>_%sk zy5J`qW1Ylmn6QnX2DRts8rNl3T6lRwMeF+#&kbvS&xsIqOJR8A$DsiWXmk-gep%;T zL$6*&#pLyh{vsPtJrSYJWS1#)q?t7oj3)AVldOUE4 zBN?mOjKG6omrHN&ZCy^D=^^aZwf{0$dKt!Ia*fMQ#lVM!`_&4910(~OVpk2U0U6~BM;<> z|C~9KL$IsvIW)J(F$C?dtRV5d+0+pIKbK~A)}#UJaEp#AqLsFx|IW!5joQJU_6L*< zTWWLdhW@P6`IM)d`Av8@sICvJ(n2N|>~*v~tz$-Kat}L#T36A?x{ruiuy=-YY^kQV z?DK7c01d_vPN=*Ze-w$ap}#w?84zm2{CIM!?;GzUjoVq`wJzSDJhl!lD6)X$M2q*E ziDU{Yp_QkHS+~D_5&n;0Z2z8ol;)`J7#s!@joQlohd>1WN@#H6e-|jp$&mCDfJ#Dm$9iJp4WeT3!J*+n z2C<1zK?b2wv4H?26wWLw?T_{{GJ4_8?RWm*31p5aLIV}mkEpxc(U4I$aHN=aU6DCb qk1*hljXdw}@7C>>=Tgo+Iu8XxEw$KXd$ZrZ#?JYynT@%?=Ny)4&r-DkVcbGG~3C)?c>v2TyY z9uNq$@9a;fFM>cjVB3GC-2mj?xf>I}V^@fyiz5hBPl9Y*0t4$KK|ft|0f7>Zfk0`Q zAkZ2BN)v-XL}L(05(omp?}0$jsAA>?TL7{9vNPf|Xlwg>pIvzefG85qx|~s*R!}%( zWTNrsMk5HMQFQjSBl7yYPh;^6EE+SpIPaXX>*UwLO49GXJK)&pdHToOj>Ya}st-Iv zM-+TQPdgttWcaU}yIE(z3Qg6s%{!Gc%o^2x`4Mtb&++h4$8WyAZL;gs!6UmKt&`;C z1Hb-iHQF$2Kb>Ol*~#4NiQ7sEpNLtSpJOliY`@EYKmQpEaK2V_(g*p9S0p|Bv-}XW zzV61runtANR5UslCp#V*IiQL4t5~fc zjM;5|9Gz=c$&?=p2TzJO&I^rxzMObDD6bD0S0b5JH8(8m1$!jVR7k*+&&F75!T;d( z0Q_>1q!c|^<7WaROX|rF52xwHoRgLG1SoaKc9&SeJnRB#q}uyuSI;lTBx0;;vW{-O zRSQ8{X;XHsk06(6{_YfOcwzNoW>rcA;UMEHz+hdcXzL``3^}%FethG?49*9({ILq~ z=2wF7?KndVKZ;~Nk@oNd&95lcR=P6|rd(%fXY*P+53F5ALI`&o7*E(#zjE0S89*WIi|KinPgCc2Ao_e;{z+2+=+V`WOxvsYih@17BJ3W#y zG7J@RVs|ClttN#)_!h*P^Ug~(m>RsED1-2a%^l!@y)MMss`2`amdtz$7)1i3_NoLH zj?tNoi`*BLv8vRR|{@bB&_Yx$aU$~ zM39U)uy~(7E5iZqf1be-BI?5f0POd@tm`Vr-QE>7S%q$UvimiSXe9>Y}bHkNxtyD zIzHLR`-;PR)yg;)&@@@USrIR^Z7r8aU6|N!@rY^;tLerYj&M;Yr95zhk3PvYP@(q<)xG&{PgaxH=Qpyg&=jf?{Uy;!pYf}F z@P7RbS9JmJSZBRh(#4mP^{sHxC4<>q?jVMy8`|M4tq>g( z?Wds|HA$08&ca6ll3jvZEsZbFwJmD{T-F`T0HTvrkMf}9r&iiZujIMmaAxFFq>)$G zi)M2E{#>1qG;CaCpPwJjggF=#l_M#n?T4A8dJopoJE(2X-~HerAKjyovE?#ERHtLcCTf99fBNIPwFp{02xlu3g4I$<97*;u9x9gHzcPZ{5I#s;@uPR ze@IrU{F^dF!cgI*q@IZ*a%5bP7Eu z8WvX9%{}arz}#o=dvI>%pxjOsP zj!s_GvS@w+E1I=vq~4Wek8 zTkS2Zxj&af(=>FKmXJl+Y^x$k#n8EVEro@@n%nSguEP0dKR-LVNlre+pG}C8d&mM5 zUn_I$o49#>LC_a++J{;`rMI{_6@NeBy!5vXYR~%&5h51Ez>mCZE%su^m5tUNPhl~a zT8Eax<}R>PC$De37At@PQw48;_)vSX{^CQS@3g^-xQF8=q37+rFfm_1AjW!g6F$EC zWPE^0%2n}OW`Y&gU;pY;fo&*Z5j5L@jqr=~#WcH*-|VT&v-d_A+s_|1w(oRcMuB1{ zEG=Pfe&0`@5|WD0;xV-VWHRBs3G>fYPb^4q4;${MjND?l_ZRlIVW+{X?;)fiq?{uz zR}f%F-n5Pq%aV*pZm2lTa1Na;hYk41b%H4T=VB`&Q^x43EW-aB@Y=fXp!{Ff9pct6rEoKT-Q z`UBby4N{omovFOE@9zyY3gwKNHG~>GzxNCyXj^A` z(;(j7k~s)YB9sY_H`N_teEkpQFAp^SyiI2h;$b?N?-?!Iv@k!h_4+0Mo{OZY0(c1L z-L})BT^8LZmQ=c1V>Jd_1(UQ|nS41=v1vaI}4vmS{vMe$WqP z{B5_?Yi;o`XUr(Vi`I;pkbbpvbX(=B_#2#~wo!z7&Cd_`TqwN22FADns2E(pR8P4= zWbwKqIj5ku1wG(xJLkXj1st3%xdb>RNFTM|TkKUD(Z&kmWNfo+Mt23t3uD7o$L6ad z8wczC3%jQS^~3xopS4#^Ep@gz%H&kYxZdzX9oFQcKYTkzx7qA3u%o+#b{$g%y%T>T zP%YjI*MY{wm~j~JU$jxcf|nINC6dt9E50kq27*_GU+o0l7x6Qo&8V}Q{kR^DMS`uV z+p?MC8NG?bKD+*%Y8Ci&y|}A=9re*kvUbr4qY;q}Xw^$|A*|m2L2{c9N<1bZU%~^c>R}xW(o2vUDp(JdLjTv#y*qqV?-yY#Ap2 zz2Nf^Z(DL;%eN#U2xzKBB#Pb2e4ZBMkob3vxI>=zkAN>F46)}i-_IN5R5tfJ*t*vC zA2vUx`_Ml83utfDn+y5Fw|kybAz-f@EO6r0j5oN?q18G)DCTpG$whUckvdL!y>9tB zX^!SF5M<;{&jSepsCj1mFW?~8kxpxOyNY=E-3!7f$G(ne_Aw1;S{61&7)_D1XMOIZ zt%S-{_!f%AtO1<=uee~KbV_$#3xMQ7IFEtSTD4XM5HYP;YZW4!Bji}B;1uwXZPG-)`TnTjWlKg#fH%zzt_^s(ah$bV$90a+bc*Q|fD zu8VJz*Ubuw?vYUwL+$wYl_>Y&vPBY?G_z!u2U&k{u;uQ1NwHsYaJsMU2hc; zF&#BJMs8kqjl-00*3ue6rLEzB(}3#h=f2z07DR z8W^s?v@n5~1%)#paelNpKMd2|uzWM($#jfwtMGBhqaJWgpf9^BK_vceDchNb$aCG{ zRnh}qdgF#nwSs3SPuBA%NdF7#=C@c3{OgjLzrgk{b-_*GUcWqkQ9ddDF7|aBKhA2P z4Yc%49wed%hYHDcz1-{PWA`UKmGCVGIG6>+2vpr+%G3ONw43NU9g31MkSLbZA0(Qz zmVDP>McayYry}$f zYHs#`3l+Ax689^WhP_`z{ z(#oh#lk)5w1|^S@-5U*4pl1S7meYZmM2?gGBU31P;)48z_;i}=RVzmwikd*m-DTOl z=QSMPYL;iG86arP9A*p@?j~jkv0og#I{_toz$YX0chr>jfO`_LvZH)p8uExCt)8#Z zd9!g(3Do@fIX6z%QadCM_QiO#a<%^XL7~YHu<#WAY7bR05mC%Ho&NNZm$jp&JKIp& zy}rcD(y0hXlB(^S2V0LHuB)3fNl1%(IpQlCrC2lF(=6H=xv7gOf!keuO!XFfP!Iv{ z4;%k(F_U+LdBa{H8YunhBM2RpLf-l;zfKG!lp)@~;oiCKz~rc7Kfs2ckmtXxU(TSMOmiPM8~c5k`us)<;+7>)cQNxM>&@IbjYc*%Tkvm!z=uQG9Y)AfwG!mrR(( zXe;Nrd8Q5}LB%E5>7Z8Z@{nN+J-~FEdR11ix?>a2AaXEW0^?RMZsKKh5t_7oZ59?8 zi7fG#uS<4QAr+kwe++Pk5YRn^W=~3+ysqW_o@f~m6^DQH9?5(e_fDbm$u zMQX{N!TZ%Q`@5{G+Pd2Aul+>ibpffTKhv;(vpq@Y;7k-%ggsagh=@ruSXnDbe7g9I z*Zo-MI-xK5YRA>>jEP|ttRGA{B?@AUDVw|9!${0x$iN9*!^Iogw0+S;K=<}eIvOr} zpD?Ox+UZv{5#DAb?8K(jsyB1L;%oZOHGJOSfuEEScs0-a)NJ2CQ4Yq;K##CT5*?=E(u;rd4j;Q3w6XJeWpctuk^x?5)w%u;;KA##-xV6|ycK zwz|5~%R95PX3WEPDJPz`c|?lwY7rJq7af1-#CSBp6#f2{sF#YMa$l_m+vJn z&52mW&^*^&_A0DX4Y7bnOo4%uV#@a}5MPcqw7I(!6<`BEE~LJ)^;So@2_HAN;$>@+ z3aU!FwsccumpyI;KVYo|j0_F!-IEcrDqc26xvgf6Zf+QHTL@fi5t9^dXT!1vMQQey z{?#$H=3K;?2(qvmdZh!RRqasKQNK!uL;%gwi1;196dk8aQHrSQq|cFCZV7<-XET3a zH5yq-a^2PZass^^QDU{*7a@FX$s)?o-o?tw!wwb`Rkm@!WU{f{^QmPbTBiCI4OZ?w z!DYFJ`v|UWPa;q7(Wq3QQhC)6d&Y&QoZsc^B76+Ho+>qXm1+rq%L+Z0pBJmeW)l4b zs-+K00+M5i!!IW?suHfr#`p+?{!WhPx=6AiGK&0WDag;=ZpF*tCx+AJ4_DA*%?bX} zF{r?8GeGvM==lsI<|Y0Tn&L4=T`QC5QSYRKfm=w&s*8;agrmj^1EwCYL{!{+NPYJ8 zOT<3*8VAHsUas|jyONXpb)stB!;WkF9rQpogLdStliJC=10O<#zjHOe+rc1>)+V5& zPBB!Y(RM1|2;7duLfdamm&tmX0~05k0ibqX9k+*K{kA?s^*5< zUx|~8tfDiIQasp=@!cbZ=5>4bY5j>$ADE5hvi>UZr=Tba#;#fvb>Dd*S2MFDd12bO z8f{?(<9H6Xa&FtunHPu`iE;^CQTUFk64Ye>&K5CME_tGuS~9%xz0!YX!2VyU|8WZh zR&JykPD#W6B#uS`n;j*b@gf9YB3yw7MPC6PAX8&gGect&;DN0a^UcJOBUy literal 0 HcmV?d00001 diff --git a/teslausb-www-react/dist/icons/mstile-70x70.png b/teslausb-www-react/dist/icons/mstile-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ee01195e88581f50e2e94ae6bbb295fe775c0f GIT binary patch literal 2569 zcmds1c{r478-K?zqbym*he3%xOSTch-w(m&-1&U_rCAn@BZD-dp*w|PwHta6TZF3 zy#N64nVA~dfSL5=MS?=Kz3cP=1rO9sodr)0VczpyB@p8vwwyjqUoc@vmJ_*=_sEuuXTz9rKR( zmGXTGPJxDPY&-wMzLCFg_-6MzBG}rEZRd{o6$PE!%J#@kcgO#gzMF2>-;Dp`KHr@E zdy8LghRxQYXSRap+#=doodHCx1L6xRvLr7(-5L@~Tl4-5O@Z`Si4MmL(E#|~6 z*oF;%$_tO;^%ZMN)+s*daUvf-qa2q~S#K;7!lh_vs^~u?o%-`|Q(Ezld?8P>XroQt zwVFjCN+E60l_kc`5rth*DS2zrzLTTvo7Co|%=$L3M0iO>MiX%%)2ZPNkgdE zxmcT1o$3&jmMtyvameZV`b9&0NA`P8g?+0gfQXwt_t`4igu8BzM4|av;}W6QI?NLl z=bV!J$?b(s?16qeV#N|nvQYJ{o7z0~SmAOtWd$&g*1hf=Ca@$E5c%soshP^VK!&p%d9EQ^Dnss#bj^ldRkY#(l+b-b@i$j1XF& z*8ElzvZVSxKQ2%KU29d)ESA{s4DUIRp)D3XaNg*Vt`I)V z^1}weWjaU}e>o@ZI>iB2dsZsuROd-0x|1Cwp|HGku|8ngOTb42gG5di;ma#~1Jyo> ze+wt++?O z%*_R$MJV7}j;_cfvIq{`BwyKZCPbz;M&KSiM&J7&CWS|s5qP|6GDFrr%|tBnBE-~W zkG!tl!_Vq%3&P@7!J@kAi4R0YNk%m+GP->%ooAfkO<*S-T?Kc3sn{o9(r0}QEX%r+ z)RS@Gu}ZhpKQmpQ6oKU-chOO{C6aoxapcprdcDXOezB~$(JFgo^D91Zousr&beXH^ zsZN^~;y0g1zD?zw|Bw*cUL)q|de3dsIOAwTjBIDLEp@8#mJ~22JXUHPNQoI0R-dB` zBC_o$ukBU_1uTaGjdQ!lKW1ap5m3MlJnS@*ytZ;a(bB*<5*AY5`yr`#YiRP_1 zw9+h0(dARxif`x>+G2mWNO{w2HC9K!y*@aM?d{oAp1Y!1Az1NIzz4o)5=_<7u%34x z*U%ZQ1Y!v;hKyTh*36ULNS0i>KWN7s^hG8gZ~L7PIvkwa$;hFq6E`uYE|)WG!{E8uy2GhCN{-YK(z4)l@1EN;Nu^FmnMb>;idh9h+Y5?mj8Z{m;0iff<0@rKI&Y%AS4`05$0izZ2fcalsC%~gSEQ9 + + + diff --git a/teslausb-www-react/dist/icons/pencil.svg b/teslausb-www-react/dist/icons/pencil.svg new file mode 100644 index 00000000..f69749db --- /dev/null +++ b/teslausb-www-react/dist/icons/pencil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/teslausb-www-react/dist/icons/safari-pinned-tab.svg b/teslausb-www-react/dist/icons/safari-pinned-tab.svg new file mode 100644 index 00000000..6ee3d0c2 --- /dev/null +++ b/teslausb-www-react/dist/icons/safari-pinned-tab.svg @@ -0,0 +1,38 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + diff --git a/teslausb-www-react/dist/icons/site.webmanifest b/teslausb-www-react/dist/icons/site.webmanifest new file mode 100644 index 00000000..f4fa12e6 --- /dev/null +++ b/teslausb-www-react/dist/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "TeslaUSB", + "short_name": "TeslaUSB", + "icons": [ + { + "src": "/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/teslausb-www-react/dist/icons/trash.svg b/teslausb-www-react/dist/icons/trash.svg new file mode 100644 index 00000000..7b26e4d3 --- /dev/null +++ b/teslausb-www-react/dist/icons/trash.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/teslausb-www-react/dist/icons/upload.svg b/teslausb-www-react/dist/icons/upload.svg new file mode 100644 index 00000000..b8c22615 --- /dev/null +++ b/teslausb-www-react/dist/icons/upload.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/teslausb-www-react/dist/index.html b/teslausb-www-react/dist/index.html new file mode 100644 index 00000000..6d7286da --- /dev/null +++ b/teslausb-www-react/dist/index.html @@ -0,0 +1,51 @@ + + + + + + + + + TeslaUSB + + + + + + + + + + + + + +
      + + diff --git a/teslausb-www-react/dist/manifest.json b/teslausb-www-react/dist/manifest.json new file mode 100644 index 00000000..bffe30e1 --- /dev/null +++ b/teslausb-www-react/dist/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "TeslaUSB", + "short_name": "TeslaUSB", + "description": "TeslaUSB Dashboard - Manage your Tesla dashcam and music storage", + "start_url": "/", + "display": "standalone", + "background_color": "#1f2937", + "theme_color": "#1f2937", + "orientation": "any", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/teslausb-www-react/index.html b/teslausb-www-react/index.html new file mode 100644 index 00000000..d4101ca2 --- /dev/null +++ b/teslausb-www-react/index.html @@ -0,0 +1,50 @@ + + + + + + + + + TeslaUSB + + + + + + + + + + + +
      + + + diff --git a/teslausb-www-react/package-lock.json b/teslausb-www-react/package-lock.json new file mode 100644 index 00000000..d6d9f78a --- /dev/null +++ b/teslausb-www-react/package-lock.json @@ -0,0 +1,2006 @@ +{ + "name": "teslausb-www-react", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teslausb-www-react", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "preact": "^10.28.0", + "vite": "^7.2.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", + "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@preact/preset-vite": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz", + "integrity": "sha512-K9wHlJOtkE+cGqlyQ5v9kL3Ge0Ql4LlIZjkUTL+1zf3nNdF88F9UZN6VTV8jdzBX9Fl7WSzeNMSDG7qECPmSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@prefresh/vite": "^2.4.1", + "@rollup/pluginutils": "^4.1.1", + "babel-plugin-transform-hook-names": "^1.0.2", + "debug": "^4.3.4", + "picocolors": "^1.1.1", + "vite-prerender-plugin": "^0.5.3" + }, + "peerDependencies": { + "@babel/core": "7.x", + "vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x" + } + }, + "node_modules/@prefresh/babel-plugin": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.2.tgz", + "integrity": "sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/core": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.9.tgz", + "integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "preact": "^10.0.0 || ^11.0.0-0" + } + }, + "node_modules/@prefresh/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@prefresh/vite": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.11.tgz", + "integrity": "sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.22.1", + "@prefresh/babel-plugin": "0.5.2", + "@prefresh/core": "^1.5.0", + "@prefresh/utils": "^1.2.0", + "@rollup/pluginutils": "^4.2.1" + }, + "peerDependencies": { + "preact": "^10.4.0 || ^11.0.0-0", + "vite": ">=2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-plugin-transform-hook-names": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", + "integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.12.10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", + "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz", + "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/simple-code-frame": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz", + "integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.6.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "1.0.0-pre2", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz", + "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-prerender-plugin": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz", + "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "kolorist": "^1.8.0", + "magic-string": "0.x >= 0.26.0", + "node-html-parser": "^6.1.12", + "simple-code-frame": "^1.3.0", + "source-map": "^0.7.4", + "stack-trace": "^1.0.0-pre2" + }, + "peerDependencies": { + "vite": "5.x || 6.x || 7.x" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/teslausb-www-react/package.json b/teslausb-www-react/package.json new file mode 100644 index 00000000..550badcd --- /dev/null +++ b/teslausb-www-react/package.json @@ -0,0 +1,19 @@ +{ + "name": "teslausb-www-react", + "version": "1.0.0", + "description": "Modern web UI for TeslaUSB", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "preact": "^10.28.0", + "vite": "^7.2.7" + } +} diff --git a/teslausb-www-react/public/filebrowser.css b/teslausb-www-react/public/filebrowser.css new file mode 100644 index 00000000..76e10734 --- /dev/null +++ b/teslausb-www-react/public/filebrowser.css @@ -0,0 +1,592 @@ +/* FileBrowser CSS - Adapted for TeslaUSB React UI */ +/* Uses Lato font and consistent color scheme with main UI */ + +.fb-dragimage { + width: 300px; + height: 150px; + top: -500px; + background: #0095f6; + position: fixed; +} + +.fb-splitter { + width: 3px; + background: #e5e7eb; + border-style: solid; + border-color: #d1d5db; + border-width: 0 1px; + cursor: col-resize; + margin-left: 1px; + margin-right: 1px; + touch-action: none; +} + +.fb-splitterflag { + position: fixed; + width: 40px; + height: 64px; + top: 50%; + background: #e5e7eb; + border-style: solid; + border-color: #d1d5db; + border-width: 1px 0px 1px 1px; + cursor: col-resize; + touch-action: none; +} + +@media(hover: hover) { + .fb-splitterflag { + visibility: hidden; + } +} + +.fb-selection-rect { + position: absolute; + background-color: rgba(0, 149, 246, 0.3); +} + +.fb-tree { + overflow-y: auto; + flex-grow: 1; + padding-left: 30px; + margin-top: 4px; + margin-bottom: 0; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-tree ul { + padding-left: 20px; +} + +.fb-tree li { + display: block; + position: relative; +} + +details summary::-webkit-details-marker { + display: none; +} + +.fb-tree summary { + display: block; + cursor: pointer; + width: max-content; + padding: 4px 8px; + margin-left: 2px; + color: #1f2937; + font-weight: 500; + font-size: 13px; + border-radius: 4px; +} + +.fb-tree summary:hover { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-tree summary::before { + content: ''; + top: 4px; + left: -18px; + width: 14px; + height: 14px; + display: block; + position: absolute; + background: #e5e7eb; + border-radius: 50%; +} + +.fb-tree summary:has(+ ul:not(:empty))::before { + background: #93c5fd; +} + +.fb-tree details[open] > summary:has(+ ul:not(:empty))::before { + background: #0095f6; +} + +summary.fb-droptarget, +.fb-direntry.fb-droptarget, +.fb-fileentry.fb-droptarget { + color: #fff; + background: #22c55e; + border-radius: 4px; + box-shadow: 0 0 0 2px #22c55e; +} + +summary.fb-droptarget.fb-selected, +.fb-direntry.fb-droptarget.fb-selected, +.fb-fileentry.fb-droptarget.fb-selected { + color: #fff; + background: #22c55e; + border-radius: 4px; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-fileslist.fb-droptarget { + box-shadow: inset 0 0 0 3px #22c55e; +} + +.fb-treediv > ul.fb-droptarget, +details > ul.fb-droptarget { + box-shadow: inset 0 0 0 3px #22c55e; +} + +.fb-treerootpath { + position: relative; + padding: 8px 12px; + font-weight: 600; + font-size: 14px; + border-bottom: 1px solid #e5e7eb; + overflow: hidden; + white-space: nowrap; + min-height: 36px; + max-height: 36px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + color: #1f2937; + display: flex; + align-items: center; +} + +.fb-diskinfo-outer { + position: absolute; + right: 4px; + bottom: 8px; + width: 20px; + height: 20px; + background: white; +} + +.fb-diskinfo-inner { + position: absolute; + left: 2px; + top: 2px; + width: 16px; + height: 16px; + border-color: #9ca3af; + border: 1px solid; + border-radius: 50%; + font-weight: normal; + color: #9ca3af; + text-align: center; + font-size: 11px; + line-height: 14px; +} + +.fb-diskinfo-inner::before { + content: 'i'; +} + +.fb-diskinfo-outer:hover .fb-diskinfo-inner { + color: #0095f6; + background: #dbeafe; + border-color: #0095f6; +} + +.fb-diskinfo { + visibility: hidden; + position: fixed; + margin-top: 24px; + height: auto; + width: auto; + z-index: 1; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 8px 12px; + text-align: center; + background: #fff; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + font-size: 12px; + color: #6b7280; +} + +.fb-diskinfo-outer:hover .fb-diskinfo { + visibility: visible; +} + +.fb-treerootpath:has(.fb-treerootpathsinglelabel) { + padding: 8px 12px; + min-height: 36px; + max-height: 36px; +} + +.fb-dirpath { + padding: 8px 12px; + font-weight: 600; + font-size: 14px; + border-bottom: 1px solid #e5e7eb; + overflow: hidden; + white-space: nowrap; + min-height: 36px; + max-height: 36px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + color: #1f2937; +} + +.fb-crumb { + color: #0095f6; + cursor: pointer; + display: inline-block; +} + +.fb-crumb:hover { + text-decoration: underline; +} + +.fb-crumb.fb-droptarget { + text-decoration: underline; + text-decoration-thickness: 2px; +} + +.fb-fileentry { + cursor: pointer; + padding: 4px 8px; + width: max-content; + margin-left: 1px; + font-size: 13px; + color: #374151; + border-radius: 4px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-fileentry:hover:not([contenteditable="true"]) { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-fileentry.fb-selected { + color: #1e40af; + background: #dbeafe; +} + +.fb-fileentry.fb-selected:hover:not([contenteditable="true"]) { + color: #fff; + background: #2563eb; + border-radius: 4px; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-direntry { + cursor: pointer; + padding: 4px 8px; + font-weight: 500; + width: max-content; + margin-left: 1px; + font-size: 13px; + color: #1f2937; + border-radius: 4px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-direntry:hover:not([contenteditable="true"]) { + color: #fff; + background: #0095f6; + border-radius: 4px; +} + +.fb-direntry.fb-selected { + color: #1e40af; + background: #dbeafe; +} + +.fb-direntry.fb-selected:hover:not([contenteditable="true"]) { + color: #fff; + background: #2563eb; + box-shadow: 0 0 0 30px #dbeafe; + clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0 50%) +} + +.fb-treediv { + display: flex; + flex-direction: column; + float: left; + background: #fafafa; + min-width: 15%; + max-width: 85%; + width: 30%; + height: 100%; + overflow-y: auto; + user-select: none; + border-right: 1px solid #e5e7eb; +} + +.fb-filesdiv { + flex: 1; + display: flex; + flex-direction: column; + float: left; + background: #fff; + height: 100%; + margin-left: 0; + overflow-x: auto; + user-select: none; +} + +.fb-fileslist { + position: relative; + height: 100%; + margin-top: 4px; + overflow-y: auto; + flex-grow: 1; + padding: 4px; +} + +.fb-playertitle { + color: #fff; + height: 30px; + padding: 10px 8px 6px 8px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-playertitle::-webkit-scrollbar { + display: none; +} + +.fb-player { + position: absolute; + width: 300px; + height: 100px; + left: 50%; + top: 75%; + background: #1f2937; + border: none; + border-radius: 12px; + padding: 10px; + transform: translate(-50%, -50%); +} + +.fb-dropinfo-holder { + position: fixed; + visibility: hidden; + z-index: 98; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: rgba(0, 0, 0, 0); +} + +.fb-dropinfo { + width: 350px; + height: 150px; + top: calc(100% / 2 - 75px); + left: calc(100% / 2 - 175px); + border-radius: 8px; + position: relative; + visibility: inherit; + background: white; + border: 1px solid #e5e7eb; + z-index: 99; + padding: 1px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + user-select: none; + outline: none !important; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-dropinfo-line1 { + position: absolute; + top: 16px; + left: 20px; + font-weight: 600; + color: #1f2937; +} + +.fb-dropinfo-line2 { + position: absolute; + left: 20px; + right: 20px; + top: 40px; + height: 30px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + color: #6b7280; + font-size: 13px; +} + +.fb-dropinfo-line2::-webkit-scrollbar { + display: none; +} + +.fb-dropinfo-line3 { + position: absolute; + left: 20px; + right: 60px; + bottom: 5px; + height: 30px; + white-space: nowrap; + overflow: auto; + text-align: left; + scroll-behavior: auto; + scrollbar-width: none; + -ms-overflow-style: none; + color: #6b7280; + font-size: 12px; +} + +.fb-dropinfo-line3::-webkit-scrollbar { + display: none; +} + +.fb-dropinfo-closebutton { + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + background: white; + text-align: center; + cursor: pointer; + border-radius: 4px; + color: #9ca3af; + line-height: 24px; +} + +.fb-dropinfo-closebutton:hover { + background: #f3f4f6; + color: #1f2937; +} + +.fb-dropinfo-cancel { + position: absolute; + bottom: 12px; + right: 12px; + padding: 6px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 13px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; +} + +.fb-dropinfo-cancel:hover { + background: #f3f4f6; +} + +.fb-dropinfo-progress { + position: absolute; + left: 20px; + width: calc(100% - 40px); + top: 90px; + height: 8px; + border-radius: 4px; + overflow: hidden; + background: #e5e7eb; + -webkit-appearance: none; + appearance: none; +} + +.fb-dropinfo-progress::-webkit-progress-bar { + background: #e5e7eb; + border-radius: 4px; +} + +.fb-dropinfo-progress::-webkit-progress-value { + background: #0095f6; + border-radius: 4px; +} + +.fb-dropinfo-progress::-moz-progress-bar { + background: #0095f6; + border-radius: 4px; +} + +.fb-barbutton { + float: left; + width: 40px; + height: 40px; + border: none; + padding: 0; + z-index: 3; + display: none; + cursor: pointer; + border-radius: 8px; + margin: 4px; + background-size: 24px 24px; + background-position: center; + background-repeat: no-repeat; +} + +.fb-barbutton:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.fb-barbutton.fb-visiblebarbutton { + display: block; +} + +.fb-trashbutton { + background-image: url(icons/trash.svg); +} + +.fb-pencilbutton { + background-image: url(icons/pencil.svg); +} + +.fb-uploadbutton { + background-image: url(icons/upload.svg); +} + +.fb-downloadbutton { + background-image: url(icons/download.svg); +} + +.fb-newfolderbutton { + background-image: url(icons/newfolder.svg); +} + +.fb-locksoundbutton { + background-image: url(icons/locksound.svg); +} + +.fb-buttonbar { + position: fixed; + right: 24px; + bottom: 24px; + height: 48px; + padding: 0 4px; + background: #fff; + z-index: 2; + border-radius: 12px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + border: 1px solid #e5e7eb; + display: flex; + align-items: center; +} + +@media(hover: hover) { + .uploadbutton { + display: none; + } +} + +.fb-driveselector { + font-size: 14px; + font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif; + background: transparent; + border-width: 1px; + border-style: solid; + border-color: #d1d5db; + border-radius: 4px; + padding: 4px 8px; + color: #1f2937; +} + +.fb-driveselector:focus { + outline: none; + border-color: #0095f6; +} diff --git a/teslausb-www-react/public/filebrowser.js b/teslausb-www-react/public/filebrowser.js new file mode 100644 index 00000000..c8b513fc --- /dev/null +++ b/teslausb-www-react/public/filebrowser.js @@ -0,0 +1,1315 @@ +class FileBrowser { + DEBUG = false; + splitter_active = false; + splitter_clickoffset = 0; + root_path = ''; + root_label = ''; + anchor_elem = undefined; + dragged_path = undefined; + cancelUpload = false; + uploading = false; + drives = []; + curdrive = 0; + + constructor(anchor, drives) { + this.anchor_elem = anchor; + this.drives = drives; + this.root_path = drives[this.curdrive].path; + this.root_label = drives[this.curdrive].label; + + this.anchor_elem.style.position = "relative"; + this.anchor_elem.style.display = "flex"; + this.anchor_elem.spellcheck = false; + this.anchor_elem.innerHTML = + ` +
      +
      +
      +
      +
      +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      +
      +
        +
        +
        +
        +
        +
        +
        +
        + `; + + this.dragStart = this.dragStart.bind(this); + this.dragEnd = this.dragEnd.bind(this); + this.allowDrop = this.allowDrop.bind(this); + this.dragEnter = this.dragEnter.bind(this); + this.drop = this.drop.bind(this); + this.readPaths = this.readPaths.bind(this); + this.dirClicked = this.dirClicked.bind(this); + this.fileClicked = this.fileClicked.bind(this); + this.listPointerDown = this.listPointerDown.bind(this); + this.splitterPointerDown = this.splitterPointerDown.bind(this); + this.splitterPointerMove = this.splitterPointerMove.bind(this); + this.splitterPointerUp = this.splitterPointerUp.bind(this); + this.showContextMenu = this.showContextMenu.bind(this); + this.hideContextMenu = this.hideContextMenu.bind(this); + + const splitter = this.anchor_elem.querySelector(".fb-splitter"); + splitter.onpointerdown = (e) => { this.splitterPointerDown(e); }; + const splitterflag = this.anchor_elem.querySelector(".fb-splitterflag"); + splitterflag.onpointerdown = (e) => { this.splitterPointerDown(e); }; + this.splitterSetFlagPos(); + + const fileList = this.anchor_elem.querySelector('.fb-fileslist'); + fileList.onpointerdown = (e) => { this.listPointerDown(e); }; + fileList.oncontextmenu = (e) => { this.showContextMenu(e); }; + fileList.addEventListener("dragstart", this.dragStart); + this.anchor_elem.addEventListener("dragend", this.dragEnd); + + const rootlabel = this.anchor_elem.querySelector(".fb-treerootpath"); + if (this.drives.length > 1) { + var rootlabeldropdown = '
        ' + rootlabel.innerHTML = rootlabeldropdown; + const selector = this.anchor_elem.querySelector(".fb-driveselector"); + selector.onchange = (e) => { + this.curdrive = selector.value; + this.root_path = this.drives[this.curdrive].path; + this.root_label = this.drives[this.curdrive].label; + this.ls(".", false); + this.ls(".", true); + this.updateButtonBar(); + }; + } else { + rootlabel.innerHTML = `${this.drives[0].label}
        `; + } + + this.buttonbar = this.anchor_elem.querySelector(".fb-buttonbar"); + this.buttonbar.querySelector(".fb-uploadbutton").onclick = (e) => { this.pickFile(); }; + this.buttonbar.querySelector(".fb-downloadbutton").onclick = (e) => { this.downloadSelection(); }; + this.buttonbar.querySelector(".fb-newfolderbutton").onclick = (e) => { this.newFolder(); }; + this.buttonbar.querySelector(".fb-trashbutton").onclick = (e) => { this.deleteItems(this.selection()); }; + this.buttonbar.querySelector(".fb-pencilbutton").onclick = (e) => { + const item = this.selection()[0]; + item.scrollIntoView({block: "nearest"}); + this.renameItem(item); + }; + this.buttonbar.querySelector(".fb-locksoundbutton").onclick = (e) => { + const item = this.selection()[0]; + this.makeLockChime(item); + }; + + this.ls(".", true); + this.updateButtonBar(); + } + + log(msg) { + if (this.DEBUG) { + console.log(msg); + } + } + + async refreshLists(callback) { + var tree = this.anchor_elem.querySelector(".fb-tree"); + var openPaths = []; + tree.querySelectorAll("details[open] > summary").forEach((s) => { openPaths.push(s.dataset.fullpath); }); + await this.ls(".", false); + openPaths.forEach((path) => { + var detail = tree.querySelector(`details:has(summary[data-fullpath="${path}"])`); + if (detail != null) { + detail.open = true; + } + }); + await this.ls(this.current_path, true); + if (callback) { + callback(); + } + } + + newFolder() { + var fl = this.anchor_elem.querySelector(".fb-fileslist"); + for (var i = 1; i < 100; i++) { + var str = `${this.current_path == "." ? "" : this.current_path + "/"}New folder${i == 1 ? '' : " (" + i + ")"}`; + this.log(str); + var item = fl.querySelector(`[data-fullpath="${this.stringEncode(str)}"]`); + if (item == null) { + this.readfile( + {url:`cgi-bin/mkdir.sh?${encodeURIComponent(this.root_path)}&${encodeURIComponent(str)}`, + callback:(response, data) => { + this.refreshLists(() => { + var item = fl.querySelector(`[data-fullpath="${this.stringEncode(str)}"]`); + if (item) { + item.scrollIntoView({block: "nearest"}); + this.selectItem(item); + this.renameItem(item); + } + }); + } + }); + return; + } + } + this.log("could not find empty new folder name"); + } + + deleteItems(items) { + var pathsList = ""; + items.forEach((item) => { + const fullpath = this.stringDecode(item.dataset.fullpath) + pathsList += "&"; + pathsList += encodeURIComponent(fullpath); + }); + this.readfile( + {url:`cgi-bin/rm.sh?${this.root_path}${pathsList}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + deleteItem(item) { + this.deleteItems([item]); + } + + downloadSelection() { + const url = this.downloadURLForSelection().substr(1); + const name = url.substr(0, url.indexOf(":")); + const url2 = url.substr(url.indexOf(":") + 1); + this.log(`name: ${name}, url: ${url2}`); + + var elem = document.createElement('a'); + elem.setAttribute('href', url2); + elem.setAttribute('download', name); + elem.style.display = 'none'; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); + } + + selectItemContent(item) { + var range,selection; + if(document.createRange) + { + range = document.createRange(); + range.selectNodeContents(item); + selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + applyRename(item) { + const fullpath = this.stringDecode(item.dataset.fullpath); + const idx = fullpath.lastIndexOf("/"); + const oldname = fullpath.substr(idx + 1); + var newname = item.textContent; + this.log('renaming "' + oldname + '" to "' + newname + '"'); + this.readfile( + {url:`cgi-bin/mv.sh?${this.root_path}/${this.current_path}&${encodeURIComponent(oldname)}&${encodeURIComponent(newname)}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + stopEditingItem(item, oldValue) { + // clear onblur first because setting contentEditable to false immediately triggers a blur + item.onblur = undefined; + item.contentEditable = false; + item.onkeydown = undefined; + item.oncontextmenu = undefined; + if (item.textContent != oldValue) { + this.applyRename(item); + } + } + + renameItem(item) { + const oldValue = item.textContent; + item.contentEditable = true; + this.selectItemContent(item); + item.onkeydown = (e) => { + if (e.key == 'Enter') { + this.stopEditingItem(item, oldValue); + } else if (e.key == 'Escape') { + item.textContent = oldValue; + this.stopEditingItem(item, oldValue); + } + }; + item.onblur = (e) => { + this.stopEditingItem(item, oldValue); + }; + // Tapping on the selected text to position the cursor + // on mobile results in a context menu event, so intercept + // that while editing. + item.oncontextmenu = (e) => { e.stopPropagation(); }; + item.focus(); + } + + makeLockChime(item) { + // copy the selected item + this.readfile( + {url:`cgi-bin/cp.sh?${this.root_path}&${encodeURIComponent(item.dataset.fullpath)}&LockChime.wav`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + } + + showButton(name, show) { + if (show) { + this.buttonbar.querySelector(name).classList.add("fb-visiblebarbutton"); + } else { + this.buttonbar.querySelector(name).classList.remove("fb-visiblebarbutton"); + } + } + + updateButtonBar() { + const numsel = this.numSelected(); + const enabled = this.valid; + this.showButton(".fb-trashbutton", enabled && numsel > 0); + this.showButton(".fb-pencilbutton", enabled && numsel == 1); + this.showButton(".fb-uploadbutton", enabled && numsel == 0); + this.showButton(".fb-downloadbutton", enabled && numsel > 0); + this.showButton(".fb-newfolderbutton", enabled && numsel == 0); + this.showButton(".fb-locksoundbutton", enabled && numsel == 1 && this.isPotentialLockChime(this.selection()[0])); + if (this.buttonbar.querySelector(".fb-visiblebarbutton") == null) { + this.buttonbar.style.display = "none"; + } else { + this.buttonbar.style.display = "block"; + } + } + + eventCoordinates(e) { + if (e.targetTouches && e.targetTouches.length > 1) { + const t = e.targetTouches[0] + return [t.clientX, t.clientY]; + } + return [e.x, e.y]; + } + + makeMultiSelectContextMenu(event) { + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("Download selected items", + () => { + this.downloadSelection(); + }, null), + new ContextMenuItem("Delete selected items", + () => { + this.deleteItems(this.selection()); + }, null) + + ]).show(...this.eventCoordinates(event)); + } + + makeListContextMenu(event) { + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("New folder", () => { this.newFolder(); }, null) + ]).show(...this.eventCoordinates(event)); + } + + makeDirContextMenu(event) { + const e = event; + new ContextMenu(this.anchor_elem, + [ + new ContextMenuItem("Rename", () => { this.renameItem(e.target); }, null), + new ContextMenuItem("Download", () => { this.downloadSelection(); }, null), + new ContextMenuItem("Delete", () => { this.deleteItem(e.target); }, null) + ]).show(...this.eventCoordinates(event)); + } + + makeFileContextMenu(event) { + const filename = event.target.innerText; + const e = event; + new ContextMenu(this.anchor_elem, + [ + ...this.isPotentialLockChime(event.target) ? [ new ContextMenuItem("Use as lock sound", () => { this.makeLockChime(e.target); }, null) ] : [], + new ContextMenuItem("Rename", () => { this.renameItem(e.target); }, null), + new ContextMenuItem("Download", () => { this.downloadSelection(); }, null), + new ContextMenuItem("Delete", () => { this.deleteItem(e.target); }, null) + ]).show(...this.eventCoordinates(event)); + } + + hideContextMenu() { + var contextmenu = this.anchor_elem.querySelector(".cm-holder"); + if (contextmenu == null) { + return; + } + contextmenu.style.visibility = "hidden"; + } + + showContextMenu(event) { + this.log("context menu"); + if (!this.valid || event.ctrlKey) { + this.hideContextMenu(); + return; + } + if (event.pointerType == "touch") { + // long press triggered context menu + this.log("touch context menu"); + event.preventDefault(); + return; + } + var target = event.target; + try { + if (target.classList.contains("fb-fileslist")) { + this.unselectAll(); + this.makeListContextMenu(event); + } else if (this.numSelected() > 1 && this.isSelected(target)) { + this.makeMultiSelectContextMenu(event); + } else { + this.unselectAll(); + this.selectItem(target); + if (target.classList.contains("fb-direntry")) { + this.makeDirContextMenu(event); + } else if (target.classList.contains("fb-fileentry")) { + this.makeFileContextMenu(event); + } + } + event.preventDefault(); + } catch (e) { + this.log("error creating context menu"); + this.log(e); + } + } + + splitterSetFlagPos() { + const splitter = this.anchor_elem.querySelector(".fb-splitter"); + const splitterflag = this.anchor_elem.querySelector(".fb-splitterflag"); + splitterflag.style.left = (splitter.getBoundingClientRect().x - + splitterflag.getBoundingClientRect().width + 1) + "px"; + } + + splitterPointerMove(event) { + const treediv = this.anchor_elem.querySelector(".fb-splitter").previousElementSibling; + const treedivrect = treediv.getBoundingClientRect(); + const newwidth = event.clientX + this.splitter_clickoffset - treedivrect.x; + treediv.style.width = `${newwidth}px`; + this.splitterSetFlagPos(); + } + + splitterPointerUp(event) { + const splitter = event.target; + splitter.removeEventListener("pointermove", this.splitterPointerMove); + splitter.removeEventListener("pointerup", this.splitterPointerUp); + splitter.releasePointerCapture(event.pointerId); + this.splitter_active = false; + } + + splitterPointerDown(event) { + var splitter = event.target; + splitter.addEventListener("pointermove", this.splitterPointerMove); + splitter.addEventListener("pointerup", this.splitterPointerUp); + splitter.setPointerCapture(event.pointerId); + const treediv = this.anchor_elem.querySelector(".fb-splitter").previousElementSibling; + const treedivrect = treediv.getBoundingClientRect(); + this.splitter_clickoffset = treedivrect.x + treedivrect.width - event.clientX; + this.splitter_active = true; + } + + intersects(r1, r2) { + return !(r1.x + r1.width < r2.x || + r2.x + r2.width < r1.x || + r1.y + r1.height < r2.y || + r2.y + r2.height < r1.y); + } + + updateSelection(selectRectElem) { + const select = selectRectElem.getBoundingClientRect(); + + const {x, y, height, width} = select; + + selectRectElem.parentElement.querySelectorAll(".fb-direntry, .fb-fileentry").forEach((item) => { + if (this.intersects({x: x + window.scrollX, y: y + window.scrollY, height, width}, item.getBoundingClientRect())){ + item.classList.add("fb-selected"); + } else { + item.classList.remove("fb-selected"); + } + } ); + this.updateButtonBar(); + } + + unselectAll() { + this.selection().forEach((e) => { e.classList.remove("fb-selected");}); + this.updateButtonBar(); + } + + isSelected(elem) { + return elem.classList.contains("fb-selected") + } + + selectItem(elem) { + elem.classList.add("fb-selected"); + this.updateButtonBar(); + } + + selection() { + var fl = this.anchor_elem.querySelector(".fb-fileslist"); + return fl.querySelectorAll(".fb-selected"); + } + + numSelected() { + return this.selection().length; + } + + async createSelectionRectangle(thiz, event) { + const fileList = event.target; + const x = event.offsetX + fileList.scrollLeft; + const y = event.offsetY + fileList.scrollTop; + + const div = document.createElement("div"); + div.style.width = "0"; + div.style.height = "0"; + div.style.left = x + "px"; + div.style.top = y + "px"; + div.classList.add("fb-selection-rect"); + fileList.append(div); + + function resize(event) { + thiz.log("resize"); + if (event.buttons == 0) { + cancelSelectionRectangle(event); + return; + } + if (event.target != fileList) { + return; + } + + const rect = fileList.getBoundingClientRect(); + const offX = (event.touches ? event.touches[0].clientX - rect.left : + event.offsetX) + fileList.scrollLeft; + const offY = (event.touches ? event.touches[0].clientY - rect.top : + event.offsetY) + fileList.scrollTop; + const maxX = fileList.clientWidth + fileList.scrollLeft; + const maxY = fileList.clientHeight + fileList.scrollTop; + const curX = offX > maxX ? maxX : offX; + const curY = offY > maxY ? maxY : offY; + const dX = curX - x; + const dY = curY - y; + div.style.left = dX < 0 ? x + dX + "px" : x + "px"; + div.style.top = dY < 0 ? y + dY + "px" : y + "px"; + div.style.width = Math.abs(dX) + "px"; + div.style.height = Math.abs(dY) + "px"; + thiz.updateSelection(div); + if (event.cancelable) { + event.preventDefault(); + } + } + + if (! event.ctrlKey) { + this.unselectAll(); + } + + function cancelSelectionRectangle(event) { + thiz.log("cancelSelectionRectangle"); + event.target.removeEventListener("pointermove", resize); + try { + fileList.releasePointerCapture(event.pointerId); + } catch(error) { + thiz.log("release error"); + } + fileList.removeEventListener("touchmove", resize); + fileList.removeEventListener("touchend", pointerup); + fileList.removeEventListener("pointermove", resize); + fileList.removeEventListener("pointerup", pointerup); + fileList.removeEventListener("cancelselectionrect", cancelSelectionRectangle); + div.remove(); + } + + function pointerup(event) { + thiz.log("pointerup"); + cancelSelectionRectangle(event); + } + + fileList.setPointerCapture(event.pointerId); + if (event.pointerType == "touch") { + fileList.addEventListener("touchmove", resize); + fileList.addEventListener("touchend", pointerup); + } else { + fileList.addEventListener("pointermove", resize); + fileList.addEventListener("pointerup", pointerup); + } + fileList.addEventListener("cancelselectionrect", cancelSelectionRectangle); + } + + listPointerDown(event) { + /* only respond to left mouse button */ + if (!this.valid || event.button != 0) { + event.preventDefault(); + return; + } + event.target.dispatchEvent( + new Event("cancelselectionrect", { + bubbles: true, + cancelable: true, + composed: false + }) + ); + this.createSelectionRectangle(this, event); + } + + dirClicked(event, path) { + var expanderclicked = (event && event.offsetX < 0); + this.ls(path, !expanderclicked); + } + + isPlayable(filename) { + const lower = filename.toLowerCase(); + for (const ext of [".mp3", ".m4a", ".flac", ".ogg", ".wav"]) { + if (lower.endsWith(ext)) { + return true; + } + } + return false; + } + + isPotentialLockChime(item) { + const lower = item.dataset.fullpath.toLowerCase(); + if (lower == "lockchime.wav") { + return false; + } + if (!lower.endsWith(".wav")) { + return false; + } + if (item.dataset.filesize > 1024 * 1024) { + return false; + } + return true; + } + + fileClicked(event, path) { + this.log(`clicked: ${path}`); + + var displaypath = path; + if (this.isPlayable(path)) { + const div = document.createElement("div"); + div.style.position = "absolute"; + div.style.width = "100%"; + div.style.height = "100%"; + div.style.left = "0"; + div.style.top = "0"; + div.style.background = "#0008"; + div.onclick = (e) => { if (e.target === div) div.remove(); }; + document.firstElementChild.append(div); + div.innerHTML = `
        ${displaypath}
        `; + div.querySelector(".fb-playertitle").scrollLeft=1000; + } + } + + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. + base64ToBytes(base64) { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); + } + + // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem. + bytesToBase64(bytes) { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); + } + + stringEncode(str) { + return str; // this.bytesToBase64((new TextEncoder()).encode(str)); + } + + stringDecode(encstr) { + return encstr; // new TextDecoder().decode(this.base64ToBytes(encstr)); + } + + addCommonDragHooks(item) { + item.ondragover = this.allowDrop; + item.ondragenter = this.dragEnter; + item.ondragleave = this.dragLeave; + item.ondrop = this.drop; + } + + createTreeItem(label, fullPath) { + var li = document.createElement("li"); + li.innerHTML = '
        ' + + '' + label + '' + + '
          '; + const s = li.querySelector("summary"); + s.onclick = (e) => { this.dirClicked(e, this.stringDecode(e.target.dataset.fullpath)); }; + s.ondragstart = this.dragStart; + const u = li.querySelector("ul"); + u.dataset.fullpath = fullPath; + this.addCommonDragHooks(u); + return li; + } + + addDir(root, path) { + var pathParts = path.split("/"); + var pathSoFar = null; + for (var i = 0; i < pathParts.length; i++) { + if (pathSoFar) { + pathSoFar += "/" + pathParts[i]; + } else { + pathSoFar = pathParts[i]; + } + var node = root.querySelector(`[data-fullpath="${this.stringEncode(pathSoFar)}"]`); + if (node == null) { + /* level 'i' doesn't exist yet, add it */ + var newPath = this.createTreeItem(pathParts[i], pathSoFar); + root.appendChild(newPath); + root = newPath.querySelector("ul"); + } else { + /* level 'i' already exists, check the next level */ + root = node.nextElementSibling; + } + } + return root; + } + + readfile({url, callback, callbackarg}) { + var request = new XMLHttpRequest(); + request.open('GET', url); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status === 200) { + var type = request.getResponseHeader('Content-Type'); + if (type.indexOf("text") !== 1) { + if (callback != null) { + callback(request.responseText, callbackarg); + } + } + } else if (request.status > 400) { + if (callback != null) { + callback(null, null); + } + } + } + } + request.send(); + } + + selectFileEntry(ev) { + if (ev.button != 0) return; + ev.stopPropagation(); + if (! ev.ctrlKey) { + /* not multi-select, so deselect everything that was previously selected */ + this.unselectAll(); + } + ev.target.classList.toggle("fb-selected"); + this.updateButtonBar(); + } + + createFileEntry(isdir, name, path, size) { + var div = document.createElement("div"); + div.className = isdir ? "fb-direntry" : "fb-fileentry" + div.textContent = name; + div.draggable = true; + if (isdir) { + div.ondblclick = (e) => { this.dirClicked(null, path); }; + this.addCommonDragHooks(div); + div.dataset.fullpath = this.stringEncode(path); + } else { + const justThePath = path.substring(0, path.lastIndexOf(":")); + div.ondblclick = (e) => { this.fileClicked(null, justThePath); }; + div.dataset.fullpath = this.stringEncode(justThePath); + div.dataset.filesize = path.substring(path.lastIndexOf(":") + 1); + } + div.onclick = (e) => { this.selectFileEntry(e); }; + div.onpointerdown = (e) => { e.stopPropagation(); }; + return div; + } + + addFileEntry(path) { + var isDir = path.indexOf("d:") == 0; + path = path.substring(2); + var lastSlash = path.lastIndexOf("/"); + var name = path.substring(lastSlash + 1); + var lastColon = name.lastIndexOf(":"); + var size = 0; + if (lastColon > 0) { + size = name.substring(lastColon + 1); + name = name.substring(0, lastColon); + } + var newFile = this.createFileEntry(isDir, name, path, size); + var listDiv = this.anchor_elem.querySelector('.fb-fileslist'); + listDiv.appendChild(newFile); + } + + setDiskStats(freebytes, totalbytes) { + var diskinfospan = this.anchor_elem.querySelector('.fb-diskinfo'); + diskinfospan.innerText = `${this.niceNumber(freebytes)} free of ${this.niceNumber(totalbytes)}`; + } + + /* + switchtopath=false: only update the left-hand side tree view + switchtopath=true: update the right-hand side + */ + readPaths(path, paths, switchtopath) { + this.valid = (paths != null && switchtopath != null); + if (!this.valid) { + var pathdiv = this.anchor_elem.querySelector(".fb-dirpath"); + pathdiv.innerText = "<< error retrieving file list >>"; + } + paths = this.valid ? paths.trimEnd() : ""; + var root = this.anchor_elem.querySelector(".fb-tree"); + root.dataset.fullpath="."; + this.addCommonDragHooks(root); + if (path == "." && !switchtopath) { + root.innerHTML = ''; + } + var lines = paths.split('\n'); + if (! this.valid || switchtopath) { + this.anchor_elem.querySelector('.fb-fileslist').querySelectorAll(".fb-direntry,.fb-fileentry").forEach((entry) => entry.remove()); + } + for (var line of lines) { + if (line.indexOf("d:") == 0 || line.indexOf("D:") == 0) { + this.addDir(root, line.substring(2)); + } + if (line.indexOf("s:") == 0) { + let [freebytes, totalbytes] = line.substring(2).split(":"); + this.setDiskStats(freebytes, totalbytes); + } else if (switchtopath && ! line.indexOf("D:") == 0) { + this.addFileEntry(line); + } + } + this.updateButtonBar(); + } + + makeOnClick(thiz, path) { + return function() { thiz.dirClicked(null, path); }; + + } + + setClickablePath(container, path) { + var pathParts = []; + if (!path) { + path = "."; + } else if (path != ".") { + pathParts = path.split("/"); + } + this.current_path = path; + const fileList = this.anchor_elem.querySelector('.fb-fileslist'); + fileList.dataset.fullpath = this.stringEncode(path); + this.addCommonDragHooks(fileList); + + container.innerHTML = ""; + var pathSoFar = null; + + var a = document.createElement("a"); + if (pathParts.length > 0) { + a.className = "fb-crumb"; + a.onclick = this.makeOnClick(this, "."); + this.addCommonDragHooks(a); + a.dataset.fullpath = this.stringEncode("."); + } + a.innerText = '[' + this.root_label + ']'; + container.appendChild(a); + + for (var i = 0; i < pathParts.length; i++) { + if (pathSoFar) { + pathSoFar += "/" + pathParts[i]; + } else{ + pathSoFar = pathParts[i]; + } + var a = document.createElement("a"); + if (i < pathParts.length - 1) { + a.className = "fb-crumb"; + a.onclick = this.makeOnClick(this, pathSoFar); + this.addCommonDragHooks(a); + a.dataset.fullpath = this.stringEncode(pathSoFar); + } + a.innerText = pathParts[i]; + container.append("/"); + container.appendChild(a); + } + } + + async ls(path, switchtopath) { + return new Promise((resolve, reject) => { + + if (switchtopath) { + var pathdiv = this.anchor_elem.querySelector(".fb-dirpath"); + this.setClickablePath(pathdiv, path); + } + this.readfile({url:`cgi-bin/ls.sh?${encodeURIComponent(this.root_path)}&${encodeURIComponent(path)}`, callback:(paths,switchto) => { this.readPaths(path, paths, switchto); resolve(); }, callbackarg:switchtopath}); + }); + } + + isDropAllowedForTarget(ev) { + var destPath = this.stringDecode(ev.target.dataset.fullpath); + if (destPath == undefined) { + return false; + } + if (ev.target.classList.contains("fb-fileentry")) { + /* files are not drop targets */ + return false; + } + if (!this.dragged_path) { + /* external files are not subject to path checks */ + return true; + } + var pathList = []; + const selection = this.selection(); + if (selection.length == 0) { + // single item from the left side tree view + pathList.push(this.dragged_path); + } else { + // one or more items from the right side file/folder view + selection.forEach((srcItem) => { + pathList.push(this.stringDecode(srcItem.dataset.fullpath)); + }); + } + //this.log(`${pathList.toString()} => ${destPath}`); + for (var srcPath of pathList) { + if (destPath.startsWith(srcPath)) { + /* can't drop parent in child */ + return false; + } + const idx = srcPath.lastIndexOf("/"); + const srcDir = idx > 0 ? srcPath.substr(0, idx) : "."; + if (srcDir == destPath) { + //this.log(`not OK: ${srcDir} => ${destPath}`); + return; + } + //this.log(`OK: ${srcPath} => ${destPath}`); + } + return true; + } + + allowDrop(ev) { + if (!this.isDropAllowedForTarget(ev)) { + return; + } + ev.preventDefault(); + } + + dragEnter(ev) { + if (this.isDropAllowedForTarget(ev)) { + ev.target.classList.add("fb-droptarget"); + } + } + + dragLeave(ev) { + ev.target.classList.remove("fb-droptarget"); + } + + hasExternalFiles(ev) { + this.log(`${ev.dataTransfer.items.length} items dropped`); + this.log(ev.dataTransfer.items.length); + this.log(...ev.dataTransfer.items); + this.log(ev.dataTransfer); + this.log(ev); + if (ev.dataTransfer.items.length > 0) { + return true; + } + return false; + } + + handleInternalDrop(ev) { + const selection = this.selection(); + var pathList = []; + if (selection.length == 0) { + pathList.push(this.dragged_path); + } else { + selection.forEach((srcItem) => { + pathList.push(this.stringDecode(srcItem.dataset.fullpath)); + }); + } + var pathString = ""; + pathList.forEach((path) => { + pathString += `&${encodeURIComponent(path)}`; + }); + this.log(`${pathList} => ${this.stringDecode(ev.target.dataset.fullpath)}`); + this.readfile( + {url:`cgi-bin/mv.sh?${this.root_path}${pathString}&${this.stringDecode(ev.target.dataset.fullpath)}`, + callback:(response, data) => { + this.log(response); + this.refreshLists(); + } + }); + + } + + async cancelDrop() { + if (!this.cancelUpload) { + this.cancelUpload = true; + const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); + while (this.uploading) { + await sleep(50); + } + } + this.hideDropInfo(); + } + + showDropInfo() { + var di = this.anchor_elem.querySelector(".fb-dropinfo-holder"); + di.style.visibility = "visible"; + var cb = this.anchor_elem.querySelector(".fb-dropinfo-closebutton"); + cb.onmousedown = (e) => { this.cancelDrop(); }; + var cb = this.anchor_elem.querySelector(".fb-dropinfo-cancel"); + cb.onclick = (e) => { this.cancelDrop(); }; + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.innerText = "Building file list..."; + l1.style.visibility="inherit"; + var p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.style.visibility="hidden"; + } + + niceNumber(totalsize) { + var str = ""; + if (totalsize < 100000) { + str += `${totalsize} bytes` + } else if (totalsize < 2000000) { + str += `${(totalsize / 1024).toFixed(0)} KB` + } else if (totalsize < 1100000000) { + str += `${(totalsize / (1024 * 1024)).toFixed(0)} MB` + } else { + str += `${(totalsize / (1024 * 1024 * 1024)).toFixed(2)} GB` + } + return str; + } + + updateDropInfo(numfiles, totalsize) { + var l2 = this.anchor_elem.querySelector(".fb-dropinfo-line2"); + var str = `${numfiles} file`; + if (numfiles != 1) { + str += "s"; + } + str += ", " + this.niceNumber(totalsize); + l2.innerText = str; + + } + + hideDropInfo() { + var di = this.anchor_elem.querySelector(".fb-dropinfo-holder"); + di.style.visibility = "hidden"; + this.refreshLists(); + } + + async getFilePromise(entry) { + try { + if (entry instanceof File) { + return entry; + } + return await new Promise((resolve, reject) => { + entry.file(resolve, reject); + }); + } catch (err) { + this.log(err); + } + } + + async readEntriesPromise(reader) { + try { + return await new Promise((resolve, reject) => { + reader.readEntries(resolve, reject); + }); + } catch (err) { + this.log(err); + } + } + + async readAllDirectoryEntries(reader) { + var entries = []; + var readEntries = await this.readEntriesPromise(reader); + while (readEntries.length > 0) { + entries.push(...readEntries); + readEntries = await this.readEntriesPromise(reader); + } + return entries; + } + + async handleExternalDrop(targetpath, datatransferitems, files) { + this.cancelUpload = false; + this.uploading = true; + var totalBytes = 0; + var fileList = []; + var queue = []; + if (datatransferitems) { + // Use DataTransferItemList interface to access the file(s) + [...datatransferitems].forEach((item, i) => { + var entry = item.webkitGetAsEntry(); + this.log(entry); + if (entry != null) { + queue.push(entry); + } + }); + } else if (files) { + queue.push(...files); + } + + while (queue.length > 0) { + if (this.cancelUpload) { + this.uploading = false; + return; + } + //this.log(`processing... (${fileList.length})`); + var entry = queue.shift(); + if (entry.isDirectory) { + queue.push(...await this.readAllDirectoryEntries(entry.createReader())); + } else { + fileList.push(entry); + if (fileList.length == 1) { + this.showDropInfo(); + } + var file = await this.getFilePromise(entry); + totalBytes += file.size; + this.updateDropInfo(fileList.length, totalBytes); + } + } + this.log(`total size: ${totalBytes}`); + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.numitems = fileList.length; + + var p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.style.visibility="inherit"; + p.max = totalBytes; + p.value = 0; + this.uploadFiles(targetpath, fileList); + } + + pickFile() { + var input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.onchange = (e) => { + if (input.files.length > 0) { + this.handleExternalDrop(this.current_path, null, input.files); + } + }; + input.click(); + } + + uploadFiles(destpath, fileList) { + var lastLoaded = 0; + if (fileList.length > 0 && ! this.cancelUpload) { + var f = fileList.shift(); + var l1 = this.anchor_elem.querySelector(".fb-dropinfo-line1"); + l1.innerText = `File ${l1.numitems - fileList.length} / ${l1.numitems}`; + var l2 = this.anchor_elem.querySelector(".fb-dropinfo-line2"); + l2.innerText = f.name; + this.uploadFile(destpath, f, + (status) => { + // completion function + if (status === 200) { + this.uploadFiles(destpath, fileList); + } else { + this.log(`status: ${status}`); + this.uploading = false; + } + }, + (e, request) => { + // progress function + const p = this.anchor_elem.querySelector(".fb-dropinfo-progress"); + p.value += (e.loaded - lastLoaded); + const size1 = this.niceNumber(p.value); + const size2 = this.niceNumber(p.max); + const s = `${size1} / ${size2}`; + const l3 = this.anchor_elem.querySelector(".fb-dropinfo-line3"); + if (l3.innerText != s) { + l3.innerText = s; + } + lastLoaded = e.loaded; + if (this.cancelUpload) { + this.log("cancelling upload"); + request.abort(); + } + }); + } else { + this.hideDropInfo(); + } + } + + async uploadFile(destpath, entry, completionCallback, progressCallback) { + var sent = 0; + var file = await this.getFilePromise(entry); + var relpath = (entry instanceof File) ? file.name : entry.fullPath.substr(1); + + const request = new XMLHttpRequest(); + request.open("POST", `cgi-bin/upload.sh?${encodeURIComponent(this.root_path + "/" + destpath)}&${encodeURIComponent(relpath)}`); + request.setRequestHeader("Content-Type", "application/octet-stream"); + request.onreadystatechange = () => { + // Call a function when the state changes. + if (request.readyState === XMLHttpRequest.DONE) { + completionCallback(request.status); + } + }; + request.upload.onprogress = (e) => { + progressCallback(e, request); + }; + this.log(`uploading with progress ${file.name}`); + + request.send(file); + } + + dragColor(counter) { + if (counter > 4) { + return "#00000000"; + } + return "#000000" + ["ff", "cc", "99", "66", "33"][counter]; + } + + createDragImage(selection) { + var img = this.anchor_elem.querySelector(".fb-dragimage"); + var ctx = img.getContext("2d"); + ctx.font = "16px Arial"; + ctx.clearRect(0, 0, 300, 150); + var counter = 0; + for (var item of selection) { + ctx.fillStyle = this.dragColor(counter); + ctx.fillText(item.innerText, 20 + counter * 4, 32 + counter * 6); + counter += 1; + if (counter > 4) { + break; + } + }; + if (selection.length > 1) { + const textwidth = ctx.measureText(selection.length); + ctx.fillStyle = "#ff0000"; + ctx.beginPath(); + ctx.roundRect(0, 0, textwidth.width + 8, 20, [10]); + ctx.fill(); + ctx.fillStyle = "#ffffff"; + ctx.fillText(selection.length, 4, 16); + } + return img; + } + + drop(ev) { + this.log(ev); + ev.preventDefault(); + ev.target.classList.remove("fb-droptarget"); + if (this.dragged_path == ev.dataTransfer.getData("text/plain")) { + this.handleInternalDrop(ev); + return; + } + if (this.hasExternalFiles(ev)) { + this.handleExternalDrop(this.stringDecode(ev.target.dataset.fullpath), ev.dataTransfer.items, null); + return; + } + this.log("internal path inconsistency"); + this.dragged_path = undefined; + } + + dragStart(ev) { + /* pointer capture on the splitter element doesn't seem to + prevent drag&drop being initiated on the tree view, so + check here if the splitter is being dragged and cancel + drag&drop if so. */ + if (this.splitter_active) { + ev.preventDefault(); + return; + } + /* sometimes the browser will initiate a drag on filelist, + even though its draggable attribute is not set. Check + specifically if the thing being dragged is actually + draggable. + */ + if (!ev.target.draggable) { + ev.preventDefault(); + return; + } + ev.dataTransfer.effectAllowed = "move"; + ev.dataTransfer.dropEffect = "move"; + + const leftSideDrag = ev.target.classList.contains("fb-treedirentry"); + + if (leftSideDrag) { + /* dragging from the left side tree view, so deselect everything on the right side */ + this.unselectAll(); + } else if (!this.isSelected(ev.target)) { + /* the item being dragged was not selected, so deselect everything else and then select it */ + this.unselectAll(); + this.selectItem(ev.target); + } + if (ev.target.classList.contains("fb-fileentry") || ev.target.classList.contains("fb-direntry")) { + ev.dataTransfer.setDragImage(this.createDragImage(this.selection()), 22, 18); + } + const num = this.numSelected(); + if (num > 1) { + this.log("multi-drag"); + } else if (num == 1) { + this.log("single-drag"); + } else if (leftSideDrag) { + this.log("single left side drag"); + } else { + this.log("no-drag"); + } + + /* DragEvent.dataTransfer's data is only available in ondrop, but will be + empty in ondragover/enter/leave. Since the source path is needed during + drag to determine whether something can actually be dropped, store it + elsewhere */ + this.dragged_path = this.stringDecode(ev.target.dataset.fullpath); + ev.dataTransfer.setData("text/plain", this.dragged_path); + if (ev.target.classList.contains("fb-treedirentry")) { + ev.dataTransfer.setData("DownloadURL", this.downloadURLForTreeItem(ev.target)); + } else { + ev.dataTransfer.setData("DownloadURL", this.downloadURLForSelection()); + } + } + + dragEnd(ev) { + this.dragged_path = undefined; + } + + downloadURLForTreeItem(item) { + const downloadName = item.innerText + ".zip"; + const fullpath = this.stringDecode(item.dataset.fullpath); + const idx = fullpath.lastIndexOf("/") + 1; + const root = encodeURIComponent(`${this.root_path}${idx?"/":""}${fullpath.substr(0,idx)}`); + const relpath = encodeURIComponent(fullpath.substr(idx)); + this.log(`:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${root}&${relpath}`); + return `:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${root}&${relpath}`; + } + + downloadURLForSelection() { + var filesOnly = true; + var downloadName = "TeslaUSB-download"; + const selection = this.selection(); + selection.forEach((e) => { if (e.classList.contains("fb-direntry")) filesOnly = false;}); + + if (this.numSelected() == 1) { + const selected = selection[0]; + downloadName = selected.innerText; + if (filesOnly) { + const fullpath = encodeURIComponent(this.stringDecode(selected.dataset.fullpath)); + const root = encodeURIComponent(this.root_path); + return `:${downloadName}:${document.location.href}cgi-bin/download.sh?${root}&${fullpath}`; + } + } + + // the user is dragging multiple entries, or a single directory-entry. + downloadName += ".zip"; + var pathsList = encodeURIComponent(`${this.root_path}`); + if (this.current_path != ".") { + pathsList += encodeURIComponent(`/${this.current_path}`); + } + selection.forEach((e) => { + const fullpath = this.stringDecode(e.dataset.fullpath) + const relpath = this.current_path != "." ? fullpath.substr(this.current_path.length + 1) : fullpath; + pathsList += "&"; + pathsList += encodeURIComponent(relpath); + }); + return `:${downloadName}:${document.location.href}cgi-bin/downloadzip.sh?${pathsList}`; + } +} + +// Export to window for dynamic loading +window.FileBrowser = FileBrowser; diff --git a/teslausb-www-react/public/fonts/lato-bold.woff2 b/teslausb-www-react/public/fonts/lato-bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..11de83feaf0fb872c91fce89534fd4e2d8bbda28 GIT binary patch literal 23040 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 literal 0 HcmV?d00001 diff --git a/teslausb-www-react/public/fonts/lato-italic.woff2 b/teslausb-www-react/public/fonts/lato-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..851630ff85699de633f37170f29cbadce3d322ab GIT binary patch literal 24408 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 QOFh<%JiLFgmWQ+r07g396aWAK literal 0 HcmV?d00001 diff --git a/teslausb-www-react/public/icons/android-chrome-192x192.png b/teslausb-www-react/public/icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..6af01ed243af1f1ad5075167bf225650a7aa10cd GIT binary patch literal 8624 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 LyLQ~$&N}rUojuXs literal 0 HcmV?d00001 diff --git a/teslausb-www-react/public/icons/apple-touch-icon.png b/teslausb-www-react/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2eb8cd9ae34d91c26a91aa82a4ce28cc65e70baf GIT binary patch literal 4647 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 + + + + + #00aba9 + + + diff --git a/teslausb-www-react/public/icons/download.svg b/teslausb-www-react/public/icons/download.svg new file mode 100644 index 00000000..d15e4615 --- /dev/null +++ b/teslausb-www-react/public/icons/download.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/teslausb-www-react/public/icons/favicon-16x16.png b/teslausb-www-react/public/icons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..c3e4773827050232712c9762968c9544df8aed63 GIT binary patch literal 875 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 + + + + + + + + diff --git a/teslausb-www-react/public/icons/locksound.svg b/teslausb-www-react/public/icons/locksound.svg new file mode 100644 index 00000000..5361095c --- /dev/null +++ b/teslausb-www-react/public/icons/locksound.svg @@ -0,0 +1,4 @@ + + + + diff --git a/teslausb-www-react/public/icons/mstile-144x144.png b/teslausb-www-react/public/icons/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..355ae85c39fa7705d6ba31e229793dd1f61b0d7e GIT binary patch literal 3221 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 DGZDy% literal 0 HcmV?d00001 diff --git a/teslausb-www-react/public/icons/mstile-150x150.png b/teslausb-www-react/public/icons/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c144a8c1f6fd60aab99c4dc40933bbe9920e40 GIT binary patch literal 3200 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 literal 0 HcmV?d00001 diff --git a/teslausb-www-react/public/icons/mstile-70x70.png b/teslausb-www-react/public/icons/mstile-70x70.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ee01195e88581f50e2e94ae6bbb295fe775c0f GIT binary patch literal 2569 zcmds1c{r478-K?zqbym*he3%xOSTch-w(m&-1&U_rCAn@BZD-dp*w|PwHta6TZF3 zy#N64nVA~dfSL5=MS?=Kz3cP=1rO9sodr)0VczpyB@p8vwwyjqUoc@vmJ_*=_sEuuXTz9rKR( zmGXTGPJxDPY&-wMzLCFg_-6MzBG}rEZRd{o6$PE!%J#@kcgO#gzMF2>-;Dp`KHr@E zdy8LghRxQYXSRap+#=doodHCx1L6xRvLr7(-5L@~Tl4-5O@Z`Si4MmL(E#|~6 z*oF;%$_tO;^%ZMN)+s*daUvf-qa2q~S#K;7!lh_vs^~u?o%-`|Q(Ezld?8P>XroQt zwVFjCN+E60l_kc`5rth*DS2zrzLTTvo7Co|%=$L3M0iO>MiX%%)2ZPNkgdE zxmcT1o$3&jmMtyvameZV`b9&0NA`P8g?+0gfQXwt_t`4igu8BzM4|av;}W6QI?NLl z=bV!J$?b(s?16qeV#N|nvQYJ{o7z0~SmAOtWd$&g*1hf=Ca@$E5c%soshP^VK!&p%d9EQ^Dnss#bj^ldRkY#(l+b-b@i$j1XF& z*8ElzvZVSxKQ2%KU29d)ESA{s4DUIRp)D3XaNg*Vt`I)V z^1}weWjaU}e>o@ZI>iB2dsZsuROd-0x|1Cwp|HGku|8ngOTb42gG5di;ma#~1Jyo> ze+wt++?O z%*_R$MJV7}j;_cfvIq{`BwyKZCPbz;M&KSiM&J7&CWS|s5qP|6GDFrr%|tBnBE-~W zkG!tl!_Vq%3&P@7!J@kAi4R0YNk%m+GP->%ooAfkO<*S-T?Kc3sn{o9(r0}QEX%r+ z)RS@Gu}ZhpKQmpQ6oKU-chOO{C6aoxapcprdcDXOezB~$(JFgo^D91Zousr&beXH^ zsZN^~;y0g1zD?zw|Bw*cUL)q|de3dsIOAwTjBIDLEp@8#mJ~22JXUHPNQoI0R-dB` zBC_o$ukBU_1uTaGjdQ!lKW1ap5m3MlJnS@*ytZ;a(bB*<5*AY5`yr`#YiRP_1 zw9+h0(dARxif`x>+G2mWNO{w2HC9K!y*@aM?d{oAp1Y!1Az1NIzz4o)5=_<7u%34x z*U%ZQ1Y!v;hKyTh*36ULNS0i>KWN7s^hG8gZ~L7PIvkwa$;hFq6E`uYE|)WG!{E8uy2GhCN{-YK(z4)l@1EN;Nu^FmnMb>;idh9h+Y5?mj8Z{m;0iff<0@rKI&Y%AS4`05$0izZ2fcalsC%~gSEQ9 + + + diff --git a/teslausb-www-react/public/icons/pencil.svg b/teslausb-www-react/public/icons/pencil.svg new file mode 100644 index 00000000..f69749db --- /dev/null +++ b/teslausb-www-react/public/icons/pencil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/teslausb-www-react/public/icons/safari-pinned-tab.svg b/teslausb-www-react/public/icons/safari-pinned-tab.svg new file mode 100644 index 00000000..6ee3d0c2 --- /dev/null +++ b/teslausb-www-react/public/icons/safari-pinned-tab.svg @@ -0,0 +1,38 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + diff --git a/teslausb-www-react/public/icons/site.webmanifest b/teslausb-www-react/public/icons/site.webmanifest new file mode 100644 index 00000000..f4fa12e6 --- /dev/null +++ b/teslausb-www-react/public/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "TeslaUSB", + "short_name": "TeslaUSB", + "icons": [ + { + "src": "/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/teslausb-www-react/public/icons/trash.svg b/teslausb-www-react/public/icons/trash.svg new file mode 100644 index 00000000..7b26e4d3 --- /dev/null +++ b/teslausb-www-react/public/icons/trash.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/teslausb-www-react/public/icons/upload.svg b/teslausb-www-react/public/icons/upload.svg new file mode 100644 index 00000000..b8c22615 --- /dev/null +++ b/teslausb-www-react/public/icons/upload.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/teslausb-www-react/public/manifest.json b/teslausb-www-react/public/manifest.json new file mode 100644 index 00000000..bffe30e1 --- /dev/null +++ b/teslausb-www-react/public/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "TeslaUSB", + "short_name": "TeslaUSB", + "description": "TeslaUSB Dashboard - Manage your Tesla dashcam and music storage", + "start_url": "/", + "display": "standalone", + "background_color": "#1f2937", + "theme_color": "#1f2937", + "orientation": "any", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/teslausb-www-react/src/App.jsx b/teslausb-www-react/src/App.jsx new file mode 100644 index 00000000..c06d3357 --- /dev/null +++ b/teslausb-www-react/src/App.jsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from 'preact/hooks'; +import { useStatus } from './hooks/useStatus'; +import { Header } from './components/Header'; +import { Sidebar } from './components/Sidebar'; +import { Dashboard } from './components/Dashboard'; +import { VideoViewer } from './components/VideoViewer'; +import { FileBrowser } from './components/FileBrowser'; +import { LogViewer } from './components/LogViewer'; +import { LoadingScreen } from './components/LoadingScreen'; +import { CarIcon } from './components/Icons'; + +// Tab definitions +const TABS = { + DASHBOARD: 'dashboard', + VIEWER: 'viewer', + FILES: 'files', + LOGS: 'logs', +}; + +export function App() { + const [activeTab, setActiveTab] = useState(TABS.DASHBOARD); + const [sidebarExpanded, setSidebarExpanded] = useState(false); + const { status, config, storage, computed, loading, error, lastUpdate, refresh } = useStatus(5000); + + // Note: Dashboard is always the default tab, no auto-switching + + // Determine which tabs are available + const availableTabs = []; + availableTabs.push({ id: TABS.DASHBOARD, label: 'Dashboard' }); + + if (config?.has_cam === 'yes') { + availableTabs.push({ id: TABS.VIEWER, label: 'Viewer' }); + } + + if (config?.has_music === 'yes' || config?.has_lightshow === 'yes' || config?.has_boombox === 'yes') { + availableTabs.push({ id: TABS.FILES, label: 'Files' }); + } + + availableTabs.push({ id: TABS.LOGS, label: 'Logs' }); + + if (loading && !status) { + return ; + } + + if (error && !status) { + return ( +
          + Failed to load: {error} + +
          + ); + } + + return ( +
          + {/* Top bar */} +
          +
          + + TeslaUSB +
          +
          + + {computed?.drivesActive ? 'Connected' : 'Disconnected'} + +
          +
          + + {/* Header with tabs */} +
          + + {/* Mobile quick status bar */} +
          +
          +
          + USB +
          +
          +
          + WiFi +
          +
          +
          + {computed?.cpuTempC}ยฐC +
          +
          + + {/* Main content */} +
          + {/* Sidebar - only on dashboard */} + {activeTab === TABS.DASHBOARD && ( + setSidebarExpanded(!sidebarExpanded)} + onRefresh={refresh} + /> + )} + + {/* Tab content */} +
          + {activeTab === TABS.DASHBOARD && ( + + )} + + {activeTab === TABS.VIEWER && config?.has_cam === 'yes' && ( + + )} + + {activeTab === TABS.FILES && ( + + )} + + {activeTab === TABS.LOGS && ( + + )} +
          +
          +
          + ); +} + +export default App; diff --git a/teslausb-www-react/src/components/Dashboard.jsx b/teslausb-www-react/src/components/Dashboard.jsx new file mode 100644 index 00000000..d219cdbb --- /dev/null +++ b/teslausb-www-react/src/components/Dashboard.jsx @@ -0,0 +1,98 @@ +import { useState, useCallback, useMemo } from 'preact/hooks'; +import { useLogTail, parseSyncStatus } from '../hooks/useLogTail'; +import { useMusicSyncProgress } from '../hooks/useMusicSyncProgress'; +import { triggerSync } from '../services/api'; +import { + CameraIcon, + MusicIcon, + HardDriveIcon, + BluetoothIcon, +} from './Icons'; +import { StorageBar } from './StorageBar'; +import { SyncStatus } from './SyncStatus'; + +export function Dashboard({ status, computed, config, storage, onRefresh }) { + const [syncLoading, setSyncLoading] = useState(false); + + // Tail archiveloop log for sync status + const { lines: logLines } = useLogTail('archiveloop.log', 3000, true); + const syncStatus = parseSyncStatus(logLines); + + // Check if music sync is active (for enabling progress polling) + const isMusicSyncActive = useMemo(() => { + return syncStatus.state === 'archiving' && + syncStatus.message?.toLowerCase().includes('music'); + }, [syncStatus.state, syncStatus.message]); + + // Poll music sync progress when music sync is active + const musicProgress = useMusicSyncProgress(isMusicSyncActive, 1500); + + const handleTriggerSync = useCallback(async () => { + setSyncLoading(true); + try { + await triggerSync(); + setTimeout(onRefresh, 1000); + } catch (e) { + console.error('Trigger sync failed:', e); + } finally { + setSyncLoading(false); + } + }, [onRefresh]); + + // Feature status items for compact display + const features = [ + { key: 'cam', label: 'TeslaCam', icon: CameraIcon, enabled: config?.has_cam === 'yes' }, + { key: 'music', label: 'Music', icon: MusicIcon, enabled: config?.has_music === 'yes' }, + { key: 'lightshow', label: 'LightShow', icon: HardDriveIcon, enabled: config?.has_lightshow === 'yes' }, + { key: 'boombox', label: 'Boombox', icon: HardDriveIcon, enabled: config?.has_boombox === 'yes' }, + ]; + + // Only show BLE if configured + if (config?.uses_ble === 'yes') { + features.push({ key: 'ble', label: 'BLE', icon: BluetoothIcon, enabled: true }); + } + + return ( +
          + {/* Configured Features */} +
          +
          Configured Features
          +
          + {features.map(({ key, label, enabled }) => ( +
          + {label} +
          + ))} +
          +
          + + {/* Storage Visualization */} +
          +
          +
          + Storage + {computed?.diskTotalGB || '0'} GB +
          + +
          +
          + + {/* Sync Status */} +
          + +
          +
          + ); +} + +export default Dashboard; diff --git a/teslausb-www-react/src/components/FileBrowser.jsx b/teslausb-www-react/src/components/FileBrowser.jsx new file mode 100644 index 00000000..c9ded0c9 --- /dev/null +++ b/teslausb-www-react/src/components/FileBrowser.jsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; + +/** + * FileBrowser component - wraps the native filebrowser.js + * Dynamically loads the script and CSS, then initializes the FileBrowser class + */ +export function FileBrowser({ config }) { + const containerRef = useRef(null); + const browserRef = useRef(null); + const [scriptLoaded, setScriptLoaded] = useState(!!window.FileBrowser); + + // Load CSS on mount + useEffect(() => { + if (!document.querySelector('link[href="/filebrowser.css"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = '/filebrowser.css'; + document.head.appendChild(link); + } + }, []); + + // Load script on mount + useEffect(() => { + if (window.FileBrowser) { + setScriptLoaded(true); + return; + } + + const script = document.createElement('script'); + script.src = '/filebrowser.js'; + script.onload = () => setScriptLoaded(true); + script.onerror = () => console.error('Failed to load filebrowser.js'); + document.head.appendChild(script); + }, []); + + // Extract config values to use as stable dependencies + const hasMusic = config?.has_music === 'yes'; + const hasLightshow = config?.has_lightshow === 'yes'; + const hasBoombox = config?.has_boombox === 'yes'; + + // Initialize FileBrowser when script is loaded and config is ready + useEffect(() => { + if (!scriptLoaded || !containerRef.current) { + return; + } + + // Build drives array based on config + const drives = []; + if (hasMusic) { + drives.push({ path: 'fs/Music', label: 'Music' }); + } + if (hasLightshow) { + drives.push({ path: 'fs/LightShow', label: 'LightShow' }); + } + if (hasBoombox) { + drives.push({ path: 'fs/Boombox', label: 'Boombox' }); + } + + if (drives.length === 0) { + return; + } + + // Only initialize once - don't recreate if already exists + if (browserRef.current) { + return; + } + + // Create new FileBrowser instance + try { + browserRef.current = new window.FileBrowser(containerRef.current, drives); + } catch (e) { + console.error('Failed to initialize FileBrowser:', e); + } + + // Cleanup on unmount + return () => { + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + browserRef.current = null; + }; + }, [scriptLoaded, hasMusic, hasLightshow, hasBoombox]); + + // Check if any drives are configured + const hasDrives = config?.has_music === 'yes' || + config?.has_lightshow === 'yes' || + config?.has_boombox === 'yes'; + + if (!hasDrives) { + return ( +
          + + + + No file drives configured +
          + ); + } + + // Show loading state while script loads + if (!scriptLoaded) { + return ( +
          + Loading file browser... +
          + ); + } + + return ( +
          + ); +} + +export default FileBrowser; diff --git a/teslausb-www-react/src/components/Header.jsx b/teslausb-www-react/src/components/Header.jsx new file mode 100644 index 00000000..9b8bd17b --- /dev/null +++ b/teslausb-www-react/src/components/Header.jsx @@ -0,0 +1,49 @@ +import { DashboardIcon, VideoIcon, FolderIcon, TerminalIcon, RefreshIcon } from './Icons'; + +const TAB_ICONS = { + dashboard: DashboardIcon, + viewer: VideoIcon, + files: FolderIcon, + logs: TerminalIcon, +}; + +export function Header({ tabs, activeTab, onTabChange, lastUpdate, onRefresh }) { + const formatTime = (date) => { + if (!date) return ''; + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + return ( +
          +
          + +
          + +
          + + {lastUpdate && `Updated ${formatTime(lastUpdate)}`} + + +
          +
          + ); +} + +export default Header; diff --git a/teslausb-www-react/src/components/Icons.jsx b/teslausb-www-react/src/components/Icons.jsx new file mode 100644 index 00000000..bc7a4d05 --- /dev/null +++ b/teslausb-www-react/src/components/Icons.jsx @@ -0,0 +1,373 @@ +/** + * SVG Icon components for TeslaUSB UI + * Using inline SVGs for minimal bundle size + */ + +export function CarIcon({ className }) { + return ( + + + + + + ); +} + +export function DashboardIcon({ className }) { + return ( + + + + + + + ); +} + +export function VideoIcon({ className }) { + return ( + + + + + ); +} + +export function FolderIcon({ className }) { + return ( + + + + ); +} + +export function FileIcon({ className }) { + return ( + + + + + ); +} + +export function SettingsIcon({ className }) { + return ( + + + + + ); +} + +export function RefreshIcon({ className }) { + return ( + + + + + ); +} + +export function PlayIcon({ className }) { + return ( + + + + ); +} + +export function PauseIcon({ className }) { + return ( + + + + + ); +} + +export function SkipBackIcon({ className }) { + return ( + + + + + ); +} + +export function SkipForwardIcon({ className }) { + return ( + + + + + ); +} + +export function UploadIcon({ className }) { + return ( + + + + + + ); +} + +export function DownloadIcon({ className }) { + return ( + + + + + + ); +} + +export function TrashIcon({ className }) { + return ( + + + + + ); +} + +export function PlusIcon({ className }) { + return ( + + + + + ); +} + +export function XIcon({ className }) { + return ( + + + + + ); +} + +export function CheckIcon({ className }) { + return ( + + + + ); +} + +export function AlertIcon({ className }) { + return ( + + + + + + ); +} + +export function WifiIcon({ className }) { + return ( + + + + + + + ); +} + +export function HardDriveIcon({ className }) { + return ( + + + + + + + ); +} + +export function CpuIcon({ className }) { + return ( + + + + + + + + + + + + + ); +} + +export function UsbIcon({ className }) { + return ( + + + + + + + + + + ); +} + +export function SyncIcon({ className }) { + return ( + + + + + + + ); +} + +export function PowerIcon({ className }) { + return ( + + + + + ); +} + +export function BluetoothIcon({ className }) { + return ( + + + + ); +} + +export function ClockIcon({ className }) { + return ( + + + + + ); +} + +export function TerminalIcon({ className }) { + return ( + + + + + ); +} + +export function InfoIcon({ className }) { + return ( + + + + + + ); +} + +export function ChevronDownIcon({ className }) { + return ( + + + + ); +} + +export function ChevronRightIcon({ className }) { + return ( + + + + ); +} + +export function MenuIcon({ className }) { + return ( + + + + + + ); +} + +export function MapPinIcon({ className }) { + return ( + + + + + ); +} + +export function MusicIcon({ className }) { + return ( + + + + + + ); +} + +export function CameraIcon({ className }) { + return ( + + + + + ); +} + +export function SpeedIcon({ className }) { + return ( + + + + + ); +} + +export function GridIcon({ className }) { + return ( + + + + + + + ); +} + +export function LayoutIcon({ className }) { + return ( + + + + + + ); +} + +export function SwitchIcon({ className }) { + return ( + + + + + + + ); +} diff --git a/teslausb-www-react/src/components/LoadingScreen.jsx b/teslausb-www-react/src/components/LoadingScreen.jsx new file mode 100644 index 00000000..7a620c42 --- /dev/null +++ b/teslausb-www-react/src/components/LoadingScreen.jsx @@ -0,0 +1,10 @@ +export function LoadingScreen() { + return ( +
          +
          + Loading... +
          + ); +} + +export default LoadingScreen; diff --git a/teslausb-www-react/src/components/LogViewer.jsx b/teslausb-www-react/src/components/LogViewer.jsx new file mode 100644 index 00000000..85d4166a --- /dev/null +++ b/teslausb-www-react/src/components/LogViewer.jsx @@ -0,0 +1,227 @@ +import { useState, useEffect, useRef, useCallback } from 'preact/hooks'; +import { useLogTail } from '../hooks/useLogTail'; +import { generateDiagnostics, fetchDiagnostics } from '../services/api'; +import { + TerminalIcon, + RefreshIcon, + DownloadIcon, + TrashIcon, + InfoIcon, +} from './Icons'; + +// Log types +const LOG_TYPES = { + archiveloop: { + label: 'Archive Log', + file: 'archiveloop.log', + description: 'Sync and archive operations', + }, + setup: { + label: 'Setup Log', + file: 'teslausb-headless-setup.log', + description: 'Initial setup and configuration', + }, + diagnostics: { + label: 'Diagnostics', + file: 'diagnostics.txt', + description: 'System diagnostics report', + }, +}; + +export function LogViewer() { + const [activeLog, setActiveLog] = useState('archiveloop'); + const [diagnosticsLoading, setDiagnosticsLoading] = useState(false); + const [diagnosticsContent, setDiagnosticsContent] = useState(null); + const logContainerRef = useRef(null); + + // Use log tailing hook for archive and setup logs + const isRegularLog = activeLog !== 'diagnostics'; + const { + lines, + loading, + error, + refresh, + clear, + } = useLogTail( + isRegularLog ? LOG_TYPES[activeLog].file : '', + 2000, + isRegularLog + ); + + // Auto-scroll to bottom + useEffect(() => { + if (logContainerRef.current && isRegularLog) { + logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; + } + }, [lines, isRegularLog]); + + // Generate and load diagnostics + const handleGenerateDiagnostics = useCallback(async () => { + setDiagnosticsLoading(true); + try { + await generateDiagnostics(); + // Wait a moment for file to be generated + await new Promise(r => setTimeout(r, 2000)); + const content = await fetchDiagnostics(); + setDiagnosticsContent(content); + } catch (e) { + setDiagnosticsContent(`Error generating diagnostics: ${e.message}`); + } finally { + setDiagnosticsLoading(false); + } + }, []); + + // Load diagnostics when tab is selected + useEffect(() => { + if (activeLog === 'diagnostics' && !diagnosticsContent) { + handleGenerateDiagnostics(); + } + }, [activeLog, diagnosticsContent, handleGenerateDiagnostics]); + + // Download log file + const handleDownload = useCallback(() => { + const content = activeLog === 'diagnostics' ? diagnosticsContent : lines.join('\n'); + const filename = LOG_TYPES[activeLog].file; + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, [activeLog, lines, diagnosticsContent]); + + // Get line class for syntax highlighting + const getLineClass = (line) => { + const lower = line.toLowerCase(); + if (lower.includes('error') || lower.includes('failed') || lower.includes('fatal')) { + return 'error'; + } + if (lower.includes('warning') || lower.includes('warn')) { + return 'warning'; + } + if (lower.includes('success') || lower.includes('completed') || lower.includes('finished')) { + return 'success'; + } + return ''; + }; + + const currentLogInfo = LOG_TYPES[activeLog]; + const displayLines = activeLog === 'diagnostics' + ? (diagnosticsContent?.split('\n') || []) + : lines; + + return ( +
          + {/* Log type selector */} +
          + {Object.entries(LOG_TYPES).map(([key, log]) => ( + + ))} +
          + + {/* Log viewer */} +
          +
          +
          + {currentLogInfo.label} + + โ€” {currentLogInfo.description} + +
          +
          + {activeLog === 'diagnostics' ? ( + + ) : ( + <> + + + + )} + +
          +
          + +
          + {(loading && displayLines.length === 0) || diagnosticsLoading ? ( +
          Loading...
          + ) : error ? ( + error.includes('not found') ? ( +
          + Log file not available. {activeLog === 'setup' && 'The setup log is only present during initial setup.'} +
          + ) : ( +
          Error: {error}
          + ) + ) : displayLines.length === 0 ? ( +
          No log entries
          + ) : ( + displayLines.map((line, idx) => ( +
          + {line} +
          + )) + )} +
          +
          + + {/* Log stats */} + {isRegularLog && lines.length > 0 && ( +
          + {lines.length} lines + Auto-refreshing every 2s +
          + )} +
          + ); +} + +export default LogViewer; diff --git a/teslausb-www-react/src/components/Sidebar.jsx b/teslausb-www-react/src/components/Sidebar.jsx new file mode 100644 index 00000000..09a89a44 --- /dev/null +++ b/teslausb-www-react/src/components/Sidebar.jsx @@ -0,0 +1,310 @@ +import { useState } from 'preact/hooks'; +import { + CpuIcon, + WifiIcon, + UsbIcon, + CameraIcon, + ChevronDownIcon, + PowerIcon, + SpeedIcon, + BluetoothIcon, + SwitchIcon, +} from './Icons'; +import { toggleDrives, reboot, runSpeedTest, startBLEPairing, checkBLEStatus } from '../services/api'; + +export function Sidebar({ status, computed, config, expanded, onToggle, onRefresh }) { + const [usbLoading, setUsbLoading] = useState(false); + const [rebootLoading, setRebootLoading] = useState(false); + const [speedTestRunning, setSpeedTestRunning] = useState(false); + const [speedTestResult, setSpeedTestResult] = useState(null); + const [bleStatus, setBleStatus] = useState(null); + + const handleToggleDrives = async () => { + setUsbLoading(true); + try { + await toggleDrives(); + setTimeout(onRefresh, 1000); + } catch (e) { + console.error('Toggle drives failed:', e); + } finally { + setUsbLoading(false); + } + }; + + const handleReboot = async () => { + if (confirm('Are you sure you want to restart TeslaUSB?')) { + setRebootLoading(true); + try { + await reboot(); + } catch (e) { + console.error('Reboot failed:', e); + } + } + }; + + const handleSpeedTest = async () => { + setSpeedTestRunning(true); + setSpeedTestResult(null); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + try { + await runSpeedTest( + (speed) => setSpeedTestResult(speed.toFixed(1)), + controller.signal + ); + } catch (e) { + if (e.name !== 'AbortError') { + console.error('Speed test failed:', e); + } + } finally { + clearTimeout(timeout); + setSpeedTestRunning(false); + } + }; + + const handleBLEPairing = async () => { + setBleStatus('pairing'); + try { + const started = await startBLEPairing(); + if (started) { + for (let i = 0; i < 60; i++) { + await new Promise(r => setTimeout(r, 2000)); + const paired = await checkBLEStatus(); + if (paired) { + setBleStatus('paired'); + return; + } + } + setBleStatus('timeout'); + } else { + setBleStatus('error'); + } + } catch (e) { + console.error('BLE pairing failed:', e); + setBleStatus('error'); + } + }; + + return ( + + ); +} + +function getTempClass(temp) { + if (!temp) return ''; + const t = parseFloat(temp); + if (t >= 80) return 'status-unhealthy'; + if (t >= 70) return 'status-degraded'; + return 'status-healthy'; +} + +function getStorageClass(percent) { + if (!percent) return 'blue'; + if (percent >= 90) return 'red'; + if (percent >= 75) return 'yellow'; + return 'blue'; +} + +function formatSnapshotDate(timestamp) { + if (!timestamp) return '-'; + try { + const date = new Date(parseInt(timestamp, 10) * 1000); + return date.toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + } catch { + return timestamp; + } +} + +function formatWifiFreq(freq) { + if (!freq) return '-'; + // iwgetid returns frequency like "2.412 GHz" or just the number + const match = freq.match(/(\d+\.?\d*)/); + if (match) { + const ghz = parseFloat(match[1]); + const rawFreq = ({ghz.toFixed(3)}); + // Show band label (2.4 or 5 GHz) with actual frequency + if (ghz >= 2.4 && ghz < 2.5) { + return <>2.4 GHz{rawFreq}; + } else if (ghz >= 5 && ghz < 6) { + return <>5 GHz{rawFreq}; + } else if (ghz >= 6) { + return <>6 GHz{rawFreq}; + } + return `${ghz} GHz`; + } + return freq; +} + +export default Sidebar; diff --git a/teslausb-www-react/src/components/StorageBar.jsx b/teslausb-www-react/src/components/StorageBar.jsx new file mode 100644 index 00000000..dbf95b56 --- /dev/null +++ b/teslausb-www-react/src/components/StorageBar.jsx @@ -0,0 +1,123 @@ +/** + * Storage visualization component + * Shows per-drive allocation/usage and available free space + */ +export function StorageBar({ storage, total, free, config }) { + // Format bytes to human readable + const formatBytes = (bytes) => { + if (bytes === 0 || bytes === null || bytes === undefined) return '0 B'; + const gb = bytes / (1024 * 1024 * 1024); + if (gb >= 1) return `${gb.toFixed(1)} GB`; + const mb = bytes / (1024 * 1024); + return `${mb.toFixed(0)} MB`; + }; + + const freeBytes = storage?.total?.free || free; + + // Build list of configured drives + const drives = []; + + const addDrive = (driveData, type, label, configKey) => { + if (config?.[configKey] !== 'yes' || !driveData) return; + + drives.push({ + type, + label, + allocated: driveData.total, + used: driveData.used, + mounted: driveData.mounted, + isCached: driveData.cached || false, + }); + }; + + addDrive(storage?.cam, 'teslacam', 'TeslaCam', 'has_cam'); + addDrive(storage?.music, 'music', 'Music', 'has_music'); + addDrive(storage?.lightshow, 'lightshow', 'LightShow', 'has_lightshow'); + addDrive(storage?.boombox, 'boombox', 'Boombox', 'has_boombox'); + + if (drives.length === 0 && !freeBytes) { + return ( +
          +
          No storage data available
          +
          + ); + } + + // Calculate total for bar (sum of all allocations + free) + const totalAllocated = drives.reduce((sum, d) => sum + (d.allocated || 0), 0); + const barTotal = totalAllocated + freeBytes; + + // Build segments for bar + const segments = drives.map(drive => ({ + type: drive.type, + label: drive.label, + bytes: drive.allocated, + percent: Math.max(1, Math.round((drive.allocated / barTotal) * 100)), + })); + + // Add free space segment + if (freeBytes > 0) { + segments.push({ + type: 'free', + label: 'Free', + bytes: freeBytes, + percent: Math.max(1, Math.round((freeBytes / barTotal) * 100)), + }); + } + + return ( +
          + {/* Visual bar */} +
          + {segments.map((seg, idx) => ( +
          + ))} +
          + + {/* Legend with details */} +
          + {drives.map((drive, idx) => { + // Calculate usage percentage within the drive (not allocation percentage) + const usagePercent = drive.used !== null && drive.allocated > 0 + ? Math.round((drive.used / drive.allocated) * 100) + : null; + return ( +
          +
          + {drive.label} + + {formatBytes(drive.allocated)} + {drive.used !== null && ( + + {' '}({formatBytes(drive.used)} used{drive.isCached ? '~' : ''} + {usagePercent !== null && `, ${usagePercent}%`}) + + )} + +
          + ); + })} +
          +
          + Free + + {formatBytes(freeBytes)} + +
          +
          + + {drives.some(d => d.isCached) && ( +
          + ~ Last known (drive not currently mounted) +
          + )} +
          + ); +} + +export default StorageBar; diff --git a/teslausb-www-react/src/components/SyncStatus.jsx b/teslausb-www-react/src/components/SyncStatus.jsx new file mode 100644 index 00000000..b8d46bd8 --- /dev/null +++ b/teslausb-www-react/src/components/SyncStatus.jsx @@ -0,0 +1,139 @@ +import { SyncIcon, CheckIcon, AlertIcon, ClockIcon } from './Icons'; +import { formatBytes, formatEta } from '../hooks/useMusicSyncProgress'; + +/** + * Compact Sync Status Card + */ +export function SyncStatus({ syncStatus, onTriggerSync, loading, musicProgress }) { + const { state, totalFiles, archivedFiles, message, elapsedTime, lastActivity } = syncStatus; + + // Check if this is an active music sync with progress data + const isMusicSync = message?.toLowerCase().includes('music') && state === 'archiving'; + const hasMusicProgress = musicProgress?.active && musicProgress?.percentage > 0; + + const getStateInfo = () => { + switch (state) { + case 'idle': + return { label: 'Idle', color: 'idle', description: 'Ready to archive' }; + case 'connecting': + return { label: 'Connecting', color: 'connecting', description: message || 'Connecting to server...' }; + case 'archiving': + return { label: 'Archiving', color: 'archiving', description: message || 'Syncing files...' }; + case 'complete': + return { label: 'Complete', color: 'idle', description: message || 'Archive complete' }; + case 'error': + return { label: 'Error', color: 'error', description: message || 'Archive failed' }; + default: + return { label: 'Unknown', color: 'idle', description: 'Status unknown' }; + } + }; + + const stateInfo = getStateInfo(); + const isActive = state === 'archiving' || state === 'connecting'; + + // Determine which progress to show + const showMusicProgress = isMusicSync && hasMusicProgress; + const showFileProgress = state === 'archiving' && totalFiles > 0 && !showMusicProgress; + + // Calculate progress percentage + let progressPercent = null; + if (showMusicProgress) { + progressPercent = musicProgress.percentage; + } else if (showFileProgress && archivedFiles > 0) { + progressPercent = Math.min(Math.round((archivedFiles / totalFiles) * 100), 100); + } + + const formatLastActivity = (date) => { + if (!date) return null; + const now = new Date(); + const diff = Math.floor((now - date) / 1000); + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return date.toLocaleDateString(); + }; + + return ( +
          +
          + Sync Status +
          +
          + {stateInfo.label} +
          +
          + +
          + {stateInfo.description} +
          + + {/* Progress bar - shown during archiving */} + {(showMusicProgress || showFileProgress || isActive) && ( +
          +
          +
          + )} + + {/* Music sync progress details */} + {showMusicProgress && ( +
          +
          + {formatBytes(musicProgress.bytesTransferred)} transferred + {progressPercent !== null && ` (${progressPercent}%)`} +
          + {(musicProgress.speed || musicProgress.eta) && ( +
          + {musicProgress.speed && {musicProgress.speed}} + {musicProgress.speed && musicProgress.eta && ยท } + {musicProgress.eta && {formatEta(musicProgress.eta)}} +
          + )} +
          + )} + + {/* File progress details (for TeslaCam archiving) */} + {showFileProgress && ( +
          + {archivedFiles} / {totalFiles} files + {progressPercent !== null && ` (${progressPercent}%)`} +
          + )} + + {/* Last activity for idle state */} + {!isActive && !showFileProgress && !showMusicProgress && lastActivity && ( +
          + Last sync: {formatLastActivity(lastActivity)} +
          + )} + + {/* Elapsed time for complete state */} + {elapsedTime && state === 'complete' && ( +
          + Completed in {elapsedTime} +
          + )} + + {/* Manual trigger button */} + {(state === 'idle' || state === 'complete' || state === 'error') && ( + + )} +
          + ); +} + +export default SyncStatus; diff --git a/teslausb-www-react/src/components/VideoViewer.jsx b/teslausb-www-react/src/components/VideoViewer.jsx new file mode 100644 index 00000000..2c105b88 --- /dev/null +++ b/teslausb-www-react/src/components/VideoViewer.jsx @@ -0,0 +1,386 @@ +import { useState, useEffect, useRef, useCallback } from 'preact/hooks'; +import { fetchVideoList, fetchEventData, getVideoUrl } from '../services/api'; +import { + PlayIcon, + PauseIcon, + SkipBackIcon, + SkipForwardIcon, + LayoutIcon, + MapPinIcon, + ChevronDownIcon, +} from './Icons'; + +// Camera angles in Tesla vehicles +const CAMERAS = { + front: 'Front', + back: 'Back', + left_repeater: 'Left Repeater', + right_repeater: 'Right Repeater', + left_pillar: 'Left Pillar', + right_pillar: 'Right Pillar', +}; + +// Layout configurations +const LAYOUTS = [ + { id: '6', name: 'All Cameras', cameras: Object.keys(CAMERAS), cols: 3 }, + { id: '4-front', name: 'Front Focus', cameras: ['front', 'left_repeater', 'right_repeater', 'back'], cols: 2 }, + { id: '4-rear', name: 'Rear Focus', cameras: ['back', 'left_repeater', 'right_repeater', 'front'], cols: 2 }, + { id: '2-side', name: 'Side View', cameras: ['left_repeater', 'right_repeater'], cols: 2 }, + { id: '1-front', name: 'Front Only', cameras: ['front'], cols: 1 }, + { id: '1-back', name: 'Rear Only', cameras: ['back'], cols: 1 }, +]; + +export function VideoViewer() { + const [videoList, setVideoList] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Playback state + const [selectedCategory, setSelectedCategory] = useState('SentryClips'); + const [selectedSequence, setSelectedSequence] = useState(null); + const [playing, setPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [layout, setLayout] = useState(LAYOUTS[0]); + const [showLayoutMenu, setShowLayoutMenu] = useState(false); + + // Event data for sentry clips + const [eventData, setEventData] = useState(null); + + // Video refs for synchronized playback + const videoRefs = useRef({}); + + // Load video list + useEffect(() => { + loadVideoList(); + }, []); + + const loadVideoList = async () => { + try { + setLoading(true); + const list = await fetchVideoList(); + setVideoList(list); + + // Select first available sequence + for (const category of ['SentryClips', 'SavedClips', 'RecentClips']) { + const sequences = Object.keys(list[category] || {}); + if (sequences.length > 0) { + setSelectedCategory(category); + setSelectedSequence(sequences[0]); + break; + } + } + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + // Load event data when sequence changes + useEffect(() => { + if (selectedCategory === 'SentryClips' && selectedSequence) { + fetchEventData(selectedSequence).then(setEventData); + } else { + setEventData(null); + } + }, [selectedCategory, selectedSequence]); + + // Get video files for current sequence + const getVideoFiles = useCallback(() => { + if (!videoList || !selectedSequence) return {}; + + const files = videoList[selectedCategory]?.[selectedSequence] || []; + const result = {}; + + for (const file of files) { + // Extract camera from filename: 2025-01-15_12-30-45-front.mp4 + for (const camera of Object.keys(CAMERAS)) { + if (file.includes(`-${camera}.mp4`) || file.includes(`_${camera}.mp4`)) { + result[camera] = getVideoUrl(selectedCategory, selectedSequence, file); + break; + } + } + } + + return result; + }, [videoList, selectedCategory, selectedSequence]); + + const videoFiles = getVideoFiles(); + + // Synchronized playback controls + const playAll = useCallback(() => { + Object.values(videoRefs.current).forEach(video => { + if (video) video.play(); + }); + setPlaying(true); + }, []); + + const pauseAll = useCallback(() => { + Object.values(videoRefs.current).forEach(video => { + if (video) video.pause(); + }); + setPlaying(false); + }, []); + + const seekAll = useCallback((time) => { + Object.values(videoRefs.current).forEach(video => { + if (video) video.currentTime = time; + }); + setCurrentTime(time); + }, []); + + const skipBack = useCallback(() => { + seekAll(Math.max(0, currentTime - 10)); + }, [currentTime, seekAll]); + + const skipForward = useCallback(() => { + seekAll(Math.min(duration, currentTime + 30)); + }, [currentTime, duration, seekAll]); + + // Handle time updates from videos + const handleTimeUpdate = useCallback((e) => { + setCurrentTime(e.target.currentTime); + }, []); + + const handleDurationChange = useCallback((e) => { + if (e.target.duration && e.target.duration !== Infinity) { + setDuration(e.target.duration); + } + }, []); + + // Handle timeline click + const handleTimelineClick = useCallback((e) => { + const rect = e.currentTarget.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + const time = percent * duration; + seekAll(time); + }, [duration, seekAll]); + + // Format time display + const formatTime = (seconds) => { + if (!seconds || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + // Get sequences for category dropdown + const sequences = videoList?.[selectedCategory] ? Object.keys(videoList[selectedCategory]).sort().reverse() : []; + + if (loading) { + return ( +
          + Loading recordings... +
          + ); + } + + if (error) { + return ( +
          + Error: {error} + +
          + ); + } + + if (!selectedSequence || Object.keys(videoFiles).length === 0) { + return ( +
          + No recordings available +
          + ); + } + + return ( +
          + {/* Clip selector bar */} +
          + {/* Category selector */} + + + {/* Sequence selector */} + + + {/* Layout selector */} +
          + + {showLayoutMenu && ( +
          + {LAYOUTS.map(l => ( + + ))} +
          + )} +
          + + {/* Event location info */} + {eventData && eventData.city && ( +
          + + {eventData.city} +
          + )} +
          + + {/* Video grid */} +
          + {layout.cameras.map(camera => ( +
          + {videoFiles[camera] ? ( +
          + ))} +
          + + {/* Video controls */} +
          + + + + + + +
          + {formatTime(currentTime)} / {formatTime(duration)} +
          + +
          +
          0 ? `${(currentTime / duration) * 100}%` : '0%' }} + /> +
          +
          +
          + ); +} + +// Format sequence name (date/time folder) to readable string +function formatSequenceName(sequence) { + // Format: 2025-01-15_12-30-45 or 2025-01-15 + const match = sequence.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/); + if (match) { + const [, year, month, day, hour, min, sec] = match; + const date = new Date(year, month - 1, day, hour || 0, min || 0, sec || 0); + if (hour) { + return date.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } + return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }); + } + return sequence; +} + +export default VideoViewer; diff --git a/teslausb-www-react/src/hooks/useLogTail.js b/teslausb-www-react/src/hooks/useLogTail.js new file mode 100644 index 00000000..ed4d72f0 --- /dev/null +++ b/teslausb-www-react/src/hooks/useLogTail.js @@ -0,0 +1,347 @@ +import { useState, useEffect, useCallback, useRef } from 'preact/hooks'; +import { fetchLog } from '../services/api'; + +/** + * Hook for tailing log files with efficient range requests + * @param {string} logFile - Log file path (e.g., 'archiveloop.log') + * @param {number} pollInterval - Polling interval in ms (default 2000) + * @param {boolean} enabled - Whether to enable polling + * @returns {Object} Log content, sync state, and control functions + */ +export function useLogTail(logFile, pollInterval = 2000, enabled = true) { + const [lines, setLines] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const lastSizeRef = useRef(0); + const autoScrollRef = useRef(true); + const logFileRef = useRef(logFile); + + const refresh = useCallback(async (reset = false) => { + if (!logFile) return; + + try { + if (reset) { + lastSizeRef.current = 0; + } + + const result = await fetchLog(logFile, lastSizeRef.current); + + if (result.truncated) { + // Log was truncated, start fresh + lastSizeRef.current = 0; + setLines([]); + return; + } + + if (result.content) { + const newLines = result.content.split('\n').filter(Boolean); + if (reset) { + setLines(newLines.slice(-1000)); + } else { + setLines(prev => [...prev, ...newLines].slice(-1000)); // Keep last 1000 lines + } + } + + lastSizeRef.current = result.size; + setError(null); + } catch (e) { + // Only set error, don't clear lines on error + setError(e.message); + } finally { + setLoading(false); + } + }, [logFile]); + + // Reset when log file changes + useEffect(() => { + if (logFile !== logFileRef.current) { + logFileRef.current = logFile; + lastSizeRef.current = 0; + setLines([]); + setLoading(true); + setError(null); + } + }, [logFile]); + + useEffect(() => { + if (!enabled || !logFile) return; + + refresh(true); // Initial load + const interval = setInterval(() => refresh(false), pollInterval); + return () => clearInterval(interval); + }, [logFile, pollInterval, enabled]); // Don't depend on refresh to avoid recreation + + const setAutoScroll = useCallback((value) => { + autoScrollRef.current = value; + }, []); + + const clear = useCallback(() => { + setLines([]); + lastSizeRef.current = 0; + }, []); + + return { + lines, + loading, + error, + autoScroll: autoScrollRef.current, + setAutoScroll, + refresh: () => refresh(false), + clear, + }; +} + +/** + * Parse archiveloop.log to extract sync status + * @param {string[]} lines - Log lines + * @returns {Object} Sync status object + */ +export function parseSyncStatus(lines) { + const status = { + state: 'idle', // idle, connecting, archiving, complete, error + totalFiles: 0, + archivedFiles: 0, + currentFile: null, + startTime: null, + elapsedTime: null, + lastActivity: null, + message: null, + }; + + if (!lines || lines.length === 0) { + return status; + } + + // Parse from most recent lines backwards to find current state + const recentLines = lines.slice(-100); + + // Always extract timestamp from most recent line first + if (recentLines.length > 0) { + status.lastActivity = extractTimestamp(recentLines[recentLines.length - 1]); + } + + // First pass: look for file counts and completion messages + for (let i = recentLines.length - 1; i >= 0; i--) { + const line = recentLines[i]; + + // File count before archiving: "There are X event folder(s) with Y file(s) and Z track mode file(s)" + const fileCountMatch = line.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/); + if (fileCountMatch) { + const sentryFiles = parseInt(fileCountMatch[2], 10); + const trackModeFiles = fileCountMatch[3] ? parseInt(fileCountMatch[3], 10) : 0; + status.totalFiles = sentryFiles + trackModeFiles; + break; // Found the count for this archive session + } + + // Archiving message with count: "Archiving X file(s) including Y event folder(s)" + const archivingMatch = line.match(/Archiving (\d+)(?: track mode)? file\(s\)/); + if (archivingMatch && !line.includes('completed')) { + status.totalFiles = parseInt(archivingMatch[1], 10); + } + } + + // Check if music sync is in progress (started but not finished) + // This needs to be detected first because snapshot/idle messages can appear during music sync + let musicSyncActive = false; + for (let i = recentLines.length - 1; i >= 0; i--) { + const line = recentLines[i]; + if (line.includes('Finished copying music') || line.includes('Copying music failed')) { + // Music sync completed, not active + break; + } + if (line.includes('Syncing music from archive') || line.includes('Starting music sync')) { + // Music sync started and not yet finished + musicSyncActive = true; + break; + } + // If we hit "Connected usb to host" or "Waiting for archive to be unreachable", + // we're in a new cycle - no active music sync + if (line.includes('Connected usb to host') || line.includes('Waiting for archive to be unreachable')) { + break; + } + } + + // If music sync is active, show that status + if (musicSyncActive) { + status.state = 'archiving'; + status.message = 'Syncing music...'; + return status; + } + + // Second pass: determine current state + for (let i = recentLines.length - 1; i >= 0; i--) { + const line = recentLines[i]; + + // Archiving completed successfully + if (line.includes('Archiving completed successfully')) { + status.state = 'complete'; + status.message = 'Archive completed'; + break; + } + + // Finished copying music + if (line.includes('Finished copying music')) { + status.state = 'complete'; + status.message = 'Music sync complete'; + break; + } + + // Completion message with stats: "Copied X music file(s)" + if (line.includes('Copied') && line.includes('file(s)')) { + const match = line.match(/Copied (\d+)/); + if (match) { + status.state = 'complete'; + status.archivedFiles = parseInt(match[1], 10); + status.message = `Copied ${status.archivedFiles} files`; + break; + } + } + + // Starting recording archiving + if (line.includes('Starting recording archiving')) { + status.state = 'archiving'; + status.message = status.totalFiles > 0 + ? `Archiving ${status.totalFiles} files...` + : 'Archiving recordings...'; + break; + } + + // Currently archiving (fallback) + if (line.includes('Archiving...')) { + status.state = 'archiving'; + status.message = status.totalFiles > 0 + ? `Archiving ${status.totalFiles} files...` + : 'Archiving files...'; + break; + } + + // Syncing music (fallback if musicSyncActive didn't catch it) + if (line.includes('Syncing music from archive')) { + status.state = 'archiving'; + status.message = 'Syncing music...'; + break; + } + + // Copying music + if (line.includes('Copying music')) { + status.state = 'archiving'; + status.message = 'Copying music...'; + break; + } + + // Finished archiving + if (line.includes('Finished archiving')) { + status.state = 'complete'; + status.message = 'Archive complete'; + break; + } + + // Running fsck + if (line.includes('Running fsck')) { + status.state = 'archiving'; + status.message = 'Checking filesystem...'; + break; + } + + // Checking folder count + if (line.includes('Checking saved folder count')) { + status.state = 'archiving'; + status.message = 'Scanning files...'; + break; + } + + // Waiting for archive to be reachable (connecting) + if (line.includes('Waiting for archive to be reachable')) { + status.state = 'connecting'; + status.message = 'Connecting to archive server...'; + break; + } + + // Archive is reachable + if (line.includes('Archive is reachable')) { + status.state = 'archiving'; + status.message = 'Connected, preparing...'; + break; + } + + // Disconnecting USB (preparing to archive) + if (line.includes('Disconnecting usb from host')) { + status.state = 'archiving'; + status.message = 'Disconnecting from vehicle...'; + break; + } + + // Connected to host (idle) + if (line.includes('Connected usb to host')) { + status.state = 'idle'; + status.message = 'Connected to vehicle'; + break; + } + + // Waiting for archive to be unreachable (idle, connected to car) + if (line.includes('Waiting for archive to be unreachable')) { + status.state = 'idle'; + status.message = 'Connected to vehicle'; + break; + } + + // Taking snapshot or snapshot-related messages + if (line.includes('snapshot')) { + status.state = 'idle'; + status.message = 'Managing snapshots...'; + break; + } + + // Low space cleanup + if (line.includes('low space, deleting')) { + status.state = 'idle'; + status.message = 'Cleaning up old snapshots...'; + break; + } + + // Waiting for idle interval + if (line.includes('waiting up to') && line.includes('idle interval')) { + status.state = 'idle'; + status.message = 'Waiting for idle...'; + break; + } + + // Mass storage process check + if (line.includes('mass storage process')) { + status.state = 'idle'; + status.message = 'Ready'; + break; + } + + // Check for errors + if (line.includes('error') || line.includes('failed')) { + // Only set error if it's a significant error, not just "sntp failed" + if (!line.includes('sntp failed')) { + status.state = 'error'; + status.message = 'Error occurred'; + break; + } + } + } + + return status; +} + +/** + * Extract timestamp from log line + * @param {string} line - Log line + * @returns {Date|null} Parsed date or null + */ +function extractTimestamp(line) { + // Format: "Tue 9 Dec 13:22:02 PST 2025:" or "Wed 27 Aug 20:14:11 PDT 2025:" + const match = line.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/); + if (match) { + const date = new Date(match[1]); + if (!isNaN(date.getTime())) { + return date; + } + } + return null; +} + +export default useLogTail; diff --git a/teslausb-www-react/src/hooks/useMusicSyncProgress.js b/teslausb-www-react/src/hooks/useMusicSyncProgress.js new file mode 100644 index 00000000..9f5737ce --- /dev/null +++ b/teslausb-www-react/src/hooks/useMusicSyncProgress.js @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback, useRef } from 'preact/hooks'; +import { fetchMusicSyncProgress } from '../services/api'; + +/** + * Hook for polling music sync progress + * @param {boolean} enabled - Whether to enable polling (should be true when music sync is active) + * @param {number} pollInterval - Polling interval in ms (default 1500) + * @returns {Object} Music sync progress data + */ +export function useMusicSyncProgress(enabled = false, pollInterval = 1500) { + const [progress, setProgress] = useState({ + active: false, + bytesTransferred: 0, + percentage: 0, + speed: '', + eta: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const enabledRef = useRef(enabled); + + const refresh = useCallback(async () => { + try { + const data = await fetchMusicSyncProgress(); + setProgress(data); + setError(null); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }, []); + + // Update ref when enabled changes + useEffect(() => { + enabledRef.current = enabled; + }, [enabled]); + + useEffect(() => { + if (!enabled) { + // Reset progress when disabled + setProgress({ + active: false, + bytesTransferred: 0, + percentage: 0, + speed: '', + eta: '', + }); + return; + } + + setLoading(true); + refresh(); // Initial fetch + + const interval = setInterval(() => { + if (enabledRef.current) { + refresh(); + } + }, pollInterval); + + return () => clearInterval(interval); + }, [enabled, pollInterval, refresh]); + + return { + ...progress, + loading, + error, + refresh, + }; +} + +/** + * Format bytes to human-readable string + * @param {number} bytes - Bytes to format + * @returns {string} Formatted string (e.g., "1.23 GB") + */ +export function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const k = 1024; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${(bytes / Math.pow(k, i)).toFixed(i > 0 ? 2 : 0)} ${units[i]}`; +} + +/** + * Format ETA to human-readable string + * @param {string} eta - ETA in format "H:MM:SS" or "MM:SS" + * @returns {string} Formatted string (e.g., "~5:23 remaining") + */ +export function formatEta(eta) { + if (!eta || eta === '0:00:00') return ''; + + // Remove leading zeros from hours if present + const cleaned = eta.replace(/^0:/, ''); + return `~${cleaned} remaining`; +} + +export default useMusicSyncProgress; diff --git a/teslausb-www-react/src/hooks/useStatus.js b/teslausb-www-react/src/hooks/useStatus.js new file mode 100644 index 00000000..6df06b68 --- /dev/null +++ b/teslausb-www-react/src/hooks/useStatus.js @@ -0,0 +1,113 @@ +import { useState, useEffect, useCallback } from 'preact/hooks'; +import { fetchStatus, fetchConfig, fetchStorage } from '../services/api'; + +/** + * Hook for managing system status with polling + * @param {number} pollInterval - Polling interval in ms (default 5000) + * @returns {Object} Status, config, storage, loading state, and refresh function + */ +export function useStatus(pollInterval = 5000) { + const [status, setStatus] = useState(null); + const [config, setConfig] = useState(null); + const [storage, setStorage] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [lastUpdate, setLastUpdate] = useState(null); + + const refresh = useCallback(async () => { + try { + const [statusData, configData, storageData] = await Promise.all([ + fetchStatus(), + fetchConfig(), + fetchStorage().catch(() => null), // Storage API is optional + ]); + setStatus(statusData); + setConfig(configData); + setStorage(storageData); + setLastUpdate(new Date()); + setError(null); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + refresh(); + const interval = setInterval(refresh, pollInterval); + return () => clearInterval(interval); + }, [refresh, pollInterval]); + + // Compute derived values + const computed = status ? { + cpuTempC: status.cpu_temp ? (parseInt(status.cpu_temp, 10) / 1000).toFixed(1) : null, + uptimeFormatted: formatUptime(parseInt(status.uptime || '0', 10)), + diskUsedPercent: status.total_space && status.free_space + ? Math.round(((parseInt(status.total_space, 10) - parseInt(status.free_space, 10)) / parseInt(status.total_space, 10)) * 100) + : 0, + diskUsedGB: status.total_space && status.free_space + ? ((parseInt(status.total_space, 10) - parseInt(status.free_space, 10)) / (1024 * 1024 * 1024)).toFixed(1) + : '0', + diskTotalGB: status.total_space + ? (parseInt(status.total_space, 10) / (1024 * 1024 * 1024)).toFixed(1) + : '0', + diskFreeGB: status.free_space + ? (parseInt(status.free_space, 10) / (1024 * 1024 * 1024)).toFixed(1) + : '0', + drivesActive: status.drives_active === 'yes', + wifiConnected: !!status.wifi_ssid && status.wifi_ssid !== '', + wifiSignalPercent: parseWifiSignal(status.wifi_strength), + ethernetConnected: !!status.ether_ip && status.ether_ip !== '', + snapshotCount: parseInt(status.num_snapshots || '0', 10), + } : null; + + return { + status, + config, + storage, + computed, + loading, + error, + lastUpdate, + refresh, + }; +} + +/** + * Format uptime seconds into human-readable string + * @param {number} seconds - Uptime in seconds + * @returns {string} Formatted uptime + */ +function formatUptime(seconds) { + if (!seconds || isNaN(seconds)) return '0s'; + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + const parts = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); + + return parts.join(' '); +} + +/** + * Parse WiFi signal strength string + * @param {string} strength - e.g., "40/70" + * @returns {number} Signal percentage + */ +function parseWifiSignal(strength) { + if (!strength) return 0; + const parts = strength.split('/'); + if (parts.length !== 2) return 0; + const [current, max] = parts.map(Number); + if (isNaN(current) || isNaN(max) || max === 0) return 0; + return Math.round((current / max) * 100); +} + +export default useStatus; diff --git a/teslausb-www-react/src/main.jsx b/teslausb-www-react/src/main.jsx new file mode 100644 index 00000000..d39a8332 --- /dev/null +++ b/teslausb-www-react/src/main.jsx @@ -0,0 +1,5 @@ +import { render } from 'preact'; +import { App } from './App'; +import './styles/index.css'; + +render(, document.getElementById('app')); diff --git a/teslausb-www-react/src/services/api.js b/teslausb-www-react/src/services/api.js new file mode 100644 index 00000000..67724d53 --- /dev/null +++ b/teslausb-www-react/src/services/api.js @@ -0,0 +1,434 @@ +/** + * TeslaUSB API Service + * Handles all communication with the backend CGI scripts + */ + +const API_BASE = '/cgi-bin'; + +/** + * Fetch system status + * @returns {Promise} Status object with cpu_temp, disk space, wifi, etc. + */ +export async function fetchStatus() { + const response = await fetch(`${API_BASE}/status.sh`); + if (!response.ok) throw new Error('Failed to fetch status'); + return response.json(); +} + +/** + * Fetch system configuration + * @returns {Promise} Config object with has_cam, has_music, etc. + */ +export async function fetchConfig() { + const response = await fetch(`${API_BASE}/config.sh`); + if (!response.ok) throw new Error('Failed to fetch config'); + return response.json(); +} + +/** + * Fetch per-drive storage information + * @returns {Promise} Storage object with cam, music, lightshow, boombox, total + */ +export async function fetchStorage() { + const response = await fetch(`${API_BASE}/storage.sh`); + if (!response.ok) throw new Error('Failed to fetch storage'); + return response.json(); +} + +/** + * List directory contents + * @param {string} rootPath - Root path (e.g., /mnt/music) + * @param {string} dirPath - Directory path relative to root + * @returns {Promise} Parsed directory listing + */ +export async function listDirectory(rootPath, dirPath = '') { + const params = new URLSearchParams(); + params.append('root', rootPath); + if (dirPath) params.append('path', dirPath); + + const response = await fetch(`${API_BASE}/ls.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(dirPath)}`); + if (!response.ok) throw new Error('Failed to list directory'); + + const text = await response.text(); + return parseDirectoryListing(text); +} + +/** + * Parse directory listing from ls.sh output + * @param {string} text - Raw text output + * @returns {Object} Parsed listing with directories, files, and storage stats + */ +function parseDirectoryListing(text) { + const lines = text.trim().split('\n').filter(Boolean); + const result = { + directories: [], + files: [], + storage: null, + }; + + for (const line of lines) { + if (line.startsWith('d:')) { + result.directories.push({ name: line.slice(2), depth: 1 }); + } else if (line.startsWith('D:')) { + result.directories.push({ name: line.slice(2), depth: 2 }); + } else if (line.startsWith('f:')) { + const parts = line.slice(2).split(':'); + const size = parseInt(parts.pop(), 10); + const path = parts.join(':'); + result.files.push({ path, size }); + } else if (line.startsWith('s:')) { + const [, free, total] = line.split(':'); + result.storage = { + free: parseInt(free, 10), + total: parseInt(total, 10), + }; + } + } + + return result; +} + +/** + * Upload a file + * @param {string} rootPath - Root path + * @param {string} destPath - Destination path + * @param {File} file - File to upload + * @param {Function} onProgress - Progress callback (0-100) + * @returns {Promise} + */ +export async function uploadFile(rootPath, destPath, file, onProgress) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${API_BASE}/upload.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(destPath)}`); + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + + xhr.upload.onprogress = (e) => { + if (e.lengthComputable && onProgress) { + onProgress(Math.round((e.loaded / e.total) * 100)); + } + }; + + xhr.onload = () => { + if (xhr.status === 200) { + resolve(); + } else { + reject(new Error(`Upload failed: ${xhr.status}`)); + } + }; + + xhr.onerror = () => reject(new Error('Upload failed')); + xhr.send(file); + }); +} + +/** + * Download a file + * @param {string} rootPath - Root path + * @param {string} filePath - File path + * @returns {string} Download URL + */ +export function getDownloadUrl(rootPath, filePath) { + return `${API_BASE}/download.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(filePath)}`; +} + +/** + * Download multiple files as ZIP + * @param {string} rootPath - Root path + * @param {string[]} paths - Array of file paths + * @returns {string} Download URL + */ +export function getZipDownloadUrl(rootPath, paths) { + const params = [encodeURIComponent(rootPath), ...paths.map(p => encodeURIComponent(p))].join('&'); + return `${API_BASE}/downloadzip.sh?${params}`; +} + +/** + * Create a directory + * @param {string} rootPath - Root path + * @param {string} dirName - Directory name to create + * @returns {Promise} + */ +export async function createDirectory(rootPath, dirName) { + const response = await fetch(`${API_BASE}/mkdir.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(dirName)}`); + if (!response.ok) throw new Error('Failed to create directory'); +} + +/** + * Move/Rename a file or directory + * @param {string} rootPath - Root path + * @param {string} currentPath - Current path + * @param {string} newName - New name + * @returns {Promise} + */ +export async function moveItem(rootPath, currentPath, newName) { + const response = await fetch(`${API_BASE}/mv.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(currentPath)}&${encodeURIComponent(newName)}`); + if (!response.ok) throw new Error('Failed to move/rename item'); +} + +/** + * Delete files or directories + * @param {string} rootPath - Root path + * @param {string[]} paths - Paths to delete + * @returns {Promise} + */ +export async function deleteItems(rootPath, paths) { + const params = [encodeURIComponent(rootPath), ...paths.map(p => encodeURIComponent(p))].join('&'); + const response = await fetch(`${API_BASE}/rm.sh?${params}`); + if (!response.ok) throw new Error('Failed to delete items'); +} + +/** + * Copy a file (used for lock chime) + * @param {string} rootPath - Root path + * @param {string} sourcePath - Source file path + * @param {string} destName - Destination name + * @returns {Promise} + */ +export async function copyFile(rootPath, sourcePath, destName) { + const response = await fetch(`${API_BASE}/cp.sh?${encodeURIComponent(rootPath)}&${encodeURIComponent(sourcePath)}&${encodeURIComponent(destName)}`); + if (!response.ok) throw new Error('Failed to copy file'); +} + +/** + * Fetch video list + * @returns {Promise} Organized video list by category + */ +export async function fetchVideoList() { + const response = await fetch(`${API_BASE}/videolist.sh`); + if (!response.ok) throw new Error('Failed to fetch video list'); + + const text = await response.text(); + return parseVideoList(text); +} + +/** + * Parse video list into organized structure + * @param {string} text - Raw video list text + * @returns {Object} Organized by category > date > files + */ +function parseVideoList(text) { + const lines = text.trim().split('\n').filter(Boolean); + const result = { + RecentClips: {}, + SavedClips: {}, + SentryClips: {}, + }; + + for (const line of lines) { + const parts = line.split('/'); + if (parts.length >= 2) { + const [category, ...rest] = parts; + if (result[category]) { + const dateOrFile = rest[0]; + if (!result[category][dateOrFile]) { + result[category][dateOrFile] = []; + } + if (rest.length > 1) { + result[category][dateOrFile].push(rest.slice(1).join('/')); + } + } + } + } + + return result; +} + +/** + * Trigger sync operation + * @returns {Promise} + */ +export async function triggerSync() { + const response = await fetch(`${API_BASE}/trigger_sync.sh`); + if (!response.ok) throw new Error('Failed to trigger sync'); +} + +/** + * Fetch music sync progress + * @returns {Promise} Progress object with active, bytesTransferred, percentage, speed, eta + */ +export async function fetchMusicSyncProgress() { + const response = await fetch(`${API_BASE}/music_sync_progress.sh`); + if (!response.ok) throw new Error('Failed to fetch music sync progress'); + return response.json(); +} + +/** + * Toggle USB drives visibility + * @returns {Promise} + */ +export async function toggleDrives() { + const response = await fetch(`${API_BASE}/toggledrives.sh`); + if (!response.ok) throw new Error('Failed to toggle drives'); +} + +/** + * Reboot the system + * @returns {Promise} + */ +export async function reboot() { + const response = await fetch(`${API_BASE}/reboot.sh`); + if (!response.ok) throw new Error('Failed to reboot'); +} + +/** + * Start BLE pairing + * @returns {Promise} True if pairing initiated + */ +export async function startBLEPairing() { + const response = await fetch(`${API_BASE}/pairBLEkey.sh`); + return response.status === 202; +} + +/** + * Check BLE pairing status + * @returns {Promise} True if paired + */ +export async function checkBLEStatus() { + const response = await fetch(`${API_BASE}/checkBLEstatus.sh`); + const text = await response.text(); + return text.includes('

          paired

          '); +} + +/** + * Generate diagnostics + * @returns {Promise} + */ +export async function generateDiagnostics() { + await fetch(`${API_BASE}/diagnose.sh`); +} + +/** + * Fetch diagnostics file + * @returns {Promise} Diagnostics content + */ +export async function fetchDiagnostics() { + const response = await fetch('/diagnostics.txt'); + if (!response.ok) throw new Error('Failed to fetch diagnostics'); + return response.text(); +} + +/** + * Fetch log file with range support for efficient tailing + * @param {string} logFile - Log file path + * @param {number} lastSize - Last known size for range request + * @returns {Promise} { content, size, truncated } + */ +export async function fetchLog(logFile, lastSize = 0) { + const headers = {}; + if (lastSize > 0) { + headers['Range'] = `bytes=${lastSize}-`; + } + + const response = await fetch(`/${logFile}`, { headers }); + + if (response.status === 416) { + // Range not satisfiable - log was truncated or no new content + return { content: '', size: lastSize, truncated: false }; + } + + if (response.status === 404) { + throw new Error('Log file not found'); + } + + if (!response.ok && response.status !== 206) { + throw new Error(`Failed to fetch log: ${response.status}`); + } + + const content = await response.text(); + + // For range requests (206), calculate new size from content-range header or content length + let newSize = lastSize; + if (response.status === 206) { + const contentRange = response.headers.get('Content-Range'); + if (contentRange) { + // Format: bytes start-end/total + const match = contentRange.match(/bytes \d+-(\d+)\/(\d+)/); + if (match) { + newSize = parseInt(match[1], 10) + 1; + } else { + newSize = lastSize + content.length; + } + } else { + newSize = lastSize + content.length; + } + } else { + // Full response (200) + newSize = content.length; + } + + return { + content, + size: newSize, + truncated: false, + }; +} + +/** + * Run network speed test + * @param {Function} onProgress - Progress callback with speed in Mbps + * @param {AbortSignal} signal - Abort signal + * @returns {Promise} Final speed in Mbps + */ +export async function runSpeedTest(onProgress, signal) { + const response = await fetch(`${API_BASE}/randomdata.sh`, { signal }); + if (!response.ok) throw new Error('Failed to start speed test'); + + const reader = response.body.getReader(); + let totalBytes = 0; + const startTime = performance.now(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + totalBytes += value.length; + const elapsed = (performance.now() - startTime) / 1000; + const speedMbps = (totalBytes * 8) / (elapsed * 1000000); + + if (onProgress) { + onProgress(speedMbps); + } + } + } catch (e) { + if (e.name !== 'AbortError') throw e; + } + + const elapsed = (performance.now() - startTime) / 1000; + return (totalBytes * 8) / (elapsed * 1000000); +} + +/** + * Get video URL for a specific camera angle + * @param {string} category - RecentClips, SavedClips, or SentryClips + * @param {string} sequence - Date/time folder + * @param {string} filename - Video filename + * @returns {string} Video URL + */ +export function getVideoUrl(category, sequence, filename) { + return `/TeslaCam/${category}/${sequence}/${filename}`; +} + +/** + * Get event.json URL for sentry events + * @param {string} sequence - Date/time folder + * @returns {string} Event JSON URL + */ +export function getEventJsonUrl(sequence) { + return `/TeslaCam/SentryClips/${sequence}/event.json`; +} + +/** + * Fetch sentry event data + * @param {string} sequence - Date/time folder + * @returns {Promise} Event data or null + */ +export async function fetchEventData(sequence) { + try { + const response = await fetch(getEventJsonUrl(sequence)); + if (!response.ok) return null; + return response.json(); + } catch { + return null; + } +} diff --git a/teslausb-www-react/src/styles/fonts.css b/teslausb-www-react/src/styles/fonts.css new file mode 100644 index 00000000..4021b92e --- /dev/null +++ b/teslausb-www-react/src/styles/fonts.css @@ -0,0 +1,52 @@ +/* Lato Font - Local fallback for offline use */ +/* These fonts should be placed in /public/fonts/ */ + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Lato Regular'), local('Lato-Regular'), + url('/fonts/lato-regular.woff2') format('woff2'), + url('/fonts/lato-regular.woff') format('woff'); +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: local('Lato Medium'), local('Lato-Medium'), + url('/fonts/lato-medium.woff2') format('woff2'), + url('/fonts/lato-medium.woff') format('woff'); +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: local('Lato Semibold'), local('Lato-Semibold'), + url('/fonts/lato-semibold.woff2') format('woff2'), + url('/fonts/lato-semibold.woff') format('woff'); +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Lato Bold'), local('Lato-Bold'), + url('/fonts/lato-bold.woff2') format('woff2'), + url('/fonts/lato-bold.woff') format('woff'); +} + +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: local('Lato Italic'), local('Lato-Italic'), + url('/fonts/lato-italic.woff2') format('woff2'), + url('/fonts/lato-italic.woff') format('woff'); +} diff --git a/teslausb-www-react/src/styles/index.css b/teslausb-www-react/src/styles/index.css new file mode 100644 index 00000000..b5a59418 --- /dev/null +++ b/teslausb-www-react/src/styles/index.css @@ -0,0 +1,2226 @@ +/* TeslaUSB Modern Dashboard Styles */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background-color: #f5f5f5; + color: #333; + overflow: hidden; +} + +/* Mobile body adjustments for proper scrolling */ +@media (max-width: 767px) { + body { + overflow: visible; + height: auto; + } +} + +/* Container holding top bar + the grid */ +.app-shell { + display: flex; + flex-direction: column; + min-height: 100vh; + overflow: hidden; +} + +/* Mobile-specific shell adjustments */ +@media (max-width: 767px) { + .app-shell { + overflow: visible; + height: auto; + min-height: 100vh; + } +} + +/* Top bar styles */ +.app-topbar { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #1f2937; + color: #ffffff; + padding: 0.75rem 1rem; + border-bottom: 1px solid #374151; +} + +.app-topbar-title { + font-size: 1.1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.app-topbar-title svg { + width: 24px; + height: 24px; +} + +.app-topbar-actions { + display: flex; + gap: 1rem; + align-items: center; +} + +/* Main Dashboard Container */ +.app-dashboard { + display: flex; + flex-direction: column; + height: 100vh; + background-color: #f5f5f5; +} + +/* Header */ +.app-header { + background-color: #ffffff; + border-bottom: 1px solid #e1e5e9; + padding: 0.75rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + height: 60px; + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 40; + backdrop-filter: saturate(180%) blur(4px); +} + +.app-header-left { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.app-header-icon { + width: 20px; + height: 20px; + color: #0066cc; +} + +.app-header-title { + font-size: 16px; + font-weight: 600; + color: #333; + letter-spacing: 0.1px; +} + +.app-header-subtitle { + font-size: 14px; + color: #666; + margin-left: 0.5rem; +} + +.app-header-right { + display: flex; + align-items: center; + gap: 1rem; +} + +/* Navigation Links / Tabs */ +.dashboard-nav { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.nav-link { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + color: #6b7280; + text-decoration: none; + transition: all 0.15s ease; + border: 1px solid transparent; + position: relative; + cursor: pointer; + background: none; +} + +.nav-link:hover { + background-color: #f9fafb; + color: #374151; +} + +.nav-link.active { + background-color: #0095f6; + color: #ffffff; + border-color: #007dd1; +} + +.nav-link.active svg { + color: #ffffff; +} + +.nav-link svg { + color: #6b7280; + transition: color 0.15s ease; + width: 16px; + height: 16px; +} + +.nav-link:hover svg { + color: #374151; +} + +/* Badge for counts */ +.nav-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + background-color: #ef4444; + color: #ffffff; + font-size: 11px; + font-weight: 700; + border-radius: 9px; + margin-left: 4px; + line-height: 1; +} + +.nav-link.active .nav-badge { + background-color: #ffffff; + color: #ef4444; +} + +.app-last-update { + font-size: 13px; + color: #6b7280; +} + +.app-status { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.3px; +} + +.app-status.healthy { + color: #065f46; + background: #d1fae5; + border: 1px solid #a7f3d0; +} + +.app-status.unhealthy { + color: #7f1d1d; + background: #fee2e2; + border: 1px solid #fecaca; +} + +.app-status.warning { + color: #92400e; + background: #fef3c7; + border: 1px solid #fde68a; +} + +/* Main Body */ +.app-body { + display: flex; + flex: 1; + overflow: hidden; +} + +/* Left Sidebar */ +.app-sidebar { + width: 280px; + background-color: #ffffff; + border-right: 1px solid #e1e5e9; + padding: 1rem; + overflow-y: auto; + overflow-x: hidden; + flex-shrink: 0; + max-height: calc(100vh - 60px); + -webkit-overflow-scrolling: touch; +} + +.device-info { + display: flex; + flex-direction: column; + border-radius: 10px; + border: 1px solid #e5e7eb; + background: #fff; + margin-bottom: 1rem; +} + +.device-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 10px 12px; + border-bottom: 1px solid #eef2f7; + font-size: 14px; + font-weight: 600; + color: #1f2937; +} + +.device-header svg { + width: 16px; + height: 16px; + color: #6b7280; +} + +/* Info List Style */ +.info-list { + display: flex; + flex-direction: column; + padding: 8px 12px; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; + font-size: 13px; +} + +.info-item:last-child { + border-bottom: none; +} + +.info-item.clickable { + cursor: pointer; + transition: background-color 0.15s ease; + margin: 0 -12px; + padding: 6px 12px; + border-radius: 4px; +} + +.info-item.clickable:hover { + background-color: #f3f4f6; +} + +.info-item.clickable:active { + background-color: #e5e7eb; +} + +/* Toggle Button in sidebar */ +.toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + line-height: 1; + border: 1px solid #007dd1; + background-color: #0095f6; + color: #ffffff; + cursor: pointer; + transition: all 0.15s ease; +} + +.toggle-btn svg { + flex-shrink: 0; + vertical-align: middle; + margin-right: 4px; + color: #ffffff; +} + +.toggle-btn:hover:not(:disabled) { + background-color: #007dd1; + border-color: #006bbd; +} + +.toggle-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.toggle-btn.active { + background-color: #dcfce7; + border-color: #22c55e; + color: #166534; +} + +.toggle-btn.danger { + background-color: #fee2e2; + border-color: #f87171; + color: #b91c1c; +} + +/* UI Switch Links */ +.ui-switch-link { + color: #0095f6; + text-decoration: none; + font-size: 13px; + padding: 4px 0; + display: block; + width: 100%; +} + +.ui-switch-link:hover { + text-decoration: underline; +} + +.toggle-btn.danger:hover:not(:disabled) { + background-color: #fecaca; + border-color: #ef4444; +} + +.toggle-btn.primary { + background-color: #eff6ff; + border-color: #3b82f6; + color: #1d4ed8; +} + +.toggle-btn.primary:hover:not(:disabled) { + background-color: #dbeafe; + border-color: #2563eb; +} + +/* Sidebar Action Buttons (full width) */ +.sidebar-action { + margin-top: 8px; + padding: 0; +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.action-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.action-btn.connected { + background-color: #dcfce7; + color: #166534; + border: 1px solid #22c55e; +} + +.action-btn.connected:hover:not(:disabled) { + background-color: #bbf7d0; +} + +.action-btn.disconnected { + background-color: #fef2f2; + color: #991b1b; + border: 1px solid #f87171; +} + +.action-btn.disconnected:hover:not(:disabled) { + background-color: #fee2e2; +} + +.action-btn.restart { + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; +} + +.action-btn.restart:hover:not(:disabled) { + background-color: #e5e7eb; + border-color: #9ca3af; +} + +.info-label { + color: #6b7280; + font-weight: 500; +} + +.info-value { + color: #111827; + font-weight: 600; + text-align: right; +} + +.info-value-with-action { + display: flex; + align-items: center; + gap: 8px; +} + +.speed-result { + color: #111827; + font-weight: 600; + font-size: 12px; +} + +/* Small text class */ +.small-text { + font-size: 11px !important; + line-height: 1.2; +} + +/* Compact date formatting */ +.compact-date { + font-size: 10px !important; + line-height: 1.1; + white-space: nowrap; + color: #4b5563; +} + +/* Progress Bar */ +.progress-container { + margin-top: 0.5rem; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #eef2f7; + border-radius: 999px; + overflow: hidden; + margin-bottom: 0.25rem; +} + +.progress-fill { + height: 100%; + border-radius: 999px; + transition: width 0.3s ease; +} + +.progress-fill.blue { + background-color: #3b82f6; +} + +.progress-fill.green { + background-color: #10b981; +} + +.progress-fill.yellow { + background-color: #f59e0b; +} + +.progress-fill.red { + background-color: #ef4444; +} + +.progress-text { + font-size: 10px; + color: #6b7280; + font-weight: 500; + margin-top: 6px; + text-align: right; +} + +/* Status Colors */ +.status-healthy { + color: #00b04f !important; +} + +.status-degraded { + color: #ff9800 !important; +} + +.status-unhealthy { + color: #e74c3c !important; +} + +/* Main Content Area */ +.app-main { + flex: 1; + background-color: #ffffff; + padding: 1rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1rem; + max-height: calc(100vh - 60px); +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1px; + background-color: #e1e5e9; + border: 1px solid #e1e5e9; + border-radius: 4px; + overflow: visible; + margin-bottom: 1rem; +} + +.stats-section { + background-color: #ffffff; + padding: 1rem; +} + +.stats-section h3 { + font-size: 12px; + font-weight: 600; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.75rem; +} + +.stat-data { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.primary-stat { + font-size: 24px; + font-weight: 700; + color: #333; + line-height: 1; +} + +.stat-details { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-details span { + font-size: 11px; + color: #666; +} + +.stat-details span:first-child { + color: #333; + font-weight: 500; + font-size: 12px; +} + +/* Monospace values */ +.mono-value { + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + color: #333; + font-weight: 600; + font-size: 12px; +} + +/* Chart/Card Containers */ +.card { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 12px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02); + transition: transform 0.08s ease, box-shadow 0.12s ease, border-color 0.12s ease; +} + +.card:hover { + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); + border-color: #dee3ea; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + +.card-title { + font-size: 12px; + font-weight: 600; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.card-value { + font-size: 22px; + font-weight: 700; + color: #111827; + line-height: 1; +} + +.card-sub { + font-size: 11px; + color: #666; + margin-top: 4px; +} + +/* Dashboard sections with spacing */ +.dashboard-section { + margin-bottom: 1rem; +} + +.section-title { + font-size: 12px; + font-weight: 600; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.4px; + margin-bottom: 0.5rem; +} + +/* Features Row - compact badges */ +.features-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.feature-badge { + display: inline-block; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + line-height: 1; + text-align: center; +} + +.feature-badge.enabled { + background: #ecfdf5; + color: #059669; + border: 1px solid #a7f3d0; +} + +.feature-badge.disabled { + background: #f3f4f6; + color: #9ca3af; + border: 1px solid #e5e7eb; +} + +/* Actions Row */ +.actions-row { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +/* KPI Row */ +.kpi-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; + margin-bottom: 14px; +} + +/* Chart container */ +.chart-container { + background-color: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; +} + +.chart-header { + padding: 10px 12px; + border-bottom: 1px solid #eef2f7; + margin: -1rem -1rem 1rem -1rem; +} + +.chart-title { + font-size: 14px; + font-weight: 600; + color: #1f2937; +} + +/* Loading and Error States */ +.loading-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + gap: 0.5rem; + font-size: 14px; + color: #666; +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid #e5e7eb; + border-top-color: #0095f6; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.spinning { + animation: spin 1s linear infinite; +} + +.error-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + gap: 0.5rem; + font-size: 14px; + color: #e74c3c; +} + +.retry-btn { + background-color: #e74c3c; + border: none; + color: white; + padding: 0.375rem 0.75rem; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + margin-left: 0.5rem; +} + +.retry-btn:hover { + background-color: #c0392b; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 12px; + font-size: 13px; + font-weight: 500; + line-height: 1.4; + border-radius: 6px; + border: 1px solid #cfd8e3; + background: #ffffff; + color: #374151; + cursor: pointer; + transition: all 0.15s ease-in-out; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); +} + +.btn svg { + color: #374151; + transition: color 0.15s ease-in-out; + width: 14px; + height: 14px; +} + +.btn:hover:not(:disabled) { + background: #0095f6; + border-color: #007dd1; + color: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.btn:hover:not(:disabled) svg { + color: #ffffff; +} + +.btn:active:not(:disabled) { + background: #007dd1; + border-color: #006bbd; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + background: #f9fafb; + color: #9ca3af; + border-color: #e5e7eb; +} + +.btn-primary { + background: #0095f6; + border-color: #007dd1; + color: #ffffff; +} + +.btn-primary svg { + color: #ffffff; +} + +.btn-primary:hover:not(:disabled) { + background: #007dd1; + border-color: #006bbd; +} + +.btn-danger { + background: #ef4444; + border-color: #dc2626; + color: #ffffff; +} + +.btn-danger svg { + color: #ffffff; +} + +.btn-danger:hover:not(:disabled) { + background: #dc2626; + border-color: #b91c1c; +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; +} + +.btn-lg { + padding: 10px 20px; + font-size: 15px; +} + +/* Storage Bar */ +.storage-bar-container { + margin: 1rem 0; +} + +/* Storage Bar */ +.storage-bar { + display: flex; + height: 24px; + border-radius: 6px; + overflow: hidden; + background-color: #e5e7eb; +} + +.storage-segment { + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; + color: #fff; + transition: width 0.3s ease; + min-width: 0; +} + +.storage-segment.teslacam { + background-color: #3b82f6; +} + +.storage-segment.music { + background-color: #8b5cf6; +} + +.storage-segment.lightshow { + background-color: #ec4899; +} + +.storage-segment.boombox { + background-color: #f97316; +} + +.storage-segment.free { + background-color: #d1d5db; +} + +/* Storage Legend */ +.storage-legend { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-top: 0.75rem; + font-size: 12px; +} + +.storage-legend-item { + display: flex; + align-items: center; + gap: 6px; +} + +.storage-legend-dot { + width: 10px; + height: 10px; + border-radius: 2px; +} + +.storage-legend-dot.teslacam { + background-color: #3b82f6; +} + +.storage-legend-dot.music { + background-color: #8b5cf6; +} + +.storage-legend-dot.lightshow { + background-color: #ec4899; +} + +.storage-legend-dot.boombox { + background-color: #f97316; +} + +.storage-legend-dot.free { + background-color: #d1d5db; +} + +.storage-legend-label { + color: #374151; + font-weight: 500; +} + +.storage-legend-value { + color: #6b7280; +} + +.storage-legend-used { + color: #9ca3af; + font-size: 11px; +} + +.storage-legend-percent { + margin-left: 4px; + color: #9ca3af; + font-size: 11px; +} + +.storage-note { + margin-top: 0.5rem; + font-size: 10px; + color: #9ca3af; + font-style: italic; +} + +/* Sync Status Card */ +.sync-status-card { + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 1rem; + background: #fff; +} + +.sync-status-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.sync-status-title { + font-size: 12px; + font-weight: 600; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.sync-status-indicator { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; +} + +.sync-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.sync-status-dot.idle { + background-color: #10b981; +} + +.sync-status-dot.connecting { + background-color: #f59e0b; + animation: pulse 1.5s ease-in-out infinite; +} + +.sync-status-dot.archiving { + background-color: #3b82f6; + animation: pulse 1s ease-in-out infinite; +} + +.sync-status-dot.error { + background-color: #ef4444; +} + +.sync-status-description { + font-size: 13px; + color: #6b7280; + margin-bottom: 0.5rem; +} + +.sync-details { + font-size: 12px; + color: #9ca3af; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.sync-progress-bar { + height: 8px; + background-color: #e5e7eb; + border-radius: 4px; + overflow: hidden; + margin: 0.75rem 0; +} + +.sync-progress-fill { + height: 100%; + background-color: #3b82f6; + border-radius: 4px; + transition: width 0.3s ease; +} + +.sync-details { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 11px; + color: #6b7280; +} + +.sync-progress-main { + font-weight: 500; +} + +.sync-progress-secondary { + font-size: 10px; + color: #9ca3af; +} + +.sync-file { + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: 'Monaco', 'Menlo', monospace; +} + +/* Log Viewer */ +.log-viewer { + background-color: #1e1e1e; + border-radius: 8px; + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 12px; + line-height: 1.5; + overflow: hidden; + display: flex; + flex-direction: column; + height: 400px; +} + +.log-viewer-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background-color: #2d2d2d; + border-bottom: 1px solid #3d3d3d; +} + +.log-viewer-title { + color: #e0e0e0; + font-size: 13px; + font-weight: 600; +} + +.log-viewer-actions { + display: flex; + gap: 8px; +} + +.log-action-btn { + background: #4a4a4a !important; + border-color: #666 !important; + color: #e0e0e0 !important; +} + +.log-action-btn svg { + color: #e0e0e0 !important; +} + +.log-action-btn:hover:not(:disabled) { + background: #5a5a5a !important; + border-color: #888 !important; +} + +.log-action-btn:hover:not(:disabled) svg { + color: #ffffff !important; +} + +.log-action-btn:disabled { + opacity: 0.4; +} + +.log-viewer-content { + flex: 1; + overflow-y: auto; + padding: 12px; + color: #d4d4d4; +} + +.log-viewer-content::-webkit-scrollbar { + width: 8px; +} + +.log-viewer-content::-webkit-scrollbar-track { + background: #1e1e1e; +} + +.log-viewer-content::-webkit-scrollbar-thumb { + background: #4d4d4d; + border-radius: 4px; +} + +.log-line { + white-space: pre-wrap; + word-break: break-all; +} + +.log-line.error { + color: #f87171; +} + +.log-line.warning { + color: #fbbf24; +} + +.log-line.success { + color: #34d399; +} + +/* Video Viewer */ +.video-viewer { + display: flex; + flex-direction: column; + height: 100%; + background: #000; + border-radius: 8px; + overflow: hidden; +} + +.video-grid { + display: grid; + flex: 1; + gap: 2px; + background: #1a1a1a; + padding: 2px; +} + +.video-grid.layout-6 { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); +} + +.video-grid.layout-4 { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); +} + +.video-grid.layout-1 { + grid-template-columns: 1fr; + grid-template-rows: 1fr; +} + +.video-cell { + position: relative; + background: #000; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.video-cell video { + width: 100%; + height: 100%; + object-fit: contain; +} + +.video-cell-label { + position: absolute; + top: 8px; + left: 8px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.video-controls { + display: flex; + align-items: center; + gap: 1rem; + padding: 12px 16px; + background: #1a1a1a; + border-top: 1px solid #333; +} + +.video-timeline { + flex: 1; + height: 6px; + background: #333; + border-radius: 3px; + cursor: pointer; + position: relative; +} + +.video-timeline-progress { + height: 100%; + background: #0095f6; + border-radius: 3px; + transition: width 0.1s linear; +} + +.video-timeline-markers { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; +} + +.video-timeline-marker { + position: absolute; + top: -2px; + width: 2px; + height: 10px; + background: #ef4444; + border-radius: 1px; +} + +.video-play-btn { + background: #0095f6; + border: none; + color: #fff; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s; +} + +.video-play-btn:hover { + background: #007dd1; +} + +.video-time { + color: #fff; + font-size: 13px; + font-family: 'Monaco', 'Menlo', monospace; + min-width: 100px; +} + +/* File Browser */ +.file-browser { + display: flex; + height: 100%; + border: 1px solid #e5e7eb; + border-radius: 8px; + overflow: hidden; +} + +.file-tree { + width: 250px; + background: #f9fafb; + border-right: 1px solid #e5e7eb; + overflow-y: auto; +} + +.file-tree-item { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + cursor: pointer; + font-size: 13px; + color: #374151; + border-bottom: 1px solid #f0f0f0; +} + +.file-tree-item:hover { + background: #f3f4f6; +} + +.file-tree-item.active { + background: #e0f2fe; + color: #0369a1; +} + +.file-tree-item svg { + width: 16px; + height: 16px; + color: #6b7280; + flex-shrink: 0; +} + +.file-tree-item.active svg { + color: #0369a1; +} + +.file-list { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +.file-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e5e7eb; +} + +.file-list-title { + font-size: 14px; + font-weight: 600; + color: #1f2937; +} + +.file-list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 12px; +} + +.file-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 12px; + border: 1px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; +} + +.file-item:hover { + background: #f9fafb; + border-color: #e5e7eb; +} + +.file-item.selected { + background: #e0f2fe; + border-color: #0ea5e9; +} + +.file-item-icon { + width: 48px; + height: 48px; + margin-bottom: 8px; + color: #6b7280; +} + +.file-item-icon.folder { + color: #f59e0b; +} + +.file-item-icon.video { + color: #8b5cf6; +} + +.file-item-icon.audio { + color: #ec4899; +} + +.file-item-name { + font-size: 12px; + text-align: center; + word-break: break-word; + color: #374151; +} + +.file-item-size { + font-size: 10px; + color: #9ca3af; + margin-top: 2px; +} + +/* Dropzone */ +.dropzone { + border: 2px dashed #d1d5db; + border-radius: 8px; + padding: 2rem; + text-align: center; + color: #6b7280; + transition: all 0.15s; +} + +.dropzone svg { + width: 24px; + height: 24px; +} + +.dropzone.active { + border-color: #0095f6; + background: #f0f9ff; + color: #0369a1; +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal { + background: #fff; + border-radius: 12px; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow: hidden; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e7eb; +} + +.modal-title { + font-size: 16px; + font-weight: 600; + color: #1f2937; +} + +.modal-close { + background: none; + border: none; + cursor: pointer; + color: #6b7280; + padding: 4px; +} + +.modal-close:hover { + color: #374151; +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e7eb; + background: #f9fafb; +} + +/* Toast notifications */ +.toast-container { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 1100; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.toast { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: #1f2937; + color: #fff; + border-radius: 8px; + font-size: 13px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + animation: slideIn 0.2s ease; +} + +.toast.success { + background: #059669; +} + +.toast.error { + background: #dc2626; +} + +.toast.warning { + background: #d97706; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Responsive Design */ + +/* iPad Pro (1024px and up) */ +@media (min-width: 1024px) and (max-width: 1366px) { + .app-sidebar { + width: 260px; + } + + .kpi-row { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; + } + + .app-header-title { + font-size: 15px; + } +} + +/* iPad (768px - 1023px) */ +@media (min-width: 768px) and (max-width: 1023px) { + .app-body { + flex-direction: row; + } + + .app-sidebar { + width: 220px; + max-height: calc(100vh - 60px); + overflow-y: auto; + } + + .kpi-row { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } + + .card { + padding: 10px; + } + + .card-value { + font-size: 18px; + } + + .card-title { + font-size: 11px; + } + + .card-sub { + font-size: 10px; + } + + .app-header-right { + gap: 8px; + } + + .app-last-update { + font-size: 11px; + } + + .chart-container { + padding: 12px; + margin-bottom: 8px; + } + + .chart-title { + font-size: 13px; + } +} + +/* Mobile Quick Status Bar */ +.mobile-quick-status { + display: none; +} + +/* Desktop Status Bar */ +.desktop-status-bar { + display: block; +} + +/* Sidebar Toggle Button */ +.sidebar-toggle-btn { + display: none; + background: none; + border: none; + color: #6b7280; + cursor: pointer; + padding: 4px; + margin-left: auto; + transition: color 0.2s ease; +} + +.sidebar-toggle-btn:hover { + color: #374151; +} + +/* Mobile (below 768px) */ +@media (max-width: 767px) { + .desktop-status-bar { + display: none !important; + } + + .mobile-quick-status { + display: flex; + align-items: center; + justify-content: space-around; + padding: 6px 8px; + background-color: #f9fafb; + border-bottom: 1px solid #e5e7eb; + gap: 4px; + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .quick-status-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + flex-shrink: 0; + } + + .status-dot-mini { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .status-dot-mini.healthy { + background-color: #10b981; + box-shadow: 0 0 4px rgba(16, 185, 129, 0.5); + } + + .status-dot-mini.unhealthy { + background-color: #ef4444; + box-shadow: 0 0 4px rgba(239, 68, 68, 0.5); + } + + .quick-status-label { + font-size: 9px; + color: #6b7280; + font-weight: 500; + white-space: nowrap; + text-align: center; + } + + .app-body { + flex-direction: column; + height: auto; + overflow: visible; + } + + .app-sidebar { + width: 100%; + height: auto; + max-height: none; + border-right: none; + border-bottom: 1px solid #e1e5e9; + padding: 8px; + overflow: visible; + overflow-x: hidden; + flex-shrink: 0; + -webkit-overflow-scrolling: touch; + transition: max-height 0.3s ease; + } + + .sidebar-toggle-btn { + display: inline-flex; + } + + .app-sidebar:not(.expanded) { + max-height: 60px; + overflow: hidden; + } + + .app-sidebar:not(.expanded) .device-info:not(:first-child) { + display: none; + } + + .app-sidebar:not(.expanded) .info-item:nth-child(n + 4) { + display: none; + } + + .app-sidebar.expanded { + max-height: 600px; + overflow-y: auto; + } + + .device-info { + margin-top: 8px; + } + + .device-info:first-child { + margin-top: 0; + } + + .info-item { + padding: 4px 0; + font-size: 12px; + } + + .app-main { + flex: 1; + padding: 12px 8px 150px 8px; + overflow-y: auto; + overflow-x: hidden; + height: auto; + min-height: 0; + max-height: none; + -webkit-overflow-scrolling: touch; + } + + .kpi-row { + grid-template-columns: repeat(2, 1fr); + gap: 6px; + margin-bottom: 10px; + } + + .card { + padding: 6px 8px; + min-height: 75px; + } + + .card-header { + margin-bottom: 3px; + } + + .card-value { + font-size: 18px; + line-height: 1.1; + margin-bottom: 2px; + } + + .card-title { + font-size: 10px; + letter-spacing: 0.3px; + } + + .card-sub { + font-size: 9px; + line-height: 1.3; + margin-top: 2px; + } + + .app-header { + padding: 8px 10px 10px 10px !important; + height: auto !important; + flex-direction: column !important; + gap: 10px !important; + align-items: stretch !important; + position: static !important; + top: auto !important; + z-index: auto !important; + backdrop-filter: none !important; + } + + .app-header-left { + justify-content: center; + padding-bottom: 4px; + } + + .app-header-title { + font-size: 14px; + } + + .app-header-right { + flex-direction: row; + gap: 6px; + align-items: center; + justify-content: space-between; + width: 100%; + padding-bottom: 2px; + } + + .dashboard-nav { + gap: 4px; + flex: 1; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .nav-link { + padding: 7px 8px; + font-size: 11px; + gap: 4px; + flex-shrink: 0; + justify-content: center; + border-radius: 4px; + } + + .nav-link svg { + width: 14px; + height: 14px; + } + + .nav-badge { + min-width: 16px; + height: 16px; + font-size: 10px; + padding: 0 4px; + margin-left: 2px; + } + + .app-last-update { + font-size: 10px; + } + + .app-status { + font-size: 9px; + padding: 1px 6px; + } + + .btn { + padding: 7px 8px; + font-size: 10px; + gap: 3px; + white-space: nowrap; + border-radius: 4px; + flex-shrink: 0; + } + + .btn svg { + width: 12px; + height: 12px; + } + + .chart-container { + padding: 8px; + margin-bottom: 10px; + } + + .chart-title { + font-size: 11px; + font-weight: 600; + } + + .chart-header { + margin-bottom: 6px; + padding-bottom: 4px; + } + + .chart-container:last-child { + margin-bottom: 100px; + } + + /* File browser mobile */ + .file-browser { + flex-direction: column; + } + + .file-tree { + width: 100%; + max-height: 150px; + border-right: none; + border-bottom: 1px solid #e5e7eb; + } + + .file-list-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } + + /* Video viewer mobile */ + .video-grid.layout-6 { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + } + + .video-controls { + flex-wrap: wrap; + gap: 0.5rem; + padding: 8px 12px; + } + + .video-time { + font-size: 11px; + min-width: 80px; + } + + /* Log viewer mobile */ + .log-viewer { + height: 300px; + } + + .log-viewer-content { + font-size: 10px; + } +} + +/* iPhone Pro Max and similar (393px - 430px) */ +@media (min-width: 393px) and (max-width: 430px) { + .kpi-row { + grid-template-columns: 1fr; + } + + .card { + min-height: 80px; + } + + .app-main { + padding-bottom: 140px; + } +} + +/* Smaller phones (below 393px) */ +@media (max-width: 392px) { + .kpi-row { + grid-template-columns: 1fr; + } + + .app-header { + padding: 6px 8px; + height: 45px; + } + + .app-header-title { + font-size: 13px; + } + + .app-main { + padding: 6px; + padding-bottom: 160px; + } + + .card { + padding: 6px; + min-height: 70px; + } + + .card-value { + font-size: 14px; + } + + .chart-title { + font-size: 11px; + } +} + +/* Touch-friendly improvements */ +@media (pointer: coarse) { + .btn { + min-height: 44px; + min-width: 44px; + } + + .info-item { + padding: 8px 0; + min-height: 44px; + align-items: center; + } + + .card { + min-height: 100px; + } + + .nav-link { + min-height: 44px; + } +} + +/* High DPI displays */ +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .card { + border-width: 0.5px; + } + + .chart-container { + border-width: 0.5px; + } + + .app-header { + border-bottom-width: 0.5px; + } +} + +/* Landscape orientation for tablets */ +@media (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) { + .kpi-row { + grid-template-columns: repeat(3, 1fr); + } + + .app-sidebar { + width: 200px; + max-height: calc(100vh - 60px); + overflow-y: auto; + } + + .device-info { + margin-top: 0.75rem; + } + + .device-info:first-child { + margin-top: 0; + } +} + +/* Portrait orientation for tablets */ +@media (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) { + .kpi-row { + grid-template-columns: repeat(2, 1fr); + } + + .app-sidebar { + width: 240px; + max-height: calc(100vh - 60px); + overflow-y: auto; + } + + .device-info { + margin-top: 0.75rem; + } + + .device-info:first-child { + margin-top: 0; + } +} + +/* Ensure no scrolling on dashboard */ +.app-dashboard { + max-height: 100vh; + overflow: hidden; +} + +/* Hide scrollbar for nav on mobile but allow scrolling */ +.dashboard-nav::-webkit-scrollbar { + display: none; +} + +.dashboard-nav { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Tab content */ +.tab-content { + display: none; + flex: 1; + overflow: hidden; +} + +.tab-content.active { + display: flex; + flex-direction: column; +} + +/* Utility classes */ +.hidden { + display: none !important; +} + +.flex { + display: flex; +} + +.flex-1 { + flex: 1; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-sm { + font-size: 12px; +} + +.text-xs { + font-size: 10px; +} + +.text-muted { + color: #6b7280; +} + +.font-medium { + font-weight: 500; +} + +.font-semibold { + font-weight: 600; +} + +.font-bold { + font-weight: 700; +} diff --git a/teslausb-www-react/vite.config.js b/teslausb-www-react/vite.config.js new file mode 100644 index 00000000..33b9902f --- /dev/null +++ b/teslausb-www-react/vite.config.js @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import preact from '@preact/preset-vite'; + +export default defineConfig({ + plugins: [preact()], + build: { + outDir: 'dist', + // Use esbuild for minification (faster, no extra dependency) + minify: 'esbuild', + // Target older browsers for maximum compatibility + target: 'es2018', + // Generate smaller chunks for Raspberry Pi + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, + // Base path for deployment + // Set VITE_BASE_PATH=/react/ for tarball releases, defaults to / for deploy.sh + base: process.env.VITE_BASE_PATH || '/', +}); diff --git a/teslausb-www/html/cgi-bin/music_sync_progress.sh b/teslausb-www/html/cgi-bin/music_sync_progress.sh new file mode 100755 index 00000000..9f57fe8a --- /dev/null +++ b/teslausb-www/html/cgi-bin/music_sync_progress.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Returns current music sync progress by parsing rsync output + +LOG="/tmp/rsyncmusiclog.txt" + +# Default values +active="false" +bytes="0" +percentage="0" +speed="" +eta="" + +# Check if music sync is active (rsync process for music) +if pgrep -f "rsync.*music" > /dev/null 2>&1; then + active="true" + + # Get last progress line from rsync --info=progress2 output + # Format: " 1,234,567,890 45% 12.34MB/s 0:01:23" + # Or: "1695349328 6% 1.07MB/s 6:27:37" + if [ -f "$LOG" ]; then + # Get the last progress update from rsync --info=progress2 output + # rsync uses \r for in-place updates, so the latest progress may not have a trailing newline + # Use tail -c to get the last chunk, then extract the most recent progress line + progress_line=$(tail -c 500 "$LOG" 2>/dev/null | tr '\r' '\n' | grep -E '[0-9]+%' | tail -1) + + if [ -n "$progress_line" ]; then + # Parse using more flexible pattern matching + # Extract bytes: first number (with possible commas) + bytes=$(echo "$progress_line" | grep -oE '^[[:space:]]*[0-9,]+' | tr -d ', ') + + # Extract percentage: number followed by % + percentage=$(echo "$progress_line" | grep -oE '[0-9]+%' | head -1 | tr -d '%') + + # Extract speed: number followed by unit/s (e.g., 1.07MB/s, 500kB/s) + speed=$(echo "$progress_line" | grep -oE '[0-9.]+[kMGT]?B/s' | head -1) + + # Extract ETA: time format like 0:01:23 or 6:27:37 + eta=$(echo "$progress_line" | grep -oE '[0-9]+:[0-9]+:[0-9]+' | head -1) + + # Ensure we have valid defaults + [ -z "$bytes" ] && bytes="0" + [ -z "$percentage" ] && percentage="0" + fi + fi +fi + +# Output JSON +echo "HTTP/1.0 200 OK" +echo "Content-type: application/json" +echo "" +echo "{" +echo " \"active\": $active," +echo " \"bytesTransferred\": $bytes," +echo " \"percentage\": $percentage," +echo " \"speed\": \"$speed\"," +echo " \"eta\": \"$eta\"" +echo "}" diff --git a/teslausb-www/html/cgi-bin/status.sh b/teslausb-www/html/cgi-bin/status.sh index fa981dbc..62b121b0 100755 --- a/teslausb-www/html/cgi-bin/status.sh +++ b/teslausb-www/html/cgi-bin/status.sh @@ -46,12 +46,26 @@ fi read -r -d ' ' ut < /proc/uptime +# Get device model (works on Raspberry Pi, Radxa, etc.) +device_model=$(tr -d '\0' < /proc/device-tree/model 2>/dev/null || echo "Unknown") + +# Get CPU temperature (check multiple locations for different devices) +cpu_temp="" +if [ -f /sys/class/thermal/thermal_zone0/temp ]; then + # Raspberry Pi and many other devices + cpu_temp=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null) +elif [ -f /sys/class/hwmon/hwmon0/temp1_input ]; then + # Radxa Rock Pi and similar devices + cpu_temp=$(cat /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null) +fi + cat << EOF HTTP/1.0 200 OK Content-type: application/json { - "cpu_temp": "$(cat /sys/class/thermal/thermal_zone0/temp)", + "device_model": "$device_model", + "cpu_temp": "$cpu_temp", "num_snapshots": "$numsnapshots", "snapshot_oldest": "$oldestsnapshot", "snapshot_newest": "$newestsnapshot", diff --git a/teslausb-www/html/cgi-bin/storage.sh b/teslausb-www/html/cgi-bin/storage.sh new file mode 100755 index 00000000..c69273bb --- /dev/null +++ b/teslausb-www/html/cgi-bin/storage.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +cat << EOF +HTTP/1.0 200 OK +Content-type: application/json + +EOF + +# Cache directory for storing last known usage when drives were mounted +CACHE_DIR="/tmp/teslausb-storage-cache" +mkdir -p "$CACHE_DIR" 2>/dev/null + +echo "{" + +first=true + +# Function to output drive info +output_drive() { + local name=$1 + local mount_point=$2 + local backing_file=$3 + local cache_file="$CACHE_DIR/${name}_usage" + + if [ "$first" = false ]; then + echo "," + fi + first=false + + # Check if mount point has actual filesystem mounted (not just the root fs) + mount_info=$(df "$mount_point" 2>/dev/null | tail -1) + mount_device=$(echo "$mount_info" | awk '{print $1}') + + # Check if it's mounted from a backing file or loop device (not root filesystem) + if echo "$mount_device" | grep -qE "(backingfiles|loop)"; then + drive_info=$(df -B1 "$mount_point" 2>/dev/null | tail -1) + if [ -n "$drive_info" ]; then + total=$(echo "$drive_info" | awk '{print $2}') + used=$(echo "$drive_info" | awk '{print $3}') + free=$(echo "$drive_info" | awk '{print $4}') + # Cache the current values for when drive is unmounted + echo "$used $total $free" > "$cache_file" + echo -n " \"$name\": { \"total\": $total, \"used\": $used, \"free\": $free, \"mounted\": true }" + return + fi + fi + + # Fall back to backing file size if exists + if [ -f "$backing_file" ]; then + total=$(stat -c%s "$backing_file" 2>/dev/null) + if [ -n "$total" ]; then + # Check if we have cached usage data from when it was last mounted + if [ -f "$cache_file" ]; then + read cached_used cached_total cached_free < "$cache_file" + # Use cached values if total matches (same drive) + if [ "$cached_total" = "$total" ] || [ -n "$cached_used" ]; then + echo -n " \"$name\": { \"total\": $total, \"used\": $cached_used, \"free\": $cached_free, \"mounted\": false, \"cached\": true }" + return + fi + fi + # No cache available, report allocation only + echo -n " \"$name\": { \"total\": $total, \"used\": null, \"free\": null, \"mounted\": false }" + return + fi + fi + + # Drive not configured + echo -n " \"$name\": null" +} + +# TeslaCam drive +output_drive "cam" "/mnt/cam" "/backingfiles/cam_disk.bin" + +# Music drive +output_drive "music" "/mnt/music" "/backingfiles/music_disk.bin" + +# LightShow drive +output_drive "lightshow" "/mnt/lightshow" "/backingfiles/lightshow_disk.bin" + +# Boombox drive +output_drive "boombox" "/mnt/boombox" "/backingfiles/boombox_disk.bin" + +# Overall backingfiles partition (total storage) +echo "," +bf_info=$(df -B1 /backingfiles 2>/dev/null | tail -1) +if [ -n "$bf_info" ]; then + bf_total=$(echo "$bf_info" | awk '{print $2}') + bf_used=$(echo "$bf_info" | awk '{print $3}') + bf_free=$(echo "$bf_info" | awk '{print $4}') + echo " \"total\": { \"total\": $bf_total, \"used\": $bf_used, \"free\": $bf_free }" +else + echo " \"total\": null" +fi + +echo "}" From 8cb815e2a5186bccdf2f4377a0f770c90331ce20 Mon Sep 17 00:00:00 2001 From: oaquique <30644106+oaquique@users.noreply.github.com> Date: Sun, 14 Dec 2025 16:34:38 -0800 Subject: [PATCH 2/4] Fix diagnostics, video viewer timestamp selector, and other improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix diagnostics generation by properly waiting for CGI response body - Add timestamp selector for all video clip types (Sentry, Saved, Recent) - Fix "No recordings" issue when switching between clip categories - Remove unnecessary 2-second delay in diagnostics loading - Improve CSS for video selector bar styling ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../dist/assets/index-B0yppBFU.js | 5 + .../dist/assets/index-Cyod02f8.css | 1 - .../dist/assets/index-DxhX7Us7.js | 5 - .../dist/assets/index-Vc9in5g7.css | 1 + teslausb-www-react/dist/index.html | 4 +- .../src/components/LogViewer.jsx | 3 +- .../src/components/VideoViewer.jsx | 229 +++++++++++++----- teslausb-www-react/src/services/api.js | 24 +- teslausb-www-react/src/styles/index.css | 29 ++- 9 files changed, 224 insertions(+), 77 deletions(-) create mode 100644 teslausb-www-react/dist/assets/index-B0yppBFU.js delete mode 100644 teslausb-www-react/dist/assets/index-Cyod02f8.css delete mode 100644 teslausb-www-react/dist/assets/index-DxhX7Us7.js create mode 100644 teslausb-www-react/dist/assets/index-Vc9in5g7.css diff --git a/teslausb-www-react/dist/assets/index-B0yppBFU.js b/teslausb-www-react/dist/assets/index-B0yppBFU.js new file mode 100644 index 00000000..9f8bd372 --- /dev/null +++ b/teslausb-www-react/dist/assets/index-B0yppBFU.js @@ -0,0 +1,5 @@ +(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function r(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(o){if(o.ep)return;o.ep=!0;const i=r(o);fetch(o.href,i)}})();var se,N,je,V,Ne,We,He,Ue,pe,ce,de,Z={},Pe=[],fn=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,oe=Array.isArray;function H(e,n){for(var r in n)e[r]=n[r];return e}function me(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function _n(e,n,r){var s,o,i,a={};for(i in n)i=="key"?s=n[i]:i=="ref"?o=n[i]:a[i]=n[i];if(arguments.length>2&&(a.children=arguments.length>3?se.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return ne(e,a,s,o,null)}function ne(e,n,r,s,o){var i={type:e,props:n,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++je:o,__i:-1,__u:0};return o==null&&N.vnode!=null&&N.vnode(i),i}function D(e){return e.children}function te(e,n){this.props=e,this.context=n}function Y(e,n){if(n==null)return e.__?Y(e.__,e.__i+1):null;for(var r;nl&&V.sort(He),e=V.shift(),l=V.length,e.__d&&(r=void 0,s=void 0,o=(s=(n=e).__v).__e,i=[],a=[],n.__P&&((r=H({},s)).__v=s.__v+1,N.vnode&&N.vnode(r),ve(n.__P,r,s,n.__n,n.__P.namespaceURI,32&s.__u?[o]:null,i,o==null?Y(s):o,!!(32&s.__u),a),r.__v=s.__v,r.__.__k[r.__i]=r,qe(i,r,a),s.__e=s.__=null,r.__e!=o&&Oe(r)));ie.__r=0}function Ve(e,n,r,s,o,i,a,l,u,d,_){var c,h,f,g,y,b,v,m=s&&s.__k||Pe,w=n.length;for(u=pn(r,n,m,u,w),c=0;c0?a=e.__k[i]=ne(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,u=i+h,a.__=e,a.__b=e.__b+1,(d=a.__i=mn(a,r,u,c))!=-1&&(c--,(l=r[d])&&(l.__u|=2)),l==null||l.__v==null?(d==-1&&(o>_?h--:o<_&&h++),typeof a.type!="function"&&(a.__u|=4)):d!=u&&(d==u-1?h--:d==u+1?h++:(d>u?h--:h++,a.__u|=4))):e.__k[i]=null;if(c)for(i=0;i<_;i++)(l=r[i])!=null&&(2&l.__u)==0&&(l.__e==s&&(s=Y(l)),Ke(l,l));return s}function ze(e,n,r,s){var o,i;if(typeof e.type=="function"){for(o=e.__k,i=0;o&&i(_?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&(2&d.__u)==0&&l==d.key&&u==d.type)return a}return-1}function Ce(e,n,r){n[0]=="-"?e.setProperty(n,r==null?"":r):e[n]=r==null?"":typeof r!="number"||fn.test(n)?r:r+"px"}function Q(e,n,r,s,o){var i,a;e:if(n=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof s=="string"&&(e.style.cssText=s=""),s)for(n in s)r&&n in r||Ce(e.style,n,"");if(r)for(n in r)s&&r[n]==s[n]||Ce(e.style,n,r[n])}else if(n[0]=="o"&&n[1]=="n")i=n!=(n=n.replace(Ue,"$1")),a=n.toLowerCase(),n=a in e||n=="onFocusOut"||n=="onFocusIn"?a.slice(2):n.slice(2),e.l||(e.l={}),e.l[n+i]=r,r?s?r.u=s.u:(r.u=pe,e.addEventListener(n,i?de:ce,i)):e.removeEventListener(n,i?de:ce,i);else{if(o=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in e)try{e[n]=r==null?"":r;break e}catch(l){}typeof r=="function"||(r==null||r===!1&&n[4]!="-"?e.removeAttribute(n):e.setAttribute(n,n=="popover"&&r==1?"":r))}}function Se(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n.t==null)n.t=pe++;else if(n.t0?e:oe(e)?e.map(Ge):H({},e)}function vn(e,n,r,s,o,i,a,l,u){var d,_,c,h,f,g,y,b=r.props||Z,v=n.props,m=n.type;if(m=="svg"?o="http://www.w3.org/2000/svg":m=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(d=0;d=r.__.length&&r.__.push({}),r.__[e]}function k(e){return X=1,kn(Je,e)}function kn(e,n,r){var s=ye(J++,2);if(s.t=e,!s.__c&&(s.__=[Je(void 0,n),function(l){var u=s.__N?s.__N[0]:s.__[0],d=s.t(u,l);u!==d&&(s.__N=[d,s.__[1]],s.__c.setState({}))}],s.__c=T,!T.__f)){var o=function(l,u,d){if(!s.__c.__H)return!0;var _=s.__c.__H.__.filter(function(h){return!!h.__c});if(_.every(function(h){return!h.__N}))return!i||i.call(this,l,u,d);var c=s.__c.props!==l;return _.forEach(function(h){if(h.__N){var f=h.__[0];h.__=h.__N,h.__N=void 0,f!==h.__[0]&&(c=!0)}}),i&&i.call(this,l,u,d)||c};T.__f=!0;var i=T.shouldComponentUpdate,a=T.componentWillUpdate;T.componentWillUpdate=function(l,u,d){if(this.__e){var _=i;i=void 0,o(l,u,d),i=_}a&&a.call(this,l,u,d)},T.shouldComponentUpdate=o}return s.__N||s.__}function F(e,n){var r=ye(J++,3);!$.__s&&Ze(r.__H,n)&&(r.__=e,r.u=n,T.__H.__h.push(r))}function U(e){return X=5,be(function(){return{current:e}},[])}function be(e,n){var r=ye(J++,7);return Ze(r.__H,n)&&(r.__=e(),r.__H=n,r.__h=e),r.__}function B(e,n){return X=8,be(function(){return e},n)}function wn(){for(var e;e=Ye.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(re),e.__H.__h.forEach(ue),e.__H.__h=[]}catch(n){e.__H.__h=[],$.__e(n,e.__v)}}$.__b=function(e){T=null,Te&&Te(e)},$.__=function(e,n){e&&n.__k&&n.__k.__m&&(e.__m=n.__k.__m),Fe&&Fe(e,n)},$.__r=function(e){$e&&$e(e),J=0;var n=(T=e.__c).__H;n&&(le===T?(n.__h=[],T.__h=[],n.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(n.__h.forEach(re),n.__h.forEach(ue),n.__h=[],J=0)),le=T},$.diffed=function(e){Be&&Be(e);var n=e.__c;n&&n.__H&&(n.__H.__h.length&&(Ye.push(n)!==1&&Le===$.requestAnimationFrame||((Le=$.requestAnimationFrame)||Nn)(wn)),n.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),le=T=null},$.__c=function(e,n){n.some(function(r){try{r.__h.forEach(re),r.__h=r.__h.filter(function(s){return!s.__||ue(s)})}catch(s){n.some(function(o){o.__h&&(o.__h=[])}),n=[],$.__e(s,r.__v)}}),Ie&&Ie(e,n)},$.unmount=function(e){Me&&Me(e);var n,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(s){try{re(s)}catch(o){n=o}}),r.__H=void 0,n&&$.__e(n,r.__v))};var Ae=typeof requestAnimationFrame=="function";function Nn(e){var n,r=function(){clearTimeout(s),Ae&&cancelAnimationFrame(n),setTimeout(e)},s=setTimeout(r,35);Ae&&(n=requestAnimationFrame(r))}function re(e){var n=T,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),T=n}function ue(e){var n=T;e.__c=e.__(),T=n}function Ze(e,n){return!e||e.length!==n.length||n.some(function(r,s){return r!==e[s]})}function Je(e,n){return typeof n=="function"?n(e):n}const A="/cgi-bin";async function xn(){const e=await fetch(`${A}/status.sh`);if(!e.ok)throw new Error("Failed to fetch status");return e.json()}async function Cn(){const e=await fetch(`${A}/config.sh`);if(!e.ok)throw new Error("Failed to fetch config");return e.json()}async function Sn(){const e=await fetch(`${A}/storage.sh`);if(!e.ok)throw new Error("Failed to fetch storage");return e.json()}async function Ln(){const e=await fetch(`${A}/videolist.sh`);if(!e.ok)throw new Error("Failed to fetch video list");const n=await e.text();return Tn(n)}function Tn(e){const n=e.trim().split(` +`).filter(Boolean),r={RecentClips:{},SavedClips:{},SentryClips:{}};for(const s of n){const o=s.split("/");if(o.length>=2){const[i,...a]=o;if(r[i]){const l=a[0];r[i][l]||(r[i][l]=[]),a.length>1&&r[i][l].push(a.slice(1).join("/"))}}}return r}async function $n(){if(!(await fetch(`${A}/trigger_sync.sh`)).ok)throw new Error("Failed to trigger sync")}async function Bn(){const e=await fetch(`${A}/music_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch music sync progress");return e.json()}async function In(){if(!(await fetch(`${A}/toggledrives.sh`)).ok)throw new Error("Failed to toggle drives")}async function Mn(){if(!(await fetch(`${A}/reboot.sh`)).ok)throw new Error("Failed to reboot")}async function Fn(){return(await fetch(`${A}/pairBLEkey.sh`)).status===202}async function An(){return(await(await fetch(`${A}/checkBLEstatus.sh`)).text()).includes("

          paired

          ")}async function En(){const e=await fetch(`${A}/diagnose.sh`);if(await e.text(),!e.ok)throw new Error("Failed to generate diagnostics")}async function Rn(){const e=await fetch("/diagnostics.txt");if(!e.ok)throw new Error("Failed to fetch diagnostics");return e.text()}async function Dn(e,n=0){if(n>0){const a=await fetch(`/${e}`,{method:"HEAD"});if(a.ok){const l=parseInt(a.headers.get("Content-Length")||"0",10);if(l<=n)return l0&&(r.Range=`bytes=${n}-`);const s=await fetch(`/${e}`,{headers:r});if(s.status===416)return{content:"",size:n,truncated:!1};if(s.status===404)throw new Error("Log file not found");if(!s.ok&&s.status!==206)throw new Error(`Failed to fetch log: ${s.status}`);const o=await s.text();let i=n;if(s.status===206){const a=s.headers.get("Content-Range");if(a){const l=a.match(/bytes \d+-(\d+)\/(\d+)/);l?i=parseInt(l[1],10)+1:i=n+o.length}else i=n+o.length}else i=o.length;return{content:o,size:i,truncated:!1}}async function jn(e,n){const r=await fetch(`${A}/randomdata.sh`,{signal:n});if(!r.ok)throw new Error("Failed to start speed test");const s=r.body.getReader();let o=0;const i=performance.now();try{for(;;){const{done:l,value:u}=await s.read();if(l)break;o+=u.length;const d=(performance.now()-i)/1e3,_=o*8/(d*1e6);e&&e(_)}}catch(l){if(l.name!=="AbortError")throw l}const a=(performance.now()-i)/1e3;return o*8/(a*1e6)}function Wn(e,n,r){return`/TeslaCam/${e}/${n}/${r}`}function Hn(e){return`/TeslaCam/SentryClips/${e}/event.json`}async function Un(e){try{const n=await fetch(Hn(e));return n.ok?n.json():null}catch(n){return null}}function Pn(e=5e3){const[n,r]=k(null),[s,o]=k(null),[i,a]=k(null),[l,u]=k(!0),[d,_]=k(null),[c,h]=k(null),f=B(async()=>{try{const[y,b,v]=await Promise.all([xn(),Cn(),Sn().catch(()=>null)]);r(y),o(b),a(v),h(new Date),_(null)}catch(y){_(y.message)}finally{u(!1)}},[]);F(()=>{f();const y=setInterval(f,e);return()=>clearInterval(y)},[f,e]);const g=n?{cpuTempC:n.cpu_temp?(parseInt(n.cpu_temp,10)/1e3).toFixed(1):null,uptimeFormatted:On(parseInt(n.uptime||"0",10)),diskUsedPercent:n.total_space&&n.free_space?Math.round((parseInt(n.total_space,10)-parseInt(n.free_space,10))/parseInt(n.total_space,10)*100):0,diskUsedGB:n.total_space&&n.free_space?((parseInt(n.total_space,10)-parseInt(n.free_space,10))/(1024*1024*1024)).toFixed(1):"0",diskTotalGB:n.total_space?(parseInt(n.total_space,10)/(1024*1024*1024)).toFixed(1):"0",diskFreeGB:n.free_space?(parseInt(n.free_space,10)/(1024*1024*1024)).toFixed(1):"0",drivesActive:n.drives_active==="yes",wifiConnected:!!n.wifi_ssid&&n.wifi_ssid!=="",wifiSignalPercent:Vn(n.wifi_strength),ethernetConnected:!!n.ether_ip&&n.ether_ip!=="",snapshotCount:parseInt(n.num_snapshots||"0",10)}:null;return{status:n,config:s,storage:i,computed:g,loading:l,error:d,lastUpdate:c,refresh:f}}function On(e){if(!e||isNaN(e))return"0s";const n=Math.floor(e/86400),r=Math.floor(e%86400/3600),s=Math.floor(e%3600/60),o=e%60,i=[];return n>0&&i.push(`${n}d`),r>0&&i.push(`${r}h`),s>0&&i.push(`${s}m`),(o>0||i.length===0)&&i.push(`${o}s`),i.join(" ")}function Vn(e){if(!e)return 0;const n=e.split("/");if(n.length!==2)return 0;const[r,s]=n.map(Number);return isNaN(r)||isNaN(s)||s===0?0:Math.round(r/s*100)}function zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2-4H8L6 10l-2.5 1.1C2.7 11.3 2 12.1 2 13v3c0 .6.4 1 1 1h2"}),t("circle",{cx:"7",cy:"17",r:"2"}),t("circle",{cx:"17",cy:"17",r:"2"})]})}function Xe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"})]})}function qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"23 7 16 12 23 17 23 7"}),t("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2",ry:"2"})]})}function Gn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}function fe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"23 4 23 10 17 10"}),t("path",{d:"M20.49 15a9 9 0 1 1-2.12-9.36L23 10"})]})}function Kn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:t("polygon",{points:"5 3 19 12 5 21 5 3"})})}function Yn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:[t("rect",{x:"6",y:"4",width:"4",height:"16"}),t("rect",{x:"14",y:"4",width:"4",height:"16"})]})}function Zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"19 20 9 12 19 4 19 20"}),t("line",{x1:"5",y1:"19",x2:"5",y2:"5"})]})}function Jn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"5 4 15 12 5 20 5 4"}),t("line",{x1:"19",y1:"5",x2:"19",y2:"19"})]})}function Xn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),t("polyline",{points:"7 10 12 15 17 10"}),t("line",{x1:"12",y1:"15",x2:"12",y2:"3"})]})}function Qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"3 6 5 6 21 6"}),t("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}function et({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M5 12.55a11 11 0 0 1 14.08 0"}),t("path",{d:"M1.42 9a16 16 0 0 1 21.16 0"}),t("path",{d:"M8.53 16.11a6 6 0 0 1 6.95 0"}),t("line",{x1:"12",y1:"20",x2:"12.01",y2:"20"})]})}function Ee({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("line",{x1:"22",y1:"12",x2:"2",y2:"12"}),t("path",{d:"M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}),t("line",{x1:"6",y1:"16",x2:"6.01",y2:"16"}),t("line",{x1:"10",y1:"16",x2:"10.01",y2:"16"})]})}function nt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",ry:"2"}),t("rect",{x:"9",y:"9",width:"6",height:"6"}),t("line",{x1:"9",y1:"1",x2:"9",y2:"4"}),t("line",{x1:"15",y1:"1",x2:"15",y2:"4"}),t("line",{x1:"9",y1:"20",x2:"9",y2:"23"}),t("line",{x1:"15",y1:"20",x2:"15",y2:"23"}),t("line",{x1:"20",y1:"9",x2:"23",y2:"9"}),t("line",{x1:"20",y1:"14",x2:"23",y2:"14"}),t("line",{x1:"1",y1:"9",x2:"4",y2:"9"}),t("line",{x1:"1",y1:"14",x2:"4",y2:"14"})]})}function tt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"4",cy:"20",r:"1"}),t("circle",{cx:"20",cy:"20",r:"1"}),t("circle",{cx:"12",cy:"10",r:"1"}),t("path",{d:"M12 3v7"}),t("path",{d:"M4 20v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"}),t("path",{d:"M12 10v9"}),t("path",{d:"M7 10l5-7 5 7"})]})}function rt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 2v6h-6"}),t("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"}),t("path",{d:"M3 22v-6h6"}),t("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"})]})}function it({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M18.36 6.64a9 9 0 1 1-12.73 0"}),t("line",{x1:"12",y1:"2",x2:"12",y2:"12"})]})}function Qe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"})})}function en({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"4 17 10 11 4 5"}),t("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function st({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"12",cy:"12",r:"10"}),t("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),t("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]})}function nn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6 9 12 15 18 9"})})}function ot({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"}),t("circle",{cx:"12",cy:"10",r:"3"})]})}function at({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M9 18V5l12-2v13"}),t("circle",{cx:"6",cy:"18",r:"3"}),t("circle",{cx:"18",cy:"16",r:"3"})]})}function tn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})]})}function lt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19.4 14.9C20.2 13.4 20.6 11.7 20.6 10c0-5-4-9-9-9s-9 4-9 9 4 9 9 9c1.7 0 3.4-.4 4.9-1.2"}),t("path",{d:"M11.6 10l6.4 6.4"})]})}function ct({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2",ry:"2"}),t("line",{x1:"3",y1:"9",x2:"21",y2:"9"}),t("line",{x1:"9",y1:"21",x2:"9",y2:"9"})]})}function dt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"17 1 21 5 17 9"}),t("path",{d:"M3 11V9a4 4 0 0 1 4-4h14"}),t("polyline",{points:"7 23 3 19 7 15"}),t("path",{d:"M21 13v2a4 4 0 0 1-4 4H3"})]})}const ht={dashboard:Xe,viewer:qn,files:Gn,logs:en};function ut({tabs:e,activeTab:n,onTabChange:r,lastUpdate:s,onRefresh:o}){const i=a=>a?a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"";return t("header",{className:"app-header",children:[t("div",{className:"app-header-left",children:t("nav",{className:"dashboard-nav",children:e.map(a=>{const l=ht[a.id]||Xe;return t("button",{className:`nav-link ${n===a.id?"active":""}`,onClick:()=>r(a.id),children:[t(l,{}),t("span",{children:a.label})]},a.id)})})}),t("div",{className:"app-header-right desktop-status-bar",children:[t("span",{className:"app-last-update",style:{marginRight:"12px"},children:s&&`Updated ${i(s)}`}),t("button",{className:"btn",onClick:o,children:[t(fe,{}),t("span",{children:"Refresh"})]})]})]})}function ft({status:e,computed:n,config:r,expanded:s,onToggle:o,onRefresh:i}){const[a,l]=k(!1),[u,d]=k(!1),[_,c]=k(!1),[h,f]=k(null),[g,y]=k(null),b=async()=>{l(!0);try{await In(),setTimeout(i,1e3)}catch(C){console.error("Toggle drives failed:",C)}finally{l(!1)}},v=async()=>{if(confirm("Are you sure you want to restart TeslaUSB?")){d(!0);try{await Mn()}catch(C){console.error("Reboot failed:",C)}}},m=async()=>{c(!0),f(null);const C=new AbortController,S=setTimeout(()=>C.abort(),1e4);try{await jn(L=>f(L.toFixed(1)),C.signal)}catch(L){L.name!=="AbortError"&&console.error("Speed test failed:",L)}finally{clearTimeout(S),c(!1)}},w=async()=>{y("pairing");try{if(await Fn()){for(let S=0;S<60;S++)if(await new Promise(P=>setTimeout(P,2e3)),await An()){y("paired");return}y("timeout")}else y("error")}catch(C){console.error("BLE pairing failed:",C),y("error")}};return t("aside",{className:`app-sidebar ${s?"expanded":""}`,children:[t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(nt,{}),t("span",{children:"System"}),t("button",{className:"sidebar-toggle-btn",onClick:o,children:t(nn,{className:s?"rotate-180":""})})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.device_model)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Model"}),t("span",{className:"info-value small-text",children:e.device_model})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Uptime"}),t("span",{className:"info-value",children:(n==null?void 0:n.uptimeFormatted)||"-"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"CPU Temp"}),t("span",{className:`info-value ${_t(n==null?void 0:n.cpuTempC)}`,children:n!=null&&n.cpuTempC?`${n.cpuTempC}ยฐC`:"-"})]}),t("div",{className:"info-item clickable",onClick:b,children:[t("span",{className:"info-label",children:"USB Drives"}),t("button",{className:`toggle-btn ${n!=null&&n.drivesActive?"active":"danger"}`,disabled:a,children:[a&&t(tt,{style:{width:12,height:12},className:"spinning"}),n!=null&&n.drivesActive?"Disconnect from host":"Connect to host"]})]}),t("div",{className:"info-item clickable",onClick:v,children:[t("span",{className:"info-label",children:"Power"}),t("button",{className:"toggle-btn danger",disabled:u,children:[u&&t(it,{style:{width:12,height:12},className:"spinning"}),"Restart"]})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(et,{}),t("span",{children:"Network"})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.wifi_ssid)&&t(D,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"WiFi SSID"}),t("span",{className:"info-value",children:e.wifi_ssid})]}),(e==null?void 0:e.wifi_freq)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Frequency"}),t("span",{className:"info-value",children:pt(e.wifi_freq)})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Signal"}),t("span",{className:"info-value",children:[(n==null?void 0:n.wifiSignalPercent)||0,"%"]})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.wifi_ip||"-"})]})]}),(e==null?void 0:e.ether_ip)&&t(D,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Ethernet"}),t("span",{className:"info-value",children:e.ether_speed||"Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.ether_ip})]})]}),!(e!=null&&e.wifi_ssid)&&!(e!=null&&e.ether_ip)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Status"}),t("span",{className:"info-value status-unhealthy",children:"Not Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Speed Test"}),t("span",{className:"info-value-with-action",children:[h&&t("span",{className:"speed-result",children:[h," Mbps"]}),t("button",{className:"toggle-btn",onClick:m,disabled:_,children:[_&&t(lt,{style:{width:12,height:12},className:"spinning"}),_?"Testing...":"Run"]})]})]}),(r==null?void 0:r.uses_ble)==="yes"&&t("div",{className:"info-item clickable",onClick:w,children:[t("span",{className:"info-label",children:"Bluetooth"}),t("button",{className:"toggle-btn",disabled:g==="pairing",children:[g==="pairing"&&t(Qe,{style:{width:12,height:12},className:"spinning"}),g==="pairing"?"Pairing...":g==="paired"?"Paired":g==="error"?"Failed":"Pair"]})]})]})]}),(n==null?void 0:n.snapshotCount)>0&&t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(tn,{}),t("span",{children:"Snapshots"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Count"}),t("span",{className:"info-value",children:n.snapshotCount})]}),(e==null?void 0:e.snapshot_oldest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Oldest"}),t("span",{className:"info-value compact-date",children:Re(e.snapshot_oldest)})]}),(e==null?void 0:e.snapshot_newest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Newest"}),t("span",{className:"info-value compact-date",children:Re(e.snapshot_newest)})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(dt,{}),t("span",{children:"Switch UI"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:t("a",{href:"/",className:"ui-switch-link",children:"Standard UI"})}),t("div",{className:"info-item",children:t("a",{href:"/new/",className:"ui-switch-link",children:"Vue UI"})})]})]})]})}function _t(e){if(!e)return"";const n=parseFloat(e);return n>=80?"status-unhealthy":n>=70?"status-degraded":"status-healthy"}function Re(e){if(!e)return"-";try{return new Date(parseInt(e,10)*1e3).toLocaleDateString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch(n){return e}}function pt(e){if(!e)return"-";const n=e.match(/(\d+\.?\d*)/);if(n){const r=parseFloat(n[1]),s=t("span",{style:{fontWeight:"normal"},children:[" (",r.toFixed(3),")"]});return r>=2.4&&r<2.5?t(D,{children:["2.4 GHz",s]}):r>=5&&r<6?t(D,{children:["5 GHz",s]}):r>=6?t(D,{children:["6 GHz",s]}):`${r} GHz`}return e}function rn(e,n=2e3,r=!0){const[s,o]=k([]),[i,a]=k(!0),[l,u]=k(null),d=U(0),_=U(!0),c=U(e),h=B(async(y=!1)=>{if(e)try{y&&(d.current=0);const b=await Dn(e,d.current);if(b.truncated){d.current=0,o([]);return}if(b.content){const v=b.content.split(` +`).filter(Boolean);o(y?v.slice(-1e3):m=>[...m,...v].slice(-1e3))}d.current=b.size,u(null)}catch(b){u(b.message)}finally{a(!1)}},[e]);F(()=>{e!==c.current&&(c.current=e,d.current=0,o([]),a(!0),u(null))},[e]),F(()=>{if(!r||!e)return;h(!0);const y=setInterval(()=>h(!1),n);return()=>clearInterval(y)},[e,n,r]);const f=B(y=>{_.current=y},[]),g=B(()=>{o([]),d.current=0},[]);return{lines:s,loading:i,error:l,autoScroll:_.current,setAutoScroll:f,refresh:()=>h(!1),clear:g}}function mt(e){const n={state:"idle",totalFiles:0,archivedFiles:0,currentFile:null,startTime:null,elapsedTime:null,lastActivity:null,message:null};if(!e||e.length===0)return n;const r=e.slice(-100);r.length>0&&(n.lastActivity=vt(r[r.length-1]));for(let o=r.length-1;o>=0;o--){const i=r[o],a=i.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/);if(a){const u=parseInt(a[2],10),d=a[3]?parseInt(a[3],10):0;n.totalFiles=u+d;break}const l=i.match(/Archiving (\d+)(?: track mode)? file\(s\)/);l&&!i.includes("completed")&&(n.totalFiles=parseInt(l[1],10))}let s=!1;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Finished copying music")||i.includes("Copying music failed"))break;if(i.includes("Syncing music from archive")||i.includes("Starting music sync")){s=!0;break}if(i.includes("Connected usb to host")||i.includes("Waiting for archive to be unreachable"))break}if(s)return n.state="archiving",n.message="Syncing music...",n;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Archiving completed successfully")){n.state="complete",n.message="Archive completed";break}if(i.includes("Finished copying music")){n.state="complete",n.message="Music sync complete";break}if(i.includes("Copied")&&i.includes("file(s)")){const a=i.match(/Copied (\d+)/);if(a){n.state="complete",n.archivedFiles=parseInt(a[1],10),n.message=`Copied ${n.archivedFiles} files`;break}}if(i.includes("Starting recording archiving")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving recordings...";break}if(i.includes("Archiving...")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving files...";break}if(i.includes("Syncing music from archive")){n.state="archiving",n.message="Syncing music...";break}if(i.includes("Copying music")){n.state="archiving",n.message="Copying music...";break}if(i.includes("Finished archiving")){n.state="complete",n.message="Archive complete";break}if(i.includes("Running fsck")){n.state="archiving",n.message="Checking filesystem...";break}if(i.includes("Checking saved folder count")){n.state="archiving",n.message="Scanning files...";break}if(i.includes("Waiting for archive to be reachable")){n.state="connecting",n.message="Connecting to archive server...";break}if(i.includes("Archive is reachable")){n.state="archiving",n.message="Connected, preparing...";break}if(i.includes("Disconnecting usb from host")){n.state="archiving",n.message="Disconnecting from vehicle...";break}if(i.includes("Connected usb to host")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("Waiting for archive to be unreachable")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("snapshot")){n.state="idle",n.message="Managing snapshots...";break}if(i.includes("low space, deleting")){n.state="idle",n.message="Cleaning up old snapshots...";break}if(i.includes("waiting up to")&&i.includes("idle interval")){n.state="idle",n.message="Waiting for idle...";break}if(i.includes("mass storage process")){n.state="idle",n.message="Ready";break}if((i.includes("error")||i.includes("failed"))&&!i.includes("sntp failed")){n.state="error",n.message="Error occurred";break}}return n}function vt(e){const n=e.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/);if(n){const r=new Date(n[1]);if(!isNaN(r.getTime()))return r}return null}function gt(e=!1,n=1500){const[r,s]=k({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""}),[o,i]=k(!1),[a,l]=k(null),u=U(e),d=B(async()=>{try{const _=await Bn();s(_),l(null)}catch(_){l(_.message)}finally{i(!1)}},[]);return F(()=>{u.current=e},[e]),F(()=>{if(!e){s({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""});return}i(!0),d();const _=setInterval(()=>{u.current&&d()},n);return()=>clearInterval(_)},[e,n,d]),{...r,loading:o,error:a,refresh:d}}function yt(e){if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],r=1024,s=Math.floor(Math.log(e)/Math.log(r));return`${(e/Math.pow(r,s)).toFixed(s>0?2:0)} ${n[s]}`}function bt(e){return!e||e==="0:00:00"?"":`~${e.replace(/^0:/,"")} remaining`}function kt({storage:e,total:n,free:r,config:s}){var c;const o=h=>{if(h===0||h===null||h===void 0)return"0 B";const f=h/(1024*1024*1024);return f>=1?`${f.toFixed(1)} GB`:`${(h/(1024*1024)).toFixed(0)} MB`},i=((c=e==null?void 0:e.total)==null?void 0:c.free)||r,a=[],l=(h,f,g,y)=>{(s==null?void 0:s[y])!=="yes"||!h||a.push({type:f,label:g,allocated:h.total,used:h.used,mounted:h.mounted,isCached:h.cached||!1})};if(l(e==null?void 0:e.cam,"teslacam","TeslaCam","has_cam"),l(e==null?void 0:e.music,"music","Music","has_music"),l(e==null?void 0:e.lightshow,"lightshow","LightShow","has_lightshow"),l(e==null?void 0:e.boombox,"boombox","Boombox","has_boombox"),a.length===0&&!i)return t("div",{className:"storage-bar-container",children:t("div",{className:"storage-info",children:"No storage data available"})});const d=a.reduce((h,f)=>h+(f.allocated||0),0)+i,_=a.map(h=>({type:h.type,label:h.label,bytes:h.allocated,percent:Math.max(1,Math.round(h.allocated/d*100))}));return i>0&&_.push({type:"free",label:"Free",bytes:i,percent:Math.max(1,Math.round(i/d*100))}),t("div",{className:"storage-bar-container",children:[t("div",{className:"storage-bar",children:_.map((h,f)=>t("div",{className:`storage-segment ${h.type}`,style:{width:`${h.percent}%`},title:`${h.label}: ${o(h.bytes)}`},f))}),t("div",{className:"storage-legend",children:[a.map((h,f)=>{const g=h.used!==null&&h.allocated>0?Math.round(h.used/h.allocated*100):null;return t("div",{className:"storage-legend-item",children:[t("div",{className:`storage-legend-dot ${h.type}`}),t("span",{className:"storage-legend-label",children:h.label}),t("span",{className:"storage-legend-value",children:[o(h.allocated),h.used!==null&&t("span",{className:"storage-legend-used",children:[" ","(",o(h.used)," used",h.isCached?"~":"",g!==null&&`, ${g}%`,")"]})]})]},f)}),t("div",{className:"storage-legend-item",children:[t("div",{className:"storage-legend-dot free"}),t("span",{className:"storage-legend-label",children:"Free"}),t("span",{className:"storage-legend-value",children:o(i)})]})]}),a.some(h=>h.isCached)&&t("div",{className:"storage-note",children:"~ Last known (drive not currently mounted)"})]})}function wt({syncStatus:e,onTriggerSync:n,loading:r,musicProgress:s}){const{state:o,totalFiles:i,archivedFiles:a,message:l,elapsedTime:u,lastActivity:d}=e,_=(l==null?void 0:l.toLowerCase().includes("music"))&&o==="archiving",c=(s==null?void 0:s.active)&&(s==null?void 0:s.percentage)>0,f=(()=>{switch(o){case"idle":return{label:"Idle",color:"idle",description:"Ready to archive"};case"connecting":return{label:"Connecting",color:"connecting",description:l||"Connecting to server..."};case"archiving":return{label:"Archiving",color:"archiving",description:l||"Syncing files..."};case"complete":return{label:"Complete",color:"idle",description:l||"Archive complete"};case"error":return{label:"Error",color:"error",description:l||"Archive failed"};default:return{label:"Unknown",color:"idle",description:"Status unknown"}}})(),g=o==="archiving"||o==="connecting",y=_&&c,b=o==="archiving"&&i>0&&!y;let v=null;y?v=s.percentage:b&&a>0&&(v=Math.min(Math.round(a/i*100),100));const m=w=>{if(!w)return null;const S=Math.floor((new Date-w)/1e3);return S<60?"just now":S<3600?`${Math.floor(S/60)}m ago`:S<86400?`${Math.floor(S/3600)}h ago`:w.toLocaleDateString()};return t("div",{className:"sync-status-card",children:[t("div",{className:"sync-status-header",children:[t("span",{className:"sync-status-title",children:"Sync Status"}),t("div",{className:"sync-status-indicator",children:[t("div",{className:`sync-status-dot ${f.color}`}),t("span",{children:f.label})]})]}),t("div",{className:"sync-status-description",children:f.description}),(y||b||g)&&t("div",{className:"sync-progress-bar",children:t("div",{className:"sync-progress-fill",style:{width:v!==null?`${v}%`:"100%",animation:v===null?"pulse 1.5s ease-in-out infinite":"none",opacity:v===null?.6:1}})}),y&&t("div",{className:"sync-details",children:[t("div",{className:"sync-progress-main",children:[yt(s.bytesTransferred)," transferred",v!==null&&` (${v}%)`]}),(s.speed||s.eta)&&t("div",{className:"sync-progress-secondary",children:[s.speed&&t("span",{children:s.speed}),s.speed&&s.eta&&t("span",{children:" ยท "}),s.eta&&t("span",{children:bt(s.eta)})]})]}),b&&t("div",{className:"sync-details",children:[a," / ",i," files",v!==null&&` (${v}%)`]}),!g&&!b&&!y&&d&&t("div",{className:"sync-details",children:["Last sync: ",m(d)]}),u&&o==="complete"&&t("div",{className:"sync-details",children:["Completed in ",u]}),(o==="idle"||o==="complete"||o==="error")&&t("button",{className:"btn btn-primary btn-sm",onClick:n,disabled:r,style:{marginTop:"0.75rem"},children:[t(rt,{style:{width:14,height:14},className:r?"spinning":""}),t("span",{children:r?"Starting...":"Sync Now"})]})]})}function Nt({status:e,computed:n,config:r,storage:s,onRefresh:o}){const[i,a]=k(!1),{lines:l}=rn("archiveloop.log",3e3,!0),u=mt(l),d=be(()=>{var f;return u.state==="archiving"&&((f=u.message)==null?void 0:f.toLowerCase().includes("music"))},[u.state,u.message]),_=gt(d,1500),c=B(async()=>{a(!0);try{await $n(),setTimeout(o,1e3)}catch(f){console.error("Trigger sync failed:",f)}finally{a(!1)}},[o]),h=[{key:"cam",label:"TeslaCam",icon:tn,enabled:(r==null?void 0:r.has_cam)==="yes"},{key:"music",label:"Music",icon:at,enabled:(r==null?void 0:r.has_music)==="yes"},{key:"lightshow",label:"LightShow",icon:Ee,enabled:(r==null?void 0:r.has_lightshow)==="yes"},{key:"boombox",label:"Boombox",icon:Ee,enabled:(r==null?void 0:r.has_boombox)==="yes"}];return(r==null?void 0:r.uses_ble)==="yes"&&h.push({key:"ble",label:"BLE",icon:Qe,enabled:!0}),t("div",{className:"dashboard-content",children:[t("div",{className:"dashboard-section",children:[t("div",{className:"section-title",children:"Configured Features"}),t("div",{className:"features-row",children:h.map(({key:f,label:g,enabled:y})=>t("div",{className:`feature-badge ${y?"enabled":"disabled"}`,children:g},f))})]}),t("div",{className:"dashboard-section",children:t("div",{className:"card",children:[t("div",{className:"card-header",children:[t("span",{className:"card-title",children:"Storage"}),t("span",{className:"card-value",children:[(n==null?void 0:n.diskTotalGB)||"0"," GB"]})]}),t(kt,{storage:s,total:e!=null&&e.total_space?parseInt(e.total_space,10):0,free:e!=null&&e.free_space?parseInt(e.free_space,10):0,config:r})]})}),t("div",{className:"dashboard-section",children:t(wt,{syncStatus:u,onTriggerSync:c,loading:i,musicProgress:_})})]})}const _e={front:"Front",back:"Back",left_repeater:"Left Repeater",right_repeater:"Right Repeater",left_pillar:"Left Pillar",right_pillar:"Right Pillar"},De=[{id:"6",name:"All Cameras",cameras:Object.keys(_e),cols:3},{id:"4-front",name:"Front Focus",cameras:["front","left_repeater","right_repeater","back"],cols:2},{id:"4-rear",name:"Rear Focus",cameras:["back","left_repeater","right_repeater","front"],cols:2},{id:"2-side",name:"Side View",cameras:["left_repeater","right_repeater"],cols:2},{id:"1-front",name:"Front Only",cameras:["front"],cols:1},{id:"1-back",name:"Rear Only",cameras:["back"],cols:1}];function xt(){const[e,n]=k(null),[r,s]=k(!0),[o,i]=k(null),[a,l]=k("SentryClips"),[u,d]=k(null),[_,c]=k(null),[h,f]=k(!1),[g,y]=k(0),[b,v]=k(0),[m,w]=k(De[0]),[C,S]=k(!1),[L,P]=k(null),W=U({}),E=U(null);F(()=>{z()},[]);const z=async()=>{try{s(!0);const p=await Ln();n(p);for(const x of["SentryClips","SavedClips","RecentClips"]){const I=Object.keys(p[x]||{});if(I.length>0){l(x),d(I[0]);break}}}catch(p){i(p.message)}finally{s(!1)}};F(()=>{E.current=null,y(0),v(0),a==="SentryClips"&&u?Un(u).then(P):P(null)},[a,u]);const M=B(()=>{var O;if(!e||!u)return[];const p=((O=e[a])==null?void 0:O[u])||[],x=new Set;for(const G of p){const K=G.match(/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})/);K&&x.add(K[1])}const I=Array.from(x).sort().reverse();return I.length>1?I:[]},[e,a,u])();F(()=>{M.length>0&&!_&&c(M[0])},[M,_]);const ae=B(()=>{var O;if(!e||!u)return{};const p=((O=e[a])==null?void 0:O[u])||[],x={},I=M.length>0?_:null;for(const G of p)if(!(I&&!G.startsWith(I))){for(const K of Object.keys(_e))if(G.includes(`-${K}.mp4`)||G.includes(`_${K}.mp4`)){x[K]=Wn(a,u,G);break}}return x},[e,a,u,_,M])(),sn=B(()=>{Object.values(W.current).forEach(p=>{p&&p.play()}),f(!0)},[]),ke=B(()=>{Object.values(W.current).forEach(p=>{p&&p.pause()}),f(!1)},[]),q=B(p=>{Object.values(W.current).forEach(x=>{x&&(x.currentTime=p)}),y(p)},[]),on=B(()=>{q(Math.max(0,g-10))},[g,q]),an=B(()=>{q(Math.min(b,g+30))},[g,b,q]),ln=B(p=>x=>{p===E.current&&y(x.target.currentTime)},[]),cn=B(p=>x=>{const I=x.target.duration;I&&I!==1/0&&!isNaN(I)&&(E.current||(E.current=p),p===E.current&&v(I))},[]),dn=B(p=>{const x=p.currentTarget.getBoundingClientRect(),O=(p.clientX-x.left)/x.width*b;q(O)},[b,q]),we=p=>{if(!p||isNaN(p))return"0:00";const x=Math.floor(p/60),I=Math.floor(p%60);return`${x}:${I.toString().padStart(2,"0")}`},hn=e!=null&&e[a]?Object.keys(e[a]).sort().reverse():[];if(r)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"Loading recordings..."})});if(o)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:[t("span",{className:"status-unhealthy",children:["Error: ",o]}),t("button",{className:"btn",onClick:z,style:{marginTop:"1rem"},children:"Retry"})]});const un=u&&Object.keys(ae).length>0;return t("div",{className:"video-viewer",children:[t("div",{className:"video-selector",style:{display:"flex",gap:"0.5rem",padding:"8px 12px",background:"#1a1a1a",borderBottom:"1px solid #333",alignItems:"center",flexWrap:"wrap"},children:[t("select",{value:a,onChange:p=>{l(p.target.value);const x=Object.keys(e[p.target.value]||{});d(x[0]||null),c(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px"},children:[t("option",{value:"SentryClips",children:"Sentry Clips"}),t("option",{value:"SavedClips",children:"Saved Clips"}),t("option",{value:"RecentClips",children:"Recent Clips"})]}),t("select",{value:u||"",onChange:p=>{d(p.target.value),c(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",flex:1,maxWidth:"180px"},children:hn.map(p=>t("option",{value:p,children:Ct(p)},p))}),M.length>0&&t("select",{value:_||"",onChange:p=>c(p.target.value),style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",maxWidth:"120px"},children:M.map(p=>t("option",{value:p,children:St(p)},p))}),t("div",{style:{position:"relative"},children:[t("button",{className:"btn btn-sm btn-dark",onClick:()=>S(!C),children:[t(ct,{}),t("span",{children:m.name}),t(nn,{})]}),C&&t("div",{style:{position:"absolute",top:"100%",right:0,background:"#2a2a2a",border:"1px solid #444",borderRadius:"6px",marginTop:"4px",zIndex:100,minWidth:"150px"},children:De.map(p=>t("button",{onClick:()=>{w(p),S(!1)},style:{display:"block",width:"100%",padding:"8px 12px",background:m.id===p.id?"#0095f6":"transparent",color:"#fff",border:"none",textAlign:"left",cursor:"pointer",fontSize:"13px"},children:p.name},p.id))})]}),L&&L.city&&t("div",{style:{display:"flex",alignItems:"center",gap:"4px",color:"#888",fontSize:"12px",marginLeft:"auto"},children:[t(ot,{style:{width:14,height:14}}),t("span",{children:L.city})]})]}),un?t(D,{children:[t("div",{className:`video-grid layout-${m.cameras.length}`,style:{gridTemplateColumns:`repeat(${m.cols}, 1fr)`},children:m.cameras.map(p=>t("div",{className:"video-cell",children:[ae[p]?t("video",{ref:x=>{W.current[p]=x},src:ae[p],onTimeUpdate:ln(p),onDurationChange:cn(p),onEnded:ke,muted:!0,playsInline:!0}):t("div",{style:{color:"#666",fontSize:"12px"},children:"No video"}),t("div",{className:"video-cell-label",children:_e[p]})]},p))}),t("div",{className:"video-controls",children:[t("button",{className:"btn btn-sm",onClick:on,title:"Skip back 10s",children:t(Zn,{})}),t("button",{className:"video-play-btn",onClick:h?ke:sn,children:h?t(Yn,{}):t(Kn,{})}),t("button",{className:"btn btn-sm",onClick:an,title:"Skip forward 30s",children:t(Jn,{})}),t("div",{className:"video-time",children:[we(g)," / ",we(b)]}),t("div",{className:"video-timeline",onClick:dn,children:t("div",{className:"video-timeline-progress",style:{width:b>0?`${g/b*100}%`:"0%"}})})]})]}):t("div",{style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",color:"#888",fontSize:"14px"},children:["No recordings in ",a==="SentryClips"?"Sentry Clips":a==="SavedClips"?"Saved Clips":"Recent Clips"]})]})}function Ct(e){const n=e.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/);if(n){const[,r,s,o,i,a,l]=n,u=new Date(r,s-1,o,i||0,a||0,l||0);return i?u.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):u.toLocaleDateString([],{month:"short",day:"numeric",year:"numeric"})}return e}function St(e){const n=e.match(/\d{4}-\d{2}-\d{2}_(\d{2})-(\d{2})-(\d{2})/);if(n){const[,r,s]=n;return new Date(2e3,0,1,parseInt(r),parseInt(s)).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}return e}function Lt({config:e}){const n=U(null),r=U(null),[s,o]=k(!!window.FileBrowser);F(()=>{if(!document.querySelector('link[href="/filebrowser.css"]')){const d=document.createElement("link");d.rel="stylesheet",d.href="/filebrowser.css",document.head.appendChild(d)}},[]),F(()=>{if(window.FileBrowser){o(!0);return}const d=document.createElement("script");d.src="/filebrowser.js",d.onload=()=>o(!0),d.onerror=()=>console.error("Failed to load filebrowser.js"),document.head.appendChild(d)},[]);const i=(e==null?void 0:e.has_music)==="yes",a=(e==null?void 0:e.has_lightshow)==="yes",l=(e==null?void 0:e.has_boombox)==="yes";return F(()=>{if(!s||!n.current)return;const d=[];if(i&&d.push({path:"fs/Music",label:"Music"}),a&&d.push({path:"fs/LightShow",label:"LightShow"}),l&&d.push({path:"fs/Boombox",label:"Boombox"}),d.length!==0&&!r.current){try{r.current=new window.FileBrowser(n.current,d)}catch(_){console.error("Failed to initialize FileBrowser:",_)}return()=>{n.current&&(n.current.innerHTML=""),r.current=null}}},[s,i,a,l]),(e==null?void 0:e.has_music)==="yes"||(e==null?void 0:e.has_lightshow)==="yes"||(e==null?void 0:e.has_boombox)==="yes"?s?t("div",{ref:n,style:{width:"100%",height:"calc(100% - 4px)",minHeight:"400px",flex:1,display:"flex",position:"relative"}}):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",minHeight:"400px",color:"#9ca3af",fontSize:"14px"},children:"Loading file browser..."}):t("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:"#9ca3af",fontSize:"14px"},children:[t("svg",{style:{width:32,height:32,marginBottom:8,color:"#d1d5db"},viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})}),t("span",{children:"No file drives configured"})]})}const ee={archiveloop:{label:"Archive Log",file:"archiveloop.log",description:"Sync and archive operations"},setup:{label:"Setup Log",file:"teslausb-headless-setup.log",description:"Initial setup and configuration"},diagnostics:{label:"Diagnostics",file:"diagnostics.txt",description:"System diagnostics report"}};function Tt(){const[e,n]=k("archiveloop"),[r,s]=k(!1),[o,i]=k(null),a=U(null),l=e!=="diagnostics",{lines:u,loading:d,error:_,refresh:c,clear:h}=rn(l?ee[e].file:"",2e3,l);F(()=>{a.current&&l&&(a.current.scrollTop=a.current.scrollHeight)},[u,l]);const f=B(async()=>{s(!0),i(null);try{await En();const m=await Rn();i(m)}catch(m){i(`Error generating diagnostics: ${m.message}`)}finally{s(!1)}},[]);F(()=>{e==="diagnostics"&&!o&&f()},[e,o,f]);const g=B(()=>{const m=e==="diagnostics"?o:u.join(` +`),w=ee[e].file,C=new Blob([m],{type:"text/plain"}),S=URL.createObjectURL(C),L=document.createElement("a");L.href=S,L.download=w,document.body.appendChild(L),L.click(),document.body.removeChild(L),URL.revokeObjectURL(S)},[e,u,o]),y=m=>{const w=m.toLowerCase();return w.includes("error")||w.includes("failed")||w.includes("fatal")?"error":w.includes("warning")||w.includes("warn")?"warning":w.includes("success")||w.includes("completed")||w.includes("finished")?"success":""},b=ee[e],v=e==="diagnostics"?(o==null?void 0:o.split(` +`))||[]:u;return t("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[t("div",{style:{display:"flex",gap:"0.5rem",marginBottom:"1rem",flexWrap:"wrap"},children:Object.entries(ee).map(([m,w])=>t("button",{className:`btn ${e===m?"btn-primary":""}`,onClick:()=>n(m),children:[m==="diagnostics"?t(st,{}):t(en,{}),t("span",{children:w.label})]},m))}),t("div",{className:"log-viewer",style:{flex:1},children:[t("div",{className:"log-viewer-header",children:[t("div",{className:"log-viewer-title",children:[b.label,t("span",{style:{fontWeight:400,marginLeft:"8px",opacity:.7},children:["โ€” ",b.description]})]}),t("div",{className:"log-viewer-actions",children:[e==="diagnostics"?t("button",{className:"btn btn-sm log-action-btn",onClick:f,disabled:r,children:[t(fe,{className:r?"spinning":""}),t("span",{children:"Regenerate"})]}):t(D,{children:[t("button",{className:"btn btn-sm log-action-btn",onClick:c,disabled:d,title:"Refresh",children:t(fe,{className:d?"spinning":""})}),t("button",{className:"btn btn-sm log-action-btn",onClick:h,title:"Clear",children:t(Qn,{})})]}),t("button",{className:"btn btn-sm log-action-btn",onClick:g,disabled:v.length===0,title:"Download",children:t(Xn,{})})]})]}),t("div",{className:"log-viewer-content",ref:a,children:d&&v.length===0||r?t("div",{style:{color:"#888",fontStyle:"italic"},children:"Loading..."}):_?_.includes("not found")?t("div",{style:{color:"#888",fontStyle:"italic"},children:["Log file not available. ",e==="setup"&&"The setup log is only present during initial setup."]}):t("div",{style:{color:"#f87171"},children:["Error: ",_]}):v.length===0?t("div",{style:{color:"#888",fontStyle:"italic"},children:"No log entries"}):v.map((m,w)=>t("div",{className:`log-line ${y(m)}`,children:m},w))})]}),l&&u.length>0&&t("div",{style:{marginTop:"0.5rem",fontSize:"11px",color:"#666",display:"flex",gap:"1rem"},children:[t("span",{children:[u.length," lines"]}),t("span",{children:"Auto-refreshing every 2s"})]})]})}function $t(){return t("div",{className:"loading-container",children:[t("div",{className:"spinner"}),t("span",{children:"Loading..."})]})}const j={DASHBOARD:"dashboard",VIEWER:"viewer",FILES:"files",LOGS:"logs"};function Bt(){const[e,n]=k(j.DASHBOARD),[r,s]=k(!1),{status:o,config:i,storage:a,computed:l,loading:u,error:d,lastUpdate:_,refresh:c}=Pn(5e3),h=[];return h.push({id:j.DASHBOARD,label:"Dashboard"}),(i==null?void 0:i.has_cam)==="yes"&&h.push({id:j.VIEWER,label:"Viewer"}),((i==null?void 0:i.has_music)==="yes"||(i==null?void 0:i.has_lightshow)==="yes"||(i==null?void 0:i.has_boombox)==="yes")&&h.push({id:j.FILES,label:"Files"}),h.push({id:j.LOGS,label:"Logs"}),u&&!o?t($t,{}):d&&!o?t("div",{className:"error-container",children:[t("span",{children:["Failed to load: ",d]}),t("button",{className:"retry-btn",onClick:c,children:"Retry"})]}):t("div",{className:"app-shell",children:[t("div",{className:"app-topbar",children:[t("div",{className:"app-topbar-title",children:[t(zn,{}),t("span",{children:"TeslaUSB"})]}),t("div",{className:"app-topbar-actions",children:t("span",{className:`app-status ${l!=null&&l.drivesActive?"healthy":"warning"}`,children:l!=null&&l.drivesActive?"Connected":"Disconnected"})})]}),t(ut,{tabs:h,activeTab:e,onTabChange:n,lastUpdate:_,onRefresh:c}),t("div",{className:"mobile-quick-status",children:[t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.drivesActive?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"USB"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.wifiConnected?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"WiFi"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.cpuTempC&&parseFloat(l.cpuTempC)<70?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:[l==null?void 0:l.cpuTempC,"ยฐC"]})]})]}),t("div",{className:"app-body",children:[e===j.DASHBOARD&&t(ft,{status:o,computed:l,config:i,expanded:r,onToggle:()=>s(!r),onRefresh:c}),t("main",{className:"app-main",children:[e===j.DASHBOARD&&t(Nt,{status:o,computed:l,config:i,storage:a,onRefresh:c}),e===j.VIEWER&&(i==null?void 0:i.has_cam)==="yes"&&t(xt,{}),e===j.FILES&&t(Lt,{config:i}),e===j.LOGS&&t(Tt,{})]})]})]})}yn(t(Bt,{}),document.getElementById("app")); diff --git a/teslausb-www-react/dist/assets/index-Cyod02f8.css b/teslausb-www-react/dist/assets/index-Cyod02f8.css deleted file mode 100644 index 5b8edd68..00000000 --- a/teslausb-www-react/dist/assets/index-Cyod02f8.css +++ /dev/null @@ -1 +0,0 @@ -*{margin:0;padding:0;box-sizing:border-box}body{font-family:Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background-color:#f5f5f5;color:#333;overflow:hidden}@media(max-width:767px){body{overflow:visible;height:auto}}.app-shell{display:flex;flex-direction:column;min-height:100vh;overflow:hidden}@media(max-width:767px){.app-shell{overflow:visible;height:auto;min-height:100vh}}.app-topbar{display:flex;justify-content:space-between;align-items:center;background-color:#1f2937;color:#fff;padding:.75rem 1rem;border-bottom:1px solid #374151}.app-topbar-title{font-size:1.1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}.app-topbar-title svg{width:24px;height:24px}.app-topbar-actions{display:flex;gap:1rem;align-items:center}.app-dashboard{display:flex;flex-direction:column;height:100vh;background-color:#f5f5f5}.app-header{background-color:#fff;border-bottom:1px solid #e1e5e9;padding:.75rem 1rem;display:flex;justify-content:space-between;align-items:center;height:60px;flex-shrink:0;position:sticky;top:0;z-index:40;backdrop-filter:saturate(180%) blur(4px)}.app-header-left{display:flex;align-items:center;gap:.5rem}.app-header-icon{width:20px;height:20px;color:#06c}.app-header-title{font-size:16px;font-weight:600;color:#333;letter-spacing:.1px}.app-header-subtitle{font-size:14px;color:#666;margin-left:.5rem}.app-header-right{display:flex;align-items:center;gap:1rem}.dashboard-nav{display:flex;gap:.5rem;align-items:center}.nav-link{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:6px;font-size:14px;font-weight:500;color:#6b7280;text-decoration:none;transition:all .15s ease;border:1px solid transparent;position:relative;cursor:pointer;background:none}.nav-link:hover{background-color:#f9fafb;color:#374151}.nav-link.active{background-color:#0095f6;color:#fff;border-color:#007dd1}.nav-link.active svg{color:#fff}.nav-link svg{color:#6b7280;transition:color .15s ease;width:16px;height:16px}.nav-link:hover svg{color:#374151}.nav-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background-color:#ef4444;color:#fff;font-size:11px;font-weight:700;border-radius:9px;margin-left:4px;line-height:1}.nav-link.active .nav-badge{background-color:#fff;color:#ef4444}.app-last-update{font-size:13px;color:#6b7280}.app-status{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.3px}.app-status.healthy{color:#065f46;background:#d1fae5;border:1px solid #a7f3d0}.app-status.unhealthy{color:#7f1d1d;background:#fee2e2;border:1px solid #fecaca}.app-status.warning{color:#92400e;background:#fef3c7;border:1px solid #fde68a}.app-body{display:flex;flex:1;overflow:hidden}.app-sidebar{width:280px;background-color:#fff;border-right:1px solid #e1e5e9;padding:1rem;overflow-y:auto;overflow-x:hidden;flex-shrink:0;max-height:calc(100vh - 60px);-webkit-overflow-scrolling:touch}.device-info{display:flex;flex-direction:column;border-radius:10px;border:1px solid #e5e7eb;background:#fff;margin-bottom:1rem}.device-header{display:flex;align-items:center;gap:.5rem;padding:10px 12px;border-bottom:1px solid #eef2f7;font-size:14px;font-weight:600;color:#1f2937}.device-header svg{width:16px;height:16px;color:#6b7280}.info-list{display:flex;flex-direction:column;padding:8px 12px}.info-item{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f0f0f0;font-size:13px}.info-item:last-child{border-bottom:none}.info-item.clickable{cursor:pointer;transition:background-color .15s ease;margin:0 -12px;padding:6px 12px;border-radius:4px}.info-item.clickable:hover{background-color:#f3f4f6}.info-item.clickable:active{background-color:#e5e7eb}.toggle-btn{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:600;line-height:1;border:1px solid #007dd1;background-color:#0095f6;color:#fff;cursor:pointer;transition:all .15s ease}.toggle-btn svg{flex-shrink:0;vertical-align:middle;margin-right:4px;color:#fff}.toggle-btn:hover:not(:disabled){background-color:#007dd1;border-color:#006bbd}.toggle-btn:disabled{opacity:.6;cursor:not-allowed}.toggle-btn.active{background-color:#dcfce7;border-color:#22c55e;color:#166534}.toggle-btn.danger{background-color:#fee2e2;border-color:#f87171;color:#b91c1c}.ui-switch-link{color:#0095f6;text-decoration:none;font-size:13px;padding:4px 0;display:block;width:100%}.ui-switch-link:hover{text-decoration:underline}.toggle-btn.danger:hover:not(:disabled){background-color:#fecaca;border-color:#ef4444}.toggle-btn.primary{background-color:#eff6ff;border-color:#3b82f6;color:#1d4ed8}.toggle-btn.primary:hover:not(:disabled){background-color:#dbeafe;border-color:#2563eb}.sidebar-action{margin-top:8px;padding:0}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;border-radius:6px;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .15s ease}.action-btn:disabled{opacity:.6;cursor:not-allowed}.action-btn.connected{background-color:#dcfce7;color:#166534;border:1px solid #22c55e}.action-btn.connected:hover:not(:disabled){background-color:#bbf7d0}.action-btn.disconnected{background-color:#fef2f2;color:#991b1b;border:1px solid #f87171}.action-btn.disconnected:hover:not(:disabled){background-color:#fee2e2}.action-btn.restart{background-color:#f3f4f6;color:#374151;border:1px solid #d1d5db}.action-btn.restart:hover:not(:disabled){background-color:#e5e7eb;border-color:#9ca3af}.info-label{color:#6b7280;font-weight:500}.info-value{color:#111827;font-weight:600;text-align:right}.info-value-with-action{display:flex;align-items:center;gap:8px}.speed-result{color:#111827;font-weight:600;font-size:12px}.small-text{font-size:11px!important;line-height:1.2}.compact-date{font-size:10px!important;line-height:1.1;white-space:nowrap;color:#4b5563}.progress-container{margin-top:.5rem}.progress-bar{width:100%;height:8px;background-color:#eef2f7;border-radius:999px;overflow:hidden;margin-bottom:.25rem}.progress-fill{height:100%;border-radius:999px;transition:width .3s ease}.progress-fill.blue{background-color:#3b82f6}.progress-fill.green{background-color:#10b981}.progress-fill.yellow{background-color:#f59e0b}.progress-fill.red{background-color:#ef4444}.progress-text{font-size:10px;color:#6b7280;font-weight:500;margin-top:6px;text-align:right}.status-healthy{color:#00b04f!important}.status-degraded{color:#ff9800!important}.status-unhealthy{color:#e74c3c!important}.app-main{flex:1;background-color:#fff;padding:1rem;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;max-height:calc(100vh - 60px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1px;background-color:#e1e5e9;border:1px solid #e1e5e9;border-radius:4px;overflow:visible;margin-bottom:1rem}.stats-section{background-color:#fff;padding:1rem}.stats-section h3{font-size:12px;font-weight:600;color:#666;text-transform:uppercase;letter-spacing:.5px;margin-bottom:.75rem}.stat-data{display:flex;flex-direction:column;gap:.5rem}.primary-stat{font-size:24px;font-weight:700;color:#333;line-height:1}.stat-details{display:flex;flex-direction:column;gap:.25rem}.stat-details span{font-size:11px;color:#666}.stat-details span:first-child{color:#333;font-weight:500;font-size:12px}.mono-value{font-family:Monaco,Menlo,Consolas,monospace;color:#333;font-weight:600;font-size:12px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:12px;box-shadow:0 1px #00000005;transition:transform .08s ease,box-shadow .12s ease,border-color .12s ease}.card:hover{transform:translateY(-1px);box-shadow:0 6px 18px #0000000f;border-color:#dee3ea}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.card-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.card-value{font-size:22px;font-weight:700;color:#111827;line-height:1}.card-sub{font-size:11px;color:#666;margin-top:4px}.dashboard-section{margin-bottom:1rem}.section-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px;margin-bottom:.5rem}.features-row{display:flex;flex-wrap:wrap;gap:8px}.feature-badge{display:inline-block;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:500;line-height:1;text-align:center}.feature-badge.enabled{background:#ecfdf5;color:#059669;border:1px solid #a7f3d0}.feature-badge.disabled{background:#f3f4f6;color:#9ca3af;border:1px solid #e5e7eb}.actions-row{display:flex;flex-wrap:wrap;gap:10px}.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:14px}.chart-container{background-color:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:1rem;margin-bottom:1rem}.chart-header{padding:10px 12px;border-bottom:1px solid #eef2f7;margin:-1rem -1rem 1rem}.chart-title{font-size:14px;font-weight:600;color:#1f2937}.loading-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#666}.spinner{width:20px;height:20px;border:2px solid #e5e7eb;border-top-color:#0095f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.spinning{animation:spin 1s linear infinite}.error-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#e74c3c}.retry-btn{background-color:#e74c3c;border:none;color:#fff;padding:.375rem .75rem;border-radius:4px;font-size:12px;cursor:pointer;margin-left:.5rem}.retry-btn:hover{background-color:#c0392b}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 12px;font-size:13px;font-weight:500;line-height:1.4;border-radius:6px;border:1px solid #cfd8e3;background:#fff;color:#374151;cursor:pointer;transition:all .15s ease-in-out;box-shadow:0 1px 2px #00000008}.btn svg{color:#374151;transition:color .15s ease-in-out;width:14px;height:14px}.btn:hover:not(:disabled){background:#0095f6;border-color:#007dd1;color:#fff;box-shadow:0 2px 8px #0000000f}.btn:hover:not(:disabled) svg{color:#fff}.btn:active:not(:disabled){background:#007dd1;border-color:#006bbd}.btn:disabled{opacity:.6;cursor:not-allowed;background:#f9fafb;color:#9ca3af;border-color:#e5e7eb}.btn-primary{background:#0095f6;border-color:#007dd1;color:#fff}.btn-primary svg{color:#fff}.btn-primary:hover:not(:disabled){background:#007dd1;border-color:#006bbd}.btn-danger{background:#ef4444;border-color:#dc2626;color:#fff}.btn-danger svg{color:#fff}.btn-danger:hover:not(:disabled){background:#dc2626;border-color:#b91c1c}.btn-sm{padding:4px 8px;font-size:12px}.btn-lg{padding:10px 20px;font-size:15px}.storage-bar-container{margin:1rem 0}.storage-bar{display:flex;height:24px;border-radius:6px;overflow:hidden;background-color:#e5e7eb}.storage-segment{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#fff;transition:width .3s ease;min-width:0}.storage-segment.teslacam{background-color:#3b82f6}.storage-segment.music{background-color:#8b5cf6}.storage-segment.lightshow{background-color:#ec4899}.storage-segment.boombox{background-color:#f97316}.storage-segment.free{background-color:#d1d5db}.storage-legend{display:flex;flex-wrap:wrap;gap:1rem;margin-top:.75rem;font-size:12px}.storage-legend-item{display:flex;align-items:center;gap:6px}.storage-legend-dot{width:10px;height:10px;border-radius:2px}.storage-legend-dot.teslacam{background-color:#3b82f6}.storage-legend-dot.music{background-color:#8b5cf6}.storage-legend-dot.lightshow{background-color:#ec4899}.storage-legend-dot.boombox{background-color:#f97316}.storage-legend-dot.free{background-color:#d1d5db}.storage-legend-label{color:#374151;font-weight:500}.storage-legend-value{color:#6b7280}.storage-legend-used{color:#9ca3af;font-size:11px}.storage-legend-percent{margin-left:4px;color:#9ca3af;font-size:11px}.storage-note{margin-top:.5rem;font-size:10px;color:#9ca3af;font-style:italic}.sync-status-card{border:1px solid #e5e7eb;border-radius:10px;padding:1rem;background:#fff}.sync-status-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.sync-status-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.sync-status-indicator{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600}.sync-status-dot{width:8px;height:8px;border-radius:50%}.sync-status-dot.idle{background-color:#10b981}.sync-status-dot.connecting{background-color:#f59e0b;animation:pulse 1.5s ease-in-out infinite}.sync-status-dot.archiving{background-color:#3b82f6;animation:pulse 1s ease-in-out infinite}.sync-status-dot.error{background-color:#ef4444}.sync-status-description{font-size:13px;color:#6b7280;margin-bottom:.5rem}.sync-details{font-size:12px;color:#9ca3af}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.sync-progress-bar{height:8px;background-color:#e5e7eb;border-radius:4px;overflow:hidden;margin:.75rem 0}.sync-progress-fill{height:100%;background-color:#3b82f6;border-radius:4px;transition:width .3s ease}.sync-details{display:flex;flex-direction:column;gap:2px;font-size:11px;color:#6b7280}.sync-progress-main{font-weight:500}.sync-progress-secondary{font-size:10px;color:#9ca3af}.sync-file{max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:Monaco,Menlo,monospace}.log-viewer{background-color:#1e1e1e;border-radius:8px;font-family:Monaco,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;overflow:hidden;display:flex;flex-direction:column;height:400px}.log-viewer-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background-color:#2d2d2d;border-bottom:1px solid #3d3d3d}.log-viewer-title{color:#e0e0e0;font-size:13px;font-weight:600}.log-viewer-actions{display:flex;gap:8px}.log-action-btn{background:#4a4a4a!important;border-color:#666!important;color:#e0e0e0!important}.log-action-btn svg{color:#e0e0e0!important}.log-action-btn:hover:not(:disabled){background:#5a5a5a!important;border-color:#888!important}.log-action-btn:hover:not(:disabled) svg{color:#fff!important}.log-action-btn:disabled{opacity:.4}.log-viewer-content{flex:1;overflow-y:auto;padding:12px;color:#d4d4d4}.log-viewer-content::-webkit-scrollbar{width:8px}.log-viewer-content::-webkit-scrollbar-track{background:#1e1e1e}.log-viewer-content::-webkit-scrollbar-thumb{background:#4d4d4d;border-radius:4px}.log-line{white-space:pre-wrap;word-break:break-all}.log-line.error{color:#f87171}.log-line.warning{color:#fbbf24}.log-line.success{color:#34d399}.video-viewer{display:flex;flex-direction:column;height:100%;background:#000;border-radius:8px;overflow:hidden}.video-grid{display:grid;flex:1;gap:2px;background:#1a1a1a;padding:2px}.video-grid.layout-6{grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-4{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.video-cell{position:relative;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden}.video-cell video{width:100%;height:100%;object-fit:contain}.video-cell-label{position:absolute;top:8px;left:8px;background:#000000b3;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500}.video-controls{display:flex;align-items:center;gap:1rem;padding:12px 16px;background:#1a1a1a;border-top:1px solid #333}.video-timeline{flex:1;height:6px;background:#333;border-radius:3px;cursor:pointer;position:relative}.video-timeline-progress{height:100%;background:#0095f6;border-radius:3px;transition:width .1s linear}.video-timeline-markers{position:absolute;inset:0;pointer-events:none}.video-timeline-marker{position:absolute;top:-2px;width:2px;height:10px;background:#ef4444;border-radius:1px}.video-play-btn{background:#0095f6;border:none;color:#fff;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s}.video-play-btn:hover{background:#007dd1}.video-time{color:#fff;font-size:13px;font-family:Monaco,Menlo,monospace;min-width:100px}.file-browser{display:flex;height:100%;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}.file-tree{width:250px;background:#f9fafb;border-right:1px solid #e5e7eb;overflow-y:auto}.file-tree-item{display:flex;align-items:center;gap:6px;padding:8px 12px;cursor:pointer;font-size:13px;color:#374151;border-bottom:1px solid #f0f0f0}.file-tree-item:hover{background:#f3f4f6}.file-tree-item.active{background:#e0f2fe;color:#0369a1}.file-tree-item svg{width:16px;height:16px;color:#6b7280;flex-shrink:0}.file-tree-item.active svg{color:#0369a1}.file-list{flex:1;overflow-y:auto;padding:1rem}.file-list-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #e5e7eb}.file-list-title{font-size:14px;font-weight:600;color:#1f2937}.file-list-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px}.file-item{display:flex;flex-direction:column;align-items:center;padding:12px;border:1px solid transparent;border-radius:8px;cursor:pointer;transition:all .15s}.file-item:hover{background:#f9fafb;border-color:#e5e7eb}.file-item.selected{background:#e0f2fe;border-color:#0ea5e9}.file-item-icon{width:48px;height:48px;margin-bottom:8px;color:#6b7280}.file-item-icon.folder{color:#f59e0b}.file-item-icon.video{color:#8b5cf6}.file-item-icon.audio{color:#ec4899}.file-item-name{font-size:12px;text-align:center;word-break:break-word;color:#374151}.file-item-size{font-size:10px;color:#9ca3af;margin-top:2px}.dropzone{border:2px dashed #d1d5db;border-radius:8px;padding:2rem;text-align:center;color:#6b7280;transition:all .15s}.dropzone svg{width:24px;height:24px}.dropzone.active{border-color:#0095f6;background:#f0f9ff;color:#0369a1}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:#fff;border-radius:12px;max-width:500px;width:90%;max-height:90vh;overflow:hidden;box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid #e5e7eb}.modal-title{font-size:16px;font-weight:600;color:#1f2937}.modal-close{background:none;border:none;cursor:pointer;color:#6b7280;padding:4px}.modal-close:hover{color:#374151}.modal-body{padding:1.5rem;overflow-y:auto}.modal-footer{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid #e5e7eb;background:#f9fafb}.toast-container{position:fixed;bottom:1rem;right:1rem;z-index:1100;display:flex;flex-direction:column;gap:.5rem}.toast{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;background:#1f2937;color:#fff;border-radius:8px;font-size:13px;box-shadow:0 10px 15px -3px #0000001a;animation:slideIn .2s ease}.toast.success{background:#059669}.toast.error{background:#dc2626}.toast.warning{background:#d97706}@keyframes slideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@media(min-width:1024px)and (max-width:1366px){.app-sidebar{width:260px}.kpi-row{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.app-header-title{font-size:15px}}@media(min-width:768px)and (max-width:1023px){.app-body{flex-direction:row}.app-sidebar{width:220px;max-height:calc(100vh - 60px);overflow-y:auto}.kpi-row{grid-template-columns:repeat(2,1fr);gap:8px}.card{padding:10px}.card-value{font-size:18px}.card-title{font-size:11px}.card-sub{font-size:10px}.app-header-right{gap:8px}.app-last-update{font-size:11px}.chart-container{padding:12px;margin-bottom:8px}.chart-title{font-size:13px}}.mobile-quick-status{display:none}.desktop-status-bar{display:block}.sidebar-toggle-btn{display:none;background:none;border:none;color:#6b7280;cursor:pointer;padding:4px;margin-left:auto;transition:color .2s ease}.sidebar-toggle-btn:hover{color:#374151}@media(max-width:767px){.desktop-status-bar{display:none!important}.mobile-quick-status{display:flex;align-items:center;justify-content:space-around;padding:6px 8px;background-color:#f9fafb;border-bottom:1px solid #e5e7eb;gap:4px;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.quick-status-item{display:flex;flex-direction:column;align-items:center;gap:3px;flex-shrink:0}.status-dot-mini{width:8px;height:8px;border-radius:50%;flex-shrink:0}.status-dot-mini.healthy{background-color:#10b981;box-shadow:0 0 4px #10b98180}.status-dot-mini.unhealthy{background-color:#ef4444;box-shadow:0 0 4px #ef444480}.quick-status-label{font-size:9px;color:#6b7280;font-weight:500;white-space:nowrap;text-align:center}.app-body{flex-direction:column;height:auto;overflow:visible}.app-sidebar{width:100%;height:auto;max-height:none;border-right:none;border-bottom:1px solid #e1e5e9;padding:8px;overflow:visible;overflow-x:hidden;flex-shrink:0;-webkit-overflow-scrolling:touch;transition:max-height .3s ease}.sidebar-toggle-btn{display:inline-flex}.app-sidebar:not(.expanded){max-height:60px;overflow:hidden}.app-sidebar:not(.expanded) .device-info:not(:first-child){display:none}.app-sidebar:not(.expanded) .info-item:nth-child(n+4){display:none}.app-sidebar.expanded{max-height:600px;overflow-y:auto}.device-info{margin-top:8px}.device-info:first-child{margin-top:0}.info-item{padding:4px 0;font-size:12px}.app-main{flex:1;padding:12px 8px 150px;overflow-y:auto;overflow-x:hidden;height:auto;min-height:0;max-height:none;-webkit-overflow-scrolling:touch}.kpi-row{grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:10px}.card{padding:6px 8px;min-height:75px}.card-header{margin-bottom:3px}.card-value{font-size:18px;line-height:1.1;margin-bottom:2px}.card-title{font-size:10px;letter-spacing:.3px}.card-sub{font-size:9px;line-height:1.3;margin-top:2px}.app-header{padding:8px 10px 10px!important;height:auto!important;flex-direction:column!important;gap:10px!important;align-items:stretch!important;position:static!important;top:auto!important;z-index:auto!important;backdrop-filter:none!important}.app-header-left{justify-content:center;padding-bottom:4px}.app-header-title{font-size:14px}.app-header-right{flex-direction:row;gap:6px;align-items:center;justify-content:space-between;width:100%;padding-bottom:2px}.dashboard-nav{gap:4px;flex:1;overflow-x:auto;-webkit-overflow-scrolling:touch}.nav-link{padding:7px 8px;font-size:11px;gap:4px;flex-shrink:0;justify-content:center;border-radius:4px}.nav-link svg{width:14px;height:14px}.nav-badge{min-width:16px;height:16px;font-size:10px;padding:0 4px;margin-left:2px}.app-last-update{font-size:10px}.app-status{font-size:9px;padding:1px 6px}.btn{padding:7px 8px;font-size:10px;gap:3px;white-space:nowrap;border-radius:4px;flex-shrink:0}.btn svg{width:12px;height:12px}.chart-container{padding:8px;margin-bottom:10px}.chart-title{font-size:11px;font-weight:600}.chart-header{margin-bottom:6px;padding-bottom:4px}.chart-container:last-child{margin-bottom:100px}.file-browser{flex-direction:column}.file-tree{width:100%;max-height:150px;border-right:none;border-bottom:1px solid #e5e7eb}.file-list-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.video-grid.layout-6{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(3,1fr)}.video-controls{flex-wrap:wrap;gap:.5rem;padding:8px 12px}.video-time{font-size:11px;min-width:80px}.log-viewer{height:300px}.log-viewer-content{font-size:10px}}@media(min-width:393px)and (max-width:430px){.kpi-row{grid-template-columns:1fr}.card{min-height:80px}.app-main{padding-bottom:140px}}@media(max-width:392px){.kpi-row{grid-template-columns:1fr}.app-header{padding:6px 8px;height:45px}.app-header-title{font-size:13px}.app-main{padding:6px 6px 160px}.card{padding:6px;min-height:70px}.card-value{font-size:14px}.chart-title{font-size:11px}}@media(pointer:coarse){.btn{min-height:44px;min-width:44px}.info-item{padding:8px 0;min-height:44px;align-items:center}.card{min-height:100px}.nav-link{min-height:44px}}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.card,.chart-container{border-width:.5px}.app-header{border-bottom-width:.5px}}@media(min-width:768px)and (max-width:1024px)and (orientation:landscape){.kpi-row{grid-template-columns:repeat(3,1fr)}.app-sidebar{width:200px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}@media(min-width:768px)and (max-width:1024px)and (orientation:portrait){.kpi-row{grid-template-columns:repeat(2,1fr)}.app-sidebar{width:240px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}.app-dashboard{max-height:100vh;overflow:hidden}.dashboard-nav::-webkit-scrollbar{display:none}.dashboard-nav{-ms-overflow-style:none;scrollbar-width:none}.tab-content{display:none;flex:1;overflow:hidden}.tab-content.active{display:flex;flex-direction:column}.hidden{display:none!important}.flex{display:flex}.flex-1{flex:1}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.text-sm{font-size:12px}.text-xs{font-size:10px}.text-muted{color:#6b7280}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700} diff --git a/teslausb-www-react/dist/assets/index-DxhX7Us7.js b/teslausb-www-react/dist/assets/index-DxhX7Us7.js deleted file mode 100644 index f27d85d6..00000000 --- a/teslausb-www-react/dist/assets/index-DxhX7Us7.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function r(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(o){if(o.ep)return;o.ep=!0;const i=r(o);fetch(o.href,i)}})();var te,N,Ae,P,ye,Ee,Re,De,ue,oe,ae,G={},je=[],an=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,re=Array.isArray;function j(e,n){for(var r in n)e[r]=n[r];return e}function fe(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function ln(e,n,r){var s,o,i,a={};for(i in n)i=="key"?s=n[i]:i=="ref"?o=n[i]:a[i]=n[i];if(arguments.length>2&&(a.children=arguments.length>3?te.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return X(e,a,s,o,null)}function X(e,n,r,s,o){var i={type:e,props:n,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++Ae:o,__i:-1,__u:0};return o==null&&N.vnode!=null&&N.vnode(i),i}function R(e){return e.children}function Q(e,n){this.props=e,this.context=n}function O(e,n){if(n==null)return e.__?O(e.__,e.__i+1):null;for(var r;nl&&P.sort(Re),e=P.shift(),l=P.length,e.__d&&(r=void 0,s=void 0,o=(s=(n=e).__v).__e,i=[],a=[],n.__P&&((r=j({},s)).__v=s.__v+1,N.vnode&&N.vnode(r),_e(n.__P,r,s,n.__n,n.__P.namespaceURI,32&s.__u?[o]:null,i,o==null?O(s):o,!!(32&s.__u),a),r.__v=s.__v,r.__.__k[r.__i]=r,Pe(i,r,a),s.__e=s.__=null,r.__e!=o&&We(r)));ne.__r=0}function He(e,n,r,s,o,i,a,l,u,d,_){var c,h,f,v,y,b,m,p=s&&s.__k||je,w=n.length;for(u=cn(r,n,p,u,w),c=0;c0?a=e.__k[i]=X(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,u=i+h,a.__=e,a.__b=e.__b+1,(d=a.__i=dn(a,r,u,c))!=-1&&(c--,(l=r[d])&&(l.__u|=2)),l==null||l.__v==null?(d==-1&&(o>_?h--:o<_&&h++),typeof a.type!="function"&&(a.__u|=4)):d!=u&&(d==u-1?h--:d==u+1?h++:(d>u?h--:h++,a.__u|=4))):e.__k[i]=null;if(c)for(i=0;i<_;i++)(l=r[i])!=null&&(2&l.__u)==0&&(l.__e==s&&(s=O(l)),Ve(l,l));return s}function Ue(e,n,r,s){var o,i;if(typeof e.type=="function"){for(o=e.__k,i=0;o&&i(_?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&(2&d.__u)==0&&l==d.key&&u==d.type)return a}return-1}function ke(e,n,r){n[0]=="-"?e.setProperty(n,r==null?"":r):e[n]=r==null?"":typeof r!="number"||an.test(n)?r:r+"px"}function Z(e,n,r,s,o){var i,a;e:if(n=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof s=="string"&&(e.style.cssText=s=""),s)for(n in s)r&&n in r||ke(e.style,n,"");if(r)for(n in r)s&&r[n]==s[n]||ke(e.style,n,r[n])}else if(n[0]=="o"&&n[1]=="n")i=n!=(n=n.replace(De,"$1")),a=n.toLowerCase(),n=a in e||n=="onFocusOut"||n=="onFocusIn"?a.slice(2):n.slice(2),e.l||(e.l={}),e.l[n+i]=r,r?s?r.u=s.u:(r.u=ue,e.addEventListener(n,i?ae:oe,i)):e.removeEventListener(n,i?ae:oe,i);else{if(o=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in e)try{e[n]=r==null?"":r;break e}catch(l){}typeof r=="function"||(r==null||r===!1&&n[4]!="-"?e.removeAttribute(n):e.setAttribute(n,n=="popover"&&r==1?"":r))}}function we(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n.t==null)n.t=ue++;else if(n.t0?e:re(e)?e.map(Oe):j({},e)}function hn(e,n,r,s,o,i,a,l,u){var d,_,c,h,f,v,y,b=r.props||G,m=n.props,p=n.type;if(p=="svg"?o="http://www.w3.org/2000/svg":p=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(d=0;d=r.__.length&&r.__.push({}),r.__[e]}function k(e){return Y=1,pn(Ge,e)}function pn(e,n,r){var s=me(K++,2);if(s.t=e,!s.__c&&(s.__=[Ge(void 0,n),function(l){var u=s.__N?s.__N[0]:s.__[0],d=s.t(u,l);u!==d&&(s.__N=[d,s.__[1]],s.__c.setState({}))}],s.__c=S,!S.__f)){var o=function(l,u,d){if(!s.__c.__H)return!0;var _=s.__c.__H.__.filter(function(h){return!!h.__c});if(_.every(function(h){return!h.__N}))return!i||i.call(this,l,u,d);var c=s.__c.props!==l;return _.forEach(function(h){if(h.__N){var f=h.__[0];h.__=h.__N,h.__N=void 0,f!==h.__[0]&&(c=!0)}}),i&&i.call(this,l,u,d)||c};S.__f=!0;var i=S.shouldComponentUpdate,a=S.componentWillUpdate;S.componentWillUpdate=function(l,u,d){if(this.__e){var _=i;i=void 0,o(l,u,d),i=_}a&&a.call(this,l,u,d)},S.shouldComponentUpdate=o}return s.__N||s.__}function F(e,n){var r=me(K++,3);!$.__s&&qe(r.__H,n)&&(r.__=e,r.u=n,S.__H.__h.push(r))}function H(e){return Y=5,ve(function(){return{current:e}},[])}function ve(e,n){var r=me(K++,7);return qe(r.__H,n)&&(r.__=e(),r.__H=n,r.__h=e),r.__}function B(e,n){return Y=8,ve(function(){return e},n)}function mn(){for(var e;e=ze.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(ee),e.__H.__h.forEach(ce),e.__H.__h=[]}catch(n){e.__H.__h=[],$.__e(n,e.__v)}}$.__b=function(e){S=null,xe&&xe(e)},$.__=function(e,n){e&&n.__k&&n.__k.__m&&(e.__m=n.__k.__m),Te&&Te(e,n)},$.__r=function(e){Ce&&Ce(e),K=0;var n=(S=e.__c).__H;n&&(se===S?(n.__h=[],S.__h=[],n.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(n.__h.forEach(ee),n.__h.forEach(ce),n.__h=[],K=0)),se=S},$.diffed=function(e){Le&&Le(e);var n=e.__c;n&&n.__H&&(n.__H.__h.length&&(ze.push(n)!==1&&Ne===$.requestAnimationFrame||((Ne=$.requestAnimationFrame)||vn)(mn)),n.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),se=S=null},$.__c=function(e,n){n.some(function(r){try{r.__h.forEach(ee),r.__h=r.__h.filter(function(s){return!s.__||ce(s)})}catch(s){n.some(function(o){o.__h&&(o.__h=[])}),n=[],$.__e(s,r.__v)}}),Se&&Se(e,n)},$.unmount=function(e){$e&&$e(e);var n,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(s){try{ee(s)}catch(o){n=o}}),r.__H=void 0,n&&$.__e(n,r.__v))};var Be=typeof requestAnimationFrame=="function";function vn(e){var n,r=function(){clearTimeout(s),Be&&cancelAnimationFrame(n),setTimeout(e)},s=setTimeout(r,35);Be&&(n=requestAnimationFrame(r))}function ee(e){var n=S,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),S=n}function ce(e){var n=S;e.__c=e.__(),S=n}function qe(e,n){return!e||e.length!==n.length||n.some(function(r,s){return r!==e[s]})}function Ge(e,n){return typeof n=="function"?n(e):n}const A="/cgi-bin";async function gn(){const e=await fetch(`${A}/status.sh`);if(!e.ok)throw new Error("Failed to fetch status");return e.json()}async function yn(){const e=await fetch(`${A}/config.sh`);if(!e.ok)throw new Error("Failed to fetch config");return e.json()}async function bn(){const e=await fetch(`${A}/storage.sh`);if(!e.ok)throw new Error("Failed to fetch storage");return e.json()}async function kn(){const e=await fetch(`${A}/videolist.sh`);if(!e.ok)throw new Error("Failed to fetch video list");const n=await e.text();return wn(n)}function wn(e){const n=e.trim().split(` -`).filter(Boolean),r={RecentClips:{},SavedClips:{},SentryClips:{}};for(const s of n){const o=s.split("/");if(o.length>=2){const[i,...a]=o;if(r[i]){const l=a[0];r[i][l]||(r[i][l]=[]),a.length>1&&r[i][l].push(a.slice(1).join("/"))}}}return r}async function Nn(){if(!(await fetch(`${A}/trigger_sync.sh`)).ok)throw new Error("Failed to trigger sync")}async function xn(){const e=await fetch(`${A}/music_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch music sync progress");return e.json()}async function Cn(){if(!(await fetch(`${A}/toggledrives.sh`)).ok)throw new Error("Failed to toggle drives")}async function Ln(){if(!(await fetch(`${A}/reboot.sh`)).ok)throw new Error("Failed to reboot")}async function Sn(){return(await fetch(`${A}/pairBLEkey.sh`)).status===202}async function $n(){return(await(await fetch(`${A}/checkBLEstatus.sh`)).text()).includes("

          paired

          ")}async function Tn(){await fetch(`${A}/diagnose.sh`)}async function Bn(){const e=await fetch("/diagnostics.txt");if(!e.ok)throw new Error("Failed to fetch diagnostics");return e.text()}async function Mn(e,n=0){const r={};n>0&&(r.Range=`bytes=${n}-`);const s=await fetch(`/${e}`,{headers:r});if(s.status===416)return{content:"",size:n,truncated:!1};if(s.status===404)throw new Error("Log file not found");if(!s.ok&&s.status!==206)throw new Error(`Failed to fetch log: ${s.status}`);const o=await s.text();let i=n;if(s.status===206){const a=s.headers.get("Content-Range");if(a){const l=a.match(/bytes \d+-(\d+)\/(\d+)/);l?i=parseInt(l[1],10)+1:i=n+o.length}else i=n+o.length}else i=o.length;return{content:o,size:i,truncated:!1}}async function In(e,n){const r=await fetch(`${A}/randomdata.sh`,{signal:n});if(!r.ok)throw new Error("Failed to start speed test");const s=r.body.getReader();let o=0;const i=performance.now();try{for(;;){const{done:l,value:u}=await s.read();if(l)break;o+=u.length;const d=(performance.now()-i)/1e3,_=o*8/(d*1e6);e&&e(_)}}catch(l){if(l.name!=="AbortError")throw l}const a=(performance.now()-i)/1e3;return o*8/(a*1e6)}function Fn(e,n,r){return`/TeslaCam/${e}/${n}/${r}`}function An(e){return`/TeslaCam/SentryClips/${e}/event.json`}async function En(e){try{const n=await fetch(An(e));return n.ok?n.json():null}catch(n){return null}}function Rn(e=5e3){const[n,r]=k(null),[s,o]=k(null),[i,a]=k(null),[l,u]=k(!0),[d,_]=k(null),[c,h]=k(null),f=B(async()=>{try{const[y,b,m]=await Promise.all([gn(),yn(),bn().catch(()=>null)]);r(y),o(b),a(m),h(new Date),_(null)}catch(y){_(y.message)}finally{u(!1)}},[]);F(()=>{f();const y=setInterval(f,e);return()=>clearInterval(y)},[f,e]);const v=n?{cpuTempC:n.cpu_temp?(parseInt(n.cpu_temp,10)/1e3).toFixed(1):null,uptimeFormatted:Dn(parseInt(n.uptime||"0",10)),diskUsedPercent:n.total_space&&n.free_space?Math.round((parseInt(n.total_space,10)-parseInt(n.free_space,10))/parseInt(n.total_space,10)*100):0,diskUsedGB:n.total_space&&n.free_space?((parseInt(n.total_space,10)-parseInt(n.free_space,10))/(1024*1024*1024)).toFixed(1):"0",diskTotalGB:n.total_space?(parseInt(n.total_space,10)/(1024*1024*1024)).toFixed(1):"0",diskFreeGB:n.free_space?(parseInt(n.free_space,10)/(1024*1024*1024)).toFixed(1):"0",drivesActive:n.drives_active==="yes",wifiConnected:!!n.wifi_ssid&&n.wifi_ssid!=="",wifiSignalPercent:jn(n.wifi_strength),ethernetConnected:!!n.ether_ip&&n.ether_ip!=="",snapshotCount:parseInt(n.num_snapshots||"0",10)}:null;return{status:n,config:s,storage:i,computed:v,loading:l,error:d,lastUpdate:c,refresh:f}}function Dn(e){if(!e||isNaN(e))return"0s";const n=Math.floor(e/86400),r=Math.floor(e%86400/3600),s=Math.floor(e%3600/60),o=e%60,i=[];return n>0&&i.push(`${n}d`),r>0&&i.push(`${r}h`),s>0&&i.push(`${s}m`),(o>0||i.length===0)&&i.push(`${o}s`),i.join(" ")}function jn(e){if(!e)return 0;const n=e.split("/");if(n.length!==2)return 0;const[r,s]=n.map(Number);return isNaN(r)||isNaN(s)||s===0?0:Math.round(r/s*100)}function Wn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2-4H8L6 10l-2.5 1.1C2.7 11.3 2 12.1 2 13v3c0 .6.4 1 1 1h2"}),t("circle",{cx:"7",cy:"17",r:"2"}),t("circle",{cx:"17",cy:"17",r:"2"})]})}function Ke({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"})]})}function Hn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"23 7 16 12 23 17 23 7"}),t("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2",ry:"2"})]})}function Un({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}function de({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"23 4 23 10 17 10"}),t("path",{d:"M20.49 15a9 9 0 1 1-2.12-9.36L23 10"})]})}function Pn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:t("polygon",{points:"5 3 19 12 5 21 5 3"})})}function On({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:[t("rect",{x:"6",y:"4",width:"4",height:"16"}),t("rect",{x:"14",y:"4",width:"4",height:"16"})]})}function Vn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"19 20 9 12 19 4 19 20"}),t("line",{x1:"5",y1:"19",x2:"5",y2:"5"})]})}function zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"5 4 15 12 5 20 5 4"}),t("line",{x1:"19",y1:"5",x2:"19",y2:"19"})]})}function qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),t("polyline",{points:"7 10 12 15 17 10"}),t("line",{x1:"12",y1:"15",x2:"12",y2:"3"})]})}function Gn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"3 6 5 6 21 6"}),t("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}function Kn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M5 12.55a11 11 0 0 1 14.08 0"}),t("path",{d:"M1.42 9a16 16 0 0 1 21.16 0"}),t("path",{d:"M8.53 16.11a6 6 0 0 1 6.95 0"}),t("line",{x1:"12",y1:"20",x2:"12.01",y2:"20"})]})}function Me({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("line",{x1:"22",y1:"12",x2:"2",y2:"12"}),t("path",{d:"M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}),t("line",{x1:"6",y1:"16",x2:"6.01",y2:"16"}),t("line",{x1:"10",y1:"16",x2:"10.01",y2:"16"})]})}function Yn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",ry:"2"}),t("rect",{x:"9",y:"9",width:"6",height:"6"}),t("line",{x1:"9",y1:"1",x2:"9",y2:"4"}),t("line",{x1:"15",y1:"1",x2:"15",y2:"4"}),t("line",{x1:"9",y1:"20",x2:"9",y2:"23"}),t("line",{x1:"15",y1:"20",x2:"15",y2:"23"}),t("line",{x1:"20",y1:"9",x2:"23",y2:"9"}),t("line",{x1:"20",y1:"14",x2:"23",y2:"14"}),t("line",{x1:"1",y1:"9",x2:"4",y2:"9"}),t("line",{x1:"1",y1:"14",x2:"4",y2:"14"})]})}function Zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"4",cy:"20",r:"1"}),t("circle",{cx:"20",cy:"20",r:"1"}),t("circle",{cx:"12",cy:"10",r:"1"}),t("path",{d:"M12 3v7"}),t("path",{d:"M4 20v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"}),t("path",{d:"M12 10v9"}),t("path",{d:"M7 10l5-7 5 7"})]})}function Jn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 2v6h-6"}),t("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"}),t("path",{d:"M3 22v-6h6"}),t("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"})]})}function Xn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M18.36 6.64a9 9 0 1 1-12.73 0"}),t("line",{x1:"12",y1:"2",x2:"12",y2:"12"})]})}function Ye({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"})})}function Ze({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"4 17 10 11 4 5"}),t("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function Qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"12",cy:"12",r:"10"}),t("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),t("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]})}function Je({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6 9 12 15 18 9"})})}function et({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"}),t("circle",{cx:"12",cy:"10",r:"3"})]})}function nt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M9 18V5l12-2v13"}),t("circle",{cx:"6",cy:"18",r:"3"}),t("circle",{cx:"18",cy:"16",r:"3"})]})}function Xe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})]})}function tt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19.4 14.9C20.2 13.4 20.6 11.7 20.6 10c0-5-4-9-9-9s-9 4-9 9 4 9 9 9c1.7 0 3.4-.4 4.9-1.2"}),t("path",{d:"M11.6 10l6.4 6.4"})]})}function rt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2",ry:"2"}),t("line",{x1:"3",y1:"9",x2:"21",y2:"9"}),t("line",{x1:"9",y1:"21",x2:"9",y2:"9"})]})}function it({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"17 1 21 5 17 9"}),t("path",{d:"M3 11V9a4 4 0 0 1 4-4h14"}),t("polyline",{points:"7 23 3 19 7 15"}),t("path",{d:"M21 13v2a4 4 0 0 1-4 4H3"})]})}const st={dashboard:Ke,viewer:Hn,files:Un,logs:Ze};function ot({tabs:e,activeTab:n,onTabChange:r,lastUpdate:s,onRefresh:o}){const i=a=>a?a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"";return t("header",{className:"app-header",children:[t("div",{className:"app-header-left",children:t("nav",{className:"dashboard-nav",children:e.map(a=>{const l=st[a.id]||Ke;return t("button",{className:`nav-link ${n===a.id?"active":""}`,onClick:()=>r(a.id),children:[t(l,{}),t("span",{children:a.label})]},a.id)})})}),t("div",{className:"app-header-right desktop-status-bar",children:[t("span",{className:"app-last-update",style:{marginRight:"12px"},children:s&&`Updated ${i(s)}`}),t("button",{className:"btn",onClick:o,children:[t(de,{}),t("span",{children:"Refresh"})]})]})]})}function at({status:e,computed:n,config:r,expanded:s,onToggle:o,onRefresh:i}){const[a,l]=k(!1),[u,d]=k(!1),[_,c]=k(!1),[h,f]=k(null),[v,y]=k(null),b=async()=>{l(!0);try{await Cn(),setTimeout(i,1e3)}catch(x){console.error("Toggle drives failed:",x)}finally{l(!1)}},m=async()=>{if(confirm("Are you sure you want to restart TeslaUSB?")){d(!0);try{await Ln()}catch(x){console.error("Reboot failed:",x)}}},p=async()=>{c(!0),f(null);const x=new AbortController,L=setTimeout(()=>x.abort(),1e4);try{await In(C=>f(C.toFixed(1)),x.signal)}catch(C){C.name!=="AbortError"&&console.error("Speed test failed:",C)}finally{clearTimeout(L),c(!1)}},w=async()=>{y("pairing");try{if(await Sn()){for(let L=0;L<60;L++)if(await new Promise(U=>setTimeout(U,2e3)),await $n()){y("paired");return}y("timeout")}else y("error")}catch(x){console.error("BLE pairing failed:",x),y("error")}};return t("aside",{className:`app-sidebar ${s?"expanded":""}`,children:[t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Yn,{}),t("span",{children:"System"}),t("button",{className:"sidebar-toggle-btn",onClick:o,children:t(Je,{className:s?"rotate-180":""})})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.device_model)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Model"}),t("span",{className:"info-value small-text",children:e.device_model})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Uptime"}),t("span",{className:"info-value",children:(n==null?void 0:n.uptimeFormatted)||"-"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"CPU Temp"}),t("span",{className:`info-value ${lt(n==null?void 0:n.cpuTempC)}`,children:n!=null&&n.cpuTempC?`${n.cpuTempC}ยฐC`:"-"})]}),t("div",{className:"info-item clickable",onClick:b,children:[t("span",{className:"info-label",children:"USB Drives"}),t("button",{className:`toggle-btn ${n!=null&&n.drivesActive?"active":"danger"}`,disabled:a,children:[a&&t(Zn,{style:{width:12,height:12},className:"spinning"}),n!=null&&n.drivesActive?"Disconnect from host":"Connect to host"]})]}),t("div",{className:"info-item clickable",onClick:m,children:[t("span",{className:"info-label",children:"Power"}),t("button",{className:"toggle-btn danger",disabled:u,children:[u&&t(Xn,{style:{width:12,height:12},className:"spinning"}),"Restart"]})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Kn,{}),t("span",{children:"Network"})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.wifi_ssid)&&t(R,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"WiFi SSID"}),t("span",{className:"info-value",children:e.wifi_ssid})]}),(e==null?void 0:e.wifi_freq)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Frequency"}),t("span",{className:"info-value",children:ct(e.wifi_freq)})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Signal"}),t("span",{className:"info-value",children:[(n==null?void 0:n.wifiSignalPercent)||0,"%"]})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.wifi_ip||"-"})]})]}),(e==null?void 0:e.ether_ip)&&t(R,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Ethernet"}),t("span",{className:"info-value",children:e.ether_speed||"Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.ether_ip})]})]}),!(e!=null&&e.wifi_ssid)&&!(e!=null&&e.ether_ip)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Status"}),t("span",{className:"info-value status-unhealthy",children:"Not Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Speed Test"}),t("span",{className:"info-value-with-action",children:[h&&t("span",{className:"speed-result",children:[h," Mbps"]}),t("button",{className:"toggle-btn",onClick:p,disabled:_,children:[_&&t(tt,{style:{width:12,height:12},className:"spinning"}),_?"Testing...":"Run"]})]})]}),(r==null?void 0:r.uses_ble)==="yes"&&t("div",{className:"info-item clickable",onClick:w,children:[t("span",{className:"info-label",children:"Bluetooth"}),t("button",{className:"toggle-btn",disabled:v==="pairing",children:[v==="pairing"&&t(Ye,{style:{width:12,height:12},className:"spinning"}),v==="pairing"?"Pairing...":v==="paired"?"Paired":v==="error"?"Failed":"Pair"]})]})]})]}),(n==null?void 0:n.snapshotCount)>0&&t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(Xe,{}),t("span",{children:"Snapshots"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Count"}),t("span",{className:"info-value",children:n.snapshotCount})]}),(e==null?void 0:e.snapshot_oldest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Oldest"}),t("span",{className:"info-value compact-date",children:Ie(e.snapshot_oldest)})]}),(e==null?void 0:e.snapshot_newest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Newest"}),t("span",{className:"info-value compact-date",children:Ie(e.snapshot_newest)})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(it,{}),t("span",{children:"Switch UI"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:t("a",{href:"/",className:"ui-switch-link",children:"Standard UI"})}),t("div",{className:"info-item",children:t("a",{href:"/new/",className:"ui-switch-link",children:"Vue UI"})})]})]})]})}function lt(e){if(!e)return"";const n=parseFloat(e);return n>=80?"status-unhealthy":n>=70?"status-degraded":"status-healthy"}function Ie(e){if(!e)return"-";try{return new Date(parseInt(e,10)*1e3).toLocaleDateString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch(n){return e}}function ct(e){if(!e)return"-";const n=e.match(/(\d+\.?\d*)/);if(n){const r=parseFloat(n[1]),s=t("span",{style:{fontWeight:"normal"},children:[" (",r.toFixed(3),")"]});return r>=2.4&&r<2.5?t(R,{children:["2.4 GHz",s]}):r>=5&&r<6?t(R,{children:["5 GHz",s]}):r>=6?t(R,{children:["6 GHz",s]}):`${r} GHz`}return e}function Qe(e,n=2e3,r=!0){const[s,o]=k([]),[i,a]=k(!0),[l,u]=k(null),d=H(0),_=H(!0),c=H(e),h=B(async(y=!1)=>{if(e)try{y&&(d.current=0);const b=await Mn(e,d.current);if(b.truncated){d.current=0,o([]);return}if(b.content){const m=b.content.split(` -`).filter(Boolean);o(y?m.slice(-1e3):p=>[...p,...m].slice(-1e3))}d.current=b.size,u(null)}catch(b){u(b.message)}finally{a(!1)}},[e]);F(()=>{e!==c.current&&(c.current=e,d.current=0,o([]),a(!0),u(null))},[e]),F(()=>{if(!r||!e)return;h(!0);const y=setInterval(()=>h(!1),n);return()=>clearInterval(y)},[e,n,r]);const f=B(y=>{_.current=y},[]),v=B(()=>{o([]),d.current=0},[]);return{lines:s,loading:i,error:l,autoScroll:_.current,setAutoScroll:f,refresh:()=>h(!1),clear:v}}function dt(e){const n={state:"idle",totalFiles:0,archivedFiles:0,currentFile:null,startTime:null,elapsedTime:null,lastActivity:null,message:null};if(!e||e.length===0)return n;const r=e.slice(-100);r.length>0&&(n.lastActivity=ht(r[r.length-1]));for(let o=r.length-1;o>=0;o--){const i=r[o],a=i.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/);if(a){const u=parseInt(a[2],10),d=a[3]?parseInt(a[3],10):0;n.totalFiles=u+d;break}const l=i.match(/Archiving (\d+)(?: track mode)? file\(s\)/);l&&!i.includes("completed")&&(n.totalFiles=parseInt(l[1],10))}let s=!1;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Finished copying music")||i.includes("Copying music failed"))break;if(i.includes("Syncing music from archive")||i.includes("Starting music sync")){s=!0;break}if(i.includes("Connected usb to host")||i.includes("Waiting for archive to be unreachable"))break}if(s)return n.state="archiving",n.message="Syncing music...",n;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Archiving completed successfully")){n.state="complete",n.message="Archive completed";break}if(i.includes("Finished copying music")){n.state="complete",n.message="Music sync complete";break}if(i.includes("Copied")&&i.includes("file(s)")){const a=i.match(/Copied (\d+)/);if(a){n.state="complete",n.archivedFiles=parseInt(a[1],10),n.message=`Copied ${n.archivedFiles} files`;break}}if(i.includes("Starting recording archiving")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving recordings...";break}if(i.includes("Archiving...")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving files...";break}if(i.includes("Syncing music from archive")){n.state="archiving",n.message="Syncing music...";break}if(i.includes("Copying music")){n.state="archiving",n.message="Copying music...";break}if(i.includes("Finished archiving")){n.state="complete",n.message="Archive complete";break}if(i.includes("Running fsck")){n.state="archiving",n.message="Checking filesystem...";break}if(i.includes("Checking saved folder count")){n.state="archiving",n.message="Scanning files...";break}if(i.includes("Waiting for archive to be reachable")){n.state="connecting",n.message="Connecting to archive server...";break}if(i.includes("Archive is reachable")){n.state="archiving",n.message="Connected, preparing...";break}if(i.includes("Disconnecting usb from host")){n.state="archiving",n.message="Disconnecting from vehicle...";break}if(i.includes("Connected usb to host")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("Waiting for archive to be unreachable")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("snapshot")){n.state="idle",n.message="Managing snapshots...";break}if(i.includes("low space, deleting")){n.state="idle",n.message="Cleaning up old snapshots...";break}if(i.includes("waiting up to")&&i.includes("idle interval")){n.state="idle",n.message="Waiting for idle...";break}if(i.includes("mass storage process")){n.state="idle",n.message="Ready";break}if((i.includes("error")||i.includes("failed"))&&!i.includes("sntp failed")){n.state="error",n.message="Error occurred";break}}return n}function ht(e){const n=e.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/);if(n){const r=new Date(n[1]);if(!isNaN(r.getTime()))return r}return null}function ut(e=!1,n=1500){const[r,s]=k({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""}),[o,i]=k(!1),[a,l]=k(null),u=H(e),d=B(async()=>{try{const _=await xn();s(_),l(null)}catch(_){l(_.message)}finally{i(!1)}},[]);return F(()=>{u.current=e},[e]),F(()=>{if(!e){s({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""});return}i(!0),d();const _=setInterval(()=>{u.current&&d()},n);return()=>clearInterval(_)},[e,n,d]),{...r,loading:o,error:a,refresh:d}}function ft(e){if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],r=1024,s=Math.floor(Math.log(e)/Math.log(r));return`${(e/Math.pow(r,s)).toFixed(s>0?2:0)} ${n[s]}`}function _t(e){return!e||e==="0:00:00"?"":`~${e.replace(/^0:/,"")} remaining`}function pt({storage:e,total:n,free:r,config:s}){var c;const o=h=>{if(h===0||h===null||h===void 0)return"0 B";const f=h/(1024*1024*1024);return f>=1?`${f.toFixed(1)} GB`:`${(h/(1024*1024)).toFixed(0)} MB`},i=((c=e==null?void 0:e.total)==null?void 0:c.free)||r,a=[],l=(h,f,v,y)=>{(s==null?void 0:s[y])!=="yes"||!h||a.push({type:f,label:v,allocated:h.total,used:h.used,mounted:h.mounted,isCached:h.cached||!1})};if(l(e==null?void 0:e.cam,"teslacam","TeslaCam","has_cam"),l(e==null?void 0:e.music,"music","Music","has_music"),l(e==null?void 0:e.lightshow,"lightshow","LightShow","has_lightshow"),l(e==null?void 0:e.boombox,"boombox","Boombox","has_boombox"),a.length===0&&!i)return t("div",{className:"storage-bar-container",children:t("div",{className:"storage-info",children:"No storage data available"})});const d=a.reduce((h,f)=>h+(f.allocated||0),0)+i,_=a.map(h=>({type:h.type,label:h.label,bytes:h.allocated,percent:Math.max(1,Math.round(h.allocated/d*100))}));return i>0&&_.push({type:"free",label:"Free",bytes:i,percent:Math.max(1,Math.round(i/d*100))}),t("div",{className:"storage-bar-container",children:[t("div",{className:"storage-bar",children:_.map((h,f)=>t("div",{className:`storage-segment ${h.type}`,style:{width:`${h.percent}%`},title:`${h.label}: ${o(h.bytes)}`},f))}),t("div",{className:"storage-legend",children:[a.map((h,f)=>{const v=h.used!==null&&h.allocated>0?Math.round(h.used/h.allocated*100):null;return t("div",{className:"storage-legend-item",children:[t("div",{className:`storage-legend-dot ${h.type}`}),t("span",{className:"storage-legend-label",children:h.label}),t("span",{className:"storage-legend-value",children:[o(h.allocated),h.used!==null&&t("span",{className:"storage-legend-used",children:[" ","(",o(h.used)," used",h.isCached?"~":"",v!==null&&`, ${v}%`,")"]})]})]},f)}),t("div",{className:"storage-legend-item",children:[t("div",{className:"storage-legend-dot free"}),t("span",{className:"storage-legend-label",children:"Free"}),t("span",{className:"storage-legend-value",children:o(i)})]})]}),a.some(h=>h.isCached)&&t("div",{className:"storage-note",children:"~ Last known (drive not currently mounted)"})]})}function mt({syncStatus:e,onTriggerSync:n,loading:r,musicProgress:s}){const{state:o,totalFiles:i,archivedFiles:a,message:l,elapsedTime:u,lastActivity:d}=e,_=(l==null?void 0:l.toLowerCase().includes("music"))&&o==="archiving",c=(s==null?void 0:s.active)&&(s==null?void 0:s.percentage)>0,f=(()=>{switch(o){case"idle":return{label:"Idle",color:"idle",description:"Ready to archive"};case"connecting":return{label:"Connecting",color:"connecting",description:l||"Connecting to server..."};case"archiving":return{label:"Archiving",color:"archiving",description:l||"Syncing files..."};case"complete":return{label:"Complete",color:"idle",description:l||"Archive complete"};case"error":return{label:"Error",color:"error",description:l||"Archive failed"};default:return{label:"Unknown",color:"idle",description:"Status unknown"}}})(),v=o==="archiving"||o==="connecting",y=_&&c,b=o==="archiving"&&i>0&&!y;let m=null;y?m=s.percentage:b&&a>0&&(m=Math.min(Math.round(a/i*100),100));const p=w=>{if(!w)return null;const L=Math.floor((new Date-w)/1e3);return L<60?"just now":L<3600?`${Math.floor(L/60)}m ago`:L<86400?`${Math.floor(L/3600)}h ago`:w.toLocaleDateString()};return t("div",{className:"sync-status-card",children:[t("div",{className:"sync-status-header",children:[t("span",{className:"sync-status-title",children:"Sync Status"}),t("div",{className:"sync-status-indicator",children:[t("div",{className:`sync-status-dot ${f.color}`}),t("span",{children:f.label})]})]}),t("div",{className:"sync-status-description",children:f.description}),(y||b||v)&&t("div",{className:"sync-progress-bar",children:t("div",{className:"sync-progress-fill",style:{width:m!==null?`${m}%`:"100%",animation:m===null?"pulse 1.5s ease-in-out infinite":"none",opacity:m===null?.6:1}})}),y&&t("div",{className:"sync-details",children:[t("div",{className:"sync-progress-main",children:[ft(s.bytesTransferred)," transferred",m!==null&&` (${m}%)`]}),(s.speed||s.eta)&&t("div",{className:"sync-progress-secondary",children:[s.speed&&t("span",{children:s.speed}),s.speed&&s.eta&&t("span",{children:" ยท "}),s.eta&&t("span",{children:_t(s.eta)})]})]}),b&&t("div",{className:"sync-details",children:[a," / ",i," files",m!==null&&` (${m}%)`]}),!v&&!b&&!y&&d&&t("div",{className:"sync-details",children:["Last sync: ",p(d)]}),u&&o==="complete"&&t("div",{className:"sync-details",children:["Completed in ",u]}),(o==="idle"||o==="complete"||o==="error")&&t("button",{className:"btn btn-primary btn-sm",onClick:n,disabled:r,style:{marginTop:"0.75rem"},children:[t(Jn,{style:{width:14,height:14},className:r?"spinning":""}),t("span",{children:r?"Starting...":"Sync Now"})]})]})}function vt({status:e,computed:n,config:r,storage:s,onRefresh:o}){const[i,a]=k(!1),{lines:l}=Qe("archiveloop.log",3e3,!0),u=dt(l),d=ve(()=>{var f;return u.state==="archiving"&&((f=u.message)==null?void 0:f.toLowerCase().includes("music"))},[u.state,u.message]),_=ut(d,1500),c=B(async()=>{a(!0);try{await Nn(),setTimeout(o,1e3)}catch(f){console.error("Trigger sync failed:",f)}finally{a(!1)}},[o]),h=[{key:"cam",label:"TeslaCam",icon:Xe,enabled:(r==null?void 0:r.has_cam)==="yes"},{key:"music",label:"Music",icon:nt,enabled:(r==null?void 0:r.has_music)==="yes"},{key:"lightshow",label:"LightShow",icon:Me,enabled:(r==null?void 0:r.has_lightshow)==="yes"},{key:"boombox",label:"Boombox",icon:Me,enabled:(r==null?void 0:r.has_boombox)==="yes"}];return(r==null?void 0:r.uses_ble)==="yes"&&h.push({key:"ble",label:"BLE",icon:Ye,enabled:!0}),t("div",{className:"dashboard-content",children:[t("div",{className:"dashboard-section",children:[t("div",{className:"section-title",children:"Configured Features"}),t("div",{className:"features-row",children:h.map(({key:f,label:v,enabled:y})=>t("div",{className:`feature-badge ${y?"enabled":"disabled"}`,children:v},f))})]}),t("div",{className:"dashboard-section",children:t("div",{className:"card",children:[t("div",{className:"card-header",children:[t("span",{className:"card-title",children:"Storage"}),t("span",{className:"card-value",children:[(n==null?void 0:n.diskTotalGB)||"0"," GB"]})]}),t(pt,{storage:s,total:e!=null&&e.total_space?parseInt(e.total_space,10):0,free:e!=null&&e.free_space?parseInt(e.free_space,10):0,config:r})]})}),t("div",{className:"dashboard-section",children:t(mt,{syncStatus:u,onTriggerSync:c,loading:i,musicProgress:_})})]})}const he={front:"Front",back:"Back",left_repeater:"Left Repeater",right_repeater:"Right Repeater",left_pillar:"Left Pillar",right_pillar:"Right Pillar"},Fe=[{id:"6",name:"All Cameras",cameras:Object.keys(he),cols:3},{id:"4-front",name:"Front Focus",cameras:["front","left_repeater","right_repeater","back"],cols:2},{id:"4-rear",name:"Rear Focus",cameras:["back","left_repeater","right_repeater","front"],cols:2},{id:"2-side",name:"Side View",cameras:["left_repeater","right_repeater"],cols:2},{id:"1-front",name:"Front Only",cameras:["front"],cols:1},{id:"1-back",name:"Rear Only",cameras:["back"],cols:1}];function gt(){const[e,n]=k(null),[r,s]=k(!0),[o,i]=k(null),[a,l]=k("SentryClips"),[u,d]=k(null),[_,c]=k(!1),[h,f]=k(0),[v,y]=k(0),[b,m]=k(Fe[0]),[p,w]=k(!1),[x,L]=k(null),C=H({});F(()=>{U()},[]);const U=async()=>{try{s(!0);const g=await kn();n(g);for(const T of["SentryClips","SavedClips","RecentClips"]){const W=Object.keys(g[T]||{});if(W.length>0){l(T),d(W[0]);break}}}catch(g){i(g.message)}finally{s(!1)}};F(()=>{a==="SentryClips"&&u?En(u).then(L):L(null)},[a,u]);const D=B(()=>{var W;if(!e||!u)return{};const g=((W=e[a])==null?void 0:W[u])||[],T={};for(const q of g)for(const ie of Object.keys(he))if(q.includes(`-${ie}.mp4`)||q.includes(`_${ie}.mp4`)){T[ie]=Fn(a,u,q);break}return T},[e,a,u])(),z=B(()=>{Object.values(C.current).forEach(g=>{g&&g.play()}),c(!0)},[]),M=B(()=>{Object.values(C.current).forEach(g=>{g&&g.pause()}),c(!1)},[]),I=B(g=>{Object.values(C.current).forEach(T=>{T&&(T.currentTime=g)}),f(g)},[]),en=B(()=>{I(Math.max(0,h-10))},[h,I]),nn=B(()=>{I(Math.min(v,h+30))},[h,v,I]),tn=B(g=>{f(g.target.currentTime)},[]),rn=B(g=>{g.target.duration&&g.target.duration!==1/0&&y(g.target.duration)},[]),sn=B(g=>{const T=g.currentTarget.getBoundingClientRect(),q=(g.clientX-T.left)/T.width*v;I(q)},[v,I]),ge=g=>{if(!g||isNaN(g))return"0:00";const T=Math.floor(g/60),W=Math.floor(g%60);return`${T}:${W.toString().padStart(2,"0")}`},on=e!=null&&e[a]?Object.keys(e[a]).sort().reverse():[];return r?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"Loading recordings..."})}):o?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:[t("span",{className:"status-unhealthy",children:["Error: ",o]}),t("button",{className:"btn",onClick:U,style:{marginTop:"1rem"},children:"Retry"})]}):!u||Object.keys(D).length===0?t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"No recordings available"})}):t("div",{className:"video-viewer",children:[t("div",{className:"video-selector",style:{display:"flex",gap:"0.5rem",padding:"8px 12px",background:"#1a1a1a",borderBottom:"1px solid #333",alignItems:"center",flexWrap:"wrap"},children:[t("select",{value:a,onChange:g=>{l(g.target.value);const T=Object.keys(e[g.target.value]||{});d(T[0]||null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px"},children:[t("option",{value:"SentryClips",children:"Sentry Clips"}),t("option",{value:"SavedClips",children:"Saved Clips"}),t("option",{value:"RecentClips",children:"Recent Clips"})]}),t("select",{value:u||"",onChange:g=>d(g.target.value),style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",flex:1,maxWidth:"250px"},children:on.map(g=>t("option",{value:g,children:yt(g)},g))}),t("div",{style:{position:"relative"},children:[t("button",{className:"btn btn-sm",onClick:()=>w(!p),style:{background:"#333",borderColor:"#444"},children:[t(rt,{}),t("span",{children:b.name}),t(Je,{})]}),p&&t("div",{style:{position:"absolute",top:"100%",right:0,background:"#2a2a2a",border:"1px solid #444",borderRadius:"6px",marginTop:"4px",zIndex:100,minWidth:"150px"},children:Fe.map(g=>t("button",{onClick:()=>{m(g),w(!1)},style:{display:"block",width:"100%",padding:"8px 12px",background:b.id===g.id?"#0095f6":"transparent",color:"#fff",border:"none",textAlign:"left",cursor:"pointer",fontSize:"13px"},children:g.name},g.id))})]}),x&&x.city&&t("div",{style:{display:"flex",alignItems:"center",gap:"4px",color:"#888",fontSize:"12px",marginLeft:"auto"},children:[t(et,{style:{width:14,height:14}}),t("span",{children:x.city})]})]}),t("div",{className:`video-grid layout-${b.cameras.length}`,style:{gridTemplateColumns:`repeat(${b.cols}, 1fr)`},children:b.cameras.map(g=>t("div",{className:"video-cell",children:[D[g]?t("video",{ref:T=>{C.current[g]=T},src:D[g],onTimeUpdate:tn,onDurationChange:rn,onEnded:M,muted:!0,playsInline:!0}):t("div",{style:{color:"#666",fontSize:"12px"},children:"No video"}),t("div",{className:"video-cell-label",children:he[g]})]},g))}),t("div",{className:"video-controls",children:[t("button",{className:"btn btn-sm",onClick:en,title:"Skip back 10s",children:t(Vn,{})}),t("button",{className:"video-play-btn",onClick:_?M:z,children:_?t(On,{}):t(Pn,{})}),t("button",{className:"btn btn-sm",onClick:nn,title:"Skip forward 30s",children:t(zn,{})}),t("div",{className:"video-time",children:[ge(h)," / ",ge(v)]}),t("div",{className:"video-timeline",onClick:sn,children:t("div",{className:"video-timeline-progress",style:{width:v>0?`${h/v*100}%`:"0%"}})})]})]})}function yt(e){const n=e.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/);if(n){const[,r,s,o,i,a,l]=n,u=new Date(r,s-1,o,i||0,a||0,l||0);return i?u.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):u.toLocaleDateString([],{month:"short",day:"numeric",year:"numeric"})}return e}function bt({config:e}){const n=H(null),r=H(null),[s,o]=k(!!window.FileBrowser);F(()=>{if(!document.querySelector('link[href="/filebrowser.css"]')){const d=document.createElement("link");d.rel="stylesheet",d.href="/filebrowser.css",document.head.appendChild(d)}},[]),F(()=>{if(window.FileBrowser){o(!0);return}const d=document.createElement("script");d.src="/filebrowser.js",d.onload=()=>o(!0),d.onerror=()=>console.error("Failed to load filebrowser.js"),document.head.appendChild(d)},[]);const i=(e==null?void 0:e.has_music)==="yes",a=(e==null?void 0:e.has_lightshow)==="yes",l=(e==null?void 0:e.has_boombox)==="yes";return F(()=>{if(!s||!n.current)return;const d=[];if(i&&d.push({path:"fs/Music",label:"Music"}),a&&d.push({path:"fs/LightShow",label:"LightShow"}),l&&d.push({path:"fs/Boombox",label:"Boombox"}),d.length!==0&&!r.current){try{r.current=new window.FileBrowser(n.current,d)}catch(_){console.error("Failed to initialize FileBrowser:",_)}return()=>{n.current&&(n.current.innerHTML=""),r.current=null}}},[s,i,a,l]),(e==null?void 0:e.has_music)==="yes"||(e==null?void 0:e.has_lightshow)==="yes"||(e==null?void 0:e.has_boombox)==="yes"?s?t("div",{ref:n,style:{width:"100%",height:"calc(100% - 4px)",minHeight:"400px",flex:1,display:"flex",position:"relative"}}):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",minHeight:"400px",color:"#9ca3af",fontSize:"14px"},children:"Loading file browser..."}):t("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:"#9ca3af",fontSize:"14px"},children:[t("svg",{style:{width:32,height:32,marginBottom:8,color:"#d1d5db"},viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})}),t("span",{children:"No file drives configured"})]})}const J={archiveloop:{label:"Archive Log",file:"archiveloop.log",description:"Sync and archive operations"},setup:{label:"Setup Log",file:"teslausb-headless-setup.log",description:"Initial setup and configuration"},diagnostics:{label:"Diagnostics",file:"diagnostics.txt",description:"System diagnostics report"}};function kt(){const[e,n]=k("archiveloop"),[r,s]=k(!1),[o,i]=k(null),a=H(null),l=e!=="diagnostics",{lines:u,loading:d,error:_,refresh:c,clear:h}=Qe(l?J[e].file:"",2e3,l);F(()=>{a.current&&l&&(a.current.scrollTop=a.current.scrollHeight)},[u,l]);const f=B(async()=>{s(!0);try{await Tn(),await new Promise(w=>setTimeout(w,2e3));const p=await Bn();i(p)}catch(p){i(`Error generating diagnostics: ${p.message}`)}finally{s(!1)}},[]);F(()=>{e==="diagnostics"&&!o&&f()},[e,o,f]);const v=B(()=>{const p=e==="diagnostics"?o:u.join(` -`),w=J[e].file,x=new Blob([p],{type:"text/plain"}),L=URL.createObjectURL(x),C=document.createElement("a");C.href=L,C.download=w,document.body.appendChild(C),C.click(),document.body.removeChild(C),URL.revokeObjectURL(L)},[e,u,o]),y=p=>{const w=p.toLowerCase();return w.includes("error")||w.includes("failed")||w.includes("fatal")?"error":w.includes("warning")||w.includes("warn")?"warning":w.includes("success")||w.includes("completed")||w.includes("finished")?"success":""},b=J[e],m=e==="diagnostics"?(o==null?void 0:o.split(` -`))||[]:u;return t("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[t("div",{style:{display:"flex",gap:"0.5rem",marginBottom:"1rem",flexWrap:"wrap"},children:Object.entries(J).map(([p,w])=>t("button",{className:`btn ${e===p?"btn-primary":""}`,onClick:()=>n(p),children:[p==="diagnostics"?t(Qn,{}):t(Ze,{}),t("span",{children:w.label})]},p))}),t("div",{className:"log-viewer",style:{flex:1},children:[t("div",{className:"log-viewer-header",children:[t("div",{className:"log-viewer-title",children:[b.label,t("span",{style:{fontWeight:400,marginLeft:"8px",opacity:.7},children:["โ€” ",b.description]})]}),t("div",{className:"log-viewer-actions",children:[e==="diagnostics"?t("button",{className:"btn btn-sm log-action-btn",onClick:f,disabled:r,children:[t(de,{className:r?"spinning":""}),t("span",{children:"Regenerate"})]}):t(R,{children:[t("button",{className:"btn btn-sm log-action-btn",onClick:c,disabled:d,title:"Refresh",children:t(de,{className:d?"spinning":""})}),t("button",{className:"btn btn-sm log-action-btn",onClick:h,title:"Clear",children:t(Gn,{})})]}),t("button",{className:"btn btn-sm log-action-btn",onClick:v,disabled:m.length===0,title:"Download",children:t(qn,{})})]})]}),t("div",{className:"log-viewer-content",ref:a,children:d&&m.length===0||r?t("div",{style:{color:"#888",fontStyle:"italic"},children:"Loading..."}):_?_.includes("not found")?t("div",{style:{color:"#888",fontStyle:"italic"},children:["Log file not available. ",e==="setup"&&"The setup log is only present during initial setup."]}):t("div",{style:{color:"#f87171"},children:["Error: ",_]}):m.length===0?t("div",{style:{color:"#888",fontStyle:"italic"},children:"No log entries"}):m.map((p,w)=>t("div",{className:`log-line ${y(p)}`,children:p},w))})]}),l&&u.length>0&&t("div",{style:{marginTop:"0.5rem",fontSize:"11px",color:"#666",display:"flex",gap:"1rem"},children:[t("span",{children:[u.length," lines"]}),t("span",{children:"Auto-refreshing every 2s"})]})]})}function wt(){return t("div",{className:"loading-container",children:[t("div",{className:"spinner"}),t("span",{children:"Loading..."})]})}const E={DASHBOARD:"dashboard",VIEWER:"viewer",FILES:"files",LOGS:"logs"};function Nt(){const[e,n]=k(E.DASHBOARD),[r,s]=k(!1),{status:o,config:i,storage:a,computed:l,loading:u,error:d,lastUpdate:_,refresh:c}=Rn(5e3),h=[];return h.push({id:E.DASHBOARD,label:"Dashboard"}),(i==null?void 0:i.has_cam)==="yes"&&h.push({id:E.VIEWER,label:"Viewer"}),((i==null?void 0:i.has_music)==="yes"||(i==null?void 0:i.has_lightshow)==="yes"||(i==null?void 0:i.has_boombox)==="yes")&&h.push({id:E.FILES,label:"Files"}),h.push({id:E.LOGS,label:"Logs"}),u&&!o?t(wt,{}):d&&!o?t("div",{className:"error-container",children:[t("span",{children:["Failed to load: ",d]}),t("button",{className:"retry-btn",onClick:c,children:"Retry"})]}):t("div",{className:"app-shell",children:[t("div",{className:"app-topbar",children:[t("div",{className:"app-topbar-title",children:[t(Wn,{}),t("span",{children:"TeslaUSB"})]}),t("div",{className:"app-topbar-actions",children:t("span",{className:`app-status ${l!=null&&l.drivesActive?"healthy":"warning"}`,children:l!=null&&l.drivesActive?"Connected":"Disconnected"})})]}),t(ot,{tabs:h,activeTab:e,onTabChange:n,lastUpdate:_,onRefresh:c}),t("div",{className:"mobile-quick-status",children:[t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.drivesActive?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"USB"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.wifiConnected?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"WiFi"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.cpuTempC&&parseFloat(l.cpuTempC)<70?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:[l==null?void 0:l.cpuTempC,"ยฐC"]})]})]}),t("div",{className:"app-body",children:[e===E.DASHBOARD&&t(at,{status:o,computed:l,config:i,expanded:r,onToggle:()=>s(!r),onRefresh:c}),t("main",{className:"app-main",children:[e===E.DASHBOARD&&t(vt,{status:o,computed:l,config:i,storage:a,onRefresh:c}),e===E.VIEWER&&(i==null?void 0:i.has_cam)==="yes"&&t(gt,{}),e===E.FILES&&t(bt,{config:i}),e===E.LOGS&&t(kt,{})]})]})]})}fn(t(Nt,{}),document.getElementById("app")); diff --git a/teslausb-www-react/dist/assets/index-Vc9in5g7.css b/teslausb-www-react/dist/assets/index-Vc9in5g7.css new file mode 100644 index 00000000..a8483914 --- /dev/null +++ b/teslausb-www-react/dist/assets/index-Vc9in5g7.css @@ -0,0 +1 @@ +*{margin:0;padding:0;box-sizing:border-box}body{font-family:Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background-color:#f5f5f5;color:#333;overflow:hidden}@media(max-width:767px){body{overflow:visible;height:auto}}.app-shell{display:flex;flex-direction:column;min-height:100vh;overflow:hidden}@media(max-width:767px){.app-shell{overflow:visible;height:auto;min-height:100vh}}.app-topbar{display:flex;justify-content:space-between;align-items:center;background-color:#1f2937;color:#fff;padding:.75rem 1rem;border-bottom:1px solid #374151}.app-topbar-title{font-size:1.1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}.app-topbar-title svg{width:24px;height:24px}.app-topbar-actions{display:flex;gap:1rem;align-items:center}.app-dashboard{display:flex;flex-direction:column;height:100vh;background-color:#f5f5f5}.app-header{background-color:#fff;border-bottom:1px solid #e1e5e9;padding:.75rem 1rem;display:flex;justify-content:space-between;align-items:center;height:60px;flex-shrink:0;position:sticky;top:0;z-index:40;backdrop-filter:saturate(180%) blur(4px)}.app-header-left{display:flex;align-items:center;gap:.5rem}.app-header-icon{width:20px;height:20px;color:#06c}.app-header-title{font-size:16px;font-weight:600;color:#333;letter-spacing:.1px}.app-header-subtitle{font-size:14px;color:#666;margin-left:.5rem}.app-header-right{display:flex;align-items:center;gap:1rem}.dashboard-nav{display:flex;gap:.5rem;align-items:center}.nav-link{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:6px;font-size:14px;font-weight:500;color:#6b7280;text-decoration:none;transition:all .15s ease;border:1px solid transparent;position:relative;cursor:pointer;background:none}.nav-link:hover{background-color:#f9fafb;color:#374151}.nav-link.active{background-color:#0095f6;color:#fff;border-color:#007dd1}.nav-link.active svg{color:#fff}.nav-link svg{color:#6b7280;transition:color .15s ease;width:16px;height:16px}.nav-link:hover svg{color:#374151}.nav-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;background-color:#ef4444;color:#fff;font-size:11px;font-weight:700;border-radius:9px;margin-left:4px;line-height:1}.nav-link.active .nav-badge{background-color:#fff;color:#ef4444}.app-last-update{font-size:13px;color:#6b7280}.app-status{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.3px}.app-status.healthy{color:#065f46;background:#d1fae5;border:1px solid #a7f3d0}.app-status.unhealthy{color:#7f1d1d;background:#fee2e2;border:1px solid #fecaca}.app-status.warning{color:#92400e;background:#fef3c7;border:1px solid #fde68a}.app-body{display:flex;flex:1;overflow:hidden}.app-sidebar{width:280px;background-color:#fff;border-right:1px solid #e1e5e9;padding:1rem;overflow-y:auto;overflow-x:hidden;flex-shrink:0;max-height:calc(100vh - 60px);-webkit-overflow-scrolling:touch}.device-info{display:flex;flex-direction:column;border-radius:10px;border:1px solid #e5e7eb;background:#fff;margin-bottom:1rem}.device-header{display:flex;align-items:center;gap:.5rem;padding:10px 12px;border-bottom:1px solid #eef2f7;font-size:14px;font-weight:600;color:#1f2937}.device-header svg{width:16px;height:16px;color:#6b7280}.info-list{display:flex;flex-direction:column;padding:8px 12px}.info-item{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f0f0f0;font-size:13px}.info-item:last-child{border-bottom:none}.info-item.clickable{cursor:pointer;transition:background-color .15s ease;margin:0 -12px;padding:6px 12px;border-radius:4px}.info-item.clickable:hover{background-color:#f3f4f6}.info-item.clickable:active{background-color:#e5e7eb}.toggle-btn{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:600;line-height:1;border:1px solid #007dd1;background-color:#0095f6;color:#fff;cursor:pointer;transition:all .15s ease}.toggle-btn svg{flex-shrink:0;vertical-align:middle;margin-right:4px;color:#fff}.toggle-btn:hover:not(:disabled){background-color:#007dd1;border-color:#006bbd}.toggle-btn:disabled{opacity:.6;cursor:not-allowed}.toggle-btn.active{background-color:#dcfce7;border-color:#22c55e;color:#166534}.toggle-btn.danger{background-color:#fee2e2;border-color:#f87171;color:#b91c1c}.ui-switch-link{color:#0095f6;text-decoration:none;font-size:13px;padding:4px 0;display:block;width:100%}.ui-switch-link:hover{text-decoration:underline}.toggle-btn.danger:hover:not(:disabled){background-color:#fecaca;border-color:#ef4444}.toggle-btn.primary{background-color:#eff6ff;border-color:#3b82f6;color:#1d4ed8}.toggle-btn.primary:hover:not(:disabled){background-color:#dbeafe;border-color:#2563eb}.sidebar-action{margin-top:8px;padding:0}.action-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;border-radius:6px;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .15s ease}.action-btn:disabled{opacity:.6;cursor:not-allowed}.action-btn.connected{background-color:#dcfce7;color:#166534;border:1px solid #22c55e}.action-btn.connected:hover:not(:disabled){background-color:#bbf7d0}.action-btn.disconnected{background-color:#fef2f2;color:#991b1b;border:1px solid #f87171}.action-btn.disconnected:hover:not(:disabled){background-color:#fee2e2}.action-btn.restart{background-color:#f3f4f6;color:#374151;border:1px solid #d1d5db}.action-btn.restart:hover:not(:disabled){background-color:#e5e7eb;border-color:#9ca3af}.info-label{color:#6b7280;font-weight:500}.info-value{color:#111827;font-weight:600;text-align:right}.info-value-with-action{display:flex;align-items:center;gap:8px}.speed-result{color:#111827;font-weight:600;font-size:12px}.small-text{font-size:11px!important;line-height:1.2}.compact-date{font-size:10px!important;line-height:1.1;white-space:nowrap;color:#4b5563}.progress-container{margin-top:.5rem}.progress-bar{width:100%;height:8px;background-color:#eef2f7;border-radius:999px;overflow:hidden;margin-bottom:.25rem}.progress-fill{height:100%;border-radius:999px;transition:width .3s ease}.progress-fill.blue{background-color:#3b82f6}.progress-fill.green{background-color:#10b981}.progress-fill.yellow{background-color:#f59e0b}.progress-fill.red{background-color:#ef4444}.progress-text{font-size:10px;color:#6b7280;font-weight:500;margin-top:6px;text-align:right}.status-healthy{color:#00b04f!important}.status-degraded{color:#ff9800!important}.status-unhealthy{color:#e74c3c!important}.app-main{flex:1;background-color:#fff;padding:1rem;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;max-height:calc(100vh - 60px)}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1px;background-color:#e1e5e9;border:1px solid #e1e5e9;border-radius:4px;overflow:visible;margin-bottom:1rem}.stats-section{background-color:#fff;padding:1rem}.stats-section h3{font-size:12px;font-weight:600;color:#666;text-transform:uppercase;letter-spacing:.5px;margin-bottom:.75rem}.stat-data{display:flex;flex-direction:column;gap:.5rem}.primary-stat{font-size:24px;font-weight:700;color:#333;line-height:1}.stat-details{display:flex;flex-direction:column;gap:.25rem}.stat-details span{font-size:11px;color:#666}.stat-details span:first-child{color:#333;font-weight:500;font-size:12px}.mono-value{font-family:Monaco,Menlo,Consolas,monospace;color:#333;font-weight:600;font-size:12px}.card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:12px;box-shadow:0 1px #00000005;transition:transform .08s ease,box-shadow .12s ease,border-color .12s ease}.card:hover{transform:translateY(-1px);box-shadow:0 6px 18px #0000000f;border-color:#dee3ea}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.card-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.card-value{font-size:22px;font-weight:700;color:#111827;line-height:1}.card-sub{font-size:11px;color:#666;margin-top:4px}.dashboard-section{margin-bottom:1rem}.section-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px;margin-bottom:.5rem}.features-row{display:flex;flex-wrap:wrap;gap:8px}.feature-badge{display:inline-block;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:500;line-height:1;text-align:center}.feature-badge.enabled{background:#ecfdf5;color:#059669;border:1px solid #a7f3d0}.feature-badge.disabled{background:#f3f4f6;color:#9ca3af;border:1px solid #e5e7eb}.actions-row{display:flex;flex-wrap:wrap;gap:10px}.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:14px}.chart-container{background-color:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:1rem;margin-bottom:1rem}.chart-header{padding:10px 12px;border-bottom:1px solid #eef2f7;margin:-1rem -1rem 1rem}.chart-title{font-size:14px;font-weight:600;color:#1f2937}.loading-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#666}.spinner{width:20px;height:20px;border:2px solid #e5e7eb;border-top-color:#0095f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.spinning{animation:spin 1s linear infinite}.error-container{display:flex;align-items:center;justify-content:center;height:100vh;gap:.5rem;font-size:14px;color:#e74c3c}.retry-btn{background-color:#e74c3c;border:none;color:#fff;padding:.375rem .75rem;border-radius:4px;font-size:12px;cursor:pointer;margin-left:.5rem}.retry-btn:hover{background-color:#c0392b}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 12px;font-size:13px;font-weight:500;line-height:1.4;border-radius:6px;border:1px solid #cfd8e3;background:#fff;color:#374151;cursor:pointer;transition:all .15s ease-in-out;box-shadow:0 1px 2px #00000008}.btn svg{color:#374151;transition:color .15s ease-in-out;width:14px;height:14px}.btn:hover:not(:disabled){background:#0095f6;border-color:#007dd1;color:#fff;box-shadow:0 2px 8px #0000000f}.btn:hover:not(:disabled) svg{color:#fff}.btn:active:not(:disabled){background:#007dd1;border-color:#006bbd}.btn:disabled{opacity:.6;cursor:not-allowed;background:#f9fafb;color:#9ca3af;border-color:#e5e7eb}.btn-primary{background:#0095f6;border-color:#007dd1;color:#fff}.btn-primary svg{color:#fff}.btn-primary:hover:not(:disabled){background:#007dd1;border-color:#006bbd}.btn-danger{background:#ef4444;border-color:#dc2626;color:#fff}.btn-danger svg{color:#fff}.btn-danger:hover:not(:disabled){background:#dc2626;border-color:#b91c1c}.btn-dark{background:#333;border-color:#444;color:#fff}.btn-dark svg{color:#fff}.btn-dark:hover:not(:disabled){background:#444;border-color:#555;color:#fff}.btn-dark:hover:not(:disabled) svg{color:#fff}.btn-sm{padding:4px 8px;font-size:12px}.btn-lg{padding:10px 20px;font-size:15px}.storage-bar-container{margin:1rem 0}.storage-bar{display:flex;height:24px;border-radius:6px;overflow:hidden;background-color:#e5e7eb}.storage-segment{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#fff;transition:width .3s ease;min-width:0}.storage-segment.teslacam{background-color:#3b82f6}.storage-segment.music{background-color:#8b5cf6}.storage-segment.lightshow{background-color:#ec4899}.storage-segment.boombox{background-color:#f97316}.storage-segment.free{background-color:#d1d5db}.storage-legend{display:flex;flex-wrap:wrap;gap:1rem;margin-top:.75rem;font-size:12px}.storage-legend-item{display:flex;align-items:center;gap:6px}.storage-legend-dot{width:10px;height:10px;border-radius:2px}.storage-legend-dot.teslacam{background-color:#3b82f6}.storage-legend-dot.music{background-color:#8b5cf6}.storage-legend-dot.lightshow{background-color:#ec4899}.storage-legend-dot.boombox{background-color:#f97316}.storage-legend-dot.free{background-color:#d1d5db}.storage-legend-label{color:#374151;font-weight:500}.storage-legend-value{color:#6b7280}.storage-legend-used{color:#9ca3af;font-size:11px}.storage-legend-percent{margin-left:4px;color:#9ca3af;font-size:11px}.storage-note{margin-top:.5rem;font-size:10px;color:#9ca3af;font-style:italic}.sync-status-card{border:1px solid #e5e7eb;border-radius:10px;padding:1rem;background:#fff}.sync-status-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.sync-status-title{font-size:12px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.4px}.sync-status-indicator{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600}.sync-status-dot{width:8px;height:8px;border-radius:50%}.sync-status-dot.idle{background-color:#10b981}.sync-status-dot.connecting{background-color:#f59e0b;animation:pulse 1.5s ease-in-out infinite}.sync-status-dot.archiving{background-color:#3b82f6;animation:pulse 1s ease-in-out infinite}.sync-status-dot.error{background-color:#ef4444}.sync-status-description{font-size:13px;color:#6b7280;margin-bottom:.5rem}.sync-details{font-size:12px;color:#9ca3af}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.sync-progress-bar{height:8px;background-color:#e5e7eb;border-radius:4px;overflow:hidden;margin:.75rem 0}.sync-progress-fill{height:100%;background-color:#3b82f6;border-radius:4px;transition:width .3s ease}.sync-details{display:flex;flex-direction:column;gap:2px;font-size:11px;color:#6b7280}.sync-progress-main{font-weight:500}.sync-progress-secondary{font-size:10px;color:#9ca3af}.sync-file{max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:Monaco,Menlo,monospace}.log-viewer{background-color:#1e1e1e;border-radius:8px;font-family:Monaco,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;overflow:hidden;display:flex;flex-direction:column;height:400px}.log-viewer-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background-color:#2d2d2d;border-bottom:1px solid #3d3d3d}.log-viewer-title{color:#e0e0e0;font-size:13px;font-weight:600}.log-viewer-actions{display:flex;gap:8px}.log-action-btn{background:#4a4a4a!important;border-color:#666!important;color:#e0e0e0!important}.log-action-btn svg{color:#e0e0e0!important}.log-action-btn:hover:not(:disabled){background:#5a5a5a!important;border-color:#888!important}.log-action-btn:hover:not(:disabled) svg{color:#fff!important}.log-action-btn:disabled{opacity:.4}.log-viewer-content{flex:1;overflow-y:auto;padding:12px;color:#d4d4d4}.log-viewer-content::-webkit-scrollbar{width:8px}.log-viewer-content::-webkit-scrollbar-track{background:#1e1e1e}.log-viewer-content::-webkit-scrollbar-thumb{background:#4d4d4d;border-radius:4px}.log-line{white-space:pre-wrap;word-break:break-all}.log-line.error{color:#f87171}.log-line.warning{color:#fbbf24}.log-line.success{color:#34d399}.video-viewer{display:flex;flex-direction:column;height:100%;background:#000;border-radius:8px;overflow:hidden}.video-grid{display:grid;flex:1;gap:2px;background:#1a1a1a;padding:2px}.video-grid.layout-6{grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-4{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(2,1fr)}.video-grid.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.video-cell{position:relative;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden}.video-cell video{width:100%;height:100%;object-fit:contain}.video-cell-label{position:absolute;top:8px;left:8px;background:#000000b3;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500}.video-controls{display:flex;align-items:center;gap:1rem;padding:12px 16px;background:#1a1a1a;border-top:1px solid #333}.video-timeline{flex:1;height:6px;background:#333;border-radius:3px;cursor:pointer;position:relative}.video-timeline-progress{height:100%;background:#0095f6;border-radius:3px;transition:width .1s linear}.video-timeline-markers{position:absolute;inset:0;pointer-events:none}.video-timeline-marker{position:absolute;top:-2px;width:2px;height:10px;background:#ef4444;border-radius:1px}.video-play-btn{background:#0095f6;border:none;color:#fff;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s}.video-play-btn svg{width:18px;height:18px}.video-play-btn:hover{background:#007dd1}.video-time{color:#fff;font-size:13px;font-family:Monaco,Menlo,monospace;min-width:100px}.file-browser{display:flex;height:100%;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}.file-tree{width:250px;background:#f9fafb;border-right:1px solid #e5e7eb;overflow-y:auto}.file-tree-item{display:flex;align-items:center;gap:6px;padding:8px 12px;cursor:pointer;font-size:13px;color:#374151;border-bottom:1px solid #f0f0f0}.file-tree-item:hover{background:#f3f4f6}.file-tree-item.active{background:#e0f2fe;color:#0369a1}.file-tree-item svg{width:16px;height:16px;color:#6b7280;flex-shrink:0}.file-tree-item.active svg{color:#0369a1}.file-list{flex:1;overflow-y:auto;padding:1rem}.file-list-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #e5e7eb}.file-list-title{font-size:14px;font-weight:600;color:#1f2937}.file-list-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px}.file-item{display:flex;flex-direction:column;align-items:center;padding:12px;border:1px solid transparent;border-radius:8px;cursor:pointer;transition:all .15s}.file-item:hover{background:#f9fafb;border-color:#e5e7eb}.file-item.selected{background:#e0f2fe;border-color:#0ea5e9}.file-item-icon{width:48px;height:48px;margin-bottom:8px;color:#6b7280}.file-item-icon.folder{color:#f59e0b}.file-item-icon.video{color:#8b5cf6}.file-item-icon.audio{color:#ec4899}.file-item-name{font-size:12px;text-align:center;word-break:break-word;color:#374151}.file-item-size{font-size:10px;color:#9ca3af;margin-top:2px}.dropzone{border:2px dashed #d1d5db;border-radius:8px;padding:2rem;text-align:center;color:#6b7280;transition:all .15s}.dropzone svg{width:24px;height:24px}.dropzone.active{border-color:#0095f6;background:#f0f9ff;color:#0369a1}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:#fff;border-radius:12px;max-width:500px;width:90%;max-height:90vh;overflow:hidden;box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid #e5e7eb}.modal-title{font-size:16px;font-weight:600;color:#1f2937}.modal-close{background:none;border:none;cursor:pointer;color:#6b7280;padding:4px}.modal-close:hover{color:#374151}.modal-body{padding:1.5rem;overflow-y:auto}.modal-footer{display:flex;justify-content:flex-end;gap:.75rem;padding:1rem 1.5rem;border-top:1px solid #e5e7eb;background:#f9fafb}.toast-container{position:fixed;bottom:1rem;right:1rem;z-index:1100;display:flex;flex-direction:column;gap:.5rem}.toast{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;background:#1f2937;color:#fff;border-radius:8px;font-size:13px;box-shadow:0 10px 15px -3px #0000001a;animation:slideIn .2s ease}.toast.success{background:#059669}.toast.error{background:#dc2626}.toast.warning{background:#d97706}@keyframes slideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}@media(min-width:1024px)and (max-width:1366px){.app-sidebar{width:260px}.kpi-row{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.app-header-title{font-size:15px}}@media(min-width:768px)and (max-width:1023px){.app-body{flex-direction:row}.app-sidebar{width:220px;max-height:calc(100vh - 60px);overflow-y:auto}.kpi-row{grid-template-columns:repeat(2,1fr);gap:8px}.card{padding:10px}.card-value{font-size:18px}.card-title{font-size:11px}.card-sub{font-size:10px}.app-header-right{gap:8px}.app-last-update{font-size:11px}.chart-container{padding:12px;margin-bottom:8px}.chart-title{font-size:13px}}.mobile-quick-status{display:none}.desktop-status-bar{display:block}.sidebar-toggle-btn{display:none;background:none;border:none;color:#6b7280;cursor:pointer;padding:4px;margin-left:auto;transition:color .2s ease}.sidebar-toggle-btn:hover{color:#374151}@media(max-width:767px){.desktop-status-bar{display:none!important}.mobile-quick-status{display:flex;align-items:center;justify-content:space-around;padding:6px 8px;background-color:#f9fafb;border-bottom:1px solid #e5e7eb;gap:4px;flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch}.quick-status-item{display:flex;flex-direction:column;align-items:center;gap:3px;flex-shrink:0}.status-dot-mini{width:8px;height:8px;border-radius:50%;flex-shrink:0}.status-dot-mini.healthy{background-color:#10b981;box-shadow:0 0 4px #10b98180}.status-dot-mini.unhealthy{background-color:#ef4444;box-shadow:0 0 4px #ef444480}.quick-status-label{font-size:9px;color:#6b7280;font-weight:500;white-space:nowrap;text-align:center}.app-body{flex-direction:column;height:auto;overflow:visible}.app-sidebar{width:100%;height:auto;max-height:none;border-right:none;border-bottom:1px solid #e1e5e9;padding:8px;overflow:visible;overflow-x:hidden;flex-shrink:0;-webkit-overflow-scrolling:touch;transition:max-height .3s ease}.sidebar-toggle-btn{display:inline-flex}.app-sidebar:not(.expanded){max-height:60px;overflow:hidden}.app-sidebar:not(.expanded) .device-info:not(:first-child){display:none}.app-sidebar:not(.expanded) .info-item:nth-child(n+4){display:none}.app-sidebar.expanded{max-height:600px;overflow-y:auto}.device-info{margin-top:8px}.device-info:first-child{margin-top:0}.info-item{padding:4px 0;font-size:12px}.app-main{flex:1;padding:12px 8px 150px;overflow-y:auto;overflow-x:hidden;height:auto;min-height:0;max-height:none;-webkit-overflow-scrolling:touch}.kpi-row{grid-template-columns:repeat(2,1fr);gap:6px;margin-bottom:10px}.card{padding:6px 8px;min-height:75px}.card-header{margin-bottom:3px}.card-value{font-size:18px;line-height:1.1;margin-bottom:2px}.card-title{font-size:10px;letter-spacing:.3px}.card-sub{font-size:9px;line-height:1.3;margin-top:2px}.app-header{padding:8px 10px 10px!important;height:auto!important;flex-direction:column!important;gap:10px!important;align-items:stretch!important;position:static!important;top:auto!important;z-index:auto!important;backdrop-filter:none!important}.app-header-left{justify-content:center;padding-bottom:4px}.app-header-title{font-size:14px}.app-header-right{flex-direction:row;gap:6px;align-items:center;justify-content:space-between;width:100%;padding-bottom:2px}.dashboard-nav{gap:4px;flex:1;overflow-x:auto;-webkit-overflow-scrolling:touch}.nav-link{padding:7px 8px;font-size:11px;gap:4px;flex-shrink:0;justify-content:center;border-radius:4px}.nav-link svg{width:14px;height:14px}.nav-badge{min-width:16px;height:16px;font-size:10px;padding:0 4px;margin-left:2px}.app-last-update{font-size:10px}.app-status{font-size:9px;padding:1px 6px}.btn{padding:7px 8px;font-size:10px;gap:3px;white-space:nowrap;border-radius:4px;flex-shrink:0}.btn svg{width:12px;height:12px}.chart-container{padding:8px;margin-bottom:10px}.chart-title{font-size:11px;font-weight:600}.chart-header{margin-bottom:6px;padding-bottom:4px}.chart-container:last-child{margin-bottom:100px}.file-browser{flex-direction:column}.file-tree{width:100%;max-height:150px;border-right:none;border-bottom:1px solid #e5e7eb}.file-list-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.video-grid.layout-6{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(3,1fr)}.video-controls{flex-wrap:wrap;gap:.5rem;padding:8px 12px}.video-time{font-size:11px;min-width:80px}.log-viewer{height:auto;min-height:300px;flex:1}.log-viewer-content{font-size:10px}}@media(min-width:393px)and (max-width:430px){.kpi-row{grid-template-columns:1fr}.card{min-height:80px}.app-main{padding-bottom:140px}}@media(max-width:392px){.kpi-row{grid-template-columns:1fr}.app-header{padding:6px 8px;height:45px}.app-header-title{font-size:13px}.app-main{padding:6px 6px 160px}.card{padding:6px;min-height:70px}.card-value{font-size:14px}.chart-title{font-size:11px}}@media(pointer:coarse){.btn{min-height:44px;min-width:44px}.info-item{padding:8px 0;min-height:44px;align-items:center}.card{min-height:100px}.nav-link{min-height:44px}}@media(-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.card,.chart-container{border-width:.5px}.app-header{border-bottom-width:.5px}}@media(min-width:768px)and (max-width:1024px)and (orientation:landscape){.kpi-row{grid-template-columns:repeat(3,1fr)}.app-sidebar{width:200px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}@media(min-width:768px)and (max-width:1024px)and (orientation:portrait){.kpi-row{grid-template-columns:repeat(2,1fr)}.app-sidebar{width:240px;max-height:calc(100vh - 60px);overflow-y:auto}.device-info{margin-top:.75rem}.device-info:first-child{margin-top:0}}.app-dashboard{max-height:100vh;overflow:hidden}.dashboard-nav::-webkit-scrollbar{display:none}.dashboard-nav{-ms-overflow-style:none;scrollbar-width:none}.tab-content{display:none;flex:1;overflow:hidden}.tab-content.active{display:flex;flex-direction:column}.hidden{display:none!important}.flex{display:flex}.flex-1{flex:1}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.text-center{text-align:center}.text-right{text-align:right}.text-sm{font-size:12px}.text-xs{font-size:10px}.text-muted{color:#6b7280}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700} diff --git a/teslausb-www-react/dist/index.html b/teslausb-www-react/dist/index.html index 6d7286da..e97bc083 100644 --- a/teslausb-www-react/dist/index.html +++ b/teslausb-www-react/dist/index.html @@ -42,8 +42,8 @@ url('/fonts/lato-italic.woff2') format('woff2'); } - - + +
          diff --git a/teslausb-www-react/src/components/LogViewer.jsx b/teslausb-www-react/src/components/LogViewer.jsx index 85d4166a..8e55f859 100644 --- a/teslausb-www-react/src/components/LogViewer.jsx +++ b/teslausb-www-react/src/components/LogViewer.jsx @@ -58,10 +58,9 @@ export function LogViewer() { // Generate and load diagnostics const handleGenerateDiagnostics = useCallback(async () => { setDiagnosticsLoading(true); + setDiagnosticsContent(null); try { await generateDiagnostics(); - // Wait a moment for file to be generated - await new Promise(r => setTimeout(r, 2000)); const content = await fetchDiagnostics(); setDiagnosticsContent(content); } catch (e) { diff --git a/teslausb-www-react/src/components/VideoViewer.jsx b/teslausb-www-react/src/components/VideoViewer.jsx index 2c105b88..b4cb36a7 100644 --- a/teslausb-www-react/src/components/VideoViewer.jsx +++ b/teslausb-www-react/src/components/VideoViewer.jsx @@ -38,6 +38,7 @@ export function VideoViewer() { // Playback state const [selectedCategory, setSelectedCategory] = useState('SentryClips'); const [selectedSequence, setSelectedSequence] = useState(null); + const [selectedTimestamp, setSelectedTimestamp] = useState(null); // For RecentClips time selection const [playing, setPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); @@ -49,6 +50,7 @@ export function VideoViewer() { // Video refs for synchronized playback const videoRefs = useRef({}); + const masterCamera = useRef(null); // Load video list useEffect(() => { @@ -79,6 +81,11 @@ export function VideoViewer() { // Load event data when sequence changes useEffect(() => { + // Reset master camera when sequence changes + masterCamera.current = null; + setCurrentTime(0); + setDuration(0); + if (selectedCategory === 'SentryClips' && selectedSequence) { fetchEventData(selectedSequence).then(setEventData); } else { @@ -86,6 +93,35 @@ export function VideoViewer() { } }, [selectedCategory, selectedSequence]); + // Extract unique timestamps from files (for all clip types with multiple timestamps) + const getTimestamps = useCallback(() => { + if (!videoList || !selectedSequence) return []; + + const files = videoList[selectedCategory]?.[selectedSequence] || []; + const timestamps = new Set(); + + for (const file of files) { + // Extract timestamp: 2025-12-14_14-15-05-front.mp4 -> 2025-12-14_14-15-05 + const match = file.match(/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})/); + if (match) { + timestamps.add(match[1]); + } + } + + // Only show time selector if there's more than one timestamp + const result = Array.from(timestamps).sort().reverse(); + return result.length > 1 ? result : []; + }, [videoList, selectedCategory, selectedSequence]); + + const timestamps = getTimestamps(); + + // Auto-select most recent timestamp when timestamps change + useEffect(() => { + if (timestamps.length > 0 && !selectedTimestamp) { + setSelectedTimestamp(timestamps[0]); + } + }, [timestamps, selectedTimestamp]); + // Get video files for current sequence const getVideoFiles = useCallback(() => { if (!videoList || !selectedSequence) return {}; @@ -93,7 +129,15 @@ export function VideoViewer() { const files = videoList[selectedCategory]?.[selectedSequence] || []; const result = {}; + // Only filter by timestamp when there are multiple timestamps to choose from + const filterTimestamp = timestamps.length > 0 ? selectedTimestamp : null; + for (const file of files) { + // If filtering by timestamp, skip files that don't match + if (filterTimestamp && !file.startsWith(filterTimestamp)) { + continue; + } + // Extract camera from filename: 2025-01-15_12-30-45-front.mp4 for (const camera of Object.keys(CAMERAS)) { if (file.includes(`-${camera}.mp4`) || file.includes(`_${camera}.mp4`)) { @@ -104,7 +148,7 @@ export function VideoViewer() { } return result; - }, [videoList, selectedCategory, selectedSequence]); + }, [videoList, selectedCategory, selectedSequence, selectedTimestamp, timestamps]); const videoFiles = getVideoFiles(); @@ -138,14 +182,25 @@ export function VideoViewer() { seekAll(Math.min(duration, currentTime + 30)); }, [currentTime, duration, seekAll]); - // Handle time updates from videos - const handleTimeUpdate = useCallback((e) => { - setCurrentTime(e.target.currentTime); + // Handle time updates from videos - only use master camera to avoid jumping + const handleTimeUpdate = useCallback((camera) => (e) => { + if (camera === masterCamera.current) { + setCurrentTime(e.target.currentTime); + } }, []); - const handleDurationChange = useCallback((e) => { - if (e.target.duration && e.target.duration !== Infinity) { - setDuration(e.target.duration); + const handleDurationChange = useCallback((camera) => (e) => { + const dur = e.target.duration; + // Only accept valid, finite durations + if (dur && dur !== Infinity && !isNaN(dur)) { + // Set master camera when we first get a valid duration + if (!masterCamera.current) { + masterCamera.current = camera; + } + // Only update duration from master camera + if (camera === masterCamera.current) { + setDuration(dur); + } } }, []); @@ -187,17 +242,11 @@ export function VideoViewer() { ); } - if (!selectedSequence || Object.keys(videoFiles).length === 0) { - return ( -
          - No recordings available -
          - ); - } + const hasVideos = selectedSequence && Object.keys(videoFiles).length > 0; return (
          - {/* Clip selector bar */} + {/* Clip selector bar - always visible so users can switch categories */}
          setSelectedSequence(e.target.value)} + onChange={(e) => { + setSelectedSequence(e.target.value); + setSelectedTimestamp(null); // Reset timestamp when date changes + }} style={{ background: '#333', color: '#fff', @@ -241,7 +294,7 @@ export function VideoViewer() { padding: '6px 10px', fontSize: '13px', flex: 1, - maxWidth: '250px', + maxWidth: '180px', }} > {sequences.map(seq => ( @@ -249,12 +302,32 @@ export function VideoViewer() { ))} + {/* Timestamp selector (when multiple timestamps exist in a folder) */} + {timestamps.length > 0 && ( + + )} + {/* Layout selector */}
          - {/* Video grid */} -
          - {layout.cameras.map(camera => ( -
          - {videoFiles[camera] ? ( -
          - {/* Video controls */} -
          - + {/* Video controls */} +
          + - + - + -
          - {formatTime(currentTime)} / {formatTime(duration)} -
          +
          + {formatTime(currentTime)} / {formatTime(duration)} +
          -
          -
          0 ? `${(currentTime / duration) * 100}%` : '0%' }} - /> +
          +
          0 ? `${(currentTime / duration) * 100}%` : '0%' }} + /> +
          +
          + + ) : ( +
          + No recordings in {selectedCategory === 'SentryClips' ? 'Sentry Clips' : + selectedCategory === 'SavedClips' ? 'Saved Clips' : 'Recent Clips'}
          -
          + )}
          ); } @@ -383,4 +472,16 @@ function formatSequenceName(sequence) { return sequence; } +// Format timestamp to just show time (HH:MM) +function formatTimestamp(timestamp) { + // Format: 2025-12-14_14-15-05 -> 2:15 PM + const match = timestamp.match(/\d{4}-\d{2}-\d{2}_(\d{2})-(\d{2})-(\d{2})/); + if (match) { + const [, hour, min] = match; + const date = new Date(2000, 0, 1, parseInt(hour), parseInt(min)); + return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + } + return timestamp; +} + export default VideoViewer; diff --git a/teslausb-www-react/src/services/api.js b/teslausb-www-react/src/services/api.js index 67724d53..e86eaae7 100644 --- a/teslausb-www-react/src/services/api.js +++ b/teslausb-www-react/src/services/api.js @@ -294,7 +294,10 @@ export async function checkBLEStatus() { * @returns {Promise} */ export async function generateDiagnostics() { - await fetch(`${API_BASE}/diagnose.sh`); + const response = await fetch(`${API_BASE}/diagnose.sh`); + // Wait for the response body to ensure the script completes + await response.text(); + if (!response.ok) throw new Error('Failed to generate diagnostics'); } /** @@ -314,6 +317,23 @@ export async function fetchDiagnostics() { * @returns {Promise} { content, size, truncated } */ export async function fetchLog(logFile, lastSize = 0) { + // Use HEAD request first to check file size and avoid 416 errors + if (lastSize > 0) { + const headResponse = await fetch(`/${logFile}`, { method: 'HEAD' }); + if (headResponse.ok) { + const contentLength = parseInt(headResponse.headers.get('Content-Length') || '0', 10); + if (contentLength <= lastSize) { + // No new content or file was truncated + if (contentLength < lastSize) { + // File was truncated, return truncated flag to trigger full reload + return { content: '', size: 0, truncated: true }; + } + // No new content + return { content: '', size: lastSize, truncated: false }; + } + } + } + const headers = {}; if (lastSize > 0) { headers['Range'] = `bytes=${lastSize}-`; @@ -322,7 +342,7 @@ export async function fetchLog(logFile, lastSize = 0) { const response = await fetch(`/${logFile}`, { headers }); if (response.status === 416) { - // Range not satisfiable - log was truncated or no new content + // Range not satisfiable - shouldn't happen now but handle just in case return { content: '', size: lastSize, truncated: false }; } diff --git a/teslausb-www-react/src/styles/index.css b/teslausb-www-react/src/styles/index.css index b5a59418..0cd28831 100644 --- a/teslausb-www-react/src/styles/index.css +++ b/teslausb-www-react/src/styles/index.css @@ -876,6 +876,26 @@ body { border-color: #b91c1c; } +.btn-dark { + background: #333; + border-color: #444; + color: #fff; +} + +.btn-dark svg { + color: #fff; +} + +.btn-dark:hover:not(:disabled) { + background: #444; + border-color: #555; + color: #fff; +} + +.btn-dark:hover:not(:disabled) svg { + color: #fff; +} + .btn-sm { padding: 4px 8px; font-size: 12px; @@ -1325,6 +1345,11 @@ body { transition: background 0.15s; } +.video-play-btn svg { + width: 18px; + height: 18px; +} + .video-play-btn:hover { background: #007dd1; } @@ -1976,7 +2001,9 @@ body { /* Log viewer mobile */ .log-viewer { - height: 300px; + height: auto; + min-height: 300px; + flex: 1; } .log-viewer-content { From d53156474457cc396d4c6941edf04d4b360fac4b Mon Sep 17 00:00:00 2001 From: oaquique <30644106+oaquique@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:19:47 -0800 Subject: [PATCH 3/4] Add TeslaCam archive progress tracking for consistent sync display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cgi-bin/cam_sync_progress.sh to track CAM archive progress - Add useCamSyncProgress hook for polling CAM sync progress - Refactor SyncStatus.jsx for consistent progress display - Fix "0/X files" showing without actual progress - Fix stale progress text persisting after sync completes ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../dist/assets/index-B0yppBFU.js | 5 - .../dist/assets/index-CNaAh2IE.js | 5 + teslausb-www-react/dist/index.html | 2 +- .../src/components/Dashboard.jsx | 12 +- .../src/components/SyncStatus.jsx | 98 ++++++++++---- .../src/hooks/useMusicSyncProgress.js | 76 ++++++++++- teslausb-www-react/src/services/api.js | 10 ++ .../html/cgi-bin/cam_sync_progress.sh | 124 ++++++++++++++++++ 8 files changed, 300 insertions(+), 32 deletions(-) delete mode 100644 teslausb-www-react/dist/assets/index-B0yppBFU.js create mode 100644 teslausb-www-react/dist/assets/index-CNaAh2IE.js create mode 100755 teslausb-www/html/cgi-bin/cam_sync_progress.sh diff --git a/teslausb-www-react/dist/assets/index-B0yppBFU.js b/teslausb-www-react/dist/assets/index-B0yppBFU.js deleted file mode 100644 index 9f8bd372..00000000 --- a/teslausb-www-react/dist/assets/index-B0yppBFU.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))s(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function r(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function s(o){if(o.ep)return;o.ep=!0;const i=r(o);fetch(o.href,i)}})();var se,N,je,V,Ne,We,He,Ue,pe,ce,de,Z={},Pe=[],fn=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,oe=Array.isArray;function H(e,n){for(var r in n)e[r]=n[r];return e}function me(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function _n(e,n,r){var s,o,i,a={};for(i in n)i=="key"?s=n[i]:i=="ref"?o=n[i]:a[i]=n[i];if(arguments.length>2&&(a.children=arguments.length>3?se.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return ne(e,a,s,o,null)}function ne(e,n,r,s,o){var i={type:e,props:n,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o==null?++je:o,__i:-1,__u:0};return o==null&&N.vnode!=null&&N.vnode(i),i}function D(e){return e.children}function te(e,n){this.props=e,this.context=n}function Y(e,n){if(n==null)return e.__?Y(e.__,e.__i+1):null;for(var r;nl&&V.sort(He),e=V.shift(),l=V.length,e.__d&&(r=void 0,s=void 0,o=(s=(n=e).__v).__e,i=[],a=[],n.__P&&((r=H({},s)).__v=s.__v+1,N.vnode&&N.vnode(r),ve(n.__P,r,s,n.__n,n.__P.namespaceURI,32&s.__u?[o]:null,i,o==null?Y(s):o,!!(32&s.__u),a),r.__v=s.__v,r.__.__k[r.__i]=r,qe(i,r,a),s.__e=s.__=null,r.__e!=o&&Oe(r)));ie.__r=0}function Ve(e,n,r,s,o,i,a,l,u,d,_){var c,h,f,g,y,b,v,m=s&&s.__k||Pe,w=n.length;for(u=pn(r,n,m,u,w),c=0;c0?a=e.__k[i]=ne(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,u=i+h,a.__=e,a.__b=e.__b+1,(d=a.__i=mn(a,r,u,c))!=-1&&(c--,(l=r[d])&&(l.__u|=2)),l==null||l.__v==null?(d==-1&&(o>_?h--:o<_&&h++),typeof a.type!="function"&&(a.__u|=4)):d!=u&&(d==u-1?h--:d==u+1?h++:(d>u?h--:h++,a.__u|=4))):e.__k[i]=null;if(c)for(i=0;i<_;i++)(l=r[i])!=null&&(2&l.__u)==0&&(l.__e==s&&(s=Y(l)),Ke(l,l));return s}function ze(e,n,r,s){var o,i;if(typeof e.type=="function"){for(o=e.__k,i=0;o&&i(_?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&(2&d.__u)==0&&l==d.key&&u==d.type)return a}return-1}function Ce(e,n,r){n[0]=="-"?e.setProperty(n,r==null?"":r):e[n]=r==null?"":typeof r!="number"||fn.test(n)?r:r+"px"}function Q(e,n,r,s,o){var i,a;e:if(n=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof s=="string"&&(e.style.cssText=s=""),s)for(n in s)r&&n in r||Ce(e.style,n,"");if(r)for(n in r)s&&r[n]==s[n]||Ce(e.style,n,r[n])}else if(n[0]=="o"&&n[1]=="n")i=n!=(n=n.replace(Ue,"$1")),a=n.toLowerCase(),n=a in e||n=="onFocusOut"||n=="onFocusIn"?a.slice(2):n.slice(2),e.l||(e.l={}),e.l[n+i]=r,r?s?r.u=s.u:(r.u=pe,e.addEventListener(n,i?de:ce,i)):e.removeEventListener(n,i?de:ce,i);else{if(o=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in e)try{e[n]=r==null?"":r;break e}catch(l){}typeof r=="function"||(r==null||r===!1&&n[4]!="-"?e.removeAttribute(n):e.setAttribute(n,n=="popover"&&r==1?"":r))}}function Se(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n.t==null)n.t=pe++;else if(n.t0?e:oe(e)?e.map(Ge):H({},e)}function vn(e,n,r,s,o,i,a,l,u){var d,_,c,h,f,g,y,b=r.props||Z,v=n.props,m=n.type;if(m=="svg"?o="http://www.w3.org/2000/svg":m=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(d=0;d=r.__.length&&r.__.push({}),r.__[e]}function k(e){return X=1,kn(Je,e)}function kn(e,n,r){var s=ye(J++,2);if(s.t=e,!s.__c&&(s.__=[Je(void 0,n),function(l){var u=s.__N?s.__N[0]:s.__[0],d=s.t(u,l);u!==d&&(s.__N=[d,s.__[1]],s.__c.setState({}))}],s.__c=T,!T.__f)){var o=function(l,u,d){if(!s.__c.__H)return!0;var _=s.__c.__H.__.filter(function(h){return!!h.__c});if(_.every(function(h){return!h.__N}))return!i||i.call(this,l,u,d);var c=s.__c.props!==l;return _.forEach(function(h){if(h.__N){var f=h.__[0];h.__=h.__N,h.__N=void 0,f!==h.__[0]&&(c=!0)}}),i&&i.call(this,l,u,d)||c};T.__f=!0;var i=T.shouldComponentUpdate,a=T.componentWillUpdate;T.componentWillUpdate=function(l,u,d){if(this.__e){var _=i;i=void 0,o(l,u,d),i=_}a&&a.call(this,l,u,d)},T.shouldComponentUpdate=o}return s.__N||s.__}function F(e,n){var r=ye(J++,3);!$.__s&&Ze(r.__H,n)&&(r.__=e,r.u=n,T.__H.__h.push(r))}function U(e){return X=5,be(function(){return{current:e}},[])}function be(e,n){var r=ye(J++,7);return Ze(r.__H,n)&&(r.__=e(),r.__H=n,r.__h=e),r.__}function B(e,n){return X=8,be(function(){return e},n)}function wn(){for(var e;e=Ye.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(re),e.__H.__h.forEach(ue),e.__H.__h=[]}catch(n){e.__H.__h=[],$.__e(n,e.__v)}}$.__b=function(e){T=null,Te&&Te(e)},$.__=function(e,n){e&&n.__k&&n.__k.__m&&(e.__m=n.__k.__m),Fe&&Fe(e,n)},$.__r=function(e){$e&&$e(e),J=0;var n=(T=e.__c).__H;n&&(le===T?(n.__h=[],T.__h=[],n.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(n.__h.forEach(re),n.__h.forEach(ue),n.__h=[],J=0)),le=T},$.diffed=function(e){Be&&Be(e);var n=e.__c;n&&n.__H&&(n.__H.__h.length&&(Ye.push(n)!==1&&Le===$.requestAnimationFrame||((Le=$.requestAnimationFrame)||Nn)(wn)),n.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),le=T=null},$.__c=function(e,n){n.some(function(r){try{r.__h.forEach(re),r.__h=r.__h.filter(function(s){return!s.__||ue(s)})}catch(s){n.some(function(o){o.__h&&(o.__h=[])}),n=[],$.__e(s,r.__v)}}),Ie&&Ie(e,n)},$.unmount=function(e){Me&&Me(e);var n,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(s){try{re(s)}catch(o){n=o}}),r.__H=void 0,n&&$.__e(n,r.__v))};var Ae=typeof requestAnimationFrame=="function";function Nn(e){var n,r=function(){clearTimeout(s),Ae&&cancelAnimationFrame(n),setTimeout(e)},s=setTimeout(r,35);Ae&&(n=requestAnimationFrame(r))}function re(e){var n=T,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),T=n}function ue(e){var n=T;e.__c=e.__(),T=n}function Ze(e,n){return!e||e.length!==n.length||n.some(function(r,s){return r!==e[s]})}function Je(e,n){return typeof n=="function"?n(e):n}const A="/cgi-bin";async function xn(){const e=await fetch(`${A}/status.sh`);if(!e.ok)throw new Error("Failed to fetch status");return e.json()}async function Cn(){const e=await fetch(`${A}/config.sh`);if(!e.ok)throw new Error("Failed to fetch config");return e.json()}async function Sn(){const e=await fetch(`${A}/storage.sh`);if(!e.ok)throw new Error("Failed to fetch storage");return e.json()}async function Ln(){const e=await fetch(`${A}/videolist.sh`);if(!e.ok)throw new Error("Failed to fetch video list");const n=await e.text();return Tn(n)}function Tn(e){const n=e.trim().split(` -`).filter(Boolean),r={RecentClips:{},SavedClips:{},SentryClips:{}};for(const s of n){const o=s.split("/");if(o.length>=2){const[i,...a]=o;if(r[i]){const l=a[0];r[i][l]||(r[i][l]=[]),a.length>1&&r[i][l].push(a.slice(1).join("/"))}}}return r}async function $n(){if(!(await fetch(`${A}/trigger_sync.sh`)).ok)throw new Error("Failed to trigger sync")}async function Bn(){const e=await fetch(`${A}/music_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch music sync progress");return e.json()}async function In(){if(!(await fetch(`${A}/toggledrives.sh`)).ok)throw new Error("Failed to toggle drives")}async function Mn(){if(!(await fetch(`${A}/reboot.sh`)).ok)throw new Error("Failed to reboot")}async function Fn(){return(await fetch(`${A}/pairBLEkey.sh`)).status===202}async function An(){return(await(await fetch(`${A}/checkBLEstatus.sh`)).text()).includes("

          paired

          ")}async function En(){const e=await fetch(`${A}/diagnose.sh`);if(await e.text(),!e.ok)throw new Error("Failed to generate diagnostics")}async function Rn(){const e=await fetch("/diagnostics.txt");if(!e.ok)throw new Error("Failed to fetch diagnostics");return e.text()}async function Dn(e,n=0){if(n>0){const a=await fetch(`/${e}`,{method:"HEAD"});if(a.ok){const l=parseInt(a.headers.get("Content-Length")||"0",10);if(l<=n)return l0&&(r.Range=`bytes=${n}-`);const s=await fetch(`/${e}`,{headers:r});if(s.status===416)return{content:"",size:n,truncated:!1};if(s.status===404)throw new Error("Log file not found");if(!s.ok&&s.status!==206)throw new Error(`Failed to fetch log: ${s.status}`);const o=await s.text();let i=n;if(s.status===206){const a=s.headers.get("Content-Range");if(a){const l=a.match(/bytes \d+-(\d+)\/(\d+)/);l?i=parseInt(l[1],10)+1:i=n+o.length}else i=n+o.length}else i=o.length;return{content:o,size:i,truncated:!1}}async function jn(e,n){const r=await fetch(`${A}/randomdata.sh`,{signal:n});if(!r.ok)throw new Error("Failed to start speed test");const s=r.body.getReader();let o=0;const i=performance.now();try{for(;;){const{done:l,value:u}=await s.read();if(l)break;o+=u.length;const d=(performance.now()-i)/1e3,_=o*8/(d*1e6);e&&e(_)}}catch(l){if(l.name!=="AbortError")throw l}const a=(performance.now()-i)/1e3;return o*8/(a*1e6)}function Wn(e,n,r){return`/TeslaCam/${e}/${n}/${r}`}function Hn(e){return`/TeslaCam/SentryClips/${e}/event.json`}async function Un(e){try{const n=await fetch(Hn(e));return n.ok?n.json():null}catch(n){return null}}function Pn(e=5e3){const[n,r]=k(null),[s,o]=k(null),[i,a]=k(null),[l,u]=k(!0),[d,_]=k(null),[c,h]=k(null),f=B(async()=>{try{const[y,b,v]=await Promise.all([xn(),Cn(),Sn().catch(()=>null)]);r(y),o(b),a(v),h(new Date),_(null)}catch(y){_(y.message)}finally{u(!1)}},[]);F(()=>{f();const y=setInterval(f,e);return()=>clearInterval(y)},[f,e]);const g=n?{cpuTempC:n.cpu_temp?(parseInt(n.cpu_temp,10)/1e3).toFixed(1):null,uptimeFormatted:On(parseInt(n.uptime||"0",10)),diskUsedPercent:n.total_space&&n.free_space?Math.round((parseInt(n.total_space,10)-parseInt(n.free_space,10))/parseInt(n.total_space,10)*100):0,diskUsedGB:n.total_space&&n.free_space?((parseInt(n.total_space,10)-parseInt(n.free_space,10))/(1024*1024*1024)).toFixed(1):"0",diskTotalGB:n.total_space?(parseInt(n.total_space,10)/(1024*1024*1024)).toFixed(1):"0",diskFreeGB:n.free_space?(parseInt(n.free_space,10)/(1024*1024*1024)).toFixed(1):"0",drivesActive:n.drives_active==="yes",wifiConnected:!!n.wifi_ssid&&n.wifi_ssid!=="",wifiSignalPercent:Vn(n.wifi_strength),ethernetConnected:!!n.ether_ip&&n.ether_ip!=="",snapshotCount:parseInt(n.num_snapshots||"0",10)}:null;return{status:n,config:s,storage:i,computed:g,loading:l,error:d,lastUpdate:c,refresh:f}}function On(e){if(!e||isNaN(e))return"0s";const n=Math.floor(e/86400),r=Math.floor(e%86400/3600),s=Math.floor(e%3600/60),o=e%60,i=[];return n>0&&i.push(`${n}d`),r>0&&i.push(`${r}h`),s>0&&i.push(`${s}m`),(o>0||i.length===0)&&i.push(`${o}s`),i.join(" ")}function Vn(e){if(!e)return 0;const n=e.split("/");if(n.length!==2)return 0;const[r,s]=n.map(Number);return isNaN(r)||isNaN(s)||s===0?0:Math.round(r/s*100)}function zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2-4H8L6 10l-2.5 1.1C2.7 11.3 2 12.1 2 13v3c0 .6.4 1 1 1h2"}),t("circle",{cx:"7",cy:"17",r:"2"}),t("circle",{cx:"17",cy:"17",r:"2"})]})}function Xe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"})]})}function qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"23 7 16 12 23 17 23 7"}),t("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2",ry:"2"})]})}function Gn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}function fe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"23 4 23 10 17 10"}),t("path",{d:"M20.49 15a9 9 0 1 1-2.12-9.36L23 10"})]})}function Kn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:t("polygon",{points:"5 3 19 12 5 21 5 3"})})}function Yn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:[t("rect",{x:"6",y:"4",width:"4",height:"16"}),t("rect",{x:"14",y:"4",width:"4",height:"16"})]})}function Zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"19 20 9 12 19 4 19 20"}),t("line",{x1:"5",y1:"19",x2:"5",y2:"5"})]})}function Jn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"5 4 15 12 5 20 5 4"}),t("line",{x1:"19",y1:"5",x2:"19",y2:"19"})]})}function Xn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),t("polyline",{points:"7 10 12 15 17 10"}),t("line",{x1:"12",y1:"15",x2:"12",y2:"3"})]})}function Qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"3 6 5 6 21 6"}),t("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}function et({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M5 12.55a11 11 0 0 1 14.08 0"}),t("path",{d:"M1.42 9a16 16 0 0 1 21.16 0"}),t("path",{d:"M8.53 16.11a6 6 0 0 1 6.95 0"}),t("line",{x1:"12",y1:"20",x2:"12.01",y2:"20"})]})}function Ee({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("line",{x1:"22",y1:"12",x2:"2",y2:"12"}),t("path",{d:"M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}),t("line",{x1:"6",y1:"16",x2:"6.01",y2:"16"}),t("line",{x1:"10",y1:"16",x2:"10.01",y2:"16"})]})}function nt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",ry:"2"}),t("rect",{x:"9",y:"9",width:"6",height:"6"}),t("line",{x1:"9",y1:"1",x2:"9",y2:"4"}),t("line",{x1:"15",y1:"1",x2:"15",y2:"4"}),t("line",{x1:"9",y1:"20",x2:"9",y2:"23"}),t("line",{x1:"15",y1:"20",x2:"15",y2:"23"}),t("line",{x1:"20",y1:"9",x2:"23",y2:"9"}),t("line",{x1:"20",y1:"14",x2:"23",y2:"14"}),t("line",{x1:"1",y1:"9",x2:"4",y2:"9"}),t("line",{x1:"1",y1:"14",x2:"4",y2:"14"})]})}function tt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"4",cy:"20",r:"1"}),t("circle",{cx:"20",cy:"20",r:"1"}),t("circle",{cx:"12",cy:"10",r:"1"}),t("path",{d:"M12 3v7"}),t("path",{d:"M4 20v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"}),t("path",{d:"M12 10v9"}),t("path",{d:"M7 10l5-7 5 7"})]})}function rt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 2v6h-6"}),t("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"}),t("path",{d:"M3 22v-6h6"}),t("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"})]})}function it({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M18.36 6.64a9 9 0 1 1-12.73 0"}),t("line",{x1:"12",y1:"2",x2:"12",y2:"12"})]})}function Qe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"})})}function en({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"4 17 10 11 4 5"}),t("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function st({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"12",cy:"12",r:"10"}),t("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),t("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]})}function nn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6 9 12 15 18 9"})})}function ot({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"}),t("circle",{cx:"12",cy:"10",r:"3"})]})}function at({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M9 18V5l12-2v13"}),t("circle",{cx:"6",cy:"18",r:"3"}),t("circle",{cx:"18",cy:"16",r:"3"})]})}function tn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})]})}function lt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19.4 14.9C20.2 13.4 20.6 11.7 20.6 10c0-5-4-9-9-9s-9 4-9 9 4 9 9 9c1.7 0 3.4-.4 4.9-1.2"}),t("path",{d:"M11.6 10l6.4 6.4"})]})}function ct({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2",ry:"2"}),t("line",{x1:"3",y1:"9",x2:"21",y2:"9"}),t("line",{x1:"9",y1:"21",x2:"9",y2:"9"})]})}function dt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"17 1 21 5 17 9"}),t("path",{d:"M3 11V9a4 4 0 0 1 4-4h14"}),t("polyline",{points:"7 23 3 19 7 15"}),t("path",{d:"M21 13v2a4 4 0 0 1-4 4H3"})]})}const ht={dashboard:Xe,viewer:qn,files:Gn,logs:en};function ut({tabs:e,activeTab:n,onTabChange:r,lastUpdate:s,onRefresh:o}){const i=a=>a?a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"";return t("header",{className:"app-header",children:[t("div",{className:"app-header-left",children:t("nav",{className:"dashboard-nav",children:e.map(a=>{const l=ht[a.id]||Xe;return t("button",{className:`nav-link ${n===a.id?"active":""}`,onClick:()=>r(a.id),children:[t(l,{}),t("span",{children:a.label})]},a.id)})})}),t("div",{className:"app-header-right desktop-status-bar",children:[t("span",{className:"app-last-update",style:{marginRight:"12px"},children:s&&`Updated ${i(s)}`}),t("button",{className:"btn",onClick:o,children:[t(fe,{}),t("span",{children:"Refresh"})]})]})]})}function ft({status:e,computed:n,config:r,expanded:s,onToggle:o,onRefresh:i}){const[a,l]=k(!1),[u,d]=k(!1),[_,c]=k(!1),[h,f]=k(null),[g,y]=k(null),b=async()=>{l(!0);try{await In(),setTimeout(i,1e3)}catch(C){console.error("Toggle drives failed:",C)}finally{l(!1)}},v=async()=>{if(confirm("Are you sure you want to restart TeslaUSB?")){d(!0);try{await Mn()}catch(C){console.error("Reboot failed:",C)}}},m=async()=>{c(!0),f(null);const C=new AbortController,S=setTimeout(()=>C.abort(),1e4);try{await jn(L=>f(L.toFixed(1)),C.signal)}catch(L){L.name!=="AbortError"&&console.error("Speed test failed:",L)}finally{clearTimeout(S),c(!1)}},w=async()=>{y("pairing");try{if(await Fn()){for(let S=0;S<60;S++)if(await new Promise(P=>setTimeout(P,2e3)),await An()){y("paired");return}y("timeout")}else y("error")}catch(C){console.error("BLE pairing failed:",C),y("error")}};return t("aside",{className:`app-sidebar ${s?"expanded":""}`,children:[t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(nt,{}),t("span",{children:"System"}),t("button",{className:"sidebar-toggle-btn",onClick:o,children:t(nn,{className:s?"rotate-180":""})})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.device_model)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Model"}),t("span",{className:"info-value small-text",children:e.device_model})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Uptime"}),t("span",{className:"info-value",children:(n==null?void 0:n.uptimeFormatted)||"-"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"CPU Temp"}),t("span",{className:`info-value ${_t(n==null?void 0:n.cpuTempC)}`,children:n!=null&&n.cpuTempC?`${n.cpuTempC}ยฐC`:"-"})]}),t("div",{className:"info-item clickable",onClick:b,children:[t("span",{className:"info-label",children:"USB Drives"}),t("button",{className:`toggle-btn ${n!=null&&n.drivesActive?"active":"danger"}`,disabled:a,children:[a&&t(tt,{style:{width:12,height:12},className:"spinning"}),n!=null&&n.drivesActive?"Disconnect from host":"Connect to host"]})]}),t("div",{className:"info-item clickable",onClick:v,children:[t("span",{className:"info-label",children:"Power"}),t("button",{className:"toggle-btn danger",disabled:u,children:[u&&t(it,{style:{width:12,height:12},className:"spinning"}),"Restart"]})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(et,{}),t("span",{children:"Network"})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.wifi_ssid)&&t(D,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"WiFi SSID"}),t("span",{className:"info-value",children:e.wifi_ssid})]}),(e==null?void 0:e.wifi_freq)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Frequency"}),t("span",{className:"info-value",children:pt(e.wifi_freq)})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Signal"}),t("span",{className:"info-value",children:[(n==null?void 0:n.wifiSignalPercent)||0,"%"]})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.wifi_ip||"-"})]})]}),(e==null?void 0:e.ether_ip)&&t(D,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Ethernet"}),t("span",{className:"info-value",children:e.ether_speed||"Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.ether_ip})]})]}),!(e!=null&&e.wifi_ssid)&&!(e!=null&&e.ether_ip)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Status"}),t("span",{className:"info-value status-unhealthy",children:"Not Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Speed Test"}),t("span",{className:"info-value-with-action",children:[h&&t("span",{className:"speed-result",children:[h," Mbps"]}),t("button",{className:"toggle-btn",onClick:m,disabled:_,children:[_&&t(lt,{style:{width:12,height:12},className:"spinning"}),_?"Testing...":"Run"]})]})]}),(r==null?void 0:r.uses_ble)==="yes"&&t("div",{className:"info-item clickable",onClick:w,children:[t("span",{className:"info-label",children:"Bluetooth"}),t("button",{className:"toggle-btn",disabled:g==="pairing",children:[g==="pairing"&&t(Qe,{style:{width:12,height:12},className:"spinning"}),g==="pairing"?"Pairing...":g==="paired"?"Paired":g==="error"?"Failed":"Pair"]})]})]})]}),(n==null?void 0:n.snapshotCount)>0&&t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(tn,{}),t("span",{children:"Snapshots"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Count"}),t("span",{className:"info-value",children:n.snapshotCount})]}),(e==null?void 0:e.snapshot_oldest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Oldest"}),t("span",{className:"info-value compact-date",children:Re(e.snapshot_oldest)})]}),(e==null?void 0:e.snapshot_newest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Newest"}),t("span",{className:"info-value compact-date",children:Re(e.snapshot_newest)})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(dt,{}),t("span",{children:"Switch UI"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:t("a",{href:"/",className:"ui-switch-link",children:"Standard UI"})}),t("div",{className:"info-item",children:t("a",{href:"/new/",className:"ui-switch-link",children:"Vue UI"})})]})]})]})}function _t(e){if(!e)return"";const n=parseFloat(e);return n>=80?"status-unhealthy":n>=70?"status-degraded":"status-healthy"}function Re(e){if(!e)return"-";try{return new Date(parseInt(e,10)*1e3).toLocaleDateString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch(n){return e}}function pt(e){if(!e)return"-";const n=e.match(/(\d+\.?\d*)/);if(n){const r=parseFloat(n[1]),s=t("span",{style:{fontWeight:"normal"},children:[" (",r.toFixed(3),")"]});return r>=2.4&&r<2.5?t(D,{children:["2.4 GHz",s]}):r>=5&&r<6?t(D,{children:["5 GHz",s]}):r>=6?t(D,{children:["6 GHz",s]}):`${r} GHz`}return e}function rn(e,n=2e3,r=!0){const[s,o]=k([]),[i,a]=k(!0),[l,u]=k(null),d=U(0),_=U(!0),c=U(e),h=B(async(y=!1)=>{if(e)try{y&&(d.current=0);const b=await Dn(e,d.current);if(b.truncated){d.current=0,o([]);return}if(b.content){const v=b.content.split(` -`).filter(Boolean);o(y?v.slice(-1e3):m=>[...m,...v].slice(-1e3))}d.current=b.size,u(null)}catch(b){u(b.message)}finally{a(!1)}},[e]);F(()=>{e!==c.current&&(c.current=e,d.current=0,o([]),a(!0),u(null))},[e]),F(()=>{if(!r||!e)return;h(!0);const y=setInterval(()=>h(!1),n);return()=>clearInterval(y)},[e,n,r]);const f=B(y=>{_.current=y},[]),g=B(()=>{o([]),d.current=0},[]);return{lines:s,loading:i,error:l,autoScroll:_.current,setAutoScroll:f,refresh:()=>h(!1),clear:g}}function mt(e){const n={state:"idle",totalFiles:0,archivedFiles:0,currentFile:null,startTime:null,elapsedTime:null,lastActivity:null,message:null};if(!e||e.length===0)return n;const r=e.slice(-100);r.length>0&&(n.lastActivity=vt(r[r.length-1]));for(let o=r.length-1;o>=0;o--){const i=r[o],a=i.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/);if(a){const u=parseInt(a[2],10),d=a[3]?parseInt(a[3],10):0;n.totalFiles=u+d;break}const l=i.match(/Archiving (\d+)(?: track mode)? file\(s\)/);l&&!i.includes("completed")&&(n.totalFiles=parseInt(l[1],10))}let s=!1;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Finished copying music")||i.includes("Copying music failed"))break;if(i.includes("Syncing music from archive")||i.includes("Starting music sync")){s=!0;break}if(i.includes("Connected usb to host")||i.includes("Waiting for archive to be unreachable"))break}if(s)return n.state="archiving",n.message="Syncing music...",n;for(let o=r.length-1;o>=0;o--){const i=r[o];if(i.includes("Archiving completed successfully")){n.state="complete",n.message="Archive completed";break}if(i.includes("Finished copying music")){n.state="complete",n.message="Music sync complete";break}if(i.includes("Copied")&&i.includes("file(s)")){const a=i.match(/Copied (\d+)/);if(a){n.state="complete",n.archivedFiles=parseInt(a[1],10),n.message=`Copied ${n.archivedFiles} files`;break}}if(i.includes("Starting recording archiving")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving recordings...";break}if(i.includes("Archiving...")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving files...";break}if(i.includes("Syncing music from archive")){n.state="archiving",n.message="Syncing music...";break}if(i.includes("Copying music")){n.state="archiving",n.message="Copying music...";break}if(i.includes("Finished archiving")){n.state="complete",n.message="Archive complete";break}if(i.includes("Running fsck")){n.state="archiving",n.message="Checking filesystem...";break}if(i.includes("Checking saved folder count")){n.state="archiving",n.message="Scanning files...";break}if(i.includes("Waiting for archive to be reachable")){n.state="connecting",n.message="Connecting to archive server...";break}if(i.includes("Archive is reachable")){n.state="archiving",n.message="Connected, preparing...";break}if(i.includes("Disconnecting usb from host")){n.state="archiving",n.message="Disconnecting from vehicle...";break}if(i.includes("Connected usb to host")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("Waiting for archive to be unreachable")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("snapshot")){n.state="idle",n.message="Managing snapshots...";break}if(i.includes("low space, deleting")){n.state="idle",n.message="Cleaning up old snapshots...";break}if(i.includes("waiting up to")&&i.includes("idle interval")){n.state="idle",n.message="Waiting for idle...";break}if(i.includes("mass storage process")){n.state="idle",n.message="Ready";break}if((i.includes("error")||i.includes("failed"))&&!i.includes("sntp failed")){n.state="error",n.message="Error occurred";break}}return n}function vt(e){const n=e.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/);if(n){const r=new Date(n[1]);if(!isNaN(r.getTime()))return r}return null}function gt(e=!1,n=1500){const[r,s]=k({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""}),[o,i]=k(!1),[a,l]=k(null),u=U(e),d=B(async()=>{try{const _=await Bn();s(_),l(null)}catch(_){l(_.message)}finally{i(!1)}},[]);return F(()=>{u.current=e},[e]),F(()=>{if(!e){s({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""});return}i(!0),d();const _=setInterval(()=>{u.current&&d()},n);return()=>clearInterval(_)},[e,n,d]),{...r,loading:o,error:a,refresh:d}}function yt(e){if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],r=1024,s=Math.floor(Math.log(e)/Math.log(r));return`${(e/Math.pow(r,s)).toFixed(s>0?2:0)} ${n[s]}`}function bt(e){return!e||e==="0:00:00"?"":`~${e.replace(/^0:/,"")} remaining`}function kt({storage:e,total:n,free:r,config:s}){var c;const o=h=>{if(h===0||h===null||h===void 0)return"0 B";const f=h/(1024*1024*1024);return f>=1?`${f.toFixed(1)} GB`:`${(h/(1024*1024)).toFixed(0)} MB`},i=((c=e==null?void 0:e.total)==null?void 0:c.free)||r,a=[],l=(h,f,g,y)=>{(s==null?void 0:s[y])!=="yes"||!h||a.push({type:f,label:g,allocated:h.total,used:h.used,mounted:h.mounted,isCached:h.cached||!1})};if(l(e==null?void 0:e.cam,"teslacam","TeslaCam","has_cam"),l(e==null?void 0:e.music,"music","Music","has_music"),l(e==null?void 0:e.lightshow,"lightshow","LightShow","has_lightshow"),l(e==null?void 0:e.boombox,"boombox","Boombox","has_boombox"),a.length===0&&!i)return t("div",{className:"storage-bar-container",children:t("div",{className:"storage-info",children:"No storage data available"})});const d=a.reduce((h,f)=>h+(f.allocated||0),0)+i,_=a.map(h=>({type:h.type,label:h.label,bytes:h.allocated,percent:Math.max(1,Math.round(h.allocated/d*100))}));return i>0&&_.push({type:"free",label:"Free",bytes:i,percent:Math.max(1,Math.round(i/d*100))}),t("div",{className:"storage-bar-container",children:[t("div",{className:"storage-bar",children:_.map((h,f)=>t("div",{className:`storage-segment ${h.type}`,style:{width:`${h.percent}%`},title:`${h.label}: ${o(h.bytes)}`},f))}),t("div",{className:"storage-legend",children:[a.map((h,f)=>{const g=h.used!==null&&h.allocated>0?Math.round(h.used/h.allocated*100):null;return t("div",{className:"storage-legend-item",children:[t("div",{className:`storage-legend-dot ${h.type}`}),t("span",{className:"storage-legend-label",children:h.label}),t("span",{className:"storage-legend-value",children:[o(h.allocated),h.used!==null&&t("span",{className:"storage-legend-used",children:[" ","(",o(h.used)," used",h.isCached?"~":"",g!==null&&`, ${g}%`,")"]})]})]},f)}),t("div",{className:"storage-legend-item",children:[t("div",{className:"storage-legend-dot free"}),t("span",{className:"storage-legend-label",children:"Free"}),t("span",{className:"storage-legend-value",children:o(i)})]})]}),a.some(h=>h.isCached)&&t("div",{className:"storage-note",children:"~ Last known (drive not currently mounted)"})]})}function wt({syncStatus:e,onTriggerSync:n,loading:r,musicProgress:s}){const{state:o,totalFiles:i,archivedFiles:a,message:l,elapsedTime:u,lastActivity:d}=e,_=(l==null?void 0:l.toLowerCase().includes("music"))&&o==="archiving",c=(s==null?void 0:s.active)&&(s==null?void 0:s.percentage)>0,f=(()=>{switch(o){case"idle":return{label:"Idle",color:"idle",description:"Ready to archive"};case"connecting":return{label:"Connecting",color:"connecting",description:l||"Connecting to server..."};case"archiving":return{label:"Archiving",color:"archiving",description:l||"Syncing files..."};case"complete":return{label:"Complete",color:"idle",description:l||"Archive complete"};case"error":return{label:"Error",color:"error",description:l||"Archive failed"};default:return{label:"Unknown",color:"idle",description:"Status unknown"}}})(),g=o==="archiving"||o==="connecting",y=_&&c,b=o==="archiving"&&i>0&&!y;let v=null;y?v=s.percentage:b&&a>0&&(v=Math.min(Math.round(a/i*100),100));const m=w=>{if(!w)return null;const S=Math.floor((new Date-w)/1e3);return S<60?"just now":S<3600?`${Math.floor(S/60)}m ago`:S<86400?`${Math.floor(S/3600)}h ago`:w.toLocaleDateString()};return t("div",{className:"sync-status-card",children:[t("div",{className:"sync-status-header",children:[t("span",{className:"sync-status-title",children:"Sync Status"}),t("div",{className:"sync-status-indicator",children:[t("div",{className:`sync-status-dot ${f.color}`}),t("span",{children:f.label})]})]}),t("div",{className:"sync-status-description",children:f.description}),(y||b||g)&&t("div",{className:"sync-progress-bar",children:t("div",{className:"sync-progress-fill",style:{width:v!==null?`${v}%`:"100%",animation:v===null?"pulse 1.5s ease-in-out infinite":"none",opacity:v===null?.6:1}})}),y&&t("div",{className:"sync-details",children:[t("div",{className:"sync-progress-main",children:[yt(s.bytesTransferred)," transferred",v!==null&&` (${v}%)`]}),(s.speed||s.eta)&&t("div",{className:"sync-progress-secondary",children:[s.speed&&t("span",{children:s.speed}),s.speed&&s.eta&&t("span",{children:" ยท "}),s.eta&&t("span",{children:bt(s.eta)})]})]}),b&&t("div",{className:"sync-details",children:[a," / ",i," files",v!==null&&` (${v}%)`]}),!g&&!b&&!y&&d&&t("div",{className:"sync-details",children:["Last sync: ",m(d)]}),u&&o==="complete"&&t("div",{className:"sync-details",children:["Completed in ",u]}),(o==="idle"||o==="complete"||o==="error")&&t("button",{className:"btn btn-primary btn-sm",onClick:n,disabled:r,style:{marginTop:"0.75rem"},children:[t(rt,{style:{width:14,height:14},className:r?"spinning":""}),t("span",{children:r?"Starting...":"Sync Now"})]})]})}function Nt({status:e,computed:n,config:r,storage:s,onRefresh:o}){const[i,a]=k(!1),{lines:l}=rn("archiveloop.log",3e3,!0),u=mt(l),d=be(()=>{var f;return u.state==="archiving"&&((f=u.message)==null?void 0:f.toLowerCase().includes("music"))},[u.state,u.message]),_=gt(d,1500),c=B(async()=>{a(!0);try{await $n(),setTimeout(o,1e3)}catch(f){console.error("Trigger sync failed:",f)}finally{a(!1)}},[o]),h=[{key:"cam",label:"TeslaCam",icon:tn,enabled:(r==null?void 0:r.has_cam)==="yes"},{key:"music",label:"Music",icon:at,enabled:(r==null?void 0:r.has_music)==="yes"},{key:"lightshow",label:"LightShow",icon:Ee,enabled:(r==null?void 0:r.has_lightshow)==="yes"},{key:"boombox",label:"Boombox",icon:Ee,enabled:(r==null?void 0:r.has_boombox)==="yes"}];return(r==null?void 0:r.uses_ble)==="yes"&&h.push({key:"ble",label:"BLE",icon:Qe,enabled:!0}),t("div",{className:"dashboard-content",children:[t("div",{className:"dashboard-section",children:[t("div",{className:"section-title",children:"Configured Features"}),t("div",{className:"features-row",children:h.map(({key:f,label:g,enabled:y})=>t("div",{className:`feature-badge ${y?"enabled":"disabled"}`,children:g},f))})]}),t("div",{className:"dashboard-section",children:t("div",{className:"card",children:[t("div",{className:"card-header",children:[t("span",{className:"card-title",children:"Storage"}),t("span",{className:"card-value",children:[(n==null?void 0:n.diskTotalGB)||"0"," GB"]})]}),t(kt,{storage:s,total:e!=null&&e.total_space?parseInt(e.total_space,10):0,free:e!=null&&e.free_space?parseInt(e.free_space,10):0,config:r})]})}),t("div",{className:"dashboard-section",children:t(wt,{syncStatus:u,onTriggerSync:c,loading:i,musicProgress:_})})]})}const _e={front:"Front",back:"Back",left_repeater:"Left Repeater",right_repeater:"Right Repeater",left_pillar:"Left Pillar",right_pillar:"Right Pillar"},De=[{id:"6",name:"All Cameras",cameras:Object.keys(_e),cols:3},{id:"4-front",name:"Front Focus",cameras:["front","left_repeater","right_repeater","back"],cols:2},{id:"4-rear",name:"Rear Focus",cameras:["back","left_repeater","right_repeater","front"],cols:2},{id:"2-side",name:"Side View",cameras:["left_repeater","right_repeater"],cols:2},{id:"1-front",name:"Front Only",cameras:["front"],cols:1},{id:"1-back",name:"Rear Only",cameras:["back"],cols:1}];function xt(){const[e,n]=k(null),[r,s]=k(!0),[o,i]=k(null),[a,l]=k("SentryClips"),[u,d]=k(null),[_,c]=k(null),[h,f]=k(!1),[g,y]=k(0),[b,v]=k(0),[m,w]=k(De[0]),[C,S]=k(!1),[L,P]=k(null),W=U({}),E=U(null);F(()=>{z()},[]);const z=async()=>{try{s(!0);const p=await Ln();n(p);for(const x of["SentryClips","SavedClips","RecentClips"]){const I=Object.keys(p[x]||{});if(I.length>0){l(x),d(I[0]);break}}}catch(p){i(p.message)}finally{s(!1)}};F(()=>{E.current=null,y(0),v(0),a==="SentryClips"&&u?Un(u).then(P):P(null)},[a,u]);const M=B(()=>{var O;if(!e||!u)return[];const p=((O=e[a])==null?void 0:O[u])||[],x=new Set;for(const G of p){const K=G.match(/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})/);K&&x.add(K[1])}const I=Array.from(x).sort().reverse();return I.length>1?I:[]},[e,a,u])();F(()=>{M.length>0&&!_&&c(M[0])},[M,_]);const ae=B(()=>{var O;if(!e||!u)return{};const p=((O=e[a])==null?void 0:O[u])||[],x={},I=M.length>0?_:null;for(const G of p)if(!(I&&!G.startsWith(I))){for(const K of Object.keys(_e))if(G.includes(`-${K}.mp4`)||G.includes(`_${K}.mp4`)){x[K]=Wn(a,u,G);break}}return x},[e,a,u,_,M])(),sn=B(()=>{Object.values(W.current).forEach(p=>{p&&p.play()}),f(!0)},[]),ke=B(()=>{Object.values(W.current).forEach(p=>{p&&p.pause()}),f(!1)},[]),q=B(p=>{Object.values(W.current).forEach(x=>{x&&(x.currentTime=p)}),y(p)},[]),on=B(()=>{q(Math.max(0,g-10))},[g,q]),an=B(()=>{q(Math.min(b,g+30))},[g,b,q]),ln=B(p=>x=>{p===E.current&&y(x.target.currentTime)},[]),cn=B(p=>x=>{const I=x.target.duration;I&&I!==1/0&&!isNaN(I)&&(E.current||(E.current=p),p===E.current&&v(I))},[]),dn=B(p=>{const x=p.currentTarget.getBoundingClientRect(),O=(p.clientX-x.left)/x.width*b;q(O)},[b,q]),we=p=>{if(!p||isNaN(p))return"0:00";const x=Math.floor(p/60),I=Math.floor(p%60);return`${x}:${I.toString().padStart(2,"0")}`},hn=e!=null&&e[a]?Object.keys(e[a]).sort().reverse():[];if(r)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"Loading recordings..."})});if(o)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:[t("span",{className:"status-unhealthy",children:["Error: ",o]}),t("button",{className:"btn",onClick:z,style:{marginTop:"1rem"},children:"Retry"})]});const un=u&&Object.keys(ae).length>0;return t("div",{className:"video-viewer",children:[t("div",{className:"video-selector",style:{display:"flex",gap:"0.5rem",padding:"8px 12px",background:"#1a1a1a",borderBottom:"1px solid #333",alignItems:"center",flexWrap:"wrap"},children:[t("select",{value:a,onChange:p=>{l(p.target.value);const x=Object.keys(e[p.target.value]||{});d(x[0]||null),c(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px"},children:[t("option",{value:"SentryClips",children:"Sentry Clips"}),t("option",{value:"SavedClips",children:"Saved Clips"}),t("option",{value:"RecentClips",children:"Recent Clips"})]}),t("select",{value:u||"",onChange:p=>{d(p.target.value),c(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",flex:1,maxWidth:"180px"},children:hn.map(p=>t("option",{value:p,children:Ct(p)},p))}),M.length>0&&t("select",{value:_||"",onChange:p=>c(p.target.value),style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",maxWidth:"120px"},children:M.map(p=>t("option",{value:p,children:St(p)},p))}),t("div",{style:{position:"relative"},children:[t("button",{className:"btn btn-sm btn-dark",onClick:()=>S(!C),children:[t(ct,{}),t("span",{children:m.name}),t(nn,{})]}),C&&t("div",{style:{position:"absolute",top:"100%",right:0,background:"#2a2a2a",border:"1px solid #444",borderRadius:"6px",marginTop:"4px",zIndex:100,minWidth:"150px"},children:De.map(p=>t("button",{onClick:()=>{w(p),S(!1)},style:{display:"block",width:"100%",padding:"8px 12px",background:m.id===p.id?"#0095f6":"transparent",color:"#fff",border:"none",textAlign:"left",cursor:"pointer",fontSize:"13px"},children:p.name},p.id))})]}),L&&L.city&&t("div",{style:{display:"flex",alignItems:"center",gap:"4px",color:"#888",fontSize:"12px",marginLeft:"auto"},children:[t(ot,{style:{width:14,height:14}}),t("span",{children:L.city})]})]}),un?t(D,{children:[t("div",{className:`video-grid layout-${m.cameras.length}`,style:{gridTemplateColumns:`repeat(${m.cols}, 1fr)`},children:m.cameras.map(p=>t("div",{className:"video-cell",children:[ae[p]?t("video",{ref:x=>{W.current[p]=x},src:ae[p],onTimeUpdate:ln(p),onDurationChange:cn(p),onEnded:ke,muted:!0,playsInline:!0}):t("div",{style:{color:"#666",fontSize:"12px"},children:"No video"}),t("div",{className:"video-cell-label",children:_e[p]})]},p))}),t("div",{className:"video-controls",children:[t("button",{className:"btn btn-sm",onClick:on,title:"Skip back 10s",children:t(Zn,{})}),t("button",{className:"video-play-btn",onClick:h?ke:sn,children:h?t(Yn,{}):t(Kn,{})}),t("button",{className:"btn btn-sm",onClick:an,title:"Skip forward 30s",children:t(Jn,{})}),t("div",{className:"video-time",children:[we(g)," / ",we(b)]}),t("div",{className:"video-timeline",onClick:dn,children:t("div",{className:"video-timeline-progress",style:{width:b>0?`${g/b*100}%`:"0%"}})})]})]}):t("div",{style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",color:"#888",fontSize:"14px"},children:["No recordings in ",a==="SentryClips"?"Sentry Clips":a==="SavedClips"?"Saved Clips":"Recent Clips"]})]})}function Ct(e){const n=e.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/);if(n){const[,r,s,o,i,a,l]=n,u=new Date(r,s-1,o,i||0,a||0,l||0);return i?u.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):u.toLocaleDateString([],{month:"short",day:"numeric",year:"numeric"})}return e}function St(e){const n=e.match(/\d{4}-\d{2}-\d{2}_(\d{2})-(\d{2})-(\d{2})/);if(n){const[,r,s]=n;return new Date(2e3,0,1,parseInt(r),parseInt(s)).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}return e}function Lt({config:e}){const n=U(null),r=U(null),[s,o]=k(!!window.FileBrowser);F(()=>{if(!document.querySelector('link[href="/filebrowser.css"]')){const d=document.createElement("link");d.rel="stylesheet",d.href="/filebrowser.css",document.head.appendChild(d)}},[]),F(()=>{if(window.FileBrowser){o(!0);return}const d=document.createElement("script");d.src="/filebrowser.js",d.onload=()=>o(!0),d.onerror=()=>console.error("Failed to load filebrowser.js"),document.head.appendChild(d)},[]);const i=(e==null?void 0:e.has_music)==="yes",a=(e==null?void 0:e.has_lightshow)==="yes",l=(e==null?void 0:e.has_boombox)==="yes";return F(()=>{if(!s||!n.current)return;const d=[];if(i&&d.push({path:"fs/Music",label:"Music"}),a&&d.push({path:"fs/LightShow",label:"LightShow"}),l&&d.push({path:"fs/Boombox",label:"Boombox"}),d.length!==0&&!r.current){try{r.current=new window.FileBrowser(n.current,d)}catch(_){console.error("Failed to initialize FileBrowser:",_)}return()=>{n.current&&(n.current.innerHTML=""),r.current=null}}},[s,i,a,l]),(e==null?void 0:e.has_music)==="yes"||(e==null?void 0:e.has_lightshow)==="yes"||(e==null?void 0:e.has_boombox)==="yes"?s?t("div",{ref:n,style:{width:"100%",height:"calc(100% - 4px)",minHeight:"400px",flex:1,display:"flex",position:"relative"}}):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",minHeight:"400px",color:"#9ca3af",fontSize:"14px"},children:"Loading file browser..."}):t("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:"#9ca3af",fontSize:"14px"},children:[t("svg",{style:{width:32,height:32,marginBottom:8,color:"#d1d5db"},viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})}),t("span",{children:"No file drives configured"})]})}const ee={archiveloop:{label:"Archive Log",file:"archiveloop.log",description:"Sync and archive operations"},setup:{label:"Setup Log",file:"teslausb-headless-setup.log",description:"Initial setup and configuration"},diagnostics:{label:"Diagnostics",file:"diagnostics.txt",description:"System diagnostics report"}};function Tt(){const[e,n]=k("archiveloop"),[r,s]=k(!1),[o,i]=k(null),a=U(null),l=e!=="diagnostics",{lines:u,loading:d,error:_,refresh:c,clear:h}=rn(l?ee[e].file:"",2e3,l);F(()=>{a.current&&l&&(a.current.scrollTop=a.current.scrollHeight)},[u,l]);const f=B(async()=>{s(!0),i(null);try{await En();const m=await Rn();i(m)}catch(m){i(`Error generating diagnostics: ${m.message}`)}finally{s(!1)}},[]);F(()=>{e==="diagnostics"&&!o&&f()},[e,o,f]);const g=B(()=>{const m=e==="diagnostics"?o:u.join(` -`),w=ee[e].file,C=new Blob([m],{type:"text/plain"}),S=URL.createObjectURL(C),L=document.createElement("a");L.href=S,L.download=w,document.body.appendChild(L),L.click(),document.body.removeChild(L),URL.revokeObjectURL(S)},[e,u,o]),y=m=>{const w=m.toLowerCase();return w.includes("error")||w.includes("failed")||w.includes("fatal")?"error":w.includes("warning")||w.includes("warn")?"warning":w.includes("success")||w.includes("completed")||w.includes("finished")?"success":""},b=ee[e],v=e==="diagnostics"?(o==null?void 0:o.split(` -`))||[]:u;return t("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[t("div",{style:{display:"flex",gap:"0.5rem",marginBottom:"1rem",flexWrap:"wrap"},children:Object.entries(ee).map(([m,w])=>t("button",{className:`btn ${e===m?"btn-primary":""}`,onClick:()=>n(m),children:[m==="diagnostics"?t(st,{}):t(en,{}),t("span",{children:w.label})]},m))}),t("div",{className:"log-viewer",style:{flex:1},children:[t("div",{className:"log-viewer-header",children:[t("div",{className:"log-viewer-title",children:[b.label,t("span",{style:{fontWeight:400,marginLeft:"8px",opacity:.7},children:["โ€” ",b.description]})]}),t("div",{className:"log-viewer-actions",children:[e==="diagnostics"?t("button",{className:"btn btn-sm log-action-btn",onClick:f,disabled:r,children:[t(fe,{className:r?"spinning":""}),t("span",{children:"Regenerate"})]}):t(D,{children:[t("button",{className:"btn btn-sm log-action-btn",onClick:c,disabled:d,title:"Refresh",children:t(fe,{className:d?"spinning":""})}),t("button",{className:"btn btn-sm log-action-btn",onClick:h,title:"Clear",children:t(Qn,{})})]}),t("button",{className:"btn btn-sm log-action-btn",onClick:g,disabled:v.length===0,title:"Download",children:t(Xn,{})})]})]}),t("div",{className:"log-viewer-content",ref:a,children:d&&v.length===0||r?t("div",{style:{color:"#888",fontStyle:"italic"},children:"Loading..."}):_?_.includes("not found")?t("div",{style:{color:"#888",fontStyle:"italic"},children:["Log file not available. ",e==="setup"&&"The setup log is only present during initial setup."]}):t("div",{style:{color:"#f87171"},children:["Error: ",_]}):v.length===0?t("div",{style:{color:"#888",fontStyle:"italic"},children:"No log entries"}):v.map((m,w)=>t("div",{className:`log-line ${y(m)}`,children:m},w))})]}),l&&u.length>0&&t("div",{style:{marginTop:"0.5rem",fontSize:"11px",color:"#666",display:"flex",gap:"1rem"},children:[t("span",{children:[u.length," lines"]}),t("span",{children:"Auto-refreshing every 2s"})]})]})}function $t(){return t("div",{className:"loading-container",children:[t("div",{className:"spinner"}),t("span",{children:"Loading..."})]})}const j={DASHBOARD:"dashboard",VIEWER:"viewer",FILES:"files",LOGS:"logs"};function Bt(){const[e,n]=k(j.DASHBOARD),[r,s]=k(!1),{status:o,config:i,storage:a,computed:l,loading:u,error:d,lastUpdate:_,refresh:c}=Pn(5e3),h=[];return h.push({id:j.DASHBOARD,label:"Dashboard"}),(i==null?void 0:i.has_cam)==="yes"&&h.push({id:j.VIEWER,label:"Viewer"}),((i==null?void 0:i.has_music)==="yes"||(i==null?void 0:i.has_lightshow)==="yes"||(i==null?void 0:i.has_boombox)==="yes")&&h.push({id:j.FILES,label:"Files"}),h.push({id:j.LOGS,label:"Logs"}),u&&!o?t($t,{}):d&&!o?t("div",{className:"error-container",children:[t("span",{children:["Failed to load: ",d]}),t("button",{className:"retry-btn",onClick:c,children:"Retry"})]}):t("div",{className:"app-shell",children:[t("div",{className:"app-topbar",children:[t("div",{className:"app-topbar-title",children:[t(zn,{}),t("span",{children:"TeslaUSB"})]}),t("div",{className:"app-topbar-actions",children:t("span",{className:`app-status ${l!=null&&l.drivesActive?"healthy":"warning"}`,children:l!=null&&l.drivesActive?"Connected":"Disconnected"})})]}),t(ut,{tabs:h,activeTab:e,onTabChange:n,lastUpdate:_,onRefresh:c}),t("div",{className:"mobile-quick-status",children:[t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.drivesActive?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"USB"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.wifiConnected?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"WiFi"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.cpuTempC&&parseFloat(l.cpuTempC)<70?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:[l==null?void 0:l.cpuTempC,"ยฐC"]})]})]}),t("div",{className:"app-body",children:[e===j.DASHBOARD&&t(ft,{status:o,computed:l,config:i,expanded:r,onToggle:()=>s(!r),onRefresh:c}),t("main",{className:"app-main",children:[e===j.DASHBOARD&&t(Nt,{status:o,computed:l,config:i,storage:a,onRefresh:c}),e===j.VIEWER&&(i==null?void 0:i.has_cam)==="yes"&&t(xt,{}),e===j.FILES&&t(Lt,{config:i}),e===j.LOGS&&t(Tt,{})]})]})]})}yn(t(Bt,{}),document.getElementById("app")); diff --git a/teslausb-www-react/dist/assets/index-CNaAh2IE.js b/teslausb-www-react/dist/assets/index-CNaAh2IE.js new file mode 100644 index 00000000..6bb858ce --- /dev/null +++ b/teslausb-www-react/dist/assets/index-CNaAh2IE.js @@ -0,0 +1,5 @@ +(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const a of i.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&o(a)}).observe(document,{childList:!0,subtree:!0});function r(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function o(s){if(s.ep)return;s.ep=!0;const i=r(s);fetch(s.href,i)}})();var oe,N,We,V,Ne,He,Ue,Pe,me,de,he,Z={},Oe=[],pn=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ae=Array.isArray;function P(e,n){for(var r in n)e[r]=n[r];return e}function ve(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function _n(e,n,r){var o,s,i,a={};for(i in n)i=="key"?o=n[i]:i=="ref"?s=n[i]:a[i]=n[i];if(arguments.length>2&&(a.children=arguments.length>3?oe.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return ne(e,a,o,s,null)}function ne(e,n,r,o,s){var i={type:e,props:n,key:r,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s==null?++We:s,__i:-1,__u:0};return s==null&&N.vnode!=null&&N.vnode(i),i}function M(e){return e.children}function te(e,n){this.props=e,this.context=n}function Y(e,n){if(n==null)return e.__?Y(e.__,e.__i+1):null;for(var r;nl&&V.sort(Ue),e=V.shift(),l=V.length,e.__d&&(r=void 0,o=void 0,s=(o=(n=e).__v).__e,i=[],a=[],n.__P&&((r=P({},o)).__v=o.__v+1,N.vnode&&N.vnode(r),ge(n.__P,r,o,n.__n,n.__P.namespaceURI,32&o.__u?[s]:null,i,s==null?Y(o):s,!!(32&o.__u),a),r.__v=o.__v,r.__.__k[r.__i]=r,Ge(i,r,a),o.__e=o.__=null,r.__e!=s&&Ve(r)));ie.__r=0}function ze(e,n,r,o,s,i,a,l,h,c,f){var d,u,p,g,v,k,b,_=o&&o.__k||Oe,y=n.length;for(h=mn(r,n,_,h,y),d=0;d0?a=e.__k[i]=ne(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[i]=a,h=i+u,a.__=e,a.__b=e.__b+1,(c=a.__i=vn(a,r,h,d))!=-1&&(d--,(l=r[c])&&(l.__u|=2)),l==null||l.__v==null?(c==-1&&(s>f?u--:sh?u--:u++,a.__u|=4))):e.__k[i]=null;if(d)for(i=0;i(f?1:0)){for(s=r-1,i=r+1;s>=0||i=0?s--:i++])!=null&&(2&c.__u)==0&&l==c.key&&h==c.type)return a}return-1}function Ce(e,n,r){n[0]=="-"?e.setProperty(n,r==null?"":r):e[n]=r==null?"":typeof r!="number"||pn.test(n)?r:r+"px"}function Q(e,n,r,o,s){var i,a;e:if(n=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof o=="string"&&(e.style.cssText=o=""),o)for(n in o)r&&n in r||Ce(e.style,n,"");if(r)for(n in r)o&&r[n]==o[n]||Ce(e.style,n,r[n])}else if(n[0]=="o"&&n[1]=="n")i=n!=(n=n.replace(Pe,"$1")),a=n.toLowerCase(),n=a in e||n=="onFocusOut"||n=="onFocusIn"?a.slice(2):n.slice(2),e.l||(e.l={}),e.l[n+i]=r,r?o?r.u=o.u:(r.u=me,e.addEventListener(n,i?he:de,i)):e.removeEventListener(n,i?he:de,i);else{if(s=="http://www.w3.org/2000/svg")n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(n!="width"&&n!="height"&&n!="href"&&n!="list"&&n!="form"&&n!="tabIndex"&&n!="download"&&n!="rowSpan"&&n!="colSpan"&&n!="role"&&n!="popover"&&n in e)try{e[n]=r==null?"":r;break e}catch(l){}typeof r=="function"||(r==null||r===!1&&n[4]!="-"?e.removeAttribute(n):e.setAttribute(n,n=="popover"&&r==1?"":r))}}function Se(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n.t==null)n.t=me++;else if(n.t0?e:ae(e)?e.map(Ke):P({},e)}function gn(e,n,r,o,s,i,a,l,h){var c,f,d,u,p,g,v,k=r.props||Z,b=n.props,_=n.type;if(_=="svg"?s="http://www.w3.org/2000/svg":_=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),i!=null){for(c=0;c=r.__.length&&r.__.push({}),r.__[e]}function w(e){return X=1,wn(Xe,e)}function wn(e,n,r){var o=be(J++,2);if(o.t=e,!o.__c&&(o.__=[Xe(void 0,n),function(l){var h=o.__N?o.__N[0]:o.__[0],c=o.t(h,l);h!==c&&(o.__N=[c,o.__[1]],o.__c.setState({}))}],o.__c=L,!L.__f)){var s=function(l,h,c){if(!o.__c.__H)return!0;var f=o.__c.__H.__.filter(function(u){return!!u.__c});if(f.every(function(u){return!u.__N}))return!i||i.call(this,l,h,c);var d=o.__c.props!==l;return f.forEach(function(u){if(u.__N){var p=u.__[0];u.__=u.__N,u.__N=void 0,p!==u.__[0]&&(d=!0)}}),i&&i.call(this,l,h,c)||d};L.__f=!0;var i=L.shouldComponentUpdate,a=L.componentWillUpdate;L.componentWillUpdate=function(l,h,c){if(this.__e){var f=i;i=void 0,s(l,h,c),i=f}a&&a.call(this,l,h,c)},L.shouldComponentUpdate=s}return o.__N||o.__}function F(e,n){var r=be(J++,3);!T.__s&&Je(r.__H,n)&&(r.__=e,r.u=n,L.__H.__h.push(r))}function H(e){return X=5,se(function(){return{current:e}},[])}function se(e,n){var r=be(J++,7);return Je(r.__H,n)&&(r.__=e(),r.__H=n,r.__h=e),r.__}function B(e,n){return X=8,se(function(){return e},n)}function Nn(){for(var e;e=Ze.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(re),e.__H.__h.forEach(fe),e.__H.__h=[]}catch(n){e.__H.__h=[],T.__e(n,e.__v)}}T.__b=function(e){L=null,Te&&Te(e)},T.__=function(e,n){e&&n.__k&&n.__k.__m&&(e.__m=n.__k.__m),Fe&&Fe(e,n)},T.__r=function(e){$e&&$e(e),J=0;var n=(L=e.__c).__H;n&&(ce===L?(n.__h=[],L.__h=[],n.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(n.__h.forEach(re),n.__h.forEach(fe),n.__h=[],J=0)),ce=L},T.diffed=function(e){Be&&Be(e);var n=e.__c;n&&n.__H&&(n.__H.__h.length&&(Ze.push(n)!==1&&Le===T.requestAnimationFrame||((Le=T.requestAnimationFrame)||xn)(Nn)),n.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),ce=L=null},T.__c=function(e,n){n.some(function(r){try{r.__h.forEach(re),r.__h=r.__h.filter(function(o){return!o.__||fe(o)})}catch(o){n.some(function(s){s.__h&&(s.__h=[])}),n=[],T.__e(o,r.__v)}}),Ie&&Ie(e,n)},T.unmount=function(e){Me&&Me(e);var n,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(o){try{re(o)}catch(s){n=s}}),r.__H=void 0,n&&T.__e(n,r.__v))};var Ae=typeof requestAnimationFrame=="function";function xn(e){var n,r=function(){clearTimeout(o),Ae&&cancelAnimationFrame(n),setTimeout(e)},o=setTimeout(r,35);Ae&&(n=requestAnimationFrame(r))}function re(e){var n=L,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),L=n}function fe(e){var n=L;e.__c=e.__(),L=n}function Je(e,n){return!e||e.length!==n.length||n.some(function(r,o){return r!==e[o]})}function Xe(e,n){return typeof n=="function"?n(e):n}const D="/cgi-bin";async function Cn(){const e=await fetch(`${D}/status.sh`);if(!e.ok)throw new Error("Failed to fetch status");return e.json()}async function Sn(){const e=await fetch(`${D}/config.sh`);if(!e.ok)throw new Error("Failed to fetch config");return e.json()}async function Ln(){const e=await fetch(`${D}/storage.sh`);if(!e.ok)throw new Error("Failed to fetch storage");return e.json()}async function Tn(){const e=await fetch(`${D}/videolist.sh`);if(!e.ok)throw new Error("Failed to fetch video list");const n=await e.text();return $n(n)}function $n(e){const n=e.trim().split(` +`).filter(Boolean),r={RecentClips:{},SavedClips:{},SentryClips:{}};for(const o of n){const s=o.split("/");if(s.length>=2){const[i,...a]=s;if(r[i]){const l=a[0];r[i][l]||(r[i][l]=[]),a.length>1&&r[i][l].push(a.slice(1).join("/"))}}}return r}async function Bn(){if(!(await fetch(`${D}/trigger_sync.sh`)).ok)throw new Error("Failed to trigger sync")}async function In(){const e=await fetch(`${D}/music_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch music sync progress");return e.json()}async function Mn(){const e=await fetch(`${D}/cam_sync_progress.sh`);if(!e.ok)throw new Error("Failed to fetch cam sync progress");return e.json()}async function Fn(){if(!(await fetch(`${D}/toggledrives.sh`)).ok)throw new Error("Failed to toggle drives")}async function An(){if(!(await fetch(`${D}/reboot.sh`)).ok)throw new Error("Failed to reboot")}async function En(){return(await fetch(`${D}/pairBLEkey.sh`)).status===202}async function Dn(){return(await(await fetch(`${D}/checkBLEstatus.sh`)).text()).includes("

          paired

          ")}async function Rn(){const e=await fetch(`${D}/diagnose.sh`);if(await e.text(),!e.ok)throw new Error("Failed to generate diagnostics")}async function jn(){const e=await fetch("/diagnostics.txt");if(!e.ok)throw new Error("Failed to fetch diagnostics");return e.text()}async function Wn(e,n=0){if(n>0){const a=await fetch(`/${e}`,{method:"HEAD"});if(a.ok){const l=parseInt(a.headers.get("Content-Length")||"0",10);if(l<=n)return l0&&(r.Range=`bytes=${n}-`);const o=await fetch(`/${e}`,{headers:r});if(o.status===416)return{content:"",size:n,truncated:!1};if(o.status===404)throw new Error("Log file not found");if(!o.ok&&o.status!==206)throw new Error(`Failed to fetch log: ${o.status}`);const s=await o.text();let i=n;if(o.status===206){const a=o.headers.get("Content-Range");if(a){const l=a.match(/bytes \d+-(\d+)\/(\d+)/);l?i=parseInt(l[1],10)+1:i=n+s.length}else i=n+s.length}else i=s.length;return{content:s,size:i,truncated:!1}}async function Hn(e,n){const r=await fetch(`${D}/randomdata.sh`,{signal:n});if(!r.ok)throw new Error("Failed to start speed test");const o=r.body.getReader();let s=0;const i=performance.now();try{for(;;){const{done:l,value:h}=await o.read();if(l)break;s+=h.length;const c=(performance.now()-i)/1e3,f=s*8/(c*1e6);e&&e(f)}}catch(l){if(l.name!=="AbortError")throw l}const a=(performance.now()-i)/1e3;return s*8/(a*1e6)}function Un(e,n,r){return`/TeslaCam/${e}/${n}/${r}`}function Pn(e){return`/TeslaCam/SentryClips/${e}/event.json`}async function On(e){try{const n=await fetch(Pn(e));return n.ok?n.json():null}catch(n){return null}}function Vn(e=5e3){const[n,r]=w(null),[o,s]=w(null),[i,a]=w(null),[l,h]=w(!0),[c,f]=w(null),[d,u]=w(null),p=B(async()=>{try{const[v,k,b]=await Promise.all([Cn(),Sn(),Ln().catch(()=>null)]);r(v),s(k),a(b),u(new Date),f(null)}catch(v){f(v.message)}finally{h(!1)}},[]);F(()=>{p();const v=setInterval(p,e);return()=>clearInterval(v)},[p,e]);const g=n?{cpuTempC:n.cpu_temp?(parseInt(n.cpu_temp,10)/1e3).toFixed(1):null,uptimeFormatted:zn(parseInt(n.uptime||"0",10)),diskUsedPercent:n.total_space&&n.free_space?Math.round((parseInt(n.total_space,10)-parseInt(n.free_space,10))/parseInt(n.total_space,10)*100):0,diskUsedGB:n.total_space&&n.free_space?((parseInt(n.total_space,10)-parseInt(n.free_space,10))/(1024*1024*1024)).toFixed(1):"0",diskTotalGB:n.total_space?(parseInt(n.total_space,10)/(1024*1024*1024)).toFixed(1):"0",diskFreeGB:n.free_space?(parseInt(n.free_space,10)/(1024*1024*1024)).toFixed(1):"0",drivesActive:n.drives_active==="yes",wifiConnected:!!n.wifi_ssid&&n.wifi_ssid!=="",wifiSignalPercent:qn(n.wifi_strength),ethernetConnected:!!n.ether_ip&&n.ether_ip!=="",snapshotCount:parseInt(n.num_snapshots||"0",10)}:null;return{status:n,config:o,storage:i,computed:g,loading:l,error:c,lastUpdate:d,refresh:p}}function zn(e){if(!e||isNaN(e))return"0s";const n=Math.floor(e/86400),r=Math.floor(e%86400/3600),o=Math.floor(e%3600/60),s=e%60,i=[];return n>0&&i.push(`${n}d`),r>0&&i.push(`${r}h`),o>0&&i.push(`${o}m`),(s>0||i.length===0)&&i.push(`${s}s`),i.join(" ")}function qn(e){if(!e)return 0;const n=e.split("/");if(n.length!==2)return 0;const[r,o]=n.map(Number);return isNaN(r)||isNaN(o)||o===0?0:Math.round(r/o*100)}function Gn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9L18 10l-2-4H8L6 10l-2.5 1.1C2.7 11.3 2 12.1 2 13v3c0 .6.4 1 1 1h2"}),t("circle",{cx:"7",cy:"17",r:"2"}),t("circle",{cx:"17",cy:"17",r:"2"})]})}function Qe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"3",width:"7",height:"7",rx:"1"}),t("rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}),t("rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"})]})}function Kn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"23 7 16 12 23 17 23 7"}),t("rect",{x:"1",y:"5",width:"15",height:"14",rx:"2",ry:"2"})]})}function Yn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})})}function pe({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"23 4 23 10 17 10"}),t("path",{d:"M20.49 15a9 9 0 1 1-2.12-9.36L23 10"})]})}function Zn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:t("polygon",{points:"5 3 19 12 5 21 5 3"})})}function Jn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"currentColor",children:[t("rect",{x:"6",y:"4",width:"4",height:"16"}),t("rect",{x:"14",y:"4",width:"4",height:"16"})]})}function Xn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"19 20 9 12 19 4 19 20"}),t("line",{x1:"5",y1:"19",x2:"5",y2:"5"})]})}function Qn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polygon",{points:"5 4 15 12 5 20 5 4"}),t("line",{x1:"19",y1:"5",x2:"19",y2:"19"})]})}function et({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}),t("polyline",{points:"7 10 12 15 17 10"}),t("line",{x1:"12",y1:"15",x2:"12",y2:"3"})]})}function nt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"3 6 5 6 21 6"}),t("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})}function tt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M5 12.55a11 11 0 0 1 14.08 0"}),t("path",{d:"M1.42 9a16 16 0 0 1 21.16 0"}),t("path",{d:"M8.53 16.11a6 6 0 0 1 6.95 0"}),t("line",{x1:"12",y1:"20",x2:"12.01",y2:"20"})]})}function Ee({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("line",{x1:"22",y1:"12",x2:"2",y2:"12"}),t("path",{d:"M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}),t("line",{x1:"6",y1:"16",x2:"6.01",y2:"16"}),t("line",{x1:"10",y1:"16",x2:"10.01",y2:"16"})]})}function rt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",ry:"2"}),t("rect",{x:"9",y:"9",width:"6",height:"6"}),t("line",{x1:"9",y1:"1",x2:"9",y2:"4"}),t("line",{x1:"15",y1:"1",x2:"15",y2:"4"}),t("line",{x1:"9",y1:"20",x2:"9",y2:"23"}),t("line",{x1:"15",y1:"20",x2:"15",y2:"23"}),t("line",{x1:"20",y1:"9",x2:"23",y2:"9"}),t("line",{x1:"20",y1:"14",x2:"23",y2:"14"}),t("line",{x1:"1",y1:"9",x2:"4",y2:"9"}),t("line",{x1:"1",y1:"14",x2:"4",y2:"14"})]})}function it({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"4",cy:"20",r:"1"}),t("circle",{cx:"20",cy:"20",r:"1"}),t("circle",{cx:"12",cy:"10",r:"1"}),t("path",{d:"M12 3v7"}),t("path",{d:"M4 20v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5"}),t("path",{d:"M12 10v9"}),t("path",{d:"M7 10l5-7 5 7"})]})}function st({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 2v6h-6"}),t("path",{d:"M3 12a9 9 0 0 1 15-6.7L21 8"}),t("path",{d:"M3 22v-6h6"}),t("path",{d:"M21 12a9 9 0 0 1-15 6.7L3 16"})]})}function ot({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M18.36 6.64a9 9 0 1 1-12.73 0"}),t("line",{x1:"12",y1:"2",x2:"12",y2:"12"})]})}function en({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"})})}function nn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"4 17 10 11 4 5"}),t("line",{x1:"12",y1:"19",x2:"20",y2:"19"})]})}function at({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("circle",{cx:"12",cy:"12",r:"10"}),t("line",{x1:"12",y1:"16",x2:"12",y2:"12"}),t("line",{x1:"12",y1:"8",x2:"12.01",y2:"8"})]})}function tn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:t("polyline",{points:"6 9 12 15 18 9"})})}function lt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"}),t("circle",{cx:"12",cy:"10",r:"3"})]})}function ct({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M9 18V5l12-2v13"}),t("circle",{cx:"6",cy:"18",r:"3"}),t("circle",{cx:"18",cy:"16",r:"3"})]})}function rn({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"}),t("circle",{cx:"12",cy:"13",r:"4"})]})}function dt({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("path",{d:"M19.4 14.9C20.2 13.4 20.6 11.7 20.6 10c0-5-4-9-9-9s-9 4-9 9 4 9 9 9c1.7 0 3.4-.4 4.9-1.2"}),t("path",{d:"M11.6 10l6.4 6.4"})]})}function ht({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("rect",{x:"3",y:"3",width:"18",height:"18",rx:"2",ry:"2"}),t("line",{x1:"3",y1:"9",x2:"21",y2:"9"}),t("line",{x1:"9",y1:"21",x2:"9",y2:"9"})]})}function ut({className:e}){return t("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[t("polyline",{points:"17 1 21 5 17 9"}),t("path",{d:"M3 11V9a4 4 0 0 1 4-4h14"}),t("polyline",{points:"7 23 3 19 7 15"}),t("path",{d:"M21 13v2a4 4 0 0 1-4 4H3"})]})}const ft={dashboard:Qe,viewer:Kn,files:Yn,logs:nn};function pt({tabs:e,activeTab:n,onTabChange:r,lastUpdate:o,onRefresh:s}){const i=a=>a?a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):"";return t("header",{className:"app-header",children:[t("div",{className:"app-header-left",children:t("nav",{className:"dashboard-nav",children:e.map(a=>{const l=ft[a.id]||Qe;return t("button",{className:`nav-link ${n===a.id?"active":""}`,onClick:()=>r(a.id),children:[t(l,{}),t("span",{children:a.label})]},a.id)})})}),t("div",{className:"app-header-right desktop-status-bar",children:[t("span",{className:"app-last-update",style:{marginRight:"12px"},children:o&&`Updated ${i(o)}`}),t("button",{className:"btn",onClick:s,children:[t(pe,{}),t("span",{children:"Refresh"})]})]})]})}function _t({status:e,computed:n,config:r,expanded:o,onToggle:s,onRefresh:i}){const[a,l]=w(!1),[h,c]=w(!1),[f,d]=w(!1),[u,p]=w(null),[g,v]=w(null),k=async()=>{l(!0);try{await Fn(),setTimeout(i,1e3)}catch(x){console.error("Toggle drives failed:",x)}finally{l(!1)}},b=async()=>{if(confirm("Are you sure you want to restart TeslaUSB?")){c(!0);try{await An()}catch(x){console.error("Reboot failed:",x)}}},_=async()=>{d(!0),p(null);const x=new AbortController,$=setTimeout(()=>x.abort(),1e4);try{await Hn(S=>p(S.toFixed(1)),x.signal)}catch(S){S.name!=="AbortError"&&console.error("Speed test failed:",S)}finally{clearTimeout($),d(!1)}},y=async()=>{v("pairing");try{if(await En()){for(let $=0;$<60;$++)if(await new Promise(E=>setTimeout(E,2e3)),await Dn()){v("paired");return}v("timeout")}else v("error")}catch(x){console.error("BLE pairing failed:",x),v("error")}};return t("aside",{className:`app-sidebar ${o?"expanded":""}`,children:[t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(rt,{}),t("span",{children:"System"}),t("button",{className:"sidebar-toggle-btn",onClick:s,children:t(tn,{className:o?"rotate-180":""})})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.device_model)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Model"}),t("span",{className:"info-value small-text",children:e.device_model})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Uptime"}),t("span",{className:"info-value",children:(n==null?void 0:n.uptimeFormatted)||"-"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"CPU Temp"}),t("span",{className:`info-value ${mt(n==null?void 0:n.cpuTempC)}`,children:n!=null&&n.cpuTempC?`${n.cpuTempC}ยฐC`:"-"})]}),t("div",{className:"info-item clickable",onClick:k,children:[t("span",{className:"info-label",children:"USB Drives"}),t("button",{className:`toggle-btn ${n!=null&&n.drivesActive?"active":"danger"}`,disabled:a,children:[a&&t(it,{style:{width:12,height:12},className:"spinning"}),n!=null&&n.drivesActive?"Disconnect from host":"Connect to host"]})]}),t("div",{className:"info-item clickable",onClick:b,children:[t("span",{className:"info-label",children:"Power"}),t("button",{className:"toggle-btn danger",disabled:h,children:[h&&t(ot,{style:{width:12,height:12},className:"spinning"}),"Restart"]})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(tt,{}),t("span",{children:"Network"})]}),t("div",{className:"info-list",children:[(e==null?void 0:e.wifi_ssid)&&t(M,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"WiFi SSID"}),t("span",{className:"info-value",children:e.wifi_ssid})]}),(e==null?void 0:e.wifi_freq)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Frequency"}),t("span",{className:"info-value",children:vt(e.wifi_freq)})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Signal"}),t("span",{className:"info-value",children:[(n==null?void 0:n.wifiSignalPercent)||0,"%"]})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.wifi_ip||"-"})]})]}),(e==null?void 0:e.ether_ip)&&t(M,{children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Ethernet"}),t("span",{className:"info-value",children:e.ether_speed||"Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"IP Address"}),t("span",{className:"info-value small-text",children:e.ether_ip})]})]}),!(e!=null&&e.wifi_ssid)&&!(e!=null&&e.ether_ip)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Status"}),t("span",{className:"info-value status-unhealthy",children:"Not Connected"})]}),t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Speed Test"}),t("span",{className:"info-value-with-action",children:[u&&t("span",{className:"speed-result",children:[u," Mbps"]}),t("button",{className:"toggle-btn",onClick:_,disabled:f,children:[f&&t(dt,{style:{width:12,height:12},className:"spinning"}),f?"Testing...":"Run"]})]})]}),(r==null?void 0:r.uses_ble)==="yes"&&t("div",{className:"info-item clickable",onClick:y,children:[t("span",{className:"info-label",children:"Bluetooth"}),t("button",{className:"toggle-btn",disabled:g==="pairing",children:[g==="pairing"&&t(en,{style:{width:12,height:12},className:"spinning"}),g==="pairing"?"Pairing...":g==="paired"?"Paired":g==="error"?"Failed":"Pair"]})]})]})]}),(n==null?void 0:n.snapshotCount)>0&&t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(rn,{}),t("span",{children:"Snapshots"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Count"}),t("span",{className:"info-value",children:n.snapshotCount})]}),(e==null?void 0:e.snapshot_oldest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Oldest"}),t("span",{className:"info-value compact-date",children:De(e.snapshot_oldest)})]}),(e==null?void 0:e.snapshot_newest)&&t("div",{className:"info-item",children:[t("span",{className:"info-label",children:"Newest"}),t("span",{className:"info-value compact-date",children:De(e.snapshot_newest)})]})]})]}),t("div",{className:"device-info",children:[t("div",{className:"device-header",children:[t(ut,{}),t("span",{children:"Switch UI"})]}),t("div",{className:"info-list",children:[t("div",{className:"info-item",children:t("a",{href:"/",className:"ui-switch-link",children:"Standard UI"})}),t("div",{className:"info-item",children:t("a",{href:"/new/",className:"ui-switch-link",children:"Vue UI"})})]})]})]})}function mt(e){if(!e)return"";const n=parseFloat(e);return n>=80?"status-unhealthy":n>=70?"status-degraded":"status-healthy"}function De(e){if(!e)return"-";try{return new Date(parseInt(e,10)*1e3).toLocaleDateString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch(n){return e}}function vt(e){if(!e)return"-";const n=e.match(/(\d+\.?\d*)/);if(n){const r=parseFloat(n[1]),o=t("span",{style:{fontWeight:"normal"},children:[" (",r.toFixed(3),")"]});return r>=2.4&&r<2.5?t(M,{children:["2.4 GHz",o]}):r>=5&&r<6?t(M,{children:["5 GHz",o]}):r>=6?t(M,{children:["6 GHz",o]}):`${r} GHz`}return e}function sn(e,n=2e3,r=!0){const[o,s]=w([]),[i,a]=w(!0),[l,h]=w(null),c=H(0),f=H(!0),d=H(e),u=B(async(v=!1)=>{if(e)try{v&&(c.current=0);const k=await Wn(e,c.current);if(k.truncated){c.current=0,s([]);return}if(k.content){const b=k.content.split(` +`).filter(Boolean);s(v?b.slice(-1e3):_=>[..._,...b].slice(-1e3))}c.current=k.size,h(null)}catch(k){h(k.message)}finally{a(!1)}},[e]);F(()=>{e!==d.current&&(d.current=e,c.current=0,s([]),a(!0),h(null))},[e]),F(()=>{if(!r||!e)return;u(!0);const v=setInterval(()=>u(!1),n);return()=>clearInterval(v)},[e,n,r]);const p=B(v=>{f.current=v},[]),g=B(()=>{s([]),c.current=0},[]);return{lines:o,loading:i,error:l,autoScroll:f.current,setAutoScroll:p,refresh:()=>u(!1),clear:g}}function gt(e){const n={state:"idle",totalFiles:0,archivedFiles:0,currentFile:null,startTime:null,elapsedTime:null,lastActivity:null,message:null};if(!e||e.length===0)return n;const r=e.slice(-100);r.length>0&&(n.lastActivity=yt(r[r.length-1]));for(let s=r.length-1;s>=0;s--){const i=r[s],a=i.match(/There are (\d+) event folder\(s\) with (\d+) file\(s\)(?: and (\d+) track mode file\(s\))?/);if(a){const h=parseInt(a[2],10),c=a[3]?parseInt(a[3],10):0;n.totalFiles=h+c;break}const l=i.match(/Archiving (\d+)(?: track mode)? file\(s\)/);l&&!i.includes("completed")&&(n.totalFiles=parseInt(l[1],10))}let o=!1;for(let s=r.length-1;s>=0;s--){const i=r[s];if(i.includes("Finished copying music")||i.includes("Copying music failed"))break;if(i.includes("Syncing music from archive")||i.includes("Starting music sync")){o=!0;break}if(i.includes("Connected usb to host")||i.includes("Waiting for archive to be unreachable"))break}if(o)return n.state="archiving",n.message="Syncing music...",n;for(let s=r.length-1;s>=0;s--){const i=r[s];if(i.includes("Archiving completed successfully")){n.state="complete",n.message="Archive completed";break}if(i.includes("Finished copying music")){n.state="complete",n.message="Music sync complete";break}if(i.includes("Copied")&&i.includes("file(s)")){const a=i.match(/Copied (\d+)/);if(a){n.state="complete",n.archivedFiles=parseInt(a[1],10),n.message=`Copied ${n.archivedFiles} files`;break}}if(i.includes("Starting recording archiving")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving recordings...";break}if(i.includes("Archiving...")){n.state="archiving",n.message=n.totalFiles>0?`Archiving ${n.totalFiles} files...`:"Archiving files...";break}if(i.includes("Syncing music from archive")){n.state="archiving",n.message="Syncing music...";break}if(i.includes("Copying music")){n.state="archiving",n.message="Copying music...";break}if(i.includes("Finished archiving")){n.state="complete",n.message="Archive complete";break}if(i.includes("Running fsck")){n.state="archiving",n.message="Checking filesystem...";break}if(i.includes("Checking saved folder count")){n.state="archiving",n.message="Scanning files...";break}if(i.includes("Waiting for archive to be reachable")){n.state="connecting",n.message="Connecting to archive server...";break}if(i.includes("Archive is reachable")){n.state="archiving",n.message="Connected, preparing...";break}if(i.includes("Disconnecting usb from host")){n.state="archiving",n.message="Disconnecting from vehicle...";break}if(i.includes("Connected usb to host")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("Waiting for archive to be unreachable")){n.state="idle",n.message="Connected to vehicle";break}if(i.includes("snapshot")){n.state="idle",n.message="Managing snapshots...";break}if(i.includes("low space, deleting")){n.state="idle",n.message="Cleaning up old snapshots...";break}if(i.includes("waiting up to")&&i.includes("idle interval")){n.state="idle",n.message="Waiting for idle...";break}if(i.includes("mass storage process")){n.state="idle",n.message="Ready";break}if((i.includes("error")||i.includes("failed"))&&!i.includes("sntp failed")){n.state="error",n.message="Error occurred";break}}return n}function yt(e){const n=e.match(/^([A-Z][a-z]{2}\s+\d+\s+[A-Z][a-z]{2}\s+\d+:\d+:\d+\s+\w+\s+\d+):/);if(n){const r=new Date(n[1]);if(!isNaN(r.getTime()))return r}return null}function bt(e=!1,n=1500){const[r,o]=w({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""}),[s,i]=w(!1),[a,l]=w(null),h=H(e),c=B(async()=>{try{const f=await In();o(f),l(null)}catch(f){l(f.message)}finally{i(!1)}},[]);return F(()=>{h.current=e},[e]),F(()=>{if(!e){o({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:""});return}i(!0),c();const f=setInterval(()=>{h.current&&c()},n);return()=>clearInterval(f)},[e,n,c]),{...r,loading:s,error:a,refresh:c}}function Re(e){if(e===0)return"0 B";const n=["B","KB","MB","GB","TB"],r=1024,o=Math.floor(Math.log(e)/Math.log(r));return`${(e/Math.pow(r,o)).toFixed(o>0?2:0)} ${n[o]}`}function kt(e){return!e||e==="0:00:00"?"":`~${e.replace(/^0:/,"")} remaining`}function wt(e=!1,n=1500){const[r,o]=w({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:"",filesDone:0,filesTotal:0,currentFile:""}),[s,i]=w(!1),[a,l]=w(null),h=H(e),c=B(async()=>{try{const f=await Mn();o(f),l(null)}catch(f){l(f.message)}finally{i(!1)}},[]);return F(()=>{h.current=e},[e]),F(()=>{if(!e){o({active:!1,bytesTransferred:0,percentage:0,speed:"",eta:"",filesDone:0,filesTotal:0,currentFile:""});return}i(!0),c();const f=setInterval(()=>{h.current&&c()},n);return()=>clearInterval(f)},[e,n,c]),{...r,loading:s,error:a,refresh:c}}function Nt({storage:e,total:n,free:r,config:o}){var d;const s=u=>{if(u===0||u===null||u===void 0)return"0 B";const p=u/(1024*1024*1024);return p>=1?`${p.toFixed(1)} GB`:`${(u/(1024*1024)).toFixed(0)} MB`},i=((d=e==null?void 0:e.total)==null?void 0:d.free)||r,a=[],l=(u,p,g,v)=>{(o==null?void 0:o[v])!=="yes"||!u||a.push({type:p,label:g,allocated:u.total,used:u.used,mounted:u.mounted,isCached:u.cached||!1})};if(l(e==null?void 0:e.cam,"teslacam","TeslaCam","has_cam"),l(e==null?void 0:e.music,"music","Music","has_music"),l(e==null?void 0:e.lightshow,"lightshow","LightShow","has_lightshow"),l(e==null?void 0:e.boombox,"boombox","Boombox","has_boombox"),a.length===0&&!i)return t("div",{className:"storage-bar-container",children:t("div",{className:"storage-info",children:"No storage data available"})});const c=a.reduce((u,p)=>u+(p.allocated||0),0)+i,f=a.map(u=>({type:u.type,label:u.label,bytes:u.allocated,percent:Math.max(1,Math.round(u.allocated/c*100))}));return i>0&&f.push({type:"free",label:"Free",bytes:i,percent:Math.max(1,Math.round(i/c*100))}),t("div",{className:"storage-bar-container",children:[t("div",{className:"storage-bar",children:f.map((u,p)=>t("div",{className:`storage-segment ${u.type}`,style:{width:`${u.percent}%`},title:`${u.label}: ${s(u.bytes)}`},p))}),t("div",{className:"storage-legend",children:[a.map((u,p)=>{const g=u.used!==null&&u.allocated>0?Math.round(u.used/u.allocated*100):null;return t("div",{className:"storage-legend-item",children:[t("div",{className:`storage-legend-dot ${u.type}`}),t("span",{className:"storage-legend-label",children:u.label}),t("span",{className:"storage-legend-value",children:[s(u.allocated),u.used!==null&&t("span",{className:"storage-legend-used",children:[" ","(",s(u.used)," used",u.isCached?"~":"",g!==null&&`, ${g}%`,")"]})]})]},p)}),t("div",{className:"storage-legend-item",children:[t("div",{className:"storage-legend-dot free"}),t("span",{className:"storage-legend-label",children:"Free"}),t("span",{className:"storage-legend-value",children:s(i)})]})]}),a.some(u=>u.isCached)&&t("div",{className:"storage-note",children:"~ Last known (drive not currently mounted)"})]})}function xt({syncStatus:e,onTriggerSync:n,loading:r,musicProgress:o,camProgress:s}){const{state:i,message:a,elapsedTime:l,lastActivity:h}=e,c=(a==null?void 0:a.toLowerCase().includes("music"))&&i==="archiving",f=i==="archiving"&&!c,d=o==null?void 0:o.active,u=s==null?void 0:s.active,g=(()=>{switch(i){case"idle":return{label:"Idle",color:"idle",description:"Ready to archive"};case"connecting":return{label:"Connecting",color:"connecting",description:a||"Connecting to server..."};case"archiving":return{label:"Archiving",color:"archiving",description:a||"Syncing files..."};case"complete":return{label:"Complete",color:"idle",description:a||"Archive complete"};case"error":return{label:"Error",color:"error",description:a||"Archive failed"};default:return{label:"Unknown",color:"idle",description:"Status unknown"}}})(),v=i==="archiving"||i==="connecting",k=c&&d,b=f&&u;let _=null,y=null;k&&o.percentage>0?(_=o.percentage,y={type:"music",bytesTransferred:o.bytesTransferred,speed:o.speed,eta:o.eta}):b&&(s.percentage>0?_=s.percentage:s.filesTotal>0&&s.filesDone>0&&(_=Math.min(Math.round(s.filesDone/s.filesTotal*100),100)),y={type:"cam",bytesTransferred:s.bytesTransferred,speed:s.speed,eta:s.eta,filesDone:s.filesDone,filesTotal:s.filesTotal,currentFile:s.currentFile});const x=$=>{if(!$)return null;const E=Math.floor((new Date-$)/1e3);return E<60?"just now":E<3600?`${Math.floor(E/60)}m ago`:E<86400?`${Math.floor(E/3600)}h ago`:$.toLocaleDateString()};return t("div",{className:"sync-status-card",children:[t("div",{className:"sync-status-header",children:[t("span",{className:"sync-status-title",children:"Sync Status"}),t("div",{className:"sync-status-indicator",children:[t("div",{className:`sync-status-dot ${g.color}`}),t("span",{children:g.label})]})]}),t("div",{className:"sync-status-description",children:g.description}),v&&t("div",{className:"sync-progress-bar",children:t("div",{className:"sync-progress-fill",style:{width:_!==null?`${_}%`:"100%",animation:_===null?"pulse 1.5s ease-in-out infinite":"none",opacity:_===null?.6:1}})}),v&&y&&t("div",{className:"sync-details",children:[t("div",{className:"sync-progress-main",children:y.type==="music"?t(M,{children:[Re(y.bytesTransferred)," transferred",_!==null&&` (${_}%)`]}):t(M,{children:y.bytesTransferred>0?t(M,{children:[Re(y.bytesTransferred)," transferred",_!==null&&` (${_}%)`]}):y.filesTotal>0?t(M,{children:[y.filesDone," / ",y.filesTotal," files",_!==null&&` (${_}%)`]}):"Archiving..."})}),(y.speed||y.eta)&&t("div",{className:"sync-progress-secondary",children:[y.speed&&t("span",{children:y.speed}),y.speed&&y.eta&&t("span",{children:" ยท "}),y.eta&&t("span",{children:kt(y.eta)})]})]}),v&&!y&&i==="archiving"&&t("div",{className:"sync-details",children:t("div",{className:"sync-progress-main",children:"Preparing..."})}),!v&&h&&t("div",{className:"sync-details",children:["Last sync: ",x(h)]}),l&&i==="complete"&&t("div",{className:"sync-details",children:["Completed in ",l]}),(i==="idle"||i==="complete"||i==="error")&&t("button",{className:"btn btn-primary btn-sm",onClick:n,disabled:r,style:{marginTop:"0.75rem"},children:[t(st,{style:{width:14,height:14},className:r?"spinning":""}),t("span",{children:r?"Starting...":"Sync Now"})]})]})}function Ct({status:e,computed:n,config:r,storage:o,onRefresh:s}){const[i,a]=w(!1),{lines:l}=sn("archiveloop.log",3e3,!0),h=gt(l),c=se(()=>{var v;return h.state==="archiving"&&((v=h.message)==null?void 0:v.toLowerCase().includes("music"))},[h.state,h.message]),f=se(()=>{var v;return h.state==="archiving"&&!((v=h.message)!=null&&v.toLowerCase().includes("music"))},[h.state,h.message]),d=bt(c,1500),u=wt(f,1500),p=B(async()=>{a(!0);try{await Bn(),setTimeout(s,1e3)}catch(v){console.error("Trigger sync failed:",v)}finally{a(!1)}},[s]),g=[{key:"cam",label:"TeslaCam",icon:rn,enabled:(r==null?void 0:r.has_cam)==="yes"},{key:"music",label:"Music",icon:ct,enabled:(r==null?void 0:r.has_music)==="yes"},{key:"lightshow",label:"LightShow",icon:Ee,enabled:(r==null?void 0:r.has_lightshow)==="yes"},{key:"boombox",label:"Boombox",icon:Ee,enabled:(r==null?void 0:r.has_boombox)==="yes"}];return(r==null?void 0:r.uses_ble)==="yes"&&g.push({key:"ble",label:"BLE",icon:en,enabled:!0}),t("div",{className:"dashboard-content",children:[t("div",{className:"dashboard-section",children:[t("div",{className:"section-title",children:"Configured Features"}),t("div",{className:"features-row",children:g.map(({key:v,label:k,enabled:b})=>t("div",{className:`feature-badge ${b?"enabled":"disabled"}`,children:k},v))})]}),t("div",{className:"dashboard-section",children:t("div",{className:"card",children:[t("div",{className:"card-header",children:[t("span",{className:"card-title",children:"Storage"}),t("span",{className:"card-value",children:[(n==null?void 0:n.diskTotalGB)||"0"," GB"]})]}),t(Nt,{storage:o,total:e!=null&&e.total_space?parseInt(e.total_space,10):0,free:e!=null&&e.free_space?parseInt(e.free_space,10):0,config:r})]})}),t("div",{className:"dashboard-section",children:t(xt,{syncStatus:h,onTriggerSync:p,loading:i,musicProgress:d,camProgress:u})})]})}const _e={front:"Front",back:"Back",left_repeater:"Left Repeater",right_repeater:"Right Repeater",left_pillar:"Left Pillar",right_pillar:"Right Pillar"},je=[{id:"6",name:"All Cameras",cameras:Object.keys(_e),cols:3},{id:"4-front",name:"Front Focus",cameras:["front","left_repeater","right_repeater","back"],cols:2},{id:"4-rear",name:"Rear Focus",cameras:["back","left_repeater","right_repeater","front"],cols:2},{id:"2-side",name:"Side View",cameras:["left_repeater","right_repeater"],cols:2},{id:"1-front",name:"Front Only",cameras:["front"],cols:1},{id:"1-back",name:"Rear Only",cameras:["back"],cols:1}];function St(){const[e,n]=w(null),[r,o]=w(!0),[s,i]=w(null),[a,l]=w("SentryClips"),[h,c]=w(null),[f,d]=w(null),[u,p]=w(!1),[g,v]=w(0),[k,b]=w(0),[_,y]=w(je[0]),[x,$]=w(!1),[S,E]=w(null),U=H({}),R=H(null);F(()=>{z()},[]);const z=async()=>{try{o(!0);const m=await Tn();n(m);for(const C of["SentryClips","SavedClips","RecentClips"]){const I=Object.keys(m[C]||{});if(I.length>0){l(C),c(I[0]);break}}}catch(m){i(m.message)}finally{o(!1)}};F(()=>{R.current=null,v(0),b(0),a==="SentryClips"&&h?On(h).then(E):E(null)},[a,h]);const A=B(()=>{var O;if(!e||!h)return[];const m=((O=e[a])==null?void 0:O[h])||[],C=new Set;for(const G of m){const K=G.match(/(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})/);K&&C.add(K[1])}const I=Array.from(C).sort().reverse();return I.length>1?I:[]},[e,a,h])();F(()=>{A.length>0&&!f&&d(A[0])},[A,f]);const le=B(()=>{var O;if(!e||!h)return{};const m=((O=e[a])==null?void 0:O[h])||[],C={},I=A.length>0?f:null;for(const G of m)if(!(I&&!G.startsWith(I))){for(const K of Object.keys(_e))if(G.includes(`-${K}.mp4`)||G.includes(`_${K}.mp4`)){C[K]=Un(a,h,G);break}}return C},[e,a,h,f,A])(),on=B(()=>{Object.values(U.current).forEach(m=>{m&&m.play()}),p(!0)},[]),ke=B(()=>{Object.values(U.current).forEach(m=>{m&&m.pause()}),p(!1)},[]),q=B(m=>{Object.values(U.current).forEach(C=>{C&&(C.currentTime=m)}),v(m)},[]),an=B(()=>{q(Math.max(0,g-10))},[g,q]),ln=B(()=>{q(Math.min(k,g+30))},[g,k,q]),cn=B(m=>C=>{m===R.current&&v(C.target.currentTime)},[]),dn=B(m=>C=>{const I=C.target.duration;I&&I!==1/0&&!isNaN(I)&&(R.current||(R.current=m),m===R.current&&b(I))},[]),hn=B(m=>{const C=m.currentTarget.getBoundingClientRect(),O=(m.clientX-C.left)/C.width*k;q(O)},[k,q]),we=m=>{if(!m||isNaN(m))return"0:00";const C=Math.floor(m/60),I=Math.floor(m%60);return`${C}:${I.toString().padStart(2,"0")}`},un=e!=null&&e[a]?Object.keys(e[a]).sort().reverse():[];if(r)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:t("span",{className:"text-muted",children:"Loading recordings..."})});if(s)return t("div",{className:"video-viewer",style:{justifyContent:"center",alignItems:"center"},children:[t("span",{className:"status-unhealthy",children:["Error: ",s]}),t("button",{className:"btn",onClick:z,style:{marginTop:"1rem"},children:"Retry"})]});const fn=h&&Object.keys(le).length>0;return t("div",{className:"video-viewer",children:[t("div",{className:"video-selector",style:{display:"flex",gap:"0.5rem",padding:"8px 12px",background:"#1a1a1a",borderBottom:"1px solid #333",alignItems:"center",flexWrap:"wrap"},children:[t("select",{value:a,onChange:m=>{l(m.target.value);const C=Object.keys(e[m.target.value]||{});c(C[0]||null),d(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px"},children:[t("option",{value:"SentryClips",children:"Sentry Clips"}),t("option",{value:"SavedClips",children:"Saved Clips"}),t("option",{value:"RecentClips",children:"Recent Clips"})]}),t("select",{value:h||"",onChange:m=>{c(m.target.value),d(null)},style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",flex:1,maxWidth:"180px"},children:un.map(m=>t("option",{value:m,children:Lt(m)},m))}),A.length>0&&t("select",{value:f||"",onChange:m=>d(m.target.value),style:{background:"#333",color:"#fff",border:"1px solid #444",borderRadius:"4px",padding:"6px 10px",fontSize:"13px",maxWidth:"120px"},children:A.map(m=>t("option",{value:m,children:Tt(m)},m))}),t("div",{style:{position:"relative"},children:[t("button",{className:"btn btn-sm btn-dark",onClick:()=>$(!x),children:[t(ht,{}),t("span",{children:_.name}),t(tn,{})]}),x&&t("div",{style:{position:"absolute",top:"100%",right:0,background:"#2a2a2a",border:"1px solid #444",borderRadius:"6px",marginTop:"4px",zIndex:100,minWidth:"150px"},children:je.map(m=>t("button",{onClick:()=>{y(m),$(!1)},style:{display:"block",width:"100%",padding:"8px 12px",background:_.id===m.id?"#0095f6":"transparent",color:"#fff",border:"none",textAlign:"left",cursor:"pointer",fontSize:"13px"},children:m.name},m.id))})]}),S&&S.city&&t("div",{style:{display:"flex",alignItems:"center",gap:"4px",color:"#888",fontSize:"12px",marginLeft:"auto"},children:[t(lt,{style:{width:14,height:14}}),t("span",{children:S.city})]})]}),fn?t(M,{children:[t("div",{className:`video-grid layout-${_.cameras.length}`,style:{gridTemplateColumns:`repeat(${_.cols}, 1fr)`},children:_.cameras.map(m=>t("div",{className:"video-cell",children:[le[m]?t("video",{ref:C=>{U.current[m]=C},src:le[m],onTimeUpdate:cn(m),onDurationChange:dn(m),onEnded:ke,muted:!0,playsInline:!0}):t("div",{style:{color:"#666",fontSize:"12px"},children:"No video"}),t("div",{className:"video-cell-label",children:_e[m]})]},m))}),t("div",{className:"video-controls",children:[t("button",{className:"btn btn-sm",onClick:an,title:"Skip back 10s",children:t(Xn,{})}),t("button",{className:"video-play-btn",onClick:u?ke:on,children:u?t(Jn,{}):t(Zn,{})}),t("button",{className:"btn btn-sm",onClick:ln,title:"Skip forward 30s",children:t(Qn,{})}),t("div",{className:"video-time",children:[we(g)," / ",we(k)]}),t("div",{className:"video-timeline",onClick:hn,children:t("div",{className:"video-timeline-progress",style:{width:k>0?`${g/k*100}%`:"0%"}})})]})]}):t("div",{style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",color:"#888",fontSize:"14px"},children:["No recordings in ",a==="SentryClips"?"Sentry Clips":a==="SavedClips"?"Saved Clips":"Recent Clips"]})]})}function Lt(e){const n=e.match(/(\d{4})-(\d{2})-(\d{2})(?:_(\d{2})-(\d{2})-(\d{2}))?/);if(n){const[,r,o,s,i,a,l]=n,h=new Date(r,o-1,s,i||0,a||0,l||0);return i?h.toLocaleString([],{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):h.toLocaleDateString([],{month:"short",day:"numeric",year:"numeric"})}return e}function Tt(e){const n=e.match(/\d{4}-\d{2}-\d{2}_(\d{2})-(\d{2})-(\d{2})/);if(n){const[,r,o]=n;return new Date(2e3,0,1,parseInt(r),parseInt(o)).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}return e}function $t({config:e}){const n=H(null),r=H(null),[o,s]=w(!!window.FileBrowser);F(()=>{if(!document.querySelector('link[href="/filebrowser.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/filebrowser.css",document.head.appendChild(c)}},[]),F(()=>{if(window.FileBrowser){s(!0);return}const c=document.createElement("script");c.src="/filebrowser.js",c.onload=()=>s(!0),c.onerror=()=>console.error("Failed to load filebrowser.js"),document.head.appendChild(c)},[]);const i=(e==null?void 0:e.has_music)==="yes",a=(e==null?void 0:e.has_lightshow)==="yes",l=(e==null?void 0:e.has_boombox)==="yes";return F(()=>{if(!o||!n.current)return;const c=[];if(i&&c.push({path:"fs/Music",label:"Music"}),a&&c.push({path:"fs/LightShow",label:"LightShow"}),l&&c.push({path:"fs/Boombox",label:"Boombox"}),c.length!==0&&!r.current){try{r.current=new window.FileBrowser(n.current,c)}catch(f){console.error("Failed to initialize FileBrowser:",f)}return()=>{n.current&&(n.current.innerHTML=""),r.current=null}}},[o,i,a,l]),(e==null?void 0:e.has_music)==="yes"||(e==null?void 0:e.has_lightshow)==="yes"||(e==null?void 0:e.has_boombox)==="yes"?o?t("div",{ref:n,style:{width:"100%",height:"calc(100% - 4px)",minHeight:"400px",flex:1,display:"flex",position:"relative"}}):t("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%",minHeight:"400px",color:"#9ca3af",fontSize:"14px"},children:"Loading file browser..."}):t("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:"#9ca3af",fontSize:"14px"},children:[t("svg",{style:{width:32,height:32,marginBottom:8,color:"#d1d5db"},viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:t("path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"})}),t("span",{children:"No file drives configured"})]})}const ee={archiveloop:{label:"Archive Log",file:"archiveloop.log",description:"Sync and archive operations"},setup:{label:"Setup Log",file:"teslausb-headless-setup.log",description:"Initial setup and configuration"},diagnostics:{label:"Diagnostics",file:"diagnostics.txt",description:"System diagnostics report"}};function Bt(){const[e,n]=w("archiveloop"),[r,o]=w(!1),[s,i]=w(null),a=H(null),l=e!=="diagnostics",{lines:h,loading:c,error:f,refresh:d,clear:u}=sn(l?ee[e].file:"",2e3,l);F(()=>{a.current&&l&&(a.current.scrollTop=a.current.scrollHeight)},[h,l]);const p=B(async()=>{o(!0),i(null);try{await Rn();const _=await jn();i(_)}catch(_){i(`Error generating diagnostics: ${_.message}`)}finally{o(!1)}},[]);F(()=>{e==="diagnostics"&&!s&&p()},[e,s,p]);const g=B(()=>{const _=e==="diagnostics"?s:h.join(` +`),y=ee[e].file,x=new Blob([_],{type:"text/plain"}),$=URL.createObjectURL(x),S=document.createElement("a");S.href=$,S.download=y,document.body.appendChild(S),S.click(),document.body.removeChild(S),URL.revokeObjectURL($)},[e,h,s]),v=_=>{const y=_.toLowerCase();return y.includes("error")||y.includes("failed")||y.includes("fatal")?"error":y.includes("warning")||y.includes("warn")?"warning":y.includes("success")||y.includes("completed")||y.includes("finished")?"success":""},k=ee[e],b=e==="diagnostics"?(s==null?void 0:s.split(` +`))||[]:h;return t("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[t("div",{style:{display:"flex",gap:"0.5rem",marginBottom:"1rem",flexWrap:"wrap"},children:Object.entries(ee).map(([_,y])=>t("button",{className:`btn ${e===_?"btn-primary":""}`,onClick:()=>n(_),children:[_==="diagnostics"?t(at,{}):t(nn,{}),t("span",{children:y.label})]},_))}),t("div",{className:"log-viewer",style:{flex:1},children:[t("div",{className:"log-viewer-header",children:[t("div",{className:"log-viewer-title",children:[k.label,t("span",{style:{fontWeight:400,marginLeft:"8px",opacity:.7},children:["โ€” ",k.description]})]}),t("div",{className:"log-viewer-actions",children:[e==="diagnostics"?t("button",{className:"btn btn-sm log-action-btn",onClick:p,disabled:r,children:[t(pe,{className:r?"spinning":""}),t("span",{children:"Regenerate"})]}):t(M,{children:[t("button",{className:"btn btn-sm log-action-btn",onClick:d,disabled:c,title:"Refresh",children:t(pe,{className:c?"spinning":""})}),t("button",{className:"btn btn-sm log-action-btn",onClick:u,title:"Clear",children:t(nt,{})})]}),t("button",{className:"btn btn-sm log-action-btn",onClick:g,disabled:b.length===0,title:"Download",children:t(et,{})})]})]}),t("div",{className:"log-viewer-content",ref:a,children:c&&b.length===0||r?t("div",{style:{color:"#888",fontStyle:"italic"},children:"Loading..."}):f?f.includes("not found")?t("div",{style:{color:"#888",fontStyle:"italic"},children:["Log file not available. ",e==="setup"&&"The setup log is only present during initial setup."]}):t("div",{style:{color:"#f87171"},children:["Error: ",f]}):b.length===0?t("div",{style:{color:"#888",fontStyle:"italic"},children:"No log entries"}):b.map((_,y)=>t("div",{className:`log-line ${v(_)}`,children:_},y))})]}),l&&h.length>0&&t("div",{style:{marginTop:"0.5rem",fontSize:"11px",color:"#666",display:"flex",gap:"1rem"},children:[t("span",{children:[h.length," lines"]}),t("span",{children:"Auto-refreshing every 2s"})]})]})}function It(){return t("div",{className:"loading-container",children:[t("div",{className:"spinner"}),t("span",{children:"Loading..."})]})}const W={DASHBOARD:"dashboard",VIEWER:"viewer",FILES:"files",LOGS:"logs"};function Mt(){const[e,n]=w(W.DASHBOARD),[r,o]=w(!1),{status:s,config:i,storage:a,computed:l,loading:h,error:c,lastUpdate:f,refresh:d}=Vn(5e3),u=[];return u.push({id:W.DASHBOARD,label:"Dashboard"}),(i==null?void 0:i.has_cam)==="yes"&&u.push({id:W.VIEWER,label:"Viewer"}),((i==null?void 0:i.has_music)==="yes"||(i==null?void 0:i.has_lightshow)==="yes"||(i==null?void 0:i.has_boombox)==="yes")&&u.push({id:W.FILES,label:"Files"}),u.push({id:W.LOGS,label:"Logs"}),h&&!s?t(It,{}):c&&!s?t("div",{className:"error-container",children:[t("span",{children:["Failed to load: ",c]}),t("button",{className:"retry-btn",onClick:d,children:"Retry"})]}):t("div",{className:"app-shell",children:[t("div",{className:"app-topbar",children:[t("div",{className:"app-topbar-title",children:[t(Gn,{}),t("span",{children:"TeslaUSB"})]}),t("div",{className:"app-topbar-actions",children:t("span",{className:`app-status ${l!=null&&l.drivesActive?"healthy":"warning"}`,children:l!=null&&l.drivesActive?"Connected":"Disconnected"})})]}),t(pt,{tabs:u,activeTab:e,onTabChange:n,lastUpdate:f,onRefresh:d}),t("div",{className:"mobile-quick-status",children:[t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.drivesActive?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"USB"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.wifiConnected?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:"WiFi"})]}),t("div",{className:"quick-status-item",children:[t("div",{className:`status-dot-mini ${l!=null&&l.cpuTempC&&parseFloat(l.cpuTempC)<70?"healthy":"unhealthy"}`}),t("span",{className:"quick-status-label",children:[l==null?void 0:l.cpuTempC,"ยฐC"]})]})]}),t("div",{className:"app-body",children:[e===W.DASHBOARD&&t(_t,{status:s,computed:l,config:i,expanded:r,onToggle:()=>o(!r),onRefresh:d}),t("main",{className:"app-main",children:[e===W.DASHBOARD&&t(Ct,{status:s,computed:l,config:i,storage:a,onRefresh:d}),e===W.VIEWER&&(i==null?void 0:i.has_cam)==="yes"&&t(St,{}),e===W.FILES&&t($t,{config:i}),e===W.LOGS&&t(Bt,{})]})]})]})}bn(t(Mt,{}),document.getElementById("app")); diff --git a/teslausb-www-react/dist/index.html b/teslausb-www-react/dist/index.html index e97bc083..08076b20 100644 --- a/teslausb-www-react/dist/index.html +++ b/teslausb-www-react/dist/index.html @@ -42,7 +42,7 @@ url('/fonts/lato-italic.woff2') format('woff2'); } - + diff --git a/teslausb-www-react/src/components/Dashboard.jsx b/teslausb-www-react/src/components/Dashboard.jsx index d219cdbb..d2a33a6f 100644 --- a/teslausb-www-react/src/components/Dashboard.jsx +++ b/teslausb-www-react/src/components/Dashboard.jsx @@ -1,6 +1,6 @@ import { useState, useCallback, useMemo } from 'preact/hooks'; import { useLogTail, parseSyncStatus } from '../hooks/useLogTail'; -import { useMusicSyncProgress } from '../hooks/useMusicSyncProgress'; +import { useMusicSyncProgress, useCamSyncProgress } from '../hooks/useMusicSyncProgress'; import { triggerSync } from '../services/api'; import { CameraIcon, @@ -24,9 +24,18 @@ export function Dashboard({ status, computed, config, storage, onRefresh }) { syncStatus.message?.toLowerCase().includes('music'); }, [syncStatus.state, syncStatus.message]); + // Check if CAM archiving is active (for enabling progress polling) + const isCamSyncActive = useMemo(() => { + return syncStatus.state === 'archiving' && + !syncStatus.message?.toLowerCase().includes('music'); + }, [syncStatus.state, syncStatus.message]); + // Poll music sync progress when music sync is active const musicProgress = useMusicSyncProgress(isMusicSyncActive, 1500); + // Poll CAM sync progress when CAM archiving is active + const camProgress = useCamSyncProgress(isCamSyncActive, 1500); + const handleTriggerSync = useCallback(async () => { setSyncLoading(true); try { @@ -89,6 +98,7 @@ export function Dashboard({ status, computed, config, storage, onRefresh }) { onTriggerSync={handleTriggerSync} loading={syncLoading} musicProgress={musicProgress} + camProgress={camProgress} /> diff --git a/teslausb-www-react/src/components/SyncStatus.jsx b/teslausb-www-react/src/components/SyncStatus.jsx index b8d46bd8..f74c3713 100644 --- a/teslausb-www-react/src/components/SyncStatus.jsx +++ b/teslausb-www-react/src/components/SyncStatus.jsx @@ -4,12 +4,16 @@ import { formatBytes, formatEta } from '../hooks/useMusicSyncProgress'; /** * Compact Sync Status Card */ -export function SyncStatus({ syncStatus, onTriggerSync, loading, musicProgress }) { - const { state, totalFiles, archivedFiles, message, elapsedTime, lastActivity } = syncStatus; +export function SyncStatus({ syncStatus, onTriggerSync, loading, musicProgress, camProgress }) { + const { state, message, elapsedTime, lastActivity } = syncStatus; - // Check if this is an active music sync with progress data + // Determine sync type based on message const isMusicSync = message?.toLowerCase().includes('music') && state === 'archiving'; - const hasMusicProgress = musicProgress?.active && musicProgress?.percentage > 0; + const isCamSync = state === 'archiving' && !isMusicSync; + + // Check if we have active progress data from the respective APIs + const hasMusicProgress = musicProgress?.active; + const hasCamProgress = camProgress?.active; const getStateInfo = () => { switch (state) { @@ -31,16 +35,39 @@ export function SyncStatus({ syncStatus, onTriggerSync, loading, musicProgress } const stateInfo = getStateInfo(); const isActive = state === 'archiving' || state === 'connecting'; - // Determine which progress to show + // Determine which progress source to use const showMusicProgress = isMusicSync && hasMusicProgress; - const showFileProgress = state === 'archiving' && totalFiles > 0 && !showMusicProgress; + const showCamProgress = isCamSync && hasCamProgress; + const showAnyProgress = showMusicProgress || showCamProgress; - // Calculate progress percentage + // Calculate progress percentage from the active source let progressPercent = null; - if (showMusicProgress) { + let progressDetails = null; + + if (showMusicProgress && musicProgress.percentage > 0) { progressPercent = musicProgress.percentage; - } else if (showFileProgress && archivedFiles > 0) { - progressPercent = Math.min(Math.round((archivedFiles / totalFiles) * 100), 100); + progressDetails = { + type: 'music', + bytesTransferred: musicProgress.bytesTransferred, + speed: musicProgress.speed, + eta: musicProgress.eta, + }; + } else if (showCamProgress) { + // Use percentage if available, otherwise calculate from files + if (camProgress.percentage > 0) { + progressPercent = camProgress.percentage; + } else if (camProgress.filesTotal > 0 && camProgress.filesDone > 0) { + progressPercent = Math.min(Math.round((camProgress.filesDone / camProgress.filesTotal) * 100), 100); + } + progressDetails = { + type: 'cam', + bytesTransferred: camProgress.bytesTransferred, + speed: camProgress.speed, + eta: camProgress.eta, + filesDone: camProgress.filesDone, + filesTotal: camProgress.filesTotal, + currentFile: camProgress.currentFile, + }; } const formatLastActivity = (date) => { @@ -68,7 +95,7 @@ export function SyncStatus({ syncStatus, onTriggerSync, loading, musicProgress } {/* Progress bar - shown during archiving */} - {(showMusicProgress || showFileProgress || isActive) && ( + {isActive && (
          )} - {/* Music sync progress details */} - {showMusicProgress && ( + {/* Progress details - consistent format for both sync types */} + {isActive && progressDetails && (
          + {/* Primary progress line */}
          - {formatBytes(musicProgress.bytesTransferred)} transferred - {progressPercent !== null && ` (${progressPercent}%)`} + {progressDetails.type === 'music' ? ( + <> + {formatBytes(progressDetails.bytesTransferred)} transferred + {progressPercent !== null && ` (${progressPercent}%)`} + + ) : ( + <> + {progressDetails.bytesTransferred > 0 ? ( + <> + {formatBytes(progressDetails.bytesTransferred)} transferred + {progressPercent !== null && ` (${progressPercent}%)`} + + ) : progressDetails.filesTotal > 0 ? ( + <> + {progressDetails.filesDone} / {progressDetails.filesTotal} files + {progressPercent !== null && ` (${progressPercent}%)`} + + ) : ( + 'Archiving...' + )} + + )}
          - {(musicProgress.speed || musicProgress.eta) && ( + {/* Secondary line with speed and ETA */} + {(progressDetails.speed || progressDetails.eta) && (
          - {musicProgress.speed && {musicProgress.speed}} - {musicProgress.speed && musicProgress.eta && ยท } - {musicProgress.eta && {formatEta(musicProgress.eta)}} + {progressDetails.speed && {progressDetails.speed}} + {progressDetails.speed && progressDetails.eta && ยท } + {progressDetails.eta && {formatEta(progressDetails.eta)}}
          )}
          )} - {/* File progress details (for TeslaCam archiving) */} - {showFileProgress && ( + {/* Indeterminate progress message when archiving but no progress data yet */} + {isActive && !progressDetails && state === 'archiving' && (
          - {archivedFiles} / {totalFiles} files - {progressPercent !== null && ` (${progressPercent}%)`} +
          + Preparing... +
          )} {/* Last activity for idle state */} - {!isActive && !showFileProgress && !showMusicProgress && lastActivity && ( + {!isActive && lastActivity && (
          Last sync: {formatLastActivity(lastActivity)}
          diff --git a/teslausb-www-react/src/hooks/useMusicSyncProgress.js b/teslausb-www-react/src/hooks/useMusicSyncProgress.js index 9f5737ce..97a01c7c 100644 --- a/teslausb-www-react/src/hooks/useMusicSyncProgress.js +++ b/teslausb-www-react/src/hooks/useMusicSyncProgress.js @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'preact/hooks'; -import { fetchMusicSyncProgress } from '../services/api'; +import { fetchMusicSyncProgress, fetchCamSyncProgress } from '../services/api'; /** * Hook for polling music sync progress @@ -97,4 +97,78 @@ export function formatEta(eta) { return `~${cleaned} remaining`; } +/** + * Hook for polling TeslaCam archive progress + * @param {boolean} enabled - Whether to enable polling (should be true when cam archiving is active) + * @param {number} pollInterval - Polling interval in ms (default 1500) + * @returns {Object} Cam sync progress data + */ +export function useCamSyncProgress(enabled = false, pollInterval = 1500) { + const [progress, setProgress] = useState({ + active: false, + bytesTransferred: 0, + percentage: 0, + speed: '', + eta: '', + filesDone: 0, + filesTotal: 0, + currentFile: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const enabledRef = useRef(enabled); + + const refresh = useCallback(async () => { + try { + const data = await fetchCamSyncProgress(); + setProgress(data); + setError(null); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }, []); + + // Update ref when enabled changes + useEffect(() => { + enabledRef.current = enabled; + }, [enabled]); + + useEffect(() => { + if (!enabled) { + // Reset progress when disabled + setProgress({ + active: false, + bytesTransferred: 0, + percentage: 0, + speed: '', + eta: '', + filesDone: 0, + filesTotal: 0, + currentFile: '', + }); + return; + } + + setLoading(true); + refresh(); // Initial fetch + + const interval = setInterval(() => { + if (enabledRef.current) { + refresh(); + } + }, pollInterval); + + return () => clearInterval(interval); + }, [enabled, pollInterval, refresh]); + + return { + ...progress, + loading, + error, + refresh, + }; +} + export default useMusicSyncProgress; diff --git a/teslausb-www-react/src/services/api.js b/teslausb-www-react/src/services/api.js index e86eaae7..4159f6f2 100644 --- a/teslausb-www-react/src/services/api.js +++ b/teslausb-www-react/src/services/api.js @@ -252,6 +252,16 @@ export async function fetchMusicSyncProgress() { return response.json(); } +/** + * Fetch TeslaCam archive progress + * @returns {Promise} Progress object with active, bytesTransferred, percentage, speed, eta, filesDone, filesTotal + */ +export async function fetchCamSyncProgress() { + const response = await fetch(`${API_BASE}/cam_sync_progress.sh`); + if (!response.ok) throw new Error('Failed to fetch cam sync progress'); + return response.json(); +} + /** * Toggle USB drives visibility * @returns {Promise} diff --git a/teslausb-www/html/cgi-bin/cam_sync_progress.sh b/teslausb-www/html/cgi-bin/cam_sync_progress.sh new file mode 100755 index 00000000..3acc5de7 --- /dev/null +++ b/teslausb-www/html/cgi-bin/cam_sync_progress.sh @@ -0,0 +1,124 @@ +#!/bin/bash +# Returns current TeslaCam archive progress by parsing rsync output or counting files + +# Possible rsync log locations for cam archiving +RSYNC_LOGS=("/tmp/rsyncarclog.txt" "/tmp/rsync-cam.log" "/tmp/archive-rsync.log") + +# Default values +active="false" +bytes="0" +percentage="0" +speed="" +eta="" +files_done="0" +files_total="0" +current_file="" + +# Check if cam archiving is active (rsync process for TeslaCam) +# Look for rsync processes that involve TeslaCam or cam paths +if pgrep -f "rsync.*TeslaCam" > /dev/null 2>&1 || \ + pgrep -f "rsync.*/mnt/cam" > /dev/null 2>&1 || \ + pgrep -f "rsync.*archive" > /dev/null 2>&1; then + active="true" + + # Try each possible log file location + LOG="" + for log_file in "${RSYNC_LOGS[@]}"; do + if [ -f "$log_file" ]; then + LOG="$log_file" + break + fi + done + + # If we found a log file, parse it for progress + if [ -n "$LOG" ] && [ -f "$LOG" ]; then + # Get the last progress update from rsync --info=progress2 output + # rsync uses \r for in-place updates, so the latest progress may not have a trailing newline + progress_line=$(tail -c 500 "$LOG" 2>/dev/null | tr '\r' '\n' | grep -E '[0-9]+%' | tail -1) + + if [ -n "$progress_line" ]; then + # Parse using more flexible pattern matching + # Extract bytes: first number (with possible commas) + bytes=$(echo "$progress_line" | grep -oE '^[[:space:]]*[0-9,]+' | tr -d ', ') + + # Extract percentage: number followed by % + percentage=$(echo "$progress_line" | grep -oE '[0-9]+%' | head -1 | tr -d '%') + + # Extract speed: number followed by unit/s (e.g., 1.07MB/s, 500kB/s) + speed=$(echo "$progress_line" | grep -oE '[0-9.]+[kMGT]?B/s' | head -1) + + # Extract ETA: time format like 0:01:23 or 6:27:37 + eta=$(echo "$progress_line" | grep -oE '[0-9]+:[0-9]+:[0-9]+' | head -1) + + # Ensure we have valid defaults + [ -z "$bytes" ] && bytes="0" + [ -z "$percentage" ] && percentage="0" + fi + + # Try to get file counts from rsync stats (at the end of the log) + # Format: "Number of regular files transferred: X" + xfr_line=$(grep -E 'xfr#[0-9]+' "$LOG" 2>/dev/null | tail -1) + if [ -n "$xfr_line" ]; then + # Extract xfr#N where N is files transferred so far + files_done=$(echo "$xfr_line" | grep -oE 'xfr#[0-9]+' | grep -oE '[0-9]+') + # Try to get total from to-chk=X/Y + total_from_chk=$(echo "$xfr_line" | grep -oE 'to-chk=[0-9]+/[0-9]+' | cut -d'/' -f2) + if [ -n "$total_from_chk" ]; then + files_total="$total_from_chk" + fi + fi + + # Try to get current file being transferred + current_file=$(tail -c 500 "$LOG" 2>/dev/null | tr '\r' '\n' | grep -E '^\S+\.(mp4|MP4)' | tail -1 | head -c 100) + fi +fi + +# If not active via rsync detection, check archiveloop.log for archiving state +if [ "$active" = "false" ]; then + ARCHIVELOG="/mutable/archiveloop.log" + if [ -f "$ARCHIVELOG" ]; then + # Get last 50 lines to check state + recent=$(tail -50 "$ARCHIVELOG" 2>/dev/null) + + # Check if we're in archiving state (started but not finished) + if echo "$recent" | grep -q "Starting recording archiving\|Archiving\.\.\.\|Running fsck\|Checking saved folder count"; then + # Check if archiving has completed + if ! echo "$recent" | grep -q "Archiving completed successfully\|Finished archiving"; then + active="true" + + # Try to get file counts from log + # Format: "There are X event folder(s) with Y file(s) and Z track mode file(s)" + file_count_line=$(echo "$recent" | grep -E 'There are [0-9]+ event folder' | tail -1) + if [ -n "$file_count_line" ]; then + sentry_files=$(echo "$file_count_line" | grep -oE 'with [0-9]+ file' | grep -oE '[0-9]+') + track_files=$(echo "$file_count_line" | grep -oE 'and [0-9]+ track mode' | grep -oE '[0-9]+') + [ -z "$track_files" ] && track_files="0" + files_total=$((sentry_files + track_files)) + fi + + # Alternative format: "Archiving X file(s)" + if [ "$files_total" = "0" ]; then + archiving_line=$(echo "$recent" | grep -E 'Archiving [0-9]+ (track mode )?file' | tail -1) + if [ -n "$archiving_line" ]; then + files_total=$(echo "$archiving_line" | grep -oE '[0-9]+' | head -1) + fi + fi + fi + fi + fi +fi + +# Output JSON +echo "HTTP/1.0 200 OK" +echo "Content-type: application/json" +echo "" +echo "{" +echo " \"active\": $active," +echo " \"bytesTransferred\": $bytes," +echo " \"percentage\": $percentage," +echo " \"speed\": \"$speed\"," +echo " \"eta\": \"$eta\"," +echo " \"filesDone\": $files_done," +echo " \"filesTotal\": $files_total," +echo " \"currentFile\": \"$current_file\"" +echo "}" From 25574870f03057063cd62465e31f957c0f3f297e Mon Sep 17 00:00:00 2001 From: oaquique <30644106+oaquique@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:41:45 -0800 Subject: [PATCH 4/4] Fix files tab content being cut off at bottom of viewport Increased bottom padding in .app-main from 1rem to 3.5rem (56px) to ensure all file list rows are fully visible and scrollable. Co-Authored-By: Claude Opus 4.5 --- teslausb-www-react/src/styles/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslausb-www-react/src/styles/index.css b/teslausb-www-react/src/styles/index.css index 0cd28831..2d3a19f8 100644 --- a/teslausb-www-react/src/styles/index.css +++ b/teslausb-www-react/src/styles/index.css @@ -543,7 +543,7 @@ body { .app-main { flex: 1; background-color: #ffffff; - padding: 1rem; + padding: 1rem 1rem 3.5rem 1rem; overflow-y: auto; display: flex; flex-direction: column;