| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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>
|
| |
|
|
| |
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
|
| |\ |
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every "<Saga> Episode <N> - <subtitle>" file in a numbered franchise
resolved to the series' first entry (a real, older film). `sequel_variants`
stripped "Episode <N>" and offered the bare "<Saga>" as a candidate query;
that matches the first film's original_title at ratio 1.0 and beat PASS 1's
correct-but-lower hit. A franchise's bare name is very often a real,
different film.
When a Part/Episode/Chapitre/… keyword carried the index, sequel_variants
no longer emits the bare base — only "<base> <digit>" and "<base> <roman>".
Without a keyword ("<Franchise> 3") the bare base is still offered, so that
fix is untouched. Verified live against TMDB: the franchise's episodes each
resolve to their own entry; the earlier numbered-sequel, two-part-film and
franchise-subtitle regressions all hold.
docs/mediacenter.md §10.3.
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
|
| |\ \ \
| |_|/
|/| | |
|
| |/ /
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Found on demo35: "Some.Film.2017.MULTI.108.grp.mkv" — the release name's
"1080p" truncated to "108" — makes guessit read S01E08, so enrich.py's
flat-library branch (elif ep.episode is not None) filed a standalone film
as a series. "Fix match" then only offered TV results for the phantom
show, so there was no way out from the UI.
- title_parse.has_episode_marker(): true only for an explicit SxxExx /
1x08 / "Episode N" / "Season N" token, not a bare 3-4 digit run.
- enrich.py: the flat-library branch now needs ep.season AND ep.episode,
plus either a real marker or the absence of a "(2019)"-style year.
Every genuine flat-dumped episode in the corpus carries a marker, so
real shows are untouched; the same misparse on "1280" ("...2013.1280...")
is covered too.
docs/mediacenter.md §10.2. Known gap left open: no operator control over
the movie/show kind itself — a "this is a movie / a show" toggle would.
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
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sequel_variants
V8: the TV/show branch of _tmdb_search used the old "first candidate over
0.6 wins" shape. It now shares one _tmdb_ladder helper with the movie
branch — score every candidate query, keep the best, fast-path a
confident primary hit. A year lifted off the show's folder name
(title_parse.year_in, e.g. "Some.Show.2022.S01") rescues a sub-0.6 hit
that lands on the exact year. title_parse.clean_query de-dots a
folder-derived title without naive_title's extension-stripping trap.
V9: _best_match gains an optional `year`. When the top result is not a
confident textual hit (ratio < 0.6) and a year was requested, a
different result of that exact release year is preferred — TMDB already
year-filtered the search, so this is a hard corroboration, not the fuzzy
re-rank §3.3 warns against. A confident top hit is never overridden.
search_movie/search_tv forward the year.
V10: sequel_variants widened — trailing Roman→digit as well as
digit→Roman, spelled-out indices (one..twelve / un..douze / ordinals),
and a "Part N" / "Chapitre N" wrapper. Still empty for a trailing word
that is not an index or a 4-digit year.
V11: when the primary hit is already decent (>= 0.6) and there is nothing
more specific to try (no alternative_title, no sequel variant — only a
punctuation restatement left), the ladder returns without the extra
requests. The clean-title common case is back to one call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
|
| |
|
|
|
|
|
|
|
|
|
| |
b6f3139 renamed `fit` to `fitAndPin` in chat-app.js and rebound the
resize / orientationchange / visualViewport listeners to it, but
test_the_panel_refits_when_the_viewport_changes still grepped for the old
name and failed. `fit` itself still exists (fitAndPin wraps it), so
test_the_measurement_survives_a_scrolled_page was unaffected.
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
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
VPN/virtual adapters
aioice sends STUN binding requests from every IPv4 interface and waits up
to 5 seconds for all to complete. On a machine with Tailscale (wt0), the
STUN request never gets a response, adding a fixed 5-second penalty to
every WebRTC connection — measured at 6 s total (vs 1-2 s without it).
Auto-exclude virtual/VPN adapters (tailscale, virbr, docker, veth, podman,
cni) and CGNAT-range IPs (100.64.0.0/10). Operator can override with
ice_interfaces in node.toml [node] section for explicit control.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
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>
|
| |\ |
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Regression from device linking (Stage C, 2026-08-18). Once a device is
pinned on a node — as a member of one group, or an operator pairing — the
`known` fast-path in `_do_join_request` dropped straight into `_join_ok`.
For any *other* invite-only group it had no roster row for, that answered
`not_authorized_for_group` and stopped there: the client never got
`code_required`, so the pairing-code form never appeared and a legitimately
invited member could not join.
The `known` branch now, when there is no membership for the group being
opened:
- with a valid code → consumes the invite and admits (as the unknown-
device path already does);
- with no code but an invite waiting for this user here → `code_required`,
so the client prompts;
- with no code and nothing inviting them → `not_authorized_for_group`,
unchanged, so the H3 guarantee (a hub-invented pin gets no key) holds.
Also fixed: the group's own roster row is now consulted first, so an
existing member opening their group is never mistaken for a stranger.
Tests in test_roster_pairing.py cover all three branches plus the H3 guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
|
| |\ |
|
| |/
|
|
|
|
|
|
|
|
|
| |
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
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Amend v6 (§1 rows 11–13, §§2.8–2.10) with the three architectural changes
made after the 2026-08-17 desktop-client discussion:
- §2.8 hub_settings — a runtime instance-policy store; public groups can be
switched off hub-wide, enforced on every hub-mediated path. Records the
suspend-vs-revoke distinction (hub flag vs signed node-enforced revocation).
- §2.9 group names unique per owner account; identity still the UUID; shown
as name@owner.
- §2.10 chat link previews as a new instance of the "node on demand, asking
device caches, nothing durable" rule; the SSRF gate; MNP 0.12.
Also notes hub_settings in §2.5's list of what the hub holds, so that rule
stays accurate.
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
|