12 Commits

Author SHA1 Message Date
Jack Kingsman
5a9489eff1 Updating changelog + build for 2.7.8 2026-03-08 20:47:09 -07:00
Jack Kingsman
beb28b1f31 Updating changelog + build for 2.7.8 2026-03-08 20:42:03 -07:00
Jack Kingsman
7d688fa5f8 Move to more stable docker reqs without disrupting windows users 2026-03-08 20:38:33 -07:00
Jack Kingsman
09b68c37ba Better ci scripts 2026-03-08 19:56:58 -07:00
Jack Kingsman
df7dbad73d Fix bad file refs in decoder that break npm 10 2026-03-08 19:56:44 -07:00
Jack Kingsman
060fb1ef59 Updating changelog + build for 2.7.1 2026-03-08 18:48:14 -07:00
Jack Kingsman
b14e99ff24 Patch a bizarre browser quirk of leaky elements (???) in the packet list 2026-03-08 18:45:07 -07:00
Jack Kingsman
77523c1b15 Patch up to use a published patched meshcore-decoder and add a test script for different node versions 2026-03-08 18:35:58 -07:00
Jack Kingsman
9673b25ab3 yeeeikes fix raw packet feed sorry 2026-03-08 17:38:20 -07:00
Jack Kingsman
2732506f3c Fix historical DM packet length passing and fix up some docs 2026-03-08 17:12:36 -07:00
Jack Kingsman
523fe3e28e Updating changelog + build for 2.7.0 2026-03-08 16:23:23 -07:00
Jack Kingsman
3663db6ed3 Multibyte path support 2026-03-08 14:53:14 -07:00
19 changed files with 8038 additions and 74 deletions

View File

@@ -1,3 +1,24 @@
## [2.7.8] - 2026-03-08
## [2.7.8] - 2026-03-08
Bugfix: Improve frontend asset resolution and fixup the build/push script
## [2.7.1] - 2026-03-08
Bugfix: Fix historical DM packet length passing
Misc: Follow better inclusion patterns for the patched meshcore-decoder and just publish the dang package
Misc: Patch a bewildering browser quirk that cause large raw packet lists to extend past the bottom of the page
## [2.7.0] - 2026-03-08
Feature: Multibyte path support
Feature: Add multibyte statistics to statistics pane
Feature: Add path bittage to contact info pane
Feature: Put tools in a collapsible
## [2.6.1] - 2026-03-08
Misc: Fix busted docker builds; we don't have a 2.6.0 build sorry

View File

@@ -6,7 +6,6 @@ ARG COMMIT_HASH=unknown
WORKDIR /build
COPY frontend/package.json frontend/.npmrc ./
COPY frontend/lib/meshcore-decoder ./lib/meshcore-decoder
RUN npm install
COPY frontend/ ./

View File

@@ -1141,7 +1141,7 @@ SOFTWARE.
</details>
### meshcore-hashtag-cracker (1.10.0) — MIT
### meshcore-hashtag-cracker (1.11.0) — MIT
<details>
<summary>Full license text</summary>

View File

@@ -20,7 +20,7 @@ app/
├── database.py # SQLite connection + base schema + migration runner
├── migrations.py # Schema migrations (SQLite user_version)
├── models.py # Pydantic request/response models
├── repository/ # Data access layer (contacts, channels, messages, raw_packets, settings)
├── repository/ # Data access layer (contacts, channels, messages, raw_packets, settings, fanout)
├── radio.py # RadioManager + auto-reconnect monitor
├── radio_sync.py # Polling, sync, periodic advertisement loop
├── decoder.py # Packet parsing/decryption
@@ -29,6 +29,7 @@ app/
├── websocket.py # WS manager + broadcast helpers
├── fanout/ # Fanout bus: MQTT, bots, webhooks, Apprise (see fanout/AGENTS_fanout.md)
├── dependencies.py # Shared FastAPI dependency providers
├── path_utils.py # Path hex rendering and hop-width helpers
├── keystore.py # Ephemeral private/public key storage for DM decryption
├── frontend_static.py # Mount/serve built frontend (production)
└── routers/
@@ -296,6 +297,10 @@ tests/
├── test_send_messages.py # Outgoing messages, bot triggers, concurrent sends
├── test_settings_router.py # Settings endpoints, advert validation
├── test_statistics.py # Statistics aggregation
├── test_channel_sender_backfill.py # Sender key backfill for channel messages
├── test_fanout_hitlist.py # Fanout-related hitlist regression tests
├── test_main_startup.py # App startup and lifespan
├── test_path_utils.py # Path hex rendering helpers
├── test_websocket.py # WS manager broadcast/cleanup
└── test_websocket_route.py # WS endpoint lifecycle
```

View File

@@ -71,6 +71,7 @@ async def _run_historical_channel_decryption(
timestamp=result.timestamp,
received_at=packet_timestamp,
path=path_hex,
path_len=packet_info.path_length if packet_info else None,
realtime=False, # Historical decryption should not trigger fanout
)

View File

@@ -13,7 +13,7 @@ services:
# Set your serial device for passthrough here! #
################################################
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
- /dev/ttyACM0:/dev/ttyUSB0
environment:
MESHCORE_DATABASE_PATH: data/meshcore.db

View File

@@ -12,9 +12,9 @@ Keep it aligned with `frontend/src` source code.
- Tailwind utility classes + local CSS (`index.css`, `styles.css`)
- Sonner (toasts)
- Leaflet / react-leaflet (map)
- Vendored `@michaelhart/meshcore-decoder` in `frontend/lib/meshcore-decoder` (local file dependency for multibyte-support build)
- `@michaelhart/meshcore-decoder` installed via npm alias to `meshcore-decoder-multibyte-patch`
- `meshcore-hashtag-cracker` + `nosleep.js` (channel cracker)
- `@michaelhart/meshcore-decoder` pinned to the multibyte-aware `jkingsman/meshcore-decoder-multibyte` fork
- Multibyte-aware decoder build published as `meshcore-decoder-multibyte-patch`
## Frontend Map
@@ -141,8 +141,6 @@ frontend/src/
├── useWebSocket.dispatch.test.ts
└── useWebSocket.lifecycle.test.ts
frontend/lib/
└── meshcore-decoder/ # Vendored local decoder package used by app + hashtag cracker
```
## Architecture Notes

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "remoteterm-meshcore-frontend",
"private": true,
"version": "2.6.1",
"version": "2.7.8",
"type": "module",
"scripts": {
"dev": "vite",
@@ -17,7 +17,7 @@
"dependencies": {
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.3",
"@michaelhart/meshcore-decoder": "file:./lib/meshcore-decoder",
"@michaelhart/meshcore-decoder": "npm:meshcore-decoder-multibyte-patch@0.2.7",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
@@ -31,7 +31,7 @@
"d3-force-3d": "^3.0.6",
"leaflet": "^1.9.4",
"lucide-react": "^0.562.0",
"meshcore-hashtag-cracker": "^1.10.0",
"meshcore-hashtag-cracker": "^1.11.0",
"nosleep.js": "^0.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -64,10 +64,5 @@
"typescript-eslint": "^8.19.0",
"vite": "^6.0.3",
"vitest": "^2.1.0"
},
"overrides": {
"meshcore-hashtag-cracker": {
"@michaelhart/meshcore-decoder": "file:./lib/meshcore-decoder"
}
}
}

View File

@@ -614,7 +614,7 @@ export function App() {
const settingsSidebarContent = (
<nav
className="sidebar w-60 h-full min-h-0 bg-card border-r border-border flex flex-col"
className="sidebar w-60 h-full min-h-0 overflow-hidden bg-card border-r border-border flex flex-col"
aria-label="Settings"
>
<div className="flex justify-between items-center px-3 py-2.5 border-b border-border">
@@ -631,7 +631,7 @@ export function App() {
&larr; Back to Chat
</button>
</div>
<div className="flex-1 overflow-y-auto py-1">
<div className="flex-1 min-h-0 overflow-y-auto py-1 [contain:layout_paint]">
{SETTINGS_SECTION_ORDER.map((section) => (
<button
key={section}
@@ -681,7 +681,7 @@ export function App() {
<div className="flex flex-1 overflow-hidden">
{/* Desktop sidebar - hidden on mobile */}
<div className="hidden md:block">{activeSidebarContent}</div>
<div className="hidden md:block min-h-0 overflow-hidden">{activeSidebarContent}</div>
{/* Mobile sidebar - Sheet that slides in */}
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>

View File

@@ -202,14 +202,17 @@ export function RawPacketList({ packets }: RawPacketListProps) {
if (packets.length === 0) {
return (
<div className="h-full overflow-y-auto p-5 text-center text-muted-foreground">
<div className="h-full overflow-y-auto p-5 text-center text-muted-foreground [contain:layout_paint]">
No packets received yet. Packets will appear here in real-time.
</div>
);
}
return (
<div className="h-full overflow-y-auto p-4 flex flex-col gap-2" ref={listRef}>
<div
className="h-full overflow-y-auto p-4 flex flex-col gap-2 [contain:layout_paint]"
ref={listRef}
>
{sortedPackets.map(({ packet, decoded }) => (
<div
key={getRawPacketObservationKey(packet)}

View File

@@ -625,7 +625,7 @@ export function Sidebar({
return (
<nav
className="sidebar w-60 h-full min-h-0 bg-card border-r border-border flex flex-col"
className="sidebar w-60 h-full min-h-0 overflow-hidden bg-card border-r border-border flex flex-col"
aria-label="Conversations"
>
{/* Header */}
@@ -668,7 +668,7 @@ export function Sidebar({
</div>
{/* List */}
<div className="flex-1 overflow-y-auto">
<div className="flex-1 min-h-0 overflow-y-auto [contain:layout_paint]">
{/* Tools */}
{toolRows.length > 0 && (
<>

View File

@@ -1,6 +1,6 @@
[project]
name = "remoteterm-meshcore"
version = "2.6.1"
version = "2.7.8"
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
readme = "README.md"
requires-python = ">=3.10"

View File

@@ -7,6 +7,9 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OUT="${1:-$REPO_ROOT/LICENSES.md}"
FRONTEND_DOCKER_LOCK="$REPO_ROOT/frontend/package-lock.docker.json"
FRONTEND_LICENSE_IMAGE="${FRONTEND_LICENSE_IMAGE:-node:20-slim}"
FRONTEND_LICENSE_NPM="${FRONTEND_LICENSE_NPM:-10.9.5}"
# ── Backend (Python) — uses pip-licenses ─────────────────────────────
backend_licenses() {
@@ -55,56 +58,33 @@ for d in data:
}
# ── Frontend (npm) ───────────────────────────────────────────────────
frontend_licenses() {
frontend_licenses_local() {
cd "$REPO_ROOT/frontend"
node -e "
const fs = require('fs');
const path = require('path');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const depNames = Object.keys(pkg.dependencies || {}).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
for (const name of depNames) {
const pkgDir = path.join('node_modules', name);
let version = 'unknown';
let licenseType = 'Unknown';
let licenseText = null;
// Read package.json for version + license type
try {
const depPkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
version = depPkg.version || version;
licenseType = depPkg.license || licenseType;
} catch {}
// Find license file (case-insensitive search)
try {
const files = fs.readdirSync(pkgDir);
const licFile = files.find(f => /^(licen[sc]e|copying)/i.test(f));
if (licFile) {
licenseText = fs.readFileSync(path.join(pkgDir, licFile), 'utf8').trim();
}
} catch {}
console.log('### ' + name + ' (' + version + ') — ' + licenseType + '\n');
if (licenseText) {
console.log('<details>');
console.log('<summary>Full license text</summary>');
console.log();
console.log('\`\`\`');
console.log(licenseText);
console.log('\`\`\`');
console.log();
console.log('</details>');
} else {
console.log('*License file not found in package.*');
}
console.log();
node "$REPO_ROOT/scripts/print_frontend_licenses.cjs"
}
"
frontend_licenses_docker() {
docker run --rm \
-v "$REPO_ROOT:/src:ro" \
-w /tmp \
"$FRONTEND_LICENSE_IMAGE" \
bash -lc "
set -euo pipefail
cp -a /src/frontend ./frontend
cd frontend
cp package-lock.docker.json package-lock.json
npm i -g npm@$FRONTEND_LICENSE_NPM >/dev/null
npm ci --ignore-scripts >/dev/null
node /src/scripts/print_frontend_licenses.cjs
"
}
frontend_licenses() {
if [ -f "$FRONTEND_DOCKER_LOCK" ]; then
frontend_licenses_docker
else
frontend_licenses_local
fi
}
# ── Assemble ─────────────────────────────────────────────────────────

52
scripts/docker_ci.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
NODE_VERSIONS=("20" "22" "24")
# Use explicit npm patch versions so resolver regressions are caught.
NPM_VERSIONS=("9.1.1" "9.9.4" "10.9.5" "11.6.2")
echo -e "${YELLOW}=== Frontend Docker CI Matrix ===${NC}"
echo -e "${BLUE}Repo:${NC} $SCRIPT_DIR"
echo
run_combo() {
local node_version="$1"
local npm_version="$2"
local image="node:${node_version}-slim"
echo -e "${YELLOW}=== Node ${node_version} / npm ${npm_version} ===${NC}"
docker run --rm \
-v "$SCRIPT_DIR:/src:ro" \
-w /tmp \
"$image" \
bash -lc "
set -euo pipefail
cp -a /src/frontend ./frontend
cd frontend
npm i -g npm@${npm_version}
echo 'Using Node:' \$(node -v)
echo 'Using npm:' \$(npm -v)
npm install
npm run build
"
echo -e "${GREEN}Passed:${NC} Node ${node_version} / npm ${npm_version}"
echo
}
for node_version in "${NODE_VERSIONS[@]}"; do
for npm_version in "${NPM_VERSIONS[@]}"; do
run_combo "$node_version" "$npm_version"
done
done
echo -e "${GREEN}=== Docker CI matrix passed ===${NC}"

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo -e "${YELLOW}=== Extended Quality Checks ===${NC}"
echo
echo -e "${BLUE}[all_quality]${NC} Running full lint, typecheck, unit tests, and builds..."
"$SCRIPT_DIR/scripts/all_quality.sh"
echo -e "${GREEN}[all_quality]${NC} Passed!"
echo
echo -e "${BLUE}[e2e]${NC} Running end-to-end tests..."
"$SCRIPT_DIR/scripts/e2e.sh" "$@"
echo -e "${GREEN}[e2e]${NC} Passed!"
echo
echo -e "${BLUE}[docker_ci]${NC} Running Docker frontend install/build matrix..."
"$SCRIPT_DIR/scripts/docker_ci.sh"
echo -e "${GREEN}[docker_ci]${NC} Passed!"
echo
echo -e "${GREEN}=== Extended quality checks passed! ===${NC}"

View File

@@ -0,0 +1,43 @@
const fs = require('fs');
const path = require('path');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const depNames = Object.keys(pkg.dependencies || {}).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
for (const name of depNames) {
const pkgDir = path.join('node_modules', name);
let version = 'unknown';
let licenseType = 'Unknown';
let licenseText = null;
try {
const depPkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
version = depPkg.version || version;
licenseType = depPkg.license || licenseType;
} catch {}
try {
const files = fs.readdirSync(pkgDir);
const licFile = files.find((file) => /^(licen[sc]e|copying)/i.test(file));
if (licFile) {
licenseText = fs.readFileSync(path.join(pkgDir, licFile), 'utf8').trim();
}
} catch {}
console.log(`### ${name} (${version}) — ${licenseType}\n`);
if (licenseText) {
console.log('<details>');
console.log('<summary>Full license text</summary>');
console.log();
console.log('```');
console.log(licenseText);
console.log('```');
console.log();
console.log('</details>');
} else {
console.log('*License file not found in package.*');
}
console.log();
}

View File

@@ -148,8 +148,9 @@ git push
echo -e "${GREEN}Changes committed!${NC}"
echo
# Get git short hash (after commit so it reflects the new commit)
# Get git hashes (after commit so they reflect the new commit)
GIT_HASH=$(git rev-parse --short HEAD)
FULL_GIT_HASH=$(git rev-parse HEAD)
# Build docker image
echo -e "${YELLOW}Building Docker image...${NC}"
@@ -168,6 +169,38 @@ docker push jkingsman/remoteterm-meshcore:$GIT_HASH
echo -e "${GREEN}Docker push complete!${NC}"
echo
# Create GitHub release using the changelog notes for this version.
echo -e "${YELLOW}Creating GitHub release...${NC}"
RELEASE_NOTES_FILE=$(mktemp)
{
echo "$CHANGELOG_HEADER"
echo
echo "$CHANGELOG_ENTRY"
} > "$RELEASE_NOTES_FILE"
# Create and push the release tag first so GitHub release creation does not
# depend on resolving a symbolic ref like HEAD on the remote side.
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
echo -e "${YELLOW}Tag $VERSION already exists locally; reusing it.${NC}"
else
git tag "$VERSION" "$FULL_GIT_HASH"
fi
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo -e "${YELLOW}Tag $VERSION already exists on origin; not pushing it again.${NC}"
else
git push origin "$VERSION"
fi
gh release create "$VERSION" \
--title "$VERSION" \
--notes-file "$RELEASE_NOTES_FILE" \
--verify-tag
rm -f "$RELEASE_NOTES_FILE"
echo -e "${GREEN}GitHub release created!${NC}"
echo
echo -e "${GREEN}=== Publish complete! ===${NC}"
echo -e "Version: ${YELLOW}$VERSION${NC}"
echo -e "Git hash: ${YELLOW}$GIT_HASH${NC}"
@@ -175,3 +208,5 @@ echo -e "Docker tags pushed:"
echo -e " - jkingsman/remoteterm-meshcore:latest"
echo -e " - jkingsman/remoteterm-meshcore:$VERSION"
echo -e " - jkingsman/remoteterm-meshcore:$GIT_HASH"
echo -e "GitHub release:"
echo -e " - $VERSION"

2
uv.lock generated
View File

@@ -1049,7 +1049,7 @@ wheels = [
[[package]]
name = "remoteterm-meshcore"
version = "2.6.1"
version = "2.7.8"
source = { virtual = "." }
dependencies = [
{ name = "aiomqtt" },