Commit Graph

653 Commits

Author SHA1 Message Date
Louis King 21dcbbc56f Add PostgreSQL migration plan
Planning doc for adding Postgres support: code compatibility fixes,
a postgres container, component-based connection config, and a
SQLAlchemy ORM-based migration command for existing SQLite databases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:13:46 +01:00
JingleManSweep 6417ed2ae2 Merge pull request #242 from ipnet-mesh/fix/observer-filter-junction-table
Fix observed_by filter to use event_observers junction table
v0.13.0
2026-06-13 20:13:04 +01:00
Louis King 888e193e09 Fix observed_by filter to use event_observers junction table
The observed_by filter on messages, advertisements, telemetry, and
trace_paths matched only the first observer (stored in observer_node_id),
silently excluding events whose secondary observers appear only in the
event_observers junction table. This caused filtered lists to appear
'several hours behind' when a dominant observer consistently won the
first-insert race for recent events.

Replace the ObserverNode.public_key predicate with an IN subquery against
the event_observers junction table (the canonical multi-observer source
already used for display). Add a shared observed_by_filter_clause() helper
in observer_utils.py to avoid duplication across all four routes.

Add regression tests proving a secondary observer (present only in
event_observers) sees events via the filter. Update existing fixtures and
inline test data to seed event_hash and EventObserver rows.

Fixes #239
2026-06-13 19:27:51 +01:00
JingleManSweep a4d9513185 Merge pull request #241 from ipnet-mesh/chore/update-agent-instructions
Updated Agent instructions
2026-06-13 18:14:52 +01:00
Louis King d674171342 Updated Agent instructions 2026-06-13 18:12:08 +01:00
JingleManSweep dd75b658b3 Merge pull request #240 from ipnet-mesh/fix/packet-group-path-badges
Packet-detail page with path-hash node lookup
2026-06-13 16:50:14 +01:00
Louis King 87e7d7676b Link rows to packet-detail page with path-hash node lookup
Adverts/Messages rows now link directly to the deduplicated packet-detail
page (/packets/hash/:hash). Each path hop renders as a clickable badge
opening a popover that looks up nodes by public-key prefix via the new
pubkey_prefix query param on GET /api/v1/nodes (case-insensitive
startswith). Adds a derived path_hash_bytes field on GroupedPacketRead.

Defaults changed: FEATURE_PACKETS now defaults to true and
RAW_PACKET_RETENTION_DAYS to 7 (independent of DATA_RETENTION_DAYS).

Fixes a mypy arg-type error by explicitly annotating the packet_hash
list as list[str].
2026-06-13 16:47:22 +01:00
Louis King 81c6b3e989 Render observer path as hop badges and fix path-hash extraction
The packet-group detail observer table only ever showed the hop count
because _extract_path_hashes looked at decoded.payload.decoded.pathHashes,
but normal packets carry the routing path at the top level as decoded.path.
Read decoded.path first (falling back to the old pathHashes location).

Frontend: render each hop as its own badge joined by arrows, wrapping on
narrow screens, with centre-truncation for paths over 16 hops. Badges carry
a path-hash-badge class + data-path-hash hook for a future node lookup.

Also expose v1/packet-groups as an open endpoint in the web proxy mapping
(it is not a prefix of v1/packets) and rebase the packet_hash index
migration onto the current head.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:07:47 +01:00
JingleManSweep 740b04d754 Merge pull request #238 from ipnet-mesh/claude/packet-dedup-research-3akvfn
Add packet-groups API endpoint for deduplicated packet list
2026-06-13 09:18:42 +01:00
Claude 191cfeafd8 Fix black formatting on routes/__init__.py
https://claude.ai/code/session_01NH2rZzuHzasJj12SZeRhbJ
2026-06-13 08:14:38 +00:00
Claude d5082626ca Fix lint and add tests for packet_groups endpoint
- Fix black formatting on packet_groups.py and raw_packets.py
- Fix mypy error: annotate sort_exprs dict as dict[str, Any] so
  asc()/desc() receive a typed expression, not object
- Add test_packet_groups.py with 33 tests covering list/detail
  endpoints, filters, redaction, observer hydration, path_hashes
  extraction, and the _extract_path_hashes helper (98% line coverage)

https://claude.ai/code/session_01NH2rZzuHzasJj12SZeRhbJ
2026-06-13 08:10:40 +00:00
Claude 7c7f8b83d3 Add frontend for deduplicated packet groups
- packets.js: switch to /api/v1/packet-groups, replace observer cell
  with reception badge (paths × observers), remove SNR/hops columns
  and filters, navigate to /packets/hash/:hash on click
- packet-group-detail.js: new detail page showing all (observer, path)
  receptions grouped by observer with path hash sequences
- app.js: register packetGroupDetail page and /packets/hash/:hash route

https://claude.ai/code/session_01NH2rZzuHzasJj12SZeRhbJ
2026-06-13 07:54:07 +00:00
Claude e3e7cb26da Add packet-groups API endpoint for deduplicated packet list
- New GroupedPacketRead/PacketReceptionInfo/GroupedPacketList schemas
- GET /api/v1/packet-groups: two-phase GROUP BY query, 7-day default
  window, lightweight Phase 2 (no raw_hex/decoded), role-aware cache
- GET /api/v1/packet-groups/{hash}: full reception list with path_hashes
  extracted from decoded.payload.decoded.pathHashes
- New (packet_hash, received_at) composite index via Alembic migration
- i18n keys for receptions, path, observer counts

https://claude.ai/code/session_01NH2rZzuHzasJj12SZeRhbJ
2026-06-13 07:48:40 +00:00
JingleManSweep d12e209b8a Merge pull request #235 from ipnet-mesh/renovate/esbuild-0.x-lockfile
chore(deps): update dependency esbuild to v0.28.1
2026-06-12 23:16:59 +01:00
JingleManSweep fa5decde46 Merge branch 'main' into renovate/esbuild-0.x-lockfile 2026-06-12 23:14:26 +01:00
JingleManSweep 16f406728a Merge pull request #236 from ipnet-mesh/renovate/tailwindcss-monorepo
Update tailwindcss monorepo to v4.3.1
2026-06-12 23:14:07 +01:00
renovate[bot] ad3bc782d7 chore(deps): update dependency esbuild to v0.28.1 2026-06-12 22:12:22 +00:00
JingleManSweep 0751df80e1 Merge branch 'main' into renovate/tailwindcss-monorepo 2026-06-12 23:11:30 +01:00
JingleManSweep 8538f5e430 Merge pull request #237 from ipnet-mesh/feat/raw-packets
feat: Raw Packets — capture, browse, classify, and link wire packets (v0.13.0)
2026-06-12 23:10:46 +01:00
Louis King b5b6872060 test: raise patch coverage for raw packets
Add tests covering the previously-uncovered new lines flagged by Codecov:
- /packets route: search, packet_type, channel_idx, route_type, observed_by,
  decryptable (both), max_snr, path-len ranges, since/until, observer-tag
  hydration, detail 404, ascending sort, and the role-aware cache key builder.
- store_raw_packet: existing-observer update branch, senderPublicKey source
  fallback, and the no-source-prefix case.
- normalizer: advertisement payload carries the wire packet_hash.
- CollectorSettings: raw-packet retention default/override and capture default.

Also stub the test decoder via setattr so mypy's method-assign analysis is
deterministic across incremental-cache states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:07:43 +01:00
JingleManSweep 725dcd9518 Merge branch 'main' into feat/raw-packets 2026-06-12 22:45:04 +01:00
Louis King 76f3dfa7eb feat: raw packet capture, browse, and classification (v0.13.0)
Add a first-class Raw Packets feature that captures every inbound MeshCore
packet from the LetsMesh `packets` feed exactly as received, independent of
how the collector later classifies it.

Capture & storage
- New `RawPacket` model + migration (raw_packets table) with single and
  composite indexes for the dominant filter-then-sort-by-newest queries.
- Collector-side `RAW_PACKET_CAPTURE_ENABLED` flag (default off); capture hook
  reuses the decoder's per-hex cache (no second decode), one row per observer
  reception, never blocks event dispatch.
- Separate `RAW_PACKET_RETENTION_DAYS` (falls back to DATA_RETENTION_DAYS);
  cleanup runs regardless of capture so disabling drains the table. Raw-packet
  observers retained in the is_observer recompute union.

API
- `GET /packets` and `/packets/{id}` with rich filtering, role-aware Redis
  cache key, and channel-visibility redaction (restricted-channel packets are
  returned metadata-only, not hidden, so pagination counts stay stable).

Web
- `FEATURE_PACKETS` flag (default off). Responsive Packets page (table desktop,
  cards mobile) plus a Packet Detail page (breadcrumb nav, raw hex + decoded).
- Nav entry after Messages on all three surfaces; home.js reordered so Map
  precedes Members; new packets icon + colour.

Finer-grained classification
- Replace the single `letsmesh_packet` catch-all with per-payload-type event
  types (req, ack, encrypted_direct, encrypted_channel, grp_data, multipart,
  control, raw_custom, ...); letsmesh_packet kept only as the unresolved-type
  safety net.

Link from structured tables
- Add `packet_hash` to advertisements and messages (populated at ingest);
  exact `packet_hash` filter on /packets; cube-icon link on the Adverts and
  Messages lists -> /packets?packet_hash=<hash>, shown only when the feature is
  on and the row has a stored hash.

Docs/config: .env.example, docker-compose (collector + web), AGENTS.md,
SCHEMAS.md, docs/letsmesh.md, docs/upgrading.md (## v0.13.0), en/nl i18n, and a
plan/tasks doc under docs/plans/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:40:31 +01:00
renovate[bot] 17d56799cd Update tailwindcss monorepo to v4.3.1 2026-06-12 19:13:24 +00:00
JingleManSweep 015cb25eab Merge pull request #234 from ipnet-mesh/fix/profile-signal-tdz
fix(web): resolve TDZ error on profile page from shadowed signal var
v0.12.3
2026-06-11 23:16:41 +01:00
Louis King f8ce554d24 fix(web): resolve TDZ error on profile page from shadowed signal var
The profile page failed to render with "Cannot access 'h' before
initialization". The /profile/me branch destructured `signal` from
params and used it in the apiGet call, but later redeclared
`const signal = ac.signal` in the same block scope. Block-scoped const
hoisting put the earlier reference in the temporal dead zone.

Drop the redundant inner declaration and pass `ac.signal` directly to
the form submit listener.

Fixes #233

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:10:39 +01:00
JingleManSweep deb5af508c Merge pull request #232 from ipnet-mesh/fix/profile-adopted-node-last-seen
fix(web): show node last_seen instead of adopted_at on profile
2026-06-11 16:59:37 +01:00
Louis King 620747baa3 fix(web): show node last_seen instead of adopted_at on profile
The User Profile page rendered each adopted node's relative time from
adopted_at (the adoption date) rather than the node's most recent
activity, so it always showed the time since adoption (e.g. "35 days
ago") even when the node had advertised today.

Expose last_seen on AdoptedNodeRead and render it on the profile page,
falling back to "-" when null (matching the nodes/node-detail pages).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:48:32 +01:00
JingleManSweep 6c9a07e4c8 Merge pull request #231 from ipnet-mesh/perf/dashboard-stats-and-spa-cancellation
perf(web): cancel in-flight requests on navigation; consolidate dashboard stats
v0.12.2
2026-06-11 13:32:47 +01:00
Louis King 6804fc0b99 perf(web): cancel in-flight requests on navigation; consolidate dashboard stats
Fix dashboard pages stalling under rapid navigation, plus reduce the cost
of the heaviest dashboard endpoint.

SPA request cancellation: apiGet never passed an AbortSignal, so navigating
away left a page's in-flight requests running — the homepage alone fires
three (/stats + two charts), the slowest being /stats. Under rapid
navigation these piled up, holding browser connections and API threadpool
threads, so the page actually wanted queued behind stale work; a late
resolver could also clobber the new page's DOM.

  - api.js: apiGet accepts an optional { signal } and forwards it to fetch;
    export isAbortError().
  - router.js: each navigation gets an AbortController; the previous one is
    aborted at the start of _handleRoute and its signal is passed to the page
    handler. A navigation-generation guard stops a superseded route from
    hiding the loader for the page that replaced it.
  - app.js: pageHandler swallows AbortError (an intentional cancel is not an
    error).
  - all 11 page modules: thread params.signal into on-load apiGet calls and
    guard their catch blocks with isAbortError.

dashboard/stats consolidation: collapse the 11 sequential COUNT(*) queries
into 4 using portable conditional aggregation (func.sum(case(...))) for
nodes, messages, advertisements, and user profiles. Responses are
unchanged.

Docs: extend the v0.12 "Read-Path Query Optimisations" note and add a
"Dashboard Navigation Responsiveness" note (front-end only, no action
required).

Tests: add test_stats_time_bucket_counts asserting the active/today/24h/7d
buckets. SPA bundles are gitignored and rebuilt by the Docker/CI build, so
only committed source changed; the esbuild build was run locally to
validate the JS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:29:53 +01:00
JingleManSweep 1f43ba3607 Merge pull request #230 from ipnet-mesh/feat/api-workers
feat(api): configurable worker processes via API_WORKERS
v0.12.1
2026-06-11 12:22:06 +01:00
Louis King ce4f0da205 test(api): cover --workers CLI branch and factory metrics path
Raise diff coverage above target by exercising the previously untested
lines:

- test_cli.py: invoke the `api` command with uvicorn.run mocked to assert
  the default path passes the app object (no workers/factory) and that
  --workers / API_WORKERS launches the env factory by import string with
  the requested worker count and factory=True.
- test_app_factory.py: add METRICS_ENABLED true/false cases that toggle
  the /metrics route, covering the env-bool parsing branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 12:17:40 +01:00
Louis King 03603a83e2 feat(api): configurable worker processes via API_WORKERS
Add a --workers / API_WORKERS option so the API can run multiple worker
processes in a single container for multi-core concurrency, without
needing to scale containers (which would conflict with the api service's
fixed container_name and complicate per-stack ops/monitoring).

The existing create_app() carries hardcoded defaults (sqlite:///./
meshcore.db, Redis off), so forked workers cannot use it — they would
open the wrong database and run without caching. Add an env-driven
factory, create_app_from_env(), that rebuilds the app from APISettings
plus the CLI-only env vars (CORS_ORIGINS, METRICS_ENABLED,
METRICS_CACHE_TTL), mirroring the single-process resolution. workers > 1
runs uvicorn against this factory via an import string; workers == 1
keeps the single-process object path so local CLI flags still apply.

Wire API_WORKERS into the api compose service (default 1, unchanged
behaviour) and document it in the README (new "Scaling the API"
section + env table) and the v0.12 upgrading notes, including the
SQLite single-host caveat and the env-vs-CLI-flag note for workers.

Tests: create_app_from_env reads DB/Redis/MQTT/metrics from env,
honours explicit overrides, and derives the collector DB path from
DATA_HOME rather than the bare create_app default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 12:09:05 +01:00
JingleManSweep e316b656fc Merge pull request #229 from ipnet-mesh/perf/batch-dashboard-sender-queries
perf(api): batch N+1 dashboard and message sender queries
2026-06-11 11:33:32 +01:00
Louis King e1199a42cd perf(api): batch N+1 dashboard and message sender queries
Two read-only query optimisations, no schema changes.

node-count history: replace the per-day COUNT(*) loop (up to 90 full
scans of the unindexed created_at column) with two queries — a baseline
count of nodes created before the window plus one GROUP BY date()
aggregate, accumulated into the running total in Python. Results are
identical; the baseline seed keeps pre-window nodes counted from day 0.

sender-name resolution: add resolve_sender_names() to observer_utils,
batching all pubkey prefixes into two queries (names + name tags) via an
OR of indexable LIKE 'prefix%' terms instead of two queries per prefix.
Wire it into list_messages (was ~2xN per page) and the dashboard
channel-messages loop (nested per channel x per prefix). The dashboard
recent-ads block already batches on full public keys via IN(), so it is
left as-is.

Tests: add cumulative+baseline correctness for node-count and a
multi-sender batched-resolution case for messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 11:25:01 +01:00
JingleManSweep fc8893a8bd Merge pull request #227 from ipnet-mesh/renovate/redis-8.x
Update redis Docker tag to v8
v0.12.0
2026-06-11 10:12:33 +01:00
renovate[bot] 33dc861a1a Update redis Docker tag to v8 2026-06-10 22:39:37 +00:00
JingleManSweep b9168d0ce5 Merge pull request #228 from ipnet-mesh/perf/threadpool-sqlite-concurrency
perf(api): threadpool handlers, SQLite concurrency tuning, precomputed is_observer
2026-06-10 23:38:41 +01:00
Louis King 38a57f4cd4 perf(api): run handlers in threadpool, tune SQLite, precompute is_observer
Three related performance fixes for slow (>500ms) API responses.

1. Stop blocking the event loop. Route handlers were declared `async def`
   but ran synchronous SQLAlchemy queries (and synchronous Redis calls via
   the cache decorator) directly on the event loop, serializing requests.
   Convert all handlers to sync `def` so FastAPI runs them in its
   threadpool, and make the `@cached` decorator dual-mode (sync wrapper for
   sync handlers, async wrapper preserved for a future async/Postgres path).

2. Tune SQLite for concurrency. Enable WAL, busy_timeout and
   synchronous=NORMAL on every connection, and size the pool above the
   threadpool so handlers don't wait on connections. In-memory SQLite is
   guarded (no overflow-pool kwargs).

3. Precompute an indexed `nodes.is_observer` flag. The `observer=true`
   filter scanned ~68k+ event rows to find a handful of observers (the
   advertisements page calls it with limit=500). Replace the 5-way OR of
   subqueries with `WHERE nodes.is_observer = ?`. The collector sets the
   flag on first observation (in add_event_observer); the cleanup job
   clears it once a node's events are all pruned; a migration adds the
   column/index and backfills from the existing union.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:35:39 +01:00
JingleManSweep 1f4445e875 Merge pull request #226 from ipnet-mesh/feat/redis-caching
feat: add optional Redis caching layer for API endpoints
2026-06-10 21:58:29 +01:00
Louis King f30786c95a Tweaked upgrading docs 2026-06-10 21:51:58 +01:00
Louis King fb435a53c4 test: add coverage for Redis caching layer
Add 29 new tests covering previously uncovered code paths:
- redis.py: delete(), close(), string value decoding
- cache.py: serialization branches (pydantic, dict, list), set error fallback
- app.py: lifespan Redis init (enabled/disabled), cache close on shutdown
- app.py: X-Cache middleware (HIT/MISS/no-status)
- app.py: /health/ready Redis status (connected/unreachable/omitted)
- cli.py: Redis banner output, create_app param passthrough
- Route key builders: dashboard/stats, dashboard/message-activity,
  channels, messages
2026-06-09 23:30:29 +01:00
Louis King 385d1ab141 feat: add optional Redis caching layer for API endpoints
Add Redis-backed response caching for read-heavy API endpoints (nodes,
advertisements, messages, channels, dashboard, profiles) with configurable
TTL, key prefix isolation, and graceful fallback when Redis is unavailable.

New files:
- common/redis.py: CacheBackend, NullCache, RedisCacheBackend
- api/cache.py: @cached decorator, sorted_query_string helper
- tests/test_api/test_cache.py: 23 unit tests

Changes:
- pyproject.toml: add redis[hiredis] dependency
- common/config.py: 8 Redis settings on APISettings
- api/cli.py: Redis Click options + startup banner
- api/app.py: Redis lifespan init/cleanup, X-Cache middleware, health check
- 6 route files: apply @cached decorator to list endpoints
- docker-compose.yml: Redis service (cache profile), env vars
- docker-compose.dev.yml: Redis port exposure
- .env.example, README.md, AGENTS.md, docs/upgrading.md: documentation

Redis is disabled by default (REDIS_ENABLED=false). Enable with
--profile cache and REDIS_ENABLED=true.
2026-06-09 23:08:49 +01:00
JingleManSweep a4419a8987 Merge pull request #223 from ipnet-mesh/renovate/daisyui-5.x-lockfile
Update dependency daisyui to v5.5.23
v0.11.1
2026-06-07 14:53:18 +01:00
renovate[bot] fc3a11ece3 Update dependency daisyui to v5.5.23 2026-06-07 13:48:51 +00:00
JingleManSweep cdb383fb2b Merge pull request #224 from ipnet-mesh/renovate/codecov-codecov-action-7.x
Update codecov/codecov-action action to v7
2026-06-07 14:48:10 +01:00
renovate[bot] b685333790 Update codecov/codecov-action action to v7 2026-06-07 13:45:33 +00:00
JingleManSweep 673d8c11c5 Merge pull request #225 from ipnet-mesh/feat/expand-radio-config-vars
Split NETWORK_RADIO_CONFIG into individual env vars and add FEATURE_R…
2026-06-07 14:44:43 +01:00
Louis King 6fd93aaa07 Add test for radio config settings fallback coverage
Exercise the else branches in create_app() where radio params
fall back to WebSettings defaults when passed as None.
2026-06-07 14:41:18 +01:00
Louis King f7d9901c9b Split NETWORK_RADIO_CONFIG into individual env vars and add FEATURE_RADIO_CONFIG flag
- Replace single NETWORK_RADIO_CONFIG comma-delimited string with six
  individual environment variables: NETWORK_RADIO_PROFILE, _FREQUENCY,
  _BANDWIDTH, _SPREADING_FACTOR, _CODING_RATE, _TX_POWER
- Radio config fields now use raw numeric types (float/int) with units
  applied dynamically via RadioConfig.format_for_display()
- Add FEATURE_RADIO_CONFIG feature flag to control radio config panel
  visibility on the home page (default: enabled)
- Remove from_config_string class method (no backwards compatibility)
- Update Click CLI options, create_app() signature, and _build_config_json()
- Update docker-compose.yml, .env.example, README.md, AGENTS.md
- Add upgrading.md v0.12.0 section with migration instructions
- Add test coverage for schema, config, and feature flag
2026-06-07 14:35:40 +01:00
JingleManSweep 187b0b7ce1 Merge pull request #222 from ipnet-mesh/feat/channel-model
Add database-backed channels with role-based visibility and web dashboard
v0.11.0
2026-06-04 14:39:55 +01:00