Compare commits

...

22 Commits

Author SHA1 Message Date
Jack Kingsman c7248222dd Updating changelog + build for 3.10.0 2026-04-10 11:16:16 -07:00
Jack Kingsman 1e18a91f12 Merge pull request #172 from YourSandwich/aur-install-instructions
Add Arch Linux (AUR) packaging infrastructure
2026-04-10 10:54:49 -07:00
Jack Kingsman 18db6e4dd8 Make test script executable 2026-04-10 10:49:49 -07:00
Jack Kingsman 2393dadf1b Unload the service on uninstall 2026-04-10 10:48:38 -07:00
Jack Kingsman fd26576e0d Use correct email 2026-04-10 10:47:21 -07:00
Sandwich cb5a76eb5f Replace manual user/group creation with sysusers.d and tmpfiles.d 2026-04-10 19:23:01 +02:00
Jack Kingsman 7f5dde119f Update AGENTS.md 2026-04-10 00:15:57 -07:00
Jack Kingsman 799a721761 Be more defensive about systemd detection 2026-04-10 00:10:53 -07:00
Jack Kingsman 152a584f35 Fix TCP host 2026-04-10 00:10:41 -07:00
Jack Kingsman 5cc0476426 Fix port numbering 2026-04-10 00:06:22 -07:00
Jack Kingsman e468c6c161 Change command palette shortcut 2026-04-09 23:45:16 -07:00
Jack Kingsman e33537018b Fix AUR username 2026-04-09 23:11:02 -07:00
Jack Kingsman 0727793560 Add test script 2026-04-09 23:08:32 -07:00
Jack Kingsman 5c4e04e024 Skip daemon reload if systemctl isn't around 2026-04-09 23:08:26 -07:00
Jack Kingsman 967269ef7d Initial AUR work 2026-04-09 23:08:22 -07:00
Jack Kingsman 1903797d0d Fix broken statistics pane e2e test 2026-04-09 22:30:12 -07:00
Jack Kingsman bb5af5ba82 Bump apprise to 1.9.9. Closes #173. 2026-04-09 17:20:57 -07:00
Sandwich 424da7e232 Add Arch Linux (AUR) install instructions to README
Adds "Install Path 3: Arch Linux (AUR)" section covering both AUR
helper and manual makepkg installation, linking to the published
remoteterm-meshcore AUR package.

Closes #171
2026-04-09 03:51:39 +02:00
Jack Kingsman 159df1ec5b Revert "Add debug lines for fav click"
This reverts commit 8e2e039985.
2026-04-08 16:33:44 -07:00
Jack Kingsman 8e2e039985 Add debug lines for fav click 2026-04-08 16:18:46 -07:00
Jack Kingsman 01c86a486e Add packet feed filters; closes #169. 2026-04-08 14:44:41 -07:00
Jack Kingsman 7d5cfdec26 Add note about startup on windows 2026-04-07 22:07:31 -07:00
25 changed files with 719 additions and 51 deletions
+73
View File
@@ -0,0 +1,73 @@
name: Publish AUR package
# Pushes the contents of pkg/aur/ to the remoteterm-meshcore AUR repository
# whenever a GitHub release is published. Can also be triggered manually for
# testing or out-of-band republishes.
#
# Required secrets:
# AUR_SSH_PRIVATE_KEY Private SSH key registered with the AUR maintainer
# account that owns the remoteterm-meshcore package.
# AUR_COMMIT_EMAIL Email used for the AUR git commit identity.
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to publish (no v prefix, e.g. 3.9.1)'
required: true
concurrency:
# Serialize publishes so a fast back-to-back release sequence cannot race
# two pushes against the AUR repo. The later one wins by virtue of being
# the final state.
group: publish-aur
cancel-in-progress: false
jobs:
publish-aur:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Resolve version from event
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ inputs.version }}"
else
VERSION="${{ github.event.release.tag_name }}"
fi
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing AUR package for version $VERSION"
- name: Stamp pkgver into PKGBUILD
run: |
sed -i "s/^pkgver=.*/pkgver=${{ steps.version.outputs.version }}/" pkg/aur/PKGBUILD
sed -i "s/^pkgrel=.*/pkgrel=1/" pkg/aur/PKGBUILD
- name: Publish to AUR
uses: KSXGitHub/github-actions-deploy-aur@v4.1.2
with:
pkgname: remoteterm-meshcore
pkgbuild: pkg/aur/PKGBUILD
assets: |
pkg/aur/remoteterm-meshcore.install
pkg/aur/remoteterm-meshcore.service
pkg/aur/remoteterm-meshcore.sysusers
pkg/aur/remoteterm-meshcore.tmpfiles
pkg/aur/remoteterm.env
commit_username: jackkingsman
commit_email: ${{ secrets.AUR_COMMIT_EMAIL }}
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: "Update to ${{ steps.version.outputs.version }}"
# Recompute sha256sums from the live release tarball + the bundled
# service/env files. The committed PKGBUILD has SKIP placeholders.
updpkgsums: true
# Validate the PKGBUILD parses and sources download, but skip the
# actual build (which would run uv sync + npm install for several
# minutes of CI time on every release).
test: true
test_flags: --clean --cleanbuild --nodeps --nobuild
+3 -1
View File
@@ -209,6 +209,7 @@ This message-layer echo/path handling is independent of raw-packet storage dedup
│ │ ├── MapView.tsx # Leaflet map showing node locations
│ │ └── ...
│ └── vite.config.ts
├── pkg/aur/ # AUR package files (PKGBUILD, systemd service, env, install hooks)
├── scripts/ # Quality / release helpers (listing below is representative, not exhaustive)
│ ├── build/
│ │ ├── collect_licenses.sh # Gather third-party license attributions
@@ -216,7 +217,8 @@ This message-layer echo/path handling is independent of raw-packet storage dedup
│ ├── quality/
│ │ ├── all_quality.sh # Repo-standard autofix + validate gate
│ │ ├── e2e.sh # End-to-end test runner
│ │ ── extended_quality.sh # Quality gate plus e2e and Docker matrix
│ │ ── extended_quality.sh # Quality gate plus e2e and Docker matrix
│ │ └── test_aur_package.sh # Build + install AUR package in Arch Docker containers
│ └── setup/
│ ├── fetch_prebuilt_frontend.py # Download release frontend fallback
│ └── install_service.sh # Install/configure Linux systemd service
+9
View File
@@ -1,3 +1,12 @@
## [3.10.0] - 2026-04-10
* Feature: Add Arch AUR package
* Feature: 72hr packet density view in statistics
* Feature: Add warnings for event loop selection for MQTT on Windows startup
* Bufix: Bump Apprise to 1.9.9 to fix Matrix bug
* Misc: More memory-conscious on recent contact fetch
* Misc: Fix statistics pane e2e test
## [3.9.0] - 2026-04-06
* Feature: Add hop counts to hop-width selection options
+2 -2
View File
@@ -56,7 +56,7 @@ SOFTWARE.
</details>
### apprise (1.9.7) — BSD-2-Clause
### apprise (1.9.9) — BSD-2-Clause
<details>
<summary>Full license text</summary>
@@ -64,7 +64,7 @@ SOFTWARE.
```
BSD 2-Clause License
Copyright (c) 2025, Chris Caron <lead2gold@gmail.com>
Copyright (c) 2026, Chris Caron <lead2gold@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
+32
View File
@@ -161,6 +161,29 @@ To stop:
sudo docker compose down
```
## Install Path 3: Arch Linux (AUR)
A [`remoteterm-meshcore`](https://aur.archlinux.org/packages/remoteterm-meshcore) package is available in the AUR. Install it with an AUR helper or build it manually:
```bash
# with an AUR helper
yay -S remoteterm-meshcore
# or manually
git clone https://aur.archlinux.org/remoteterm-meshcore.git
cd remoteterm-meshcore
makepkg -si
```
Configure your radio connection, then start the service:
```bash
sudo vi /etc/remoteterm-meshcore/remoteterm.env
sudo systemctl enable --now remoteterm-meshcore
```
Access the app at http://localhost:8000.
## Standard Environment Variables
Only one transport may be active at a time. If multiple are set, the server will refuse to start.
@@ -199,6 +222,15 @@ $env:MESHCORE_SERIAL_PORT="COM8" # or your COM port
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```
> [!WARNING]
> **Windows + MQTT fanout:** Python's default Windows event loop (ProactorEventLoop) is not compatible with the MQTT libraries used by RemoteTerm. If you configure any MQTT integration, add `--loop none` to your uvicorn command:
>
> ```powershell
> uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --loop none
> ```
>
> If you forget, the app will start normally but MQTT connections will fail and you'll see a toast in the UI with this same guidance.
If you enable Basic Auth, protect the app with HTTPS. HTTP Basic credentials are not safe on plain HTTP. Also note that the app's permissive CORS policy is a deliberate trusted-network tradeoff, so cross-origin browser JavaScript is not a reliable way to use that Basic Auth gate.
## Where To Go Next
+29
View File
@@ -12,6 +12,7 @@ from __future__ import annotations
import asyncio
import json
import logging
import sys
import time
from abc import ABC, abstractmethod
from typing import Any
@@ -252,6 +253,34 @@ class BaseMqttPublisher(ABC):
self._client = None
self._last_error = _format_error_detail(e)
# Windows ProactorEventLoop does not implement add_reader /
# add_writer, which paho-mqtt requires. The failure can
# surface as a direct NotImplementedError (add_writer in
# __aenter__) or as a generic timeout (add_reader fails
# inside an event-loop callback, so paho never hears back).
# Either way, if we're on Windows with Proactor the root
# cause is the same and retrying won't help.
_on_proactor = (
sys.platform == "win32"
and type(asyncio.get_event_loop()).__name__ == "ProactorEventLoop"
)
if _on_proactor:
broadcast_error(
"MQTT unavailable — Windows event loop incompatible",
"The default Windows event loop (ProactorEventLoop) does "
"not support MQTT. Add --loop none to your uvicorn "
"command and restart. See README.md for details.",
)
_broadcast_health()
logger.error(
"%s cannot run: Windows ProactorEventLoop does not "
"implement add_reader/add_writer required by paho-mqtt. "
"Restart uvicorn with '--loop none' to use "
"SelectorEventLoop instead. Giving up (will not retry).",
self._integration_label(),
)
return
title, detail = self._on_error()
broadcast_error(title, detail)
_broadcast_health()
+37 -1
View File
@@ -1,5 +1,41 @@
import asyncio
import logging
import sys
# ---------------------------------------------------------------------------
# Windows event-loop advisory for MQTT fanout
# ---------------------------------------------------------------------------
# On Windows, uvicorn's default event loop (ProactorEventLoop) does not
# implement add_reader()/add_writer(), which paho-mqtt (via aiomqtt) requires.
# We cannot fix this from inside the app — the loop is already created by the
# time this module is imported. Log a prominent warning so Windows operators
# who want MQTT know to add ``--loop none`` to their uvicorn command.
# ---------------------------------------------------------------------------
if sys.platform == "win32":
import asyncio as _asyncio
_loop = _asyncio.get_event_loop()
_is_proactor = type(_loop).__name__ == "ProactorEventLoop"
if _is_proactor:
print(
"\n" + "!" * 78 + "\n"
" NOTE FOR WINDOWS USERS\n" + "!" * 78 + "\n"
"\n"
" The running event loop is ProactorEventLoop, which is not\n"
" compatible with MQTT fanout (aiomqtt / paho-mqtt).\n"
"\n"
" If you use MQTT integrations, restart with --loop none:\n"
"\n"
" uv run uvicorn app.main:app \033[1m--loop none\033[0m"
" [... other options ...]\n"
"\n"
" Everything else works fine as-is.\n"
"\n" + "!" * 78 + "\n",
file=sys.stderr,
flush=True,
)
del _loop, _is_proactor
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
+1 -1
View File
@@ -31,7 +31,7 @@ services:
# TCP
# MESHCORE_TCP_HOST: 192.168.1.100
# MESHCORE_TCP_PORT: 4000
# MESHCORE_TCP_PORT: 5000
# BLE
# BLE in Docker usually needs additional manual compose changes such as
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "remoteterm-meshcore-frontend",
"private": true,
"version": "3.9.0",
"version": "3.10.0",
"type": "module",
"scripts": {
"dev": "vite",
+4 -4
View File
@@ -7,7 +7,7 @@ import {
Radio,
Route,
Search,
Sparkles,
Star,
User,
Waypoints,
} from 'lucide-react';
@@ -296,7 +296,7 @@ export function CommandPalette({
>
<Hash className="text-muted-foreground" />
<span>{ch.name}</span>
<Sparkles className="ml-auto h-3 w-3 text-yellow-500" />
<Star className="ml-auto h-3 w-3 text-favorite" />
</CommandItem>
))}
</CommandGroup>
@@ -384,7 +384,7 @@ function ContactGroup({
>
<Icon className="text-muted-foreground" />
<span>{displayName}</span>
{showStar && <Sparkles className="ml-auto h-3 w-3 text-yellow-500" />}
{showStar && <Star className="ml-auto h-3 w-3 text-favorite" />}
</CommandItem>
))}
</CommandGroup>
@@ -419,7 +419,7 @@ function RepeaterGroup({
>
<Waypoints className="text-muted-foreground" />
<span>{displayName}</span>
{showStar && <Sparkles className="ml-auto h-3 w-3 text-yellow-500" />}
{showStar && <Star className="ml-auto h-3 w-3 text-favorite" />}
</CommandItem>,
<CommandItem
key={`${c.public_key}-acl`}
+179 -26
View File
@@ -11,11 +11,14 @@ import {
Cell,
} from 'recharts';
import { MeshCoreDecoder, Utils } from '@michaelhart/meshcore-decoder';
import { RawPacketList } from './RawPacketList';
import { RawPacketInspectorDialog } from './RawPacketDetailModal';
import { Button } from './ui/button';
import type { Channel, Contact, RawPacket } from '../types';
import {
KNOWN_PAYLOAD_TYPES,
RAW_PACKET_STATS_WINDOWS,
buildRawPacketStatsSnapshot,
type NeighborStat,
@@ -24,9 +27,26 @@ import {
type RawPacketStatsSessionState,
type RawPacketStatsWindow,
} from '../utils/rawPacketStats';
import { createDecoderOptions } from '../utils/rawPacketInspector';
import { getContactDisplayName } from '../utils/pubkey';
import { cn } from '@/lib/utils';
const KNOWN_PAYLOAD_TYPE_SET = new Set<string>(KNOWN_PAYLOAD_TYPES);
function getPacketTypeName(
packet: RawPacket,
decoderOptions?: ReturnType<typeof createDecoderOptions>
): string {
try {
const decoded = MeshCoreDecoder.decode(packet.data, decoderOptions);
if (!decoded.isValid) return 'Unknown';
const name = Utils.getPayloadTypeName(decoded.payloadType);
return KNOWN_PAYLOAD_TYPE_SET.has(name) ? name : 'Unknown';
} catch {
return 'Unknown';
}
}
interface RawPacketFeedViewProps {
packets: RawPacket[];
rawPacketStatsSession: RawPacketStatsSessionState;
@@ -428,6 +448,48 @@ export function RawPacketFeedView({
const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000));
const [selectedPacket, setSelectedPacket] = useState<RawPacket | null>(null);
const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [enabledTypes, setEnabledTypes] = useState<Set<string>>(() => new Set(KNOWN_PAYLOAD_TYPES));
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
const packetsWithTypes = useMemo(
() =>
packets.map((packet) => ({
packet,
payloadType: getPacketTypeName(packet, decoderOptions),
})),
[packets, decoderOptions]
);
const allTypesEnabled = enabledTypes.size === KNOWN_PAYLOAD_TYPES.length;
const filteredPackets = useMemo(() => {
if (allTypesEnabled) return packets;
return packetsWithTypes
.filter(({ payloadType }) => enabledTypes.has(payloadType))
.map(({ packet }) => packet);
}, [packetsWithTypes, enabledTypes, packets, allTypesEnabled]);
const handleToggleAll = () => {
setEnabledTypes(allTypesEnabled ? new Set() : new Set(KNOWN_PAYLOAD_TYPES));
};
const handleToggleType = (type: string) => {
setEnabledTypes((prev) => {
const next = new Set(prev);
if (next.has(type)) {
next.delete(type);
} else {
next.add(type);
}
return next;
});
};
const handleOnly = (type: string) => {
setEnabledTypes(new Set([type]));
};
useEffect(() => {
const interval = window.setInterval(() => {
@@ -468,38 +530,129 @@ export function RawPacketFeedView({
);
return (
<>
<div className="flex items-center justify-between gap-3 border-b border-border px-4 py-2.5">
<div>
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
<p className="text-xs text-muted-foreground">
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
</p>
<div className="border-b border-border px-4 py-2.5">
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
<p className="hidden md:block text-xs text-muted-foreground">
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
</p>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setAnalyzeModalOpen(true)}
>
Analyze Packet
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setStatsOpen((current) => !current)}
aria-expanded={statsOpen}
>
{statsOpen ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
{statsOpen ? 'Hide Stats' : 'Show Stats'}
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setAnalyzeModalOpen(true)}
>
Analyze Packet
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setStatsOpen((current) => !current)}
aria-expanded={statsOpen}
>
{statsOpen ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
{statsOpen ? 'Hide Stats' : 'Show Stats'}
</Button>
<p className="md:hidden text-xs text-muted-foreground">
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
{!mobileFiltersOpen && (
<>
{' · '}
<button
type="button"
className="text-primary hover:text-primary/80 transition-colors"
onClick={() => setMobileFiltersOpen(true)}
>
Show Filters
</button>
</>
)}
</p>
{mobileFiltersOpen && (
<div className="mt-1.5 md:hidden flex flex-wrap items-center gap-x-3 gap-y-1">
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
<input
type="checkbox"
checked={allTypesEnabled}
onChange={handleToggleAll}
className="rounded"
/>
All
</label>
{KNOWN_PAYLOAD_TYPES.map((type) => (
<span key={type} className="inline-flex items-center gap-1 text-xs">
<label className="flex items-center gap-1 text-foreground cursor-pointer">
<input
type="checkbox"
checked={enabledTypes.has(type)}
onChange={() => handleToggleType(type)}
className="rounded"
/>
{type}
</label>
<button
type="button"
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
onClick={() => handleOnly(type)}
>
(only)
</button>
</span>
))}
</div>
)}
<div className="mt-1.5 hidden md:flex flex-wrap items-center gap-x-3 gap-y-1">
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
<input
type="checkbox"
checked={allTypesEnabled}
onChange={handleToggleAll}
className="rounded"
/>
All
</label>
{KNOWN_PAYLOAD_TYPES.map((type) => (
<span key={type} className="inline-flex items-center gap-1 text-xs">
<label className="flex items-center gap-1 text-foreground cursor-pointer">
<input
type="checkbox"
checked={enabledTypes.has(type)}
onChange={() => handleToggleType(type)}
className="rounded"
/>
{type}
</label>
<button
type="button"
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
onClick={() => handleOnly(type)}
>
(only)
</button>
</span>
))}
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
<div className={cn('min-h-0 min-w-0 flex-1', statsOpen && 'md:border-r md:border-border')}>
<RawPacketList packets={packets} channels={channels} onPacketClick={setSelectedPacket} />
<RawPacketList
packets={filteredPackets}
channels={channels}
onPacketClick={setSelectedPacket}
/>
</div>
<aside
@@ -1666,7 +1666,8 @@ function AppriseConfigEditor({
rows={4}
/>
<p className="text-xs text-muted-foreground">
One URL per line. All URLs receive every matched notification.
One URL per line. All URLs receive every matched notification. For Matrix room version 12
(servername-less room IDs), append <code>?hsreq=no</code> to the URL.
</p>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ const RAW_PACKET_STATS_WINDOW_SECONDS: Record<Exclude<RawPacketStatsWindow, 'ses
export const MAX_RAW_PACKET_STATS_OBSERVATIONS = 20000;
const KNOWN_PAYLOAD_TYPES = [
export const KNOWN_PAYLOAD_TYPES = [
'Advert',
'GroupText',
'TextMessage',
+127
View File
@@ -0,0 +1,127 @@
# Maintainer: Jack Kingsman <jack@jackkingsman.me>
pkgname=remoteterm-meshcore
# pkgver is rewritten by .github/workflows/publish-aur.yml on each release.
pkgver=3.9.0
pkgrel=1
pkgdesc='Web interface for MeshCore mesh radio networks'
arch=(x86_64 aarch64)
url='https://github.com/jkingsman/Remote-Terminal-for-MeshCore'
license=('MIT')
# No system python dependency — we bundle a standalone interpreter via
# python-build-standalone so the package is immune to Arch python ABI bumps.
depends=(glibc)
makedepends=(uv nodejs npm)
optdepends=('bluez: BLE transport support')
backup=(etc/remoteterm-meshcore/remoteterm.env)
# The bundled python-build-standalone binary ships pre-stripped. makepkg's
# default strip pass corrupts its unusual ELF layout (.dynstr not in segment),
# so we disable stripping for the whole package.
options=(!strip)
install=remoteterm-meshcore.install
source=(
"$pkgname-$pkgver.tar.gz::https://github.com/jkingsman/Remote-Terminal-for-MeshCore/archive/refs/tags/$pkgver.tar.gz"
"remoteterm-meshcore.service"
"remoteterm.env"
"remoteterm-meshcore.sysusers"
"remoteterm-meshcore.tmpfiles"
)
# sha256sums are recomputed by `updpkgsums` in the publish workflow before
# the PKGBUILD is pushed to AUR. The committed values are intentionally SKIP
# so the file is honest about not tracking real hashes in this repo.
sha256sums=('SKIP'
'SKIP'
'SKIP'
'SKIP'
'SKIP')
# python-build-standalone: stripped install_only builds (~30 MB each).
# Bump _pyver and _pybuilddate when updating the bundled interpreter.
_pyver=3.13.13
_pybuilddate=20260408
source_x86_64=("python-${_pyver}-x86_64.tar.gz::https://github.com/astral-sh/python-build-standalone/releases/download/${_pybuilddate}/cpython-${_pyver}+${_pybuilddate}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz")
sha256sums_x86_64=('SKIP')
source_aarch64=("python-${_pyver}-aarch64.tar.gz::https://github.com/astral-sh/python-build-standalone/releases/download/${_pybuilddate}/cpython-${_pyver}+${_pybuilddate}-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz")
sha256sums_aarch64=('SKIP')
_srcname="Remote-Terminal-for-MeshCore-$pkgver"
build() {
cd "$_srcname"
# Build frontend
cd frontend
npm ci
npm run build
cd ..
# Create venv using the bundled standalone Python interpreter, then install
# Python dependencies into it. This produces a fully self-contained venv
# that does not reference the system Python at all.
uv venv --python "$srcdir/python/bin/python3" .venv
uv sync --no-dev --frozen
}
package() {
cd "$_srcname"
local _optdir=/opt/remoteterm-meshcore
local _instdir="$pkgdir$_optdir"
# App source
install -d "$_instdir"
cp -r app "$_instdir/"
cp pyproject.toml uv.lock "$_instdir/"
# Frontend build
install -d "$_instdir/frontend"
cp -r frontend/dist "$_instdir/frontend/"
# Bundled Python interpreter
cp -a "$srcdir/python" "$_instdir/python"
# Python venv
cp -a .venv "$_instdir/"
# Fix shebangs and venv config: replace build-time paths with final
# install paths so the venv works from /opt after installation.
# sed only operates on regular file contents, so symlinks need separate
# fixup below.
find "$_instdir/.venv/bin" -type f -exec \
sed -i "s|$srcdir/$_srcname/.venv|$_optdir/.venv|g" {} +
find "$_instdir/.venv/bin" -type f -exec \
sed -i "s|$srcdir/python|$_optdir/python|g" {} +
sed -i \
-e "s|$srcdir/$_srcname/.venv|$_optdir/.venv|g" \
-e "s|$srcdir/python|$_optdir/python|g" \
"$_instdir/.venv/pyvenv.cfg" 2>/dev/null || true
# Recreate the venv interpreter symlinks — these are symlinks (not files),
# so sed cannot fix them. Point them at the bundled Python.
ln -sf "$_optdir/python/bin/python3" "$_instdir/.venv/bin/python"
ln -sf python "$_instdir/.venv/bin/python3"
ln -sf python "$_instdir/.venv/bin/python3.13"
# Data directory symlink
ln -s /var/lib/remoteterm-meshcore "$_instdir/data"
# Systemd service
install -Dm644 "$srcdir/remoteterm-meshcore.service" \
"$pkgdir/usr/lib/systemd/system/remoteterm-meshcore.service"
# Environment file
install -Dm640 "$srcdir/remoteterm.env" \
"$pkgdir/etc/remoteterm-meshcore/remoteterm.env"
# System user and data directory
install -Dm644 "$srcdir/remoteterm-meshcore.sysusers" \
"$pkgdir/usr/lib/sysusers.d/remoteterm-meshcore.conf"
install -Dm644 "$srcdir/remoteterm-meshcore.tmpfiles" \
"$pkgdir/usr/lib/tmpfiles.d/remoteterm-meshcore.conf"
# License
install -Dm644 LICENSE.md \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
+35
View File
@@ -0,0 +1,35 @@
post_install() {
echo "==> Set your radio connection (serial, TCP, or BLE) in"
echo "==> /etc/remoteterm-meshcore/remoteterm.env"
echo "==> Start the service with: systemctl enable --now remoteterm-meshcore"
echo "==> The web UI will be at http://localhost:8000"
}
post_upgrade() {
# Clean orphaned __pycache__ dirs left by the previous Python version
find /opt/remoteterm-meshcore -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
# Skip systemd operations in chroots/containers where the binary may exist
# but PID 1 is not systemd.
if [ -d /run/systemd/system ] && command -v systemctl &>/dev/null; then
systemctl daemon-reload || true
if systemctl is-active --quiet remoteterm-meshcore; then
systemctl restart remoteterm-meshcore || true
fi
fi
}
pre_remove() {
if [ -d /run/systemd/system ] && command -v systemctl &>/dev/null; then
systemctl disable --now remoteterm-meshcore 2>/dev/null || true
fi
}
post_remove() {
if [ -d /run/systemd/system ] && command -v systemctl &>/dev/null; then
systemctl daemon-reload
fi
echo "==> Database and config remain in /var/lib/remoteterm-meshcore/, remoteterm user retained."
echo "==> To fully clean up: sudo rm -rf /var/lib/remoteterm-meshcore"
}
+29
View File
@@ -0,0 +1,29 @@
[Unit]
Description=RemoteTerm for MeshCore
Documentation=https://github.com/jkingsman/Remote-Terminal-for-MeshCore
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=remoteterm
Group=remoteterm
WorkingDirectory=/opt/remoteterm-meshcore
EnvironmentFile=/etc/remoteterm-meshcore/remoteterm.env
ExecStart=/opt/remoteterm-meshcore/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5s
StateDirectory=remoteterm-meshcore
# Hardening
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
# Serial port access (uucp group on Arch)
SupplementaryGroups=uucp
[Install]
WantedBy=multi-user.target
+1
View File
@@ -0,0 +1 @@
u remoteterm - "RemoteTerm for MeshCore" /var/lib/remoteterm-meshcore
+1
View File
@@ -0,0 +1 @@
d /var/lib/remoteterm-meshcore 0750 remoteterm remoteterm
+28
View File
@@ -0,0 +1,28 @@
# RemoteTerm for MeshCore configuration
# https://github.com/jkingsman/Remote-Terminal-for-MeshCore
# Transport: uncomment ONE section below
# Serial auto-detect (default — no config needed)
# Serial manual port
#MESHCORE_SERIAL_PORT=/dev/ttyUSB0
# TCP
#MESHCORE_TCP_HOST=192.168.1.100
#MESHCORE_TCP_PORT=5000
# BLE (also requires the optional `bluez` package)
#MESHCORE_BLE_ADDRESS=AA:BB:CC:DD:EE:FF
#MESHCORE_BLE_PIN=123456
# Database
MESHCORE_DATABASE_PATH=/var/lib/remoteterm-meshcore/meshcore.db
# Bots can run arbitrary Python on the server. Leave this set to 'true' unless
# you trust everyone on your network.
MESHCORE_DISABLE_BOTS=true
# HTTP Basic Auth (recommended when bots are enabled)
#MESHCORE_BASIC_AUTH_USERNAME=
#MESHCORE_BASIC_AUTH_PASSWORD=
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "remoteterm-meshcore"
version = "3.9.0"
version = "3.10.0"
description = "RemoteTerm - Web interface for MeshCore radio mesh networks"
readme = "README.md"
requires-python = ">=3.11"
@@ -14,7 +14,7 @@ dependencies = [
"pynacl>=1.5.0",
"meshcore==2.3.2",
"aiomqtt>=2.0",
"apprise>=1.9.7",
"apprise>=1.9.8",
"boto3>=1.38.0",
]
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
# test_aur_package.sh — Build the AUR package in one Arch container, then
# install and run it in a clean Arch container with port 8000 exposed.
#
# Usage:
# ./scripts/quality/test_aur_package.sh [--port PORT]
#
# The script streams application logs until you Ctrl-C.
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PORT=8000
if [ "${1:-}" = "--port" ]; then PORT="${2:-8000}"; fi
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ARTIFACT_DIR="$(mktemp -d)"
INSTALL_CONTAINER="remoteterm-aur-test-$$"
cleanup() {
echo
echo -e "${YELLOW}Cleaning up...${NC}"
docker rm -f "$INSTALL_CONTAINER" 2>/dev/null || true
rm -rf "$ARTIFACT_DIR"
echo -e "${GREEN}Done.${NC}"
}
trap cleanup EXIT
# ── Phase 1: Build ────────────────────────────────────────────────────────────
echo -e "${BOLD}=== Phase 1: Build AUR package ===${NC}"
docker run --rm \
-v "$REPO_ROOT/pkg/aur:/pkg:ro" \
-v "$ARTIFACT_DIR:/out" \
archlinux:latest bash -c '
set -euo pipefail
pacman -Syu --noconfirm base-devel git curl >/dev/null 2>&1
curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1
export PATH="$HOME/.local/bin:$PATH"
pacman -S --noconfirm nodejs npm >/dev/null 2>&1
useradd -m builder
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
BUILD_DIR=/home/builder/build
mkdir -p "$BUILD_DIR"
cp /pkg/PKGBUILD /pkg/remoteterm-meshcore.install \
/pkg/remoteterm-meshcore.service /pkg/remoteterm-meshcore.sysusers \
/pkg/remoteterm-meshcore.tmpfiles /pkg/remoteterm.env "$BUILD_DIR/"
chown -R builder:builder "$BUILD_DIR"
echo "Building package..."
su builder -c "export PATH=\"$HOME/.local/bin:\$PATH\" && cd $BUILD_DIR && makepkg -sf --noconfirm" 2>&1
cp "$BUILD_DIR"/remoteterm-meshcore-*.pkg.tar.zst /out/
echo "Package artifact copied to /out/"
ls -lh /out/*.pkg.tar.zst
'
PKG_FILE="$(ls "$ARTIFACT_DIR"/*.pkg.tar.zst 2>/dev/null | head -1)"
if [ -z "$PKG_FILE" ]; then
echo -e "${RED}Build failed — no .pkg.tar.zst produced${NC}"
exit 1
fi
echo -e "${GREEN}Built: $(basename "$PKG_FILE") ($(du -h "$PKG_FILE" | cut -f1))${NC}"
echo
# ── Phase 2: Install and run ─────────────────────────────────────────────────
echo -e "${BOLD}=== Phase 2: Install and run ===${NC}"
docker run -d \
--name "$INSTALL_CONTAINER" \
-p "$PORT:8000" \
-v "$ARTIFACT_DIR:/pkg:ro" \
archlinux:latest bash -c '
set -euo pipefail
# Install the package (sysusers.d creates the remoteterm user, tmpfiles.d creates the data dir)
pacman -Syu --noconfirm >/dev/null 2>&1
pacman -U --noconfirm /pkg/*.pkg.tar.zst
# In a container there is no systemd to trigger sysusers/tmpfiles automatically,
# so run them manually.
systemd-sysusers
systemd-tmpfiles --create
echo "============================================"
echo " RemoteTerm installed — starting server"
echo "============================================"
# Run as the remoteterm service user, matching the systemd unit
exec su -s /bin/bash remoteterm -c "cd /opt/remoteterm-meshcore && exec .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000"
' >/dev/null
echo -e "${CYAN}Container:${NC} $INSTALL_CONTAINER"
echo -e "${CYAN}Listening:${NC} http://localhost:$PORT"
echo -e "${CYAN}Health: ${NC} http://localhost:$PORT/api/health"
echo
echo -e "${YELLOW}Streaming logs (Ctrl-C to stop and clean up)...${NC}"
echo
docker logs -f "$INSTALL_CONTAINER"
+3 -3
View File
@@ -35,7 +35,7 @@ SERIAL_HOST_PATH="/dev/ttyACM0"
SERIAL_COMPOSE_HOST_PATH="/dev/ttyACM0"
SERIAL_CONTAINER_PATH="/dev/meshcore-radio"
TCP_HOST=""
TCP_PORT="4000"
TCP_PORT="5000"
BLE_ADDRESS=""
BLE_PIN=""
ENABLE_BOTS="N"
@@ -311,8 +311,8 @@ case "$TRANSPORT_CHOICE" in
echo -e "${RED}TCP host is required.${NC}"
read -r -p "TCP host: " TCP_HOST
done
read -r -p "TCP port (default: 4000): " TCP_PORT
TCP_PORT="${TCP_PORT:-4000}"
read -r -p "TCP port (default: 5000): " TCP_PORT
TCP_PORT="${TCP_PORT:-5000}"
echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}"
;;
3)
+2 -2
View File
@@ -114,8 +114,8 @@ case "$TRANSPORT_CHOICE" in
echo -e "${RED}TCP host is required.${NC}"
read -rp "TCP host: " TCP_HOST
done
read -rp "TCP port (default: 4000): " TCP_PORT
TCP_PORT="${TCP_PORT:-4000}"
read -rp "TCP port (default: 5000): " TCP_PORT
TCP_PORT="${TCP_PORT:-5000}"
echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}"
;;
4)
+1 -1
View File
@@ -15,6 +15,6 @@ test.describe('Statistics page', () => {
await expect(page.locator('h4').getByText('Network')).toBeVisible({ timeout: 10_000 });
await expect(page.getByText('Contacts', { exact: true }).first()).toBeVisible();
await expect(page.getByText('Channels', { exact: true }).first()).toBeVisible();
await expect(page.locator('h4').getByText('Packets')).toBeVisible();
await expect(page.locator('h4').getByText('Packets', { exact: true })).toBeVisible();
});
});
Generated
+5 -5
View File
@@ -56,7 +56,7 @@ wheels = [
[[package]]
name = "apprise"
version = "1.9.7"
version = "1.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -67,9 +67,9 @@ dependencies = [
{ name = "requests-oauthlib" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/f5/97dc06b3401bb67abcef6e8bef7155f192b75795c2a2aa4d59eb5aa7fa66/apprise-1.9.7.tar.gz", hash = "sha256:2f73cc1e0264fb119fdb9b7cde82e8fde40a0f531ac885d8c6f0cf0f6e13aec2", size = 1937173 }
sdist = { url = "https://files.pythonhosted.org/packages/20/f4/be5c7e39b83a2285ab62ae7c19bb10704836f59c0a5b4c471730f54c9f98/apprise-1.9.9.tar.gz", hash = "sha256:fd622c0df16bdc79ed385539735573488cafe2405d25747e87eebd6b09b26012", size = 2032822 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/6b/cfa80a13437896eb8f4504ddac6dfa4ef7f1d2b2261057aa4a30003b8de6/apprise-1.9.7-py3-none-any.whl", hash = "sha256:c7640a81a1097685de66e0508e3da89f49235d566cb44bbead1dd98419bf5ee3", size = 1459879 },
{ url = "https://files.pythonhosted.org/packages/e6/2f/54d068d7e011a8b4e0aae3e93b09a30b33bcf780829fe70c6e8876aeb0e0/apprise-1.9.9-py3-none-any.whl", hash = "sha256:55ceb8827a1c783d683881c9f77fa42eb43b3fc91b854419c452d557101c7068", size = 1519940 },
]
[[package]]
@@ -983,7 +983,7 @@ wheels = [
[[package]]
name = "remoteterm-meshcore"
version = "3.9.0"
version = "3.10.0"
source = { virtual = "." }
dependencies = [
{ name = "aiomqtt" },
@@ -1022,7 +1022,7 @@ dev = [
requires-dist = [
{ name = "aiomqtt", specifier = ">=2.0" },
{ name = "aiosqlite", specifier = ">=0.19.0" },
{ name = "apprise", specifier = ">=1.9.7" },
{ name = "apprise", specifier = ">=1.9.8" },
{ name = "boto3", specifier = ">=1.38.0" },
{ name = "fastapi", specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.28.1" },