summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-28 10:09:38 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-28 10:09:38 +0200
commit58ca879b017da62e77f40752d8d94fef3f315a1e (patch)
tree38cb9b1f8007009e39b462bcda16830aa2b0ede3 /packages/meshbay-hub/src/meshbay_hub/static/app.js
parent471fa6242fc56a035a8c9639722a82b9341328c2 (diff)
downloadmeshbay-58ca879b017da62e77f40752d8d94fef3f315a1e.tar.gz
feat(hub): group names unique per owner, shown as name@owner
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
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js19
1 files changed, 11 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 9164a4d..18f34fc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -16,6 +16,7 @@ import {
refreshAccessToken,
} from './hub-client.js';
import { GroupPage } from './group-page.js';
+import { GroupName } from './group-name.js';
import { APPS } from './apps.js';
// ── Constants ────────────────────────────────────────────────────────────────
@@ -352,7 +353,9 @@ function Sidebar({ groups, presence, indexProgressPct, route, menuOpen, role, ha
<span class="presence presence-${state}"
title="${label}"
aria-label="${label}"></span>
- <span class="sidebar-item-name">${g.name}</span>
+ <span class="sidebar-item-name">
+ <${GroupName} name=${g.name} owner=${g.owner_username} />
+ </span>
</a>
`;
})
@@ -699,13 +702,11 @@ function ExplorePage({ token, myGroupIds, allowPublicGroups = true }) {
${groups.map(g => html`
<div key=${g.id} class="group-card">
<a href="#/group/${g.id}" style="text-decoration:none;color:inherit">
- <h3>${g.name}</h3>
+ <h3><${GroupName} name=${g.name}
+ owner=${g.source && g.source !== 'local' ? g.source : g.owner_username} /></h3>
${g.description && html`<p class="group-card-desc">${g.description}</p>`}
</a>
<span class="badge">${g.join_policy}</span>
- ${g.source && g.source !== 'local' && html`
- ${' '}<span class="badge">${g.source}</span>
- `}
${' '}
${isMember(g.id)
? html`<span class="badge">${t('explore.member')}</span>`
@@ -1331,7 +1332,7 @@ function SearchPage() {
if (e.name.toLowerCase().includes(term) ||
(e.path && e.path.toLowerCase().includes(term))) {
hits.push({ ...e, groupId: idx.groupId, groupName: idx.groupName,
- syncedAt: idx.cachedAt });
+ groupOwner: idx.groupOwner, syncedAt: idx.cachedAt });
}
}
}
@@ -1376,7 +1377,9 @@ function SearchPage() {
</td>
<td class="file-size">${formatSize(r.size)}</td>
<td>
- <a href="#/group/${r.groupId}" class="badge">${r.groupName || r.groupId.slice(0, 8)}</a>
+ <a href="#/group/${r.groupId}" class="badge">${r.groupName
+ ? html`<${GroupName} name=${r.groupName} owner=${r.groupOwner} inline=${true} />`
+ : r.groupId.slice(0, 8)}</a>
${r.syncedAt && html`
<div class="search-synced">
${t('search.synced', { ago: formatAgo(r.syncedAt) })}
@@ -2175,7 +2178,7 @@ function AdminPage({ token, role }) {
${groups.length === 0 && html`<tr><td colspan="6" class="admin-empty">${t('admin.no_groups')}</td></tr>`}
${groups.map(g => html`
<tr key=${g.id}>
- <td>${g.name}</td>
+ <td><${GroupName} name=${g.name} owner=${g.owner_username} /></td>
<td><span class="badge">${g.visibility}</span></td>
<td>${g.member_count}</td>
<td><span class="badge ${g.status === 'active' ? 'badge-ok' : g.status === 'suspended' ? 'badge-err' : ''}">${g.status}</span></td>