diff --git a/docs/field-velocity-visibility.md b/docs/field-velocity-visibility.md new file mode 100644 index 00000000..e6458fda --- /dev/null +++ b/docs/field-velocity-visibility.md @@ -0,0 +1,51 @@ +# Field-velocity launch boundary + +Two deliberately separate changes: + +1. **Hide now:** remove the `AreaFieldVelocity` import and mount from both public focus-area page templates (the generic template and the hardcoded Economies & Governance route). No CSS-only hiding, public preview-discovery links, data loading, or chart-fragment dialogs remain on those pages. The reusable component stays staged in source for restoration. +2. **Restore later:** a separate draft PR reverses only that public-detail wiring and restores the original positive mounting tests. It does not publish `/impact/`, change navigation, or promote the cross-field preview. Merge the hiding PR first; the restore PR is stacked on its branch and must target `main` after the hiding PR merges. Do not merge the restore PR into the hiding branch. + +The cross-field page remains at `/impact-preview-eb61fba1b98e/`, with all four areas, chart deep links, source faces, inflections, methodology and toolkit. Its `noindex,nofollow` metadata and exclusions from navigation, sitemap, search, RSS and robots discovery remain unchanged. The read-only field-velocity API/data are preserved for consumers. Unlisted/noindex is not authentication or access control. + +This is distinct from the older draft PR #105, which would promote `/impact/` into navigation. That PR is not changed by either detail-page visibility PR. + +## Verification (September 10, 2026) + +Application revisions exercised: +- Hidden: `7e696f9b64aba340c4aa15b374e1c1c23d320ed5` — 79 tests passing. +- Restored: `e426c490736f055794428e39733b160c31de295b` — 75 tests passing. +- The restored `src` tree is exactly the pre-hide main/PR153 tree at `cea75883bbed07cb751a3bc17c5b677cfa587b74` (tree `61229e854adeb104781b01b0747df0027898d1c4`). No other source, styling, data, API or site changes. + +Both passed pnpm 10 frozen installs, TypeScript and production builds, plus independent isolated code reviews. Test-first regressions failed on all four public routes before hiding; the original mounting tests failed on all four hidden routes before restoring. + +Headed local production-build browser checks at 1440, 390 and 320px: +- All four public detail routes preserve Opportunity Spaces and their existing Insights/Explore content. +- Hidden state has zero field-velocity panels, tiles, preview-discovery links or old `#fv/...` chart dialogs. +- Restored state has the field panel, all five tiles, three overview/methodology/toolkit links and working old chart fragments. +- All four selected cross-field overview tabs retain five tiles and methodology. A real chart URL per field opens its modal; the data/source flip and Escape dismissal work. +- 24 route/viewport cases per state, 48 across both. Screenshots below are local captures of these real source revisions, not claims about production deployment. + +36 HTTP checks across both local production builds cover home, area index, all four detail routes, the exact preview, sitemap, robots, search, RSS, each field API, malformed API area, and missing `/impact/` and `/field-velocity/` routes. + +## Existing limitations, not changed by these PRs + +- Both states produce exactly the same measured horizontal overflow: 8px on the unchanged cross-field preview at 1440/390px; 15px at a 320px viewport with a native scrollbar (the site's existing 320px body minimum). Public detail pages have no horizontal overflow at 1440/390px. These visibility changes do not attempt a preview layout redesign. +- Builds report existing provider fallback/cache warnings (including Ma Earth snapshot fallback and indexer GraphQL errors). No new provider integration or data freshness is claimed. +- The production URL was inspected through HTTP extraction. The authenticated Chrome policy blocked direct `www.plrd.org` navigation; visual evidence uses local production builds, with hosted PR deployment verification reported separately in the PR descriptions. + +## Screenshots + +### Hidden public detail: desktop +![Hidden public detail, desktop](screenshots/field-velocity-visibility/detail-hidden-desktop.png) + +### Hidden public detail: mobile +![Hidden public detail, mobile](screenshots/field-velocity-visibility/detail-hidden-mobile.png) + +### Exact pre-hide detail behavior restored: desktop +![Restored public detail, desktop](screenshots/field-velocity-visibility/detail-restored-desktop.png) + +### Exact pre-hide detail behavior restored: mobile +![Restored public detail, mobile](screenshots/field-velocity-visibility/detail-restored-mobile.png) + +### Cross-field preview retained +![Retained cross-field preview](screenshots/field-velocity-visibility/cross-field-retained-desktop.png) diff --git a/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png b/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png new file mode 100644 index 00000000..4563291d Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png differ diff --git a/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png b/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png new file mode 100644 index 00000000..7164944d Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png differ diff --git a/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png b/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png new file mode 100644 index 00000000..3940dc84 Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png differ diff --git a/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png b/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png new file mode 100644 index 00000000..9e1c8a0c Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png differ diff --git a/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png b/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png new file mode 100644 index 00000000..8fbdc7a8 Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png differ diff --git a/scripts/velocity/field-velocity-panel.test.mjs b/scripts/velocity/field-velocity-panel.test.mjs index 62fc8732..7909f921 100644 --- a/scripts/velocity/field-velocity-panel.test.mjs +++ b/scripts/velocity/field-velocity-panel.test.mjs @@ -45,19 +45,14 @@ function elements(node) { } for (const { key, label } of FOCUS_AREAS) { - test(`${key} overview includes the shared panel, original strategy and selected-area methodology/toolkit links`, async (t) => { + test(`${key} staged panel retains charts and selected-area methodology/toolkit links without public route wiring`, async (t) => { assert.ok(existsSync('src/components/AreaFieldVelocity.tsx'), 'missing reusable area panel') t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 })) t.mock.method(console, 'warn', () => {}) const Panel = source('components/AreaFieldVelocity.tsx').default - const Page = source(key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx').default - const tree = await Page({ params: Promise.resolve({ slug: key }) }) - const children = elements(tree) - const panel = children.find(el => el.type === Panel) - assert.ok(panel, `${key} page does not mount shared panel`) - assert.equal(panel.props.area, key) - assert.ok(children.some(el => el.props.id === 'opportunity-spaces'), 'existing strategy preserved') - const panelTree = await Panel(panel.props) + // The component is retained for a later launch, but no public page mounts it. + // Public-route absence is covered separately by preview-only.test.mjs. + const panelTree = await Panel({ area: key }) if (key === 'economies-governance') { assert.deepEqual(elements(panelTree).find(el => el.type === Dashboard).props.liveOutputs, {}, 'area panel must retain the overview live-output channel even when providers are unavailable') } @@ -69,7 +64,7 @@ for (const { key, label } of FOCUS_AREAS) { assert.match(html, /id="field-velocity"/) assert.ok(html.includes(`${label} field velocity`)) - assert.ok(tree.props.className.includes('area-overview')) + assert.ok(panelTree.props.className.includes('area-field-velocity')) assert.ok(panelTree.props.className.includes(key === 'neurotech' ? 'bg-gray-100' : 'bg-gray-200')) assert.doesNotMatch(panelTree.props.className, /border-y|px-4|bg-gray-50/) diff --git a/scripts/velocity/preview-only.test.mjs b/scripts/velocity/preview-only.test.mjs new file mode 100644 index 00000000..731591cc --- /dev/null +++ b/scripts/velocity/preview-only.test.mjs @@ -0,0 +1,64 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import React from 'react' +import { readFileSync } from 'node:fs' +import { source } from './test-source-loader.mjs' + +const Panel = source('components/AreaFieldVelocity.tsx').default +const Dashboard = source('components/ImpactDashboardV2.tsx').default +const Methodology = source('components/MeasuringQuestionsV2.tsx').default +const { FOCUS_AREAS, FIELD_VELOCITY_OVERVIEW } = source('lib/field-velocity.ts') + +function elements(node) { + if (!React.isValidElement(node)) return [] + return [node, ...React.Children.toArray(node.props.children).flatMap(elements)] +} + +function offlineProviders(t) { + t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 })) + t.mock.method(console, 'warn', () => {}) +} + +for (const { key } of FOCUS_AREAS) { + test(`${key} public detail keeps existing content but does not mount field velocity or preview links`, async (t) => { + offlineProviders(t) + const route = key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx' + const Page = source(route).default + const nodes = elements(await Page({ params: Promise.resolve({ slug: key }) })) + assert.ok(!nodes.some(node => node.type === Panel || node.type === Dashboard || node.type === Methodology), 'unreleased field velocity must not mount on public pages') + assert.ok(!nodes.some(node => node.props.id === 'field-velocity')) + assert.ok(!nodes.some(node => /impact-preview|#fv\/|#field-velocity|#methodology|#toolkit/.test(node.props.href ?? '')), 'no discovery links to the unlisted preview') + assert.ok(nodes.some(node => node.props.id === 'opportunity-spaces'), 'original strategy remains') + const existingLink = key === 'economies-governance' ? '/areas/economies-governance/projects/' : '/insights/' + assert.ok(nodes.some(node => node.props.href === existingLink), 'original Explore/Insights content remains') + assert.doesNotMatch(readFileSync(`src/${route}`, 'utf8'), /from ['"]@\/(?:components\/(?:AreaFieldVelocity|ImpactDashboardV2|MeasuringQuestionsV2)|lib\/field-velocity[^'"]*)['"]/, 'public routes must not load field-velocity modules') + }) +} + +test('the exact unlisted overview retains all fields, charts, methodology and noindex metadata', async (t) => { + offlineProviders(t) + assert.equal(FIELD_VELOCITY_OVERVIEW, '/impact-preview-eb61fba1b98e/') + const { default: Overview, metadata } = source(`app${FIELD_VELOCITY_OVERVIEW}page.tsx`) + assert.deepEqual(metadata.robots, { index: false, follow: false, googleBot: { index: false, follow: false } }) + for (const { key } of FOCUS_AREAS) { + const nodes = elements(await Overview({ searchParams: Promise.resolve({ area: key }) })) + const dashboard = nodes.find(node => node.type === Dashboard) + assert.ok(dashboard) + assert.equal(dashboard.props.initialArea, key) + assert.equal(dashboard.props.fixedArea, undefined) + assert.deepEqual(Object.keys(dashboard.props.recordsByArea).sort(), FOCUS_AREAS.map(area => area.key).sort()) + assert.ok(nodes.some(node => node.type === Methodology)) + assert.ok(nodes.some(node => node.props.id === 'methodology')) + } +}) + +test('the preview stays absent from public navigation, sitemap, search, RSS and robots discovery', () => { + const sitemap = source('app/sitemap.ts').default() + const { mainNav, footerNav } = source('lib/site-config.ts') + for (const [label, value] of Object.entries({ sitemap, mainNav, footerNav })) { + assert.doesNotMatch(JSON.stringify(value), /impact-preview|\/field-velocity\/|"[^" ]*\/impact\/"/, label) + } + for (const file of ['public/search-index.json', 'public/feed.xml', 'public/robots.txt']) { + assert.doesNotMatch(readFileSync(file, 'utf8'), /impact-preview-eb61fba1b98e|\/field-velocity\//, file) + } +}) diff --git a/src/app/areas/[slug]/page.tsx b/src/app/areas/[slug]/page.tsx index b119b966..a581a5f7 100644 --- a/src/app/areas/[slug]/page.tsx +++ b/src/app/areas/[slug]/page.tsx @@ -1,6 +1,4 @@ import type { Metadata } from 'next' -import AreaFieldVelocity from '@/components/AreaFieldVelocity' -import { isFocusAreaKey } from '@/lib/field-velocity-data' import EditPageButton from '@/components/EditPageButton' import { PageEditHistoryByline } from '@/components/EditHistoryByline' @@ -289,7 +287,7 @@ export default async function AreaPage({ params }: Props) { )} - {isFocusAreaKey(slug) && } + {/* Field velocity is preview-only until a separate public launch. */} {/* Insights — latest posts, publications, and talks for this focus area */} {areaInsights.length > 0 && ( diff --git a/src/app/areas/economies-governance/page.tsx b/src/app/areas/economies-governance/page.tsx index 0fb3729f..854bb5d5 100644 --- a/src/app/areas/economies-governance/page.tsx +++ b/src/app/areas/economies-governance/page.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import type { Metadata } from 'next' -import AreaFieldVelocity from '@/components/AreaFieldVelocity' + import EditPageButton from '@/components/EditPageButton' import { PageEditHistoryByline } from '@/components/EditHistoryByline' import AuthorCard from '@/components/AuthorCard' @@ -255,7 +255,7 @@ export default async function FA2MainPage() { - + {/* Field velocity is preview-only until a separate public launch. */} {/* Explore */}

Explore