Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/scripts/generate-llms-txt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const OUTPUT_FILES = [
'llms-research-us.txt',
'llms-research-uk.txt',
];
const LLMS_GENERATION_TIMEOUT_MS = 30000;

describe('generate-llms-txt', () => {
// Store original file contents to restore after test
Expand All @@ -31,7 +32,7 @@ describe('generate-llms-txt', () => {
cwd: path.join(__dirname, '..'),
stdio: 'pipe',
});
});
}, LLMS_GENERATION_TIMEOUT_MS);

afterAll(() => {
// Restore originals
Expand Down
30 changes: 30 additions & 0 deletions app/src/components/FullScreenPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* FullScreenPortal — renders children into a fixed full-screen overlay
* via React portal. The underlying layout (sidebar, header) stays
* mounted underneath but is visually covered.
*/

import { ReactNode, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

interface FullScreenPortalProps {
children: ReactNode;
}

export default function FullScreenPortal({ children }: FullScreenPortalProps) {
const [container, setContainer] = useState<HTMLElement | null>(null);

useEffect(() => {
const el = document.getElementById('fullscreen-portal');
setContainer(el);
}, []);

if (!container) {
return null;
}

return createPortal(
<div className="tw:fixed tw:inset-0 tw:z-[100] tw:bg-white">{children}</div>,
container
);
}
3 changes: 2 additions & 1 deletion app/src/libs/calculations/CalcOrchestratorManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export class CalcOrchestratorManager {
} catch (error) {
console.error(`[CalcOrchestratorManager] Failed to start calculation ${calcId}:`, error);
this.cleanup(calcId);
throw error;
// Don't re-throw — async errors from useEffect can corrupt the Next.js
// router state. The error is logged and the orchestrator is cleaned up.
}
}

Expand Down
53 changes: 34 additions & 19 deletions app/src/libs/calculations/economy/SocietyWideCalcStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,43 @@ export class SocietyWideCalcStrategy implements CalcExecutionStrategy {
* Makes direct API call - server manages state and queuing
*/
async execute(params: CalcParams, metadata: CalcMetadata): Promise<CalcStatus> {
// Pass the region value AS-IS to the API (NO prefix stripping)
// For UK: includes prefix like "constituency/Sheffield Central" or "country/england"
// For US: just state code like "ca" or "ny"
// For National: just country code like "uk" or "us"
const apiRegion = params.region || params.countryId;
try {
// Pass the region value AS-IS to the API (NO prefix stripping)
// For UK: includes prefix like "constituency/Sheffield Central" or "country/england"
// For US: just state code like "ca" or "ny"
// For National: just country code like "uk" or "us"
const apiRegion = params.region || params.countryId;

// Build API parameters - use year from params
const apiParams: SocietyWideCalculationParams = {
region: apiRegion,
time_period: params.year,
};
// Build API parameters - use year from params
const apiParams: SocietyWideCalculationParams = {
region: apiRegion,
time_period: params.year,
};

// Call society-wide calculation API
const response = await fetchSocietyWideCalculation(
params.countryId,
params.policyIds.reform || params.policyIds.baseline,
params.policyIds.baseline,
apiParams
);
// Call society-wide calculation API
const response = await fetchSocietyWideCalculation(
params.countryId,
params.policyIds.reform || params.policyIds.baseline,
params.policyIds.baseline,
apiParams
);

// Transform to unified status with provided metadata
return this.transformResponseWithMetadata(response, metadata, params.countryId);
// Transform to unified status with provided metadata
return this.transformResponseWithMetadata(response, metadata, params.countryId);
} catch (error) {
console.error('[SocietyWideCalcStrategy.execute] Calculation failed:', error);
const errorMessage =
error instanceof Error ? error.message : 'Society-wide calculation failed';
return {
status: 'error',
error: {
code: 'SOCIETY_WIDE_CALC_FAILED',
message: errorMessage,
retryable: true,
},
metadata,
};
}
}

/**
Expand Down
10 changes: 5 additions & 5 deletions app/src/pathways/policy/PolicyPathwayWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { useCallback, useEffect, useState } from 'react';
import StandardLayout from '@/components/StandardLayout';
import FullScreenPortal from '@/components/FullScreenPortal';
import { useAppNavigate } from '@/contexts/NavigationContext';
import { useCurrentCountry } from '@/hooks/useCurrentCountry';
import { usePathwayNavigation } from '@/hooks/usePathwayNavigation';
Expand Down Expand Up @@ -114,11 +114,11 @@ export default function PolicyPathwayWrapper({ onComplete }: PolicyPathwayWrappe
currentView = <></>;
}

// Conditionally wrap with StandardLayout
// PolicyParameterSelectorView manages its own AppShell
// StandardLayout is provided by the parent layout.
// Views that manage their own AppShell render inside a portal overlay.
if (MODES_WITH_OWN_LAYOUT.has(currentMode as StandalonePolicyViewMode)) {
return currentView;
return <FullScreenPortal>{currentView}</FullScreenPortal>;
}

return <StandardLayout>{currentView}</StandardLayout>;
return currentView;
}
8 changes: 2 additions & 6 deletions app/src/pathways/population/PopulationPathwayWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import StandardLayout from '@/components/StandardLayout';
import { CURRENT_YEAR } from '@/constants';
import { useAppNavigate } from '@/contexts/NavigationContext';
import { ReportYearProvider } from '@/contexts/ReportYearContext';
Expand Down Expand Up @@ -144,9 +143,6 @@ export default function PopulationPathwayWrapper({ onComplete }: PopulationPathw
currentView = <></>;
}

return (
<ReportYearProvider year={CURRENT_YEAR}>
<StandardLayout>{currentView}</StandardLayout>
</ReportYearProvider>
);
// StandardLayout is provided by the parent layout.
return <ReportYearProvider year={CURRENT_YEAR}>{currentView}</ReportYearProvider>;
}
10 changes: 5 additions & 5 deletions app/src/pathways/simulation/SimulationPathwayWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import StandardLayout from '@/components/StandardLayout';
import FullScreenPortal from '@/components/FullScreenPortal';
import { MOCK_USER_ID } from '@/constants';
import { useAppNavigate } from '@/contexts/NavigationContext';
import { useCurrentCountry } from '@/hooks/useCurrentCountry';
Expand Down Expand Up @@ -362,11 +362,11 @@ export default function SimulationPathwayWrapper({ onComplete }: SimulationPathw
currentView = <></>;
}

// Conditionally wrap with StandardLayout
// Views in MODES_WITH_OWN_LAYOUT manage their own AppShell
// StandardLayout is provided by the parent layout.
// Views that manage their own AppShell render inside a portal overlay.
if (MODES_WITH_OWN_LAYOUT.has(currentMode as SimulationViewMode)) {
return currentView;
return <FullScreenPortal>{currentView}</FullScreenPortal>;
}

return <StandardLayout>{currentView}</StandardLayout>;
return currentView;
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,15 @@ describe('CalcOrchestratorManager', () => {
expect(manager.getDebugInfo().activeIds).toEqual(['report-1', 'report-2']);
});

it('given orchestrator start fails then cleans up and throws', async () => {
it('given orchestrator start fails then cleans up without throwing', async () => {
// Given
const config = mockSocietyWideCalcConfig();
const error = new Error(TEST_ERROR_MESSAGE);
mockOrchestrator.startCalculation.mockRejectedValue(error);

// When/Then
await expect(manager.startCalculation(config)).rejects.toThrow(TEST_ERROR_MESSAGE);
// When — should resolve without throwing (error is swallowed to
// prevent async errors from corrupting the Next.js router state)
await manager.startCalculation(config);

// Orchestrator should be cleaned up
expect(mockOrchestrator.cleanup).toHaveBeenCalled();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import PopulationPathwayWrapper from "@/pathways/population/PopulationPathwayWrapper";

export default function HouseholdCreateRoute() {
return <PopulationPathwayWrapper />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import PopulationsPage from "@/pages/Populations.page";

export default function HouseholdsRoute() {
return <PopulationsPage />;
}
22 changes: 22 additions & 0 deletions calculator-app/src/app/[countryId]/(calculator)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import StandardLayout from "@/components/StandardLayout";
import { CalculatorProviders } from "../providers";

/**
* Shared calculator shell for extracted calculator routes.
* Keeps the provider tree and layout mounted across calculator navigation
* without making them the owner of every `/:countryId/*` route.
*/
export default function CalculatorLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<CalculatorProviders>
<StandardLayout>{children}</StandardLayout>
<div id="fullscreen-portal" />
</CalculatorProviders>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import PolicyPathwayWrapper from "@/pathways/policy/PolicyPathwayWrapper";

export default function PolicyCreateRoute() {
return <PolicyPathwayWrapper />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import PoliciesPage from "@/pages/Policies.page";

export default function PoliciesRoute() {
return <PoliciesPage />;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use client";

import { use } from "react";
import StandardLayout from "@/components/StandardLayout";
import ReportOutputPage from "@/pages/ReportOutput.page";
import { CalculatorProviders } from "../../../providers";

export default function ReportOutputRoute({
params,
Expand All @@ -14,11 +12,5 @@ export default function ReportOutputRoute({
const subpage = rest?.[0];
const view = rest?.[1];

return (
<CalculatorProviders>
<StandardLayout>
<ReportOutputPage reportId={reportId} subpage={subpage} view={view} />
</StandardLayout>
</CalculatorProviders>
);
return <ReportOutputPage reportId={reportId} subpage={subpage} view={view} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";

import { use } from "react";
import ModifyReportPage from "@/pages/reportBuilder/ModifyReportPage";

export default function ModifyReportRoute({
params,
}: {
params: Promise<{ userReportId: string }>;
}) {
const { userReportId } = use(params);

return <ModifyReportPage userReportId={userReportId} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import ReportBuilderPage from "@/pages/reportBuilder/ReportBuilderPage";

export default function ReportBuilderRoute() {
return <ReportBuilderPage />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import ReportsPage from "@/pages/Reports.page";

export default function ReportsRoute() {
return <ReportsPage />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import SimulationPathwayWrapper from "@/pathways/simulation/SimulationPathwayWrapper";

export default function SimulationCreateRoute() {
return <SimulationPathwayWrapper />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import SimulationsPage from "@/pages/Simulations.page";

export default function SimulationsRoute() {
return <SimulationsPage />;
}
12 changes: 0 additions & 12 deletions calculator-app/src/app/[countryId]/households/create/page.tsx

This file was deleted.

15 changes: 0 additions & 15 deletions calculator-app/src/app/[countryId]/households/page.tsx

This file was deleted.

6 changes: 2 additions & 4 deletions calculator-app/src/app/[countryId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { countryIds, type CountryId } from "@/libs/countries";
* Layout for extracted Next.js pages under /:countryId/*.
* Provides CountryContext, NavigationContext, and LocationContext
* so shared components work identically in both router contexts.
* No-op touch to force a calculator-app redeploy.
* This comment exists to force calculator-next redeploys when needed.
*/
export default function CountryLayout({
children,
Expand Down Expand Up @@ -56,9 +56,7 @@ export default function CountryLayout({
return (
<CountryProvider value={countryId as CountryId}>
<NavigationProvider value={navValue}>
<LocationProvider value={locationValue}>
{children}
</LocationProvider>
<LocationProvider value={locationValue}>{children}</LocationProvider>
</NavigationProvider>
</CountryProvider>
);
Expand Down
12 changes: 0 additions & 12 deletions calculator-app/src/app/[countryId]/policies/create/page.tsx

This file was deleted.

Loading
Loading