Underground 2 Revived
Chrome과 Firefox에서 Need for Speed Underground 2를 WebGPU로 두고 팬 프로젝트입니다
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A mobile-first scorekeeping tool for the board game Carcassonne. It tracks players and scores during a game by manual entry; no Carcassonne scoring rules are built in.
/game/<id>). An active game can also be discarded.See repository assessment and next steps for the decisions taken, risks, and open product questions.
The project uses SvelteKit 3, Svelte 5, Vite 8, TypeScript 6, Tailwind CSS 4, and Skeleton 5 (core CSS and theme only; no Skeleton components are used). It is one SvelteKit application: its hooks, server loads, and form actions run in the app server; no separately hosted API is required. Use Node.js 24 or later (the version CI and the Docker image run), then:
npm install
npm run dev
Set the shared login password and database URL as private server environment variables, for example in a local .env file:
APP_PASSWORD=choose-a-strong-shared-password
DATABASE_URL=postgresql://user:password@localhost:5432/carcassonne
Everyone signs in on / with this one password. Every other page redirects to / until the user signs in. Sessions are stored in an httpOnly cookie that lasts 30 days. The cookie is signed with the current password. To rotate the password, change APP_PASSWORD and restart the server; this signs out every existing session. If APP_PASSWORD is unset, login is refused. Never expose it as a PUBLIC_ variable. Login is not yet rate limited, so add rate limiting and HTTPS before exposing the app publicly.
An Aspire TypeScript AppHost in aspire-apphost/ starts the app and a local Postgres database. It is optional; npm run dev still runs the app on its own, but then you must provide your own DATABASE_URL. It registers a Postgres container (with a persistent data volume), a db-migrate step that applies Drizzle migrations, and the SvelteKit app, which waits for the migrations and receives DATABASE_URL. The Postgres container requires Docker or Podman.
Install the Aspire CLI, run npm install in aspire-apphost/ once, and supply the shared password as an Aspire secret parameter:
cd aspire-apphost && npm install && cd ..
aspire run # or `aspire start` / `aspire stop`; opens the dashboard
On first run the dashboard prompts for the app-password parameter. To supply it non-interactively, set the environment variable Parameters__app-password (the hyphen means it must be set with env "Parameters__app-password=..." aspire start). Do not commit the value. Biome checks apphost.mts; aspire-apphost/.aspire/ is generated and must not be edited.
aspire init also installed Aspire agent skills in .claude/skills/ (including playwright-cli). They are vendored third-party content and are excluded from Biome. The Aspire CLI sends usage telemetry for these skills; set ASPIRE_CLI_TELEMETRY_OPTOUT=true to opt out.
.mcp.json registers the svelte MCP server (npx -y @sveltejs/mcp), which gives coding agents current Svelte 5 and SvelteKit docs and a static-analysis autofixer. Claude Code asks you to approve it once per project; no keys are needed. AGENTS.md tells agents when to use its tools. Claude Code also offers a per-user svelte plugin (/plugin marketplace add sveltejs/ai-tools, then /plugin install svelte) that adds Svelte skills and a Svelte-editing subagent. See the Svelte AI docs.
Carc Keeper is a PWA: it has a web manifest (static/manifest.webmanifest, icons in static/) and a service worker (src/service-worker/index.ts, rules in src/lib/offline.ts). Browsers offer to install it, and pages you have already viewed (home, history, players, a game) stay readable offline with scoring disabled. Pages not yet viewed show static/offline.html. Offline scoring is not supported. The worker clears its saved pages on logout; to add a page to the offline set, add it to the allow-list in src/lib/offline.ts.
SvelteKit only registers the worker in production builds, not vite dev. To try it, run npm run build && npm run preview, open the app, and use DevTools > Application (Service Workers, Cache Storage, Manifest). To reset it there, choose Unregister and Clear site data.
A game open on several devices updates everywhere within about a second. The server sends a small "changed" message over Server-Sent Events (/game/<id>/events, served by src/routes/(game)/game/[id]/events/+server.ts) after each score, undo, finish, discard or rematch, and each screen reloads the game through its normal load. A heartbeat comment every 20 seconds keeps idle connections open behind Fly's proxy, and a device that was asleep or offline reloads when it reconnects or the tab is shown again. The stream is closed while the device is offline.
On the scoring device, tapped points (quick buttons and the keypad) show at once and roll back with an error if the save fails; undo, finish, discard and rematch wait for the server. Anyone logged in can still score; a single-scorer role is not built yet.
One-machine limit. Messages are fanned out in memory (src/lib/server/gameEvents.ts), which works because the app runs on one Fly machine (--ha=false). With a second machine, viewers connected to the other one would miss changes. Moving to more machines needs Postgres LISTEN/NOTIFY over a direct (non-pooled) Neon connection, or a hosted pub/sub, instead of that module.
Data is stored in Postgres through Drizzle ORM with the postgres driver. Locally, Aspire runs Postgres in a container; production is planned to use a single Neon instance. Tables: players, games, game_players (a game's players and their meeple colors) and score_events (signed points; a player's score is the sum of their events; Undo deletes the latest one and the score log can remove any active-game entry).
src/lib/server/db/schema.ts; client: src/lib/server/db/index.ts (reads the private DATABASE_URL).npm run db:generate creates a migration in drizzle/ after a schema change; commit the result.npm run db:migrate applies migrations to DATABASE_URL. Aspire runs it automatically for the local database. In production the CD workflow runs scripts/migrate.mjs before each deploy, never the app at startup.MIGRATE_DATABASE_URL). The pooled string is fine for the running app (DATABASE_URL).Unit tests use in-memory repositories and need no database. src/lib/services/gameService.integration.test.ts runs the real Drizzle repositories and is skipped unless TEST_DATABASE_URL is set (see below).
npm run check # Svelte and TypeScript checks
npm run lint # Biome formatting and lint checks
npm run test:unit -- --run # Vitest, single run
npm run playwright-test # Playwright; builds and starts a preview server
npm run build
CI runs the same commands. Playwright also requires its browser dependencies (npx playwright install). npm run playwright-test covers the login screen and the unauthenticated redirect and needs no database.
Two kinds of test run against a real database. Both need a throwaway, freshly migrated database, so do not point them at data you care about.
# 1. Create an empty database and apply the migrations to it.
DATABASE_URL=postgresql://user:password@localhost:5432/carcassonne_test npm run db:migrate
# 2. Service tests against the real Drizzle repositories. WARNING: this deletes
# every row in the players and games tables of that database.
TEST_DATABASE_URL=postgresql://user:password@localhost:5432/carcassonne_test npm run test:unit -- --run
# 3. The end-to-end journey: log in, add players, start a game, score with
# quick buttons and the keypad, undo, finish, and see it in History.
# It builds the app, starts a preview server and needs the database to be fresh.
E2E_DATABASE_URL=postgresql://user:password@localhost:5432/carcassonne_test npm run test:e2e
With Aspire, start the app host (aspire start), copy the Postgres connection string from the dashboard, create a separate empty database in that container (for example create database carcassonne_test), and use that name in the URLs above. Without Aspire, any Postgres works, for example docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=password postgres:17. The journey leaves its data behind, so reset the database before running it again.
GitHub Actions, with no conditions to read: CI checks; CD runs CI, deploys an ephemeral staging, waits for your approval, then releases production.
| Workflow | Runs on | What it does |
|---|---|---|
.github/workflows/ci.yml | every pull request (and called by CD) | check, lint, unit tests, build, the login Playwright tests; the service tests and the e2e journey against Postgres service containers |
.github/workflows/cd.yml | every push to main | runs ci.yml, then Deploy staging: a fresh Neon branch of the production database, migrated, plus a Fly app <app>-staging; after your approval, Migrate and deploy production, then tears staging down |
.github/workflows/staging-teardown.yml | after production, or by hand | destroys the staging Fly app and deletes the staging Neon branch |
If CI fails, no staging is built. Staging is ephemeral: each merge deletes the previous staging Neon branch and creates a new one from production (copy-on-write, so it holds a copy of the real data and rehearses the migration against it), runs scripts/migrate.mjs on it, and deploys to https://<app>-staging.fly.dev with the same login password. The branch also expires after 14 days by itself. When staging looks right, approve the pending production deployment on the run (GitHub emails you; the run page has a Review deployments button, also in the GitHub mobile app). It then migrates production (MIGRATE_DATABASE_URL, falling back to DATABASE_URL), deploys with flyctl deploy --remote-only (Fly builds the Dockerfile, a SvelteKit adapter-node server), smoke tests the login page, then tears staging down. If you reject the approval, staging stays up until the next merge, its 14-day expiry, or a manual run of Staging teardown. A failed migration stops the release before the new code ships. Pipelines are serialized and never cancelled half way: a newer merge waits until the current run is approved, rejected or finished, so staging always shows what is waiting for approval. The shared deploy steps live in .github/actions/fly-deploy. Fly settings (region, machine size, health check) live in fly.toml; each app runs a single machine.
Production rebuilds the same commit from source rather than reusing the staging image.
Because the database is migrated before the new code goes live (and the previous version keeps serving until then), keep migrations backward compatible: add columns and tables; do not drop or rename in the same release as the code that stops using them. If a deploy fails after its migration ran, the database stays ahead of the running code, which is safe for additive migrations.
The app knows which environment it runs in so you cannot mistake staging for production:
| Environment | How it is set | What you see |
|---|---|---|
| Local | automatic whenever the Vite dev server runs (npm run dev, Aspire) | a purple "LOCAL: development server" banner and [Local] tab titles |
| Staging | APP_ENV=staging, set by the staging deploy (environment: staging in cd.yml) | an orange "STAGING" banner on every page (login included), [Staging] tab titles, and an installed app named "Carc Staging" with an orange icon |
| Production | anything else, including unset or a misspelled value | no banner; unchanged name, icon and colours |
APP_ENV is not a secret. The deploy action passes it with flyctl deploy --env, and its smoke test fails the deploy if staging lacks the banner or production shows one. Setting APP_ENV=development forces the Local look outside the dev server (for example under npm run preview). Staging and production already have different origins, so both install side by side. iOS saves the icon when you add the app to the home screen, so remove and re-add an existing staging install to get the new icon. The staging icons are static/*staging*.png and are committed (the meeple on orange with an "STG" mark).
Infrastructure is not managed as code yet, so these are done by hand once:
Neon: create a project (the free plan works) in a region near your Fly region. Copy two connection strings from its default (production) branch: the pooled one (host contains -pooler) for the app, and the direct one for migrations. Also copy the project ID (Settings) and create an API key (Account settings > API keys) so the pipeline can branch the database.
Fly.io: run fly auth login, then create an organization-scoped token with fly tokens create org. An org token lets the pipeline create and destroy apps.
GitHub: add these repository secrets (for example with gh secret set NAME, pasting the value when prompted), and never commit them.
| Secret | Value |
|---|---|
FLY_API_TOKEN | the org token from step 2 |
APP_PASSWORD | the shared login password (staging uses it too) |
DATABASE_URL | Neon pooled connection string (production branch) |
MIGRATE_DATABASE_URL | Neon direct connection string, used only to migrate (optional; falls back to DATABASE_URL) |
NEON_API_KEY | the Neon API key from step 1 |
And a repository variable NEON_PROJECT_ID (gh variable set NEON_PROJECT_ID). Optionally add the variable FLY_ORG (defaults to personal). Under Settings > Environments > production, add yourself as a required reviewer; that is the approval gate (without it production deploys straight after staging).
fly.toml: change app (it must be unique across Fly) and primary_region if you like. Staging uses <app>-staging.
Merge to main. CD creates the staging app and Neon branch and deploys it. Approve the production deployment on the run to create the production app, migrate and deploy it at https://<app>.fly.dev.
fly releases and fly deploy --image <previous image>, or revert the commit and release again. Migrations only go forward, so a rollback does not undo schema changes.APP_PASSWORD secret, then merge or re-run the workflows; this signs everyone out.neondb, neondb_owner); if yours differ, set database and role on the branch step in cd.yml.docker build . builds the same image locally, and docker run -p 3000:3000 -e APP_PASSWORD=... -e DATABASE_URL=... <image> runs it.