| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
| |
All three packages (common, hub, node) bump 0.9.0 -> 0.10.0 together.
Highlights since v0.9.0: email verification for registration, email
change and invitations; passphrase change and account recovery;
reCAPTCHA v2 on Register and Password Reset; node JSON-only control API
with the Node page absorbing the admin dashboard; WebRTC STUN fallback
fix; assorted hub UI fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNPfgH6VWcRzJDZGuzy1jJ
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
No-group home/explore message: when the hub offers no public groups,
drop the "browse public groups" invitation and just ask to be invited
by an admin. New home.invite_only key added to all ten catalogues.
Create-group wizard done step: reword the message to point at inviting
members, and send the button to the group's Settings tab (where the
invite form lives) instead of a stale /groups/<id> path that never
matched the router. The landing tab is a one-shot session hint, so the
usual per-user default-tab preference is left untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNPfgH6VWcRzJDZGuzy1jJ
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The cross-group "Search files" view caps its WebRTC connection pool at
MAX_POOL_SIZE but pre-connected every indexed group, so any account with
more groups than the cap thrashed the pool. An evicted transport was
never removed from SearchPage's own groupConns map, so every tile of that
group kept a `_tRef` pointing at a closed transport; MediaThumb and
useMediaMeta bail on a disconnected transport with no retry, so posters
rendered for a moment then fell back to a loading spinner for good. Every
connect also fired the module-wide bumpMediaMetaGeneration(), clearing
every mounted tile's metadata across all groups and flickering the whole
grid through the warm-up walk. A one-group account (grenet) never hit it;
a many-group account (cbesson) always did.
- ConnectionPool takes an onEvict callback; SearchPage prunes groupConns
and rebuilds the entry lists when a connection is evicted or closed.
- MAX_POOL_SIZE 3 -> 12; the pre-connect walk is capped to it. Groups
past the cap connect lazily when a tile scrolls into view (onNeedConn).
- connectGroup() no longer fires the module-wide meta/thumb generation
bumps on a normal connect (kept for operator TMDB override/rematch).
Per-group gen counter + concurrent-caller guard: a group's tiles
refetch once per (re)connect, not once per mounting tile.
- MediaThumb/useMediaMeta take an optional reloadKey; each group's
`_connGen` is threaded through so a tile refetches when its group
reconnects instead of staying stuck on a spinner.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4Yj8EuXkjjYxbeS5kPd3U
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Remove the node daemon's server-rendered admin UI (GET / and /audit, the
_render_* helpers and inline templates) and the `meshbay-node ui` CLI verb.
The loopback control API stays; it is now JSON only, ruff-clean, and 453
lines (was 1074). Also drop three never-wired endpoints (/api/config,
/api/chat/history, /ws/chat, plus broadcast_chat) and the pointless
18000/tcp firewall profiles.
The desktop client's Node page (static/node-page.js) takes over what the
dashboard showed, reorganised into six tabs (Overview, Groups, Roster,
Peers, Audit, Settings):
- Overview: version, node id, QUIC port, hub, index-cache maintenance
- Roster: node-wide view with unpin
- Peers and Audit: auto-load on open, no Load button
- Audit: real usernames and group names (resolved from the roster and
node.toml), Previous/Next pagination newest-first, Export CSV of every
matching row
- Settings: node settings, STUN, ICE, denylist, then Unlink from hub
Backend: audit.get_entries gains `offset`; /api/audit and /api/peers
resolve ids to names via a new _display_names helper; CSP tightened to
default-src 'none' now that no HTML is served. draft-v6 sections 2.11 and
2.12 corrected -- the Node page uses the loopback API, not MNP.
One capability is intentionally dropped: browser-based admin on a headless
server. The CLI covers every operation there.
See docs/refactor-node-ui.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQCaZnde4Bjjdu84dhSuF5
|
| |
|
|
|
|
|
| |
The clause was redundant with the surrounding UI context.
Removed from all 10 locale files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two bugs fixed:
1. `meshbay-node member invite <user>` created a local roster invite
but never told the hub to add the user to group_members, so the
group was invisible in the SPA. The node now calls
POST /v1/groups/{id}/members/{username} after creating the invite,
and the hub endpoint accepts node-scoped tokens (the admin_id
check is the real authorization guard).
2. The WebRTC handshake let a previously-pinned user reconnect without
a code even when a new invite was pending (e.g. after leave + re-invite).
Now any pending invite forces code entry, regardless of existing
member/pin status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
Server-side verification module, CaptchaConfig in hub.toml,
captcha_site_key exposed via /v1/hub/info, useCaptcha() hook
in the SPA with stable DOM rendering (strength bar always present
to avoid Preact re-ordering the captcha widget). Native clients
(auth_key path) skip captcha. All 10 locales updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The passphrase derives two independent client-side values: auth_key (the
hub verifier) and bundle_key (AES-GCM key for the per-node identity
bundles, which live on nodes and never on the hub). Changing or
recovering a passphrase is therefore two operations — swap the hub
verifier, and re-wrap every reachable node's identity bundle.
Flow A — change a known passphrase (Profile page)
- POST /v1/users/password re-proves the current passphrase, swaps
pw_hash/salt/version, revokes every refresh token and returns a fresh
pair so the tab that made the change stays signed in.
- MeshBayTransport.rewrapAllNodes: for every group's online node, connect
with the old key, read the identity off the handshake, store it back
under the new key. Returns updated / unreachable / failed so the UI can
point at the operator-unpin fallback for the gaps. Always-shown
confirmation dialog listing reachable and unreachable groups.
Recovery key
- keyderive.js generateRecoveryKey (32 random bytes, grouped Base32) and
deriveRecoveryKey (HKDF-SHA256, domain meshbay:recovery:v1:<username>).
- Every per-node identity gets a second copy wrapped under the recovery
key: keypair_bundles.bundle_enc_recovery (node-only column, added in
_SCHEMA_KEYPAIR and via a PRAGMA-guarded ALTER for existing DBs),
carried on keypair_bundle_store / _resp. MNP 0.13 -> 0.14, additive.
- session.recoveryKey is persisted in IndexedDB (slot rk) and lazy-loaded
on connect, so a group joined in any later session still leaves a
recovery copy.
- Shown once at registration; optionally folded into the verification
e-mail as a pass-through the hub never stores or logs, with an opt-out.
- Profile -> Recovery key re-loads R and backfills every reachable node
via rewrapAllNodes in bundleKey mode (no passphrase re-entry).
Flow B — recover a lost passphrase (#/reset, linked from sign-in)
- POST /v1/users/password/reset-request {username, email}: both must be
the pair on file, checked against the blind email_hash (never
decrypted). A mismatch — wrong e-mail, unknown username, non-active
account — takes the identical no-op path (no code, no mail, same 200),
so it reveals nothing and cannot be used to spray reset mail from a
username alone. 5/min, 1-hour single-use code.
- POST /v1/users/password/reset {username, code, new_auth_key}: same
expiry / attempts / single-use checks as e-mail verification; revokes
every session and deletes every registered device key so a stored one
cannot sign back in past the reset.
- ResetPasswordPage: request code -> code + optional recovery key + new
passphrase -> reset + sign-in -> fan-out. connect() falls back to the
recovery-wrapped copy when the passphrase key cannot open bundle_enc.
Without a recovery key: sign-in is restored and each group needs the
operator-unpin fallback.
Supporting fixes (found in live testing)
- member unpin now also deletes the keypair bundle; connect() mints a
fresh identity when handed a bundle it cannot open (unless _rewrapOnly,
set by rewrapAllNodes), so a rejoin completes instead of dead-ending
before the invite-code prompt.
- A browser with no bundle key gets a passphrase prompt on the group page
instead of a "go back to the browser you registered on" message.
- RegisterPage / LoginPage / ResetPasswordPage trim the username so every
key derivation matches the hub's stored form.
Docs: docs/auth-confirm.md. Locale keys across all ten catalogues.
Tests: test_password_change, test_password_reset, test_recovery_email,
test_recovery_key, test_rewrap_fanout, test_bundle_store_recovery, plus
additions to test_admin_ops_mnp and test_webrtc_transport. Hub suite 492
passed; node suite 741 passed (the lone test_packaging_units failure is a
pre-existing RPM-spec flake, reproducible on main).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGkxJW9br8Y9bhT8ywJ3oc
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Registration now creates a pending account and sends a 6-digit code via
email; the account activates only after verification. Email changes on
the profile page follow the same flow. Group invitations send a
notification email to the invitee (without revealing their address to
the inviter) containing the invite code and hub link.
Backend: blind HMAC-SHA256 email index for uniqueness without decryption,
mail.py for localhost Postfix delivery, verification endpoints, cleanup
of expired codes and stale pending accounts, startup backfill of
email_hash for existing users.
Frontend: 3-phase register page, inline email change verification on
profile, invite-notify call with status display. All 10 locales updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
| |
Packaging system complete and verified on Ubuntu 26.04 and Fedora 44.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
ExplorePage → explore-page.js (static import), LoginPage/RegisterPage/
FirstRunPage → auth-page.js (static import, LoginPage receives onLogin
as a prop), CreateGroupPage/wizard → create-group-page.js (lazy-loaded
via dynamic import(), same pattern as AdminPage/NodePage).
app.js goes from 1803 to 914 lines. webapp.py _ASSETS extended with the
three new files and the previously missing extracted pages. Test fixtures
updated to follow the moved components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
| |
Admin page is lazy-loaded so non-admin users never fetch it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
| |
Replace checkboxes with the existing toggle-switch component for
notifications, keep-screen-on, and allow-public-groups. Remove the
"Notifications" label from per-group lines, and shorten the
music-keep-screen-on and hub-URL hints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
The ice_interfaces setting (auto-exclude vs explicit whitelist) is now
editable from the Node page, persisted via the settings API and roster,
and hot-swapped at runtime by re-installing the aioice filter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
| |
The WebRTC transport relied on a single Google STUN server — if it was
unreachable, ICE gathering waited the full 4s timeout. Now four public
servers are used by default (Google ×2, Cloudflare, Mozilla), configurable
via node.toml, the Node page UI, and the CLI (meshbay-node stun list|add|
remove|reset). Changes are hot-swapped on the live transport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Move NodePage and NodeServicePanel from app.js into node-page.js,
following the existing pattern (group-page.js, search-page.js).
Lazy-loaded via dynamic import so browser users never fetch it.
Replace all MNP/WebRTC calls with platform.node.call() (loopback HTTP
API), eliminating the ~6s ICE gathering delay. The MNP protocol types
and server-side handlers are kept for potential future browser use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
Real franchise / show / release-group names had crept back into test
fixtures, code comments, a docstring and docs/mediacenter.md while fixing
the saga-match and misclassification bugs. Replace them all with invented
placeholders ("Some Saga", "A Different Show") and shape descriptions
("a franchise-origin film", "a 3-season show"). Behaviour and assertions
unchanged; 738 node tests still pass.
Record the rule in CLAUDE.md so it stops recurring.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
V12 collapsed movies that TMDB resolved to the same tmdb_id into one
card. With the matcher still imperfect that fuses *different films*:
every numbered entry of a saga whose bare title resolves to the same
base id becomes one card, and two unrelated movies sharing a title do
too (seen live on a 9-film saga and a 2-film pair). A tmdb_id-keyed
merge only works once matching is reliable, which it is not yet.
Movies render one card per file again; VideoDetailModal loses the
`files` prop and the versions list, back to a single Play button;
`.video-version-list` and the `video.versions` key (×10 locales) are
removed. mergedShows (V6) is untouched — the operator's report was
about movies.
docs/mediacenter.md §10.1: V12 un-struck, marked reverted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
| |
.video-fix-match carried `margin: 8px 0` from when it was a standalone
button; now that it sits in the .video-admin-actions flex row next to the
plain .admin-btn "Re-match", `align-items: stretch` (the flex default)
made the margined button 16px shorter than its neighbour — "Re-match"
looked twice as big. Drop the margin (the row already has its own),
pin the font-size to .admin-btn's, and center the row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
In a real group the node streams every index change to connected clients
as it happens (onIndexSync / onIndexDelta), so a manual re-fetch button
there just duplicates the live push. The cross-group Search page has no
such connection — its file list is a one-shot cached fetch — so the
button belongs there and only there. Gated on a new explicit `showRefresh`
prop that only search-page.js passes; `onRefreshIndex` stays in
commonProps for ChatPanel's own use after an attachment upload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The refresh button (added in 1cb9054, left of the breadcrumb home icon)
was gated on `onRefreshIndex`, which the Search page's embedded FilesPanel
did not pass — so it never showed on the one page it was asked for. The
Search page now passes an `onRefreshIndex` that re-runs its all-groups
index fetch (the only cache it has); the existing progress bar is the
feedback. Unchanged in the normal group Files view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |\ |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Expose invite_ttl_hours, pair_ttl_hours, device_request_ttl_minutes,
max_concurrent_streams and transcode_incompatible_video in the Node
management panel. Changes are applied immediately via roster.db and
written back to node.toml so they survive a DB wipe. On startup,
roster overrides take precedence over node.toml defaults.
Draft v6 §2.11 documents the design; MNP gains node_settings_set /
node_settings_set_ack for the browser path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |\ \
| |/
|/| |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
1. The "Search Files" <h2> is removed from the search page — it duplicated
the nav and cost a line of vertical space on mobile. `search.title` is
dropped from all ten locales (its only use).
2. A monochrome refresh button sits left of the Files breadcrumb home
icon. It calls `onRefreshIndex` (re-fetch the group index from the
node — the file list is a view over a cached copy with no other way to
pull a fresh one), spins while in flight, and only renders where an
`onRefreshIndex` is wired (the real group view, not the cross-group
search results). New `refresh` icon; `group.refresh_index` in all ten
locales.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Root of the 2026-08-29 demo35 storms. `_do_media_meta_request` could not
tell "this is a movie" from "this video isn't enriched yet" — both have
season/episode None — so during a slow initial scan with a browser on the
Videos tab, every show episode requested was run through the *movie*
search path with its raw filename as the query
(`search/movie?query=Show S01E01 1080p WEB DL ...`), hundreds per second,
until TMDB rate-limited and posters stopped loading. Worse, an
un-enriched show episode's own valid cached "tv" match was treated as
stale (its provisional kind was "movie"), so a file already resolved got
re-queried anyway.
- While a video is un-enriched (no display_title — enrich.py always sets
one), never run a TMDB *search*. Serve the cached match if the content
hash has one, honouring the cached kind ("tv"/"movie") rather than the
provisional split; otherwise answer confidence 0.
- Once enriched, the strict `cached_media_type == media_type` check is
unchanged: an enrichment fix that reclassifies a folder movie->tv still
drops the stale match and re-resolves.
- video-app.js: `useMediaMeta` gains an `enrichSig` dependency
(`entry.display_title`) so the client refetches once the enriched fields
arrive on an index delta — the fileId is a content hash and never
changes, so nothing else would retrigger it.
Not caused by the V8-V13 work; it raised the per-file call count so the
pre-existing race became a visible storm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A one-click alternative to the full "Fix match" search-and-pick flow, and
reachable without SSH (`meshbay-node video rematch` clears a whole group).
- MNP 0.13: tmdb_rematch / tmdb_rematch_ack (additive — an older node
logs "unknown type", the button just does nothing). OP_TMDB_REMATCH,
signed like tmdb_override (media_cache is shared node-wide).
- media_cache.drop_tmdb_match(file_id): forgets the match AND the
override marker — deliberately stronger than clear_file_tmdb, since the
operator is explicitly asking for a fresh resolution.
- webrtc_server: _do_tmdb_rematch / _admin_exec_tmdb_rematch, dispatch +
admin-response routing, broadcasts tmdb_rematch_ack.
- transport.js: rematchTmdbMatch(fileId, signFn); 'tmdb_rematch' in the
admin-op allowlist; tmdb_rematch_ack handled like tmdb_override_ack.
- video-app.js: a "Re-match" button beside "Fix match" in the detail
modal (isNodeAdmin), then bumpMediaMetaGeneration(). video.rematch_one
key in all ten locales.
docs/mediacenter.md §10.1: V8–V13 marked done.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two movie files that TMDB resolves to the same id (the same film at two
resolutions, or the same rip in two folders) now collapse to one poster
card, mirroring the existing show merge — same keying discipline so an
unmerged movie keeps its card and a merge updates props rather than
remounting. The detail modal lists the versions (resolution · duration ·
size), each a Play button, when there is more than one; a single-file
movie is unchanged. New `video.versions` key in all ten locales.
Known edge, noted in §10.1: "Fix match" on a merged movie corrects only
the representative file; the other version un-merges and can be corrected
on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |\ |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
A batch of wrong poster-grid matches found live on a real library
(2026-08-29): a two-volume film's second part matched the first; a
numbered sequel matched a same-year making-of documentary; several
entries of one franchise matched a single early entry whose localized
TMDB title is the franchise name; one matched nothing. One mechanism:
_tmdb_search returned the first candidate query whose title-similarity
ratio merely cleared 0.6, before alternative_title / the Roman-numeral
variant was ever tried.
Matching:
- title_parse: fold guessit's volume/part number back into display_title
so the parts of a multi-part film stay distinct in the query, the card
and the override.
- _tmdb_search: keep a strong PASS 1 fast path (ratio >= 0.85, one
request), otherwise score every candidate query and pick the best. A
year-exact rescue lifts a sub-0.6 top hit to the confidence floor only
when TMDB's own year-filtered result lands exactly on the filename's
year. No local re-ranking of any single result list; no tmdb.py change.
Fix match / rematch:
- _admin_exec_tmdb_override: a movie override touches its own file only
(guessit gives a whole franchise one display_title); a show override
still fans out. Corrected files are marked in media_cache.tmdb_override.
- media_cache: tmdb_override table; clear_file_tmdb / clear_tmdb_matches
drop auto-resolved matches while sparing manual corrections.
- ops.rematch_video + `meshbay-node video rematch` (loopback endpoint +
CLI verb): re-resolve a group's video matches after a matcher fix.
file_tmdb is keyed by content hash and otherwise only pruned on
deletion, so nothing dislodged a cached match before.
- a rename now drops the stale auto match too (daemon
_reenrich_renamed_video_entries).
UI:
- VideoDetailModal shows the source filename and resolved TMDB id; an
unmatched poster gets a badge (3 new video.* i18n keys x 10 locales).
So a wrong match can actually be identified before hitting Fix match.
docs/mediacenter.md 10.1 records this and the V8-V13 follow-up backlog
(show-branch ladder, year-aware _best_match, wider sequel_variants, the
0.6-0.85 extra calls, movie grid merge, per-card rematch).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
spinner
Three regressions fixed in ChatPanel:
- Scroll pinning: layout effect now depends on [messages, hasMore] so the
"load older" button appearance triggers a re-pin; setHasMore is called
before setMessages to avoid an intermediate render without the button;
fit() is wrapped in fitAndPin() so panel resizing re-pins the scroll.
- Message loading on group switch: fetch effect depends on the status prop
instead of transportRef.current?.connected to avoid racing with
GroupPage's cleanup; loadedRef resets on fetch failure so a retry works.
- Connecting spinner restored in the Chat tab empty state.
Adds structural regression tests (test_chat_scroll_bottom.py) that lock
the dependency arrays and setState ordering so these invariants break
loudly in CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |/
|
|
|
|
|
|
|
| |
App components were keyed by app name only (e.g. 'chat'), so switching
groups while staying on the same tab reused the component instance —
loadedRef stayed true and messages state was never cleared. Now keyed
by app name + groupId, forcing a clean remount on group change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback
Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay
MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster
Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
chat_hist_resp checked only the single oldest pending entry across all
types. When switching tabs, keyed requests (media_meta_req, file_req)
from Videos/Music/Photos stayed in _pending and were older than the
chat_hist entry, so the response was dropped and the chat appeared empty
on return. Now scans for the first chat_hist entry instead. Same fix
applied to index_sync dispatch for consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
| |
Search-page connections (indexing and pool) now time out after 10s
instead of the transport's default 30s, so down nodes are skipped
faster. Downloads show a user-facing error when a group is unreachable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Downloads from search: FilesPanel accepts a getTransport callback for
cross-group file downloads. Select/Download are available in search;
delete and zip are hidden (readOnly). Each download connects to the
entry's group via the pool on demand.
Photos view: PhotosApp reused in search with the same pattern as
Videos/Music — synthetic root, per-entry transport refs, group badges
on album cards. Lightbox uses per-entry transport for full-resolution
image fetches. Zip download hidden in search context.
Pre-connecting: connections to all groups start as soon as indexing
finishes, regardless of the active view. Thumbnails cache across pool
evictions, so they're ready when the user switches views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Search page fetches indexes from all groups, then renders consolidated
entries through the existing FilesPanel, VideoApp, and MusicApp
components — no reimplemented views. Groups appear as top-level
directories in the file browser; video/music entries use a synthetic
root with per-entry transport refs for thumbnails and metadata across
groups. Music player lifted to app.js with getConnection(groupId) for
cross-group playback. Connection pool (max 3, LRU eviction) manages
lazy WebRTC connections. All 10 locales updated with search keys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
| |
The button detaches a group from this node's config. For a group that still
exists on the hub that strands it with no host; deleting such a group is the
group Settings tab's job (detach + hub delete together). It now shows only
when the group is `stale` — present on the node, gone from the hub — which
the page already computes for the "not on hub" badge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
|
| |
The @owner line turned the sidebar name into a two-line block, and the
status dot (a flex sibling, align-items:center) floated to its vertical
middle. A group entry is now a flex column: dot+name on one row (.si-head),
@owner on a subordinate line indented past the dot. Non-group sidebar links
are untouched (.sidebar-item stays a row; only .sidebar-group flips).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A group's identity stays its UUID. What changes is that "the name is
unique" — until now an unenforced expectation — becomes real, scoped to the
owner account, and the owner's username is surfaced so two groups called
"photos" on different nodes can be told apart.
Hub:
- `groups` gains a functional unique index `uq_groups_owner_name` on
`(admin_id, lower(name))` (model + migration c3d4e5f6a7b8). The migration
pre-flights: if the data already clashes it aborts and lists the
offending (admin_id, name) pairs rather than renaming anyone's group.
meshbay.org checked clean.
- `create_group` trims the name, rejects blank (422) and an owner-scoped
case-insensitive clash (409), with an IntegrityError backstop for the
race, and returns `owner_username`.
- `owner_username` added to `/v1/groups/mine`, `GET /v1/groups` (local
rows), `POST /v1/groups/{id}/join`, `GET /v1/admin/groups`.
SPA:
- new `static/group-name.js` — `<GroupName name owner [inline]>` renders the
name with the `@owner` handle on a smaller grey line under it.
- used in the sidebar, the group-page header, Explore cards, the Admin
groups table, and cross-group Search (via a widened `cacheGroupIndex`
carrying the owner). Delete/leave confirmations show `name@owner` inline.
- federated Explore rows show `@<source_hub>` instead of an account.
Design record and the locked decisions: ~/next/groupnames.md (out of repo).
MNP unchanged. Tests: test_group_name_unique.py, test_group_name_migration.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
| |
When the hub has public groups disabled, /explore showed an empty "Public
Groups" screen with a search box and a Create button that do nothing. It now
renders a single line — "This hub does not have public groups." — and the
server already returns an empty directory regardless.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Bump the three packages to 0.8.0 (released together) and realign
meshbay-common's __version__, which had drifted to 0.7.0 while the
pyproject stayed at 0.6.0. Dependency pins updated to meshbay-common>=0.8.0.
Protocol versions are independent and unchanged: MNP 0.12, MHP 0.1. The
Electron client stays on its own 0.1.0 track (hub MIN_CLIENT_VERSION).
Since 0.6/0.7: public-groups admin switch with full server-side enforcement
and a group Revoke action; chat link previews (node-side URL unfurl, SSRF-
gated); chat composer focus + scroll-to-bottom on tab entry; whole-group
"Filter files" search.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The filter only ever matched files whose folder was exactly the current
path, so at the top of a group — where every row is a root, never a loose
file — typing in the field did nothing at all.
With text in the field the panel now searches every file in the group by
name or containing folder, wherever it lives, and hides the folder rows
(you are searching, not browsing). Each result shows its folder as a
sub-line; clicking it clears the filter and opens that folder. Empty field
restores the normal folder-by-folder view unchanged.
Also hardened the sort comparators against an entry missing `name`/`type`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
| |
ChatPanel remounts on every tab switch, so a mount effect is the tab-entry
hook. `focus({ preventScroll: true })` so it does not fight the panel-sizing
and scroll-to-bottom effects still settling, and so a phone opens the
keyboard without the view jumping.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |\ |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Paste an http(s) link in a group's chat and it unfurls into an OpenGraph
card — title, description, site name, and image — the way WhatsApp/Signal/
Slack do it.
The fetch is the node's, never the browser's or the hub's. The browser
cannot: a strict img-src/connect-src and CORS block it, and a direct fetch
would leak every reader's IP to the linked host on each render. The hub must
not touch group content (draft-v6 §2.5). The node already fetches third-party
metadata for the Videos and Music apps, over the same authorised path.
Flow mirrors media_meta_req: the client sends `link_preview_req {url}`, the
node replies `link_preview_resp` with the card fields (or `ok: false`), and
any OG image is stored under its blake3 in the existing media_cache thumb
store — the client then fetches it via the normal file_req path, exactly like
a poster. Nothing durable is added: the card text lives in a bounded in-memory
TTL cache on the node (draft-v6 §2.7 — enrichment on demand, the asking device
caches), and MNP goes 0.11 → 0.12 (additive: an older node logs "unknown type"
and the client shows the bare link).
Because the URL is chosen by a *member* and triggers an outbound request from
the operator's machine, `linkpreview.safe_url` is an SSRF gate: http(s) only,
no credentials, and every resolved address must be globally routable — no
loopback, private, link-local, multicast or reserved range, cloud-metadata
included. Redirects are followed by hand so each hop is re-checked. Residual,
documented in the module: DNS rebinding between the check and connect, closed
properly by pinning the checked IP — a follow-up.
Also fixes a long-standing chat annoyance the preview cards made worse:
opening the Chat tab landed a screen or two above the newest message because
the scroll-to-bottom ran before attachment thumbnails and (now) preview cards
had loaded and grown the content. A ResizeObserver keeps the view pinned to
the bottom through late content growth, and does nothing once the reader
scrolls up.
Tests: test_linkpreview.py (the SSRF gate and the OpenGraph parse, incl.
redirect re-validation and image downscaling) and test_link_preview_request.py
(reply shape, the media_cache image round-trip, the result cache).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A new General tab in Administration carries one switch, allow_public_groups,
stored in a hub_settings key/value table (runtime-editable, unlike hub.toml).
Default is on; an absent row means on, so an upgrade changes nothing.
Enforcement is server-side on every hub-mediated path, not just the SPA:
- create_group refuses visibility=public (403), staff included
- list_public_groups the directory returns nothing (local + federated)
- join_group open-joining a public group is refused
- group_online_nodes a non-member of a public group is handed no node
- signaling.webrtc_offer drops the "node hosts an open group" fallback
- federation.export_directory advertises nothing to peer hubs
The switch is read live, so flipping it back restores every path. Existing
members of a group that predates the switch keep their membership row and
their access — this is plan A, not a purge. GET /v1/hub/info exposes the
flag (unauthenticated) so the create-group form and the sidebar's "Public
groups" link render correctly.
Also in the admin Groups tab: a Revoke action beside Suspend. Suspend is the
reversible hub flag; Revoke calls POST /v1/admin/revoke, which sets
status=revoked and broadcasts a signed revocation every node enforces
(denylist + dropped live sessions). It is confirm-guarded and names the group.
And a message fix the revoke work surfaced: group_online_nodes, join_group and
webrtc_offer answered "Group is suspended" for any non-active status. They now
report the real state, so a member of a revoked group is told "Group is
revoked" rather than something reversible-sounding.
Tests: test_public_groups_toggle.py (10) covers the switch end to end and the
five enforcement paths; test_revocation.py gains the status-message assertion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
merge various-artists compilations into one album
Two real, confirmed bugs in a large flat music library:
- enrich_audio.py's sibling-cover fallback assumed one folder is one
release. A large flat "chart ranking" folder mixing dozens of unrelated
artists carried several distinct WMP AlbumArt-cache guids (one per
original album a track was ripped from), and the fallback picked
whichever one WMP had copied to Folder.jpg — attaching one unrelated
release's cover to every other track in the folder. Now refuses to pick
a cover at all once 2+ distinct guids show up, rather than guess.
- music-app.js's groupMusicEntries grouped by artist first, album second,
so a various-artists compilation (many genuinely different per-track
artists, one shared album tag, no album-artist tag at all — a real
~20-track soundtrack rip has exactly this shape) could never be
recognized as one release: every track landed alone in its own artist's
bucket and got folded into a singleton pile. Now detects an album key
shared across 2+ distinct artist keys and merges those tracks into one
compilation card under a "Various" heading instead.
Both verified against real, previously-affected files and live in the
browser: the shared wrong cover is gone, and the compilation renders as
one card with all its tracks in order.
|
| |
|
|
|
|
|
|
| |
video-app.js's toolbar isn't built from .toolbar-group, so it never got the
existing row-per-group mobile treatment. Mode buttons plus the new
All/Movies/Series control left nothing for the search field on a phone
width. Wraps now, with the filter field taking its own full-width line
like .toolbar-group's search field already does elsewhere.
|
| |
|
|
|
|
|
| |
A segmented control to the left of the search box, defaulting to "All".
Applied before the text filter — a title match within a type nobody asked
to see still isn't shown. Resets to "All" on group change, matching the
text filter's own reset.
|