Sync build v0.6.79

Automated sync from private repository.
Commit: b73bd1649d6337a28052f0e296c9d09fa9f03641
This commit is contained in:
GitHub Actions Bot
2025-12-28 06:04:05 +00:00
parent 98355ea9b2
commit 8c377634aa
55 changed files with 4070 additions and 2 deletions
+66
View File
@@ -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://<your-pi-ip>: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
```
+21
View File
@@ -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.
+303 -2
View File
@@ -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://<your-pi-ip>: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://<pi-ip>: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
+313
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
0.6.79
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c as e,j as t}from"./index-CylDUtFA.js";import{b as n,c as o}from"./recharts-CGff6_7g.js";const a=e("check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]),r=e("copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]),s=e("map-pin",[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0",key:"1r0f0z"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]]);function c({hash:e,prefixLength:s=8,suffixLength:c=6,full:i=!1,className:d,size:l="md"}){const[p,h]=n.useState(!1),[u,x]=n.useState(!1),m=n.useRef(null),y=n.useCallback(t=>{t.preventDefault(),t.stopPropagation(),m.current&&clearTimeout(m.current),function(e){if("undefined"!=typeof window&&(window.isSecureContext||"https:"===window.location.protocol||"localhost"===window.location.hostname)&&navigator.clipboard&&"function"==typeof navigator.clipboard.writeText)return navigator.clipboard.writeText(e).catch(()=>{}),!0;try{const t=window.scrollX,n=window.scrollY,o=document.createElement("textarea");o.value=e,o.style.position="fixed",o.style.left="-9999px",o.style.top="-9999px",o.style.opacity="0",o.style.pointerEvents="none",o.setAttribute("readonly",""),document.body.appendChild(o),o.focus({preventScroll:!0}),o.select(),o.setSelectionRange(0,e.length);const a=document.execCommand("copy");return document.body.removeChild(o),window.scrollTo(t,n),a}catch(t){return!1}}(e),h(!0),m.current=setTimeout(()=>h(!1),2e3)},[e]),w=i||e.length<=s+c+3?e:`${e.slice(0,s)}${e.slice(-c)}`,f={xs:"text-[9px] px-1 py-0.5 gap-0.5",sm:"text-[10px] px-1.5 py-0.5 gap-1",md:"text-xs px-2 py-1 gap-1.5"}[l];return t.jsxs("button",{type:"button",onClick:y,onMouseEnter:()=>x(!0),onMouseLeave:()=>x(!1),className:o("inline-flex items-center font-mono rounded-md transition-all duration-200","bg-bg-subtle/80 hover:bg-bg-elevated border border-border-subtle hover:border-border-strong","cursor-pointer select-none",f,d),title:`Click to copy: ${e}`,"aria-label":`Copy hash ${e}`,children:[t.jsx("span",{className:o("transition-colors duration-200",p?"text-accent-success":"text-text-secondary"),children:w}),t.jsx("span",{className:o("flex items-center justify-center transition-all duration-200","xs"===l?"w-2.5 h-2.5":"sm"===l?"w-3 h-3":"w-3.5 h-3.5"),children:p?t.jsx(a,{className:o("text-accent-success","xs"===l?"w-2 h-2":"sm"===l?"w-2.5 h-2.5":"w-3 h-3")}):t.jsx(r,{className:o("transition-opacity duration-200",u?"opacity-70":"opacity-40","xs"===l?"w-2 h-2":"sm"===l?"w-2.5 h-2.5":"w-3 h-3")})})]})}export{r as C,c as H,s as M,a};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{j as e}from"./index-CylDUtFA.js";import{c as a}from"./recharts-CGff6_7g.js";function l({children:l,className:s}){return e.jsx("div",{className:a("section-gap",s),children:l})}function s({title:a,icon:l,controls:s,subtitle:c}){return e.jsxs("div",{className:"flex flex-col gap-2",children:[e.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",children:[e.jsxs("h1",{className:"type-title text-text-primary flex items-center gap-3",children:[l&&e.jsx("span",{className:"w-6 h-6 text-accent-primary flex-shrink-0",children:l}),a]}),s&&e.jsx("div",{className:"flex items-center gap-2 sm:gap-3",children:s})]}),c&&e.jsx("div",{children:c})]})}function c({children:l,className:s}){return e.jsx("div",{className:a("grid-12",s),children:l})}const t=(e,a)=>{const l=a?`${a}:`:"";return"full"===e?`${l}col-span-full`:`${l}col-span-${e}`};function r({children:l,span:s="full",sm:c,md:r,lg:i,xl:n,className:d}){const m=a(t(s),c&&t(c,"sm"),r&&t(r,"md"),i&&t(i,"lg"),n&&t(n,"xl"),d);return e.jsx("div",{className:m,children:l})}const i={sm:"card-sm",md:"card-md",lg:"card-lg",hero:"card-hero",auto:"card-auto"};function n({children:l,size:s,compact:c,noPadding:t,className:r,style:n,onClick:d}){return e.jsx("div",{className:a("glass-card",!t&&(c?"card-padding-sm":"card-padding"),s&&i[s],r),style:n,onClick:d,children:l})}function d({icon:l,title:s,badge:c,subtitle:t,actions:r,iconColor:i="text-accent-primary",largeTitle:n=!1,listHeader:d=!1}){return e.jsxs("div",{className:a("flex flex-col gap-1",d?"px-4 py-3 border-b border-border-subtle bg-bg-elevated/20":"mb-3"),children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[l&&e.jsx("span",{className:a("icon-md",i),children:l}),e.jsx("span",{className:n?"type-subheading text-text-primary":"type-micro",children:s}),c&&e.jsx("span",{className:"pill-tag",children:c})]}),r&&e.jsx("div",{className:"flex items-center gap-2 flex-wrap",children:r})]}),t&&e.jsx("p",{className:"type-label text-text-muted ml-7",children:t})]})}export{n as C,c as G,l as P,s as a,d as b,r as c};
+1
View File
@@ -0,0 +1 @@
import{j as e}from"./index-CylDUtFA.js";import{b as t}from"./recharts-CGff6_7g.js";import{M as o,S as n,L as r,a,P as i}from"./maplibre-gl-zy3TF1FS.js";import"./maplibre-gl-DsiI67my.js";const s="#4F46E5",l="#F9D26F",d="#B49DFF",c="#39D98A",p="#B49DFF";function u(e,t,o,n=!1,r=!1,a=!1){let i;i=r?c:a?p:e?s:t?"#6366F1":o?"transparent":l;const u=e||t||!o||r||a;return`<div style="\n width: 14px;\n height: 14px;\n background: ${i};\n border-radius: 50%;\n border: ${u?0:5}px solid ${u?"transparent":"#4338CA"};\n box-sizing: border-box;\n transition: box-shadow 0.15s ease;\n ${n?`box-shadow: 0 0 0 3px ${d}40, 0 0 8px ${d}60;`:""}\n "></div>`}const m={id:"path-line",type:"line",paint:{"line-color":"#3B3F4A","line-width":2,"line-opacity":.7},layout:{"line-cap":"round","line-join":"round"}};function x({resolvedPath:d,localNode:x,hubNodes:h=[],hoveredHopIndex:f,onHoverHop:b}){const y=t.useRef(null),g=t.useMemo(()=>new Set(h),[h]),[v,N]=t.useState(null),{positions:C,markers:F,pathLineGeoJSON:k}=t.useMemo(()=>{const e=[],t=[],o=[];d.hops.forEach((n,r)=>{const a=n.candidates.filter(e=>{return t=e.latitude,o=e.longitude,0!==t||0!==o;var t,o});if(0===a.length)return;const i=[...a].sort((e,t)=>t.probability-e.probability)[0];o.push([i.longitude,i.latitude]);const s=!0===n.isSource,l=!0===n.isDestination;a.forEach((o,i)=>{const d=[o.latitude,o.longitude];e.push(d);const c=0===i;t.push({position:d,prefix:n.prefix,confidence:n.confidence,candidateCount:a.length,hopIndex:r,candidate:o,isHub:g.has(o.hash),isPrimary:c,isSource:s,isDestination:l})})});const n={type:"FeatureCollection",features:o.length>=2?[{type:"Feature",properties:{},geometry:{type:"LineString",coordinates:o}}]:[]};return{positions:e,markers:t,pathLineGeoJSON:n}},[d,g]),{center:S,zoom:j}=t.useMemo(()=>{if(0===C.length)return x?{center:[x.longitude,x.latitude],zoom:10}:{center:[0,0],zoom:2};let e=1/0,t=-1/0,o=1/0,n=-1/0;for(const[c,p]of C)c<e&&(e=c),c>t&&(t=c),p<o&&(o=p),p>n&&(n=p);const r=(e+t)/2,a=(o+n)/2,i=t-e,s=n-o,l=Math.max(i,s);let d;return d=l<.01?15:l<.05?13:l<.1?12:l<.5?10:l<1?9:l<5?7:5,{center:[a,r],zoom:d}},[C,x]);t.useEffect(()=>{if(!y.current||C.length<2)return;let e=1/0,t=-1/0,o=1/0,n=-1/0;for(const[r,a]of C)r<e&&(e=r),r>t&&(t=r),a<o&&(o=a),a>n&&(n=a);y.current.fitBounds([[o,e],[n,t]],{padding:30,maxZoom:16,duration:0})},[C]);const L=t.useCallback(e=>{N({longitude:e.position[1],latitude:e.position[0],marker:e})},[]);return 0===C.length?e.jsx("div",{className:"h-[200px] flex items-center justify-center text-text-muted text-sm bg-bg-elevated",children:"No mappable path data"}):e.jsxs(o,{ref:y,initialViewState:{longitude:S[0],latitude:S[1],zoom:j},style:{height:"200px",width:"100%"},mapStyle:"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",attributionControl:!1,children:[k.features.length>0&&e.jsx(n,{id:"path-line-source",type:"geojson",data:k,children:e.jsx(r,{...m})}),F.map(t=>{const o=f===t.hopIndex;return e.jsx(a,{longitude:t.position[1],latitude:t.position[0],anchor:"center",onClick:()=>L(t),children:e.jsx("div",{style:{cursor:"pointer",opacity:t.isPrimary?1:.5},onMouseEnter:()=>null==b?void 0:b(t.hopIndex),onMouseLeave:()=>null==b?void 0:b(null),dangerouslySetInnerHTML:{t:u(t.candidate.isLocal||!1,t.isHub,t.isPrimary,o,t.isSource,t.isDestination)}})},`${t.hopIndex}-${t.candidate.hash}`)}),v&&e.jsx(i,{longitude:v.longitude,latitude:v.latitude,anchor:"bottom",offset:[0,-12],closeOnClick:!1,onClose:()=>N(null),className:"maplibre-popup",children:e.jsxs("div",{className:"text-xs",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"font-semibold",children:v.marker.candidate.name}),v.marker.isSource&&e.jsx("span",{className:"px-1 py-0.5 text-[8px] font-bold rounded",style:{backgroundColor:c,color:"#000"},children:"SRC"}),v.marker.isDestination&&e.jsx("span",{className:"px-1 py-0.5 text-[8px] font-bold rounded",style:{backgroundColor:p,color:"#000"},children:"DST"}),v.marker.isHub&&e.jsx("span",{className:"px-1 py-0.5 text-[8px] font-bold rounded",style:{backgroundColor:"#FBBF24",color:"#000"},children:"HUB"}),v.marker.candidate.isLocal&&e.jsx("span",{className:"px-1 py-0.5 text-[8px] font-bold rounded",style:{backgroundColor:s,color:"#fff"},children:"LOCAL"})]}),e.jsxs("div",{className:"text-text-muted font-mono text-[10px]",children:[v.marker.prefix," • ",v.marker.candidate.hash.slice(0,10),"..."]}),!v.marker.isPrimary&&v.marker.candidateCount>1&&e.jsxs("div",{style:{color:l},children:["Alternative (",(100*v.marker.candidate.probability).toFixed(0),"%)"]}),v.marker.isPrimary&&v.marker.candidateCount>1&&e.jsxs("div",{className:"text-text-muted",children:[v.marker.candidateCount," candidates"]})]})})]})}export{x as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c as e,j as t}from"./index-CylDUtFA.js";import{c as a,b as s}from"./recharts-CGff6_7g.js";const n=e("signal-high",[["path",{d:"M2 20h.01",key:"4haj6o"}],["path",{d:"M7 20v-4",key:"j294jx"}],["path",{d:"M12 20v-8",key:"i3yub9"}],["path",{d:"M17 20V8",key:"1tkaf5"}]]),r=e("signal-low",[["path",{d:"M2 20h.01",key:"4haj6o"}],["path",{d:"M7 20v-4",key:"j294jx"}]]),c=e("signal-medium",[["path",{d:"M2 20h.01",key:"4haj6o"}],["path",{d:"M7 20v-4",key:"j294jx"}],["path",{d:"M12 20v-8",key:"i3yub9"}]]),i=e("signal-zero",[["path",{d:"M2 20h.01",key:"4haj6o"}]]),o=e("signal",[["path",{d:"M2 20h.01",key:"4haj6o"}],["path",{d:"M7 20v-4",key:"j294jx"}],["path",{d:"M12 20v-8",key:"i3yub9"}],["path",{d:"M17 20V8",key:"1tkaf5"}],["path",{d:"M22 4v16",key:"sih9yq"}]]);function d(e){return e>=-90?"excellent":e>=-100?"good":e>=-110?"fair":e>=-120?"weak":"poor"}function l(e,t){if(!t)return"bg-white/10";switch(e){case"excellent":return"bg-accent-success";case"good":return"bg-[#71F8E5]";case"fair":return"bg-[#F9D26F]";case"weak":return"bg-[#FB923C]";case"poor":return"bg-accent-danger";default:return"bg-white/20"}}function h({rssi:e,className:s="w-4 h-4"}){const l=d(e),h=function(e){switch(e){case"excellent":return"text-accent-success";case"good":return"text-[#71F8E5]";case"fair":return"text-[#F9D26F]";case"weak":return"text-[#FB923C]";case"poor":return"text-accent-danger";default:return"text-text-muted"}}(l),u=a(h,s);switch(l){case"excellent":return t.jsx(o,{className:u});case"good":return t.jsx(n,{className:u});case"fair":return t.jsx(c,{className:u});case"weak":return t.jsx(r,{className:u});default:return t.jsx(i,{className:u})}}const u=s.memo(function({rssi:e,snr:s,compact:n=!1,showValues:r=!0}){const c=d(e),i={excellent:4,good:3,fair:2,weak:1,poor:0}[c];return n?t.jsxs("div",{className:"flex items-center justify-end gap-1.5",children:[r&&t.jsx("span",{className:"text-[10px] font-mono text-text-secondary",children:e}),t.jsx("div",{className:"flex items-end gap-[2px] h-3",children:Array.from({length:4}).map((e,s)=>t.jsx("div",{className:a("w-[3px] rounded-[1px] transition-colors",l(c,s<i)),style:{height:(s+1)/4*100+"%"}},s))})]}):t.jsxs("div",{className:"flex items-center justify-end gap-2",children:[r&&t.jsxs("div",{className:"flex flex-col items-end",children:[t.jsxs("span",{className:"text-xs font-mono text-text-secondary leading-tight",children:[e," dBm"]}),void 0!==s&&t.jsxs("span",{className:"text-[10px] font-mono text-text-muted leading-tight",children:[s.toFixed(1)," dB"]})]}),t.jsx("div",{className:"flex items-end gap-[2px] h-3.5",children:Array.from({length:4}).map((e,s)=>t.jsx("div",{className:a("w-[3px] rounded-[1px] transition-colors",l(c,s<i)),style:{height:(s+1)/4*100+"%"}},s))})]})});function x(e){const t=d(e);return t.charAt(0).toUpperCase()+t.slice(1)}export{u as S,h as a,x as g};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{b as e}from"./recharts-CGff6_7g.js";import{j as t}from"./index-CylDUtFA.js";function o(t,o){const[r,n]=e.useState(t);return e.useEffect(()=>{const e=setTimeout(()=>n(t),o);return()=>clearTimeout(e)},[t,o]),r}const r=e.memo(function({ranges:e,selectedIndex:o,onSelect:r}){return t.jsx("div",{className:"toggle-group flex-shrink-0 overflow-x-auto",children:e.map((e,n)=>t.jsx("button",{onClick:()=>r(n),className:"toggle-group-item "+(o===n?"active":""),children:e.label},e.label))})});export{r as T,o as u};
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const t=a("activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);export{t as A};
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+1
View File
@@ -0,0 +1 @@
import{c}from"./index-CylDUtFA.js";const r=c("circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);export{r as C};
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const s=a("house",[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",key:"5wwlr5"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"r6nss1"}]]);export{s as H};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c as e}from"./index-CylDUtFA.js";const c=e("info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);export{c as I};
+1
View File
@@ -0,0 +1 @@
import"./recharts-CGff6_7g.js";
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const e=a("loader-circle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);export{e as L};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{r as t,g as e}from"./maplibre-gl-DsiI67my.js";function r(t,e){for(var r=0;r<e.length;r++){const o=e[r];if("string"!=typeof o&&!Array.isArray(o))for(const e in o)if("default"!==e&&!(e in t)){const r=Object.getOwnPropertyDescriptor(o,e);r&&Object.defineProperty(t,e,r.get?r:{enumerable:!0,get:()=>o[e]})}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}var o=t();const n=r({__proto__:null,default:e(o)},[o]);export{n as m};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const e=a("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{e as R};
+1
View File
@@ -0,0 +1 @@
!function(){"use strict";const n=216e5;function t(n){return n?n.startsWith("0x")||n.startsWith("0X")?n.slice(2,4).toUpperCase():n.slice(0,2).toUpperCase():""}function o(n){if(!n)return null;if(Array.isArray(n))return n;if("string"==typeof n)try{const t=JSON.parse(n);return Array.isArray(t)?t:null}catch{return null}return null}self.onmessage=r=>{const{type:e,payload:c}=r.data;if("compute"!==e)return void self.postMessage({type:"error",error:`Unknown message type: ${e}`});const s=performance.now();try{const{packets:r,nodeHashes:e}=c,f=function(r,e){const c=Date.now(),s=c-6048e5,f=new Map;for(const n of e){const o=t(n);o&&(f.has(o)||f.set(o,new Set),f.get(o).add(n))}const a=new Map,i=new Map;for(const n of e)a.set(n,new Map),i.set(n,c);for(const p of r){const r=p.timestamp??0,e=r>1e12?r:1e3*r;if(e<s)continue;const c=new Set,u=o(p.forwarded_path??p.original_path);if(u&&u.length>0)for(const n of u){const t=String(n).toUpperCase();f.has(t)&&c.add(t)}if((!u||0===u.length)&&p.src_hash){const n=t(p.src_hash);f.has(n)&&c.add(n)}if(c.size>0){const t=Math.floor((e-s)/n),o=Math.max(0,Math.min(27,t));for(const n of c){const t=f.get(n);for(const n of t){const t=a.get(n);t.set(o,(t.get(o)||0)+1),e<i.get(n)&&i.set(n,e)}}}}const u=new Map;for(const t of e){const o=a.get(t),r=i.get(t);if(0===o.size){u.set(t,[]);continue}const e=Math.max(0,Math.floor((r-s)/n)),c=[];for(let t=e;t<28;t++)c.push({idx:t-e,count:o.get(t)||0,timestamp:s+t*n});u.set(t,c)}return u}(r,e),a=performance.now()-s,i={type:"result",payload:{sparklineEntries:Array.from(f.entries())},computeTimeMs:a,packetCount:r.length,nodeCount:e.length};self.postMessage(i)}catch(f){const n={type:"error",error:f instanceof Error?f.message:"Unknown error in sparkline worker"};self.postMessage(n)}}}();
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{c as t}from"./index-CylDUtFA.js";const e=t("network",[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]]),h=t("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]]);export{e as N,h as T};
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const e=a("triangle-alert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);export{e as T};
+1
View File
@@ -0,0 +1 @@
import{b as r}from"./recharts-CGff6_7g.js";function t(t,e,n=!0,s=!1){const a=r.useRef(t);r.useEffect(()=>{a.current=t},[t]),r.useEffect(()=>{if(!n)return;s||a.current();const r=setInterval(()=>{a.current()},e);return()=>clearInterval(r)},[e,n,s])}export{t as u};
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const e=a("users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]]);export{e as U};
+1
View File
@@ -0,0 +1 @@
import{c as a}from"./index-CylDUtFA.js";const t=a("minus",[["path",{d:"M5 12h14",key:"1ays0h"}]]),d=a("trending-down",[["path",{d:"M16 17h6v-6",key:"t6n2it"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7",key:"x473p"}]]),h=a("zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);export{t as M,d as T,h as Z};
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>pyMC Console</title>
<meta name="description" content="LoRa Mesh Network Repeater Monitoring Dashboard" />
<!-- Fonts: Inter (display) and JetBrains Mono (data) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,700;1,800;1,900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<!-- Theme initialization - prevent flash of unstyled content -->
<script>
(function() {
try {
// Try new key first, fall back to legacy key for migration
var scheme = localStorage.getItem('pymc-color-scheme');
if (!scheme) {
// Migration: read legacy key and map to color scheme
var legacy = localStorage.getItem('pymc-background');
if (legacy && legacy !== 'default') {
scheme = legacy; // Legacy stored preset ID which matches scheme ID
}
}
// Apply theme if not default (default uses :root styles)
if (scheme && scheme !== 'default') {
document.documentElement.setAttribute('data-theme', scheme);
}
} catch (e) {}
})();
</script>
<style>
:root {
--font-display: 'Inter', system-ui, -apple-system, sans-serif;
--font-data: 'JetBrains Mono', 'SF Mono', Monaco, monospace;
}
</style>
<script type="module" crossorigin src="/assets/index-CylDUtFA.js"></script>
<link rel="modulepreload" crossorigin href="/assets/maplibre-gl-DsiI67my.js">
<link rel="modulepreload" crossorigin href="/assets/recharts-CGff6_7g.js">
<link rel="stylesheet" crossorigin href="/assets/index-CLg20r2M.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+48
View File
@@ -0,0 +1,48 @@
{
"name": "pymc_console",
"version": "0.6.79",
"description": "Vite + React Dashboard for pyMC_Repeater",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"typecheck": "tsc --noEmit",
"build": "vite build",
"build:static": "npm run build && npm run package",
"package": "rm -rf dist && mkdir -p dist && cp -r out/* dist/",
"preview": "vite preview",
"lint": "eslint"
},
"dependencies": {
"@types/leaflet": "^1.9.21",
"clsx": "^2.1.1",
"leaflet": "^1.9.4",
"lucide-react": "^0.559.0",
"maplibre-gl": "^5.15.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-is": "^19.2.3",
"react-leaflet": "^4.2.1",
"react-map-gl": "^8.1.0",
"react-router-dom": "^7.1.1",
"recharts": "^3.5.1",
"zustand": "^5.0.9"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react-swc": "^3.7.2",
"eslint": "^9",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.25",
"globals": "^16.5.0",
"tailwindcss": "^4",
"terser": "^5.44.1",
"typescript": "^5",
"typescript-eslint": "^8.50.0",
"vite": "^6.0.5"
}
}
Executable
+3224
View File
File diff suppressed because it is too large Load Diff