Commit Graph

731 Commits

Author SHA1 Message Date
Louis King 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 King 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 King 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 King 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 King 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 King 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
renovate[bot] 0b534781da chore(deps): update actions/checkout action to v7 2026-06-18 21:40:43 +00: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
v0.14.0
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
renovate[bot] 8cd70f87af chore(deps): update anomalyco/opencode action to v1.17.8 2026-06-18 03:02:00 +00: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
renovate[bot] a57b84d482 chore(deps): update postgres docker tag to v18 2026-06-15 18:32:18 +00: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 King 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)
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
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
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