Files
buzz-sheet/README.md
T
gunshiz 89be58c517
CI / Verify (push) Successful in 1m29s
CI / Build immutable images and deploy (push) Successful in 1m48s
perf : better concurrent handle
2026-09-10 19:08:25 +07:00

271 lines
10 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. Authors
work in a visual block editor; visitors get readable guides rather than a
spreadsheet interface.
Production target: `https://guide.sudloh.com`
## Included capabilities
- Searchable character directory with element and role filters.
- Unlimited character pages with custom slugs, ordering, notes, and visibility.
- Visual admin workspace with inline editing, inspector, preview, keyboard and
pointer reordering, 750 ms autosave, conflict recovery, and revisions.
- Six reusable templates covering classic guides, rotations, one-page builds,
multi-role builds, team databases, and calculation-backed comparisons.
- Validated block schemas with migrations and a safe fallback for unknown
future block types.
- Restricted decimal formula engine with named references, dependency ordering,
cycle detection, typed failures, and presentation-only rounding.
- Read-only comparison charts with visible values and accessible table fallbacks.
- Better Auth email/password login with administrator-managed accounts.
- Private S3-compatible media uploads with reference-aware same-origin delivery.
- PostgreSQL transactions, immutable data-source versions, revisions, 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. 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 db:seed
bun run dev
```
The seed command installs the six starter built-in templates; it does not publish
reference workbook content.
### 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 db:seed` | Upsert the built-in templates |
| `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 content, immutable data-source
versions, revision snapshots, media references, 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 Lunaris `Version Change` announcements and
then fetches and synchronizes the latest version reported by Lunaris. On startup
it checks recent channel history, so it does not need to poll Lunaris while idle.
Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord
Message Content intent and grant the bot View Channel, Read Message History, and
Send Messages in the configured channels.
Public snapshots 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 visible current snapshot references it; otherwise administrator
authorization is required. Retained revisions keep their referenced objects
alive.
Formula fields accept arithmetic only: `+`, `-`, `*`, `/`, parentheses,
percentages, unary signs, and stable field references. They cannot execute HTML,
JavaScript, React, CSS, external scripts, packages, or network calls.
## 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 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.
## 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.