Commit Graph
100 Commits
Author SHA1 Message Date
Louis King 27d6160164 fix: use boolean literal for reversible server_default
PostgreSQL rejects DEFAULT 1 for boolean columns; use DEFAULT true
which works on both PostgreSQL and SQLite (3.23.0+).
2026-07-12 22:46:35 +01:00
Louis King 818d4a2abe fix: collision in reversible migration revision ID
The reversible migration used revision ID a1b2c3d4e5f6, which was
already taken by the rename_receiver_to_observer migration.  Renamed
to f1e2d3c4b5a6.
2026-07-12 22:43:39 +01:00
Louis King 0e202b3999 feat: reversible route matching
Routes now default to bidirectional matching — packets traversing the
configured path in reverse (C->B->A instead of A->B->C) also count
towards health.  A 'reversible' toggle lets users constrain a route
to one direction when needed.

- Route model: new 'reversible' column (default true, server_default 1)
- Migration: a1b2c3d4e5f6 (additive ALTER TABLE)
- Matching engine: _match_hops + _fetch_candidate_paths_maybe_bidirectional
  helpers shared across evaluate_route, recent_matches, and preview_route
- API schemas: reversible on RouteCreate/Update/Read/Detail/PreviewRequest
- SPA: toggle in modal; card path chips show <-> for reversible, -> otherwise
- Seed loader + example YAML: reads 'reversible' key
- 4 new collector tests (reverse match, non-reversible, both-directions, API)
2026-07-12 22:39:15 +01:00
Louis King 14fbc45387 feat: route health monitoring with visual path builder
Add complete route health monitoring feature that tracks whether packets
traverse expected multi-hop paths through the mesh network.

Models & migration:
- 5 new models: PacketPathHop, Route, RouteNode, RouteObserver, RouteResult
- Alembic migration with keyset-paginated backfill of existing packets

Collector:
- store_raw_packet refactored to persist path hops via bulk insert
- Matching engine (collector/routes.py) with subsequence matching, quality
  bands (clear/marginal/failing/no_coverage), and collision detection
- Background route evaluator (60s loop) wired into subscriber lifespan
- Route seed loader in CLI (resolves by public_key, matching YAML format)

API:
- 6 CRUD endpoints + preview endpoint under /api/v1/routes
- Schemas accept node_public_keys (64-char hex) instead of internal UUIDs
- 5 Prometheus gauges for route health metrics
- Packet groups endpoint reads from hop table

Web UI:
- Full SPA routes page with summary strip, grouped cards, expandable detail
- Routes nav entry in both desktop (spa.html) and mobile (app.js) navbars
- Home page nav card with feature gate
- API proxy access mapping for v1/routes endpoints
- Visual node-search path builder with autocomplete dropdown, ordered chips
  with reorder/remove controls, and paste-64-char-key support
- Observer picker with same search UX (unordered chips)
- i18n strings in en.json and nl.json

Config:
- feature_routes flag (default: true)
- route_evaluator_interval_seconds (default: 60)
- routes_file seed path
- example/seed/routes.yaml
2026-07-12 22:26:44 +01:00
Louis King d60e2e8b1d docs: review link plan, add UI design, perf decisions, packet-detail swap
Review pass: fix stale citations (event_observer upsert path/method,
raw_packet hop-insert point, web path prefixes, VISIBILITY_LEVELS
location, features dict range); add Review section.

UI Design: status-board layout, four-state health badge
(healthy/unhealthy/no_coverage/disabled), inline accordion expand,
segmented match_width control, lazy GET /{id} detail, mesh_links.*
i18n namespace, POST /links/preview endpoint.

Performance: fetch-and-check engine over N-way self-join,
(node_hash, received_at) + (raw_packet_id, position) indexes,
preview candidate cap + debounce, evaluator threshold short-circuit.

Phase 9: consolidate packet-detail path read onto packet_path_hops;
delete duplicate _extract_path_hashes. Enabled Future Capabilities note
(path-node filtering, relay stats).
2026-07-07 00:07:46 +01:00
Louis King ae832fe73f docs: add seeding support and default visibility to link plan
Add F8/T9 + Phase 8 seed loader (links.yaml via existing meshcore-hub seed command, mirroring channels.yaml), referencing path nodes by public_key with expected_hash derived by the importer. Refine visibility to keep auth-scope but default to community (like channels).
2026-07-06 23:32:24 +01:00
Louis King 7530f8cc15 docs: add mesh link monitoring plan
Plan for a new Link entity that monitors whether packets traverse a configured ordered sequence of mesh nodes within a time window + count threshold, with background evaluation and Prometheus exposure. Includes shareable plain-language overview.
2026-07-05 23:22:01 +01:00
JingleManSweep 62d5b637f0 Merge pull request #298 from ipnet-mesh/fix/illegal-user-name-header
fix(web): sanitize X-User-Name header to prevent 502 on registration
2026-07-04 20:10:54 +01:00
Louis King cbd12b6c25 Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into fix/illegal-user-name-header 2026-07-04 20:08:12 +01:00
Louis King 7b44049e6e fix(web): sanitize X-User-Name header to prevent 502 on registration
When an IdP name claim contains trailing whitespace (e.g. 'Matt '), every
proxied API request failed with 502 'Illegal header value' because httpx
enforces RFC 7230 which forbids leading/trailing OWS in header values.

Defense in depth:
- strip_userinfo() trims the IdP name at ingress (oidc.py)
- _sanitize_header_value() removes CTL/DEL chars at both header-injection
  sites (API proxy + auth-callback bootstrap) in app.py
- update_profile() trims user-supplied names in PUT handler

X-User-Name is informational only (seeds non-unique UserProfile.name);
identity/auth are keyed on X-User-Id and X-User-Roles, so sanitization is
safe.
2026-07-04 20:06:04 +01:00
JingleManSweep dc72657b0a Merge pull request #297 from ipnet-mesh/feat/packet-breakdown-charts
feat: add packet breakdown charts to dashboard
2026-07-04 15:14:19 +01:00
Louis King 03006091e5 feat: add packet breakdown charts to dashboard
Add two horizontal 100% stacked-bar charts to the Dashboard, gated
behind the existing packets feature flag:

- Packet Types: top 6 event_type values + 'other' rollup
- Path Bytes: 1b/2b/3b path-hash width distribution (NULL excluded)

Driven by a single new cached endpoint
GET /api/v1/dashboard/packet-breakdown?days=7 returning pre-bucketed
counts. Frontend normalizes to percentages via a new
createStackedBarChart helper in charts.js with a 7-hue oklch palette.
2026-07-04 15:11:17 +01:00
JingleManSweep b608d9873b Merge pull request #296 from ipnet-mesh/chore/update-observer-docs
Updated Observer Documentation/Examples
2026-07-04 14:19:45 +01:00
Louis King a2cbbd6468 Updated observer docs 2026-07-04 14:17:02 +01:00
JingleManSweep 024114fc5b Merge pull request #294 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.6.13
2026-07-04 10:50:20 +01:00
JingleManSweep c0ac1baf04 Merge pull request #295 from ipnet-mesh/fix/dashboard-charts-and-path-popover-scroll
fix(web): 2x2 dashboard charts on mobile landscape & path-node popover scroll
2026-07-04 10:40:32 +01:00
Louis KingandClaude Opus 4.8 5dbf0697ce fix(web): path-node popover follows page scroll
The popover used position: fixed with viewport-relative coordinates, so
it stayed pinned to the viewport while the page scrolled away beneath
it. Switch to absolute positioning anchored to the document (adding
scrollX/scrollY) and read the badge's live rect on each reposition so
it tracks the anchor after the async node fetch resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:38:08 +01:00
Louis KingandClaude Opus 4.8 f3a2fe7a05 fix(web): 2x2 dashboard chart grid on mobile landscape
Charts jumped from a single column straight to N-across at the md
breakpoint, cramming them on landscape phones. Introduce a 2-column
layout at the sm breakpoint (2x2 for 4 charts; 3 charts flow to
2-on-top + 1 bottom-left with a gap on the right) and defer the full
N-across row to lg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:38:06 +01:00
JingleManSweep 5e9182f0b4 Merge pull request #293 from ipnet-mesh/feat/path-hash-bytes-filter
feat: persist path_hash_bytes column and add packet list filter
2026-07-04 00:04:50 +01:00
Louis King c029eae524 feat: persist path_hash_bytes column and add packet list filter
Persist the path-hash byte width (1/2/3) as a nullable Integer column on
RawPacket, computed at ingest by the collector and backfilled for historical
rows via a self-contained Python migration. Replace the per-request Python
decode loop in the grouped-list route with a SQL MAX() aggregate + HAVING
filter, and add a discrete <select> filter (Any/1B/2B/3B) to the /packets
SPA page.

- Model: add path_hash_bytes column to RawPacket (nullable Integer)
- Migration: batch_alter_table add_column + keyset-paginated Python backfill
  reading decoded via Core select() on sa.JSON column (portable across
  SQLite and Postgres)
- Collector: compute path_hash_bytes at ingest via two-tier path-hash
  extraction (decoded.path -> payload.decoded.pathHashes)
- API: add func.max() aggregate to group query, HAVING filter on
  ?path_hash_bytes=1|2|3 param; delete Phase 3 decode loop and dead helper
- Frontend: add path-width select filter wired through query/apiParams/
  pagination/headerParams; add i18n keys (en/nl)
- Tests: 1186 passed, 22 skipped; collector + API + model coverage
2026-07-04 00:02:18 +01:00
JingleManSweep 1615c71e18 Merge pull request #292 from ipnet-mesh/feat/json-tree-path-flow
feat(web): expandable JSON tree, packet path flow, chart polish
2026-07-03 22:38:39 +01:00
Louis King f845830848 feat(web): expandable JSON tree, packet path flow, chart polish
- Add reusable lit-html JSON tree component (json-tree.js) with
  expand/collapse-all toolbar and type-coloured primitives; replace
  flat <pre> decoded blocks on packet detail and packet group detail
- Render packet path as a complete flow with static sender (green dot)
  and observer (satellite dish) terminators around hop badges
- Fill homepage chart areas and move legend to top-right to match
  dashboard charts
- Add iconChevronRight helper and expand_all/collapse_all strings (en, nl)
2026-07-03 22:35:43 +01:00
JingleManSweep 0073723269 Merge pull request #291 from ipnet-mesh/feat/regional-numbers-filter-toggle
feat(web): locale-aware number formatting + filter panel redesign
2026-07-03 16:10:18 +01:00
Louis King 41f7fefcda feat(web): convert auto-refresh button to toggle slider for consistency
Replace the pause/play icon button with a DaisyUI toggle toggle-sm
toggle-primary slider, matching the filter toggle's visual style.
The label shows iconRefresh + interval (e.g. '30s'); checked = actively
refreshing, unchecked = paused.
2026-07-03 16:07:27 +01:00
Louis King 2c25a9cd17 feat(web): add locale-aware number formatting and redesign filter panel
Replace raw integer counts with Intl.NumberFormat()-grouped numbers
across all SPA pages — stat cards, dashboard stats, list-page total
badges, inline reception/observer counts, chart axis ticks and
tooltips, map counts, and packet-group-detail fields. Formatting uses
the visitor's browser locale (no explicit locale argument), decoupled
from the admin's datetime_locale.

Redesign the filter panel on all five filter-bearing pages (nodes,
packets, advertisements, messages, map): replace the heavy DaisyUI
collapse card with a compact right-aligned toggle slider plus bare
filter fields rendered below the control row. Filter open-state
survives auto-refresh and navigation via DOM-read of #filter-toggle.

Also fixes a pre-existing bug where pubkey_prefix was missing from
nodes.js hasActiveFilters, causing the filter to not default open
when only the public-key-prefix field was filled.
2026-07-03 16:02:45 +01:00
JingleManSweep ee4e8fa768 Merge pull request #290 from ipnet-mesh/feat/dashboard-recent-adverts
feat(dashboard): enrich recent adverts card and add packets widget
2026-07-03 14:34:24 +01:00
Louis King 215690f4bf feat(dashboard): enrich recent adverts card and add packets widget
Recent adverts card now shows route-type badges, an observer column
with three-way fallback (observers -> observed_by -> dash), and renders
all 10 rows instead of 5. The Type column hides on mobile to prevent
overflow, and the misleading cursor-help on observer badges is removed.

Also adds a Packets chart card to the dashboard and a packets stat to
the homepage, backed by the new dashboard packet-activity endpoint.

Promotes routeTypeBadge to a shared export in components.js (previously
local to advertisements.js), and enriches RecentAdvertisement with
route_type, observers, and observed_by fields.

Backend resolves observers via fetch_observers_for_events and
observer_node_id -> public_key in batched queries.
2026-07-03 14:31:44 +01:00
JingleManSweep 3c61a139a0 Merge pull request #289 from ipnet-mesh/fix/mobile-nav-clickthrough
fix(web): stop closed mobile nav from intercepting page clicks
2026-07-03 00:16:33 +01:00
Louis King cb677b3545 fix(web): stop closed mobile nav from intercepting page clicks
The two-column mobile nav set display:block on #mobile-nav (an ID
selector) to override .menu's display:flex so column-count worked.
That ID specificity also beat DaisyUI's closed-state display:none
rule, so the menu stayed display:block while 'closed' (only opacity
dropped to 0). The invisible, two-column-wide overlay captured clicks
over underlying page content and fired SPA navigation.

Scope display:block (and the column layout) to the dropdown's open
state (.dropdown-open / :focus-within) so DaisyUI's display:none can
hide the menu and its links when closed.
2026-07-03 00:14:28 +01:00
JingleManSweep 74acc3bec8 Merge pull request #288 from ipnet-mesh/feat/more-ui-tweaks
feat(web): panel accent redesign, mobile nav two-column, light-mode polish
2026-07-02 23:26:26 +01:00
Louis King 479c263f17 feat(web): redesign panel accent system, mobile nav two-column, and light-mode polish
- Replace panel-glow radial gradient with panel-accent flat tint + 5px
  colored left border for section identity
- Add theme-aware --panel-tint-strength/--panel-tint-bg variables: dark
  mode keeps 8% colored tint, light mode uses neutral grey fill (0%)
- Remove inline section color from stat-value numbers so they inherit
  base-content (white/black) while icons and borders keep section colors
- Fix hero layout: center welcome text vertically, drop spacer div,
  add flex-col to content wrapper
- Split mobile nav dropdown into two balanced columns (column-count: 2)
  with responsive width to reduce vertical height on phones
- Soften flash banner in light mode with direct amber oklch values to
  avoid hue drift from low-chroma color-mix
- Boost stat-title/stat-desc opacity from 60% to 80% in light mode for
  better contrast on grey panels
- Dashboard chart cards now use section colors instead of neutral,
  subtitles bumped from opacity-70 to opacity-80
2026-07-02 23:23:50 +01:00
JingleManSweep 9c8684183c Merge pull request #287 from ipnet-mesh/ui-cosmetic-polish
UI cosmetic polish and IBM Plex typography
2026-07-02 22:13:51 +01:00
JingleManSweep 921e8f0099 Merge branch 'main' into ui-cosmetic-polish 2026-07-02 22:11:49 +01:00
JingleManSweep eb5f7e77f5 Merge pull request #285 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.13
2026-07-02 22:06:26 +01:00
JingleManSweep 86e41d3a08 Merge pull request #286 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.6.10
2026-07-02 22:03:29 +01:00
Louis KingandClaude Fable 5 510612d69b feat(web): adopt self-hosted IBM Plex Sans and IBM Plex Mono typography
Replace the OS system font stack with IBM Plex: the variable-weight
sans (100-700, one file per subset) for UI and headings, and Plex Mono
400 for the public keys, packet hashes, and hex that font-mono renders
across the app. Latin + latin-ext subsets only (shipped locales are
en/nl); no italics; mono is never rendered bold here.

Fonts are self-hosted from @fontsource packages via the existing
build.js vendor pipeline (no CDN), copied to static/vendor/fonts/ with
a hard build failure on wrong filenames. Wiring:

- input.css: @theme --font-sans/--font-mono + @font-face rules with
  unicode-ranges taken verbatim from the package CSS; Tailwind v4
  derives the document default from --font-sans, re-fonting daisyUI
  components with no other changes.
- spa.html: preload the latin sans variable woff2 (crossorigin, URL
  identical to the @font-face src) to minimize FOUT.
- charts.js: Chart.defaults.font.family to match (Chart.js otherwise
  uses Helvetica/Arial).
- error.html: name-prepend only; the page stays dependency-free.
- middleware.py: long-term immutable cache for /static/vendor/fonts/
  (stable names referenced from CSS, so ?v= versioning can't apply),
  with a matching test.
- app.css: slight hero-title letter-spacing tightening for Plex at
  display sizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:58:58 +01:00
Louis KingandClaude Fable 5 91a3fcf237 feat(web): make custom clickable elements keyboard accessible
The channel cards, member node badges, and member URL text are
clickable divs/spans, so keyboard users could not reach or activate
them. Add role/tabindex, Enter/Space key handling, and a
focus-visible outline (invisible to mouse users). daisyUI btn/link/
input components already ship focus-visible outlines, so only these
custom elements needed it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:32:25 +01:00
Louis KingandClaude Fable 5 79565326e2 style(web): normalize heading, spacing, and radius scales across pages
- Page <h1>s: text-3xl font-bold everywhere (packet-detail,
  packet-group-detail, channels were text-2xl; channels icon bumped to
  match).
- Page-header rows: mb-6 on all pages (list pages used mb-4).
- Empty states: text-center py-8 opacity-70 (members used py-12,
  channels py-10 opacity-60).
- QR backings and adopted-node rows: rounded-box like other
  panel-level surfaces; QR padding unified at p-2.
- Home hero welcome text: max-w-[90%] sm:max-w-[70%] so narrow screens
  don't wrap into a skinny column (desktop unchanged).
- Document the styling conventions in the components.js header so
  future pages don't drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:31:40 +01:00
Louis KingandClaude Fable 5 bb929dfe5f refactor(web): remove deprecated class names and hoist map marker colors
Zero-visual-delta cleanups:

- Drop input-bordered/select-bordered (removed in daisyUI v5; emitted
  no CSS) from all inputs and selects.
- Rename bare `shadow` to `shadow-sm` — in Tailwind v4 bare `shadow` is
  a deprecated alias with the identical value.
- Hoist the hardcoded map marker hex colors into CSS variables in the
  app.css palette block (same values; markers sit on map tiles and stay
  theme-independent by design).
- Convert the five text-base-content/* outliers to the repo's dominant
  opacity-* muted-text idiom (identical rendering on plain text).
- Remove a dead ternary in renderNodeDisplay and an unused iconLock
  import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:30:07 +01:00
Louis KingandClaude Fable 5 e2c7a0f987 fix(web): repair unstyled channel modal labels and invisible prose link hover
The channels modal still used daisyUI v4's label-text class, which was
removed in v5, so its form labels rendered unstyled. Replace with the
Tailwind equivalent of what v4 produced (text-sm, muted).

.prose a:hover used --color-primary-content (the on-primary foreground),
making hovered markdown links nearly invisible against the page
background. Blend primary toward base-content instead so the hover
shade works in both themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:28:06 +01:00
JingleManSweep 35037c64f2 Merge pull request #284 from ipnet-mesh/renovate/tailwindcss-monorepo
chore(deps): update tailwindcss monorepo to v4.3.2
2026-06-30 22:39:11 +01:00
JingleManSweep d90e6c1ff7 Merge branch 'main' into renovate/tailwindcss-monorepo 2026-06-30 22:36:57 +01:00
JingleManSweep 75264265f4 Merge pull request #283 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.6.6
2026-06-30 22:36:44 +01:00
JingleManSweep 369b3c6eb5 Merge pull request #282 from ipnet-mesh/fix/fastapi-0.137-route-introspection
fix(tests): introspect mounted paths via OpenAPI schema, unpin fastapi
2026-06-25 21:03:42 +01:00
Louis KingandClaude Opus 4.8 b413904ace fix(tests): introspect mounted paths via OpenAPI schema, unpin fastapi
FastAPI 0.137.0 refactored include_router to keep included routers as
nested objects rather than flattening their routes into app.routes, so
test_app_factory's `{route.path for route in app.routes}` no longer found
the /metrics route (the endpoint still serves; only this introspection
broke). FastAPI now treats router.routes as an internal implementation
detail.

Switch the metrics route checks to the public OpenAPI schema
(app.openapi()["paths"]), which is stable across versions and resolves
router prefixes correctly, and drop the <0.137.0 pin that was blocking
the upgrade.

Verified: app factory tests pass on both 0.136.3 and 0.137.2; full
tests/test_api suite (462 tests) passes on 0.137.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 21:00:35 +01:00
JingleManSweep 01077b3783 Merge pull request #281 from ipnet-mesh/feat/observer-ingestion-filters
feat(collector): observer allow/deny ingestion filter
2026-06-25 20:52:47 +01:00
Louis KingandClaude Opus 4.8 5fe8ce9156 feat(collector): add observer allow/deny ingestion filter
Restrict which remote observers may ingest events, keyed on the observer's
public key (the <public_key> segment of its LetsMesh upload topic). Anyone
with broker access can publish as an observer via JWT auth, so operators can
now gate ingestion.

- New ObserverFilter (case-insensitive prefix matching, allowlist overrides
  denylist, accept-all when both empty)
- New OBSERVER_ALLOWLIST / OBSERVER_DENYLIST collector settings, wired through
  the CLI, run_collector, create_subscriber, and Subscriber
- Filter applied at the top of _handle_mqtt_message: blocked observers' packets
  are dropped before any decode, raw-packet capture, or DB write; zero added
  work on the default accept-all path
- Tests: ObserverFilter unit tests, subscriber drop/allow integration tests,
  config parsing tests
- Docs: configuration.md, observer.md, upgrading.md (v0.16.0), .env.example,
  docker-compose.yml

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:25:29 +01:00
JingleManSweep 133e064a17 Merge pull request #279 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.11
2026-06-25 19:03:52 +01:00
JingleManSweep d86a00ea87 Merge pull request #278 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.10
2026-06-24 22:49:34 +01:00
JingleManSweep 9df7bf2f92 Merge pull request #277 from ipnet-mesh/renovate/actions-cache-6.x
chore(deps): update actions/cache action to v6
2026-06-24 09:10:39 +01:00
JingleManSweep d1d621c35b Merge pull request #276 from ipnet-mesh/fix/message-row-height
fix(web): stop pre-wrap inflating message table row height
2026-06-23 09:55:17 +01:00
JingleManSweep c9d5b34587 Merge branch 'main' into fix/message-row-height 2026-06-23 09:50:53 +01:00
JingleManSweep ba185f0896 Merge pull request #274 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.9
2026-06-23 09:50:39 +01:00
Louis KingandClaude Opus 4.8 c955ea7a64 fix(web): stop pre-wrap inflating message table row height
The spam-detection change wrapped the message table cell content in a
multi-line div/span so the spam badge could sit alongside the text, but left
white-space: pre-wrap on the td. The template literal's own indentation
(newlines + spaces around the div) then rendered as literal whitespace inside
the pre-wrap cell, padding out every row regardless of spam state.

Move pre-wrap onto the span that holds the message text so multi-line bodies
still wrap while the cell's layout whitespace collapses normally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:49:54 +01:00
JingleManSweep 45a76a9ad3 Merge pull request #275 from ipnet-mesh/feat/spam-detection
feat(spam): score messages at ingest and hide likely spam
2026-06-23 09:07:26 +01:00
Louis KingandClaude Opus 4.8 461dbc5008 feat(spam): ship spam detection enabled by default
Make FEATURE_SPAM_DETECTION on by default with opt-out, mirroring
FEATURE_PACKETS: flip the web feature flag's Python default to true and the
Compose substitutions (collector/api/web) to :-true, so the shipped stack
scores and hides likely-spam without configuration. Opt out with
FEATURE_SPAM_DETECTION=false.

Update .env.example, docs/configuration.md and the v0.15 upgrade notes to
describe the feature as enabled-by-default with opt-out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:00:16 +01:00
Louis KingandClaude Opus 4.8 db952d632a fix(spam): align badge threshold with API hide-filter
The Messages page badged a row as spam at a hardcoded score >= 0.6 while the
API hides rows at SPAM_SCORE_THRESHOLD (now 0.65), so messages scored in
[0.6, 0.65) appeared flagged but were never hidden by the "show potential
spam" toggle.

Expose spam_score_threshold in the SPA config and use it for the badge so a
row is badged exactly when the API would hide it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 08:51:20 +01:00
Louis KingandClaude Opus 4.8 caaecfb3c2 feat(spam): retune default scoring config and add v0.15 upgrade notes
Adjust the default spam-scoring knobs across Python settings, SpamConfig,
docker-compose, .env.example and docs to reduce false positives on chatty
legitimate users:

  SPAM_MIN_PATH_HOPS   5  -> 3
  SPAM_PATH_THRESHOLD  5  -> 6
  SPAM_NAME_THRESHOLD  5  -> 10
  SPAM_WEIGHT_PATH     0.7 -> 0.75
  SPAM_WEIGHT_NAME     0.3 -> 0.25
  SPAM_SCORE_THRESHOLD 0.6 -> 0.65

Also document the spam-detection feature and the pull_policy change in a
new v0.15.0 section of docs/upgrading.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 08:40:24 +01:00
JingleManSweep fa9e4c811a Merge branch 'main' into feat/spam-detection 2026-06-23 00:42:43 +01:00
Louis KingandClaude Opus 4.8 691a5f45c8 test(spam): fix mypy union-attr in sweep-error test
Guard the optional `_spam_rescore_thread` with an `is not None` assert before
calling `.is_alive()`, matching the sibling test. CI runs `pre-commit run
--all-files`, so mypy checks the test files too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:28:55 +01:00
Louis KingandClaude Opus 4.8 4d680b1de7 test(spam): cover scheduler, config cache, and scoring edge cases
Raise diff/patch coverage above the Codecov gate (was 79.8%, target 80.29%)
by exercising the previously-untested spam paths:

- subscriber: TestSpamRescoreScheduler covers the disabled early-return, the
  enabled thread spawning + one sweep + clean stop, and the swallowed-error
  branch of the background re-scoring loop.
- handler: a contact-message scoring test covers the contact log branch.
- spam: get/reset_spam_config caching, the zero-weight combine path, the
  default-`now` path, and the null-sender reset in rescore_recent.

Patch coverage for the change is now ~97%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:25:06 +01:00
Louis KingandClaude Opus 4.8 c48db03afb feat(spam): score messages at ingest and hide likely spam
Add an optional, off-by-default spam-detection feature that scores each
message's spam likelihood at ingest, stores the score on the row, and lets
the display layer hide likely-spam by default behind a "show potential spam"
toggle. Nothing is ever dropped at ingest, so the threshold can be retuned
without reprocessing.

Scoring (collector/spam.py): windowed COUNT(*) over new
(path_prefix, received_at) and (sender_normalized, received_at) indexes —
joint path+sender signal plus a sender-name signal (trailing-digit suffix
stripped so bob1/bob2 collapse to bob). When the path is short/zero-hop or
absent, the name signal stands alone at full weight so local spam is still
flaggable. A background sweep re-scores recent rows with hindsight to catch
the leading edge of bursts. The collector logs each score (WARNING at/above
the threshold).

Display: the messages API gains include_spam and a master-switch-aware
hide-filter; the SPA shows the toggle + a badge only when the feature is on.

Config: FEATURE_SPAM_DETECTION is the single operator switch, bridged in
Compose to the backend SPAM_DETECTION_ENABLED for collector + api (mirrors
the FEATURE_PACKETS / RAW_PACKET_CAPTURE_ENABLED pattern). Both default off.

Works on SQLite and Postgres: DB-agnostic queries, an Alembic batch migration
for the three new columns + two indexes, and backend-aware collector test
fixtures (lifted db_backend/db_url into the shared conftest).

Also: move the meshcore-hub image pull_policy out of the base compose file.
It lived in docker-compose.yml as pull_policy: daily and made `make up` pull
the published image over a freshly built local one. Base is now policy-neutral
(default missing); dev sets pull_policy: build on the hub services so it only
ever uses local builds. Prod refreshes images via a manual `docker compose
... pull`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:11:39 +01:00
JingleManSweep 0e8950d4d1 Merge pull request #272 from ipnet-mesh/renovate/actions-checkout-7.x
chore(deps): update actions/checkout action to v7
2026-06-20 13:18:58 +01:00
JingleManSweep 6bf3c1c62b Merge pull request #271 from ipnet-mesh/docs/reorganize-configuration-and-deployment
docs: centralise env vars and split deployment/observer/maintenance docs
2026-06-18 12:36:04 +01:00
Louis King 973bf23fe8 docs: centralise env vars and split deployment/observer/maintenance docs
Move scattered configuration tables and operational sections out of the
README into dedicated reference documents:

- docs/configuration.md: single source of truth for all environment
  variables, grouped into 12 sections (Common, Database, Caching,
  Collector, Webhooks, Auth, Data Retention, API, Web Dashboard,
  Feature Flags, Traefik, Prometheus & Alertmanager)
- docs/deployment.md: production setup, reverse proxy, multi-instance,
  API scaling, Redis caching
- docs/observer.md: remote observers plus PACKETCAPTURE_* and
  SERIAL_PORT reference
- docs/maintenance.md: backup and restore

README is reduced from 712 to 385 lines; the ARM32/Raspberry Pi note
is dropped. database.md, auth.md, webhooks.md, and content.md have
their env-var tables removed and link back to configuration.md. Stale
cross-references in database.md, upgrading.md, and .env.example are
updated to point at the new locations.
2026-06-18 12:20:49 +01:00
JingleManSweep a96bb548f6 Merge pull request #269 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.8
2026-06-18 11:00:21 +01:00
JingleManSweep 4e25b2fbf0 Merge pull request #267 from ipnet-mesh/docs/tidy-database-docs-and-sqlite-deprecation
docs: tidy database docs and deprecate SQLite
2026-06-17 22:00:40 +01:00
JingleManSweep e0b2a2cb89 Merge branch 'main' into docs/tidy-database-docs-and-sqlite-deprecation 2026-06-17 21:58:47 +01:00
JingleManSweep 2d45da2c1c Merge pull request #268 from ipnet-mesh/fix/ci-required-checks-on-prs
fix: ensure CI required checks run on all PRs
2026-06-17 21:57:20 +01:00
Louis King 6ede32b2d4 fix: ensure CI required checks run on all PRs
Remove paths-ignore from the pull_request trigger so the CI workflow
always runs and the required Lint/Test/Build Package checks report on
docs-only PRs (previously skipped entirely, blocking merges).

Drop the github.event_name == 'push' gate on the build job so the
required Build Package check also reports on pull requests.
2026-06-17 21:54:20 +01:00
Louis King 0bc66d9871 docs: drop pgloader explanation from upgrade guide 2026-06-17 15:20:11 +01:00
Louis King 30e8e88ee9 docs: add SQLite deprecation notice and consolidate DB docs
Replace the lingering v0.9 'Breaking Changes' alert with a concise v0.14
'DEPRECATION NOTICE' for SQLite (dual compatibility for ~3 months, then
PostgreSQL-only).

Move all database-specific instructions (SQLite + PostgreSQL) out of the
README into a new canonical docs/database.md covering:
- SQLite zero-config default (DATA_HOME / meshcore.db, WAL/single-host)
- PostgreSQL: DATABASE_* env vars, bundled Docker profile, production
  role/database provisioning (mirrors the ipnet-mesh/infrastructure init
  script), managed/external Postgres and DATABASE_URL
- Schema-per-instance (search_path) isolation for multiple instances on a
  shared cluster
- Pointer to the SQLite->PostgreSQL migration runbook in upgrading.md

Update the README Multi-Instance Deployments and Scaling the API sections
to link to docs/database.md, and add the new doc to the docs list and
project tree. Add a pointer in .env.example and a Postgres note in
AGENTS.md.

Consolidate docs/upgrading.md v0.14: move the env-var/schema/provisioning
reference to docs/database.md (single source of truth) and keep only the
upgrade-time migration runbook and dashboard chart fix.
2026-06-17 15:14:53 +01:00
JingleManSweep b38d0f20ff Merge pull request #266 from ipnet-mesh/chore/added-explicit-compose-pull-policies
Added explicit pull policies for core services
2026-06-16 22:58:33 +01:00
Louis King 3f739a783b Added explicit pull policies for core services 2026-06-16 22:55:19 +01:00
JingleManSweep 6ed2e6dc6a Merge pull request #265 from ipnet-mesh/fix/packets-feature-flag-check
fix: normalize packets feature flag check to !== false
2026-06-16 22:09:43 +01:00
Louis King 9071634606 fix: normalize packets feature flag check to !== false
The packets feature was the only flag gated with a strict `=== true` check
in the SPA JavaScript while every other flag uses `!== false`. Align packets
to the shared pattern across route registration, mobile nav, page titles, the
home nav card, and the messages/advertisements packet-detail link gates.
2026-06-16 22:02:59 +01:00
JingleManSweep f045de0dc3 Merge pull request #264 from ipnet-mesh/fix/postgres-charts-flatline
fix: normalize date-bucket keys for Postgres dashboard charts
2026-06-16 21:18:41 +01:00
Louis King cf5add9924 fix: normalize date-bucket keys for Postgres dashboard charts
Dashboard charts (activity, message-activity, node-count) rendered as
flat zeros on Postgres because func.date() returns a str on SQLite but
a datetime.date on Postgres — the dict lookup by string key always
missed. Fixed with a dialect-neutral _date_bucket_key() helper and
pinned the Postgres session timezone to UTC at the engine level.

Also adds dual-backend test infrastructure (TEST_DATABASE_BACKEND env
var), per-worker Postgres databases for pytest-xdist isolation, and
strengthened regression tests asserting non-zero date buckets.
2026-06-16 21:16:00 +01:00
JingleManSweep d4d55b16d9 Merge pull request #263 from ipnet-mesh/fix/node-list-nulls-last
fix: sink NULL last_seen nodes to bottom of node list
2026-06-16 19:25:19 +01:00
Louis King 8bf45362bb fix: sink NULL last_seen nodes to bottom of node list
After the Postgres migration, nodes with no last_seen timestamp floated
to the top of the default list because Postgres sorts NULLs first under
ORDER BY ... DESC, whereas SQLite (the previous backend) sorts them last.

Wrap the last_seen ORDER BY with SQLAlchemy nullslast() so NULL last_seen
nodes always sink to the end regardless of database backend or sort
direction. Adds three regression tests covering DESC, ASC, and all-NULL
cases.
2026-06-16 19:20:19 +01:00
JingleManSweep 34410dea2f Merge pull request #260 from ipnet-mesh/chore/revert-to-postgres-17
Reverted to Postgres 17
2026-06-15 20:05:28 +01:00
Louis King a554e095df Reverted to Postgres 17 2026-06-15 20:03:19 +01:00
JingleManSweep e8fd31a464 Merge pull request #258 from ipnet-mesh/renovate/postgres-18.x
chore(deps): update postgres docker tag to v18
2026-06-15 19:56:01 +01:00
JingleManSweep 520a11ada2 Merge pull request #259 from ipnet-mesh/fix/migrate-overlay-network
Added migrate service to overlay network
2026-06-15 19:22:07 +01:00
Louis King ccc8943971 Added migrate service to overlay network 2026-06-15 19:18:55 +01:00
JingleManSweep 1ba3e178a8 Merge pull request #243 from ipnet-mesh/feat/postgres-support
Add optional PostgreSQL backend (v0.14.0)
2026-06-15 18:57:55 +01:00
Louis King 4662e46d3a Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into feat/postgres-support 2026-06-15 15:11:29 +01:00
JingleManSweep 72b2b938f6 Merge pull request #256 from ipnet-mesh/chore/ghcr-package-cleanup
ci: prune old untagged GHCR package versions
2026-06-15 15:08:32 +01:00
Louis KingandClaude Opus 4.8 ebb6547457 ci: prune old untagged GHCR package versions
Add actions/delete-package-versions@v5 to both Docker build pipelines to
clean up orphaned untagged manifests left behind by multi-arch rebuilds.

Uses delete-only-untagged-versions so tagged versions (v*, latest, main,
sha-*) are never deleted, with min-versions-to-keep: 10 to protect the
freshly-pushed image's own untagged child/attestation manifests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:00:34 +01:00
Louis King 8c77c2a978 Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into feat/postgres-support 2026-06-14 23:34:42 +01:00
JingleManSweep 7f19d47aa0 Merge pull request #255 from ipnet-mesh/fix/observer-filter-cache-key-collision
fix: forward/cache repeated query params (observer filters)
2026-06-14 23:32:47 +01:00
Louis KingandClaude Opus 4.8 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 KingandClaude Opus 4.8 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
Louis King 7603b16092 Fixed pre-commit issues 2026-06-14 22:58:52 +01:00
Louis King fd582ebbc2 Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into feat/postgres-support 2026-06-14 22:53:20 +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
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