From 8c377634aa21983a8a13b78fcacbf2d6197875de Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Sun, 28 Dec 2025 06:04:05 +0000 Subject: [PATCH] Sync build v0.6.79 Automated sync from private repository. Commit: b73bd1649d6337a28052f0e296c9d09fa9f03641 --- INSTALL.md | 66 + LICENSE | 21 + README.md | 305 +- RELEASE.md | 313 ++ frontend/dist/VERSION | 1 + frontend/dist/assets/Contacts-MBZsEaUa.js | 2 + .../assets/ContactsMapMapLibre-l8mOIn1t.js | 1 + frontend/dist/assets/Dashboard-qkMQrXs8.js | 1 + frontend/dist/assets/HashBadge-CTAxN-Gl.js | 1 + frontend/dist/assets/Logs-Ie0JKnoJ.js | 1 + .../dist/assets/PacketDetailModal-CyODg2k8.js | 2 + frontend/dist/assets/Packets-DmDlJKBx.js | 1 + frontend/dist/assets/PageLayout-Com69Ajh.js | 1 + .../dist/assets/PathMapMapLibre-BGMw9FN4.js | 1 + frontend/dist/assets/Settings-C1vzdsku.js | 1 + .../dist/assets/SignalIndicator-BjDxWf6l.js | 1 + frontend/dist/assets/Statistics-CTKpbczO.js | 1 + frontend/dist/assets/System-C64dU0Df.js | 1 + frontend/dist/assets/Terminal-Cxgo-Id7.js | 1 + .../dist/assets/TimeRangeSelector-COWpqZUO.js | 1 + frontend/dist/assets/WCM_Waves-CI2kWZFp.gif | Bin 0 -> 12801 bytes frontend/dist/assets/activity-BjSGn1T6.js | 1 + frontend/dist/assets/bg-amber.jpg | Bin 0 -> 252845 bytes frontend/dist/assets/bg-black.jpg | Bin 0 -> 41504 bytes frontend/dist/assets/bg-flora.jpg | Bin 0 -> 302966 bytes frontend/dist/assets/bg-grey.jpg | Bin 0 -> 221455 bytes frontend/dist/assets/bg.jpg | Bin 0 -> 102248 bytes frontend/dist/assets/circle-D-zPvD77.js | 1 + frontend/dist/assets/house-CqfQQFQV.js | 1 + frontend/dist/assets/index-CLg20r2M.css | 1 + frontend/dist/assets/index-CylDUtFA.js | 2 + frontend/dist/assets/info-CqYfnuqu.js | 1 + frontend/dist/assets/leaflet-2NxvYO9C.js | 1 + .../dist/assets/loader-circle-BeVz0YM3.js | 1 + frontend/dist/assets/maplibre-gl-B1CfjdFi.css | 1 + frontend/dist/assets/maplibre-gl-DsiI67my.js | 1 + frontend/dist/assets/maplibre-gl-aaDJVzOI.js | 1 + frontend/dist/assets/maplibre-gl-zy3TF1FS.js | 2 + frontend/dist/assets/recharts-CGff6_7g.js | 1 + frontend/dist/assets/refresh-cw-O6setgEB.js | 1 + .../dist/assets/sparkline.worker-DYe5lgJ2.js | 1 + .../dist/assets/topology.worker-CVZSTP75.js | 1 + frontend/dist/assets/trending-up-Crl55Pcw.js | 1 + .../dist/assets/triangle-alert-BFXHkJUa.js | 1 + frontend/dist/assets/usePolling-9BR4yMYM.js | 1 + frontend/dist/assets/users-Dq_Md6JI.js | 1 + frontend/dist/assets/zap-CBQbQNHy.js | 1 + frontend/dist/file.svg | 1 + frontend/dist/globe.svg | 1 + frontend/dist/index.html | 49 + frontend/dist/next.svg | 1 + frontend/dist/vercel.svg | 1 + frontend/dist/window.svg | 1 + frontend/package.json | 48 + manage.sh | 3224 +++++++++++++++++ 55 files changed, 4070 insertions(+), 2 deletions(-) create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 RELEASE.md create mode 100644 frontend/dist/VERSION create mode 100644 frontend/dist/assets/Contacts-MBZsEaUa.js create mode 100644 frontend/dist/assets/ContactsMapMapLibre-l8mOIn1t.js create mode 100644 frontend/dist/assets/Dashboard-qkMQrXs8.js create mode 100644 frontend/dist/assets/HashBadge-CTAxN-Gl.js create mode 100644 frontend/dist/assets/Logs-Ie0JKnoJ.js create mode 100644 frontend/dist/assets/PacketDetailModal-CyODg2k8.js create mode 100644 frontend/dist/assets/Packets-DmDlJKBx.js create mode 100644 frontend/dist/assets/PageLayout-Com69Ajh.js create mode 100644 frontend/dist/assets/PathMapMapLibre-BGMw9FN4.js create mode 100644 frontend/dist/assets/Settings-C1vzdsku.js create mode 100644 frontend/dist/assets/SignalIndicator-BjDxWf6l.js create mode 100644 frontend/dist/assets/Statistics-CTKpbczO.js create mode 100644 frontend/dist/assets/System-C64dU0Df.js create mode 100644 frontend/dist/assets/Terminal-Cxgo-Id7.js create mode 100644 frontend/dist/assets/TimeRangeSelector-COWpqZUO.js create mode 100644 frontend/dist/assets/WCM_Waves-CI2kWZFp.gif create mode 100644 frontend/dist/assets/activity-BjSGn1T6.js create mode 100644 frontend/dist/assets/bg-amber.jpg create mode 100644 frontend/dist/assets/bg-black.jpg create mode 100644 frontend/dist/assets/bg-flora.jpg create mode 100644 frontend/dist/assets/bg-grey.jpg create mode 100644 frontend/dist/assets/bg.jpg create mode 100644 frontend/dist/assets/circle-D-zPvD77.js create mode 100644 frontend/dist/assets/house-CqfQQFQV.js create mode 100644 frontend/dist/assets/index-CLg20r2M.css create mode 100644 frontend/dist/assets/index-CylDUtFA.js create mode 100644 frontend/dist/assets/info-CqYfnuqu.js create mode 100644 frontend/dist/assets/leaflet-2NxvYO9C.js create mode 100644 frontend/dist/assets/loader-circle-BeVz0YM3.js create mode 100644 frontend/dist/assets/maplibre-gl-B1CfjdFi.css create mode 100644 frontend/dist/assets/maplibre-gl-DsiI67my.js create mode 100644 frontend/dist/assets/maplibre-gl-aaDJVzOI.js create mode 100644 frontend/dist/assets/maplibre-gl-zy3TF1FS.js create mode 100644 frontend/dist/assets/recharts-CGff6_7g.js create mode 100644 frontend/dist/assets/refresh-cw-O6setgEB.js create mode 100644 frontend/dist/assets/sparkline.worker-DYe5lgJ2.js create mode 100644 frontend/dist/assets/topology.worker-CVZSTP75.js create mode 100644 frontend/dist/assets/trending-up-Crl55Pcw.js create mode 100644 frontend/dist/assets/triangle-alert-BFXHkJUa.js create mode 100644 frontend/dist/assets/usePolling-9BR4yMYM.js create mode 100644 frontend/dist/assets/users-Dq_Md6JI.js create mode 100644 frontend/dist/assets/zap-CBQbQNHy.js create mode 100644 frontend/dist/file.svg create mode 100644 frontend/dist/globe.svg create mode 100644 frontend/dist/index.html create mode 100644 frontend/dist/next.svg create mode 100644 frontend/dist/vercel.svg create mode 100644 frontend/dist/window.svg create mode 100644 frontend/package.json create mode 100755 manage.sh diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..15bc6490 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,66 @@ +# Installing pyMC Console UI + +This guide covers standalone UI installation. For full pyMC Console with manage.sh installer, see [README.md](README.md). + +## Quick Install (Standalone UI) + +Download and extract the latest release to your pyMC_Repeater's web directory: + +```bash +# Download the latest release +cd /tmp +wget https://github.com/dmduran12/pymc_console/releases/latest/download/pymc-ui-latest.tar.gz + +# Extract to pyMC_Repeater's web directory +sudo mkdir -p /opt/pymc_repeater/repeater/web/html +sudo tar -xzf pymc-ui-latest.tar.gz -C /opt/pymc_repeater/repeater/web/html/ + +# Set proper permissions +sudo chown -R repeater:repeater /opt/pymc_repeater/repeater/web/html + +# Clean up +rm pymc-ui-latest.tar.gz +``` + +The dashboard will be served at `http://:8000/` + +--- + +## Quick Update + +To update an existing installation to the latest version: + +```bash +# Download latest version +cd /tmp +wget https://github.com/dmduran12/pymc_console/releases/latest/download/pymc-ui-latest.tar.gz + +# Backup and update +sudo cp -r /opt/pymc_repeater/repeater/web/html /opt/pymc_repeater/repeater/web/html.backup +sudo rm -rf /opt/pymc_repeater/repeater/web/html/* +sudo tar -xzf pymc-ui-latest.tar.gz -C /opt/pymc_repeater/repeater/web/html/ +sudo chown -R repeater:repeater /opt/pymc_repeater/repeater/web/html + +# Clean up +rm pymc-ui-latest.tar.gz +``` + +## Specific Version + +To install a specific version: + +```bash +# Replace v0.2.0 with desired version +wget https://github.com/dmduran12/pymc_console/releases/download/v0.2.0/pymc-ui-v0.2.0.tar.gz +sudo tar -xzf pymc-ui-v0.2.0.tar.gz -C /opt/pymc_repeater/repeater/web/html/ +``` + +## Using manage.sh (Recommended) + +For most users, use the manage.sh installer which handles everything automatically: + +```bash +git clone https://github.com/dmduran12/pymc_console.git +cd pymc_console +sudo ./manage.sh install +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..491c6172 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Danny Duran + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5c3331ec..f78f94db 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,303 @@ -# pymc_console -test Sat Dec 27 21:51:52 PST 2025 +# pyMC Console + +[![GitHub Release](https://img.shields.io/github/v/release/dmduran12/pymc_console)](https://github.com/dmduran12/pymc_console/releases) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + +A modern web dashboard for monitoring and managing your [MeshCore](https://meshcore.co.uk/) LoRa mesh repeater. + +Built on [pyMC_Repeater](https://github.com/rightup/pyMC_Repeater) by [RightUp](https://github.com/rightup), pyMC Console provides real-time visibility into your mesh network with an intuitive, feature-rich interface. + +## Features + +### Dashboard +- **Live packet metrics** — Received, forwarded, dropped packets with sparkline charts +- **TX Delay Recommendations** — Slot-based delay optimization with network role classification (edge/relay/hub/backbone) +- **LBT Insights widgets** — Channel health, collision risk, noise floor, link quality at a glance +- **Time range selector** — View stats from 20 minutes to 7 days +- **Recent packets** — Live feed of incoming traffic + +![Dashboard](docs/images/dashboard.png) + +### Statistics +- **Airtime utilization** — RX/TX utilization spectrum with peak and mean metrics +- **Link quality polar** — Neighbor signal strength plotted by compass bearing +- **Network composition** — Breakdown of repeaters, companions, and room servers +- **Packet types treemap** — Distribution of ADVERT, TXT_MSG, ACK, RESPONSE, etc. +- **RF noise floor** — Noise floor heatmap showing interference patterns over time +- **Prefix disambiguation** — Health metrics for the topology inference system + +![Statistics](docs/images/statistics.png) + +### Contacts & Topology +- **Interactive map** — MapLibre GL with dark theme, neighbor positions, and smooth animations +- **Mesh topology graph** — Network connections inferred from packet paths +- **Deep Analysis** — One-click full topology rebuild from 20K+ packets +- **Intelligent disambiguation** — Four-factor scoring resolves prefix collisions +- **Edge confidence** — Line thickness scales with observation count +- **Animated edges** — Trace-in effect on toggle, smooth fade-out +- **Filter toggles** — Solo view for hub nodes or direct neighbors only +- **Loop detection** — Identifies redundant paths (double-line rendering) +- **Visual identity** — Yellow house icon for local node, indigo rings for neighbors +- **Path health panel** — Health scores, weakest links, and latency estimates for observed routes +- **Mobile node detection** — Identifies volatile nodes that appear/disappear frequently + +![Topology Map](docs/images/topology.png) + +### Packets +- **Searchable history** — Filter by type, route, time range +- **Packet details** — Hash, path, payload, signal info, duplicates +- **Path visualization** — Interactive map showing packet route with hop confidence + +### Settings +- **Mode toggle** — Forward (repeating) or Monitor (RX only) +- **Duty cycle** — Enable/disable enforcement +- **Radio config** — Live frequency, power, SF, bandwidth changes + +![Settings](docs/images/settings.png) + +### System & Logs +- **System resources** — 20-minute rolling CPU and memory utilization chart +- **Disk usage** — Storage utilization with progress bar +- **Temperature** — Multi-sensor temperature gauges with threshold coloring +- **Live logs** — Stream from repeater daemon with DEBUG/INFO toggle + +![System](docs/images/system.png) + +## Quick Start + +### Requirements + +- Raspberry Pi (3, 4, 5, or Zero 2 W) +- LoRa module (SX1262 or SX1276 based) +- Raspbian/Raspberry Pi OS (Bookworm recommended) + +### Installation + +```bash +# Clone this repository +git clone https://github.com/dmduran12/pymc_console.git +cd pymc_console + +# Run the installer (requires sudo) +sudo bash manage.sh install +``` + +The installer will: +1. Install all dependencies +2. Set up [pyMC_Repeater](https://github.com/rightup/pyMC_Repeater) +3. Deploy the web dashboard +4. Configure the systemd service + +Once complete, access your dashboard at `http://:8000` + +## Management Menu + +After installation, run `sudo bash manage.sh` to access the management menu: + +``` +┌─────────────────────────────────────┐ +│ pyMC Console Management │ +├─────────────────────────────────────┤ +│ 1. Start Service │ +│ 2. Stop Service │ +│ 3. Restart Service │ +│ 4. View Logs │ +│ 5. Configure Radio │ +│ 6. Configure GPIO │ +│ 7. Upgrade │ +│ 8. Uninstall │ +│ 9. Exit │ +└─────────────────────────────────────┘ +``` + +### Menu Options + +- **Start/Stop/Restart** — Control the repeater service +- **View Logs** — Live log output from the repeater +- **Configure Radio** — Set frequency, power, bandwidth, SF via preset selection +- **Configure GPIO** — Set up SPI bus and GPIO pins for your LoRa module +- **Upgrade** — Pull latest updates and reinstall +- **Uninstall** — Remove the installation completely + +## Upgrading + +To update to the latest version: + +```bash +cd pymc_console +git pull +sudo bash manage.sh upgrade +``` + +Or use the TUI menu: `sudo bash manage.sh` → **Upgrade** + +## Configuration + +### Radio Settings + +Use the **Configure Radio** menu option, or edit directly: + +```bash +sudo nano /etc/pymc_repeater/config.yaml +``` + +Key settings: +```yaml +radio: + frequency: 927875000 # Frequency in Hz + spreading_factor: 7 # SF7-SF12 + bandwidth: 62500 # Bandwidth in Hz + tx_power: 28 # TX power in dBm + coding_rate: 6 # 4/5, 4/6, 4/7, or 4/8 +``` + +### Service Management + +```bash +# Check status +sudo systemctl status pymc-repeater + +# Start/stop/restart +sudo systemctl start pymc-repeater +sudo systemctl stop pymc-repeater +sudo systemctl restart pymc-repeater + +# View live logs +sudo journalctl -u pymc-repeater -f +``` + +## Hardware Requirements + +- **Raspberry Pi** (3, 4, 5, or Zero 2 W recommended) +- **LoRa Module** — SX1262 or SX1276 based (e.g., Waveshare SX1262, LILYGO T3S3) +- **SPI Connection** — Module connected via SPI with GPIO for reset/busy/DIO1 + +### Tested Modules + +- Waveshare SX1262 HAT +- LILYGO T3S3 (via USB serial) +- Ebyte E22 modules +- Heltec LoRa 32 + +## Troubleshooting + +### Service won't start + +```bash +# Check for errors +sudo journalctl -u pymc-repeater -n 50 + +# Verify config syntax +cat /etc/pymc_repeater/config.yaml +``` + +### No packets being received + +1. Verify SPI is enabled: `ls /dev/spidev*` +2. Check GPIO configuration in manage.sh → Configure GPIO +3. Confirm frequency matches your network + +### Dashboard not loading + +1. Verify service is running: `sudo systemctl status pymc-repeater` +2. Check if port 8000 is accessible: `curl http://localhost:8000/api/stats` + +## Uninstalling + +```bash +cd pymc_console +sudo bash manage.sh +``` + +Select **Uninstall** from the menu. This removes: +- `/opt/pymc_repeater` (installation) +- `/etc/pymc_repeater` (configuration) +- `/var/log/pymc_repeater` (logs) +- The systemd service + +## How It Works + +### Mesh Topology Analysis + +The dashboard reconstructs network topology from packet paths. MeshCore packets contain 2-character hex prefixes representing the route through the mesh: + +``` +Packet path: ["FA", "79", "24", "19"] + Origin → Hop1 → Hop2 → Local +``` + +**The Challenge**: Multiple nodes may share the same 2-char prefix (1 in 256 collision chance). The system uses four-factor scoring inspired by [meshcore-bot](https://github.com/agessaman/meshcore-bot) to resolve ambiguity: + +1. **Position (15%)** — Where in paths does this prefix typically appear? +2. **Co-occurrence (15%)** — Which prefixes appear adjacent to this one? +3. **Geographic (35%)** — How close is the candidate to anchor points? +4. **Recency (35%)** — How recently was this node seen? + +**Key techniques:** + +- **Recency scoring** — Exponential decay `e^(-hours/12)` favors recently-active nodes +- **Age filtering** — Nodes not seen in 14 days are excluded from consideration +- **Dual-hop anchoring** — Candidates scored by distance to both previous and next hops (a relay must be within RF range of both neighbors) +- **Score-weighted redistribution** — Appearance counts redistributed proportionally by combined score +- **Source-geographic correlation** — Position-1 prefixes scored by distance from packet origin + +The system loads up to 20,000 packets (~7 days of traffic) to build comprehensive topology evidence. + +![Prefix Disambiguation](docs/images/disambiguation.png) + +### Edge Rendering + +Topology edges are rendered with visual cues indicating confidence: + +- **Line thickness** — Scales from 1.5px (5 validations) to 10px (100+ validations) +- **Validation threshold** — Edges require 5+ certain observations to render +- **Certainty conditions** — An edge is "certain" when: + - Both endpoints have ≥0.6 confidence (HIGH threshold), OR + - The destination has ≥0.9 confidence (VERY_HIGH threshold), OR + - It's the last hop to local node +- **Inclusion threshold** — Edges require ≥0.4 confidence (MEDIUM threshold) for topology +- **Trace animation** — Edges "draw" from point A to B when topology is enabled +- **Fade animation** — Edges smoothly fade out when topology is disabled +- **Loop edges** — Redundant paths rendered as parallel double-lines in accent color + +### Path Visualization + +Clicking a packet shows its route on a map with confidence indicators: + +- **Green** — 100% confidence (unique prefix, no collision) +- **Yellow** — 50-99% confidence (high certainty) +- **Orange** — 25-49% confidence (medium certainty) +- **Red** — 1-24% confidence (low certainty) +- **Gray** — Unknown prefix (not in neighbor list) + +## Development + +See [WARP.md](WARP.md) for architecture details and [RELEASE.md](RELEASE.md) for the release process. + +```bash +cd frontend +npm install +npm run dev # Starts dev server at http://localhost:5173 +npm run typecheck # Run TypeScript type checking +npm run build # Production build +``` + +To connect to a remote Pi during development, create `frontend/.env.local`: + +```env +VITE_API_URL=http://:8000 +``` + +## License + +MIT — See [LICENSE](LICENSE) + +## Credits + +Built on the excellent work of: + +- **[RightUp](https://github.com/rightup)** — Creator of pyMC_Repeater, pymc_core, and the MeshCore ecosystem +- **[pyMC_Repeater](https://github.com/rightup/pyMC_Repeater)** — Core repeater daemon for LoRa communication and mesh routing +- **[pymc_core](https://github.com/rightup/pyMC_core)** — Underlying mesh protocol library +- **[meshcore-bot](https://github.com/agessaman/meshcore-bot)** — Inspiration for recency scoring and dual-hop anchor disambiguation +- **[MeshCore](https://meshcore.co.uk/)** — The MeshCore project and community diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..c3d73aaa --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,313 @@ +# pyMC UI Release Guide + +Complete guide for developers to create and publish new releases of the pyMC UI. + +## Table of Contents +- [Quick Release Steps](#quick-release-steps) +- [Understanding the Release Process](#understanding-the-release-process) +- [Manual Build Process](#manual-build-process) +- [Troubleshooting](#troubleshooting) +- [Version Numbering](#version-numbering) + +--- + +## Quick Release Steps + +For experienced developers, here's the TL;DR: + +```bash +cd pymc_console/frontend + +# 1. Update version (creates git tag automatically if configured) +npm version patch + +# 2. Push changes and tag +git push origin main +git push origin --tags + +# Done! Check GitHub Actions for build status +``` + +--- + +## Understanding the Release Process + +### What Happens Automatically + +This project uses **GitHub Actions** for continuous integration and deployment: + +#### On Every Push to `main` or `dev`: +- ✅ Installs dependencies +- ✅ Builds static files (`npm run build:static`) +- ✅ Creates versioned `.tar.gz` and `.zip` archives +- ✅ Uploads build artifacts to GitHub (available for 30 days) +- ❌ Does NOT create a GitHub Release + +#### On Version Tag Push (e.g., `v0.1.1`): +- ✅ Everything above, PLUS +- ✅ Creates a GitHub Release +- ✅ Attaches downloadable archives to the release +- ✅ Generates automatic release notes from commits + +### The Workflow File + +Located at: `.github/workflows/build-ui.yml` + +This workflow has two jobs: +1. **build** - Runs on all pushes and PRs +2. **release** - Only runs when a tag starting with `v` is pushed + +--- + +## Detailed Release Steps + +### Step 1: Make Your Changes + +Work on your feature branch as normal: +```bash +git checkout -b feature/my-new-feature +# ... make changes ... +git add . +git commit -m "feat: add awesome new feature" +git push origin feature/my-new-feature +``` + +Create a PR, get it reviewed, and merge to `main`. + +### Step 2: Decide on Version Number + +Use [Semantic Versioning](https://semver.org/): +- **MAJOR** (`1.0.0` → `2.0.0`) - Breaking changes +- **MINOR** (`0.1.0` → `0.2.0`) - New features, backwards compatible +- **PATCH** (`0.1.1` → `0.1.2`) - Bug fixes only + +### Step 3: Update Version + +Navigate to the frontend directory: +```bash +cd pymc_console/frontend +``` + +Run the appropriate npm version command: +```bash +# For bug fixes +npm version patch + +# For new features +npm version minor + +# For breaking changes +npm version major + +# For pre-releases +npm version prerelease --preid=beta +``` + +**What this does:** +- Updates `version` in `package.json` and `package-lock.json` +- Creates a git commit with message like "0.1.2" +- Creates a git tag like `v0.1.2` (if git is configured properly) + +### Step 4: Push Changes and Tags + +Push your version commit: +```bash +git push origin main +``` + +Push the tag to trigger the release: +```bash +git push origin --tags +``` + +Or push a specific tag: +```bash +git push origin v0.1.2 +``` + +### Step 5: Monitor the Release + +1. Go to your repository on GitHub +2. Click the **Actions** tab +3. You should see a workflow running for your tag +4. Wait for both jobs (build and release) to complete + +### Step 6: Verify the Release + +Once the workflow completes: + +1. Go to the **Releases** section of your GitHub repo +2. You should see your new release (e.g., `v0.1.2`) +3. It should have two attached files: + - `pymc-ui-v0.1.2.tar.gz` + - `pymc-ui-v0.1.2.zip` +4. Release notes are auto-generated from commit messages + +--- + +## Manual Build Process + +If you need to build locally without creating a release: + +```bash +cd pymc_console/frontend + +# Install dependencies (first time only) +npm install + +# Build static files +npm run build:static +``` + +The output will be in `frontend/dist/` directory. + +### What the Build Does + +1. `vite build` - Compiles React app to static HTML/CSS/JS +2. Outputs to `frontend/out/` +3. Copies `out/` contents to `frontend/dist/` + +--- + +## Troubleshooting + +### "Release job was skipped" + +**Problem:** You pushed code but no GitHub Release was created. + +**Solution:** The release job only runs for version tags. Make sure you: +1. Created a tag with `npm version` or `git tag v0.1.x` +2. Pushed the tag with `git push origin --tags` +3. The tag name starts with `v` (e.g., `v0.1.2`, not `0.1.2`) + +### "Build failed: npm ERR! code ELIFECYCLE" + +**Problem:** The build process failed. + +**Solution:** +1. Pull the latest code: `git pull origin main` +2. Clean install locally: `cd frontend && rm -rf node_modules package-lock.json && npm install` +3. Test build locally: `npm run build:static` +4. Fix any errors before pushing + +### "Tag already exists" + +**Problem:** You tried to create a tag that already exists. + +**Solution:** +```bash +# Delete local tag +git tag -d v0.1.2 + +# Delete remote tag (if already pushed) +git push origin :refs/tags/v0.1.2 + +# Create new tag +npm version patch +git push origin --tags +``` + +### "Permission denied" when creating release + +**Problem:** GitHub Actions doesn't have permission to create releases. + +**Solution:** The workflow already has `permissions: contents: write` set. Check your repository settings: +1. Go to Settings → Actions → General +2. Under "Workflow permissions", ensure "Read and write permissions" is selected + +--- + +## Version Numbering + +### Semantic Versioning Format + +`MAJOR.MINOR.PATCH` (e.g., `0.1.2`) + +### When to Increment Each Part + +| Change Type | Example | Command | Version Change | +|-------------|---------|---------|----------------| +| Bug fix | Fix broken map display | `npm version patch` | `0.1.1` → `0.1.2` | +| New feature | Add dark mode | `npm version minor` | `0.1.2` → `0.2.0` | +| Breaking change | Require new API version | `npm version major` | `0.2.0` → `1.0.0` | +| Pre-release | Beta testing | `npm version prerelease --preid=beta` | `0.1.2` → `0.1.3-beta.0` | + +### Pre-release Tags + +For alpha, beta, or release candidate versions: + +```bash +# First beta +npm version 0.2.0-beta.1 + +# Subsequent betas +npm version prerelease --preid=beta # 0.2.0-beta.2 + +# Release candidate +npm version 0.2.0-rc.1 + +# Final release +npm version 0.2.0 +``` + +Pre-releases are automatically marked as "Pre-release" on GitHub. + +--- + +## End User Deployment + +Once a release is published, end users can deploy it: + +### Download and Extract + +```bash +# Download latest release +wget https://github.com/dmduran12/pymc_console/releases/download/v0.2.0/pymc-ui-v0.2.0.tar.gz + +# Extract to pyMC_Repeater's web directory +tar -xzf pymc-ui-v0.2.0.tar.gz -C /opt/pymc_repeater/repeater/web/html/ + +# Or use zip +unzip pymc-ui-v0.2.0.zip -d /opt/pymc_repeater/repeater/web/html/ +``` + +### Configure Backend + +The backend (pyMC_API) should be configured to serve these static files. Users control where they deploy the UI files. + +--- + +## Quick Reference Commands + +```bash +# View current version +cat frontend/package.json | grep version + +# List all tags +git tag --list + +# View latest tag +git describe --tags --abbrev=0 + +# Delete local tag +git tag -d v0.1.2 + +# Delete remote tag +git push origin :refs/tags/v0.1.2 + +# Create tag manually (if npm version didn't work) +git tag v0.1.2 +git push origin v0.1.2 + +# Check GitHub Actions status +# Visit: https://github.com/dmduran12/pymc_console/actions +``` + +--- + +## Need Help? + +- Check GitHub Actions logs for detailed error messages +- Review commit messages to ensure they follow conventions +- Test builds locally before pushing tags +- Ask in the project's discussion forum or issues section diff --git a/frontend/dist/VERSION b/frontend/dist/VERSION new file mode 100644 index 00000000..4ca36554 --- /dev/null +++ b/frontend/dist/VERSION @@ -0,0 +1 @@ +0.6.79 diff --git a/frontend/dist/assets/Contacts-MBZsEaUa.js b/frontend/dist/assets/Contacts-MBZsEaUa.js new file mode 100644 index 00000000..55382624 --- /dev/null +++ b/frontend/dist/assets/Contacts-MBZsEaUa.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/ContactsMapMapLibre-l8mOIn1t.js","assets/index-CylDUtFA.js","assets/recharts-CGff6_7g.js","assets/maplibre-gl-DsiI67my.js","assets/index-CLg20r2M.css","assets/maplibre-gl-zy3TF1FS.js","assets/maplibre-gl-B1CfjdFi.css","assets/HashBadge-CTAxN-Gl.js","assets/loader-circle-BeVz0YM3.js","assets/info-CqYfnuqu.js","assets/house-CqfQQFQV.js","assets/refresh-cw-O6setgEB.js","assets/trending-up-Crl55Pcw.js","assets/SignalIndicator-BjDxWf6l.js","assets/triangle-alert-BFXHkJUa.js","assets/zap-CBQbQNHy.js","assets/activity-BjSGn1T6.js","assets/PageLayout-Com69Ajh.js","assets/users-Dq_Md6JI.js"])))=>i.map(i=>d[i]); +import{c as e,j as t,_ as a,w as s,v as r,X as n,x as l,y as c,z as i,A as o,B as d,E as m,F as h,f as u,R as p,C as x,G as g}from"./index-CylDUtFA.js";import{b,g as v,c as y,d as N,h as f,T as w,f as k,e as M,L as C}from"./recharts-CGff6_7g.js";import{a as j}from"./SignalIndicator-BjDxWf6l.js";import{T as H}from"./triangle-alert-BFXHkJUa.js";import{T as S,Z as $,M as A}from"./zap-CBQbQNHy.js";import{A as E}from"./activity-BjSGn1T6.js";import{T as L,N as O}from"./trending-up-Crl55Pcw.js";import{M as R,H as _}from"./HashBadge-CTAxN-Gl.js";import{P as q,a as B,C as D,b as F}from"./PageLayout-Com69Ajh.js";import{U as P}from"./users-Dq_Md6JI.js";const T=e("arrow-left-right",[["path",{d:"M8 3 4 7l4 4",key:"9rb6wj"}],["path",{d:"M4 7h16",key:"6tx8e3"}],["path",{d:"m16 21 4-4-4-4",key:"siv7j2"}],["path",{d:"M20 17H4",key:"h6l3hr"}]]),z=e("arrow-up-down",[["path",{d:"m21 16-4 4-4-4",key:"f6ql7i"}],["path",{d:"M17 20V4",key:"1ejh1v"}],["path",{d:"m3 8 4-4 4 4",key:"11wl7u"}],["path",{d:"M7 4v16",key:"1glfcx"}]]),W=e("chevron-up",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]]),K=e("git-branch",[["line",{x1:"6",x2:"6",y1:"3",y2:"15",key:"17qcm7"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}],["path",{d:"M18 9a9 9 0 0 1-9 9",key:"n2h4wq"}]]),V=e("messages-square",[["path",{d:"M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z",key:"1n2ejm"}],["path",{d:"M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1",key:"1qfcsi"}]]),U=e("monitor-smartphone",[["path",{d:"M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8",key:"10dyio"}],["path",{d:"M10 19v-3.96 3.15",key:"1irgej"}],["path",{d:"M7 19h5",key:"qswx4l"}],["rect",{width:"6",height:"10",x:"16",y:"12",rx:"2",key:"1egngj"}]]),G=e("ruler",[["path",{d:"M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z",key:"icamh8"}],["path",{d:"m14.5 12.5 2-2",key:"inckbg"}],["path",{d:"m11.5 9.5 2-2",key:"fmmyf7"}],["path",{d:"m8.5 6.5 2-2",key:"vc6u1g"}],["path",{d:"m17.5 15.5 2-2",key:"wo5hmg"}]]),I=e("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Z=e("share-2",[["circle",{cx:"18",cy:"5",r:"3",key:"gq8acd"}],["circle",{cx:"6",cy:"12",r:"3",key:"w7nqdw"}],["circle",{cx:"18",cy:"19",r:"3",key:"1xt0gg"}],["line",{x1:"8.59",x2:"15.42",y1:"13.51",y2:"17.49",key:"47mynk"}],["line",{x1:"15.41",x2:"8.59",y1:"6.51",y2:"10.49",key:"1n3mei"}]]),J=e("trash-2",[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]]);class Q extends b.Component{constructor(e){super(e),this.state={hasError:!1}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}render(){var e;return this.state.hasError?t.jsx("div",{className:"glass-card h-[400px] flex items-center justify-center",children:t.jsxs("div",{className:"text-center text-white/50 p-4",children:[t.jsx("p",{className:"text-lg mb-2",children:"Map failed to load"}),t.jsx("p",{className:"text-sm text-white/30",children:(null==(e=this.state.error)?void 0:e.message)||"Unknown error"})]})}):this.props.children}}const X=b.lazy(()=>a(()=>import("./ContactsMapMapLibre-l8mOIn1t.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18])));function Y({neighbors:e,localNode:a,localHash:s,onRemoveNode:r,selectedNodeHash:n,onNodeSelected:l,highlightedEdgeKey:c}){return t.jsx(Q,{children:t.jsx(b.Suspense,{fallback:t.jsx("div",{className:"glass-card h-[500px] flex items-center justify-center",children:t.jsx("div",{className:"text-white/50",children:"Loading map..."})}),children:t.jsx(X,{neighbors:e,localNode:a,localHash:s,onRemoveNode:r,selectedNodeHash:n,onNodeSelected:l,highlightedEdgeKey:c})})})}function ee(e){return e>=.7?"text-accent-success":e>=.5?"text-accent-secondary":e>=.3?"text-signal-poor":"text-accent-danger"}function te(e){return`${Math.round(100*e)}%`}const ae=b.memo(function({path:e,isHighlighted:a,onHighlight:s}){const r=function(e){return e>.2?{icon:t.jsx(L,{className:"w-3 h-3"}),color:"text-accent-success"}:e<-.2?{icon:t.jsx(S,{className:"w-3 h-3"}),color:"text-accent-danger"}:{icon:t.jsx(A,{className:"w-3 h-3"}),color:"text-text-muted"}}(e.observationTrend),n=e.weakestLinkKey&&e.weakestLinkConfidence<.5;return t.jsxs("div",{className:"flex items-center gap-3 p-2 rounded-md transition-colors cursor-pointer "+(a?"bg-accent-primary/20 border border-accent-primary/40":"hover:bg-white/5"),onClick:()=>s(a?null:e.weakestLinkKey),children:[t.jsx("div",{className:"flex-shrink-0 w-12 text-center py-1 rounded-md "+(l=e.healthScore,l>=.7?"bg-accent-success/10":l>=.5?"bg-accent-secondary/10":l>=.3?"bg-signal-poor/10":"bg-accent-danger/10"),children:t.jsx("span",{className:`text-xs font-semibold tabular-nums ${ee(e.healthScore)}`,children:te(e.healthScore)})}),t.jsx("div",{className:"flex-1 flex items-center gap-0.5 overflow-x-auto min-w-0",children:e.hops.map((a,s)=>{var r;return t.jsxs("span",{className:"flex items-center",children:[t.jsx("span",{className:"text-[10px] font-mono px-1.5 py-0.5 rounded "+(n&&(null==(r=e.weakestLinkKey)?void 0:r.includes(a))?"bg-accent-danger/20 text-accent-danger":"bg-white/10 text-text-secondary"),children:a}),sl.slice(0,e),[l,e]),d=b.useMemo(()=>0===l.length?null:{avgHealth:l.reduce((e,t)=>e+t.healthScore,0)/l.length,declining:l.filter(e=>e.observationTrend<-.2).length,weakLinks:l.filter(e=>e.weakestLinkConfidence<.5).length},[l]),m=e=>{null==a||a(e)};return 0===l.length?null:t.jsxs("div",{className:"chart-container",children:[t.jsxs("button",{onClick:()=>i(!c),className:"w-full chart-header hover:bg-white/5 transition-colors rounded-t-lg cursor-pointer",children:[t.jsxs("div",{className:"chart-title",children:[t.jsx(K,{className:"chart-title-icon"}),"Path Health",t.jsxs("span",{className:"ml-2 text-[10px] font-normal text-text-muted",children:["(",l.length," paths)"]})]}),t.jsxs("div",{className:"flex items-center gap-3",children:[d&&t.jsxs("div",{className:"flex items-center gap-3 text-[10px]",children:[t.jsxs("span",{className:`tabular-nums ${ee(d.avgHealth)}`,children:["Avg: ",te(d.avgHealth)]}),d.weakLinks>0&&t.jsxs("span",{className:"text-accent-danger flex items-center gap-1",children:[t.jsx(H,{className:"w-3 h-3"}),d.weakLinks," weak"]}),d.declining>0&&t.jsxs("span",{className:"text-signal-poor flex items-center gap-1",children:[t.jsx(S,{className:"w-3 h-3"}),d.declining," declining"]})]}),c?t.jsx(W,{className:"w-4 h-4 text-text-muted"}):t.jsx(r,{className:"w-4 h-4 text-text-muted"})]})]}),c&&t.jsxs("div",{className:"p-3 pt-0 space-y-1",children:[t.jsxs("div",{className:"flex items-center gap-4 text-[10px] text-text-muted pb-2 border-b border-white/5",children:[t.jsxs("span",{className:"flex items-center gap-1",children:[t.jsx("span",{className:"w-2 h-2 rounded-full bg-accent-success"})," Healthy (≥70%)"]}),t.jsxs("span",{className:"flex items-center gap-1",children:[t.jsx("span",{className:"w-2 h-2 rounded-full bg-accent-secondary"})," Fair (50-70%)"]}),t.jsxs("span",{className:"flex items-center gap-1",children:[t.jsx("span",{className:"w-2 h-2 rounded-full bg-signal-poor"})," Weak (30-50%)"]}),t.jsxs("span",{className:"flex items-center gap-1",children:[t.jsx("span",{className:"w-2 h-2 rounded-full bg-accent-danger"})," Critical (<30%)"]})]}),t.jsx("div",{className:"space-y-1 max-h-64 overflow-y-auto",children:o.map(e=>t.jsx(ae,{path:e,isHighlighted:n===e.weakestLinkKey,onHighlight:m},e.pathKey))}),l.length>e&&t.jsxs("div",{className:"text-center text-[10px] text-text-muted pt-2",children:["Showing top ",e," of ",l.length," paths"]})]})]})}),re=b.memo(function({isOpen:e,title:a="Confirm",message:s,confirmLabel:r="Confirm",cancelLabel:l="Cancel",variant:c="default",onConfirm:i,onCancel:o}){if(b.useEffect(()=>{if(!e)return;const t=e=>{"Escape"===e.key&&o()};window.addEventListener("keydown",t);const a=document.body.style.overflow,s=document.body.style.position,r=document.body.style.width,n=document.body.style.top,l=window.scrollY;return document.body.style.overflow="hidden",document.body.style.position="fixed",document.body.style.width="100%",document.body.style.top=`-${l}px`,()=>{window.removeEventListener("keydown",t),document.body.style.overflow=a,document.body.style.position=s,document.body.style.width=r,document.body.style.top=n,window.scrollTo(0,l)}},[e,o]),!e)return null;const d={danger:{icon:"text-accent-danger",button:"bg-accent-danger hover:bg-red-600"},warning:{icon:"text-accent-secondary",button:"bg-accent-secondary hover:bg-yellow-600 text-bg-body"},default:{icon:"text-accent-primary",button:"bg-accent-primary hover:bg-violet-500 text-bg-body"}}[c];return v.createPortal(t.jsx("div",{className:"fixed inset-0 bg-black/40 backdrop-blur-md z-[10010] flex items-end sm:items-center justify-center",onClick:o,role:"dialog","aria-modal":"true","aria-labelledby":"confirm-modal-title",children:t.jsxs("div",{className:y("glass-card w-full max-w-sm overflow-hidden","sm:mx-4 sm:rounded-xl","rounded-t-2xl rounded-b-none sm:rounded-b-xl","pb-safe"),onClick:e=>e.stopPropagation(),children:[t.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-border-subtle",children:[t.jsxs("div",{className:"flex items-center gap-3",children:[t.jsx("div",{className:y("p-2 rounded-lg bg-bg-subtle",d.icon),children:t.jsx(H,{className:"w-5 h-5"})}),t.jsx("h3",{id:"confirm-modal-title",className:"text-base font-semibold text-text-primary",children:a})]}),t.jsx("button",{onClick:o,className:"p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-bg-subtle transition-colors",children:t.jsx(n,{className:"w-5 h-5"})})]}),t.jsx("div",{className:"p-4",children:t.jsx("p",{className:"text-sm text-text-secondary",children:s})}),t.jsxs("div",{className:"flex gap-3 p-4 pt-0",children:[t.jsx("button",{onClick:o,className:"flex-1 px-4 py-2.5 rounded-lg text-sm font-medium text-text-secondary bg-bg-subtle hover:bg-bg-elevated border border-border-subtle transition-colors",children:l}),t.jsx("button",{onClick:i,className:y("flex-1 px-4 py-2.5 rounded-lg text-sm font-medium text-text-primary transition-colors",d.button),children:r})]})]})}),document.body)}),ne="var(--signal-critical)",le="var(--signal-poor)",ce="var(--signal-fair)",ie="var(--signal-good)",oe="var(--signal-excellent)",de="var(--text-muted)";function me({active:e,payload:a}){if(!e||!a||!a.length)return null;const s=a[0].payload,r=new Date(s.timestamp),n=`${(r.getMonth()+1).toString().padStart(2,"0")}/${r.getDate().toString().padStart(2,"0")}`;return t.jsxs("div",{className:"bg-bg-surface/95 border border-border-subtle rounded px-1.5 py-0.5 text-[10px] shadow-lg",children:[t.jsx("span",{className:"text-text-muted",children:n}),t.jsx("span",{className:"ml-1.5 font-semibold tabular-nums",children:s.count})]})}function he({nodeHash:e,width:a=60,height:s=20,color:r,showArea:n=!0,showTooltip:i=!1,className:o=""}){const d=l(e),m=c(),h=r??(d.length>0?function(e){if(0===e.length)return ne;const t=e.slice(-4),a=t.reduce((e,t)=>e+t.count,0),s=a/t.length,r=e.reduce((e,t)=>e+t.count,0)/e.length;if(0===a)return ne;if(r>0){const e=s/r;return e>=1.2?oe:e>=.8?ie:e>=.4?ce:e>=.1?le:ne}return a>=10?oe:a>=5?ie:a>=2?ce:a>=1?le:ne}(d):de),u={width:a,height:s};if(d.length<2){const e="number"==typeof a?a:60,r=m?de:ne;return t.jsx("div",{className:`flex items-center justify-center ${o}`,style:{...u,color:r},children:t.jsx("svg",{width:"100%",height:s,viewBox:`0 0 ${e} ${s}`,preserveAspectRatio:"none",children:t.jsx("line",{x1:4,y1:s/2,x2:e-4,y2:s/2,stroke:"currentColor",strokeWidth:1.5,strokeDasharray:"3,2",className:m?"animate-pulse":""})})})}const p=`sparkline-gradient-${e.slice(-6)}`;return t.jsx("div",{className:o,style:u,children:t.jsx(N,{width:"100%",height:"100%",children:n?t.jsxs(f,{data:d,margin:{top:1,right:1,bottom:1,left:1},children:[t.jsx("defs",{children:t.jsxs("linearGradient",{id:p,x1:"0",y1:"0",x2:"0",y2:"1",children:[t.jsx("stop",{offset:"0%",stopColor:h,stopOpacity:.35}),t.jsx("stop",{offset:"100%",stopColor:h,stopOpacity:.05})]})}),i&&t.jsx(w,{content:t.jsx(me,{}),cursor:{stroke:"rgba(255,255,255,0.2)",strokeWidth:1}}),t.jsx(k,{type:"monotone",dataKey:"count",stroke:"none",fill:`url(#${p})`,isAnimationActive:!1}),t.jsx(M,{type:"monotone",dataKey:"count",stroke:h,strokeWidth:1.5,dot:!1,isAnimationActive:!1})]}):t.jsxs(C,{data:d,margin:{top:1,right:1,bottom:1,left:1},children:[i&&t.jsx(w,{content:t.jsx(me,{}),cursor:{stroke:"rgba(255,255,255,0.2)",strokeWidth:1}}),t.jsx(M,{type:"monotone",dataKey:"count",stroke:h,strokeWidth:1.5,dot:!1,isAnimationActive:!1})]})})})}function ue(e,t,a,s){const r=(a-e)*Math.PI/180,n=(s-t)*Math.PI/180,l=Math.sin(r/2)*Math.sin(r/2)+Math.cos(e*Math.PI/180)*Math.cos(a*Math.PI/180)*Math.sin(n/2)*Math.sin(n/2);return 2*Math.atan2(Math.sqrt(l),Math.sqrt(1-l))*6371e3}const pe=Object.freeze(Object.defineProperty({__proto__:null,default:function(){const{stats:e}=i(),a=o(),s=d(),r=m(),l=h(),c=u(),[v,y]=b.useState(null),[N,f]=b.useState("lastHeard"),[w,k]=b.useState("desc"),[M,C]=b.useState(""),[H,S]=b.useState(!1),[$,A]=b.useState(null),[L,W]=b.useState(null),K=b.useMemo(()=>(null==e?void 0:e.neighbors)??{},[null==e?void 0:e.neighbors]),Q=b.useMemo(()=>Object.fromEntries(Object.entries(K).filter(([e])=>!a.has(e))),[K,a]),X=b.useMemo(()=>{var t;return(null==(t=null==e?void 0:e.config)?void 0:t.repeater)?{latitude:e.config.repeater.latitude,longitude:e.config.repeater.longitude,name:e.config.node_name||"Local Node"}:void 0},[e]),ee=null==e?void 0:e.local_hash,te=b.useMemo(()=>{const e=new Map;if(!(null==X?void 0:X.latitude)||!(null==X?void 0:X.longitude))return e;for(const[t,a]of Object.entries(Q))a.latitude&&a.longitude&&0!==a.latitude&&0!==a.longitude?e.set(t,ue(X.latitude,X.longitude,a.latitude,a.longitude)):e.set(t,null);return e},[Q,X]),{neighborHashSet:ae,neighborSignalMap:ne}=b.useMemo(()=>{const e=new Set,t=new Map;for(const a of c)e.add(a.hash),t.set(a.hash,{avgRssi:a.avgRssi,avgSnr:a.avgSnr});return{neighborHashSet:e,neighborSignalMap:t}},[c]),le=b.useMemo(()=>{const e=M.toLowerCase().trim(),t="neighbor"===e||"neighbors"===e,a=H||t;return Object.fromEntries(Object.entries(Q).filter(([s,r])=>{if(a&&!ae.has(s))return!1;if(t)return!0;if(!e)return!0;const n=(r.node_name||r.name||"").toLowerCase(),l=s.slice(2,4).toLowerCase();return n.includes(e)||l.includes(e)||s.toLowerCase().includes(e)}))},[Q,M,H,ae]),ce=b.useMemo(()=>Object.entries(le).sort(([e,t],[a,s])=>{let r=0;switch(N){case"lastHeard":r=(t.last_seen||0)-(s.last_seen||0);break;case"distance":{const t=te.get(e)??null,s=te.get(a)??null;r=null===t&&null===s?0:null===t?1:null===s?-1:t-s;break}case"centrality":r=(l.get(e)||0)-(l.get(a)||0)}return"desc"===w?-r:r}),[le,N,w,te,l]),ie=ce.filter(([,e])=>e.latitude&&e.longitude&&0!==e.latitude&&0!==e.longitude).length,oe=b.useMemo(()=>new Set(r),[r]),de=b.useCallback(e=>{N===e?k(e=>"desc"===e?"asc":"desc"):(f(e),k("desc"))},[N]),me=b.useCallback(e=>{const t=Q[e];(null==t?void 0:t.latitude)&&(null==t?void 0:t.longitude)&&0!==t.latitude&&0!==t.longitude&&A(e)},[Q]),pe=b.useCallback(()=>{A(null)},[]);return t.jsxs(q,{children:[t.jsx(B,{title:"Contacts",icon:t.jsx(P,{}),controls:t.jsxs("div",{className:"flex items-baseline gap-3 sm:gap-4",children:[t.jsxs("span",{className:"roster-title tabular-nums",children:[ce.length," node",1!==ce.length?"s":""]}),ie>0&&t.jsxs("span",{className:"roster-title flex items-baseline gap-1.5 tabular-nums",children:[t.jsx(R,{className:"w-3.5 h-3.5 relative top-[2px]"}),ie," with location"]})]})}),t.jsx("div",{className:"relative",children:t.jsx(Y,{neighbors:Q,localNode:X,localHash:ee,onRemoveNode:s,selectedNodeHash:$,onNodeSelected:pe,highlightedEdgeKey:L})}),t.jsx(se,{maxPaths:10,highlightedEdge:L,onHighlightEdge:W}),t.jsxs(D,{noPadding:!0,children:[t.jsx(F,{listHeader:!0,icon:t.jsx(P,{className:"icon-sm"}),title:"Discovered Contacts",actions:t.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[ae.size>0&&t.jsxs("button",{onClick:()=>S(!H),className:"flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-colors min-h-[32px] order-1 sm:order-1 "+(H?"bg-accent-success/20 text-accent-success border border-accent-success/30":"text-text-muted hover:text-text-secondary hover:bg-white/5 border border-transparent"),title:H?"Show all contacts":"Show only MeshCore neighbors (direct RF contact)",children:[t.jsx(p,{className:"w-3.5 h-3.5"}),t.jsx("span",{className:"hidden sm:inline",children:"Neighbors"}),t.jsx("span",{className:"sm:hidden",children:ae.size}),H&&t.jsx("span",{className:"text-[10px] font-semibold tabular-nums",children:ae.size})]}),t.jsxs("div",{className:"relative order-2 sm:order-2",children:[t.jsx(I,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-muted"}),t.jsx("input",{type:"text",value:M,onChange:e=>C(e.target.value),placeholder:"Search...",className:"w-28 sm:w-32 pl-7 pr-7 py-1.5 text-xs bg-white/5 border border-white/10 rounded-lg text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent-primary/50"}),M&&t.jsx("button",{onClick:()=>{C(""),"neighbor"!==M.toLowerCase().trim()&&"neighbors"!==M.toLowerCase().trim()||S(!1)},className:"absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-secondary p-0.5",children:t.jsx(n,{className:"w-3.5 h-3.5"})})]}),t.jsxs("div",{className:"flex items-center gap-1 order-1 sm:order-2",children:[t.jsxs("button",{onClick:()=>de("lastHeard"),className:"flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg transition-colors min-h-[32px] "+("lastHeard"===N?"bg-accent-primary/20 text-accent-primary":"text-text-muted hover:text-text-secondary hover:bg-white/5"),title:"Sort by last heard",children:[t.jsx(x,{className:"w-3.5 h-3.5"}),t.jsx("span",{className:"hidden sm:inline",children:"Recent"}),"lastHeard"===N&&t.jsx(z,{className:"w-3 h-3 "+("asc"===w?"rotate-180":"")})]}),t.jsxs("button",{onClick:()=>de("distance"),className:"flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg transition-colors min-h-[32px] "+("distance"===N?"bg-accent-primary/20 text-accent-primary":"text-text-muted hover:text-text-secondary hover:bg-white/5"),title:"Sort by distance",children:[t.jsx(G,{className:"w-3.5 h-3.5"}),t.jsx("span",{className:"hidden sm:inline",children:"Distance"}),"distance"===N&&t.jsx(z,{className:"w-3 h-3 "+("asc"===w?"rotate-180":"")})]}),t.jsxs("button",{onClick:()=>de("centrality"),className:"flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg transition-colors min-h-[32px] "+("centrality"===N?"bg-accent-primary/20 text-accent-primary":"text-text-muted hover:text-text-secondary hover:bg-white/5"),title:"Sort by network centrality",children:[t.jsx(E,{className:"w-3.5 h-3.5"}),t.jsx("span",{className:"hidden sm:inline",children:"Centrality"}),"centrality"===N&&t.jsx(z,{className:"w-3 h-3 "+("asc"===w?"rotate-180":"")})]})]})]})}),ce.length>0?t.jsx("div",{className:"roster-list",children:ce.map(([e,a],s)=>{var r,n;const c=a.latitude&&a.longitude&&0!==a.latitude&&0!==a.longitude,i=a.node_name||a.name||"Unknown",o=oe.has(e),d=ae.has(e),m=te.get(e),h=l.get(e)||0,u=d?ne.get(e):void 0,p=d&&u;return t.jsxs("div",{children:[t.jsxs("div",{className:`roster-row ${o?"bg-amber-500/5 border-l-2 border-l-amber-400":""} ${c?"cursor-pointer hover:bg-white/[0.02]":""}`,onClick:()=>me(e),children:[t.jsxs("div",{className:"relative flex-shrink-0",children:[t.jsx("div",{className:"roster-icon",children:(()=>{var e;const s=null==(e=a.contact_type)?void 0:e.toLowerCase(),r="room server"===s||"room_server"===s||"room"===s||"server"===s,n="companion"===s||"client"===s||"cli"===s,l=a.is_repeater||"repeater"===s||"rep"===s,c="#4338CA",i="#F59E0B";return r&&l?t.jsxs("div",{className:"relative w-5 h-5",children:[t.jsx(V,{className:"w-5 h-5 absolute inset-0",style:{color:i}}),t.jsx(Z,{className:"w-2.5 h-2.5 absolute -bottom-0.5 -right-0.5",style:{color:c}})]}):r?t.jsx(V,{className:"w-5 h-5",style:{color:i}}):n?t.jsx(U,{className:"w-5 h-5 text-text-muted"}):l?d?t.jsx(T,{className:"w-5 h-5 text-accent-success"}):t.jsx(Z,{className:"w-5 h-5",style:{color:c}}):t.jsx(U,{className:"w-5 h-5 text-text-muted"})})()}),p&&null!==(null==u?void 0:u.avgSnr)&&t.jsx("div",{className:`absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full ${b=u.avgSnr,void 0===b?"bg-[var(--signal-unknown)]":b>=5?"bg-[var(--signal-excellent)]":b>=0?"bg-[var(--signal-good)]":b>=-5?"bg-[var(--signal-fair)]":b>=-10?"bg-[var(--signal-poor)]":"bg-[var(--signal-critical)]"} border-2 border-bg-surface`})]}),t.jsxs("div",{className:"roster-content min-w-0 flex-1",children:[t.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[t.jsx("span",{className:"roster-title",children:i}),d&&t.jsx("span",{className:"type-badge px-1.5 py-0.5 rounded",style:{backgroundColor:"rgba(57, 217, 138, 0.2)",color:"#39D98A"},children:"NBR"}),o&&t.jsxs("span",{className:"type-badge px-1.5 py-0.5 rounded flex items-center gap-1",style:{backgroundColor:"rgba(251, 191, 36, 0.2)",color:"#FBBF24"},children:[t.jsx(O,{className:"w-3 h-3"}),"HUB"]}),(a.is_repeater||"repeater"===(null==(r=a.contact_type)?void 0:r.toLowerCase())||"rep"===(null==(n=a.contact_type)?void 0:n.toLowerCase()))&&t.jsx("span",{className:"pill-tag",children:"RPT"})]}),t.jsx("div",{className:"hidden md:block",children:t.jsx(_,{hash:e,size:"sm",full:!0})}),t.jsx("div",{className:"md:hidden",children:t.jsx(_,{hash:e,size:"sm",prefixLength:8,suffixLength:6})})]}),t.jsxs("div",{className:"roster-metrics flex-shrink-0",children:[p&&null!==(null==u?void 0:u.avgRssi)&&t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx(j,{rssi:u.avgRssi,className:"w-3.5 h-3.5"}),t.jsx("span",{className:"type-data-xs tabular-nums",children:Math.round(u.avgRssi)})]}),p&&null!==(null==u?void 0:u.avgSnr)&&t.jsx("div",{className:"flex items-center gap-1.5",children:t.jsxs("span",{className:"type-data-xs tabular-nums",children:[u.avgSnr.toFixed(1)," dB"]})}),null!=m&&t.jsxs("div",{className:"flex items-center gap-1 text-accent-tertiary",children:[t.jsx(G,{className:"w-3 h-3"}),t.jsx("span",{className:"type-data-xs tabular-nums",children:(x=m,x<1e3?`${Math.round(x)}m`:`${(x/1e3).toFixed(1)}km`)})]}),h>0&&t.jsxs("div",{className:"flex items-center gap-1 text-amber-400/70",children:[t.jsx(E,{className:"w-3 h-3"}),t.jsxs("span",{className:"type-data-xs tabular-nums",children:[(100*h).toFixed(0),"%"]})]}),t.jsx("div",{className:"hidden sm:block",children:t.jsx(he,{nodeHash:e,width:48,height:16})})]}),t.jsx("div",{className:"roster-metric flex-shrink-0",children:a.last_seen?g(a.last_seen):"—"}),t.jsx("button",{onClick:t=>{t.stopPropagation(),y({hash:e,name:i})},className:"ml-2 p-1.5 rounded-lg text-text-muted/50 hover:text-red-400 hover:bg-red-500/10 transition-colors flex-shrink-0",title:"Remove contact",children:t.jsx(J,{className:"w-4 h-4"})})]}),s{v&&s(v.hash),y(null)},onCancel:()=>y(null)})]})}},Symbol.toStringTag,{value:"Module"}));export{re as C,K as G,V as M,he as N,J as T,pe as a}; diff --git a/frontend/dist/assets/ContactsMapMapLibre-l8mOIn1t.js b/frontend/dist/assets/ContactsMapMapLibre-l8mOIn1t.js new file mode 100644 index 00000000..830e589c --- /dev/null +++ b/frontend/dist/assets/ContactsMapMapLibre-l8mOIn1t.js @@ -0,0 +1 @@ +import{c as e,j as t,G as n,af as r,ag as o,f as a,ah as i}from"./index-CylDUtFA.js";import{b as l,g as s,c,r as u}from"./recharts-CGff6_7g.js";import{a as d,P as f,S as h,L as p,u as m,M as g,N as b,b as v}from"./maplibre-gl-zy3TF1FS.js";import{G as y,N as x,T as w,M as k,C as N}from"./Contacts-MBZsEaUa.js";import{a as S,C}from"./HashBadge-CTAxN-Gl.js";import{L as E}from"./loader-circle-BeVz0YM3.js";import{I as M}from"./info-CqYfnuqu.js";import{H as F}from"./house-CqfQQFQV.js";import{R}from"./refresh-cw-O6setgEB.js";import{N as $}from"./trending-up-Crl55Pcw.js";import"./maplibre-gl-DsiI67my.js";import"./SignalIndicator-BjDxWf6l.js";import"./triangle-alert-BFXHkJUa.js";import"./zap-CBQbQNHy.js";import"./activity-BjSGn1T6.js";import"./PageLayout-Com69Ajh.js";import"./users-Dq_Md6JI.js";const H=e("chart-no-axes-column",[["path",{d:"M5 21v-6",key:"1hz6c0"}],["path",{d:"M12 21V3",key:"1lcnhd"}],["path",{d:"M19 21V9",key:"unv183"}]]),T=e("chevrons-left-right-ellipsis",[["path",{d:"M12 12h.01",key:"1mp3jc"}],["path",{d:"M16 12h.01",key:"1l6xoz"}],["path",{d:"m17 7 5 5-5 5",key:"1xlxn0"}],["path",{d:"m7 7-5 5 5 5",key:"19njba"}],["path",{d:"M8 12h.01",key:"czm47f"}]]),j=e("database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]),D=e("download",[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]]),I=e("eye-off",[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",key:"ct8e1f"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242",key:"151rxh"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",key:"13bj9a"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]),P=e("maximize-2",[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"m21 3-7 7",key:"1l2asr"}],["path",{d:"m3 21 7-7",key:"tjx5ai"}],["path",{d:"M9 21H3v-6",key:"wtvkvv"}]]),z=e("minimize-2",[["path",{d:"m14 10 7-7",key:"oa77jy"}],["path",{d:"M20 10h-6V4",key:"mjg0md"}],["path",{d:"m3 21 7-7",key:"tjx5ai"}],["path",{d:"M4 14h6v6",key:"rmj7iw"}]]);function A(e,t){const n=Math.max(5,Math.min(e,300)),r=Math.log(5),o=Math.log(300);return 1+(Math.log(n)-r)/(o-r)*5}const L="#4338CA";function O({label:e,icon:n,status:r,detail:o}){return t.jsxs("div",{className:c("flex items-center gap-3 py-3 px-4 rounded-xl transition-all duration-300","active"===r&&"bg-[#4338CA]/10","complete"===r&&"bg-accent-success/10","pending"===r&&"opacity-40"),children:[t.jsx("div",{className:c("w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 transition-all duration-300","active"===r&&"bg-[#4338CA]/20","complete"===r&&"bg-accent-success/20","pending"===r&&"bg-white/5"),children:"complete"===r?t.jsx(S,{className:"w-4 h-4 text-accent-success"}):"active"===r?t.jsx(E,{className:"w-4 h-4 animate-spin",style:{color:L}}):t.jsx("span",{className:"text-text-muted",children:n})}),t.jsxs("div",{className:"flex-1 min-w-0",children:[t.jsx("div",{className:c("text-sm font-medium transition-colors","active"===r&&"text-[#4338CA]","complete"===r&&"text-accent-success","pending"===r&&"text-text-muted"),children:e}),o&&"pending"!==r&&t.jsx("div",{className:"text-xs text-text-muted mt-0.5 truncate",children:o})]})]})}const B=l.memo(function({isOpen:e,currentStep:n,packetCount:r,onClose:o}){if(l.useEffect(()=>{if(!e)return;const t=e=>{"Escape"===e.key&&o&&o()};window.addEventListener("keydown",t);const n=document.body.style.overflow,r=document.body.style.position,a=document.body.style.width,i=document.body.style.top,l=window.scrollY;return document.body.style.overflow="hidden",document.body.style.position="fixed",document.body.style.width="100%",document.body.style.top=`-${l}px`,()=>{window.removeEventListener("keydown",t),document.body.style.overflow=n,document.body.style.position=r,document.body.style.width=a,document.body.style.top=i,window.scrollTo(0,l)}},[e,o]),!e)return null;const a="complete"===n,i=e=>{const t=["fetching","analyzing","building","complete"],r=t.indexOf(n),o=t.indexOf(e);return o0?`${r.toLocaleString()} packets`:"Loading database..."}),t.jsx(O,{label:"Analyzing Database",icon:t.jsx(j,{className:"w-4 h-4"}),status:i("analyzing"),detail:"Processing packet paths"}),t.jsx(O,{label:"Building Topology",icon:t.jsx(y,{className:"w-4 h-4"}),status:i("building"),detail:"Computing mesh edges"})]}),t.jsx("p",{className:"text-xs text-text-muted text-center mt-5",children:"This may take a few seconds..."})]})})]}),document.body)}),W="#4338CA",_="#FBBF24",q="#6366F1",V="#F97316",Z="#F59E0B",K="#FBBF24",U="#4B5563",G="#6B7280",X="#374151",Y="#5EEAD4",J="#6366F1",Q="#9CA3AF",ee="#6B7280",te="#FBBF24",ne=2e3;function re({text:e}){return t.jsxs("span",{className:"group relative cursor-help",children:[t.jsx(M,{className:"w-3 h-3 text-text-muted"}),t.jsx("div",{className:"absolute bottom-full left-0 mb-1 hidden group-hover:block w-44 p-2 text-[10px] leading-tight rounded-lg z-10",style:{background:"rgba(20, 20, 22, 0.98)",border:"1px solid rgba(140, 160, 200, 0.3)"},children:e})]})}function oe(e){const t=new Date(1e3*e);return`${(t.getMonth()+1).toString().padStart(2,"0")}/${t.getDate().toString().padStart(2,"0")}`}function ae({hash:e,hashPrefix:r,name:o,isHub:a,isZeroHop:i,isMobile:s,isRoomServer:c,isStale:u,lastSeenTimestamp:d,centrality:f,affinity:h,meanSnr:p,meanRssi:m,neighbor:g,onRemove:b,txDelayRec:v}){var y,k;const[N,E]=l.useState(!1),M=i?"Direct":(null==h?void 0:h.typicalHopPosition)?`${h.typicalHopPosition}-hop`:null,F=[{label:"Packets",value:(null==h?void 0:h.frequency)||0},{label:"Adverts",value:g.advert_count||0}];i&&void 0!==p?F.push({label:"SNR",value:`${p.toFixed(1)} dB`}):a&&f>0&&F.push({label:"Centrality",value:`${(100*f).toFixed(0)}%`,highlight:!0}),i&&void 0!==m&&F.push({label:"RSSI",value:`${Math.round(m)} dBm`});const R=v&&!v.insufficientData;return t.jsxs("div",{className:"w-[220px] pr-3",children:[t.jsx("div",{className:"text-[14px] font-semibold text-text-primary leading-snug truncate mb-0.5",children:o}),t.jsxs("div",{className:"flex items-center gap-1 flex-wrap mb-1.5",children:[t.jsx("code",{className:"font-mono text-[10px] text-text-muted/70 bg-white/5 px-1 py-px rounded",children:r}),t.jsx("button",{onClick:()=>{navigator.clipboard.writeText(e),E(!0),setTimeout(()=>E(!1),1500)},className:"p-0.5 hover:bg-white/10 rounded transition-colors",title:"Copy full hash",children:N?t.jsx(S,{className:"w-2.5 h-2.5 text-accent-success"}):t.jsx(C,{className:"w-2.5 h-2.5 text-text-muted/50"})}),a&&t.jsx("span",{className:"px-1 py-px text-[8px] font-bold uppercase rounded",style:{backgroundColor:"#FBBF24",color:"#000"},children:"Hub"}),M&&t.jsx("span",{className:"px-1 py-px text-[8px] font-bold uppercase rounded",style:{backgroundColor:i?K:"rgba(255,255,255,0.08)",color:i?"#000":"rgba(255,255,255,0.5)"},children:M}),s&&t.jsx("span",{className:"px-1 py-px text-[8px] font-bold uppercase rounded bg-orange-500/25 text-orange-300",children:"Mobile"}),g.is_repeater&&t.jsx("span",{className:"px-1 py-px text-[8px] font-bold uppercase rounded bg-cyan-500/20 text-cyan-400",children:"Rptr"}),c&&t.jsx("span",{className:"px-1 py-px text-[8px] font-bold uppercase rounded bg-amber-500/25 text-amber-400",children:"Room"}),u&&d&&t.jsxs("span",{className:"px-1 py-px text-[8px] font-medium rounded bg-gray-500/30 text-gray-300",title:"Neighbor not heard in 7+ days",children:["Idle ",oe(d)]})]}),t.jsxs("div",{className:"text-[10px] text-text-muted/60 mb-2 leading-tight",children:[t.jsx("span",{children:n(g.last_seen)}),(null==h?void 0:h.distanceMeters)&&t.jsxs("span",{className:"font-medium text-text-muted/80",children:[" · ",($=h.distanceMeters,null===$?"—":$<1e3?`${Math.round($)}m`:`${($/1e3).toFixed(1)}km`)]}),g.latitude&&g.longitude&&0!==g.latitude&&0!==g.longitude&&t.jsxs("span",{className:"font-mono text-[9px]",children:[" · ",g.latitude.toFixed(4),", ",g.longitude.toFixed(4)]})]}),t.jsx("div",{className:"mb-2",children:t.jsx(x,{nodeHash:e,width:"100%",height:28,showArea:!0,showTooltip:!0})}),t.jsx("div",{className:"grid grid-cols-2 gap-x-4 gap-y-0.5 text-[11px] mb-2",children:F.map((e,n)=>t.jsxs("div",{className:"flex justify-between",children:[t.jsx("span",{className:"text-text-muted/50",children:e.label}),t.jsx("span",{className:"font-semibold tabular-nums "+("highlight"in e&&e.highlight?"text-amber-400":""),children:e.value})]},n))}),(R||b)&&t.jsxs("div",{className:"flex items-center justify-between gap-2 pt-1.5 border-t border-white/5",children:[R?t.jsxs("div",{className:"flex flex-col gap-0.5 text-[10px] text-text-muted/60",children:[t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("span",{className:"uppercase text-[8px] font-semibold tracking-wide text-text-muted/40",children:"TX"}),void 0!==v.floodSlots?t.jsxs(t.Fragment,{children:[t.jsxs("span",{children:["F ",t.jsxs("span",{className:"font-semibold tabular-nums "+("low"===v.dataConfidence?"text-amber-400/50":"text-amber-400"),children:[null==(y=v.floodDelaySec)?void 0:y.toFixed(1),"s"]}),t.jsxs("span",{className:"text-text-muted/40",children:[" (",v.floodSlots,")"]})]}),t.jsxs("span",{children:["D ",t.jsxs("span",{className:"font-semibold tabular-nums "+("low"===v.dataConfidence?"text-amber-400/50":"text-amber-400"),children:[null==(k=v.directDelaySec)?void 0:k.toFixed(1),"s"]}),t.jsxs("span",{className:"text-text-muted/40",children:[" (",v.directSlots,")"]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("span",{children:["F ",t.jsx("span",{className:"font-semibold text-amber-400 tabular-nums",children:v.txDelayFactor.toFixed(2)})]}),t.jsxs("span",{children:["D ",t.jsx("span",{className:"font-semibold text-amber-400 tabular-nums",children:v.directTxDelayFactor.toFixed(2)})]})]}),"low"===v.dataConfidence&&t.jsx("span",{className:"text-[9px] text-amber-500",title:"Low confidence - limited data or asymmetric traffic",children:"⚠️"}),"high"===v.dataConfidence&&t.jsx("span",{className:"text-[9px] text-green-400",title:"High confidence - good bidirectional visibility",children:"✓"})]}),t.jsxs("div",{className:"flex items-center gap-1",children:[v.networkRole&&t.jsx("span",{className:"text-[9px] text-text-muted/40 capitalize",children:v.networkRole}),void 0!==v.observationSymmetry&&t.jsxs("span",{className:"text-[9px] "+(v.observationSymmetry>=.5?"text-green-400/60":v.observationSymmetry<.3?"text-amber-500/60":"text-text-muted/40"),title:`Edge symmetry: ${Math.round(100*v.observationSymmetry)}% bidirectional`,children:[v.observationSymmetry>=.5?"↔":v.observationSymmetry<.3?"→":"⇄",Math.round(100*v.observationSymmetry),"%"]})]})]}):t.jsx("div",{}),b&&t.jsx("button",{onClick:b,className:"flex items-center gap-0.5 p-1 text-[10px] text-text-muted/30 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors",title:"Remove from contacts",children:t.jsx(w,{className:"w-3 h-3"})})]})]});var $}function ie({showTopology:e,validatedPolylineCount:n,filteredNeighborCount:r,hasLocalNode:o,meshTopology:a,zeroHopNeighbors:i,neighborsWithLocation:l}){return t.jsxs("div",{className:"absolute bottom-4 left-4 z-[600] text-xs",style:{background:"rgba(20, 20, 22, 0.95)",borderRadius:"0.75rem",padding:"0.625rem",border:"1px solid rgba(140, 160, 200, 0.2)",maxWidth:"150px"},children:[t.jsxs("div",{className:"text-text-secondary font-medium mb-1.5 flex items-center gap-1",children:["Nodes",t.jsx(re,{text:"Node type shown by shape/color. Yellow outer ring = direct RF neighbor."})]}),t.jsxs("div",{className:"flex flex-col gap-1",children:[t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"w-3 h-3 rounded-full flex-shrink-0",style:{background:"transparent",border:`3px solid ${W}`,boxSizing:"border-box"}}),t.jsx("span",{className:"text-text-muted",children:"Node"})]}),t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"w-3 h-3 rounded-full flex-shrink-0",style:{backgroundColor:q}}),t.jsx("span",{className:"text-text-muted",children:"Hub"})]}),t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx(F,{className:"w-3 h-3 flex-shrink-0",style:{color:_},strokeWidth:2.5}),t.jsx("span",{className:"text-text-muted",children:"Local"})]}),(s=l,s.some(([,e])=>{var t;const n=null==(t=e.contact_type)?void 0:t.toLowerCase();return"room server"===n||"room_server"===n||"room"===n||"server"===n})&&t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx(k,{className:"w-3 h-3 flex-shrink-0",style:{color:Z},strokeWidth:2.5}),t.jsx("span",{className:"text-text-muted",children:"Room"})]})),a.mobileNodes.length>0&&t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"w-3 h-3 rounded-full flex-shrink-0",style:{background:"transparent",border:`3px solid ${V}`,boxSizing:"border-box"}}),t.jsx("span",{className:"text-text-muted",children:"Mobile"})]}),i.size>0&&t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsxs("div",{className:"relative w-4 h-4 flex-shrink-0",children:[t.jsx("div",{className:"absolute inset-0 rounded-full",style:{background:"transparent",border:`1px solid ${K}`,boxSizing:"border-box",opacity:.8}}),t.jsx("div",{className:"absolute rounded-full",style:{top:"4px",left:"4px",width:"8px",height:"8px",background:"transparent",border:`2px solid ${W}`,boxSizing:"border-box"}})]}),t.jsx("span",{className:"text-text-muted",children:"Neighbor"})]})]}),i.size>0&&t.jsx("div",{className:"mt-1.5 pt-1.5 border-t border-white/10",children:t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"flex-shrink-0",style:{width:"14px",height:"2px",backgroundImage:`repeating-linear-gradient(90deg, ${ee} 0, ${ee} 3px, transparent 3px, transparent 5px)`,borderRadius:"1px"}}),t.jsx("span",{className:"text-text-muted",children:"Neighbor"}),t.jsx(re,{text:"Dashed gray → yellow on hover. Direct RF contact with local."})]})}),e&&n>0&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"text-text-secondary font-medium mt-2 pt-2 border-t border-white/10 mb-1 flex items-center gap-1",children:["Topology",t.jsx(re,{text:"Links with 5+ validations. Thickness = relative strength."})]}),t.jsxs("div",{className:"flex flex-col gap-0.5 text-text-muted",children:[t.jsxs("div",{className:"flex justify-between tabular-nums",children:[t.jsx("span",{children:"Nodes"}),t.jsx("span",{className:"text-text-secondary",children:r+(o?1:0)})]}),t.jsxs("div",{className:"flex justify-between tabular-nums",children:[t.jsx("span",{children:"Links"}),t.jsx("span",{className:"text-text-secondary",children:n})]}),a.hubNodes.length>0&&t.jsxs("div",{className:"flex justify-between tabular-nums",children:[t.jsx("span",{children:"Hubs"}),t.jsx("span",{style:{color:q},children:a.hubNodes.length})]})]}),t.jsxs("div",{className:"flex flex-col gap-1 mt-1.5 pt-1.5 border-t border-white/10",children:[t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx("div",{className:"flex-shrink-0",style:{width:"14px",height:"3px",backgroundColor:U,borderRadius:"1px"}}),t.jsx("span",{className:"text-text-muted",children:"Link"}),t.jsx(re,{text:"Gray at rest. Hover to reveal type (teal=direct, indigo=loop)."})]}),a.loops.length>0&&t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsxs("div",{className:"flex-shrink-0 flex flex-col gap-0.5",style:{width:"14px"},children:[t.jsx("div",{style:{height:"2px",backgroundColor:U,borderRadius:"1px"}}),t.jsx("div",{style:{height:"2px",backgroundColor:U,borderRadius:"1px"}})]}),t.jsx("span",{className:"text-text-muted",children:"Redundant"})]})]}),a.loops.length>0&&t.jsx("div",{className:"mt-1.5 pt-1.5 border-t border-white/10",children:t.jsxs("div",{className:"flex items-center gap-1.5",children:[t.jsx(R,{className:"w-3 h-3 flex-shrink-0",style:{color:J}}),t.jsxs("div",{className:"flex flex-col",children:[t.jsxs("span",{style:{color:J},className:"font-medium",children:[a.loops.length," ",1===a.loops.length?"Loop":"Loops"]}),t.jsx("span",{className:"text-text-muted text-[10px] leading-tight",children:"Redundant paths"})]})]})})]})]});var s}const le={background:"rgba(20, 20, 22, 0.95)",borderRadius:"0.75rem",border:"1px solid rgba(140, 160, 200, 0.2)"},se=(e,t=.4)=>({background:e,borderRadius:"0.75rem",border:`1px solid ${e.replace(/[\d.]+\)$/,`${t})`)}`});function ce({isDeepLoading:e,showDeepAnalysisModal:n,onDeepAnalysis:r,showTopology:o,onToggleTopology:a,hasValidatedPolylines:i,soloHubs:l,onToggleSoloHubs:s,hasHubNodes:c,soloDirect:u,onToggleSoloDirect:d,hasZeroHopNeighbors:f,isFullscreen:h,onToggleFullscreen:p}){return t.jsxs("div",{className:"absolute top-4 right-4 z-[600] flex gap-2",children:[t.jsxs("button",{onClick:r,disabled:e||n,className:"px-3 py-2 flex items-center gap-2 transition-colors hover:bg-white/10 disabled:opacity-50 disabled:cursor-not-allowed",style:le,title:"Deep Analysis - Load full packet history and rebuild topology",children:[t.jsx("span",{className:"text-xs font-medium text-text-primary",children:"Deep Analysis"}),t.jsx(H,{className:"w-4 h-4 text-accent-primary"})]}),i&&t.jsx("button",{onClick:a,className:"p-2 transition-colors hover:bg-white/10",style:o?se("rgba(74, 222, 128, 0.2)"):le,title:o?"Hide topology lines":"Show topology lines",children:o?t.jsx(y,{className:"w-4 h-4 text-green-400"}):t.jsx(I,{className:"w-4 h-4 text-text-secondary"})}),c&&t.jsx("button",{onClick:s,className:"p-2 transition-colors hover:bg-white/10",style:l?{...le,background:"rgba(251, 191, 36, 0.25)",border:"1px solid rgba(251, 191, 36, 0.5)"}:le,title:l?"Show all nodes":"Solo hubs & connections",children:t.jsx($,{className:"w-4 h-4 "+(l?"text-amber-400":"text-text-secondary")})}),f&&t.jsx("button",{onClick:d,className:"p-2 transition-colors hover:bg-white/10",style:u?{...le,background:"rgba(67, 56, 202, 0.35)",border:"1px solid rgba(67, 56, 202, 0.6)"}:le,title:u?"Show all nodes":"Solo direct (0-hop) nodes",children:t.jsx(T,{className:"w-4 h-4 "+(u?"text-indigo-400":"text-text-secondary")})}),t.jsx("button",{onClick:p,className:"p-2 transition-colors hover:bg-white/10",style:le,title:h?"Exit fullscreen":"Fullscreen",children:h?t.jsx(z,{className:"w-4 h-4 text-text-secondary"}):t.jsx(P,{className:"w-4 h-4 text-text-secondary"})})]})}function ue(e){return e<.5?4*e*e*e:1-Math.pow(-2*e+2,3)/2}function de(e){return e<.5?4*e*e*e:1-Math.pow(-2*e+2,3)/2}var fe,he={},pe={};var me,ge,be={};var ve,ye,xe=(ge||(ge=1,ve=function(){if(fe)return pe;fe=1;var e=u();function t(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n