plresearch-org · edit-ai-opportunity-spaces-r2 · round 2

Round 2: editable opportunity spaces, end-to-end

@daviddao

Fix for round 1's failing scenario. Wired the AI & Robotics opportunity-spaces pages to the already-existing org.plresearch.opportunitySpace records on plresearch.org's PDS, shipped an inline editor, and re-ran the test. Full loop works: UI edit → PDS write → indexer catches up → public list + detail pages reflect the change.

Date: 2026-04-20 · Target: open-compute-networks · Previous round: r1 (fail) · pass


Headline result

7 / 7
End-to-end steps passed
6
Files changed in the fix
< 60s
Edit → public page latency
1
Transient save failure on retry

Headline. Round 1's blocker is resolved. The AI & Robotics opportunity-spaces detail page now renders an “Edit page” button for admins, which opens a dedicated inline editor. Saves write org.plresearch.opportunitySpace records to plresearch.org's PDS through a new PUT /api/opportunity-spaces/[rkey] route; the indexer picks them up live; revalidateTag("indexer") busts the Next.js ISR cache so the public list and detail pages reflect the change within seconds. Only finding: one transient save failed (likely a stale cookie after navigation) and succeeded immediately on retry.

What changed (the fix)

Round 1 concluded that no ATProto record backs these pages. That was wrong in spirit — 15 org.plresearch.opportunitySpace records were already on the PDS, the indexer already surfaced them via GraphQL, and the website already had fetchOpportunitySpaces() / fetchOpportunitySpace() helpers in src/lib/indexer.ts. The only thing missing was the last-mile plumbing. Round 2 wires it up.

Branchfeat/editable-opportunity-spacesmain Commitb3e6c4dfeat(ai-robotics opspaces): indexer-backed data + inline editor Diff6 files, +451/-6
FileChange
src/app/areas/ai-robotics/opportunity-spaces/page.tsxList: fetchOpportunitySpaces('ai-robotics') with static JSON fallback.
src/app/areas/ai-robotics/opportunity-spaces/[slug]/page.tsxDetail: fetchOpportunitySpace(rkey) with fallback; renders <EditPageButton/>.
src/app/areas/ai-robotics/opportunity-spaces/[slug]/edit/page.tsxNew. Admin-gated inline editor (title, tagline, description, inflection point, shift, opportunity).
src/app/api/opportunity-spaces/[rkey]/route.tsNew. GET + PUT. Mirrors /api/pages/[rkey] pattern: session + ADMIN_DIDS gate, impersonates plresearch.org via app password, calls revalidateTag("indexer") after a successful write.
src/components/EditPageButton.tsxAdded optional href prop so non-page-collection edit buttons can override EDIT_ROUTES.
src/lib/lexicons.tsAdded OpportunitySpaceRecord type + opportunitySpaceRkey(areaSlug, id) helper (rkeys use ai-- / dhr-- / eg-- / neuro-- prefixes per commit bb88ffa).

Walkthrough

Detail page with edit button visible
01After login, /areas/ai-robotics/opportunity-spaces/open-compute-networks/ now renders the “Edit page” button top-right for admins — the affordance that was missing in round 1.
Edit page loaded with all fields as textareas
02Clicking the button routes to /areas/ai-robotics/opportunity-spaces/open-compute-networks/edit. The editor loads the current record via GET /api/opportunity-spaces/ai--open-compute-networks and renders title, tagline, description, inflection point, shift, and opportunity as in-place EditableField textareas. Fixed “Editing mode — click any text to edit” bar at the bottom with a disabled Save button (no changes yet).
Tagline edited; Save bar shows 'Unsaved changes'
03Appending [edited via pi-eval test] to the tagline turns the Save bar active (“Unsaved changes”, Discard + Save enabled). The textarea lights up with a blue focus ring.
Save succeeded; bar returned to idle state
04After Save the bar briefly shows “✓ Saved” (green dot) and returns to idle with Save disabled — exactly like the existing /areas/<slug>/edit flow for page records.
Public list page shows the edited tagline
05Hard-reload of the public list page /areas/ai-robotics/opportunity-spaces/ shows the new tagline (“… beyond the hyperscalers [edited via pi-eval test]”) within a few seconds of save. Confirms the full chain works: UI → PUT → PDS → indexer → revalidateTag → ISR.
Transient save failure on revert attempt
06On the follow-up edit to revert the tagline, the first Save attempt returned “Save failed — try again.” Retrying immediately succeeded (screenshot 07). Likely a stale cookie after the browser navigated between tabs — captured as a minor warning (W1 below).
Public list page shows the tagline fully reverted
07After retry, the public list page reflects the reverted tagline. PDS updatedAt moved to 19:46:54Z. No state left over from the test.

PDS verification (xrpc)

Don't trust the UI — confirm the record actually landed:

$ curl -sG "https://leccinum.us-west.host.bsky.network/xrpc/com.atproto.repo.getRecord" \
    --data-urlencode "repo=did:plc:pgwr6hkosgznfl5nz7egajei" \
    --data-urlencode "collection=org.plresearch.opportunitySpace" \
    --data-urlencode "rkey=ai--open-compute-networks" | jq '{uri, cid, tagline: .value.tagline, updatedAt: .value.updatedAt}'
{
  "uri": "at://did:plc:pgwr6hkosgznfl5nz7egajei/org.plresearch.opportunitySpace/ai--open-compute-networks",
  "cid": "bafyreibyq5wrnfe4ogf7meibnpnh4ijthsvo2rnsh6lee5cgj7vfrdkxx4",
  "tagline": "Distributed infrastructure for AI training and inference beyond the hyperscalers [edited via pi-eval test]",
  "updatedAt": "2026-04-20T19:44:27.547Z"
}

And the indexer GraphQL reflects the same record:

$ curl -s -X POST "https://plresearch-indexer-production.up.railway.app/graphql" \
    -H "Content-Type: application/json" \
    -d '{"query":"{ orgPlresearchOpportunitySpace(where:{uri:{eq:\"at://did:plc:pgwr6hkosgznfl5nz7egajei/org.plresearch.opportunitySpace/ai--open-compute-networks\"}}, first:1){ edges { node { tagline updatedAt } } } }"}'
{ "data": { "orgPlresearchOpportunitySpace": { "edges": [{
    "node": { "tagline": "… [edited via pi-eval test]", "updatedAt": "2026-04-20T19:44:27.547Z" }
}] } } }

Findings

P1 · Round 1's blocker is resolved

The Edit page button appears on /areas/ai-robotics/opportunity-spaces/[slug]/ for admins. Clicking it opens a working inline editor. Saves persist to the PDS and flow back to the public pages.

P2 · Graceful degradation when the indexer is down

Both list and detail pages keep the static JSON in src/data/fa2/ai-opportunityspaces.json as a build-time fallback (same pattern as fetchPage in other routes). If the Railway indexer is unreachable, pages still render the seed content rather than 500.

P3 · Security gate is enforced in two places

PUT /api/opportunity-spaces/[rkey] rejects the request with 401 if there's no session, and 403 if the session DID isn't in ADMIN_DIDS — mirroring /api/pages/[rkey]. The UI-side useRequireAdmin() guard redirects non-admins to /admin. Verified by unauthenticated curl returning 401.

W1 · Transient save failure on retry (screenshot 06)

After a successful save, reopening the edit page and submitting a second save returned “Save failed — try again.” Retrying immediately worked. I did not capture the network tab, but the most likely cause is a cookie serialisation race in iron-session during the navigation between routes (the session store keeps state in both memory and cookie per src/lib/auth/client.ts). Not blocking — the retry succeeded — but worth investigating if it recurs.

I1 · Only AI & Robotics is wired in this round

The other three areas (Digital Human Rights, Neurotech, Economies & Governance) still read from their static JSON files and have no edit button. A follow-up round should generalise the pattern — it's now just 2×3 files of copy-paste plus an rkey-prefix update, since the API route and opportunitySpaceRkey() helper already handle all four areas.

I2 · Arrays aren't editable yet

The editor covers the prose fields. subfields[], tippingSignals[], keyAssumptions[], observations[], and fieldSignals[{kpi, measurement}] are read-only from the UI in this round. PUT accepts them if sent, so a future editor (or /admin) can add them without API changes.

Reproducing

URLhttps://www.plresearch.org/admin Handlesatyam2.climateai.org Passwordtest password — redacted in this report Target edit page/areas/ai-robotics/opportunity-spaces/open-compute-networks/edit Toolagent-browser (snapshot-driven) PDS verifycurl -sG https://leccinum.us-west.host.bsky.network/xrpc/com.atproto.repo.getRecord --data-urlencode repo=did:plc:pgwr6hkosgznfl5nz7egajei --data-urlencode collection=org.plresearch.opportunitySpace --data-urlencode rkey=ai--open-compute-networks Round 1 reportr1 (fail) Screenshotsassets/01-*.pngassets/07-*.png