741 Commits

Author SHA1 Message Date
JingleManSweep d86a00ea87 Merge pull request #278 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.17.10
v0.15.0
2026-06-24 22:49:34 +01:00
renovate[bot] 39900b15db chore(deps): update anomalyco/opencode action to v1.17.10 2026-06-24 21:44:53 +00: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
renovate[bot] 316293f6a1 chore(deps): update actions/cache action to v6 2026-06-23 19:30:54 +00: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 King 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
renovate[bot] 643e9fa423 chore(deps): update anomalyco/opencode action to v1.17.9 2026-06-23 08:08:04 +00: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 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