diff --git a/AGENTS.md b/AGENTS.md
index 97547f66..0b69e684 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -113,6 +113,19 @@ UI layering is documented in `FRONTEND.md` — read it before adding styles. Sho
| `staging` | push | App Engine staging (`CD_staging.yml`) |
| `production` | push | App Engine production (`CD_production.yml`) |
+### Where unfinished work gets exercised
+
+**Preview deploys are the sandbox. `staging` is a pre-production release branch — what is on it is a candidate for `production`, not an experiment.** Anything not ready to ship gets exercised on its own PR preview (see `docs/preview-deployments.md`), which can run against an ephemeral API nobody else shares.
+
+In code, that means WIP surfaces gate on dev or preview, never on staging:
+
+```ts
+export const SHOW_WIP_FEATURES =
+ import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'preview'
+```
+
+`recordsGridLogic.ts` gates exactly this way. Do not add `'staging'` to that check, and do not add a staging arm to a new one.
+
### Where to branch from
**Everything bases off `staging` and targets `staging` in its PR — feature, fix, chore, docs, and CI work alike.** The single exception is a hotfix, which bases off `production` and targets `production`.
diff --git a/docs/access-control-ruleset.md b/docs/access-control-ruleset.md
index b63825bc..4da44649 100644
--- a/docs/access-control-ruleset.md
+++ b/docs/access-control-ruleset.md
@@ -45,7 +45,6 @@ This mapping is temporary and should be removed before the v1 release once all u
- Can view confidential data
### AMP.Admin Only
-- Can access `Sandbox`
- Can access unfinished / WIP AMP resources
### Geothermal Roles
@@ -73,8 +72,7 @@ This mapping is temporary and should be removed before the v1 release once all u
- `ocotillo.thing-well-batch-export` -> AMP view access required
- `ocotillo.groundwater-level-observation` -> AMP view access required
-### AMP / Sandbox
-- `Sandbox` -> `AMP.Admin` only
+### AMP / Water
- `water.*` -> AMP access required
- WIP `water.*` resources -> `AMP.Admin` only
- `water.locations` -> `AMP.Admin` or `Geothermal.Admin`
diff --git a/docs/geothermal-well-inventory.md b/docs/geothermal-well-inventory.md
index 534fabf3..85e4bf59 100644
--- a/docs/geothermal-well-inventory.md
+++ b/docs/geothermal-well-inventory.md
@@ -145,9 +145,8 @@ as the records grid (bypassed in local dev, enforced in prod).
## 8. Navigation & route
- Route: `/geothermal/wells/inventory`.
-- Nav: a **Geothermal Inventory** entry (Sandbox section for now, consistent
- with the current geothermal placement; promote to a real geothermal nav
- group when the feature graduates).
+- Nav: none yet. The page is reachable by URL while the work is in progress;
+ it gets a nav entry when a geothermal nav group lands.
---
@@ -164,7 +163,7 @@ as the records grid (bypassed in local dev, enforced in prod).
- Boolean, date, and **dropdown** cell kinds in `EditableDataGrid`, plus
allowed-value lists for the enum fields.
- Inventory page (grid + toolbar: Add rows, Upload CSV, Download template, Save).
-- Route + nav entry (Sandbox).
+- Route (no nav entry yet).
- Create-only save wrapper (adapt records-grid save to POST-only).
---
@@ -176,7 +175,7 @@ as the records grid (bypassed in local dev, enforced in prod).
surface the conflict inline. No client-side dedupe/upsert in v1. ✔
- **Enum fields** (`well_type`, `well_class`, `status`): **dropdowns** from
fixed allowed-value lists (new select cell kind). ✔
-- **Nav placement:** Sandbox (temporary). ✔
+- **Nav placement:** none yet — URL-only until a geothermal nav group lands. ✔
- **Records vs inventory:** inventory is create-wells only; the existing records
grid is untouched. ✔
diff --git a/docs/geothermal-well-search-contract.md b/docs/geothermal-well-search-contract.md
index f3f357f5..0fb8c507 100644
--- a/docs/geothermal-well-search-contract.md
+++ b/docs/geothermal-well-search-contract.md
@@ -6,7 +6,7 @@ endpoint at all.
## Background
-The geothermal Sandbox pages (Records Grid, Temp-Depth log) start with a well
+The geothermal grid pages (Records Grid, Temp-Depth log) start with a well
picker. It used to be a single dropdown populated by one request:
```
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index 0083504d..f3a4b7d6 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile'
-import { Outlet, Link, useLocation, useNavigate } from 'react-router'
+import { Outlet, Link, useLocation } from 'react-router'
import {
CanAccess,
useCustomMutation,
@@ -50,7 +50,6 @@ import {
Check,
ChevronDown,
ChevronRight,
- FlaskConical,
Lock,
LogOut,
Menu,
@@ -63,7 +62,7 @@ import {
import { ColorModeContext } from '@/contexts'
import SearchBar from '@/components/SearchBar'
import { ReportBugButton } from '@/components/Button'
-import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, SHOW_EXAMPLE_NAV, type NavItem } from '@/config/navigation'
+import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, type NavItem } from '@/config/navigation'
import { useAccessCapabilities } from '@/hooks'
import { useSearch } from '@/providers/search-provider'
import { SupportPanelContext } from '@/components/SupportPanelContext'
@@ -425,7 +424,7 @@ function AppSidebar() {
- {/* Resource navigation + temporary Example section — all in one group */}
+ {/* Resource navigation */}
@@ -437,8 +436,6 @@ function AppSidebar() {
canSeeNavItem={canSeeNavItem}
/>
))}
- {/* Example demos — toggle SHOW_EXAMPLE_NAV in config/navigation.ts */}
- {SHOW_EXAMPLE_NAV ? : null}
@@ -469,92 +466,6 @@ function AppSidebar() {
)
}
-const SANDBOX_GEOTHERMAL_GRID = '/geothermal/wells/records-grid'
-const SANDBOX_GEOTHERMAL_INVENTORY = '/geothermal/wells/inventory'
-const SANDBOX_GEOTHERMAL_TEMP_DEPTH = '/geothermal/wells/temp-depth'
-
-function isSandboxPath(pathname: string): boolean {
- return (
- pathname.startsWith('/example') ||
- pathname.startsWith(SANDBOX_GEOTHERMAL_GRID) ||
- pathname.startsWith(SANDBOX_GEOTHERMAL_INVENTORY) ||
- pathname.startsWith(SANDBOX_GEOTHERMAL_TEMP_DEPTH)
- )
-}
-
-function ExampleNavItem() {
- const location = useLocation()
- const navigate = useNavigate()
- const [open, setOpen] = useState(isSandboxPath(location.pathname))
-
- useEffect(() => {
- if (!isSandboxPath(location.pathname)) setOpen(false)
- }, [location.pathname])
-
- const handleClick = () => {
- setOpen(true)
- navigate('/example/typography')
- }
-
- return (
-
-
-
-
-
- Sandbox
-
-
-
-
-
-
-
- Typography
-
-
-
-
- Geothermal Records
-
-
-
-
-
- Geothermal Inventory
-
-
-
-
-
-
- Geothermal Temp-Depth
-
-
-
-
-
-
-
- )
-}
-
function SupportPanelTrigger({ collapsed }: { collapsed: boolean }) {
const { isOpen, open, close } = useContext(SupportPanelContext)
return (
diff --git a/src/components/layout/sider.tsx b/src/components/layout/sider.tsx
index ce90bc38..7a00c81e 100644
--- a/src/components/layout/sider.tsx
+++ b/src/components/layout/sider.tsx
@@ -79,9 +79,7 @@ export const ThemedSiderV2: React.FC = ({
const isAdminOnly = isResourceListAdminOnly(resourceName)
const icon = deprecatedIcon ?? meta?.icon
- const derivedLabel = meta?.label || deprecatedLabel || name
- const label =
- name === 'Sandbox' || name === 'sandbox' ? 'Sandbox' : derivedLabel
+ const label = meta?.label || deprecatedLabel || name
const isSelected = key === selectedKey
const isNested = meta?.parent !== undefined
const nestedLevel = isNested ? meta?.nestedLevel || 1 : 0
diff --git a/src/config/navigation.ts b/src/config/navigation.ts
index de40b43a..43fff60e 100644
--- a/src/config/navigation.ts
+++ b/src/config/navigation.ts
@@ -55,16 +55,6 @@ export type NavItem = {
children?: NavItem[]
}
-/**
- * Show the Sandbox nav (typography specimen, WIP geothermal grids).
- * Visible in local dev and on PR preview / staging deploys
- * (VITE_APP_ENV=preview|staging), so reviewers can exercise the WIP work;
- * hidden on production.
- */
-export const SHOW_EXAMPLE_NAV =
- import.meta.env.DEV ||
- ['preview', 'staging'].includes(import.meta.env.VITE_APP_ENV)
-
/**
* Top bar: views and tools.
* Items without `roles` are visible to every authenticated user.
diff --git a/src/pages/geothermal/wells/WellPickerPage.tsx b/src/pages/geothermal/wells/WellPickerPage.tsx
index 63bd571b..2d78d0bc 100644
--- a/src/pages/geothermal/wells/WellPickerPage.tsx
+++ b/src/pages/geothermal/wells/WellPickerPage.tsx
@@ -26,7 +26,7 @@ interface WellPickerPageProps {
}
/**
- * Shared Sandbox entry point for per-well geothermal pages (records grid,
+ * Shared entry point for per-well geothermal pages (records grid,
* temp-depth log): find a well, then navigate into `targetPath/{well_data_id}`.
* Admin-gated per BDMS-878.
*
diff --git a/src/pages/geothermal/wells/records-grid-picker.tsx b/src/pages/geothermal/wells/records-grid-picker.tsx
index e33d58f6..d9536859 100644
--- a/src/pages/geothermal/wells/records-grid-picker.tsx
+++ b/src/pages/geothermal/wells/records-grid-picker.tsx
@@ -1,6 +1,6 @@
import { WellPickerPage } from './WellPickerPage'
-/** Sandbox entry point for the geothermal records grid. */
+/** Entry point for the geothermal records grid. */
export const GeoThermalRecordsGridPicker = () => (
(
,
},
},
diff --git a/src/test/utils/accessControl.test.ts b/src/test/utils/accessControl.test.ts
index d6cbd47f..ed5b751e 100644
--- a/src/test/utils/accessControl.test.ts
+++ b/src/test/utils/accessControl.test.ts
@@ -172,20 +172,6 @@ const specialResourceExpectations: Array<{
action: 'show',
expected: true,
},
- {
- name: 'AMP viewer cannot access sandbox special resource',
- groups: ['AMP.Viewer'],
- resource: 'Sandbox',
- action: 'list',
- expected: false,
- },
- {
- name: 'AMP admin can access sandbox special resource',
- groups: ['AMP.Admin'],
- resource: 'Sandbox',
- action: 'show',
- expected: true,
- },
{
name: 'AMP viewer cannot access ocotillo hydrograph correction',
groups: ['AMP.Viewer'],
@@ -440,7 +426,6 @@ describe('isResourceListAdminOnly', () => {
it('returns true for list resources restricted to admin roles', () => {
expect(isResourceListAdminOnly('ocotillo.location')).toBe(true)
expect(isResourceListAdminOnly('ocotillo.lexicon')).toBe(true)
- expect(isResourceListAdminOnly('Sandbox')).toBe(true)
expect(isResourceListAdminOnly('water.locations')).toBe(true)
})
diff --git a/src/utils/accessControl.ts b/src/utils/accessControl.ts
index 95d61d6b..b4b1b3a2 100644
--- a/src/utils/accessControl.ts
+++ b/src/utils/accessControl.ts
@@ -115,7 +115,6 @@ const resourcePolicies: Record = {
delete: adminRoles,
manage: adminRoles,
},
- Sandbox: { list: adminRoles, show: adminRoles },
geothermal: { list: geothermalViewerRoles, show: geothermalViewerRoles },
'water.locations': {
list: ['AMP.Admin', 'Geothermal.Admin'],
@@ -265,8 +264,7 @@ export const canAccessResource = ({
if (
resource === 'ocotillo.hydrograph-correction' ||
- resource === 'ocotillo.thing-well-pdf-preview' ||
- resource === 'Sandbox'
+ resource === 'ocotillo.thing-well-pdf-preview'
) {
const policy = resourcePolicies[resource]
return matchesPolicy(policy[action], capabilities.roles)