diff --git a/README.md b/README.md index 48a9317..726810e 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,109 @@ # Buzz Guide -Buzz Guide is a Thai-first guide CMS for turning structured character, build, -team, rotation, and comparison data into responsive game-guide pages. +A Thai-first Genshin Impact guide CMS for publishing character builds, teams, +rotations, and comparisons from structured data. -Production target: `https://guide.sudloh.com` +**Live site:** [guide.sudloh.com](https://guide.sudloh.com) -This project is an independent community tool for Genshin Impact players. It is -not affiliated with or endorsed by HoYoverse. +Buzz Guide is an independent community project and is not affiliated with or +endorsed by HoYoverse. The repository and deployment resources retain the +`buzz-sheet` name. -## Included capabilities +## Features -- 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, registration email OTP, profile email changes, - and administrator-managed accounts. -- S3-compatible media uploads with reference-aware same-origin delivery for - staged private objects. -- 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. +- **Public guides:** a searchable character directory and responsive guide pages. +- **Visual editing:** structured editors for overviews, weapons, artifacts, + constellations, teams, and custom sections, with autosave and conflict recovery. +- **Accounts:** email/password authentication through Better Auth, registration + email OTP, profile email changes, and administrator-managed accounts. +- **Media:** S3-compatible uploads and publication-aware delivery for staged files. +- **Catalog updates:** Discord-triggered synchronization with Lunaris. +- **Commissions:** PromptPay checkout, Slip2Go verification, ticket attachments, + and opt-in browser push notifications. +- **Shared state:** PostgreSQL transactions, a retryable outbox, Redis caching, + and Server-Sent Events across application replicas. -The five reference workbooks are represented only by sanitized structural and +The five reference workbooks are included only as sanitized structural and formula fixtures. Their guide text and media are not imported or published. +## Contents + +- [Local development](#local-development) +- [Commands](#commands) +- [Routes](#routes) +- [How updates and uploads work](#how-updates-and-uploads-work) +- [Verification](#verification) +- [Containers](#containers) +- [Kubernetes](#kubernetes) +- [Acknowledgements](#acknowledgements) + +## Local development + +### Requirements + +- Bun **1.3.14** +- PostgreSQL +- Redis +- S3-compatible object storage + +### Setup + +```bash +git clone https://git.astrxl.dev/astral/buzz-sheet.git +cd buzz-sheet +bun install --frozen-lockfile +cp .env.example .env +``` + +Fill in `.env` using the placeholders in `.env.example`. Keep credentials out of +Git; `.env` is ignored by the repository. + +| Setting | Purpose | +| --- | --- | +| `BETTER_AUTH_URL` | Must exactly match the application's origin. | +| `BASE_URL` | Public origin used for SEO metadata, the sitemap, and robots.txt. | +| Database, Redis, and S3 settings | Connection details for the required services. | + +Apply the committed database migrations, then start the development server: + +```bash +bun run db:migrate +bun run dev +``` + +Visitors can register at `/auth/register`. For background processing, run the +outbox worker in a separate terminal. Run the Discord worker when using catalog +updates; its configuration is described below. + +```bash +bun run worker:outbox +``` + +```bash +bun run worker:discord +``` + +Email, commission payments, and push notifications need their corresponding +service credentials. See [External prerequisites](#external-prerequisites) for +the production configuration details. + +## 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 security:audit` | Check dependency advisories and time-limited exceptions | +| `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 | + ## Routes | Route | Purpose | @@ -47,79 +126,45 @@ The Discord check requires a heartbeat from the connected catalog worker within A page is public only when both its page and character are visible. A visible character without a visible page returns 404. -## Local development +## How updates and uploads work -Requirements: +### Editing and live updates -- Bun 1.3.14 -- PostgreSQL -- Redis -- S3-compatible object storage +Edits carry an `expectedVersion`. Accepted changes save guide content, media +reference counts, and outbox records in a single PostgreSQL transaction. -Install dependencies and create your local environment file: +The outbox worker invalidates the affected Redis cache tags before publishing +an event containing only `{ type, id, version }`. Clients then fetch the current +state. SSE carries change notifications, never guide content; PostgreSQL remains +the source of truth. -```bash -bun install --frozen-lockfile -cp .env.example .env -``` +Public guides use a shared Redis-backed Next.js cache. Admin routes stay dynamic. +SSE streams send a heartbeat every 90 seconds and close after 30 minutes so +clients reconnect. Response headers disable Traefik buffering. -`.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. Visitors can create public accounts at `/auth/register`. +### Discord catalog synchronization -Prepare the database and start the application: +The catalog worker watches the private channel set by `DISCORD_CHANNEL_ID`. +Every new message, including one sent by you, triggers a check and synchronization +with the latest version reported by Lunaris. -```bash -bun run db:migrate -bun run dev -``` +- Startup connects the listener without replaying history or starting a sync. +- Results go to `DISCORD_LOG_CHANNEL_ID`; start notifications link to the + triggering message. +- Send `!lunaris stop` in the watched channel to cancel the current fetch and + clear queued fetches. A later message starts another check. +- Enable the Discord Message Content intent and grant the bot View Channel and + Send Messages in the configured channels. -## Commands +### Media visibility -| Command | Purpose | +| Upload path | Visibility | | --- | --- | -| `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 security:audit` | Check dependency advisories and time-limited exceptions | -| `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 | +| Presigned staged upload | Private in S3. `/media/[id]` allows public access only while a published guide references the object; otherwise administrator authorization is required. | +| Direct guide image upload | Public CDN object, including uploads made for drafts. | +| Commission attachment | Served from `S3_PUBLIC_URL`; anyone with the URL can view it. | -## 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`. Every new message in that channel, including messages you send yourself, -triggers a check and synchronization of 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`; start notifications -include a clickable link to the message that triggered the check. Send -`!lunaris stop` in the configured channel to cancel the active fetch and clear -queued fetches; a later message starts a new check. 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. - -Presigned staged uploads remain private in S3. `/media/[id]` serves a staged -object publicly only while a published guide references it; otherwise -administrator authorization is required. The direct upload path writes public -CDN objects for guide images, so draft uploads on that path must not contain -sensitive material. +Keep sensitive material out of the direct upload path and commission attachments. ## Verification @@ -195,6 +240,8 @@ not create in-cluster data stores or credentials. ### External prerequisites +#### Application secrets + Before the first rollout, a cluster administrator must provision a `buzz-sheet-env` Secret in the `buzz-sheet` namespace containing: @@ -227,6 +274,8 @@ DISCORD_CHANNEL_ID across replicas and rolling deployments. Do not place secret values in the ConfigMap or commit them to this repository. +#### Email verification + Before deploying required email verification, add `sudloh.com` to Resend and publish the DNS records shown in its domain setup. Verify the domain, create a sending-only API key restricted to it, and set `RESEND_API_KEY` in the external @@ -235,6 +284,8 @@ Secret. Account verification and password reset emails are sent from sessions continue until they expire or the user signs out, after which an unverified address must be verified before signing in again. +#### Commission payments and attachments + The commission page accepts PromptPay mobile, national ID, or e-wallet identifiers. For `COMMISSION_PROMPTPAY_TYPE=mobile`, use a real 10-digit Thai mobile number registered with PromptPay for receiving transfers in `COMMISSION_PROMPTPAY_VALUE`. @@ -250,6 +301,8 @@ view it. Each checkout accepts payment for one hour after creation. The outbox worker removes expired unpaid checkouts; paid tickets remain available. Apply the commission database migration before enabling checkout. +#### Browser push notifications + Commission message notifications require a stable VAPID key pair and subject (`bunx web-push generate-vapid-keys --json`). Set the three `WEB_PUSH_*` values in the web deployment secret and apply the push-subscription migration before @@ -257,16 +310,22 @@ enabling notifications. Users opt in from a prompt when entering the commission arrive after tabs close; on iPhone and iPad, the site must be added to the Home Screen before the browser offers push permission. +#### Ingress and client IP handling + Traefik must overwrite `X-Real-Ip` for every request, and the web pods must be reachable only through Traefik before enabling the configured client IP based rate limits. The NetworkPolicy assumes Traefik runs in `kube-system` with the `app.kubernetes.io/name=traefik` pod label; verify those labels in the target cluster before applying it. +#### Deployment notifications + 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. +#### Initial bootstrap + 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 @@ -292,7 +351,7 @@ registry.neko-piranha.ts.net/astral/buzz-sheet: registry.neko-piranha.ts.net/astral/buzz-sheet:migrate- ``` -Configure these Gitea secrets later: +Configure the following Gitea secret before enabling deployment: | Secret | Purpose | | --- | --- |