Commit Graph

937 Commits

Author SHA1 Message Date
Louis King fd51e7298f docs(next-gen): drop LetsMesh brand terminology from new-repo vocabulary
LetsMesh (a now-inactive platform whose MQTT observer pattern + topic format
this project adopted for compatibility) is not carried into the new repo.
Genericize every brand-term-in-prose reference:

- 'LetsMesh on-air hash' / 'LetsMesh wire hash' -> 'on-air wire hash' (the
  wire_hash field) across data-model, ingest, migration, infrastructure,
  review-findings, code-warts, overview.
- 'MQTT LetsMesh feeds' -> 'MQTT upload feeds' (overview).
- 'powers the official LetsMesh analyzer' -> 'powers the reference analyzer' (D22).

Adds a 'Legacy brand terms' clause to the naming convention (phasing.md):
on-air wire hash / MQTT upload feeds / descriptive fallback event types
(unhandled_packet, not letsmesh_packet) / no letsmesh_* modules or classes in
the new repo.

Code-font citations of real legacy files/classes (letsmesh_normalizer.py,
LetsMeshNormalizer, _normalize_letsmesh_event) remain in the current-system
analysis as factual references to the old codebase.
2026-07-26 14:48:30 +01:00
Louis King 5e8aad1d9a docs(next-gen): record product naming — Meshloom (brand) / meshapp (internal)
The rewrite's product name is Meshloom (repo/npm/CLI/docs/domain). MeshCore is
the protocol/decoder's term, not ours, so new-product branding avoids it;
'MeshCore Hub' in the plan refers only to the current product being rewritten.

Internal code stays brand-agnostic so a future rename never cascades: package
root is 'meshapp' (generic), and class/identifier names stay descriptive
(MqttIngester, IngestWorker, etc.) — never brand-prefixed.

Adds a Naming convention section to phasing.md and a front-door pointer in
README.md.
2026-07-26 14:42:59 +01:00
Louis King 4d04b8ed97 docs(next-gen): iteration-10 review pass — 9 consistency fixes + 'how to use' note
- Phase count: '7-phase' -> '8-phase (Phase 0-7)' in README (x2) + phasing.md
  (the docs have 8 sections; Phase 0 carries real deliverables).
- D16: add the 7th worker job (dashboard rollups) to the parenthetical — added
  by F2 but never propagated to the ADR.
- open-questions.md: bump stale status header iteration 7 -> 9.
- testing.md: add the missing infrastructure.md row to the component->layers
  table; add the missing Phase 6 exit-criteria section (Phase 7 had one).
- D06: FastAPI Depends -> Fastify preHandler decorator (Python leftover, D22/F9).
- multi-tenancy.md: MqttIngester.__init__ -> TS instanceId constructor arg.
- frontend.md: SSE-auth anchor pointed at the parent section, not the auth
  subsection.
- D13: drop the undocumented 'Q-C' sigil.
- phasing.md: add a 'How to use this plan during implementation' section
  orienting implementers to the three-layer structure (component docs /
  checklist / testing) and stating that per-phase build order + PR-level task
  decomposition belongs in the implementation repo's tracker, not the design.
2026-07-26 14:26:29 +01:00
Louis King 70981c8fe0 docs(next-gen): iteration-9 — punch-list remediation, TS pseudocode, D23 testing policy
- Apply the full design-review punch list (PL-1..PL-36): schema/RLS/envelope
  corrections, F-number propagation, dangling section refs repointed, REWRITE.md stub.
- Rewrite all illustrative Python pseudocode as TypeScript/Drizzle/Fastify across
  the component docs (PL-29); SQL/PL-pgSQL left language-agnostic.
- Observer friendly name: per-tenant label on tenant_observers with a concrete
  GET/POST/PUT/DELETE contract, nodes?is_observer picker, and a prefix-keyed
  routing cache so label-only edits skip the ingester reload.
- Add D23 (test pyramid & CI coverage policy): vitest unit/integration/component
  + Playwright e2e, qualitative coverage, real-login E2E (closes TQ1). Wire a
  Test-strategy section + per-phase Tests blocks through testing.md, the
  implementation checklist, and phasing; bump ADR count 22 -> 23.
2026-07-26 13:41:31 +01:00
Louis King cbeb6b38c2 docs(next-gen): apply iteration-8 design-review corrections
Full-plan review surfaced 13 issues; all corrected in place and catalogued
in review-findings.md (cross-referenced F1–F13). Four schema-level blockers
resolved before the Phase 0 DDL freeze:

- F1: instance-scoped composite uniques (nodes.public_key, event_hash,
  channels.name/key_hex) + settings PK (instance_id, key) — makes D21's
  "schema does not change" actually true
- F2: only raw_receptions-sourced CAGGs remain; message/advert/node-count
  become worker-maintained dashboard rollup tables + dashboard-rollups job
- F3: FORCE RLS + non-owner app role, per-request transaction for reads,
  instance_id in cache key, explicit instance_id predicate on CAGG reads
- F5: D5 benchmark moved to Phase 0; Phase 1 decode shadow vs Phase 2
  full parallel-stack

Design-risk fixes: single INGEST stream + wildcard consumer (F8), stable
two-arg advisory-lock key (F7), diff harness keyed on wire_hash (F4),
FK-less hypertable node refs (F6), single-eval spam sweep (F10), observer
upsert + telemetry dedup note + rowid chunk exclusion (F11). Notes for
TS-translation gaps (F9), setup-wizard SSR (F12), and scope/risk (F13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 00:09:23 +01:00
Louis King 2bb2ea9a93 docs(next-gen): reconcile auth config with multi-tenancy; rename OIDC_SESSION_SECRET to JWT_SESSION_SECRET 2026-07-25 23:05:27 +01:00
Louis King f41d1eadec docs: add next-gen rewrite plan with 22 locked ADRs and 9 component docs 2026-07-25 22:32:32 +01:00
JingleManSweep 9c1d3e5210 Merge pull request #349 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.7.3
v0.17.2
2026-07-25 14:59:06 +01:00
renovate[bot] df02cf0601 chore(deps): update dependency daisyui to v5.7.3 2026-07-25 13:55:36 +00:00
JingleManSweep 10a38502eb Merge pull request #350 from ipnet-mesh/fix/dashboard-packet-retention-window
fix(dashboard): make packet displays retention-aware
2026-07-25 14:54:42 +01:00
Louis King 993ba7b5b6 docs: sync retention/evaluator defaults to #348 values
PR #348 changed several defaults but left a handful of docs, the
docker-compose fallback, example seed files, and code docstrings stale:

- docker-compose.yml: RAW_PACKET_RETENTION_DAYS fallback 7->2 (the
  :-7 override was the most consequential — operators without the env
  var set got 7 days, contradicting the code default and .env.example)
- docs/configuration.md: route-history backfill now references the
  300 s evaluator cadence (was 60 s)
- docs/seeding.md + example/seed/routes.yaml: window_hours 48/24 -> 6
  (matches the new default; both exceeded the 12 h max)
- routes.py docstring: evaluator tick 60 s -> 300 s
- subscriber.py: raw_packet_retention_days signature defaults 7 -> 2
  (overridden at runtime by effective_raw_packet_retention_days, but
  kept consistent with the authoritative config default)
2026-07-25 14:52:06 +01:00
Louis King 7802a00db3 fix(dashboard): make packet displays retention-aware
PR #348 lowered the RAW_PACKET_RETENTION_DAYS default 7->2, but the
dashboard packet count, activity chart, and breakdown chart still
advertised "Last 7 days" — a label that lied once only 2 days of data
remained. Make every raw-packet-derived window track the effective
retention so labels stay honest:

- get_dashboard_stats: packets_7d window = min(7, retention); expose
  packets_window_days on DashboardStats so the UI renders the real window
- packet-activity / packet-breakdown: clamp days = min(days, 90, retention)
  so responses never advertise days whose data has been purged
- Home StatCard + Dashboard chart subtitles: dynamic "Last N days" /
  "Per day (last N days)" via new time.last_n_days / time.per_day_last_n_days
  i18n keys ({{n}} interpolation, matching routes_over_last_n_days)
- ActivitySeries.days made optional (type shared across activity charts);
  PacketBreakdown.days added (backend always serializes it)

Messages/ads 7-day displays are unchanged — those draw on event data
which retains the full 30-day DATA_RETENTION_DAYS window.

Tests: autouse retention fixture (->90) on the three packet test classes
so existing days-handling tests stay valid; added retention-clamp tests
proving the window tracks retention=2 and relaxes to 7 at retention>=7.
2026-07-25 14:39:33 +01:00
JingleManSweep 153ba14ea0 Merge pull request #348 from ipnet-mesh/perf/route-evaluator-remediation
perf(routes): cut evaluator scan cost via window/retention/index/sweep
2026-07-25 13:21:30 +01:00
Louis King c9b247c553 perf(routes): cut evaluator scan cost via window/retention/index/sweep
The route evaluator's fetch_candidate_paths triggered a 10-30s full-table
scan of packet_path_hops per route per sweep, flooding the slow-query
log and blocking route saves on the same scan. EXPLAIN analysis (Merge
Join vs forced Nested Loop, across query rewrites, ANALYZE, CTE
MATERIALIZED) confirmed both candidate plans cost ~10-15s at 6.4M rows;
no SQL rewrite or stats change could avoid the scan. The fix applies
four orthogonal data-volume pressures:

- ROUTE_EVALUATOR_INTERVAL_SECONDS default 60->300 (5x less frequent)
- Remove synchronous _reevaluate_route on POST/PUT; route_result and
  recent_matches now refresh on the next background sweep instead of
  inline (kills save latency)
- Route.window_hours default 48->6, max 12 (bounds the candidate set);
  migration clamps pre-existing routes >12 to 12
- Covering index ix_packet_path_hops_raw_packet_id_position INCLUDE
  (node_hash, packet_hash, event_hash, received_at, observer_node_id)
  so the Merge Join outer scan becomes index-only
- RAW_PACKET_RETENTION_DAYS default 7->2 (hops cascade-delete with raw
  packets; smaller table cuts the dominant outer scan)

Migration a59611449e2a clamps window_hours and rebuilds the index
CONCURRENTLY on PostgreSQL (SQLite is a no-op; no INCLUDE support).

DESTRUCTIVE on upgrade: the retention default change purges ~5 days of
raw packets + cascaded hops on the next cleanup run. See
docs/upgrading.md for the full change notes and the new save/sweep
contract.
2026-07-25 13:15:51 +01:00
JingleManSweep 41dd92e6e7 Merge pull request #347 from ipnet-mesh/fix/pin-playwright-nix
fix(e2e): pin @playwright/test to 1.61.1 to match nixpkgs browsers
2026-07-25 00:22:16 +01:00
Louis King 2634711dd3 fix(e2e): pin @playwright/test to 1.61.1 to match nixpkgs browsers
Playwright browser binaries on this NixOS system are managed by
nixpkgs playwright.browsers (currently 1.61.1, browser revision
1228). Renovate bumped @playwright/test to 1.62.0 (revision 1234)
which has no matching browser binary in the Nix store.

Pin to exact 1.61.1 (no caret) and disable Renovate updates for
this package via renovate.json packageRules.
2026-07-25 00:20:00 +01:00
JingleManSweep e6b613c716 Merge pull request #346 from ipnet-mesh/chore/vite-8-plugin-react-6
chore(deps): upgrade vite to v8 and @vitejs/plugin-react to v6
2026-07-25 00:13:12 +01:00
Louis King 1a4b58b473 chore(deps): upgrade vite to v8 and @vitejs/plugin-react to v6
Vite 8 replaces Rollup with Rolldown as the bundler engine. This
requires migrating build.rollupOptions to build.rolldownOptions and
the removed object-form manualChunks to the new codeSplitting.groups
API.

Migrate vite.config.ts:
- rollupOptions -> rolldownOptions
- manualChunks object -> codeSplitting.groups with RegExp test patterns
  (vendor: react/react-dom/react-router, i18n: i18next packages)

@vitejs/plugin-react@6 requires vite@^8 as peer dependency, so both
upgrades must land together.

Verified: npm run build succeeds (chunks: vendor 230KB, i18n 55KB),
tsc --noEmit clean, 339/339 vitest tests passing.
2026-07-25 00:10:46 +01:00
JingleManSweep 9ce1ef9c78 Merge pull request #345 from ipnet-mesh/chore/typescript-7-tsconfig
chore(deps): upgrade TypeScript to v7 and migrate tsconfig
2026-07-25 00:05:48 +01:00
Louis King 4b745974a1 chore(deps): upgrade typescript to v7 and migrate tsconfig
TypeScript 7 removes the 'baseUrl' compiler option and requires
'paths' entries to be relative (leading './'). Update tsconfig.json
accordingly. The '@/alias is also resolved by Vite's resolve.alias
in vite.config.ts, so only tsc --noEmit was affected.

Verified: tsc --noEmit clean, e2e typecheck clean, 339/339 vitest
tests passing.
2026-07-25 00:02:55 +01:00
JingleManSweep 6dd5c47e8b Merge pull request #343 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.18.5
2026-07-24 23:58:51 +01:00
renovate[bot] e7aaebfc85 chore(deps): update anomalyco/opencode action to v1.18.5 2026-07-24 22:56:29 +00:00
JingleManSweep 1df9e0d6a3 Merge pull request #340 from ipnet-mesh/renovate/playwright-monorepo
chore(deps): update dependency @playwright/test to v1.62.0
2026-07-24 23:55:59 +01:00
renovate[bot] 6b3d6d53cb chore(deps): update dependency @playwright/test to v1.62.0 2026-07-24 22:52:54 +00:00
JingleManSweep c5437d8a34 Merge pull request #344 from ipnet-mesh/fix/routes-stats-label-tweaks
Compact Routes stats dash and chart 'Now' label
2026-07-24 23:51:51 +01:00
Louis King 41f8e395e8 fix(web): compact Routes stats dash and chart 'Now' label
Replace arrow with dash in the matched/threshold/degraded stat
(0/2→6 becomes 0/2-6) to save horizontal space and prevent the
stats row from wrapping on mobile.

Change the last history chart label from 'Last XXh' to 'Now' for
clarity and brevity.

Add 'now' key to en.json ('Now') and nl.json ('Nu'); remove the
now-dead 'last_n_hours' key from both locale files.

Add tests for the dash separator format and the 'Now' label.
2026-07-24 23:49:16 +01:00
JingleManSweep fea12780b0 Merge pull request #342 from ipnet-mesh/fix/routes-card-overflow
Fix remaining Routes horizontal overflow (card content not clipped)
2026-07-24 23:48:51 +01:00
Louis King 3575af1f7e fix(web): clip Routes card overflow with overflow-hidden on card-body
Previous min-w-0 on the card grid item prevented the card from growing
but did not clip content that overflowed the card boundary (Chart.js
canvas, flex items with min-width:auto inside card-body). The overflow
was still visible beyond the card edge, causing page-level scroll.

Add overflow-hidden to card-body — this both resolves card-body's own
min-width:auto to 0 (per CSS spec, overflow != visible on a flex/grid
item zeroes its automatic minimum size) and clips any residual content
overflow from children. Add overflow-x-hidden to the page root as a
safety net for any non-card overflow.
2026-07-24 23:38:01 +01:00
JingleManSweep 7d56090338 Merge pull request #341 from ipnet-mesh/fix/mobile-overflow-dashboard-routes
Fix horizontal overflow on Dashboard and Routes (mobile portrait)
2026-07-24 23:23:51 +01:00
Louis King 471830de8f fix(web): resolve horizontal overflow on Dashboard and Routes (mobile)
Dashboard Recent Adverts widget overflowed because its card (a CSS
grid item with default min-width: auto) grew to the table's intrinsic
min-content width, preventing the overflow-x-auto wrapper from
engaging. Add min-w-0 to both bottom-grid cards and constrain the
Node name cell with max-w + truncate to reduce the table's min-width.

Routes had page-level horizontal scroll with no visible scrollbar due
to three triggers and no overflow containment in the ancestor chain:
(1) the history day-label flex row whose flex-1 spans had no min-w-0,
(2) -mx-1 on every clickable MatchRow, and (3) a Chart.js responsive
canvas in an unconstrained parent. Add min-w-0 to the card grid item,
chart wrapper, and label spans (with truncate); add overflow-hidden
to the MatchRow container to clip the negative-margin hover-bg.
2026-07-24 23:21:00 +01:00
JingleManSweep d13695eecf Merge pull request #339 from ipnet-mesh/feat/page-header-icons
Show section icons in page headers across all pages
2026-07-24 23:11:41 +01:00
Louis King 78fab35229 feat(web): show section icons in page headers across all pages
Add a typed icon prop to PageHeader that renders the section icon at
h-8 w-8 inside the heading, replacing the ad-hoc span wrapper used by
Channels and Routes. Migrate all 10 list pages to pass their section
icon, and add section icons to the 3 detail page headers (PacketDetail,
PacketGroupDetail, NodeDetail).

Normalize Channels/Routes title keys to the entities.* namespace
(used everywhere else) and add the missing entities.channels key to
nl.json ('Kanalen'). Visible text is unchanged.

Update PageHeader tests for the icon contract and add a focused icon
assertion to each affected page test.
2026-07-24 23:06:03 +01:00
JingleManSweep e0c1a6725c Merge pull request #338 from ipnet-mesh/fix/admin-profile-edit
fix(profiles): restore admin ability to edit other users' profiles
2026-07-24 22:45:58 +01:00
Louis King 86c8079fb1 fix(profiles): restore admin ability to edit other users' profiles
The update_profile endpoint used RequireUserOwner which returns only the
caller's user_id — no role information. The ownership check blocked ALL
non-owner edits with 403, including admins. This regressed in d37b30a
when the old Member model (RequireAdmin) was replaced with UserProfile.

Backend: read X-User-Roles header directly in update_profile (same
pattern as node_tags.py / routes.py) and bypass the ownership check when
the admin role is present. Regular members editing their own profiles
are unaffected — RequireUserOwner stays as the dependency.

Frontend: extract ProfileEditForm component (with data-testids) from
OwnProfileView. PublicProfileView now shows an inline edit form when an
admin views another user's profile. Owner still gets the existing edit
link; non-admins see nothing.

Tests:
- Backend: test_update_profile_admin_can_edit_other (admin edits other
  user's profile, asserts 200 + all fields updated)
- Vitest: 4 new tests — admin button visibility, non-admin hidden,
  owner link vs admin button, form submission to correct endpoint
- E2E: admin-profile-edit.spec.ts (admin edits Mem South's profile,
  verifies persistence; admin on own profile sees no admin button);
  members.spec.ts negative assertion (member sees no admin button)
2026-07-24 22:41:32 +01:00
JingleManSweep 140bd437ed Merge pull request #337 from ipnet-mesh/feat/routes-mine-filter
feat(routes): add 'my routes only' filter
2026-07-24 22:26:22 +01:00
Louis King acacea0d85 feat(routes): add 'my routes only' filter
Adds a checkbox filter to the Routes page that narrows the list to
routes owned by the current user. The filter is URL-driven (?mine=true),
cached server-side automatically via the existing key builder, and only
shown to operators/admins (members can't own routes).

Backend: mine query param on GET /api/v1/routes filters by created_by
matching the caller's X-User-Id. Legacy NULL routes are excluded.

Tests:
- Backend: 6 new TestRouteMineFilter tests (own/other/null/admin/default)
- Vitest: 6 new tests (param passing, role gating, checkbox state)
- E2E: new seed route owned by pw-operator + mine filter spec
2026-07-24 22:22:17 +01:00
JingleManSweep ca88e9f4e9 Merge pull request #336 from ipnet-mesh/feat/routes-ownership
feat(routes): ownership-based edit/delete permissions
2026-07-24 22:02:03 +01:00
JingleManSweep 2ecca1150a Merge branch 'main' into feat/routes-ownership 2026-07-24 21:59:29 +01:00
Louis King 08a637e276 fix(routes): preserve existing ownership on admin edit
Admins editing an operator-created route no longer steal ownership.
Ownership transfer now happens only for legacy (NULL created_by) routes.
This ensures operators retain edit access to their routes after an admin
makes a small tweak.
2026-07-24 21:54:23 +01:00
Louis King 333b094a77 feat(routes): ownership-based edit/delete permissions
Replace visibility-tier-based write permissions with ownership-based:
operators can only edit/delete routes they created; admins can modify
any route and take ownership on edit. Each route stores the creator's
OIDC user_id (created_by column). Legacy routes with NULL created_by
are admin-only.

The creator's friendly name is resolved from UserProfile and displayed
on the route card with a profile link. Edit/delete buttons are hidden
per-route based on ownership rather than a flat role check.

- Migration: add nullable routes.created_by (batch mode, SQLite-safe)
- _assert_route_modifiable: dual-check visibility (404) + ownership (403)
- create_route: stamps created_by, ensures profile exists
- update_route: admin edits transfer ownership (logged)
- RouteOwner schema mirrors AdoptedByUser pattern
- Batch owner resolution in list endpoint (avoids N+1)
- Frontend: per-route canEdit gate, owner badge with profile link
- Tests: 20+ backend ownership tests, 5 frontend gating tests, e2e assertions
- Docs: routes.md + auth.md updated for ownership model
2026-07-24 21:46:06 +01:00
JingleManSweep 20aaf46a16 Merge pull request #335 from ipnet-mesh/feat/routes-operator-management
feat(routes): allow operators to manage routes
2026-07-24 21:06:00 +01:00
Louis King 83a936cd33 feat(routes): allow operators to manage routes
Operators can now create, edit, and delete routes (previously admin-only).
A user may never scope a route above their own role tier: an operator
creating/editing an admin-visibility route is rejected (403 on the
visibility value, 404 on touching an existing higher-visibility route),
preventing them from creating routes they could then never see or modify.

- routes.py: RequireAdmin -> RequireOperatorOrAdmin on create/update/delete;
  add visibility-cap enforcement helpers reusing the existing
  resolve_user_role / VISIBILITY_LEVELS ladder
- web/app.py: proxy access map admits operator for routes POST/PUT/DELETE
- Routes.tsx: canManage gate (admin||operator) on Add/Edit/Delete; visibility
  <select> filters options by caller tier so operators never see 'admin'
- tests: operator-tier coverage (create/update/delete at/below/above level),
  proxy access-map assertion, vitest role-gating + filtered select
- e2e: mint operator session + routes-operator spec
- docs: routes.md + auth.md operator/visibility-cap notes
2026-07-24 20:59:25 +01:00
JingleManSweep 5ead0fbad3 Merge pull request #333 from ipnet-mesh/renovate/react-i18next-17.x
fix(deps): update dependency react-i18next to v17
2026-07-24 20:55:17 +01:00
renovate[bot] 6bc3d0ab42 fix(deps): update dependency react-i18next to v17 2026-07-24 19:50:33 +00:00
JingleManSweep 7d3974fc3a Merge pull request #332 from ipnet-mesh/renovate/i18next-26.x
fix(deps): update dependency i18next to v26
2026-07-24 20:49:11 +01:00
renovate[bot] d5adc5c392 fix(deps): update dependency i18next to v26 2026-07-24 19:39:32 +00:00
JingleManSweep e882b0ef95 Merge pull request #334 from ipnet-mesh/renovate/major-react-router-monorepo
fix(deps): update dependency react-router to v8
2026-07-24 20:38:01 +01:00
JingleManSweep d7c7814801 Merge branch 'main' into renovate/major-react-router-monorepo 2026-07-24 20:35:27 +01:00
JingleManSweep 4726700e4c Merge pull request #328 from ipnet-mesh/renovate/actions-setup-node-7.x
chore(deps): update actions/setup-node action to v7
2026-07-24 20:34:55 +01:00
renovate[bot] 7c2cd4f538 fix(deps): update dependency react-router to v8 2026-07-23 15:44:06 +00:00