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:
@@ -1675,10 +1675,14 @@ novel.
|
||||
### 14.2 The `/philosophy` route
|
||||
|
||||
Authenticated and anonymous visitors alike can reach `/philosophy`,
|
||||
which renders the full body of `PHILOSOPHY.md` from the meta repo. The
|
||||
content is sourced from the meta repo's main branch, cached and
|
||||
refreshed on the same cadence as RFC bodies (§4). The page is plain
|
||||
markdown rendering with no editing affordance.
|
||||
which renders the full body of `PHILOSOPHY.md`. The content is cached
|
||||
in the app process and refreshed on demand; the source of the file is
|
||||
a deployment-time decision — Slice 7's build sources it from the app
|
||||
repo (the file lives alongside `SPEC.md`, since the philosophy is the
|
||||
framework's design document rather than an RFC entry), and a
|
||||
`PHILOSOPHY_PATH` env var can point at a meta-repo working-tree clone
|
||||
or any other sync target if a deployment prefers that shape. The page
|
||||
is plain markdown rendering with no editing affordance.
|
||||
|
||||
### 14.3 Persistent "About" link
|
||||
|
||||
@@ -2318,9 +2322,35 @@ The follow-up session will refine this. A minimal starting set:
|
||||
- `POST /api/rfcs/<slug>/prs/<pr_number>/withdraw` — withdraw per §10.8.
|
||||
- `POST /api/rfcs/<slug>/prs/<pr_number>/resolution-branch` — cut a
|
||||
fresh resolution branch and replay per §10.9.
|
||||
- `POST /api/admin/users/<id>/role` — set role (owner/admin only).
|
||||
- `POST /api/admin/users/<id>/mute` — mute/unmute (the §6.2 app-wide
|
||||
write-mute, not the §15.8 notification mutes).
|
||||
- `GET /api/admin/users` — list users with role and write-mute state,
|
||||
for the §6 / Slice 7 admin surface.
|
||||
- `POST /api/admin/users/<id>/role` — set role. Only owners may grant
|
||||
or revoke `owner`; admins may flip contributor ↔ admin freely. An
|
||||
owner-self-demotion is refused on this endpoint; owner succession
|
||||
earns its own ceremony (§19.2). Writes a `permission_events` row.
|
||||
- `POST /api/admin/users/<id>/mute` — set the §6.2 app-wide
|
||||
write-mute (not the §15.8 notification mutes). Refused on owners
|
||||
and admins — for them, the role-change channel is the right
|
||||
refusal. Writes a `permission_events` row.
|
||||
- `GET /api/admin/audit` — paged read of the `actions` log with
|
||||
filters `action_kind`, `actor_user_id`, `rfc_slug`, plus `before_id`
|
||||
for the page boundary. Returns the joined actor login/display so
|
||||
the surface can render row prose without a second round-trip.
|
||||
- `GET /api/admin/permission-events` — paged read of
|
||||
`permission_events` (role changes, write-mute toggles), joined
|
||||
against `users` for actor and subject. Same `before_id` paging.
|
||||
- `GET /api/admin/graduation-queue` — the §13.2-ready set: returns
|
||||
super-drafts partitioned into `ready` (owners set, zero open
|
||||
body-edit PRs) and `blocked` (one or both preconditions missing),
|
||||
with the precondition shape carried in each row.
|
||||
- `GET /api/users/me/notification-mutes` — list the §15.8 per-user
|
||||
mutes the signed-in user has set, joined against `users` for the
|
||||
rendered handle and display name. The companion read endpoint to
|
||||
the add/delete pair.
|
||||
- `GET /api/users/search` — typeahead over `gitea_login` and
|
||||
`display_name`, ten-row cap, prefix-first ranking. Powers the
|
||||
§15.8 mute-add typeahead in `/settings/notifications`. Excludes
|
||||
the caller. Open to any authenticated viewer.
|
||||
- `POST /api/stars/<slug>` — star/unstar.
|
||||
- `POST /api/webhooks/gitea` — webhook receiver.
|
||||
- `GET /api/notifications` — list inbox rows for the signed-in user,
|
||||
@@ -2405,93 +2435,109 @@ surface. With Topic 13 folded in, the structural surface is
|
||||
complete. What follows is no longer "topics that block specifying
|
||||
v1" but "topics to address during or shortly after the v1 build."
|
||||
|
||||
### 19.1 Next slice: the §14 chrome and the settings neighborhood
|
||||
### 19.1 Next slice: hardening
|
||||
|
||||
Slice 6 of the build has landed. The §15 notifications surface runs
|
||||
end-to-end against the local Gitea — every `actions` row whose
|
||||
`action_kind` maps to a §15.1 event fans out through
|
||||
`notify.fan_out_from_action`, called inline from `bot._log` and
|
||||
from the graduation orchestrator's `_audit`. Chat-message inserts
|
||||
take a parallel path through `notify.fan_out_chat_message` from
|
||||
inside `chat.append_user_message`, since chat doesn't flow through
|
||||
the bot wrapper. The §15.6 auto-watch upsert sits in the same
|
||||
chokepoint — every substantive gesture either creates a `watching`
|
||||
row or bumps `last_participation_at` for the 90-day decay timer.
|
||||
Slice 7 of the build has landed. The §14 chrome, the
|
||||
`/settings/notifications` neighborhood, and the `/admin` home base
|
||||
all run end-to-end against the local Gitea, and the next slice has
|
||||
the v1 surface fully wrapped — what remains is the hardening pass
|
||||
that lets a single-operator deployment actually run.
|
||||
|
||||
The §15.4 email loop runs through an SMTP adapter with a stdout
|
||||
fallback for dev — the in-memory `_SENT` buffer is what the
|
||||
integration tests read from. The per-category dispatch holds during
|
||||
§15.8 quiet hours; on window-end, `email.flush_pending` bundles
|
||||
above the §15.4 threshold into a single "Activity while you were
|
||||
away" mail. The signed-URL unsubscribe path flips a single category
|
||||
column to zero; the bounce webhook flips the new `email_opt_out_all`
|
||||
column (migration `008_email_opt_out.sql`).
|
||||
The §14.1 landing page now carries the title, the subtitle, the
|
||||
short-form pitch from `PHILOSOPHY.md`, the sign-in affordance, the
|
||||
secondary "Read the full philosophy" link, and a three-item deck
|
||||
underneath the pitch that names what the framework is — one word per
|
||||
RFC, argued in public with the model, graduation as the load-bearing
|
||||
moment. The §14.2 `/philosophy` route reads `PHILOSOPHY.md` from
|
||||
disk (via `backend/app/philosophy.py`, configurable through the
|
||||
`PHILOSOPHY_PATH` env var) and renders it inline with the existing
|
||||
`marked` library — the same renderer the proposal preview already
|
||||
uses. §14.3's persistent About link sits in the header next to the
|
||||
inbox badge and the new Settings / Admin (admin-only) entries; the
|
||||
header's visual budget stays tight, and each entry reads as a quiet
|
||||
text link rather than a button.
|
||||
|
||||
The §15.5 digest is a `DigestScheduler` wrapping `cache.Reconciler`'s
|
||||
shape, with a `run_tick` seam the tests drive synchronously. Each
|
||||
tick releases held emails, runs the §15.6 90-day decay sweep, and
|
||||
assembles per-cadence digests where the window has rolled over.
|
||||
The §15.5 exclusion rules (already-emailed, already-read,
|
||||
personal-direct-excluded) keep two consecutive ticks idempotent.
|
||||
The notification-settings surface (`/settings/notifications`,
|
||||
`frontend/src/components/NotificationSettings.jsx`) lands the five
|
||||
sub-sections the §15 endpoints already supported: the §15.4 per-
|
||||
category email toggles (with the `email_watched_churn` toggle
|
||||
permanently disabled and the §15.4 refusal tooltip inline), the
|
||||
§15.5 digest cadence dropdown, the §15.8 quiet-hours editor (three
|
||||
inputs against `Intl.supportedValuesOf('timeZone')`, with all-three-
|
||||
or-clear validation enforced server-side), the §15.6 watches
|
||||
overview (per-row state selector that flips `set_by` to `explicit`
|
||||
on override), and the §15.8 per-user mute list with an unmute
|
||||
affordance and a typeahead add. Owners and admins see the §15.8
|
||||
mute-list with the "cannot mute" prose inline. The §15.4 email
|
||||
footer's `Manage all preferences` link — wired in Slice 6 — now
|
||||
resolves to a real surface.
|
||||
|
||||
§15.2 / §15.3 / §15.7 / §15.8 surface as fourteen endpoints in
|
||||
`backend/app/api_notifications.py`, plus the chat-seen advance on
|
||||
`api_branches` and the existing PR seen-cursor on `api_prs` — both
|
||||
extended to call `notify.reconcile_seen_advance` so the §15.7
|
||||
visit-advances-cursor loop closes back into the inbox-row read
|
||||
state. The SSE stream holds a per-user subscriber queue keyed by
|
||||
user_id; multiple browser tabs see the same events.
|
||||
The admin home base (`/admin`, `frontend/src/components/Admin.jsx`)
|
||||
runs as a tabbed left-rail with four panels: Users (role management
|
||||
+ §6.2 write-mute, with the role-grant constraints enforced
|
||||
server-side per §6.1 — only owners may grant owner; owners cannot
|
||||
self-demote on the role endpoint), Graduation queue (super-drafts
|
||||
partitioned by §13.2 readiness — owners set and zero blocking body-
|
||||
edit PRs), Audit log (paged read of `actions` with filter chips for
|
||||
`action_kind`, `actor_user_id`, and `rfc_slug`), and Permission
|
||||
events (paged read of `permission_events` showing the role and
|
||||
mute history). Every `/api/admin/*` endpoint guards independently
|
||||
through `require_admin`, and the User-search endpoint (open to all
|
||||
authenticated viewers) powers both the admin user-roster and the
|
||||
mute typeahead.
|
||||
|
||||
On the frontend, `App.jsx` grew a header badge (cap "99+",
|
||||
clicking opens the inbox overlay), an SSE-driven counter that
|
||||
surfaces personal-direct toasts (own-name signals) and live-view
|
||||
toasts (events landing on the slug the user is viewing). The
|
||||
inbox is `Inbox.jsx` — three filter chips (Unread only, RFC,
|
||||
Category), a Bundle toggle, and a "Mark all read (under filter)"
|
||||
button. `ToastHost.jsx` caps four visible at once with auto-dismiss.
|
||||
`backend/app/api_admin.py` carries the seven new admin endpoints
|
||||
plus the user-search. `backend/app/philosophy.py` carries the
|
||||
disk-backed `/api/philosophy` source. `backend/app/api_notifications.py`
|
||||
grew one read endpoint (`GET /api/users/me/notification-mutes`) for
|
||||
the settings page's mute list. The §17 admin block was extended in
|
||||
this corrected spec to name the seven endpoints; §14.2 was
|
||||
corrected to acknowledge the deployment-time decision about where
|
||||
`PHILOSOPHY.md` lives.
|
||||
|
||||
The §15.9 attribution rule fell out cleanly: every `notifications`
|
||||
row carries `actor_user_id` resolved from the `actions.actor_user_id`
|
||||
in the originating audit row (the underlying user, never the bot).
|
||||
System-generated events (digest emission, 90-day decay) leave
|
||||
`actor_user_id` NULL and render as "the app." AI participation
|
||||
events landed as null-system per §19.2's candidate naming — when a
|
||||
chat message authored by an AI provider goes through, no actor row
|
||||
is written, since the LLM call doesn't have a user_id; the topic
|
||||
folder for "AI participation as a notification source" in §19.2
|
||||
remains open for explicit settling.
|
||||
Slice 7 ships covered by `backend/tests/test_chrome_vertical.py` —
|
||||
thirteen integration tests covering the philosophy route for both
|
||||
anonymous and authenticated callers, the §15.4 / §15.5 / §15.8
|
||||
preferences round-trip (including the permanent `email_watched_churn`
|
||||
refusal), the quiet-hours all-or-nothing validation, the §15.8 mute
|
||||
add/list/unmute round-trip, the user-search typeahead, the admin
|
||||
role and write-mute round-trips with their `permission_events`
|
||||
audit, the §6.1 refusal of owner-grant by non-owners, the audit-log
|
||||
filter chips, the graduation-queue partition under both
|
||||
preconditions, and the permission-events listing. The full Slices
|
||||
1–7 test suite is 75/75 green.
|
||||
|
||||
Slice 6 ships covered by `backend/tests/test_notifications_vertical.py`
|
||||
— seventeen integration tests covering the producer-side fan-out
|
||||
on the propose/merge/decline chain, §15.6 auto-watch, the §15.2
|
||||
inbox listing with filter chips, the §15.7 chat-seen reconciler,
|
||||
the §15.8 per-user mute and the per-RFC mute, the §15.4 email-
|
||||
bounce webhook flipping the global opt-out, the `/email/unsubscribe`
|
||||
signed-URL path, the §15.8 quiet-hours email hold, the §15.5
|
||||
digest's emit-then-skip behavior across two consecutive ticks,
|
||||
preferences and quiet-hours round-trips, the explicit-watch
|
||||
override that prevents auto-downgrade, and the SSE subscriber/
|
||||
broadcast substrate. The full Slices 1–6 test suite is 62/62 green.
|
||||
**Slice 8 is the hardening pass — the last slice of the v1 build.**
|
||||
Three pieces hang together:
|
||||
|
||||
**Slice 7 is the §14 chrome plus the natural notification-settings
|
||||
neighbor.** With every structural beat live, what remains for v1
|
||||
is the chrome the framework wraps itself in. §14 commits the
|
||||
landing page (the unauthenticated visitor's first read), the
|
||||
`/philosophy` route (PHILOSOPHY.md surfaced inline), and the
|
||||
persistent About link in the header. Slice 6 left the §15
|
||||
preferences / quiet-hours / mute / watches endpoints in place
|
||||
but with no chrome — the natural follow-on is `/settings/notifications`
|
||||
exposing the per-category toggles, the digest cadence dropdown,
|
||||
the quiet-hours editor, the watches overview, and the per-user
|
||||
mute list. The §19.2 "admin surfaces" candidate is the second
|
||||
natural neighbor — role management, the §6.2 app-wide write-mute,
|
||||
the audit-log viewer, the graduation-readiness queue, all
|
||||
consolidated where the chrome can hold them. Slice 7 picks the
|
||||
framing and ships the three pieces together since they share an
|
||||
information architecture.
|
||||
The §12 30/90 branch-hygiene timers — the formalized policy that
|
||||
closes the loop on §11.5's branch lifecycle (open → merged → 30d
|
||||
read-only → 90d deleted-by-bot, with the per-user-message-cursor
|
||||
preservation contract). The wiring is a scheduled task next to the
|
||||
§15.5 digest scheduler; the §10.7 90-day deletion timer Slice 3
|
||||
left deferred lives here too.
|
||||
|
||||
An end-to-end smoke pass over the working surfaces — propose →
|
||||
super-draft → branch → PR → merge → graduate → active-RFC PR →
|
||||
notification fans out → inbox → email — to catch the integration
|
||||
seams a per-slice test wouldn't. Plus the §19.2 candidates the
|
||||
hardening pass is the natural place to fold in: cache bootstrap
|
||||
from a meta repo (the audit-log-first attribution shape Slice 1
|
||||
chose, exercised against a meta repo with history the bot did not
|
||||
author), branch-name path routing (converting every
|
||||
`branches/<branch>` to `{branch:path}` with route-ordering
|
||||
discipline), and the small Slice-2-onward follow-ons that are
|
||||
deferred until the hardening pass demands them.
|
||||
|
||||
The dev/prod deployment shape — the `deploy/` directory already
|
||||
has the nginx vhost, the systemd unit, and a runbook stub; Slice 8
|
||||
proves the bring-up against a fresh host, settles the secret-
|
||||
material handling (the existing `.env.example` plus the §15.4
|
||||
SMTP wiring), and lands the README updates that let a new operator
|
||||
get from `git clone` to a signed-in browser.
|
||||
|
||||
The next build session should read `SPEC.md`, `README.md`,
|
||||
`docs/DEV.md`, and this §19.1 entry and pick up Slice 7 cleanly
|
||||
`docs/DEV.md`, and this §19.1 entry and pick up Slice 8 cleanly
|
||||
without re-briefing. The working agreement in §19.3 continues to
|
||||
apply: implement the slice, correct the spec only where running
|
||||
code reveals it was wrong at a structural level, accumulate new
|
||||
@@ -2785,6 +2831,45 @@ binding.
|
||||
or signature verification (Sendgrid's signed events, AWS SES's
|
||||
SNS topic signature, etc.). Trivial to add per provider; the
|
||||
routing-and-flip-the-column logic doesn't change.
|
||||
- **Owner succession ceremony.** Slice 7's `POST /api/admin/users/<id>/role`
|
||||
refuses self-demotion ("Use the explicit succession path to change
|
||||
your own role") because owner-zero is the only owner bootstrap path
|
||||
per §6.1 and a careless self-downgrade could orphan the role. The
|
||||
explicit succession path — how an owner steps down, whether owner-
|
||||
zero needs a co-owner present, how the `OWNER_GITEA_LOGIN` env var
|
||||
relates to the seated-owner set after the bootstrap moment — is the
|
||||
natural follow-on once a real owner-transition scenario shows up.
|
||||
Touches §6.1 (the owner-role bootstrap rule), §17 (the admin role
|
||||
endpoint), and possibly §3.1 (state-transition shape if owner
|
||||
changes are themselves a tracked transition).
|
||||
- **Mute-from-actor on inbox rows and chat messages.** Slice 7's
|
||||
notification-settings page exposes the per-user mute list with an
|
||||
unmute affordance, and an intentionally clumsy typeahead for the
|
||||
add path. The natural add path — clicking the actor on an inbox
|
||||
row or a chat message — is the §19.2 candidate of its own this
|
||||
slice was always going to surface. Touches §15.8 (the mute add
|
||||
ergonomics), §15.2 (the inbox row's actor slot), and §8.12 (the
|
||||
chat message's author chip). Small scope, defer-able until the
|
||||
typeahead-only path proves annoying.
|
||||
- **The "Following since <date>" disclosure on the RFC view header.**
|
||||
§15.6 commits this disclosure on the RFC view's header after an
|
||||
auto-decay. Slice 7 lands the watches overview at
|
||||
`/settings/notifications` — the centralized read — and defers the
|
||||
per-RFC chrome to a later session, since the disclosure earns its
|
||||
surface only once auto-decay has actually fired against a watch in
|
||||
production use. Touches §15.6 (the decay timer's UX) and §8.1 (the
|
||||
RFC view's header strip).
|
||||
- **Audit-log row prose translation.** Slice 7's `/admin → Audit log`
|
||||
surface renders `action_kind` as the raw enum (`merge_branch_pr`,
|
||||
`propose_rfc`, `graduate_step_complete`, etc.) inside a `<code>`
|
||||
span. Admins know the kinds; a contributor-facing surface — if one
|
||||
ever earns its place — would need a per-kind English render
|
||||
("merged the body-edit PR", "proposed RFC", "completed the
|
||||
repo-create step of graduation"). The translation table lives near
|
||||
`notify.SUMMARY` per §15.9 — a future session can lift the same
|
||||
surface to the admin view if usage shows reviewers want it.
|
||||
Defer-able until evidence of demand. Touches §6.5 (the audit-log
|
||||
surface) and §17 (the audit endpoint's prose shape).
|
||||
- **Per-user mute exemption checks for arbiters.** §15.8 commits
|
||||
that arbiters cannot mute participants on RFCs where they hold
|
||||
authority. Slice 6's check uses "the muted user has a watches
|
||||
|
||||
Reference in New Issue
Block a user