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)