Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
144 commits
Select commit Hold shift + click to select a range
3c4c085
readme matches the current repo, auth and deploy
theobong Sep 23, 2026
c271dcf
one env example with accurate api url notes
theobong Sep 23, 2026
df4ae34
wrangler header describes the direct upload deploy
theobong Sep 23, 2026
90a28e9
deploy workflow notes match same-origin api
theobong Sep 23, 2026
b748cfc
drop stale transpile comment
theobong Sep 23, 2026
c0cabe2
drop next start script, meaningless for a static export
theobong Sep 23, 2026
3bc1dd5
gitignore covers every env file but the example
theobong Sep 23, 2026
a346b4a
ci runs on stacked prs too
theobong Sep 23, 2026
7bac334
drop unreferenced pin-cleanup svg
theobong Sep 23, 2026
87da9fb
wire the favicon into layout metadata
theobong Sep 23, 2026
d79abf3
drop unused clsx, lucide-react and tailwind-merge
theobong Sep 23, 2026
f5a0dcd
vitest jsdom project with testing-library
theobong Sep 23, 2026
2d0b353
name the profile navigation test after its module
theobong Sep 23, 2026
ad5c436
shared api mock and query render helpers for dom tests
theobong Sep 23, 2026
8155e47
reset api mocks after unmount so in-flight queries cannot leak
theobong Sep 23, 2026
3f5814b
test: assert modal accessibility behaviorally
theobong Sep 23, 2026
a762d11
test: assert home tile accessibility behaviorally
theobong Sep 23, 2026
56f6e83
characterize hash routing and toast store
theobong Sep 23, 2026
860d192
characterize operator bootstrap and logout
theobong Sep 23, 2026
e8fd72a
characterize query client, key factories, error copy and dates
theobong Sep 23, 2026
c9e2f35
characterize leaflet map pins, tooltips and teardown
theobong Sep 23, 2026
6cbe1cd
characterize section page list, detail and deep-link states
theobong Sep 23, 2026
177c963
tailwind config: accurate header, real content glob, no unused animat…
theobong Sep 23, 2026
1df5dc8
deploy header names the staging and production triggers
theobong Sep 23, 2026
5918d6d
readme wraps the sign-in paragraph; gitignore keeps the plain DS_Stor…
theobong Sep 23, 2026
7c08b39
globals: drop unused spinner and scrollbar utilities, align tailwind …
theobong Sep 23, 2026
433e553
pin test locale and timezone; pane locators and fake-timer helper
theobong Sep 24, 2026
4c261a1
characterize list and infinite cache collisions by rendering the real…
theobong Sep 24, 2026
1b84906
test: assert home tile keyboard behavior instead of react internals
theobong Sep 24, 2026
6c6e41f
flippable current-behavior names, deterministic debounce tests, resto…
theobong Sep 24, 2026
dc6af84
merge origin/main
theobong Sep 24, 2026
6c8b421
merge origin/main
theobong Sep 24, 2026
3972c71
re-pin mail inbox characterization to the unified feed
theobong Sep 24, 2026
0688a6f
unknown report and user statuses render instead of blanking the page
theobong Sep 24, 2026
99172e4
create-org backdrop click keeps a filled-in draft
theobong Sep 24, 2026
0dacd14
profile editor diffs against the org it started from
theobong Sep 24, 2026
71e68f6
unknown org verification status renders instead of crashing
theobong Sep 24, 2026
eb28907
forward template modal is a real dialog and keeps an edited draft
theobong Sep 24, 2026
689b2df
attachment links open only for http(s) urls, with noopener
theobong Sep 24, 2026
e3eb018
mail compose is a real dialog and keeps a typed draft
theobong Sep 24, 2026
b036697
link-reports modal keeps its selection; unknown event status renders
theobong Sep 24, 2026
a563e54
map tooltips render citizen text as text, never html
theobong Sep 24, 2026
2eee553
malformed hash falls back to home instead of crashing the app
theobong Sep 24, 2026
cc51b25
error boundaries keep the shell usable after a failed chunk or render…
theobong Sep 24, 2026
dcf6141
dialogs: enter on close or cancel never confirms; selection drags kee…
theobong Sep 24, 2026
6639e87
merge the pr 2 branch
theobong Sep 24, 2026
8d105bc
page tests wait on the right pane and use fake timers for debounce
theobong Sep 24, 2026
782b09a
modal tests preload their section and wait for it before interacting
theobong Sep 24, 2026
dd4ce07
merge the pr 2 branch
theobong Sep 24, 2026
9e939d3
one event status fallback shows unknown values raw everywhere
theobong Sep 24, 2026
5e3b072
org status view lives in lib next to the other status views
theobong Sep 24, 2026
53a9c83
error boundary reuses ErrorState and never shows raw exception text
theobong Sep 24, 2026
35922fa
one attachment chip with a visible unavailable state
theobong Sep 24, 2026
e4e6455
one backdrop and pristine dismiss hook for every modal; enter focuses…
theobong Sep 24, 2026
e9ea743
every confirm opens on cancel and only its own button confirms
theobong Sep 24, 2026
465ed34
platform: central error toasts, typed query keys, live toast, safer a…
theobong Sep 24, 2026
6cbf7cf
users and moderation: deep links win, no jump after a decision, keybo…
theobong Sep 24, 2026
7b7ccc4
orgs: decide only pending applications, keyboard focus in create pane…
theobong Sep 24, 2026
7bedd7d
reports and events: deep links win, double-submit guards, contract-va…
theobong Sep 24, 2026
82f8760
mail and discovery: deep links found, one error toast, clearable cont…
theobong Sep 24, 2026
ce0be43
home, analytics, hosts, pages: previews independent of the summary, h…
theobong Sep 24, 2026
7335a6e
merge the pr 3 branch
theobong Sep 24, 2026
35701a2
one central error toast path with per-mutation copy; inline field err…
theobong Sep 24, 2026
2a48908
toast announces only its message and returns focus after a keyboard d…
theobong Sep 24, 2026
4c04ce8
ported css for profile rows and visually hidden text; home uses the s…
theobong Sep 24, 2026
e9e2c67
one selection rule on every page; discovery sends only edited contact…
theobong Sep 24, 2026
b6ad07c
unknown signup page and broadcast statuses render raw instead of cras…
theobong Sep 24, 2026
80653aa
mutations await their cache refresh, so buttons stay pending until th…
theobong Sep 24, 2026
cfee804
save and route sends every contact the form shows
theobong Sep 24, 2026
d50081b
decisions and report removal clear the resolved item
theobong Sep 24, 2026
415cf97
opening an unread message keeps it open under Unread and Needs attention
theobong Sep 24, 2026
a6b6c28
success toasts come from the mutation cache, so they survive a pane u…
theobong Sep 24, 2026
ec8d546
discovery writes stop refetching the boundary geometry
theobong Sep 24, 2026
e0c4a21
every mutation success toast moves to hook meta; save and route await…
theobong Sep 24, 2026
6144576
accurate flag toast comments; drop a stray blank line
theobong Sep 24, 2026
95e9410
comments: platform keeps only why-comments, no retired references
theobong Sep 24, 2026
50f06a1
comments: users and moderation
theobong Sep 24, 2026
dc2946c
comments: orgs
theobong Sep 24, 2026
1362aea
comments: reports and events, empty jsx debris removed
theobong Sep 24, 2026
3195eeb
comments: mail, inbox and discovery
theobong Sep 24, 2026
020bb92
comments: home, analytics, hosts and pages
theobong Sep 24, 2026
ab4a854
copy: no em dashes in the ui; one EMPTY_VALUE placeholder
theobong Sep 24, 2026
a6a6a42
comments: restore two why-comments the sweep dropped
theobong Sep 24, 2026
7614f24
copy: clearer handle placeholder; exact empty-value assertion
theobong Sep 24, 2026
7da1cb2
styles: delete dead admin.css rules, empty media blocks and unused ke…
theobong Sep 24, 2026
f5bda57
styles: delete dead colors-and-type utility classes
theobong Sep 24, 2026
c84fd0e
styles: delete the dead op-login-error rule
theobong Sep 24, 2026
26889d3
copy-id menu item darkens on hover (was an undefined color variable)
theobong Sep 24, 2026
ae0aef7
merge the pr 4 branch
theobong Sep 24, 2026
85d9e0a
merge the pr 5 branch
theobong Sep 24, 2026
b7f1ccf
styles: merge duplicate selectors and identical rule bodies
theobong Sep 24, 2026
2adac77
styles: raw hex colors that equal a token use the token
theobong Sep 24, 2026
ee45fc2
styles: drop class names that have no css rule
theobong Sep 24, 2026
503169d
styles: delete custom properties nothing references
theobong Sep 24, 2026
d19abd6
styles: css comments explain why, not what
theobong Sep 24, 2026
2e55239
orgs: split long panels into named parts, name magic values, un-expor…
theobong Sep 24, 2026
885d5d9
reports and events: split the god files into focused modules, name ma…
theobong Sep 24, 2026
0f8c7ad
platform: dead icons and skeleton gone, one report bucket mapper, sha…
theobong Sep 24, 2026
60c8f44
reports and events: state and mutations stay above the detail early r…
theobong Sep 24, 2026
da17cbf
orgs: evidence list resets its viewer with the original effect, not a…
theobong Sep 24, 2026
b644fd8
features import the shared timing, id and byte constants; drop the de…
theobong Sep 24, 2026
7165fd3
mail and discovery: split the page and detail god components, name ma…
theobong Sep 24, 2026
5a6cec9
users and moderation: split detail components into focused files, nam…
theobong Sep 24, 2026
a72601e
home, analytics, hosts, pages: split long components, one preview fet…
theobong Sep 24, 2026
4d5411b
merge the pr 7 branch
theobong Sep 24, 2026
e61df46
bytes: export the kilobyte size the inbox attachments now use
theobong Sep 24, 2026
26db25f
maps: one leaflet base for tiles, resize and teardown; reuse the tile…
theobong Sep 24, 2026
4e32e42
comments and spacing tidy; raw-html guard scans every mail and inbox …
theobong Sep 24, 2026
7b20083
move the home query hooks into features/home
theobong Sep 24, 2026
132cec2
query layer: one infinite-list factory, flatPages and invalidateKeys;…
theobong Sep 24, 2026
c3d1d3d
merge the pr 8 branch
theobong Sep 24, 2026
f0c743c
one useSelection hook and shared list pieces (states, card, search, l…
theobong Sep 24, 2026
a2d4d01
every section uses useSelection and the shared list pieces
theobong Sep 24, 2026
f4a088a
one implementation per helper: initials, short ids, first names, comp…
theobong Sep 24, 2026
a22a3eb
perf: date formatters built once, not per row
theobong Sep 24, 2026
aac941b
perf: invalidateKeys skips keys a broader key in the batch already co…
theobong Sep 24, 2026
b3c2932
perf: list rows are memoized with a stable select callback
theobong Sep 24, 2026
dab6585
merge the pr 1 branch (#32)
theobong Sep 24, 2026
525f9af
lint: eslint 9 flat config with typed rules; drop the meaningless no-…
theobong Sep 24, 2026
d93d62f
knip config; unresolved postcss type annotation removed
theobong Sep 24, 2026
7a2a2e2
jscpd duplication gate; knip and jscpd steps in ci; lint via the esli…
theobong Sep 24, 2026
c6b08aa
keep the https link check as it was; the shared safe-link gate is def…
theobong Sep 24, 2026
2511aeb
invalidateKeys never skips object-param keys; keep useDiscoveryTask u…
theobong Sep 24, 2026
c18e638
memoized rows take the clock as a prop, so their age labels tick ever…
theobong Sep 24, 2026
7ba9496
merge chore/admin-campaign-01-repo-hygiene
byteful Sep 24, 2026
9c23486
merge chore/admin-campaign-02-test-harness
byteful Sep 24, 2026
2a8d898
merge fix/admin-campaign-03-security-crash
byteful Sep 24, 2026
b68ec99
merge fix/admin-campaign-04-correctness
byteful Sep 24, 2026
cbe7eb1
merge chore/admin-campaign-05-comments
byteful Sep 24, 2026
34a38f0
merge chore/admin-campaign-06-styles
byteful Sep 24, 2026
d590d62
merge chore/admin-campaign-07-cleanup-a
byteful Sep 24, 2026
b802f8b
merge chore/admin-campaign-08-cleanup-b
byteful Sep 24, 2026
b2639d4
merge chore/admin-campaign-09-abstraction
byteful Sep 24, 2026
f4a5c0b
merge main into the cleanup integration branch
civfix-agent Oct 7, 2026
8c0d827
restore the mail tooltips, routed date and mail error copy cleanup ch…
civfix-agent Oct 7, 2026
83b54d4
test the fix campaigns' untested behavior changes
civfix-agent Oct 7, 2026
011b991
a new deep link opens a pick that already dropped out of its list
civfix-agent Oct 11, 2026
a1f2442
test the favicon, empty-value glyph and no-em-dash copy cleanup kept
civfix-agent Oct 11, 2026
1e771bd
a deep link to the open row keeps its drop-out tracking
civfix-agent Oct 11, 2026
7c4e8c0
report card blocked hints use the helper hint style
civfix-agent Oct 11, 2026
c81a517
merge main into chore/cleanup-integration
civfix-agent Oct 13, 2026
33773bf
ci runs in the merge queue
byteful Oct 13, 2026
22c84b8
build against the shared 0.60.0 snapshot
byteful Oct 14, 2026
4561d51
adopt @civfix/shared ^0.60.0
byteful Oct 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 0 additions & 16 deletions .env.example

This file was deleted.

12 changes: 9 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]
# main's merge queue waits for the required checks on its own gh-readonly-queue/* commit; without this
# trigger they never report there and every queued PR times out.
merge_group:
Expand Down Expand Up @@ -40,6 +40,12 @@ jobs:
- name: Lint
run: pnpm lint

- name: Unused files, exports and dependencies (knip)
run: pnpm --filter admin knip

- name: Duplication (jscpd)
run: pnpm --filter admin dup

- name: Typecheck
run: pnpm typecheck

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 @@ -120,7 +120,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
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
civfix — the operator dashboard
civfix: the operator dashboard
Copyright (c) 2026 Reach Out Los Angeles Inc. and contributors

This program is free software: you can redistribute it and/or modify it under
Expand Down
37 changes: 22 additions & 15 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,37 +19,43 @@ 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)

```
pnpm install # fetches @civfix/shared from repo.civfix.org
pnpm build # admin (next build, static export -> apps/admin/out)
pnpm typecheck
pnpm lint
pnpm lint # eslint (flat config, typed rules) over apps/admin
pnpm test # vitest unit tests
pnpm --filter admin knip # unused files, exports and dependencies
pnpm --filter admin dup # jscpd duplication gate (app code and tests measured separately)
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 +65,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
13 changes: 0 additions & 13 deletions apps/admin/.eslintrc.json

This file was deleted.

7 changes: 7 additions & 0 deletions apps/admin/.jscpd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"path": ["src"],
"format": ["typescript", "tsx", "css"],
"ignore": ["**/*.test.ts", "**/*.test.tsx", "src/test/**"],
"reporters": ["console"],
"threshold": 1
}
7 changes: 7 additions & 0 deletions apps/admin/.jscpd.tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"path": ["src"],
"format": ["typescript", "tsx"],
"pattern": "{**/*.test.ts,**/*.test.tsx,src/test/**}",
"reporters": ["console"],
"threshold": 6
}
49 changes: 49 additions & 0 deletions apps/admin/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { dirname } from "node:path"
import { fileURLToPath } from "node:url"

import { FlatCompat } from "@eslint/eslintrc"

const baseDirectory = dirname(fileURLToPath(import.meta.url))

// eslint-config-next 15.x ships only eslintrc presets; FlatCompat is the supported bridge until 16.
const compat = new FlatCompat({ baseDirectory })

const config = [
{
ignores: [".next/**", "out/**", "coverage/**", "next-env.d.ts"],
},
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
// images.unoptimized is set for the static export, so next/image would render a plain <img> anyway.
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: baseDirectory,
},
},
rules: {
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "separate-type-imports" },
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { attributes: false } },
],
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
]

export default config
28 changes: 28 additions & 0 deletions apps/admin/knip.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { KnipConfig } from "knip"

const CSS_IMPORT = /@import\s+(?:url\(\s*)?["']([^"']+)["']/g

const config: KnipConfig = {
project: ["src/**/*.{ts,tsx,css}", "*.{ts,mjs}"],
next: {
config: ["next.config.mjs"],
entry: ["src/app/**/{layout,page,not-found,error,loading}.tsx"],
},
vitest: {
config: ["vitest.config.ts"],
entry: ["src/**/*.test.{ts,tsx}", "vitest.setup.ts"],
},
// Knip skips any extension it cannot compile, so without this an orphaned stylesheet is never
// reported; following @import is what keeps colors-and-type.css (reached only from admin.css) used.
compilers: {
css: (text: string) =>
[...text.matchAll(CSS_IMPORT)].map(([, specifier]) => `import "${specifier}";`).join("\n"),
},
ignoreDependencies: [
// Loaded by name through FlatCompat.extends("next/...") in eslint.config.mjs, which knip's ESLint
// plugin cannot see into.
"eslint-config-next",
],
}

export default config
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
23 changes: 14 additions & 9 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,41 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint --max-warnings=0",
"lint": "eslint . --max-warnings=0",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"clean": "rimraf .next out *.tsbuildinfo"
"test": "vitest run",
"clean": "rimraf .next out *.tsbuildinfo",
"knip": "knip",
"dup": "jscpd -c .jscpd.json && jscpd -c .jscpd.tests.json"
},
"dependencies": {
"@civfix/shared": "^0.58.0",
"@civfix/shared": "^0.60.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": {
"@eslint/eslintrc": "^3.3.5",
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.6.7",
"@types/leaflet": "^1.9.15",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-next": "15.5.25",
"jscpd": "^5.3.2",
"jsdom": "^26.1.0",
"knip": "^6.38.0",
"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
1 change: 0 additions & 1 deletion apps/admin/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
Expand Down
6 changes: 0 additions & 6 deletions apps/admin/public/ds/pin-cleanup.svg

This file was deleted.

Loading
Loading