diff --git a/.env.example b/.env.example deleted file mode 100644 index 8c625a7..0000000 --- a/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -# civfix-admin (operator dashboard, admin.civfix.org) environment. -# -# NEXT_PUBLIC_* values are INLINED into the static export at BUILD time (not read at runtime). Set -# them before `pnpm build` (or `pnpm -C apps/admin build`). In CI they come from repo Variables -# (vars.*); see .github/workflows/deploy.yml. - -# Base URL of the civfix API. The dashboard is served at admin.civfix.org and calls this API for all -# /admin/* data and auth. The API's WEB_ORIGINS must include https://admin.civfix.org so CORS and the -# session cookie work. Defaults to the local backend when unset. -# Example (production): https://api.civfix.example -NEXT_PUBLIC_API_URL=http://localhost:8080 - -# Publishable CARTO basemap api key, appended to the Leaflet raster tile URLs. Optional: unset, the maps -# still render the same CARTO tiles, just with CARTO's watermark. Public by nature (it is inlined into -# the client bundle). -# NEXT_PUBLIC_CARTO_API_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f2635..5ed42b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,10 @@ name: CI -# main is the only long-lived branch and the only PR target (issue civfix/issue-tracker#108): main is -# the staging lane, and production ships from a published v* release. +# main is the only long-lived branch (issue civfix/issue-tracker#108): main is the staging lane, and +# production ships from a published v* release. No base filter: a stacked PR targets its parent slice's +# branch and still needs CI before it retargets onto main. on: pull_request: - branches: [main] # Cancel superseded runs on the same ref. concurrency: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e0322c5..4de4087 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,8 @@ name: Deploy (Cloudflare Pages) -# Builds the civfix-admin static export and publishes it to its Cloudflare Pages project on every -# push to main, plus on-demand via the Actions "Run workflow" button. One app ships from this repo: -# - admin -> Pages project "civfix-admin" (apps/admin/out, repo-root wrangler.jsonc) -> admin.civfix.org +# Builds the civfix-admin static export and publishes it to its Cloudflare Pages project "civfix-admin" +# (apps/admin/out, repo-root wrangler.jsonc). A push to main (or a manual run on main) deploys STAGING, +# admin.civfix.dev; a published v* release deploys PRODUCTION, admin.civfix.org (ref -> environment below). # # Required GitHub configuration (Settings -> Secrets and variables -> Actions): # @@ -11,21 +11,21 @@ name: Deploy (Cloudflare Pages) # CLOUDFLARE_ACCOUNT_ID The Cloudflare account id that owns the Pages project. # # Variables (vars.*), inlined into the static export at BUILD time (NEXT_PUBLIC_*): -# NEXT_PUBLIC_API_URL Base URL of the civfix API (e.g. https://api.civfix.example). The admin -# dashboard is served at admin.civfix.org and talks to this API. The API's -# WEB_ORIGINS must include https://admin.civfix.org for CORS + cookies. +# NEXT_PUBLIC_API_URL Leave unset: both environments call the API same-origin (see below). +# Setting it points the build at a cross-origin API instead. +# NEXT_PUBLIC_CARTO_API_KEY Optional publishable CARTO key for the basemap tiles. # # The project name + prebuilt output directory come from wrangler.jsonc, so the deploy step is just # `pages deploy` (no args). The project is created automatically by the first run. We use Direct Upload # (build here, ship the prebuilt out/) rather than Cloudflare's native Git build; @civfix/shared installs -# from the public-read private registry via the committed .npmrc, so no submodule checkout is needed. +# from the public-read private registry via the committed .npmrc. # SPA deep links and security/cache headers are handled by the app's public/{_redirects,_headers} # (copied into out/). # Ref -> environment (issue https://github.com/civfix/issue-tracker/issues/108): # push to main -> STAGING --branch=staging -> staging.civfix-admin.pages.dev -> admin.civfix.dev # (via the civfix-infra staging Caddy, which reverse-proxies the SPA to that -# alias — env/staging.sh ADMIN_SPA_UPSTREAM) +# alias: env/staging.sh ADMIN_SPA_UPSTREAM) # published v* tag -> PRODUCTION --branch=main -> civfix-admin.pages.dev -> admin.civfix.org # # There is no `dev` branch: feature branches PR into main, main IS the staging lane, and cutting a @@ -38,7 +38,7 @@ on: branches: [main] release: # `released`, NOT `published`: `published` also fires for a PRERELEASE, and a prerelease must never - # reach admin.civfix.org — the operator plane. The regex gate below is the second guard. + # reach admin.civfix.org, the operator plane. The regex gate below is the second guard. types: [released] workflow_dispatch: @@ -117,7 +117,7 @@ jobs: # hash that already includes the NEXT_PUBLIC_* vars, so a changed API URL or source file rebuilds. # The key is scoped by DEPLOY_ENV anyway: staging and production build the same commit, and # `.next/cache` is restored underneath Turbo, so the two must never share one. (Admin happens to - # build identically in both environments today — this keeps that from becoming load-bearing.) + # build identically in both environments today; this keeps that from becoming load-bearing.) - name: Restore Turbo + Next.js build cache uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index 2486917..26786cb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,10 @@ dist/ npm-debug.log* pnpm-debug.log* -# Env files (never commit secrets) +# Env files (never commit secrets); only the documented example is tracked .env -.env.local -.env.*.local +.env.* +!.env.example # Editor / OS .DS_Store diff --git a/README.md b/README.md index deb7d8a..d815528 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Workspace for the civfix ADMIN / OPERATOR dashboard, served at `admin.civfix.org`. A pnpm + Turbo monorepo. This is the internal tool operators use to triage reports, route jurisdiction contacts, -moderate content, run mail outreach, provision government accounts, and read analytics. It is a -sibling of `civfix-shared`, `civfix-backend`, `civfix-web`, and `civfix-mobile`. +moderate content, run mail outreach, provision government accounts, and read analytics. Its siblings +are `civfix-app` (community web + mobile, and the `@civfix/shared` contract), `civfix-backend` (the +API it calls) and `civfix-infra` (the edge that serves it). ## Layout @@ -18,9 +19,9 @@ civfix-admin/ `@civfix/shared` (the shared contract package) is installed from the private Verdaccio registry at `https://repo.civfix.org`. The repo-root `.npmrc` scopes `@civfix` to it and the app depends on a -published version (`^0.1.0` today); the registry allows anonymous read, so no credentials are needed. -After cloning, `pnpm install` fetches it — there is no submodule to initialize. The published tarball -already contains the built `dist`, so no local build of the contract is required. +published version by caret range (see `apps/admin/package.json`); the registry allows anonymous read, so +no credentials are needed. After cloning, `pnpm install` fetches it. The published tarball already +contains the built `dist`, so no local build of the contract is required. ## Commands (from this root) @@ -29,17 +30,21 @@ pnpm install # fetches @civfix/shared from repo.civfix.org pnpm build # admin (next build, static export -> apps/admin/out) pnpm typecheck pnpm lint +pnpm test # vitest unit tests pnpm dev # runs the dashboard dev server (admin) ``` ## Auth + API -The dashboard talks to the civfix API at `NEXT_PUBLIC_API_URL` (inlined at build time; see -`.env.example`). Operator sign-in reuses the civfix Email-OTP flow through the allowlist-gated -`/admin/auth/*` routes; only emails in the backend `ADMIN_EMAILS` allowlist can receive a code. The -shell renders only after an authenticated `operator` session exists; otherwise the login gate shows. -Cookies + CSRF + `x-client: web` are sent exactly as the public web app does. The API's -`WEB_ORIGINS` must include `https://admin.civfix.org`. +In production the dashboard calls the API same-origin: `admin.civfix.org` sits behind Cloudflare Access, +and the edge serves the SPA and proxies `/v1/admin/*` to the backend, so `NEXT_PUBLIC_API_URL` stays +unset. A development build defaults to `http://localhost:8080`; see `apps/admin/.env.example`. + +Operator sign-in is the Cloudflare Access exchange: the dashboard first reuses an existing operator +session, and otherwise posts to the admin Access-exchange route, which trades the Access identity the +edge attached for an `operator` session. An Access identity whose email is not on the operator +allowlist gets the "forbidden" screen. The shell renders only after an operator session exists. +Cookies, CSRF and `x-client: web` are sent exactly as the public web app sends them. ## Deploy @@ -47,8 +52,8 @@ Cloudflare Pages (Direct Upload). GitHub Actions builds the static export (a pla `pnpm install`, which fetches `@civfix/shared` from `repo.civfix.org`; pinned Node 22 + pnpm 9.12.0) and ships the prebuilt `apps/admin/out` via `wrangler pages deploy`. The Pages project name and output dir come from the repo-root `wrangler.jsonc` -(`civfix-admin` -> `apps/admin/out`). See `.github/workflows/deploy.yml` and -`../documents/phase2/07-civfix-admin-repo.md`. +(`civfix-admin` -> `apps/admin/out`). A push to `main` deploys staging (`admin.civfix.dev`); a published +`v*` release deploys production (`admin.civfix.org`). See `.github/workflows/deploy.yml`. ## License @@ -58,6 +63,6 @@ covered by the declaration in [REUSE.toml](REUSE.toml); there are no per-file license headers. The dashboard honors the AGPL's source offer with the "Source code" link on its home screen and sign-in screen, which points at the deployed commit. Contributions are accepted under the -[Contributor License Agreement](CLA.md) — see +[Contributor License Agreement](CLA.md); see [CONTRIBUTING.md](CONTRIBUTING.md). civfix is a project of Reach Out Los Angeles Inc.; the civfix name and logos are its trademarks and are not covered by the license. diff --git a/apps/admin/.env.example b/apps/admin/.env.example index 2ac3ae0..25b2706 100644 --- a/apps/admin/.env.example +++ b/apps/admin/.env.example @@ -1,14 +1,12 @@ # civfix admin / operator dashboard environment (admin.civfix.org). # -# NEXT_PUBLIC_* values are INLINED into the static export at BUILD time (not read at runtime). Set -# them before `pnpm -C apps/admin build` (or `pnpm build` from the repo root). In CI they come from -# repo Variables (vars.*); see .github/workflows/deploy.yml. +# NEXT_PUBLIC_* values are INLINED into the static export at BUILD time, not read at runtime. Set them +# before `pnpm build`. In CI they come from repo Variables (vars.*); see .github/workflows/deploy.yml. -# Base URL of the civfix API. The dashboard is served at admin.civfix.org and calls this API for all -# /admin/* data and auth; the API's WEB_ORIGINS must include https://admin.civfix.org. Defaults to the -# local backend when unset. -# Example (production): https://api.civfix.example -NEXT_PUBLIC_API_URL=http://localhost:8080 +# Base URL of the civfix API. Leave it unset for staging and production: there the dashboard calls the +# API same-origin, because the edge serves the SPA and proxies /v1/admin/* to the backend behind the +# same Cloudflare Access app. When unset, a development build (`pnpm dev`) calls http://localhost:8080. +# NEXT_PUBLIC_API_URL=http://localhost:8080 # Publishable CARTO basemap api key, appended to the Leaflet raster tile URLs. Optional: unset, the maps # still render the same CARTO tiles, just with CARTO's watermark. Public by nature (it is inlined into diff --git a/apps/admin/next.config.mjs b/apps/admin/next.config.mjs index fc01b45..fdfcb68 100644 --- a/apps/admin/next.config.mjs +++ b/apps/admin/next.config.mjs @@ -34,7 +34,6 @@ const nextConfig = { env: { NEXT_PUBLIC_COMMIT_SHA: resolveCommitSha(), }, - // The @civfix/shared package ships ESM + CJS from the workspace; let Next transpile it. transpilePackages: ["@civfix/shared"], } diff --git a/apps/admin/package.json b/apps/admin/package.json index 27ffa7c..f250a4b 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -8,7 +8,6 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", "lint": "next lint --max-warnings=0", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests", @@ -17,13 +16,10 @@ "dependencies": { "@civfix/shared": "^0.57.0", "@tanstack/react-query": "^5.62.7", - "clsx": "^2.1.1", "leaflet": "^1.9.4", - "lucide-react": "^0.469.0", "next": "15.5.25", "react": "19.0.0", "react-dom": "19.0.0", - "tailwind-merge": "^2.6.0", "zustand": "^5.0.2" }, "devDependencies": { @@ -37,7 +33,6 @@ "postcss": "^8.5.28", "rimraf": "^6.0.1", "tailwindcss": "^3.4.17", - "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.2", "vitest": "^3.2.7" } diff --git a/apps/admin/public/ds/pin-cleanup.svg b/apps/admin/public/ds/pin-cleanup.svg deleted file mode 100644 index 1b519be..0000000 --- a/apps/admin/public/ds/pin-cleanup.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/admin/src/app/globals.css b/apps/admin/src/app/globals.css index d973710..f76587a 100644 --- a/apps/admin/src/app/globals.css +++ b/apps/admin/src/app/globals.css @@ -5,14 +5,13 @@ * 1. Leaflet's stylesheet (the dashboard maps use Leaflet + CARTO raster tiles, matching the * design prototype exactly - a deliberate design-fidelity choice for this internal tool; it does * NOT use MapLibre like community-web). - * 2. Tailwind's base/components/utilities. Tailwind here provides only incidental LAYOUT utilities; - * the dashboard's full visual system is the ported design CSS imported in the root layout AFTER - * this file (src/styles/admin.css, which @imports src/styles/colors-and-type.css). Because the - * design CSS is imported after globals.css, its component classes win over Tailwind's base reset - * wherever they overlap on the shell. + * 2. Tailwind, which contributes only its base reset; the markup uses no intended Tailwind + * utilities. The dashboard's visual system is the ported design CSS imported in the root layout + * AFTER this file (src/styles/admin.css, which @imports src/styles/colors-and-type.css), so its + * component classes win over the reset wherever they overlap. * * Fonts: next/font assigns --font-display-next / --font-body-next / --font-mono-next on - * (layout.tsx) — exactly the names the ported design stylesheet (colors-and-type.css) aliases into the + * (layout.tsx), exactly the names the ported design stylesheet (colors-and-type.css) aliases into the * final --font-display / --font-body / --font-mono stacks. No bridge is needed (and a * `--font-*-next: var(--font-*)` bridge would be CIRCULAR, invalidating font-family -> serif fallback). */ @@ -43,27 +42,6 @@ } } -@layer utilities { - /* Hide scrollbars on scroll areas while keeping them scrollable. */ - .no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; - } - .no-scrollbar::-webkit-scrollbar { - display: none; - } - - /* Small inline spinner for loading buttons / hooks (lucide Loader2 + this class). */ - .cf-spin { - animation: cf-spin 0.8s linear infinite; - } - @keyframes cf-spin { - to { - transform: rotate(360deg); - } - } -} - /* Leaflet canvas should inherit the body font so attributions match the design. */ .leaflet-container { font-family: var(--font-body); diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx index 1736904..156d708 100644 --- a/apps/admin/src/app/layout.tsx +++ b/apps/admin/src/app/layout.tsx @@ -45,6 +45,7 @@ export const metadata: Metadata = { title: "civfix Operations", description: "civfix admin and operator dashboard: discovery, reports, events, mail, users, and analytics.", applicationName: "civfix Operations", + icons: { icon: "/favicon.svg" }, robots: { index: false, follow: false }, } diff --git a/apps/admin/tailwind.config.ts b/apps/admin/tailwind.config.ts index 7f5e795..e74d4bd 100644 --- a/apps/admin/tailwind.config.ts +++ b/apps/admin/tailwind.config.ts @@ -2,16 +2,10 @@ import type { Config } from "tailwindcss" import { tokens } from "@civfix/shared/tokens" /** - * Tailwind theme derived entirely from the shared design tokens. - * - * The admin dashboard's *visual* source of truth is the ported design stylesheet - * (src/styles/admin.css + src/styles/colors-and-type.css), which owns the warm-paper CSS custom - * properties (--bloom, --ink, --paper, --cat-*, ...) and every component class (.card, .stile, - * .qrow, .btn, ...). Tailwind here is available for incidental LAYOUT utilities only; it never needs - * to redefine those component classes. To keep the civfix rule "never hardcode a hex the design - * system covers", this config maps every shared token scale into Tailwind exactly like community-web, - * so any utility we do reach for (bg-paper, text-ink3, rounded-lg, shadow-s2, font-display, ...) draws - * from the same single source of truth the design CSS does. + * Tailwind supplies only its base reset (preflight) here: the markup uses no intended Tailwind utilities, + * because the ported design stylesheet (src/styles/admin.css + colors-and-type.css) owns every component class + * and CSS variable. The theme still maps the shared design tokens so any utility added later (bg-paper, + * text-ink-3, rounded-lg, shadow-s2, font-display, ...) draws from the same source as community-web. */ const { color, font, fontSize, radius, shadow, space } = tokens @@ -22,7 +16,7 @@ const px = (n: number): string => `${n}px` const config: Config = { // Class-based dark mode is left available but the warm palette is light-first. darkMode: ["class"], - content: ["./src/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], + content: ["./src/**/*.{ts,tsx}"], theme: { container: { center: true, @@ -126,7 +120,7 @@ const config: Config = { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [], } export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c961fb..032f2ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,15 +30,9 @@ importers: '@tanstack/react-query': specifier: ^5.62.7 version: 5.101.0(react@19.0.0) - clsx: - specifier: ^2.1.1 - version: 2.1.1 leaflet: specifier: ^1.9.4 version: 1.9.4 - lucide-react: - specifier: ^0.469.0 - version: 0.469.0(react@19.0.0) next: specifier: 15.5.25 version: 15.5.25(@types/node@22.19.20)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -48,9 +42,6 @@ importers: react-dom: specifier: 19.0.0 version: 19.0.0(react@19.0.0) - tailwind-merge: - specifier: ^2.6.0 - version: 2.6.1 zustand: specifier: ^5.0.2 version: 5.0.14(@types/react@19.2.17)(react@19.0.0) @@ -85,9 +76,6 @@ importers: tailwindcss: specifier: ^3.4.17 version: 3.4.19 - tailwindcss-animate: - specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19) typescript: specifier: ^5.7.2 version: 5.9.3 @@ -1148,10 +1136,6 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1774,11 +1758,6 @@ packages: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} - lucide-react@0.469.0: - resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2240,14 +2219,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwind-merge@2.6.1: - resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} - - tailwindcss-animate@1.0.7: - resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} - peerDependencies: - tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} @@ -3373,8 +3344,6 @@ snapshots: client-only@0.0.1: {} - clsx@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4155,10 +4124,6 @@ snapshots: lru-cache@11.5.1: {} - lucide-react@0.469.0(react@19.0.0): - dependencies: - react: 19.0.0 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4694,12 +4659,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwind-merge@2.6.1: {} - - tailwindcss-animate@1.0.7(tailwindcss@3.4.19): - dependencies: - tailwindcss: 3.4.19 - tailwindcss@3.4.19: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/wrangler.jsonc b/wrangler.jsonc index b1a6a20..f53b2a5 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,21 +1,8 @@ { - // Cloudflare Pages project configuration for civfix-admin (the operator dashboard, admin.civfix.org). - // - // DEPLOY MODEL: CI build + Direct Upload (NOT Cloudflare's native Git build). - // The @civfix/shared contract is a PRIVATE git submodule with a RELATIVE URL - // (shared/ -> ../civfix-shared). Cloudflare's native Git builds cannot reliably clone - // private/relative submodules, so we DO NOT connect this repo to Pages' git builder. - // Instead, GitHub Actions builds the static export (checking out the submodule with - // credentials, pinned Node 22 + pnpm 9.12.0) and ships the prebuilt out/ to Pages with - // `wrangler pages deploy`. See .github/workflows/deploy.yml and - // documents/phase2/07-civfix-admin-repo.md. - // - // WHAT THIS FILE DOES: makes the deploy declarative. With it present, `wrangler pages deploy` - // (no args) reads the project name and the prebuilt output directory from here, and - // `wrangler pages dev` serves that directory through the real Pages routing engine (so the - // _redirects / _headers behave exactly as in production). It does NOT configure a build - // command or submodule handling - Cloudflare Pages has no repo-level build-command file; - // the build happens in CI. + // Cloudflare Pages project for the operator dashboard. CI builds the static export and ships the + // prebuilt output with `wrangler pages deploy` (Direct Upload), which reads the project name and output + // directory from here; `wrangler pages dev` serves that directory with the real _redirects/_headers. + // There is no Pages Git build: .github/workflows/deploy.yml is the build. "name": "civfix-admin", "pages_build_output_dir": "apps/admin/out", // Pure static export: there are no Pages Functions, so the Workers runtime version is never