Add F8/T9 + Phase 8 seed loader (links.yaml via existing meshcore-hub seed command, mirroring channels.yaml), referencing path nodes by public_key with expected_hash derived by the importer. Refine visibility to keep auth-scope but default to community (like channels).
Plan for a new Link entity that monitors whether packets traverse a configured ordered sequence of mesh nodes within a time window + count threshold, with background evaluation and Prometheus exposure. Includes shareable plain-language overview.
When an IdP name claim contains trailing whitespace (e.g. 'Matt '), every
proxied API request failed with 502 'Illegal header value' because httpx
enforces RFC 7230 which forbids leading/trailing OWS in header values.
Defense in depth:
- strip_userinfo() trims the IdP name at ingress (oidc.py)
- _sanitize_header_value() removes CTL/DEL chars at both header-injection
sites (API proxy + auth-callback bootstrap) in app.py
- update_profile() trims user-supplied names in PUT handler
X-User-Name is informational only (seeds non-unique UserProfile.name);
identity/auth are keyed on X-User-Id and X-User-Roles, so sanitization is
safe.
Add two horizontal 100% stacked-bar charts to the Dashboard, gated
behind the existing packets feature flag:
- Packet Types: top 6 event_type values + 'other' rollup
- Path Bytes: 1b/2b/3b path-hash width distribution (NULL excluded)
Driven by a single new cached endpoint
GET /api/v1/dashboard/packet-breakdown?days=7 returning pre-bucketed
counts. Frontend normalizes to percentages via a new
createStackedBarChart helper in charts.js with a 7-hue oklch palette.
The popover used position: fixed with viewport-relative coordinates, so
it stayed pinned to the viewport while the page scrolled away beneath
it. Switch to absolute positioning anchored to the document (adding
scrollX/scrollY) and read the badge's live rect on each reposition so
it tracks the anchor after the async node fetch resolves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Charts jumped from a single column straight to N-across at the md
breakpoint, cramming them on landscape phones. Introduce a 2-column
layout at the sm breakpoint (2x2 for 4 charts; 3 charts flow to
2-on-top + 1 bottom-left with a gap on the right) and defer the full
N-across row to lg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist the path-hash byte width (1/2/3) as a nullable Integer column on
RawPacket, computed at ingest by the collector and backfilled for historical
rows via a self-contained Python migration. Replace the per-request Python
decode loop in the grouped-list route with a SQL MAX() aggregate + HAVING
filter, and add a discrete <select> filter (Any/1B/2B/3B) to the /packets
SPA page.
- Model: add path_hash_bytes column to RawPacket (nullable Integer)
- Migration: batch_alter_table add_column + keyset-paginated Python backfill
reading decoded via Core select() on sa.JSON column (portable across
SQLite and Postgres)
- Collector: compute path_hash_bytes at ingest via two-tier path-hash
extraction (decoded.path -> payload.decoded.pathHashes)
- API: add func.max() aggregate to group query, HAVING filter on
?path_hash_bytes=1|2|3 param; delete Phase 3 decode loop and dead helper
- Frontend: add path-width select filter wired through query/apiParams/
pagination/headerParams; add i18n keys (en/nl)
- Tests: 1186 passed, 22 skipped; collector + API + model coverage
- Add reusable lit-html JSON tree component (json-tree.js) with
expand/collapse-all toolbar and type-coloured primitives; replace
flat <pre> decoded blocks on packet detail and packet group detail
- Render packet path as a complete flow with static sender (green dot)
and observer (satellite dish) terminators around hop badges
- Fill homepage chart areas and move legend to top-right to match
dashboard charts
- Add iconChevronRight helper and expand_all/collapse_all strings (en, nl)
Replace raw integer counts with Intl.NumberFormat()-grouped numbers
across all SPA pages — stat cards, dashboard stats, list-page total
badges, inline reception/observer counts, chart axis ticks and
tooltips, map counts, and packet-group-detail fields. Formatting uses
the visitor's browser locale (no explicit locale argument), decoupled
from the admin's datetime_locale.
Redesign the filter panel on all five filter-bearing pages (nodes,
packets, advertisements, messages, map): replace the heavy DaisyUI
collapse card with a compact right-aligned toggle slider plus bare
filter fields rendered below the control row. Filter open-state
survives auto-refresh and navigation via DOM-read of #filter-toggle.
Also fixes a pre-existing bug where pubkey_prefix was missing from
nodes.js hasActiveFilters, causing the filter to not default open
when only the public-key-prefix field was filled.
Recent adverts card now shows route-type badges, an observer column
with three-way fallback (observers -> observed_by -> dash), and renders
all 10 rows instead of 5. The Type column hides on mobile to prevent
overflow, and the misleading cursor-help on observer badges is removed.
Also adds a Packets chart card to the dashboard and a packets stat to
the homepage, backed by the new dashboard packet-activity endpoint.
Promotes routeTypeBadge to a shared export in components.js (previously
local to advertisements.js), and enriches RecentAdvertisement with
route_type, observers, and observed_by fields.
Backend resolves observers via fetch_observers_for_events and
observer_node_id -> public_key in batched queries.
The two-column mobile nav set display:block on #mobile-nav (an ID
selector) to override .menu's display:flex so column-count worked.
That ID specificity also beat DaisyUI's closed-state display:none
rule, so the menu stayed display:block while 'closed' (only opacity
dropped to 0). The invisible, two-column-wide overlay captured clicks
over underlying page content and fired SPA navigation.
Scope display:block (and the column layout) to the dropdown's open
state (.dropdown-open / :focus-within) so DaisyUI's display:none can
hide the menu and its links when closed.
- Replace panel-glow radial gradient with panel-accent flat tint + 5px
colored left border for section identity
- Add theme-aware --panel-tint-strength/--panel-tint-bg variables: dark
mode keeps 8% colored tint, light mode uses neutral grey fill (0%)
- Remove inline section color from stat-value numbers so they inherit
base-content (white/black) while icons and borders keep section colors
- Fix hero layout: center welcome text vertically, drop spacer div,
add flex-col to content wrapper
- Split mobile nav dropdown into two balanced columns (column-count: 2)
with responsive width to reduce vertical height on phones
- Soften flash banner in light mode with direct amber oklch values to
avoid hue drift from low-chroma color-mix
- Boost stat-title/stat-desc opacity from 60% to 80% in light mode for
better contrast on grey panels
- Dashboard chart cards now use section colors instead of neutral,
subtitles bumped from opacity-70 to opacity-80
Replace the OS system font stack with IBM Plex: the variable-weight
sans (100-700, one file per subset) for UI and headings, and Plex Mono
400 for the public keys, packet hashes, and hex that font-mono renders
across the app. Latin + latin-ext subsets only (shipped locales are
en/nl); no italics; mono is never rendered bold here.
Fonts are self-hosted from @fontsource packages via the existing
build.js vendor pipeline (no CDN), copied to static/vendor/fonts/ with
a hard build failure on wrong filenames. Wiring:
- input.css: @theme --font-sans/--font-mono + @font-face rules with
unicode-ranges taken verbatim from the package CSS; Tailwind v4
derives the document default from --font-sans, re-fonting daisyUI
components with no other changes.
- spa.html: preload the latin sans variable woff2 (crossorigin, URL
identical to the @font-face src) to minimize FOUT.
- charts.js: Chart.defaults.font.family to match (Chart.js otherwise
uses Helvetica/Arial).
- error.html: name-prepend only; the page stays dependency-free.
- middleware.py: long-term immutable cache for /static/vendor/fonts/
(stable names referenced from CSS, so ?v= versioning can't apply),
with a matching test.
- app.css: slight hero-title letter-spacing tightening for Plex at
display sizes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The channel cards, member node badges, and member URL text are
clickable divs/spans, so keyboard users could not reach or activate
them. Add role/tabindex, Enter/Space key handling, and a
focus-visible outline (invisible to mouse users). daisyUI btn/link/
input components already ship focus-visible outlines, so only these
custom elements needed it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Page <h1>s: text-3xl font-bold everywhere (packet-detail,
packet-group-detail, channels were text-2xl; channels icon bumped to
match).
- Page-header rows: mb-6 on all pages (list pages used mb-4).
- Empty states: text-center py-8 opacity-70 (members used py-12,
channels py-10 opacity-60).
- QR backings and adopted-node rows: rounded-box like other
panel-level surfaces; QR padding unified at p-2.
- Home hero welcome text: max-w-[90%] sm:max-w-[70%] so narrow screens
don't wrap into a skinny column (desktop unchanged).
- Document the styling conventions in the components.js header so
future pages don't drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zero-visual-delta cleanups:
- Drop input-bordered/select-bordered (removed in daisyUI v5; emitted
no CSS) from all inputs and selects.
- Rename bare `shadow` to `shadow-sm` — in Tailwind v4 bare `shadow` is
a deprecated alias with the identical value.
- Hoist the hardcoded map marker hex colors into CSS variables in the
app.css palette block (same values; markers sit on map tiles and stay
theme-independent by design).
- Convert the five text-base-content/* outliers to the repo's dominant
opacity-* muted-text idiom (identical rendering on plain text).
- Remove a dead ternary in renderNodeDisplay and an unused iconLock
import.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The channels modal still used daisyUI v4's label-text class, which was
removed in v5, so its form labels rendered unstyled. Replace with the
Tailwind equivalent of what v4 produced (text-sm, muted).
.prose a:hover used --color-primary-content (the on-primary foreground),
making hovered markdown links nearly invisible against the page
background. Blend primary toward base-content instead so the hover
shade works in both themes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FastAPI 0.137.0 refactored include_router to keep included routers as
nested objects rather than flattening their routes into app.routes, so
test_app_factory's `{route.path for route in app.routes}` no longer found
the /metrics route (the endpoint still serves; only this introspection
broke). FastAPI now treats router.routes as an internal implementation
detail.
Switch the metrics route checks to the public OpenAPI schema
(app.openapi()["paths"]), which is stable across versions and resolves
router prefixes correctly, and drop the <0.137.0 pin that was blocking
the upgrade.
Verified: app factory tests pass on both 0.136.3 and 0.137.2; full
tests/test_api suite (462 tests) passes on 0.137.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restrict which remote observers may ingest events, keyed on the observer's
public key (the <public_key> segment of its LetsMesh upload topic). Anyone
with broker access can publish as an observer via JWT auth, so operators can
now gate ingestion.
- New ObserverFilter (case-insensitive prefix matching, allowlist overrides
denylist, accept-all when both empty)
- New OBSERVER_ALLOWLIST / OBSERVER_DENYLIST collector settings, wired through
the CLI, run_collector, create_subscriber, and Subscriber
- Filter applied at the top of _handle_mqtt_message: blocked observers' packets
are dropped before any decode, raw-packet capture, or DB write; zero added
work on the default accept-all path
- Tests: ObserverFilter unit tests, subscriber drop/allow integration tests,
config parsing tests
- Docs: configuration.md, observer.md, upgrading.md (v0.16.0), .env.example,
docker-compose.yml
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>