add claude skills for database, development, and features
Build and Push Image / docker (push) Successful in 7m6s

This commit is contained in:
Dreamgineer
2026-05-25 08:09:40 +07:00 Unverified
parent 540c38167f
commit a98f14d1fb
3 changed files with 750 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
---
name: buzz-database
description: Drizzle ORM workflows for the Buzz PostgreSQL database
metadata:
author: dmgnr
version: 1.0.0
---
# Buzz — Database Workflows (Drizzle ORM)
This skill covers working with the PostgreSQL database via Drizzle ORM.
## Schema Overview
All tables are defined in `lib/db/schema.ts`. The database uses **three named schemas** + the public schema:
### Public schema
| Table | Purpose |
|---|---|
| `cdn` | File storage (bytea), used for images, cards, slips |
| `auditLog` | Admin action audit trail |
| `characters` | Genshin characters (linked to versions, CDN images) |
| `versions` | Game version tracking (self-referencing FK for `from`) |
| `settings` | Global app settings (singleton row, PK is boolean `true`) |
| `guides` | Character build guide links |
| `user`, `session`, `account`, `verification` | Better-auth tables |
### `artifact` schema
| Table | Purpose |
|---|---|
| `submissions` | Artifact review queue submissions |
| `cards` | Generated character cards for submissions |
| `settings` | Artifact-specific settings (locked, limit) |
### `endgame` schema
| Table | Purpose |
|---|---|
| `submissions` | Rubgram (endgame) queue submissions |
| `sarchive` | Archived completed submissions |
| `expired` | Expired unpaid submissions |
| `slips` | SlipOK payment slip images and data |
| `settings` | Rubgram-specific settings |
| `discord` | Discord user mapping |
| `types` | Service types with pricing |
### `tierlist` schema
| Table | Purpose |
|---|---|
| `types` | Tierlist categories (Spiral Abyss, Stygian Onslaught) |
| `tiers` | Tier definitions (S, A, B, etc.) |
| `columns` | Column layout definitions |
| `badges` | Badge icons for tiers |
| `versions` | Tierlist versions per type |
| `states` | Character placements per version (JSONB badges) |
### Key schema patterns
**UUIDv7 primary keys:**
```ts
import { uuidv7 } from "uuidv7";
id: uuid("id").primaryKey().$defaultFn(uuidv7),
```
**Named schemas with `pgSchema`:**
```ts
import { pgSchema } from "drizzle-orm/pg-core";
const artifact = pgSchema("artifact");
export const submissions = artifact.table("submissions", { ... });
```
**Custom `bytea` column** (for binary data like images):
```ts
import { bytea } from "$/db/custom";
// used in: cdn, cards, slips tables
```
**Generated columns:**
```ts
paid: boolean().generatedAlwaysAs(
sql`(price <= (0)::numeric) OR (slip IS NOT NULL)`
),
```
**Serial sequences** for queue numbering per schema:
```ts
queue: integer().notNull().default(sql`(nextval('artifact.submissions_queue_seq'::regclass))`),
```
**Enums:**
```ts
export const characterElement = pgEnum("character_element", [
"anemo", "geo", "dendro", "hydro", "pyro", "cryo", "electro",
]);
```
**Foreign keys with cascade:**
```ts
char: uuid("char").notNull().references(() => characters.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
```
## Migration Workflow
### Important: database boundary
Schema changes must be pushed to the database **before** the new code deploys. Otherwise the running app will crash on startup when it queries tables or columns that don't exist yet.
| Environment | Command | Timing |
|---|---|---|
| **Dev** | `bun ds dr push` | Run inside app container (via `bun ds`) against dev DB |
| **Production** | `bun dr push` | Run locally with production env vars against prod DB. **Must run before `git push`.** |
### Full migration workflow
1. **Edit schema** in `lib/db/schema.ts`
2. **Generate migration files** (optional, for tracking):
```bash
bun dr generate
```
3. **Push to dev** to verify:
```bash
bun ds dr push
```
4. **If using migrate instead of push** for production:
```bash
bun dr migrate
```
5. **Push to production DB** (with production credentials):
```bash
bun dr push
```
6. **Deploy code** via `git push` (Gitea Actions handles the rest)
When using `bun dr push`, Drizzle automatically detects differences between the schema and the database, and applies the necessary ALTER statements. For production, always verify the SQL Drizzle generates first.
### Drizzle Studio
Accessible at `http://localhost:4983` when dev containers are running. The `drizzle` service in `dev/compose.yml` provides this:
```bash
bun ds bun dr studio --host $(hostname)
```
## Common Schema Operations
### Adding a column
```ts
// In the table definition, add:
newColumn: varchar("new_column", { length: 255 }).default(""),
```
Generate + push.
### Adding a new table
```ts
export const myTable = pgSchema("myschema").table("my_table", {
id: uuid("id").primaryKey().$defaultFn(uuidv7),
name: varchar("name", { length: 255 }).notNull(),
// ...
});
```
Export and use in queries. Generate + push.
### Adding a new enum
```ts
export const myEnum = pgEnum("my_enum", ["value1", "value2"]);
export const myTable = pgSchema("myschema").table("my_table", {
status: myEnum("status").default("value1"),
});
```
### Adding foreign key
```ts
refId: uuid("ref_id")
.notNull()
.references(() => otherTable.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
```
## Query patterns
### Basic select
```ts
import { db } from "$/db";
import { submissions } from "$/db/schema";
import { eq } from "drizzle-orm";
const items = await db.select().from(submissions).where(eq(submissions.checked, false));
```
### Select with join
```ts
const result = await db
.select({
name: characters.name,
queue: submissions.queue,
})
.from(submissions)
.innerJoin(characters, eq(submissions.char, characters.id))
.orderBy(submissions.queue);
```
### Insert
```ts
const [inserted] = await db
.insert(submissions)
.values({ name: "Test", uid: "123456789", char: charId })
.returning({ id: submissions.id, queue: submissions.queue });
```
### Update
```ts
await db
.update(submissions)
.set({ checked: true })
.where(eq(submissions.id, id));
```
### Delete with cascade check (CDN)
```ts
// Use checkCdnRefs() from $api before deleting CDN files
```
### Raw SQL
```ts
import { sql } from "drizzle-orm";
await db.execute(sql`ALTER SEQUENCE artifact.submissions_queue_seq RESTART WITH 1`);
```
### Transactions
```ts
await db.transaction(async (tx) => {
await tx.insert(...);
await tx.update(...);
});
```
### Counting
```ts
const count = await db.$count(submissions);
```
+167
View File
@@ -0,0 +1,167 @@
---
name: buzz-development
description: Buzz development environment setup, maintenance, and BrowserMCP
metadata:
author: dmgnr
version: 1.0.0
---
# Buzz — Development Environment & Maintenance
This skill covers the local dev environment, common maintenance tasks, and BrowserMCP setup.
## Prerequisites
- **Bun** (matching the project's Bun version in Docker)
- **Docker** with Compose Watch support
- Environment file at `dev/.env.development`
## Dev Environment
### Starting development
```bash
bun dev
```
This runs `docker compose up --watch` from `dev/compose.yml`, spinning up:
| Container | Purpose | Notes |
|---|---|---|
| `app` | Next.js dev server (port 3000) | File sync via Docker watch, autoreloads |
| `backend` | Background worker (card caching, cron, webhooks) | Rebuilds on `backend/` changes |
| `db` | PostgreSQL (port 5432) | Auto-creates on first run |
| `redis` | Redis (port 6379) | Pub-sub for SSE, persistence enabled |
| `redis-commander` | Redis GUI (port 6380) | Optional, for debugging |
| `drizzle` | Drizzle Studio (port 4983) | DB management UI |
The `bun dev` command runs **foreground only**. Press Ctrl+C to stop all containers.
### Attaching to the app container
```bash
bun ds
```
Short for "docker exec -it app bun". Run one-off commands inside the app container:
```bash
bun ds bun nextdev # start next dev manually
bun ds bun dr push # push schema to dev database
bun ds bun dr studio # start drizzle studio
bun ds bun lint # lint inside container
```
### Default dev credentials
Seeded automatically by the backend on first startup when `ENVIRONMENT=development`:
| Field | Value |
|---|---|
| Email | `[email protected]` |
| Password | `youshallnotpass` |
| Role | `admin` |
Seeding is idempotent — it skips if an admin account already exists.
## Command Reference
| Command | What it does |
|---|---|
| `bun dev` | Start all dev containers with file watch |
| `bun ds <cmd>` | Run a command in the app container |
| `bun nextdev` | Start Next.js dev server directly (outside Docker) |
| `bun build` | Build for production (Turbopack) |
| `bun start` | Start production server |
| `bun lint` | Run Biome check with auto-fix |
| `bun format` | Run Biome formatter |
| `bun dr <args>` | Run drizzle-kit commands |
| `bun logs` | Tail logs from all containers (`docker compose logs -fn50`) |
| `bun test` | Run Playwright tests (outdated — unreliable) |
## Common Maintenance Tasks
### Checking logs
```bash
bun logs # all services
docker compose -f dev/compose.yml logs -fn50 app # just the app
docker compose -f dev/compose.yml logs -fn50 backend
```
### Restarting a service
```bash
docker compose -f dev/compose.yml restart app
docker compose -f dev/compose.yml restart backend
```
### Applying linting and formatting
```bash
bun lint # biome check --fix
bun format # biome format --write
```
### Running Drizzle Studio
```bash
bun ds bun dr studio --host $(hostname) # via container
```
Then visit `http://localhost:4983`.
### Database migrations
See the **buzz-database** skill for the full workflow.
### Updating dependencies
```bash
bun ds bun install <package>
```
After adding new deps, you may need to rebuild the Docker images.
## BrowserMCP Setup
Before doing any UI/interaction work, you **must** set up BrowserMCP for automated browser testing.
### Flow
1. The agent will ask you: *"Do you want to set up BrowserMCP for automated browser testing?"*
2. If you agree, the agent will:
a. Install the BrowserMCP package:
```bash
bun add -d @anthropic/browser-use
```
b. Add the MCP server configuration to `opencode.json` (or create/edit `.opencode.jsonc`):
```jsonc
{
"mcpServers": {
"browser": {
"command": "bunx",
"args": ["@anthropic/browser-use"]
}
}
}
```
c. Launch the browser and connect to `http://localhost:3000`
3. The agent will then perform browser-based testing/verification using the MCP browser tools.
### When to use BrowserMCP
- Verifying UI changes after implementing a feature
- Testing form submissions, login flows, or admin interactions
- Checking that SSE live updates work correctly
- Validating responsive layout and navigation
### Important
- **Do not** run Playwright tests — they are heavily outdated and unreliable.
- Always use BrowserMCP for visual/interaction verification instead.
- The dev server must already be running for BrowserMCP to connect.
+325
View File
@@ -0,0 +1,325 @@
---
name: buzz-feature
description: Full workflow for adding new features to Buzz Events
metadata:
author: dmgnr
version: 1.0.0
---
# Buzz — Adding & Modifying Features
This skill covers the full workflow for adding new features — from routing and server actions to admin panels and real-time updates.
## Project Conventions
### Path aliases
| Alias | Maps to |
|---|---|
| `@/*` | `./*` (project root) |
| `$/*` | `./lib/*` |
| `#/*` | `./public/*` |
### Tech choices
| Concern | Choice |
|---|---|
| Styling | Tailwind CSS v4, shadcn/ui (Radix primitives), `cn()` from `@/lib/utils` |
| Forms | Custom `FormProvider`/`FormInput`/`FormAction` system with autosave |
| Data fetching | Server components (async, direct `db` calls) + React `Suspense` boundaries |
| Mutations | Server actions (`"use server"`) with Zod validation |
| Real-time | Redis pub-sub → SSE via `EventSourceEndpoint` |
| Auth | `better-auth` — server `adminCheck()`, client `createAuthClient()` |
| State | React context, ICC (`IccProvider`/`shared.state`), no global state lib |
| Icons | `lucide-react` |
### Key principles
- **Client/server split**: Pages are async server components; wrap interactive parts in client components. Use `"use client"` only where needed (event handlers, state, effects).
- **Server actions** in `lib/api.ts` (cross-feature) or per-feature `api.ts` — never inline in page files.
- **useEffect** is ONLY for synchronizing with external systems (non-React widgets, browser APIs, network subscriptions). Do NOT use for:
- Data transformation — compute during render instead
- User events — put logic in the event handler
- Chaining state updates — calculate all related updates together
- Notifying parents — call the callback alongside setState
- Resetting state on prop change — use the prop as the component's `key` instead
- **SSE events**: Use the typed `sse`/`tlSse()` endpoints — never publish raw Redis messages.
- **Admin actions** always call `adminCheck()` first and log via `actionLog()`.
- **OG images**: Dynamic `ImageResponse` in `opengraph-image.ts` files per route.
## Adding a Public Feature Page
### 1. Create route
```
app/(ui)/<feature>/
page.tsx # Server component — fetch data, render layout
client.tsx # Client component(s) — interactivity
form.tsx # Form wrapper (server action submission)
api.ts # Server actions (if per-feature)
rules.tsx # Rules/help dialog (if needed)
opengraph-image.ts # Dynamic OG image (if needed)
```
**Pattern for `page.tsx`:**
```tsx
import { Suspense } from "react";
import { FeatureClient } from "./client";
export default function FeaturePage() {
return (
<Suspense fallback={<Loading />}>
<FeatureClient />
</Suspense>
);
}
```
### 2. Add server actions
For cross-feature actions, add to `lib/api.ts`. For per-feature actions, create `app/(ui)/<feature>/api.ts`:
```ts
"use server";
import { revalidatePath } from "next/cache";
import { adminCheck } from "$/auth";
import { db } from "$/db";
import { actionLog } from "$/api";
export async function submitSomething(formData: FormData) {
// validation with zod
// mutate db
// revalidatePath(...)
// publish SSE event
// return result
}
```
### 3. Create form (using form system)
Use `FormProvider`/`FormInput`/`FormAction` from `@/components/form`:
```tsx
"use client";
import { FormProvider, FormInput, FormAction } from "@/components/form";
export function FeatureForm() {
return (
<FormProvider id="feature-form">
<FormInput name="name" label="Name" required />
<FormAction action={submitSomething}>Submit</FormAction>
</FormProvider>
);
}
```
The form system auto-saves to localStorage (keyed by `id`, 10-min TTL) and restores on remount.
### 4. Add live SSE updates
Define events in `lib/db/sse-endpoints.ts`:
```ts
export const sse = sseEndpointMap({
...existing,
myFeature: {
update: z.object({ type: z.enum(["action1", "action2"]) }),
},
});
```
Publish in your server action:
```ts
sse.myFeature.pub("update", { type: "action1" });
```
Subscribe on the client:
```ts
useEffect(() => {
const { clean } = sse.myFeature.sub("update", (data) => {
// handle update
});
return clean;
}, []);
```
For dynamic topics (e.g. per-tierlist), use `tlSse(listId)`:
```ts
// Server
tlSse(listId).pub("update_states", states);
// Client — use EventSourceEndpoint directly:
const endpoint = sseEndpoint(`tl.${listId}`, {
update_states: z.custom<(typeof tierlistStates.$inferSelect)[]>(),
});
endpoint.sub("update_states", (data) => { ... });
```
SSE endpoints are served via `app/sse/[topic]/route.ts`.
### 5. Add OG image
Create `opengraph-image.ts` using `@/lib/og`:
```tsx
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export default function OGImage() {
return new ImageResponse(
<div style={{ /* ... */ }}>Buzz Feature</div>,
{ ...size },
);
}
```
## Adding an Admin Feature Page
### 1. Route structure
```
app/(ui)/admin/
layout.tsx # Sidebar layout — already includes auth check
page.tsx # Dashboard
<feature>/
page.tsx # List/manage page
client.tsx # Client components
api.ts # Server actions
@modal/
<feature>/
[id]/page.tsx # Edit modal
create/page.tsx # Create modal
default.tsx # Returns empty div when no modal active
```
### 2. Layout already handles auth
`app/(ui)/admin/layout.tsx` checks `adminCheck()` and redirects to `/login` if unauthenticated. It also renders `{modal}` for parallel route modals.
### 3. Add sidebar navigation
In `app/(ui)/admin/layout.tsx`, add a `SidebarMenuItem`:
```tsx
<SidebarMenuItem>
<SidebarLink href="/admin/<feature>">
<IconComponent />
Feature Name
</SidebarLink>
</SidebarMenuItem>
```
### 4. List/manage page
```tsx
// app/(ui)/admin/<feature>/page.tsx
import { db } from "$/db";
import { FeatureClient } from "./client";
export default async function AdminFeaturePage() {
const items = await db.select().from(featureTable).orderBy(featureTable.order);
return <FeatureClient items={items} />;
}
```
### 5. Parallel route modal for create/edit
Create modal (`@modal/<feature>/create/page.tsx`):
```tsx
import { ModalBase } from "@/components/modal";
import { CreateForm } from "./overrides";
export default function CreateModal() {
return (
<ModalBase title="Create Item">
<CreateForm />
</ModalBase>
);
}
```
The `ModalBase` component wraps a `Dialog` that closes via `router.back()`.
For editing, use `[id]/page.tsx` with the same pattern, fetching the item by ID server-side.
The parallel route renders inside `{modal}` in the admin layout. `@modal/default.tsx` returns an empty div when no modal route is matched.
### 6. Server actions with admin check
```ts
export async function updateItem(id: string, data: FormData) {
if (!(await adminCheck())) throw "Unauthorized";
// validate with zod
// update db
revalidatePath("/admin/<feature>");
await actionLog(`Updated item ${id}`, data);
// publish SSE if needed
}
```
### 7. Audit logging
Every admin mutation must call `actionLog(text, details?)` from `$api`:
```ts
await actionLog("Description of what happened", optionalDetails);
```
This inserts into the `auditLog` table, revalidates the log page, and publishes an SSE update for the live log view.
### 8. CDN assets
For features that need image/file uploads:
- Use `CdnChooserProvider` + CDN chooser component from `@/components/chooser`
- Upload via `cdnify()` from `$api` (returns the CDN record ID)
- Reference CDN IDs in your DB schema as foreign keys to `cdn` table
- Check references before deletion via `checkCdnRefs()` from `$api`
- CDN references are tracked automatically in the `cdn_references` view
### 9. TanStack Table for data display
For list views with sorting/selection/context menus:
```tsx
import { DataTable } from "@/components/tantable";
import { columns } from "./columns";
```
The `DataTable` component supports row selection, context menus, empty states, and search.
## API Routes (non-server-action endpoints)
Create under `app/api/<feature>/route.ts`:
```ts
import { NextResponse } from "next/server";
export async function GET(req: Request) {
// ...
return NextResponse.json(data);
}
```
Patterns used in this project:
- **Proxy routes**: `/api/enka/[uid]`, `/api/amber/char` — forward to external APIs
- **Count routes**: `/api/artifact/count`, `/api/rubgram/count` — quick aggregate queries
- **Health check**: `/api/health` — returns status of DB, Enka, Amber, Redis
- **State endpoints**: `/api/tl/[ver]/states` — REST-like read model for client
## Working with the form system
The form system (`@/components/form.tsx`) provides:
- **`FormProvider`**: Creates form context with autosave. Accepts `id` (storage key), optional `defaultValues`, and optional `clean` callback.
- **`FormInput`**: Renders a labeled input bound to the form context. Supports `name`, `label`, `required`, `type`, `placeholder`, and `children` (for custom input elements).
- **`FormAction`**: A submit button that calls the server action, handles loading state, shows toast on success/error, and optionally closes a dialog.
- **`FormRow`**: Wraps fields in a horizontal or vertical layout row.
```tsx
<FormProvider id="my-form" clean={() => { /* on success */ }}>
<FormInput name="title" label="Title" required />
<FormInput name="description" label="Description" />
<FormAction action={submitAction}>Save</FormAction>
</FormProvider>
```
Typed forms (for complex data shapes) should define a `TypedFormData` type in a `type.d.ts` file within the feature directory.