diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 39c905341..391152009 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,14 +41,17 @@ jobs: ./clever-tools-latest_linux/clever link $CLEVER_APP_ID ./clever-tools-latest_linux/clever deploy -f - - name: Deploy frontend to Clever Cloud (PROD) + # The frontend used to be a Next.js Node.js app on Clever; it's now a + # Vite-built SPA served by Clever's static (Caddy) engine. The deploy + # command is identical (git push), only the target app changes — + # `FE_CLEVER_APP_ID_PROD` must be pointed at the new static app and + # `FE_CLEVER_APP_ALIAS_PROD` set to that app's alias (no underscores; + # see https://github.com/CleverCloud/clever-tools for the alias-escape + # workaround required by `clever deploy -a `). + - name: Deploy frontend (static SPA) to Clever Cloud (PROD) env: CLEVER_APP_ID: ${{ secrets.FE_CLEVER_APP_ID_PROD }} - APP_NAME: cc_dashboard_prod + CLEVER_APP_ALIAS: ${{ secrets.FE_CLEVER_APP_ALIAS_PROD }} run: | - echo $CLEVER_APP_ID - echo $APP_NAME ./clever-tools-latest_linux/clever link $CLEVER_APP_ID - # As the clever tools CLI aliasing is escaping _ character, a temporary hard-coded value is needed - # waiting for a fix from Clever - ./clever-tools-latest_linux/clever deploy -f -a ccdashboardprod --quiet + ./clever-tools-latest_linux/clever deploy -f -a $CLEVER_APP_ALIAS --quiet diff --git a/webapp/.env.development b/webapp/.env.development new file mode 100644 index 000000000..6c316dc40 --- /dev/null +++ b/webapp/.env.development @@ -0,0 +1,15 @@ +# Local dev defaults. Loaded automatically by Vite on `npm run dev`. +# Override per-developer with `.env.development.local` (gitignored). + +# Empty VITE_API_URL → the mock fetch interceptor catches every +# same-origin /api request and serves from src/api/mock/data.ts. +# Set this to a real API base (e.g. http://localhost:8008) and unset +# VITE_USE_MOCK_DATA to talk to a local carbonserver instead. +VITE_API_URL= +VITE_BASE_URL=http://localhost:3000 + +VITE_USE_MOCK_DATA=true + +# Used by share-project-button to produce shareable public links. +# Any 32-byte (256-bit) hex string works for dev. +VITE_PROJECT_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef diff --git a/webapp/.env.example b/webapp/.env.example new file mode 100644 index 000000000..a58959bd0 --- /dev/null +++ b/webapp/.env.example @@ -0,0 +1,24 @@ +# Example webapp env. Copy to `.env.development.local` (gitignored) to +# override local dev defaults from `.env.development`, or to +# `.env.production.local` for production builds. + +# Base URL of the carbonserver API. Leave empty in mock mode. +VITE_API_URL=http://localhost:8008 + +# Public origin of this webapp. Used to build OAuth redirect URLs and +# share links. +VITE_BASE_URL=http://localhost:3000 + +# Toggle the in-browser mock service (src/api/mock/). When "true", every +# same-origin fetch is resolved from synthetic data and no carbonserver +# is required. +VITE_USE_MOCK_DATA=false + +# AES key used to encrypt project ids in shareable public links. Must be +# 64 hex chars (256 bits). Keep stable across the fleet — rotating it +# invalidates existing links. +VITE_PROJECT_ENCRYPTION_KEY= + +# Fief profile base URL — surfaces a "Profile" item in the navbar that +# links to the IdP's account page. Leave empty to hide it. +VITE_FIEF_BASE_URL= diff --git a/webapp/e2e/landing.spec.ts b/webapp/e2e/landing.spec.ts index 273db3b2a..6947dee84 100644 --- a/webapp/e2e/landing.spec.ts +++ b/webapp/e2e/landing.spec.ts @@ -11,7 +11,7 @@ test.describe("Landing page (mock mode)", () => { ).toBeVisible(); // In mock mode the real-login button is hidden — there is no real - // OAuth backend in this build. + // OAuth backend in this build. Only the mock button is rendered. await expect(page.getByTestId("real-login")).toHaveCount(0); await expect(page.getByTestId("mock-login")).toBeVisible(); }); diff --git a/webapp/src/api/experiments.ts b/webapp/src/api/experiments.ts index f00cb8e14..206d45696 100644 --- a/webapp/src/api/experiments.ts +++ b/webapp/src/api/experiments.ts @@ -19,17 +19,10 @@ export async function createExperiment( export async function getExperiments(projectId: string): Promise { try { - const result = await fetchApi( + return await fetchApi( `/projects/${projectId}/experiments`, ExperimentSchema.array(), ); - // Drop experiments that somehow lack a usable id — they cannot be - // selected, fetched, or rendered downstream. Keeping them would - // surface as unselectable rows whose click silently clears the - // selection. - return result.filter( - (e) => typeof e.id === "string" && e.id.length > 0, - ); } catch (error) { console.error("[getExperiments] failed", error); return []; @@ -54,12 +47,7 @@ export async function getProjectEmissionsByExperiment( } try { - const result = await fetchApi(url, ExperimentReportSchema.array()); - return result.filter( - (r) => - typeof r.experiment_id === "string" && - r.experiment_id.length > 0, - ); + return await fetchApi(url, ExperimentReportSchema.array()); } catch (error) { console.error("[getProjectEmissionsByExperiment] failed", error); return []; diff --git a/webapp/src/api/mock/handlers.ts b/webapp/src/api/mock/handlers.ts index 64b059016..f2d49b18f 100644 --- a/webapp/src/api/mock/handlers.ts +++ b/webapp/src/api/mock/handlers.ts @@ -101,16 +101,6 @@ const handlers: Handler[] = [ } if (method === "DELETE") return noContent(); } - const publicMatch = pathname.match(/^\/projects\/public\/([^/]+)$/); - if (method === "GET" && publicMatch) { - // Treat the encrypted_id as the project id for mock purposes. - const project = MOCK.project.byId[publicMatch[1]]; - return project ? ok(project) : notFound(); - } - const shareLink = pathname.match(/^\/projects\/([^/]+)\/share-link$/); - if (method === "GET" && shareLink) { - return ok({ encrypted_id: shareLink[1] }); - } return undefined; }, diff --git a/webapp/src/api/mock/index.ts b/webapp/src/api/mock/index.ts index c04f6f09e..94bfbd9cb 100644 --- a/webapp/src/api/mock/index.ts +++ b/webapp/src/api/mock/index.ts @@ -14,7 +14,7 @@ export function installMockFetch(): void { const apiBase = (import.meta.env.VITE_API_URL ?? "").replace(/\/$/, ""); const apiPathPrefix = apiBase - ? new URL(apiBase).pathname.replace(/\/$/, "") + ? (safeUrl(apiBase)?.pathname.replace(/\/$/, "") ?? "") : ""; const realFetch = window.fetch.bind(window); @@ -29,16 +29,26 @@ export function installMockFetch(): void { ? input.toString() : input.url; - const isApiCall = apiBase ? rawUrl.startsWith(apiBase) : false; + // With an explicit VITE_API_URL we only intercept requests aimed at + // it. With no VITE_API_URL (the default dev/mock setup), every + // same-origin fetch is treated as an API call — Vite's static + // assets are loaded via