Commit Graph

932 Commits

Author SHA1 Message Date
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
renovate[bot] 694176b2d8 chore(deps): update actions/setup-node action to v7 2026-07-23 01:40:55 +00:00
JingleManSweep 24f7e10c11 Merge pull request #327 from ipnet-mesh/fix/channels-query-cache-collision
fix: Channels page crash from query cache shape collision
v0.17.1
2026-07-22 23:44:38 +01:00
Louis King 91bfe0d043 fix: align Channels queryFn return shape with Dashboard
Both Dashboard.tsx and Channels.tsx used the same React Query key
qk.channels.list({}) but returned different data shapes: Dashboard
returned the raw {items, total} object while Channels returned just
the items array. When Dashboard loaded first, React Query cached the
raw object, and Channels' for...of on the cached object threw
'g is not iterable'.

Fix: Channels queryFn now returns the raw API response (matching
Dashboard), and channels extraction uses data?.items ?? []. Added
regression test that pre-seeds the cache with the Dashboard shape.
2026-07-22 23:40:56 +01:00
JingleManSweep 7b399a520f Merge pull request #326 from ipnet-mesh/renovate/react-dom-19.x-lockfile
chore(deps): update dependency react-dom to v19.2.8
v0.17.0
2026-07-22 23:12:15 +01:00
renovate[bot] d2c9c56671 chore(deps): update dependency react-dom to v19.2.8 2026-07-22 22:08:28 +00:00