Skip to content
Closed
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
16 changes: 0 additions & 16 deletions .env.example

This file was deleted.

6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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):
#
Expand All @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -29,26 +30,30 @@ 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

Cloudflare Pages (Direct Upload). GitHub Actions builds the static export (a plain checkout +
`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

Expand All @@ -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.
14 changes: 6 additions & 8 deletions apps/admin/.env.example
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion apps/admin/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}

Expand Down
5 changes: 0 additions & 5 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -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"
}
Expand Down
6 changes: 0 additions & 6 deletions apps/admin/public/ds/pin-cleanup.svg

This file was deleted.

32 changes: 5 additions & 27 deletions apps/admin/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 <html>
* (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).
*/
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}

Expand Down
18 changes: 6 additions & 12 deletions apps/admin/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -126,7 +120,7 @@ const config: Config = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [],
}

export default config
Loading
Loading