Commit Graph

901 Commits

Author SHA1 Message Date
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
JingleManSweep 4ace88dfa9 Merge pull request #324 from ipnet-mesh/feat/react-frontend-phase1
feat(web): migrate SPA from lit-html to React 19 + TS + Vite
2026-07-22 23:07:52 +01:00
JingleManSweep 005c5102ae Merge branch 'main' into feat/react-frontend-phase1 2026-07-22 23:04:09 +01:00
JingleManSweep 2929e15753 Merge pull request #322 from ipnet-mesh/renovate/anomalyco-opencode-1.x
chore(deps): update anomalyco/opencode action to v1.18.4
2026-07-22 23:03:14 +01:00
JingleManSweep 97db402aad Merge branch 'main' into feat/react-frontend-phase1 2026-07-22 23:01:38 +01:00
renovate[bot] 5cd8af10b0 chore(deps): update anomalyco/opencode action to v1.18.4 2026-07-22 22:01:09 +00:00
JingleManSweep b0b07f4695 Merge pull request #323 from ipnet-mesh/renovate/daisyui-5.x-lockfile
chore(deps): update dependency daisyui to v5.7.0
2026-07-22 23:00:48 +01:00
Louis King a3136e59e0 ci: add Node.js setup to Lint job for frontend-typecheck hook 2026-07-22 23:00:09 +01:00
Louis King c8cc049651 feat(web): React SPA shell, markdown migration, vitest coverage, v0.17 docs
- Migrate footer, error page, and announcements to React; slim Jinja2
  shell to SEO/head/config/fonts/theme-init only
- Replace server-side markdown rendering with react-markdown (removes
  Python `markdown` dependency); custom pages + announcements ship
  raw markdown to the client
- Add heading-anchor deep-links with CSS override for inherited colors
- Extract pure helpers from pages (messageHelpers, mapMath, routesHelpers,
  profileHelpers, packetHelpers, packetGroupHelpers) for unit testability
- Add comprehensive vitest suite: 59 test files, 315 tests covering all
  components, pages, and extracted helpers; global i18next mock + matchMedia
  stub + AbortController-aware apiMock in test infrastructure
- Fix SortableTable test DOM nesting (<th> inside proper <table> context)
- Update docs for v0.17.0: upgrading.md release notes, README.md project
  structure + build description, i18n.md path fix
- Delete REACT_MIGRATION.md (migration complete, unreferenced)
- Add announcements dismiss-persistence + custom-page 404 E2E specs
2026-07-22 22:55:20 +01:00
Louis King 51d16c7766 fix(e2e): restore observer-node coordinates in seed
A flake8 B007 fix at commit time renamed the whole observer-specs tuple
to (_area, _lat, _lon), but the Node() call below still referenced lat/lon.
Those names then resolved to the leaked values from the preceding
content_specs loop (last iteration = Delta's None/None), so all observer
nodes were seeded without coordinates and dropped from the map (7 -> 3
markers). Underscore only `area` (genuinely unused in that loop); lat/lon
are used in Node(), so they stay as loop vars.
2026-07-22 14:37:51 +01:00
Louis King 94dd4c9b42 test(e2e): replace Python e2e suite with Playwright (headless Chromium)
Replaces the httpx-based smoke tests in tests/e2e/ with a browser E2E suite
under e2e/, running against a self-contained throwaway stack that never
touches the local development database.

Stack & data isolation (e2e/docker-compose.test.yml):
- Own ephemeral Postgres 17 (schema via the `migrate` service / Alembic),
  distinct project name + named volumes, no host DB port, no \${VAR}
  interpolation. `make e2e-down` destroys everything.
- OIDC enabled with a known session secret; WEB_AUTO_REFRESH_SECONDS=2 so
  polling is assertable; CONTENT_HOME mounts a test markdown page.
- Deterministic seeder (e2e/seed_data.py) run via global setup inside the
  collector container: nodes/observers with `area` tags, adverts, messages
  on the public (17) + a custom channel, raw packets + path hops keyed to
  node prefixes, a route + health/history, profiles + adoptions, and
  event_observers rows so the observer filter/badges resolve.

Auth & tests:
- Forged signed `meshcore-session` cookies (e2e/mint_session.py,
  itsdangerous) for admin/member identities -> storageState; no real IdP
  needed. 31 specs across 12 files cover global nav/theme, profile
  menu+edit, home hero, dashboard widgets, list filters/auto-refresh/row
  actions, observer toggles, the path-node overlay, map filters +
  show-labels, members, markdown pages, and routes add/edit/delete
  (persistence, validation, confirm dialog). workers:1 / fullyParallel:false
  against the single shared backend; targeted data-testids added to React
  components for stable selectors.

Fixes surfaced while running the suite on fresh Postgres:
- Migration 5e3b712ccf10 aborted on Postgres: its route-health backfill
  queries the live Route model (which now has max_path_length) before that
  column exists. The swallowed error left the transaction aborted, killing
  the subsequent alembic_version stamp. Wrapped the backfill in a SAVEPOINT
  so a failure rolls back cleanly without blocking the migration.
- Restored the full ABUSE_* env set required by the MQTT broker, and set
  the web service's API_KEY to the admin key (admin writes go through the
  proxy as a Bearer token).

31/31 passing; tsc (frontend+e2e), pytest (1460), and pre-commit green.
2026-07-22 10:41:53 +01:00
Louis King 789b756aa8 Merge branch 'main' of github.com:ipnet-mesh/meshcore-hub into feat/react-frontend-phase1 2026-07-22 08:19:32 +01:00
Louis King de9784211d feat(web): adopt TanStack Query for SPA data layer; consolidate shared UI
Migrate the React SPA off the bespoke useApiFetch hook and raw useEffect fetches to TanStack Query: useQuery/useQueries for reads, useMutation + invalidateQueries for writes; a central query-key factory and invalidation helpers mirroring the backend cache_invalidation prefixes (channels/routes/nodes/messages/profiles/dashboard/adoptions); polling via refetchInterval (useAutoRefresh now returns pause state only); RouteCard self-fetches its detail/history, dropping the hand-rolled caches. Adds QueryClientProvider in App, a renderWithProviders test helper, and deletes useApiFetch.

Also consolidates recurring UI into reusable components (Breadcrumbs, ListToolbar, Modal/ConfirmDialog, NotFoundState, TimeAgo, Definition, CopyableValue, MeshQrCode, Badges, SectionGroup, PageHeader) and fixes the FilterForm clear button, merged toggle wrappers, and role-aware channels/messages cache keys so client invalidation matches the server.

Verified: tsc --noEmit clean, vitest 113 passed, pytest test_web 251 + test_cache 119 passed, pre-commit --all-files green.
2026-07-21 23:24:58 +01:00
Louis King bf8e0620a5 fix(web): path-node popup scrolls with page; wire frontend tests/typecheck into make + pre-commit
- PacketGroupDetail: render the path-node popover via createPortal(document.body)
  with position:absolute + document coordinates (rect + scrollX/scrollY) instead of
  position:fixed with one-shot viewport coords, so it scrolls with the page rather
  than staying pinned to the viewport. Outside-click/Escape close unchanged (DOM-based).
- Makefile: 'make test' now runs pytest then the frontend vitest suite via a new
  'test-frontend' target (npm run test:frontend).
- pre-commit: add a local 'frontend-typecheck' hook (language: system) running
  'npm run typecheck' (tsc --noEmit) on TS/TSX + tsconfig/package(-lock).json changes.
- AGENTS.md: document the pre-commit TS gate and that 'make test' includes the frontend.

Verified: tsc clean, vitest 49 passed, pre-commit --all-files green, make test
(1459 backend passed + 49 frontend passed).
2026-07-21 20:25:50 +01:00
Louis King 527c860bf8 feat(web): frontend CI, vitest suite, navbar→React SPA shell — Phase 5
Frontend CI (closes the no-coverage gap for the TSX):
- ci.yml: new 'frontend' job — npm ci, tsc --noEmit, test:frontend, build.
- package.json: engines.node>=20, test:frontend/typecheck scripts.

vitest unit + component tests (jsdom, @testing-library/react):
- utils/charts.test.ts — tier math + every chart builder.
- utils/format.test.ts — parseAppDate, formatNumber, truncateKey, emoji helpers,
  formatRelativeTime.
- components/Navbar.test.tsx — feature-gated nav, custom pages, OIDC/maintenance
  auth gating.
- components/Announcements.test.tsx — banner rendering, ordering, dismiss +
  sessionStorage (covers behaviour moved out of the Python suite).

Navbar → React (full SPA shell):
- New Navbar/ThemeToggle/Announcements components + useNavItems hook (shared
  feature-gated nav for desktop + mobile); nav uses react-router NavLink
  (client-side nav + auto active) — drops the imperative data-nav-link bridge.
- main.tsx single root; App.tsx renders Navbar + Announcements above routed <main>.
- spa.html slimmed to SEO/config/footer shell (Jinja2 navbar/banners/theme script
  removed; #app is a plain div React fills).
- Backend: _build_config_json exposes system_announcement/network_announcement.

Tests (server-rendered nav/banner assertions → config):
- conftest.py: get_app_config() helper (robust __APP_CONFIG__ extraction).
- test_features/app/home/pages/dashboard rewritten to assert __APP_CONFIG__;
  dashboard client-rendered-stats tests replaced with a shell assertion.

Docs: REACT_MIGRATION.md Phase 5 (incl. deliberately-skipped react-query/
Storybook/Playwright), AGENTS.md Frontend section.

Verified: tsc clean, npm run build, vitest (49 passed), pytest tests/test_web
(251 passed), full suite (1459 passed), pre-commit (passed).
2026-07-21 20:06:52 +01:00
Louis King a5fabf7d46 chore(web): remove lit-html fallback & legacy code — Phase 4
The React frontend is complete (Phases 1-3); the lit-html fallback is dead
(its vendor globals were removed in Phase 3). Delete it and the scaffolding:

- Delete the entire src/meshcore_hub/web/static/js/spa/ lit-html tree,
  LitBridge.tsx, and legacy.d.ts.
- Remove the @legacy alias from vite.config.ts and tsconfig.json.
- Remove lit-html and qrcodejs from package.json (both unused now).
- Remove the lit-html fallback {% else %} branch from spa.html — the Vite
  build is now required to serve the UI (no fallback bundle).

Tests (fallback no longer exists):
- test_home/advertisements/nodes/messages.py: assert the React mount point
  (id="app") instead of a bundled-or-fallback script tag.
- test_caching.py: JS-cache tests are header-only (static JS is bundled into
  dist/, absent in test env; the middleware sets headers on 404 too); the
  dist-bundle HTML test drops its fallback branch.

Docs:
- AGENTS.md: new Frontend (React) section (host-run npm/vite/tsc toolchain,
  react-chartjs-2/react-leaflet/react-qr-code, CSS load order); clarified the
  compose-stack rule to exempt frontend tooling.
- REACT_MIGRATION.md: Phase 4 complete, final file structure, decisions.

Verified: tsc --noEmit clean, npm run build, full pytest (1463 passed,
22 skipped), pre-commit (passed).
2026-07-21 19:13:01 +01:00
Louis King 715659607a feat(web): React charts/maps/QR — Phase 3
Replace all window.Chart / window.L / window.QRCode globals and the
charts.js helper script with bundled React components:

- react-chartjs-2: typed config builders in utils/charts.ts
  (buildLineChart, buildActivityChart, buildStackedBar, buildRoutesTrend,
  buildRouteDetailStrip + ChartColors, averageRouteTier, routeQualityToTier)
  and wrappers in components/charts/Charts.tsx (ActivityChart,
  TrendLineChart, StackedBarChart, RoutesTrendChart, RouteDetailStrip).
  utils/charts.ts imports chart.js/auto. Wired into Home, Dashboard, Routes.
- react-leaflet: MapPage rewritten with MapContainer/TileLayer/Marker/Popup
  + a useMap MapController for fit-bounds and memoized markers; NodeDetail
  static hero map with divIcon marker + OffsetCenter. Both import
  leaflet/dist/leaflet.css.
- react-qr-code: replaces window.QRCode in Channels and NodeDetail.

Bundling & shell:
- Chart.js, Leaflet (+CSS), react-qr-code now bundled by Vite; removed the
  leaflet/chart.js/qrcodejs vendor <script>/<link> tags from spa.html and
  their copy steps from build.js (fonts stay vendored). Deleted charts.js.
- Moved the Vite CSS bundle (asset_app_css) into <head> before app.css so
  app.css dark-mode Leaflet overrides win over the bundled leaflet.css.
- Dropped the chart globals from the Window type declaration.

Tests/docs:
- test_caching.py: removed charts.js-specific tests; generic JS-cache tests
  now target spa/app.js.
- Updated charts.js cross-references in collector/routes.py + test_routes.py
  to point at spa-react/utils/charts.ts.

Verified: tsc --noEmit clean, npm run build (153 modules),
pytest tests/test_web (255 passed), pre-commit (passed).
2026-07-21 19:03:04 +01:00
Louis King 8322b5cf9f feat(web): convert all 15 SPA pages to native React — Phase 2
Convert every remaining lit-html page to React 19 + TypeScript and wire
them directly into the router, removing all LitBridge usage from App.tsx:

- Home, CustomPage, Profile, Members, Channels
- Advertisements, Messages, Routes, Nodes, NodeDetail
- Packets, PacketDetail, PacketGroupDetail, Dashboard, MapPage

Pages use the shared React infrastructure (apiGet<T>, useAutoRefresh,
usePageTitle, useFormatDateTime, Pagination, FilterForm, SortableTable,
NodeDisplay, ObserverBadges, RouteTypeBadge, JsonTree, StatCard, icons).

Charts/maps still call window.Chart / window.L / window.QRCode / charts.js
globals — these move to react-chartjs-2 / react-leaflet in Phase 3.

The old lit-html code in spa/ is intentionally kept as the spa.html
fallback (rendered only when the Vite bundle is absent) and is still
referenced by 5 web tests; it will be removed in Phase 4.

Added IconSatelliteDish, IconRuler, IconHopSpan, IconPathLength icons.

Verified: tsc --noEmit clean, npm run build (94 modules),
pytest tests/test_web/ (256 passed), pre-commit (passed).
2026-07-21 16:50:33 +01:00
Louis King 6cd14a58aa feat(web): React frontend scaffolding — Phase 1
- Add Vite 6 + TypeScript build replacing esbuild, React 19, React Router 7
- LitBridge wraps unconverted lit-html pages inside React app lifecycle
- Shared components: SortableTable, Pagination, FilterForm, StatCard, JsonTree,
  NodeDisplay, ObserverBadges, RouteTypeBadge, icons, ErrorBoundary, Alerts
- Hooks: useAutoRefresh, usePageTitle; utils: api, format, clipboard
- i18n via react-i18next mirroring existing translation keys
- Native React pages: NotFound, Maintenance; all other routes via LitBridge
- Jinja2 shell (spa.html) preserved for navbar, SEO, vendor globals, theme
- build.js generates legacy-compatible assets.json from Vite manifest
- REACT_MIGRATION.md documents full plan and conversion patterns
2026-07-21 10:21:39 +01:00
renovate[bot] 44eb79d011 chore(deps): update dependency daisyui to v5.7.0 2026-07-21 03:50:57 +00:00
JingleManSweep f232b90187 Merge pull request #320 from ipnet-mesh/renovate/actions-setup-python-7.x
chore(deps): update actions/setup-python action to v7
v0.16.2
2026-07-20 14:58:16 +01:00
renovate[bot] e71918ca13 chore(deps): update actions/setup-python action to v7 2026-07-20 11:52:15 +00:00
JingleManSweep 53c1b36262 Merge pull request #321 from ipnet-mesh/feat/routes-max-path-length
feat(routes): add per-route max_path_length cap
2026-07-20 12:51:15 +01:00
Louis King 1ba481483f feat(routes): add per-route max_path_length cap
Adds a new nullable per-route knob that caps the total number of hops
in a candidate packet's path. Packets whose path exceeds the cap are
dropped from matching consideration entirely (before the subsequence
matcher runs), so over-long paths never count toward
packet_count_threshold. Complements the existing max_hop_span, which
only constrains the gap between the first and last matched configured
node.

- Route model + migration (additive, nullable, default null = unlimited)
- Threaded through matcher chain (_subsequence_indices early-return)
- All 5 evaluate/preview/recent_matches call sites updated
- API serializer/create/update/preview passthrough
- CLI seed YAML import (update + create paths)
- Frontend: distinct icons for span (<-o->) vs path-length (|<->|),
  always-rendered badges with infinity fallback, hover tooltips on
  every stats row item, i18n keys (en + nl)
- Tests: matcher unit tests (within/exceeds cap), API round-trip,
  CLI seed import
2026-07-20 12:43:47 +01:00
JingleManSweep f2137d0429 Merge pull request #319 from ipnet-mesh/fix/route-clear-observers
fix(routes): clearing observers in the edit modal now persists
v0.16.1
2026-07-19 23:09:09 +01:00
Louis King d3f6d72775 fix(routes): clearing observers in the edit modal now persists
The route edit modal builds its PUT body with a ternary that collapses
an empty observer list to null:

    observer_public_keys: observerPublicKeys.length > 0 ? observerPublicKeys : null,

Pydantic parses null as None, and the PUT handler's guard

    if body.observer_public_keys is not None:
        _sync_observers(session, route, observer_nodes)

skips the sync entirely when the field is None. So removing all
observers in the modal sent null -> no DB change. Adding observers
worked because a non-empty array passed the guard and _sync_observers
deleted + recreated.

The None-means-skip semantic is correct for true partial updates, so
the fix is on the frontend: the edit modal is a full-form PUT, so it
must always send the array. When empty, it sends [], which Pydantic
parses as [] (not None), the guard passes, and _sync_observers deletes
every existing RouteObserver row.

Tests: add test_update_clear_observers_with_empty_list next to the
existing test_update_observers as a regression guard. It seeds one
observer via PUT, then PUTs observer_public_keys: [] and asserts both
the response and a fresh GET come back with an empty list.
2026-07-19 23:05:41 +01:00
JingleManSweep ddf747fe68 Merge pull request #318 from ipnet-mesh/chore/dashboard-routes-community-only
feat(dashboard): limit Route Health widget to community routes
v0.16.0
2026-07-19 21:59:57 +01:00
Louis King fdea046749 feat(dashboard): limit Route Health widget to community routes
The Route Health and Routes Trend dashboard widgets pull from
GET /api/v1/dashboard/routes-overview, which previously filtered
routes by the caller's role tier (anonymous saw community, admin saw
all four tiers). Operators and admins therefore saw member/operator/
admin-tier routes mixed into the dashboard, even though the dedicated
/routes page is the working surface for managing those private tiers.

This change makes the dashboard widget surface ONLY community-tier
routes, regardless of the caller's role. Operators and admins still
see all four tiers on the /routes management page (unchanged).

Implementation:
- get_routes_overview: drop resolve_user_role/get_max_visibility_level;
  push the filter into the SQL query
  (where Route.visibility == RouteVisibility.COMMUNITY.value) so
  higher-tier rows are no longer loaded just to be discarded.
- _dashboard_routes_overview_key_builder: drop the role dimension from
  the cache key. The response is now identical across roles, so the
  per-role cache slots were storing four identical copies. The prefix
  is preserved so invalidate_routes' pattern invalidation still hits.
- Drop unused VISIBILITY_LEVELS import; add RouteVisibility to the
  models import.

Tests:
- test_visibility_filter_hides_admin_routes renamed to
  test_dashboard_only_shows_community_routes_regardless_of_role;
  now seeds one route per visibility tier and asserts every role
  (anonymous/member/operator/admin) sees only ['Public'].
- test_cache_key_is_role_scoped renamed to
  test_cache_key_is_role_agnostic; asserts all four role-variants
  produce the SAME cache key (no 'role=' dimension).
2026-07-19 21:57:03 +01:00
JingleManSweep f3c8da56a4 Merge pull request #317 from ipnet-mesh/chore/routes-default-thresholds
chore(routes): default packet_count_threshold=5 and 3× clear multiplier
2026-07-19 21:44:59 +01:00
Louis King 8cf5dadc38 chore(routes): default packet_count_threshold=5 and clear multiplier=3x
Change the new-route defaults so the create modal, REST API, YAML
import, and preview helper pre-fill packet_count_threshold=5 instead
of 3, and bump the effective-clear auto-multiplier from 2x to 3x so
the default comfort bar tracks to 15 for a default threshold of 5.

Schema/model/cli/preview:
- schemas/routes.py: RouteCreate + RoutePreviewRequest
  packet_count_threshold default 3 -> 5 (clear_threshold stays None)
- models/route.py: Route INSERT default 3 -> 5 (clear_threshold stays
  nullable, no default)
- collector/cli.py: YAML import fallbacks 3 -> 5
- collector/routes.py: preview helper fallback 3 -> 5; module constant
  CLEAR_DEFAULT_MULTIPLIER 2 -> 3 (drives effective_clear_threshold)
- api/metrics.py: route_clear gauge help text 2x -> 3x

UI:
- web/spa/pages/routes.js: new-route modal pre-fills
  packet_count_threshold=5; placeholder shows 3x threshold; parseInt
  fallback 3 -> 5. clear_threshold field unchanged (clearing still
  sends null for the auto-tracking behaviour).

Docs:
- docs/routes.md defaults table: window_hours 24->48, threshold 3->5,
  clear (2x)->(3x), max_hop_span (unlimited)->8. Previous four
  entries now match the shipped defaults (some were stale from #316).
- docs/seeding.md YAML example: window_hours 24->48, threshold 3->5,
  commented clear_threshold example 10->15 with 3x note.

No migration: packet_count_threshold uses Python-side default= (no
server_default), so existing rows keep their stored values. Existing
routes with clear_threshold=NULL continue to track via the new 3x
multiplier at evaluation time.
2026-07-19 21:40:26 +01:00
JingleManSweep acd3a045ca Merge pull request #316 from ipnet-mesh/chore/routes-default-window-span
chore(routes): default window_hours=48 and max_hop_span=8
2026-07-19 21:12:42 +01:00
Louis King ae003ded58 chore(routes): default window_hours=48 and max_hop_span=8
Change the new-route defaults so the create modal and API/CLI paths
pre-fill a 48h evaluation window and a max-hop-span of 8 instead of
the previous 24h / unlimited (∞).

- schemas/routes.py: RouteCreate + RoutePreviewRequest defaults
- models/route.py: SQLAlchemy INSERT-time defaults
- collector/cli.py: YAML import fallbacks
- web/static/js/spa/pages/routes.js: new-route modal pre-fills 48/8

No migration: window_hours/max_hop_span use Python-side default=
(no server_default), so existing rows are untouched. Clearing the
max-hop-span field in the UI still sends null (no cap).
2026-07-19 21:09:19 +01:00