mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 02:53:00 +02:00
Post-merge cleanup, AGENTS.md work, unused endpoints, etc.
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -110,13 +110,13 @@ Raw packet handling uses two identities by design:
|
||||
|
||||
Frontend packet-feed consumers should treat `observation_id` as the dedup/render key, while `id` remains the storage reference.
|
||||
|
||||
## Repeater Advert Path Memory
|
||||
## Contact Advert Path Memory
|
||||
|
||||
To improve repeater disambiguation in the network visualizer, the backend stores recent unique advertisement paths per repeater in a dedicated table (`repeater_advert_paths`).
|
||||
To improve repeater disambiguation in the network visualizer, the backend stores recent unique advertisement paths per contact in a dedicated table (`contact_advert_paths`).
|
||||
|
||||
- This is independent of raw-packet payload deduplication.
|
||||
- Paths are keyed per repeater + path, with `heard_count`, `first_seen`, and `last_seen`.
|
||||
- Only the N most recent unique paths are retained per repeater (currently 10).
|
||||
- Paths are keyed per contact + path, with `heard_count`, `first_seen`, and `last_seen`.
|
||||
- Only the N most recent unique paths are retained per contact (currently 10).
|
||||
- See `frontend/src/components/AGENTS.md` § "Advert-Path Identity Hints" for how the visualizer consumes this data.
|
||||
|
||||
## Data Flow
|
||||
@@ -288,7 +288,7 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`).
|
||||
| POST | `/api/contacts/{key}/repeater/radio-settings` | Fetch radio settings via CLI |
|
||||
| POST | `/api/contacts/{key}/repeater/advert-intervals` | Fetch advert intervals |
|
||||
| POST | `/api/contacts/{key}/repeater/owner-info` | Fetch owner info |
|
||||
| POST | `/api/contacts/{key}/repeater/clock` | Fetch repeater clock |
|
||||
|
||||
| GET | `/api/channels` | List channels |
|
||||
| GET | `/api/channels/{key}` | Get channel by key |
|
||||
| POST | `/api/channels` | Create channel |
|
||||
|
||||
@@ -134,7 +134,6 @@ app/
|
||||
- `POST /contacts/{public_key}/repeater/radio-settings`
|
||||
- `POST /contacts/{public_key}/repeater/advert-intervals`
|
||||
- `POST /contacts/{public_key}/repeater/owner-info`
|
||||
- `POST /contacts/{public_key}/repeater/clock`
|
||||
|
||||
### Channels
|
||||
- `GET /channels`
|
||||
|
||||
@@ -276,12 +276,6 @@ class RepeaterOwnerInfoResponse(BaseModel):
|
||||
guest_password: str | None = Field(default=None, description="Guest password")
|
||||
|
||||
|
||||
class RepeaterClockResponse(BaseModel):
|
||||
"""Clock output from a repeater."""
|
||||
|
||||
clock_output: str | None = Field(default=None, description="Output from 'clock' command")
|
||||
|
||||
|
||||
class LppSensor(BaseModel):
|
||||
"""A single CayenneLPP sensor reading from req_telemetry_sync."""
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ from app.models import (
|
||||
NeighborInfo,
|
||||
RepeaterAclResponse,
|
||||
RepeaterAdvertIntervalsResponse,
|
||||
RepeaterClockResponse,
|
||||
RepeaterLoginRequest,
|
||||
RepeaterLoginResponse,
|
||||
RepeaterLppTelemetryResponse,
|
||||
@@ -481,21 +480,6 @@ async def repeater_owner_info(public_key: str) -> RepeaterOwnerInfoResponse:
|
||||
return RepeaterOwnerInfoResponse(**results)
|
||||
|
||||
|
||||
@router.post("/{public_key}/repeater/clock", response_model=RepeaterClockResponse)
|
||||
async def repeater_clock(public_key: str) -> RepeaterClockResponse:
|
||||
"""Fetch clock output from a repeater."""
|
||||
require_connected()
|
||||
contact = await _resolve_contact_or_404(public_key)
|
||||
_require_repeater(contact)
|
||||
|
||||
results = await _batch_cli_fetch(
|
||||
contact,
|
||||
"repeater_clock",
|
||||
[("clock", "clock_output")],
|
||||
)
|
||||
return RepeaterClockResponse(**results)
|
||||
|
||||
|
||||
@router.get("", response_model=list[Contact])
|
||||
async def list_contacts(
|
||||
limit: int = Query(default=100, ge=1, le=1000),
|
||||
|
||||
@@ -48,7 +48,10 @@ frontend/src/
|
||||
│ ├── contactAvatar.ts # Avatar color derivation from public key
|
||||
│ ├── rawPacketIdentity.ts # observation_id vs id dedup helpers
|
||||
│ ├── visualizerUtils.ts # 3D visualizer node types, colors, particles
|
||||
│ └── lastViewedConversation.ts # localStorage for last-viewed conversation
|
||||
│ ├── lastViewedConversation.ts # localStorage for last-viewed conversation
|
||||
│ ├── contactMerge.ts # Merge WS contact updates into list
|
||||
│ ├── localLabel.ts # Local label (text + color) in localStorage
|
||||
│ └── radioPresets.ts # LoRa radio preset configurations
|
||||
├── components/
|
||||
│ ├── StatusBar.tsx
|
||||
│ ├── Sidebar.tsx
|
||||
@@ -69,6 +72,7 @@ frontend/src/
|
||||
│ ├── ContactInfoPane.tsx # Contact detail sheet (stats, name history, paths)
|
||||
│ ├── RepeaterDashboard.tsx # Repeater pane-based dashboard (telemetry, neighbors, ACL, etc.)
|
||||
│ ├── RepeaterLogin.tsx # Repeater login form (password + guest)
|
||||
│ ├── NeighborsMiniMap.tsx # Leaflet mini-map for repeater neighbor locations
|
||||
│ └── ui/ # shadcn/ui primitives
|
||||
├── types/
|
||||
│ └── d3-force-3d.d.ts # Type declarations for d3-force-3d
|
||||
@@ -86,7 +90,10 @@ frontend/src/
|
||||
├── radioPresets.test.ts
|
||||
├── rawPacketIdentity.test.ts
|
||||
├── repeaterDashboard.test.tsx
|
||||
├── repeaterMode.test.ts
|
||||
├── repeaterFormatters.test.ts
|
||||
├── repeaterLogin.test.tsx
|
||||
├── repeaterMessageParsing.test.ts
|
||||
├── localLabel.test.ts
|
||||
├── settingsModal.test.tsx
|
||||
├── sidebar.test.tsx
|
||||
├── unreadCounts.test.ts
|
||||
@@ -200,13 +207,30 @@ LocalStorage migration helpers for favorites; canonical favorites are server-sid
|
||||
- `last_advert_time`
|
||||
- `bots`
|
||||
|
||||
## Contact Info Pane
|
||||
|
||||
Clicking a contact's avatar in `ChatHeader` or `MessageList` opens a `ContactInfoPane` sheet (right drawer) showing comprehensive contact details fetched from `GET /api/contacts/{key}/detail`:
|
||||
|
||||
- Header: avatar, name, public key, type badge, on-radio badge
|
||||
- Info grid: last seen, first heard, last contacted, distance, hops
|
||||
- GPS location (clickable → map)
|
||||
- Favorite toggle
|
||||
- Name history ("Also Known As") — shown only when the contact has used multiple names
|
||||
- Message stats: DM count, channel message count
|
||||
- Most active rooms (clickable → navigate to channel)
|
||||
- Advert observation rate
|
||||
- Nearest repeaters (resolved from first-hop path prefixes)
|
||||
- Recent advert paths
|
||||
|
||||
State: `infoPaneContactKey` in App.tsx controls open/close. Live contact data from WebSocket updates is preferred over the initial detail snapshot.
|
||||
|
||||
## Repeater Dashboard
|
||||
|
||||
For repeater contacts (`type=2`), App.tsx renders `RepeaterDashboard` instead of the normal chat UI (ChatHeader + MessageList + MessageInput).
|
||||
|
||||
**Login**: `RepeaterLogin` component — password or guest login via `POST /api/contacts/{key}/repeater/login`.
|
||||
|
||||
**Dashboard panes** (after login): Telemetry, Neighbors, ACL, Radio Settings, Advert Intervals, Owner Info, Clock — each fetched via granular `POST /api/contacts/{key}/repeater/{pane}` endpoints. Panes retry up to 3 times client-side. "Load All" fetches all panes serially (parallel would queue behind the radio lock).
|
||||
**Dashboard panes** (after login): Telemetry, Neighbors, ACL, Radio Settings, Advert Intervals, Owner Info — each fetched via granular `POST /api/contacts/{key}/repeater/{pane}` endpoints. Panes retry up to 3 times client-side. "Load All" fetches all panes serially (parallel would queue behind the radio lock).
|
||||
|
||||
**Actions pane**: Send Advert, Sync Clock, Reboot — all send CLI commands via `POST /api/contacts/{key}/command`.
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ When only a 1-byte prefix is known (from packet path bytes), the node is marked
|
||||
|
||||
**Problem:** When multiple repeaters share a 1-byte prefix, the visualizer can't tell which physical repeater a path hop refers to.
|
||||
|
||||
**Solution:** The backend tracks recent unique advertisement paths per repeater in `repeater_advert_paths` (see root `AGENTS.md` § "Repeater Advert Path Memory"). On mount (and when new contacts appear), the visualizer fetches this data via `GET /api/contacts/repeaters/advert-paths` and builds an index keyed by 12-char prefix.
|
||||
**Solution:** The backend tracks recent unique advertisement paths per contact in `contact_advert_paths` (see root `AGENTS.md` § "Contact Advert Path Memory"). On mount (and when new contacts appear), the visualizer fetches this data via `GET /api/contacts/repeaters/advert-paths` and builds an index keyed by 12-char prefix.
|
||||
|
||||
**Scoring:** `pickLikelyRepeaterByAdvertPath(candidates, nextPrefix)` scores each candidate repeater by how often its stored advert paths' `next_hop` matches the packet's actual next-hop prefix. It requires:
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ async def _insert_contact(public_key=KEY_A, name="Alice", on_radio=False, **over
|
||||
"last_seen": None,
|
||||
"on_radio": on_radio,
|
||||
"last_contacted": None,
|
||||
"first_seen": None,
|
||||
}
|
||||
data.update(overrides)
|
||||
await ContactRepository.upsert(data)
|
||||
|
||||
@@ -15,7 +15,6 @@ from app.routers.contacts import (
|
||||
_fetch_repeater_response,
|
||||
repeater_acl,
|
||||
repeater_advert_intervals,
|
||||
repeater_clock,
|
||||
repeater_login,
|
||||
repeater_lpp_telemetry,
|
||||
repeater_neighbors,
|
||||
@@ -84,6 +83,7 @@ async def _insert_contact(public_key: str, name: str = "Node", contact_type: int
|
||||
"last_seen": None,
|
||||
"on_radio": False,
|
||||
"last_contacted": None,
|
||||
"first_seen": None,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1057,45 +1057,6 @@ class TestRepeaterOwnerInfo:
|
||||
assert response.guest_password is None
|
||||
|
||||
|
||||
class TestRepeaterClock:
|
||||
@pytest.mark.asyncio
|
||||
async def test_success(self, test_db):
|
||||
mc = _mock_mc()
|
||||
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
|
||||
|
||||
mc.commands.get_msg = AsyncMock(
|
||||
return_value=_radio_result(
|
||||
EventType.CONTACT_MSG_RECV,
|
||||
{"pubkey_prefix": KEY_A[:12], "text": "2026-02-25 12:00:00 UTC", "txt_type": 1},
|
||||
)
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.routers.contacts.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch(_MONOTONIC, side_effect=_advancing_clock()),
|
||||
):
|
||||
response = await repeater_clock(KEY_A)
|
||||
|
||||
assert response.clock_output == "2026-02-25 12:00:00 UTC"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_returns_none(self, test_db):
|
||||
mc = _mock_mc()
|
||||
await _insert_contact(KEY_A, name="Repeater", contact_type=2)
|
||||
mc.commands.get_msg = AsyncMock(return_value=_radio_result(EventType.NO_MORE_MSGS))
|
||||
|
||||
with (
|
||||
patch("app.routers.contacts.require_connected", return_value=mc),
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch(_MONOTONIC, side_effect=[0.0, 5.0, 11.0]),
|
||||
patch("app.routers.contacts.asyncio.sleep", new_callable=AsyncMock),
|
||||
):
|
||||
response = await repeater_clock(KEY_A)
|
||||
|
||||
assert response.clock_output is None
|
||||
|
||||
|
||||
def _make_contact(
|
||||
public_key: str = KEY_A, name: str = "Repeater", contact_type: int = 2
|
||||
) -> Contact:
|
||||
|
||||
Reference in New Issue
Block a user