- Replace single NETWORK_RADIO_CONFIG comma-delimited string with six
individual environment variables: NETWORK_RADIO_PROFILE, _FREQUENCY,
_BANDWIDTH, _SPREADING_FACTOR, _CODING_RATE, _TX_POWER
- Radio config fields now use raw numeric types (float/int) with units
applied dynamically via RadioConfig.format_for_display()
- Add FEATURE_RADIO_CONFIG feature flag to control radio config panel
visibility on the home page (default: enabled)
- Remove from_config_string class method (no backwards compatibility)
- Update Click CLI options, create_app() signature, and _build_config_json()
- Update docker-compose.yml, .env.example, README.md, AGENTS.md
- Add upgrading.md v0.12.0 section with migration instructions
- Add test coverage for schema, config, and feature flag
- Group channel cards by visibility with section headings
- Move channels before messages in all nav menus for logical grouping
- Add optgroup labels (Standard/Custom) to message channel filter
- Capitalize built-in "Test" channel name for consistency
- Shorten "Advertisements" to "Adverts" in UI labels
- Lay out channel cards with side-by-side QR codes
- Shrink homepage nav cards for better fit
Replaces env-var channel keys with a Channel database model and periodic
DB refresh in the collector. Adds Channels dashboard page with QR codes,
channel visibility filtering on messages/dashboard APIs, and channel card
navigation to filtered messages view.
- Fix Members page badge showing higher count than displayed profiles
by counting operators + members instead of all profiles
- Filter dropdowns on Nodes, Advertisements, and Map pages to show
only operators (since only operators can adopt nodes)
- Add roles field to /map/data profiles for client-side filtering
- Add all_operators and filter_operator_label i18n keys (en, nl)
- Fix flash banner test isolation from .env NETWORK_ANNOUNCEMENT
Add NETWORK_ANNOUNCEMENT env var that displays a dismissible flash banner
on every page when set. Announcement text supports Markdown (bold, italic,
links, inline code) rendered to HTML server-side at startup.
Add OIDC_ROLE_TEST config var (default: 'test') to exclude test users
from dashboard stats, member counts, and the Members page. Uses
server-side filtering with exclude_test query param (default: true) and
client-side defense-in-depth filter in members.js.
- Add oidc_role_test to WebSettings in config.py
- Exclude test users from operator/member count queries in dashboard.py
- Add exclude_test param to GET /api/v1/user/profiles in user_profiles.py
- Filter test users client-side in members.js via role_names.test config
- Wire oidc_role_test into app.state and frontend config in web/app.py
- Document OIDC_ROLE_TEST in AGENTS.md and .env.example
The web proxy's endpoint access mapping previously required one of
(admin/operator/member) roles for PUT /api/v1/user/profile. This
blocked OIDC users with no assigned roles from saving their own profile.
Add an _AUTHENTICATED sentinel access level that grants access to any
logged-in user regardless of roles, and apply it to the profile PUT
endpoint. The API layer already enforces owner-only checks via
RequireUserOwner, so the proxy role gate was redundant.
- Add .prose > :first-child margin-top:0 to eliminate double-spacing before headings
- Add nested list CSS (circle/square bullets, lower-alpha/roman numbering)
- Replace map popup <p>/space-y-1 layout with CSS grid for aligned label-value pairs
- Convert map filter card to collapsible <details> with state persistence
- Add nested list HTML output tests and markdown features docs
Bundle SPA JavaScript with esbuild for production builds, generating
content-hashed filenames for immutable caching. Vendor assets (Leaflet,
Chart.js, QRCode.js) get SHA256-based query params. Locale JSON files
get a combined hash version. Falls back to unbuiltsources when dist/
is absent.
Replace the dedicated admin tag management page with inline tag editing
on the node detail page. Operators can now edit tags directly on nodes
they've adopted; admins retain unrestricted access.
Key changes:
- Remove admin SPA page (admin/index.js, admin/node-tags.js)
- Add inline tag editor to node-detail.js with add/edit/delete modals
- Replace RequireAdmin with RequireOperatorOrAdmin for tag API routes
- Add ownership check: operators restricted to adopted nodes only
- Add validate_and_coerce_tag_value for number/boolean coercion
- Remove unused bulk endpoints (copy, move, replace all)
- Use AbortController for event listeners to prevent accumulation
on lit-html DOM reuse across re-renders
- Track Leaflet map instance at module scope for defensive cleanup
- Fix checkAuthResponse to only redirect on 401 (not 403)
- Update tests for new OIDC-based auth model
- Update en.json locale, i18n.md, upgrading.md, AGENTS.md
Replace the role=infra NodeTag convention with UserProfileNode adoption
as the canonical infrastructure indicator across map, Prometheus metrics,
and alerting. Renames is_infra to is_adopted, infra_center to
adopted_center. Map icons change to blue (adopted) / green (normal),
with all adoption UI gated on OIDC_ENABLED. Adds meshcore_nodes_adopted
gauge and Alembic migration to clean up obsolete tags.
- Add member filter dropdown to Nodes, Advertisements, and Map pages
(visible only when OIDC is enabled), showing profiles as
"Name (Callsign)" format
- Add adopted_by query param to /map/data endpoint for server-side
member filtering on the map
- Fix members feature flag: auto-disables when OIDC is disabled
- Fix profile page: remove duplicate adopted nodes section,
extract renderMemberSince(), align form labels with fixed-width
label column
- Add i18n keys: common.all_members, common.filter_member_label
- Add map endpoint adopted_by tests, update features tests
Remove the static Member model/table, CRUD API, YAML seed files, and
admin UI. Replace with UserProfile-driven members page that reads roles
from OIDC identity provider. Key changes:
- Drop members table, add roles column to user_profiles (Alembic migration)
- Add GET /api/v1/user/profiles (paginated, no user_id exposed)
- Add GET /api/v1/user/profile/me (auto-creates profile for current user)
- Replace member_id node tag filter with adopted_by (profile UUID)
- Members page now shows profiles grouped by operator/member roles
- Profile page supports public view (/profile/:id) and owner edit (/profile)
- Node detail page shows adoption card side-by-side with public key card
- Auto-create user profile during OIDC login callback
- Hide Adopted Nodes section for non-operator/admin users
- Add member since date to profile cards
- Add role badges and adopted node badges to member tiles
- Add antenna/users icons to Members page group headers
- Add OPTIONS to the web API proxy route methods for CORS preflight support
- Fix event listener accumulation in admin/node-tags.js and admin/members.js
using AbortController with cleanup functions returned to the SPA router.
lit-html reuses DOM elements across re-renders, causing addEventListener
calls to accumulate and fire multiple times per form submission.
- Rename admin routes from /a/ prefix to /admin/ for clarity
- Add debug logging for admin route access and OIDC role checks
- Move auth section in navbar after theme toggle
- Update tests and AGENTS.md accordingly
- Pass client_id in logout redirect so LogTo can validate post_logout_redirect_uri
- Add OIDC_POST_LOGOUT_REDIRECT_URI config option with fallback derivation
- Move session.clear() after logout_redirect() to allow state data save
- Add 'username' to strip_userinfo() name fallback chain (LogTo uses this)
- Strip quotes from OIDC_SCOPES and pass as list to Authlib (fixes direnv
quoting issue where literal quotes were sent in the authorization URL)
- Add OIDC_POST_LOGOUT_REDIRECT_URI to config, app state, and docs
- Add INFO-level logging to callback and logout handlers for diagnostics
- Update .env.example, README.md, AGENTS.md, docs/upgrading.md
Simplify the variable name to remove the legacy LetsMesh decoder prefix.
Also fix unparenthesized except tuples in web/app.py and promote the
parenthesized-exception rule to a prominent position in AGENTS.md.
Pin starlette<1.0.0 to avoid TemplateResponse breaking change, update
the TemplateResponse call to new-style request-first signature, and
bump Python version to 3.14 across Dockerfile, pyproject.toml, and
pre-commit hooks.
Update path hash handling to accept variable-length hex-encoded hashes
(e.g. "4a" for single-byte, "b3fa" for multibyte) instead of requiring
exactly 2-character hashes. Bump meshcore dependency to >=2.3.0.
- Update normalizer to accept even-length hex strings >= 2 chars
- Update schemas and model docstrings for variable-length hashes
- Add tests for multibyte and mixed-length path hash round-trips
- Fix web test flakiness from local .env datetime locale leaking
- Use hmac.compare_digest for constant-time API key comparison in auth
and metrics endpoints to prevent timing attacks
- Escape user-controlled data in admin JS templates (members, node-tags)
to prevent XSS via innerHTML
- Escape </script> sequences in embedded JSON config to prevent XSS
breakout from <script> blocks
- Add configurable WEB_TRUSTED_PROXY_HOSTS setting instead of trusting
all proxy headers unconditionally
- Warn on startup when admin is enabled with default trust-all proxy
- Remove legacy HTML dashboard endpoint (unused, superseded by SPA)
- Add comprehensive auth and dashboard test coverage
Implement cache-control middleware to optimize browser caching and reduce
bandwidth usage. Static files are cached for 1 year when accessed with
version parameters, while dynamic content is never cached.
Changes:
- Add CacheControlMiddleware with path-based caching logic
- Register middleware in web app after ProxyHeadersMiddleware
- Add version query parameters to CSS, JS, and app.js references
- Create comprehensive test suite (20 tests) for all cache behaviors
Cache strategy:
- Static files with ?v=X.Y.Z: 1 year (immutable)
- Static files without version: 1 hour (fallback)
- SPA shell HTML: no-cache (dynamic config)
- Health endpoints: no-cache, no-store (always fresh)
- Map data: 5 minutes (location updates)
- Custom pages: 1 hour (stable markdown)
- API proxy: pass-through (backend controls)
All 458 tests passing, 95% middleware coverage.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Operators can now disable specific pages (Dashboard, Nodes, Advertisements,
Messages, Map, Members, Pages) via FEATURE_* environment variables. Disabled
features are fully hidden: removed from navigation, return 404 on routes,
and excluded from sitemap/robots.txt. Dashboard auto-disables when all of
Nodes/Advertisements/Messages are off. Map auto-disables when Nodes is off.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The admin pages only checked config.admin_enabled but not
config.is_authenticated, allowing unauthenticated users to access
admin functionality when WEB_ADMIN_ENABLED=true. Additionally, the
API proxy forwarded the service-level Bearer token on all requests
regardless of user authentication, granting full admin API access
to unauthenticated browsers.
Server-side: block POST/PUT/DELETE/PATCH through the API proxy when
admin is enabled and no X-Forwarded-User header is present.
Client-side: add is_authenticated check to all three admin pages,
showing a sign-in prompt instead of admin content.
https://claude.ai/code/session_01HYuz5XLjYZ6JaowWqz643A
Replace server-side rendered Jinja2 page routes with a client-side SPA
using ES modules, lit-html templating, and a custom History API router.
All page rendering now happens in the browser with efficient DOM diffing.
Key changes:
- Add SPA router, API client, shared components, and 14 page modules
- Serve single spa.html shell template with catch-all route
- Remove server-side page routes (web/routes/) and legacy JS files
- Add centralized OKLCH color palette in CSS custom properties
- Add colored nav icons, navbar spacing, and loading spinner
- Add canonical URL and SEO path exclusions to SPA router
- Update charts.js to read from shared color palette
- Update tests for SPA architecture (template-agnostic assertions)
- Update AGENTS.md and README.md with SPA documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add GPS coordinate fallback: use tag coords, fall back to model coords
- Filter out nodes at (0, 0) coordinates (likely unset defaults)
- Add "Show" filter to toggle between All Nodes and Infrastructure Only
- Add "Show Labels" checkbox (labels hidden by default, appear on hover)
- Infrastructure nodes display network logo instead of emoji
- Add radius-based bounds filtering (20km) to prevent outlier zoom issues
- Position labels underneath pins, centered with transparent background
- Calculate and return infra_center for infrastructure node focus
- Initial map view focuses on infrastructure nodes when available
- Update popup button to outline style
- Add comprehensive tests for new functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace PAGES_HOME with CONTENT_HOME configuration (default: ./content)
- Content directory now contains pages/ and media/ subdirectories
- Add support for custom logo at $CONTENT_HOME/media/images/logo.svg
- Custom logo replaces favicon and navbar/home logos when present
- Mount media directory as /media for serving custom assets
- Simplify default logo to generic WiFi-style radiating arcs
- Update documentation and example directory structure
- Update tests for new CONTENT_HOME structure
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Renamed network route, template, and tests to dashboard
- Added logo.svg for favicon and navbar branding
- Created reusable Jinja2 icon macros for navigation and UI elements
- Updated home page hero layout with centered content and larger logo
- Added Map button alongside Dashboard button in hero section
- Navigation menu items now display icons before labels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allows adding static content pages (About, FAQ, etc.) as markdown files
with YAML frontmatter. Pages are stored in PAGES_HOME directory (default:
./pages), automatically appear in navigation menu, and are included in
the sitemap.
- Add PageLoader class to parse markdown with frontmatter
- Add /pages/{slug} route for rendering custom pages
- Add PAGES_HOME config setting to WebSettings
- Add prose CSS styles for markdown content
- Add pages to navigation and sitemap
- Update docker-compose.yml with pages volume mount
- Add comprehensive tests for PageLoader and routes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /api/v1/nodes/prefix/{prefix} for prefix-based node lookup
- Change /api/v1/nodes/{public_key} to exact match only
- /n/{prefix} now simply redirects to /nodes/{prefix}
- /nodes/{key} resolves prefixes via API and redirects to full key
When users try to access /a/ without valid OAuth2Proxy headers (e.g.,
GitHub account not in org), they now see a friendly 403 page instead
of a 500 error. Added authentication checks to all admin routes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix XSS vulnerability by using data attributes instead of inline
onclick handlers in node_tags.html template
- Fix URL injection by using urlencode for all redirect URL parameters
- Add validation to reject moves where source and destination nodes
are the same (returns 400 Bad Request)
- Add error handling for response.json() calls that may fail
- Add missing test coverage for update endpoint error scenarios
Implement CRUD operations for NodeTags in the admin interface:
- Add NodeTagMove schema for moving tags between nodes
- Add PUT /nodes/{public_key}/tags/{key}/move API endpoint
- Add web routes at /a/node-tags for tag management
- Create admin templates with node selector and tag management UI
- Support editing, adding, moving, and deleting tags via API calls
- Add comprehensive tests for new functionality
The interface allows selecting a node from a dropdown, viewing its
tags, and performing all CRUD operations including moving a tag
to a different node without having to delete and recreate it.
Members can now have multiple associated nodes, each with a public_key
and node_role (e.g., 'chat', 'repeater'). This replaces the single
public_key field on members with a one-to-many relationship.
Changes:
- Add MemberNode model for member-node associations
- Update Member model to remove public_key, add nodes relationship
- Update Pydantic schemas with MemberNodeCreate/MemberNodeRead
- Update member_import.py to handle nodes list in seed files
- Update API routes to handle nodes in create/update/read operations
- Add Alembic migration to create member_nodes table and migrate data
- Update example seed file with new format
The test was checking for adv_type values (REPEATER, CLIENT) but the
nodes.html template doesn't display that column. Updated to check for
public key prefixes instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Member database model with name, callsign, role, description, contact, and public_key fields
- Add Member Pydantic schemas (MemberCreate, MemberUpdate, MemberRead, MemberList)
- Add members table to initial migration
- Add members API endpoints (GET/POST/PUT/DELETE /api/v1/members)
- Add member_import.py for importing from JSON files
- Update web layer to fetch members from API instead of file
- Add SEED_HOME setting (defaults to ./seed) for seed data files
- Add 'collector seed' command to import node_tags.json and members.json
- Rename tags.json to node_tags.json for consistency
- Move example seed data from example/data/* to example/seed/
- Update tests and configuration
- Create conftest.py with MockHttpClient for testing web routes
- Add test_home.py with 9 tests for home page
- Add test_members.py with 11 tests for members page and load_members function
- Add test_network.py with 7 tests for network overview page
- Add test_nodes.py with 15 tests for nodes list and detail pages
- Add test_map.py with 12 tests for map page and data endpoint
- Add test_messages.py with 13 tests for messages page with filtering
- All 67 web tests pass, 184 total tests pass
- Update TASKS.md to mark Phase 5 as 100% complete (186/221 total)