Slice 7: §14 chrome + settings and admin neighborhoods

§14.1 richer landing, §14.2 /philosophy route (disk-backed), §14.3
persistent About link. /settings/notifications surfaces Slice 6's
preferences/quiet-hours/mute/watches endpoints. /admin home base
consolidates role management, the §6.2 write-mute, the audit-log
viewer, the permission-events log, and the §13.2 graduation queue.

Backend: backend/app/philosophy.py, backend/app/api_admin.py (seven
admin endpoints + user-search), GET /api/users/me/notification-mutes.
Frontend: Landing.jsx (deck), Philosophy.jsx, NotificationSettings.jsx,
Admin.jsx, App.jsx routing for the chrome surfaces.

Tests: backend/tests/test_chrome_vertical.py — 13 cases. Full suite
75/75 green.

Spec corrections: §14.2 (PHILOSOPHY.md source is a deployment-time
decision), §17 (admin block extended to name the seven new endpoints
+ user-search and notification-mutes read). §19.1 rewritten for
Slice 8 hardening; §19.2 grew four candidates (owner succession,
mute-from-actor, the "Following since <date>" disclosure, audit-log
row prose).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-24 23:40:49 -07:00
parent f67d0aa0db
commit 060fa408a2
14 changed files with 2722 additions and 158 deletions
+79 -17
View File
@@ -8,6 +8,9 @@ import PRView from './components/PRView.jsx'
import ProposalView from './components/ProposalView.jsx'
import ProposeModal from './components/ProposeModal.jsx'
import Landing from './components/Landing.jsx'
import Philosophy from './components/Philosophy.jsx'
import NotificationSettings from './components/NotificationSettings.jsx'
import Admin from './components/Admin.jsx'
import ToastHost, { showToast } from './components/ToastHost.jsx'
import './App.css'
@@ -65,8 +68,16 @@ export default function App() {
return <div className="boot">Loading</div>
}
// §14.2: the philosophy route is reachable by anonymous visitors too.
// Resolve it before the authentication gate so a signed-out reader
// who follows the §14.1 landing link does not get bounced to sign-in.
if (!me?.authenticated) {
return <Landing />
return (
<Routes>
<Route path="/philosophy" element={<Philosophy authenticated={false} />} />
<Route path="*" element={<Landing />} />
</Routes>
)
}
return (
@@ -76,6 +87,21 @@ export default function App() {
<Link to="/">Wiggleverse RFCs</Link>
</div>
<div className="header-right">
{/* §14.3: the persistent About link. One word, no badge, no
state — visible from every authenticated screen so a
contributor mid-PR who wonders why a conversation is
public can reach the answer in two clicks. */}
<Link to="/philosophy" className="header-about" title="Why this exists (§14)">
About
</Link>
<Link to="/settings/notifications" className="header-settings" title="Notification settings (§15)">
Settings
</Link>
{(me.user.role === 'owner' || me.user.role === 'admin') && (
<Link to="/admin" className="header-admin" title="Admin home base">
Admin
</Link>
)}
<button
className="inbox-trigger"
onClick={() => setInboxOpen(o => !o)}
@@ -92,18 +118,27 @@ export default function App() {
</div>
</header>
<div className="app-body">
<Catalog
onProposeRFC={() => setProposeOpen(true)}
version={catalogVersion}
/>
<main className="main-pane">
<Routes>
<Route path="/" element={<Welcome viewer={me.user} />} />
<Route path="/rfc/:slug" element={<RFCView viewer={me.user} />} />
<Route path="/rfc/:slug/pr/:prNumber" element={<PRView viewer={me.user} />} />
<Route path="/proposals/:prNumber" element={<ProposalView viewer={me.user} onChange={() => setCatalogVersion(v => v + 1)} />} />
</Routes>
</main>
<Routes>
<Route path="/philosophy" element={<PhilosophyWithSidebar viewer={me.user} />} />
<Route path="/settings/notifications" element={<NotificationSettingsWithSidebar viewer={me.user} />} />
<Route path="/admin/*" element={<AdminWithSidebar viewer={me.user} />} />
<Route path="*" element={
<>
<Catalog
onProposeRFC={() => setProposeOpen(true)}
version={catalogVersion}
/>
<main className="main-pane">
<Routes>
<Route path="/" element={<Welcome viewer={me.user} />} />
<Route path="/rfc/:slug" element={<RFCView viewer={me.user} />} />
<Route path="/rfc/:slug/pr/:prNumber" element={<PRView viewer={me.user} />} />
<Route path="/proposals/:prNumber" element={<ProposalView viewer={me.user} onChange={() => setCatalogVersion(v => v + 1)} />} />
</Routes>
</main>
</>
} />
</Routes>
</div>
{proposeOpen && (
<ProposeModal
@@ -123,6 +158,34 @@ export default function App() {
)
}
function PhilosophyWithSidebar() {
// The chrome surfaces (§14.2 philosophy, §15 settings, §6/§17 admin)
// all use the full app body — no catalog left pane, no propose modal.
// The header carries the navigation back; the body is a single
// reading surface.
return (
<main className="chrome-pane">
<Philosophy authenticated={true} />
</main>
)
}
function NotificationSettingsWithSidebar({ viewer }) {
return (
<main className="chrome-pane">
<NotificationSettings viewer={viewer} />
</main>
)
}
function AdminWithSidebar({ viewer }) {
return (
<main className="chrome-pane">
<Admin viewer={viewer} />
</main>
)
}
function Welcome({ viewer }) {
return (
<div className="welcome">
@@ -134,10 +197,9 @@ function Welcome({ viewer }) {
idea PR against the meta repository.
</p>
<p>
Slice 1 of the build is in place: propose idea PR owner merges
super-draft appears in the catalog super-draft view renders. The
revision flow, per-branch chat, AI participation, and the PR surface
land in subsequent slices.
Wondering why a conversation is public, why graduation costs what it
does, or why the model is in the chat? <Link to="/philosophy">Read the
philosophy</Link>.
</p>
</div>
)