678 Commits

Author SHA1 Message Date
JingleManSweep 7f19d47aa0 Merge pull request #255 from ipnet-mesh/fix/observer-filter-cache-key-collision
fix: forward/cache repeated query params (observer filters)
v0.13.4
2026-06-14 23:32:47 +01:00
Louis King fd55968b39 fix(web): forward repeated query params through the API proxy
The SPA reaches the backend via the web api_proxy, which forwarded query
params using dict(request.query_params). dict() on Starlette's QueryParams
multidict keeps only the last value of a repeated key, so a multi-valued
filter like ?observed_by=A&observed_by=B was forwarded to the backend as
observed_by=B only. The backend then filtered to B's events, making a
message observed only by A disappear as soon as B was also selected — the
reported "filters act like AND" symptom. This happens independently of the
Redis response cache.

Forward request.query_params.multi_items() (a list of (key, value) tuples)
so all repeated values reach the backend. Add web proxy regression tests
asserting both observed_by values are forwarded, and capture forwarded
params in the MockHttpClient.

This is the primary fix; the earlier cache-key change (multi_items in
sorted_query_string) addressed the same collapse pattern at the cache layer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:30:35 +01:00
Louis King b9083d6bb7 fix(cache): preserve repeated query params in cache key
The API response cache key was built from request.query_params.items(),
which in Starlette keeps only the last value of a repeated key. Repeated
params like observed_by (?observed_by=A&observed_by=B) therefore collapsed
to observed_by=B in the key, colliding with any other filter set sharing the
same last value. The first response cached under that key was then served
for the whole TTL, making observer-filtered Messages/Adverts return stale,
wrong results (e.g. an A-only message vanishing when B was also enabled).

Use multi_items() so all repeated values are included, sorted by the full
(key, value) tuple so order-independent filter sets map to one key. Add
regression tests covering preservation, order-independence, and the
{A,B} vs {B} collision case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:25:15 +01:00
JingleManSweep 9dc494ec54 Merge pull request #253 from ipnet-mesh/renovate/actions-cache-5.x
chore(deps): update actions/cache action to v5
2026-06-14 22:25:48 +01:00
renovate[bot] 1a94da9f32 chore(deps): update actions/cache action to v5 2026-06-14 21:23:48 +00:00
JingleManSweep 62fa83b9e0 Merge pull request #254 from ipnet-mesh/fix/dockerfile-oci-source-label
fix(docker): correct OCI source label to ipnet-mesh/meshcore-hub
2026-06-14 22:22:57 +01:00
Louis King 0abee2f4e5 fix(docker): correct OCI source label to ipnet-mesh/meshcore-hub
The org.opencontainers.image.source label was pointing at
meshcore-dev/meshcore-hub (the upstream MeshCore project's org)
instead of this repo's canonical location at ipnet-mesh/meshcore-hub.
Verify via: docker inspect ghcr.io/ipnet-mesh/meshcore-hub:main
2026-06-14 22:20:58 +01:00
JingleManSweep c8547a7720 Merge pull request #252 from ipnet-mesh/chore/ci-workflow-optimisations
chore(ci): optimise GitHub workflows
2026-06-14 22:19:03 +01:00
Louis King 5866428f69 chore(ci): optimise GitHub workflows
Add concurrency (PR-cancel only), dependency caching, timeouts, and
path filters across all four workflows. Pin opencode action to v1.17.7
and tighten the /oc trigger. Skip MQTT broker rebuild when upstream
SHA is unchanged. Gate sdist/wheel build job to main-only pushes.
2026-06-14 22:16:57 +01:00
JingleManSweep bd8d62fa9f Merge pull request #251 from ipnet-mesh/chore/faster-pytest
chore(tests): speed up pytest from >2min to ~12s
2026-06-14 22:06:02 +01:00
Louis King 96a78d79f6 chore(tests): speed up pytest from >2min to ~12s
- Default-off coverage in pyproject.toml addopts; opt-in via make test-cov
- Add pytest-xdist for parallel execution (make test = pytest -nauto --no-cov)
- Promote API test fixtures to session/module scope (engine, app, mocks);
  per-test isolation via table truncation instead of schema rebuild
- Remove Makefile include .env/export that leaked config vars into tests;
  docker-compose reads .env natively
- Add _ignore_dotenv autouse fixture: disables env_file, clears leaked env
  vars from Settings fields and Click CLI envvars
- Patch time.sleep in 3 subscriber scheduler tests (~3s -> ~0.03s)
- Fix pytest.raises(Exception, match='') warning -> IntegrityError
- Add .venv activation to .envrc
- Suppress warn_unused_ignores for tests in mypy config (single-file
  pre-commit checks lack full-project context)
2026-06-14 22:03:30 +01:00
JingleManSweep 6c85ea04c0 Merge pull request #250 from ipnet-mesh/chore/nix-support
Added nix-shell support
2026-06-14 20:37:32 +01:00
Louis King 3f6c9bceac Added nix-shell support 2026-06-14 20:27:52 +01:00
JingleManSweep ab1cd6e541 Merge pull request #249 from ipnet-mesh/fix/collapse-message-newlines
fix(web): collapse newlines in rendered message text
v0.13.3
2026-06-14 19:59:03 +01:00
Louis King 434d78e24a fix(web): collapse newlines in rendered message text
Messages containing runs of newlines were preserved by the pre-wrap
styling on the messages view, stretching table rows and cards. Collapse
any run of newlines (and surrounding whitespace) into a single space at
display time, leaving stored text untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:56:40 +01:00
JingleManSweep adc7f77dfc Merge pull request #247 from ipnet-mesh/chore/python-requires
Updated Python Requirements
v0.13.2
2026-06-14 18:47:03 +01:00
Louis King 13ab4682ed Updates 2026-06-14 18:42:55 +01:00
JingleManSweep 6c4c98236d Merge pull request #245 from ipnet-mesh/feat/system-announcement-maintenance
feat(web): system announcement banner and maintenance mode
2026-06-14 18:37:03 +01:00
Louis King 5015946ab5 fix(deps): pin fastapi below 0.137.0 to fix CI route introspection
FastAPI 0.137.0 regressed include_router: endpoints registered via
app.include_router no longer appear in app.routes (they still serve, but
introspection breaks). tests/test_api/test_app_factory.py asserts on
app.routes, so CI (which pip-installs the latest fastapi) failed on the
/metrics route check while local pinned 0.136.3 passed. Constrain fastapi
to <0.137.0 until a fixed release is available.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:34:51 +01:00
Louis King 2563ad4cf2 test(web): isolate SYSTEM_ANNOUNCEMENT/SYSTEM_MAINTENANCE from local .env
The web_app fixture already neutralizes NETWORK_ANNOUNCEMENT so a developer's
local .env does not leak into tests. Do the same for the two new system
settings, fixing test_system_banner_absent_when_none when SYSTEM_ANNOUNCEMENT
is set in the environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:55:17 +01:00
Louis King 22a5ed26d5 fix(web): center the system announcement banner like the network banner
Apply the #flash-banner flex centering rule to #system-banner so it matches
the network announcement layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:48:31 +01:00
Louis King 413e3f7e7b docs: add v0.13.0 upgrade notes for system announcement and maintenance mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:47:55 +01:00
Louis King ebad9013d3 feat(web): pass SYSTEM_ANNOUNCEMENT/SYSTEM_MAINTENANCE through docker-compose
Wire the two new web settings into the web service env block, matching the
NETWORK_ANNOUNCEMENT passthrough. SYSTEM_MAINTENANCE defaults to false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:47:26 +01:00
Louis King 17e6b65f8c feat(web): add system announcement banner and maintenance mode
Add two operator-controlled, startup-time settings:

- SYSTEM_ANNOUNCEMENT: non-dismissable Markdown banner rendered above the
  network announcement on every page (navbar -> system -> network).
- SYSTEM_MAINTENANCE: when enabled, forces all feature flags off so the nav
  collapses to Home, hides the profile menu, and the SPA renders a maintenance
  page for every route. The maintenance page makes no API calls, so the API
  and database can be offline while the web component keeps running.

CLI exposes --system-announcement and tri-state --system-maintenance; the bool
falls back to pydantic settings to parse SYSTEM_MAINTENANCE reliably from env.
Adds i18n strings (en/nl), tests, and docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:42:42 +01:00
JingleManSweep 5d2f0b90d7 Merge pull request #244 from ipnet-mesh/feat/observer-filter-badges
feat(web): observer filter as toggle badges on adverts/messages
v0.13.1
2026-06-14 12:59:30 +01:00
Louis King 56696bdcd6 feat(web): observer filter as toggle badges on adverts/messages
Replace the multi-select Observer dropdown buried in the filter panel with
a row of clickable observer badges rendered between the filter panel and the
data list (and below the Sorting dropdown on mobile).

- Selection is stored in localStorage (shared across Adverts and Messages)
  as the disabled set, so new observers default to enabled.
- Badge style reflects enabled (filled) vs disabled (muted) state; the last
  enabled observer cannot be toggled off.
- Observer filter is sourced from localStorage instead of the URL query;
  a two-phase fetch resolves the enabled include-list (the API filters by
  inclusion only) before fetching data, with no flash of unfiltered results.
- Toggling re-scopes data and resets to page 1.
- Add enable/disable tooltip strings (en/nl) and the previously-missing
  Dutch observer label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:47:57 +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