This file provides guidance to OpenCode / Claude Code when working in this repository.
pnpm install # Install dependencies
pnpm dev # Start dev server with Turbopack at localhost:3000
pnpm build # Production build
pnpm lint # Run ESLint
pnpm test # Run tests in watch mode (vitest)
pnpm test:run # Run tests once (CI)
pnpm preview # Build + serve in the Cloudflare Workers runtime (workerd)
pnpm deploy # Build + deploy the Worker
pnpm cf-typegen # Regenerate cloudflare-env.d.ts from wrangler.jsoncLinting: pnpm lint runs eslint . directly. The previous Next.js 16.1.6 / eslint 10 incompatibility has been resolved by upgrading to Next.js 16.3.x and eslint 9.x. eslint is deliberately held at 9.x — the 10.x major has not been verified against eslint-config-next. cloudflare-env.d.ts and .open-next/** are ignored.
Deployed as a Cloudflare Worker at helldivers.michi.onl via the OpenNext adapter (@opennextjs/cloudflare). CI/CD is Cloudflare Workers Builds (connect the Worker to the repo in the dashboard); no GitHub Actions deploy job. wrangler.jsonc owns the Worker name, bindings and custom domain; open-next.config.ts owns the cache.
- Incremental cache: R2 bucket
companion-next-cache+ a Durable Object revalidation queue (DOQueueHandler), with the long-lived regional cache. This backs thefetch(..., { next: { revalidate } })times inlib/api/endpoints.ts. - No tag cache: the app never calls
revalidateTag/revalidatePath. - Images:
images.unoptimizedis set; Cloudflare image optimization is a paid binding and all assets are local. - Compatibility date/flags:
2026-09-19,nodejs_compat,global_fetch_strictly_public. - Secrets: none.
.dev.varsonly setsNEXTJS_ENV.
The DOQueueHandler "not exported" warning during build/preview is the documented OpenNext known issue and safe to ignore — it is not used at build time.
Next.js 16 App Router project using React Server Components throughout. The app is a single-page dashboard (/) with anchor sections (#news, #statistics, #faq); the /news, /statistics, and /faq routes are permanentRedirects to those anchors.
Data is fetched through a layered architecture:
lib/api/client.ts—getAPI()wrapsfetchagainsthttps://api.helldivers2.dev. Theurlis an absolute origin path including its/apior/rawprefix (seeAPI_ORIGIN); raw ArrowHead passthrough requests are not nested under/api. It requiresX-Super-ClientandX-Super-Contactheaders (configured inconfig/site.ts), supports per-requestrevalidate, and retries once on HTTP 429.lib/api/endpoints.ts— central registry of API URLs and revalidation times (10 min–24 h depending on volatility). Fixed endpoints are{ url, revalidate }objects; parameterised ones (by index, gid or war season) are(arg) => Endpointfactories.lib/services/*.ts— one service per domain; callsgetAPI()and validates the raw DTO shape. Each list endpoint has a matchingfetch*ByIndexwhere the API offers one.lib/data/*.ts— Reactcache()-wrapped data loaders used by Server Components. Each exposesget*functions that map DTOs to domain models and returnnullon failure. Loaders that need the current war season id resolve it once viagetWarId()and cache it.lib/transformers/*.ts— pure mapping/calculation helpers (liberation math, status labels, DTO-to-domain mapping).
All public endpoints are wired up:
lib/services/campaigns.ts—/v1/campaigns,/v1/war,/v1/campaigns/{index}lib/services/planets.ts—/v1/planets,/v1/planet-events,/v1/planets/{index}lib/services/war-metadata.ts— raw/raw/api/WarSeason/current/WarID,/WarSeason/{id}/WarInfo,/WarSeason/{id}/Status,/raw/api/Stats/war/{id}/summary,/raw/api/NewsFeed/{id}lib/services/assignments.ts—/v1/assignments,/v1/assignments/{index}lib/services/dispatches.ts—/v2/dispatches,/v2/dispatches/{index}lib/services/space-station.ts—/v2/space-stations,/v2/space-stations/{index}lib/services/news.ts—/v1/steam,/v1/steam/{gid}
The raw war payloads are numeric: races are 1 Humans / 2 Terminids / 3 Automaton / 4 Illuminate (getFactionFromRace), and NewsFeed.Published is seconds since the war start, not a unix timestamp (mapNewsFeedItems anchors it).
Widgets in components/widgets/ are async Server Components that call the lib/data/* loaders:
components/widgets/root/— dashboard cards for/(major order, campaigns, map, dispatches, space station, war summary, planet events).components/widgets/merged/— section wrappers that compose root/news/statistics widgets onto the single page (news-section,galaxy-section,statistics-section,faq-section).components/widgets/news/—newsfeed,super-earth-newsandwiki.components/widgets/galaxy/— the all-planets browser,planet-overview,planet-battle-stats.components/widgets/statistics/— reused in the merged statistics section.components/widgets/space-station/—space-station-detailfor the station route.
Interactive client pieces are kept separate:
campaign-map-dynamic.tsxdynamically importscampaign-map.tsxwithssr: falsebecause Leaflet needs the DOM.campaign-table-client.tsxhandles filtering and the planet detail dialog.galaxy/planet-filters.tsxhandles faction/sector/search filters over the planet list.
/ is the single-page dashboard; the section anchors (#news, #galaxy, #statistics, #faq) also have permanentRedirect routes (/news, /galaxy, /statistics, /faq). Detail routes backed by the by-index endpoints are /planet/[index], /campaign/[index], /dispatch/[id], /news/[gid] and /station/[index]; each validates a numeric id (or any gid), 404s on a failed lookup, and sets generateMetadata.
getCampaignStats()— categorizes campaigns into active/liberated/total, and splitsactivePlanetsintomovingPlanets/parkedPlanets. Sorts by attention (defenses by soonestendTime, then progress, then player count), not by remaining health.getPlanetStats()— combinesgetEffectiveHealth,getLiberation,getRegenRate, andgetStatusgetEffectiveHealth()— resolves planet health vs. event healthgetCampaignProgress()— the one percentage worth showing per row: event defense health, else planet health, else the leading region's progress (labelled with the region name). Planet health alone reads 0.00% on most fronts under the region system.getLeadingRegion()— the unlocked region players are actually pushing; ignores locked regions, which stay pinned at full healthisMoving()— whether a front has an event or any planet/region progress; drives the moving/parked splitgetFactionIcon()/species— maps faction name to icon path
types/campaigns.ts defines Planet, Campaign, PlanetRegion, CampaignStats. types/planets.ts covers the galaxy-wide listings, types/assignments.ts major orders, types/war-metadata.ts the raw ArrowHead payloads, and types/space-station.ts space station data.
Tailwind CSS v4 with tailwind-merge + clsx via cn() in lib/utils.ts. Theme variables live in app/globals.css. UI primitives are in components/ui/ (Radix UI-based). Font: Space Grotesk via next/font/google. Theme switching via next-themes.
Tests use Vitest with the node environment and Vite's native resolve.tsconfigPaths for alias resolution (see vitest.config.mts). The tests/ tree mirrors lib/:
tests/lib/api/client.test.ts— mocksglobal.fetchwithvi.tests/lib/services/*.test.ts— mocks services and validates error handling.tests/lib/data/*.test.ts— mocks services to test the cached data layer.tests/lib/transformers/*.test.ts— pure unit tests for mapper/calculation helpers.
Run a single file: pnpm vitest run tests/lib/transformers/campaigns.test.ts.
- Package manager: pnpm.
pnpm-workspace.yamlonly configures built dependencies (sharp,@tailwindcss/oxide, etc.). - Path alias:
@/*maps to./*(seetsconfig.json). - PWA manifest:
app/manifest.json; icons are inpublic/. - Route types:
next-env.d.tsimports generated types from.next/types/routes.d.ts, so runpnpm buildorpnpm devat least once for route type generation.
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.