fix: normalize visibility enums + tab tracking across remaining files

Update remaining references from legacy 4-value visibility model
(public/public_read/authenticated/members_only) to simplified 3-value
model (public/permissioned/private) in rInbox, rVote, identity component,
admin panel, and create-space page. Add tab trackRecent calls in shell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-03 13:29:01 -08:00
parent eb2859d849
commit 80e42596b3
8 changed files with 27 additions and 28 deletions

View File

@ -154,8 +154,8 @@ A **space** is a collaborative context — a team, community, project, or
individual workspace. Each space has:
- **Slug** + optional subdomain (`alice.rspace.online`)
- **Visibility**: `public` | `public_read` | `authenticated` | `members_only`
- **Members**: `viewer``participant` → `moderator``admin`
- **Visibility**: `public` (👁 green — anyone reads, sign in to write) | `permissioned` (🔑 yellow — sign in to read & write) | `private` (🔒 red — invite-only)
- **Members**: `viewer``member` → `moderator``admin`
- **Enabled modules**: which rApps are available in this space
- **Module scoping**: per-module `space` (data lives in space) vs
`global` (data follows identity)

View File

@ -280,7 +280,7 @@ routes.post("/api/mailboxes", async (c) => {
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json();
const { slug, name, email, description, visibility = "members_only", imap_user, imap_password } = body;
const { slug, name, email, description, visibility = "private", imap_user, imap_password } = body;
if (!slug || !name || !email) return c.json({ error: "slug, name, email required" }, 400);
if (!/^[a-z0-9-]+$/.test(slug)) return c.json({ error: "Invalid slug" }, 400);

View File

@ -58,7 +58,7 @@ class FolkVoteDashboard extends HTMLElement {
slug: "community",
name: "Community Governance",
description: "Proposals for the rSpace ecosystem",
visibility: "public_read",
visibility: "public",
promotion_threshold: 100,
voting_period_days: 7,
credits_per_day: 10,
@ -328,7 +328,7 @@ class FolkVoteDashboard extends HTMLElement {
<div class="card" data-space="${s.slug}">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="card-title">${this.esc(s.name)}</div>
<span class="badge" style="background:rgba(129,140,248,0.15);color:#818cf8">${s.visibility === "public_read" ? "Public" : s.visibility}</span>
<span class="badge" style="background:rgba(129,140,248,0.15);color:#818cf8">${s.visibility === "public" ? "👁 Public" : s.visibility === "permissioned" ? "🔑 Permissioned" : s.visibility === "private" ? "🔒 Private" : s.visibility}</span>
</div>
<div class="card-desc">${this.esc(s.description || "")}</div>
<div class="card-meta">

View File

@ -65,7 +65,7 @@ function ensureSpaceConfigDoc(space: string): ProposalDoc {
name: '',
description: '',
ownerDid: '',
visibility: 'public_read',
visibility: 'public',
promotionThreshold: 100,
votingPeriodDays: 7,
creditsPerDay: 10,
@ -281,7 +281,7 @@ routes.post("/api/spaces", async (c) => {
try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
const body = await c.req.json();
const { name, slug, description, visibility = "public_read" } = body;
const { name, slug, description, visibility = "public" } = body;
if (!name || !slug) return c.json({ error: "name and slug required" }, 400);
if (!/^[a-z0-9-]+$/.test(slug)) return c.json({ error: "Invalid slug" }, 400);

View File

@ -291,6 +291,8 @@ export function renderShell(opts: ShellOptions): string {
// Render all tabs with the current one active
tabBar.setLayers(layers);
tabBar.setAttribute('active', 'layer-' + currentModuleId);
// Track current module as recently used
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
// Helper: save current tab list to localStorage
function saveTabs() {
@ -576,6 +578,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
tabBar.setLayers(layers);
tabBar.setAttribute('active', 'layer-' + currentModuleId);
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
function saveTabs() { localStorage.setItem(TABS_KEY, JSON.stringify(layers)); }
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });

View File

@ -1300,17 +1300,17 @@ export class RStackIdentity extends HTMLElement {
};
const visInfo = (v: string) =>
v === "members_only" ? { icon: "🔒", cls: "vis-private", label: "private" }
: v === "authenticated" ? { icon: "🔑", cls: "vis-permissioned", label: "permissioned" }
v === "private" ? { icon: "🔒", cls: "vis-private", label: "private" }
: v === "permissioned" ? { icon: "🔑", cls: "vis-permissioned", label: "permissioned" }
: { icon: "👁", cls: "vis-public", label: "public" };
const displayName = (s: any) => {
const v = s.visibility || "public_read";
if (v === "members_only") {
const v = s.visibility || "public";
if (v === "private") {
const username = getUsername();
return username ? `${username}'s (you)rSpace` : "(you)rSpace";
return username ? `${username}'s Space` : "My Space";
}
return `${(s.name || s.slug).replace(/</g, "&lt;")} rSpace`;
return (s.name || s.slug).replace(/</g, "&lt;");
};
const renderSpaces = (spaces: any[]) => {
@ -1318,7 +1318,7 @@ export class RStackIdentity extends HTMLElement {
const publicSpaces = spaces.filter((s) => !s.role && s.accessible);
const cardHTML = (s: any) => {
const vis = visInfo(s.visibility || "public_read");
const vis = visInfo(s.visibility || "public");
return `
<button class="space-card ${vis.cls}" data-slug="${s.slug}">
<div class="space-card-initial">${(s.name || s.slug).charAt(0).toUpperCase()}</div>

View File

@ -237,9 +237,8 @@
}
.badge-public { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
.badge-public_read { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
.badge-authenticated { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
.badge-members_only { background: rgba(239, 68, 68, 0.15); color: #f87171; }
.badge-permissioned { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
.badge-private { background: rgba(239, 68, 68, 0.15); color: #f87171; }
.num-cell {
text-align: right;
@ -433,9 +432,8 @@
<input type="text" class="search-input" id="search-input" placeholder="Search spaces..." />
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="public">Public</button>
<button class="filter-btn" data-filter="public_read">Public Read</button>
<button class="filter-btn" data-filter="authenticated">Auth</button>
<button class="filter-btn" data-filter="members_only">Private</button>
<button class="filter-btn" data-filter="permissioned">Permissioned</button>
<button class="filter-btn" data-filter="private">Private</button>
<select class="sort-select" id="sort-select">
<option value="created-desc">Newest First</option>
<option value="created-asc">Oldest First</option>
@ -607,10 +605,9 @@
function visibilityLabel(v) {
const labels = {
public: "Public",
public_read: "Public Read",
authenticated: "Auth Required",
members_only: "Members Only",
public: "👁 Public",
permissioned: "🔑 Permissioned",
private: "🔒 Private",
};
return labels[v] || v;
}

View File

@ -207,10 +207,9 @@
<div class="form-group">
<label for="visibility">Visibility</label>
<select id="visibility">
<option value="public">Public — anyone can read and write</option>
<option value="public_read" selected>Public Read — anyone can view, sign in to edit</option>
<option value="authenticated">Authenticated — sign in to view and edit</option>
<option value="members_only">Members Only — invite-only access</option>
<option value="public" selected>Public — anyone can read, sign in to write</option>
<option value="permissioned">Permissioned — sign in to read & write</option>
<option value="private">Private — invite-only access</option>
</select>
<p class="help-text">You can change this later in space settings.</p>
</div>