282 lines
11 KiB
Markdown
282 lines
11 KiB
Markdown
# Buzz Sheet
|
||
|
||
Buzz Sheet is a Thai-first guide CMS for turning structured character, build,
|
||
team, rotation, and comparison data into responsive game-guide pages.
|
||
|
||
Production target: `https://guide.sudloh.com`
|
||
|
||
This project is an independent community tool for Genshin Impact players. It is
|
||
not affiliated with or endorsed by HoYoverse.
|
||
|
||
## Included capabilities
|
||
|
||
- Searchable character guide directory.
|
||
- Structured editors for overview, weapons, artifacts, constellations, teams,
|
||
and custom sections with autosave and conflict recovery.
|
||
- Better Auth email/password login with administrator-managed accounts.
|
||
- Private S3-compatible media uploads with reference-aware same-origin delivery.
|
||
- PostgreSQL transactions and a retryable outbox.
|
||
- Shared Redis cache invalidation and privacy-safe Server-Sent Events across
|
||
replicas.
|
||
- Discord-triggered Lunaris catalog updates with exact-version synchronization.
|
||
|
||
The five reference workbooks are represented only by sanitized structural and
|
||
formula fixtures. Their guide text and media are not imported or published.
|
||
|
||
## Routes
|
||
|
||
| Route | Purpose |
|
||
| --- | --- |
|
||
| `/` | Searchable public character directory |
|
||
| `/[character]` | Redirect to the first visible page |
|
||
| `/[character]/[page]` | Render a public guide page |
|
||
| `/admin/login` | Administrator email/password sign-in |
|
||
| `/admin/register` | Create and remove administrator accounts |
|
||
| `/admin` | Character and page overview |
|
||
| `/admin/[character]/[page]` | Visual page editor |
|
||
| `/admin/create` | Create a structured guide from the synced character catalog |
|
||
| `/media/[id]` | Authorized same-origin media response |
|
||
| `/api/health` | Liveness response |
|
||
| `/api/health?ready=1` | PostgreSQL and Redis readiness response |
|
||
|
||
A page is public only when both its page and character are visible. A visible
|
||
character without a visible page returns 404.
|
||
|
||
## Local development
|
||
|
||
Requirements:
|
||
|
||
- Bun 1.3.14
|
||
- PostgreSQL
|
||
- Redis
|
||
- Private S3-compatible object storage
|
||
|
||
Install dependencies and create your local environment file:
|
||
|
||
```bash
|
||
bun install --frozen-lockfile
|
||
cp .env.example .env
|
||
```
|
||
|
||
`.env.example` contains placeholders only. Configure `.env` yourself; it is
|
||
ignored by Git and must never be committed. `BETTER_AUTH_URL` must exactly
|
||
match the application origin. Set `BASE_URL` to the public site origin for SEO
|
||
metadata, sitemap, and robots.txt. Existing administrators can create additional
|
||
credential accounts from `/admin/register`; public registration is disabled.
|
||
|
||
Prepare the database and start the application:
|
||
|
||
```bash
|
||
bun run db:migrate
|
||
bun run dev
|
||
```
|
||
|
||
### Fixture-only demo
|
||
|
||
The UI can be inspected without PostgreSQL, Redis, S3, or authentication
|
||
credentials by running:
|
||
|
||
```bash
|
||
BUZZ_DEMO_MODE=true bun run dev
|
||
```
|
||
|
||
Demo mode uses sanitized local fixtures and bypasses production authorization
|
||
and external services. Never enable it in a deployed environment.
|
||
|
||
## Commands
|
||
|
||
| Command | Purpose |
|
||
| --- | --- |
|
||
| `bun run dev` | Start the Next.js development server |
|
||
| `bun run test` | Run the Vitest unit and integration suite |
|
||
| `bun run test:coverage` | Produce V8 coverage output |
|
||
| `bun run typecheck` | Run TypeScript without emitting files |
|
||
| `bun run lint` | Run ESLint |
|
||
| `bun run build` | Compile the production application |
|
||
| `bun run db:generate` | Generate a Drizzle migration from schema changes |
|
||
| `bun run db:migrate` | Apply committed Drizzle migrations |
|
||
| `bun run import:guides` | Preview the one-time Google Sheets guide migration (`-- --apply` to write) |
|
||
| `bun run worker:discord` | Listen for Lunaris update announcements |
|
||
| `bun run worker:outbox` | Process cache and event outbox records |
|
||
|
||
## Runtime design
|
||
|
||
Accepted edits use an `expectedVersion` and commit guide content, media reference
|
||
counts, and outbox records in one PostgreSQL transaction. The outbox worker then invalidates affected Redis tags
|
||
before publishing an opaque `{ type, id, version }` event. Connected clients
|
||
refetch authoritative state; SSE never contains page content and is not the
|
||
source of truth.
|
||
|
||
The Discord catalog worker watches the private channel configured by
|
||
`DISCORD_CHANNEL_ID`. It parses new Lunaris `Version Change` announcements and
|
||
then fetches and synchronizes the latest version reported by Lunaris. Startup
|
||
only connects the listener; it does not replay channel history or start a sync.
|
||
Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord
|
||
Message Content intent and grant the bot View Channel and Send Messages in the
|
||
configured channels.
|
||
|
||
Public guides use a shared Redis-backed Next.js cache. Admin routes remain
|
||
dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so
|
||
clients reconnect. Traefik buffering is disabled by the response headers.
|
||
|
||
Media objects remain private in S3. `/media/[id]` serves an object publicly only
|
||
while a published guide references it; otherwise administrator authorization is
|
||
required.
|
||
|
||
## Verification
|
||
|
||
Run the complete host-safe verification set with:
|
||
|
||
```bash
|
||
bun install --frozen-lockfile
|
||
bun run test
|
||
bun run typecheck
|
||
bun run lint
|
||
kubectl kustomize k8s/ >/dev/null
|
||
```
|
||
|
||
The formula suite validates the exact sanitized corpus of 379 extracted formula
|
||
cells plus arithmetic edge cases. An optional Redis integration test uses
|
||
independent connections for two conceptual replicas:
|
||
|
||
```bash
|
||
REDIS_INTEGRATION_URL=redis://127.0.0.1:6379 \
|
||
bun run test -- tests/redis-replication.integration.test.ts
|
||
```
|
||
|
||
It uses unique key and channel prefixes and removes its cache state afterward.
|
||
Without `REDIS_INTEGRATION_URL`, that external-service test is skipped.
|
||
|
||
## Containers
|
||
|
||
The application image compiles Next.js in a Node builder stage, then runs the
|
||
standalone server and bundled workers on Bun as the unprivileged numeric
|
||
UID/GID `1000:1000`. The migration image runs the committed Drizzle migrations
|
||
with the same identity.
|
||
|
||
```bash
|
||
docker build \
|
||
--target app \
|
||
--build-arg BASE_URL=https://guide.sudloh.com \
|
||
--build-arg NEXT_DEPLOYMENT_ID=<immutable-revision> \
|
||
--build-arg VCS_REF=<immutable-revision> \
|
||
--tag buzz-sheet:<immutable-revision> \
|
||
.
|
||
|
||
docker build \
|
||
--target migration \
|
||
--build-arg VCS_REF=<immutable-revision> \
|
||
--tag buzz-sheet:migrate-<immutable-revision> \
|
||
.
|
||
```
|
||
|
||
The runtime images do not contain local `.env` files.
|
||
`BASE_URL` is required during the Next.js build and at runtime. CI supplies the
|
||
build argument, and the Kubernetes ConfigMap supplies the runtime value. Keep
|
||
both values aligned when changing the public site origin.
|
||
|
||
## Kubernetes
|
||
|
||
Kustomize resources live in `k8s/` and define:
|
||
|
||
- Namespace `buzz-sheet`.
|
||
- Two rolling web replicas, one outbox worker, and one Discord catalog worker.
|
||
- ClusterIP port 3000 and Traefik ingress for `guide.sudloh.com`.
|
||
- Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route.
|
||
- Startup, liveness, and dependency-aware readiness probes.
|
||
- Web requests of 500m CPU/1 GiB and limits of 1 CPU/2 GiB.
|
||
- HPA from 2 to 6 web replicas at 70% CPU or 75% memory and a PDB with one available.
|
||
- Non-root, read-only containers with dropped capabilities and seccomp.
|
||
- ARM64 scheduling constraints matching the production hosts and images.
|
||
- A versioned migration Job and namespace-scoped CI deployer permissions.
|
||
|
||
PostgreSQL, Redis, and S3 are external services. The manifests deliberately do
|
||
not create in-cluster data stores or credentials.
|
||
|
||
### External prerequisites
|
||
|
||
Before the first rollout, a cluster administrator must provision a
|
||
`buzz-sheet-env` Secret in the `buzz-sheet` namespace containing:
|
||
|
||
```text
|
||
DATABASE_URL
|
||
BETTER_AUTH_SECRET
|
||
REDIS_URL
|
||
CATALOG_SYNC_CONCURRENCY
|
||
S3_ENDPOINT
|
||
S3_PUBLIC_URL
|
||
S3_BUCKET
|
||
S3_ACCESS_KEY_ID
|
||
S3_SECRET_ACCESS_KEY
|
||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||
```
|
||
|
||
`NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` must be generated once and remain stable
|
||
across replicas and rolling deployments. Do not place secret values in the
|
||
ConfigMap or commit them to this repository.
|
||
|
||
Every page also subscribes to `/api/active`. When a serving pod reports a newer
|
||
deployment ID than the browser's current build, a persistent Thai notification
|
||
offers a full reload into the new release.
|
||
|
||
The base manifests contain deliberate `replace-me` image and deployment values.
|
||
For initial bootstrap, the platform operator applies `k8s/base` through an
|
||
environment overlay that supplies an existing immutable image revision. The
|
||
aggregate `k8s/` kustomization includes the migration resource for rendering and
|
||
validation; normal releases create their own revision-named migration Job.
|
||
|
||
Use this command for local manifest validation only:
|
||
|
||
```bash
|
||
kubectl kustomize k8s/ >/dev/null
|
||
```
|
||
|
||
### Gitea Actions release
|
||
|
||
`.gitea/workflows/ci.yml` verifies every push and pull request. After the
|
||
repository variable `DEPLOY_ENABLED` is explicitly set to `true`, a successful
|
||
push to `main` builds multi-architecture images at:
|
||
|
||
```text
|
||
registry.neko-piranha.ts.net/astral/buzz-sheet:<git-sha>
|
||
registry.neko-piranha.ts.net/astral/buzz-sheet:migrate-<git-sha>
|
||
```
|
||
|
||
Configure these Gitea secrets later:
|
||
|
||
| Secret | Purpose |
|
||
| --- | --- |
|
||
| `KUBE_CONFIG_B64` | Base64 kubeconfig for the `ci-deployer` identity |
|
||
| `REDIS_INTEGRATION_URL` | Optional isolated Redis endpoint for CI integration coverage |
|
||
|
||
The deploy job creates a revision-named migration Job, waits for completion,
|
||
updates `NEXT_DEPLOYMENT_ID`, and only then rolls out the immutable application
|
||
image to the web and both worker deployments. Schema changes must follow an
|
||
expand/contract sequence so the previous application revision stays compatible
|
||
during a rolling update.
|
||
|
||
## Acknowledgements
|
||
|
||
This project would not be possible without the open-source software and
|
||
community services it builds on:
|
||
|
||
- [Lunaris](https://lunaris.moe/) — character, talent, material, and game data
|
||
used by catalog synchronization and guide tools. Buzz Sheet links back to
|
||
Lunaris for material references where appropriate.
|
||
- [Next.js](https://nextjs.org/) — application framework.
|
||
- [React](https://react.dev/) — user interface library.
|
||
- [Bun](https://bun.sh/) — JavaScript runtime and package manager.
|
||
- [PostgreSQL](https://www.postgresql.org/) — primary database.
|
||
- [Redis](https://redis.io/) — cache, event transport, and presence state.
|
||
- [Discord.js](https://discord.js.org/) — Discord catalog update worker.
|
||
- [Drizzle ORM](https://orm.drizzle.team/) — database schema and migrations.
|
||
- [shadcn/ui](https://ui.shadcn.com/), [Tailwind CSS](https://tailwindcss.com/),
|
||
[Lucide](https://lucide.dev/), and [dnd-kit](https://dndkit.com/) — interface
|
||
components, styling, icons, and drag-and-drop interactions.
|
||
|
||
Special thanks are also available on the site’s [Special Thanks page](https://guide.sudloh.com/thanks).
|
||
|
||
Genshin Impact and its characters, names, and assets are property of their
|
||
respective rights holders. Please review the applicable terms and licenses for
|
||
each third-party dependency before redistributing or deploying this project.
|