Refresh repository documentation
Some checks failed
Deploy to Production / test (push) Failing after 33s
Some checks failed
Deploy to Production / test (push) Failing after 33s
- Rewrite AGENTS and README files to match the current app layout - Document API routes, trivia UI, and the active panel design language
This commit is contained in:
262
AGENTS.md
262
AGENTS.md
@@ -1,211 +1,135 @@
|
|||||||
# CLAUDE.md
|
# AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file documents the current implementation shape of the Aurora repository.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
# App
|
||||||
bun --watch bot/index.ts # Run bot + API with hot reload
|
bun run dev # bot + API in one Bun process with watch mode
|
||||||
docker compose up # Start all services (bot, API, database)
|
docker compose up # app + db
|
||||||
docker compose up app # Start just the app (bot + API)
|
docker compose up app # app only
|
||||||
docker compose up db # Start just the database
|
docker compose up db # database only
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
bun test # Run all tests
|
bun test # Bun's native runner
|
||||||
bun test path/to/file.test.ts # Run a single test file
|
bun run test # repo test wrapper script
|
||||||
bun test shared/modules/economy # Run tests in a directory
|
bun run test:ci # include CI/integration path
|
||||||
bun test --watch # Watch mode
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
bun run db:push:local # Push schema changes (local)
|
bun run db:push # drizzle-kit push via Docker
|
||||||
bun run db:studio # Open Drizzle Studio (localhost:4983)
|
bun run db:push:local # drizzle-kit push locally
|
||||||
bun run generate # Generate Drizzle migrations (Docker)
|
bun run db:generate # drizzle-kit generate via Docker
|
||||||
bun run migrate # Apply migrations (Docker)
|
bun run db:migrate # drizzle-kit migrate via Docker
|
||||||
|
bun run db:studio # local Drizzle Studio on :4983
|
||||||
|
|
||||||
# Admin Panel
|
# Panel
|
||||||
bun run panel:dev # Start Vite dev server for dashboard
|
bun run panel:dev # Vite dev server on :5173
|
||||||
bun run panel:build # Build React dashboard for production
|
bun run panel:build # build panel/dist
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Aurora is a Discord RPG bot + REST API running as a **single Bun process**. The bot and API share the same database client and services.
|
Aurora is a single-process Bun application:
|
||||||
|
|
||||||
```
|
- `bot/index.ts` boots shared config, registers domain listeners, starts the API server, then logs into Discord.
|
||||||
bot/ # Discord bot
|
- `api/src/server.ts` hosts REST routes, WebSocket traffic, and built panel assets.
|
||||||
├── commands/ # Slash commands by category (admin, economy, inventory, etc.)
|
- `shared/modules/*` contains the business logic used by both the bot and the API.
|
||||||
├── events/ # Discord event handlers
|
- `shared/games/*` contains reusable game plugins; `api/src/games/*` runs rooms and WebSocket orchestration.
|
||||||
├── lib/ # BotClient, handlers, loaders, embed helpers, commandUtils
|
|
||||||
├── modules/ # Feature modules (views, interactions per domain)
|
|
||||||
└── graphics/ # Canvas-based image generation (@napi-rs/canvas)
|
|
||||||
|
|
||||||
shared/ # Shared between bot and API
|
Current high-level layout:
|
||||||
├── db/ # Drizzle ORM client + schema (users, economy, inventory, quests, etc.)
|
|
||||||
├── lib/ # env, config, errors, logger, types, utils
|
|
||||||
└── modules/ # Domain services (economy, user, inventory, quest, moderation, etc.)
|
|
||||||
|
|
||||||
api/ # REST API (Bun HTTP server)
|
```text
|
||||||
└── src/routes/ # Route handlers for each domain
|
bot/ Discord commands, events, views, interactions
|
||||||
|
api/ Bun HTTP + WebSocket server
|
||||||
panel/ # React admin dashboard (Vite + Tailwind + Radix UI)
|
panel/ React dashboard
|
||||||
|
shared/db/ Drizzle client and schema
|
||||||
|
shared/lib/ config, env, errors, logger, events, constants
|
||||||
|
shared/modules/ domain services
|
||||||
|
shared/games/ game plugins shared by API and panel
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key architectural details:**
|
## Import conventions
|
||||||
- Bot and API both import from `shared/` — do not duplicate logic.
|
|
||||||
- Services in `shared/modules/` are singleton objects, not classes.
|
|
||||||
- The database uses PostgreSQL 16+ via Drizzle ORM with `bigint` mode for Discord IDs and currency.
|
|
||||||
- Feature modules follow a strict file suffix convention (see below).
|
|
||||||
|
|
||||||
## Import Conventions
|
Use path aliases from the repo `tsconfig.json`:
|
||||||
|
|
||||||
Use path aliases (defined in `tsconfig.json`). Order: external packages → aliases → relative.
|
- `@/*` -> `bot/*`
|
||||||
|
- `@commands/*` -> `bot/commands/*`
|
||||||
|
- `@db/*` -> `shared/db/*`
|
||||||
|
- `@lib/*` -> `bot/lib/*`
|
||||||
|
- `@modules/*` -> `bot/modules/*`
|
||||||
|
- `@shared/*` -> `shared/*`
|
||||||
|
|
||||||
```typescript
|
Import order in the repo is generally:
|
||||||
import { SlashCommandBuilder } from "discord.js"; // external
|
|
||||||
import { economyService } from "@shared/modules/economy/economy.service"; // alias
|
|
||||||
import { users } from "@db/schema"; // alias
|
|
||||||
import { createErrorEmbed } from "@lib/embeds"; // alias
|
|
||||||
import { localHelper } from "./helper"; // relative
|
|
||||||
```
|
|
||||||
|
|
||||||
**Aliases:**
|
1. external packages
|
||||||
- `@/*` → `bot/`
|
2. aliases
|
||||||
- `@shared/*` → `shared/`
|
3. relative imports
|
||||||
- `@db/*` → `shared/db/`
|
|
||||||
- `@lib/*` → `bot/lib/`
|
|
||||||
- `@modules/*` → `bot/modules/`
|
|
||||||
- `@commands/*` → `bot/commands/`
|
|
||||||
|
|
||||||
## Code Patterns
|
## File patterns
|
||||||
|
|
||||||
### Module File Suffixes
|
- `*.service.ts`: domain/business logic, usually in `shared/modules/*`
|
||||||
|
- `*.view.ts`: Discord message/view construction
|
||||||
|
- `*.interaction.ts`: component interaction handlers
|
||||||
|
- `*.types.ts`: local types and custom ID helpers
|
||||||
|
- `*.handler.ts`: bot-side orchestration around services/views
|
||||||
|
- `*.test.ts`: colocated tests
|
||||||
|
|
||||||
- `*.view.ts` — Creates Discord embeds/components
|
## Runtime config
|
||||||
- `*.interaction.ts` — Handles button/select/modal interactions
|
|
||||||
- `*.service.ts` — Business logic (lives in `shared/modules/`)
|
|
||||||
- `*.types.ts` — Module-specific TypeScript types
|
|
||||||
- `*.test.ts` — Tests (co-located with source)
|
|
||||||
|
|
||||||
### Interaction Routing
|
- Global game settings live in `game_settings` and are loaded into `shared/lib/config.ts`.
|
||||||
|
- Guild-specific settings live in `guild_settings`; `getGuildConfig()` adds a 60-second cache on top of DB reads.
|
||||||
|
- Most numeric DB values exposed through runtime config are converted to `bigint` in `shared/lib/config.ts`.
|
||||||
|
|
||||||
Component interactions (buttons, select menus, modals) flow through a centralized routing system:
|
## Interaction routing
|
||||||
|
|
||||||
```
|
Global component routing is defined in `bot/lib/interaction.routes.ts` and consumed by `ComponentInteractionHandler`.
|
||||||
Discord event → interactionCreate → ComponentInteractionHandler → interaction.routes.ts → *.interaction.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
`ComponentInteractionHandler` (`bot/lib/handlers/ComponentInteractionHandler.ts`) iterates over the route table in `bot/lib/interaction.routes.ts`. Each route has a `predicate` that matches on `customId`, a lazy `handler` import, and a `method` name to call. The handler also provides centralized `UserError` / system error handling.
|
Current route table:
|
||||||
|
|
||||||
**Route table (custom ID prefix → handler):**
|
- `trade_` and `amount` -> `bot/modules/trade/trade.interaction.ts`
|
||||||
|
- `shop_buy_` -> `bot/modules/economy/shop.interaction.ts`
|
||||||
|
- `lootdrop_` -> `bot/modules/economy/lootdrop.interaction.ts`
|
||||||
|
- `trivia_` -> `bot/modules/trivia/trivia.interaction.ts`
|
||||||
|
- `createitem_` -> `bot/modules/admin/item_wizard.ts`
|
||||||
|
- `enrollment` -> `bot/modules/user/enrollment.interaction.ts`
|
||||||
|
- `feedback_` -> `bot/modules/feedback/feedback.interaction.ts`
|
||||||
|
|
||||||
| Custom ID prefix | Handler file | Method |
|
Some features still use local collectors instead of the global route table, notably inventory.
|
||||||
| ------------------ | ----------------------------------------------- | ------------------------------ |
|
|
||||||
| `trade_`, `amount` | `bot/modules/trade/trade.interaction.ts` | `handleTradeInteraction` |
|
|
||||||
| `shop_buy_` | `bot/modules/economy/shop.interaction.ts` | `handleShopInteraction` |
|
|
||||||
| `lootdrop_` | `bot/modules/economy/lootdrop.interaction.ts` | `handleLootdropInteraction` |
|
|
||||||
| `trivia_` | `bot/modules/trivia/trivia.interaction.ts` | `handleTriviaInteraction` |
|
|
||||||
| `createitem_` | `bot/modules/admin/item_wizard.ts` | `handleItemWizardInteraction` |
|
|
||||||
| `enrollment` | `bot/modules/user/enrollment.interaction.ts` | `handleEnrollmentInteraction` |
|
|
||||||
| `feedback_` | `bot/modules/feedback/feedback.interaction.ts` | `handleFeedbackInteraction` |
|
|
||||||
|
|
||||||
Routes are evaluated in order — the first matching predicate wins. Some modules (e.g., inventory with `inv_` prefix) handle interactions locally via message component collectors instead of the global route table.
|
## Commands and access control
|
||||||
|
|
||||||
### Command Definition
|
- Slash command execution is centralized in `bot/lib/handlers/CommandHandler.ts`.
|
||||||
|
- `withCommandErrorHandling()` is the normal command wrapper for defer/reply/error behavior.
|
||||||
|
- Beta commands rely on `featureFlagsService.hasAccess()`.
|
||||||
|
- `ADMIN_USER_IDS` controls admin panel access, not Discord permissions inside command code.
|
||||||
|
|
||||||
```typescript
|
## API and panel
|
||||||
export const commandName = createCommand({
|
|
||||||
data: new SlashCommandBuilder().setName("name").setDescription("desc"),
|
|
||||||
execute: async (interaction) => {
|
|
||||||
await withCommandErrorHandling(interaction, async () => {
|
|
||||||
const result = await service.method();
|
|
||||||
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
|
|
||||||
}, { ephemeral: true });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
`withCommandErrorHandling` (from `@lib/commandUtils`) handles `deferReply`, `UserError` display, and unexpected error logging automatically.
|
- API routes are prefix-matched in `api/src/routes/index.ts`.
|
||||||
|
- `/auth/*` and `/api/health` are public.
|
||||||
|
- Players may access `/api/stats`, `/api/health`, `/api/me`, and `/api/me/inventory`.
|
||||||
|
- Remaining `/api/*` routes are admin-only.
|
||||||
|
- The panel dev server proxies back to the Bun server; the integrated server serves `panel/dist` when built.
|
||||||
|
|
||||||
### Service Pattern
|
## Database notes
|
||||||
|
|
||||||
```typescript
|
- Docker Compose uses PostgreSQL 17.
|
||||||
export const serviceName = {
|
- Discord IDs and currency/xp values are stored as `bigint`.
|
||||||
methodName: async (params: ParamType): Promise<ReturnType> => {
|
- `withTransaction()` lives in `bot/lib/db.ts` and is the normal way shared services compose DB work.
|
||||||
return await withTransaction(async (tx) => {
|
|
||||||
// database operations
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
## Testing
|
||||||
|
|
||||||
```typescript
|
- Tests use `bun:test`.
|
||||||
import { UserError, SystemError } from "@shared/lib/errors";
|
- Mock modules before importing the unit under test.
|
||||||
|
- Most service tests stub `DrizzleClient` or `withTransaction()` rather than hitting the real database.
|
||||||
|
|
||||||
throw new UserError("You don't have enough coins!"); // shown to user
|
## Key entrypoints
|
||||||
throw new SystemError("DB connection failed"); // logged, generic message shown
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Transactions
|
- `bot/index.ts`
|
||||||
|
- `bot/lib/BotClient.ts`
|
||||||
```typescript
|
- `api/src/server.ts`
|
||||||
import { withTransaction } from "@/lib/db";
|
- `api/src/routes/index.ts`
|
||||||
|
- `shared/lib/config.ts`
|
||||||
return await withTransaction(async (tx) => {
|
- `shared/db/DrizzleClient.ts`
|
||||||
const user = await tx.query.users.findFirst({ where: eq(users.id, id) });
|
- `shared/db/schema/index.ts`
|
||||||
await tx.update(users).set({ coins: newBalance }).where(eq(users.id, id));
|
|
||||||
return user;
|
|
||||||
}, existingTx); // pass existing tx for nested transactions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
Mock modules **before** imports. Use `bun:test`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
||||||
|
|
||||||
mock.module("@shared/db/DrizzleClient", () => ({
|
|
||||||
DrizzleClient: { query: mockQuery },
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("serviceName", () => {
|
|
||||||
beforeEach(() => mockFn.mockClear());
|
|
||||||
it("should handle expected case", async () => {
|
|
||||||
mockFn.mockResolvedValue(testData);
|
|
||||||
const result = await service.method(input);
|
|
||||||
expect(result).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Naming Conventions
|
|
||||||
|
|
||||||
| Element | Convention | Example |
|
|
||||||
| ---------------- | ---------------------- | -------------------------------- |
|
|
||||||
| Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` |
|
|
||||||
| Classes | PascalCase | `CommandHandler`, `UserError` |
|
|
||||||
| Functions | camelCase | `createCommand`, `handleShopInteraction` |
|
|
||||||
| Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` |
|
|
||||||
| Enums | PascalCase | `TimerType`, `TransactionType` |
|
|
||||||
| Services | camelCase singleton | `economyService`, `userService` |
|
|
||||||
| Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` |
|
|
||||||
| DB tables | snake_case | `users`, `moderation_cases` |
|
|
||||||
| Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` |
|
|
||||||
| API routes | kebab-case | `/api/guild-settings` |
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| Purpose | File |
|
|
||||||
| ----------------- | -------------------------- |
|
|
||||||
| Bot entry point | `bot/index.ts` |
|
|
||||||
| Discord client | `bot/lib/BotClient.ts` |
|
|
||||||
| DB schema index | `shared/db/schema.ts` |
|
|
||||||
| Error classes | `shared/lib/errors.ts` |
|
|
||||||
| Environment vars | `shared/lib/env.ts` |
|
|
||||||
| Config loader | `shared/lib/config.ts` |
|
|
||||||
| Embed helpers | `bot/lib/embeds.ts` |
|
|
||||||
| Command utils | `bot/lib/commandUtils.ts` |
|
|
||||||
| API server | `api/src/server.ts` |
|
|
||||||
|
|||||||
269
README.md
269
README.md
@@ -1,159 +1,180 @@
|
|||||||
# Aurora
|
# Aurora
|
||||||
|
|
||||||
> A comprehensive, feature-rich Discord RPG bot built with modern technologies.
|
Aurora is a Discord RPG bot, admin/player panel, and REST/WebSocket API that run as one Bun application. The Discord bot and HTTP server share the same database client, config, services, and domain events.
|
||||||
|
|
||||||

|
## What exists today
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Aurora is a powerful Discord bot designed to facilitate RPG-like elements within a Discord server. It features a robust economy, class system, inventory management, quests, and more, all built on top of a high-performance stack using Bun and Drizzle ORM.
|
- Discord slash commands for economy, inventory, quests, moderation, feedback, user profiles, and admin tooling.
|
||||||
|
- A Bun HTTP API under `/api/*`, Discord OAuth under `/auth/*`, and a WebSocket endpoint at `/ws`.
|
||||||
|
- A React panel for both admins and enrolled players.
|
||||||
|
- Shared domain services in `shared/modules/*` and reusable game plugins in `shared/games/*`.
|
||||||
|
- Built-in real-time games: chess and blackjack.
|
||||||
|
|
||||||
**New in v1.0:** Aurora now includes a fully integrated **REST API** for accessing bot data, statistics, and configuration, running alongside the bot in a single process.
|
## Architecture
|
||||||
|
|
||||||
## ✨ Features
|
```text
|
||||||
|
bot/ Discord bot entrypoint, commands, events, Discord-facing views/interactions
|
||||||
|
api/ Bun HTTP server, route modules, WebSocket/game room server
|
||||||
|
panel/ React 19 + Vite + Tailwind v4 dashboard
|
||||||
|
shared/ Shared DB schema, services, config, events, utilities, game plugins
|
||||||
|
docs/ Product and design notes
|
||||||
|
```
|
||||||
|
|
||||||
### Discord Bot
|
Important points:
|
||||||
* **Class System**: Users can join different classes.
|
|
||||||
* **Economy**: Complete economy system with balance, transactions, and daily rewards.
|
|
||||||
* **Inventory & Items**: Sophisticated item system with rarities, types (Material, Consumable, etc.), and inventory management.
|
|
||||||
* **Leveling**: XP-based leveling system to track user activity and progress.
|
|
||||||
* **Quests**: Quest system with requirements and rewards.
|
|
||||||
* **Trading**: Secure trading system between users.
|
|
||||||
* **Lootdrops**: Random loot drops in channels to engage users.
|
|
||||||
* **Admin Tools**: Administrative commands for server management.
|
|
||||||
|
|
||||||
### REST API
|
- `bot/index.ts` initializes DB-backed config, wires domain events, starts the API server, then logs into Discord.
|
||||||
* **Live Analytics**: Real-time statistics endpoint (commands, transactions).
|
- The API server also serves built panel assets from `panel/dist` when they exist.
|
||||||
* **Configuration Management**: Update bot settings via API.
|
- Bot commands, API routes, and the panel all rely on the same service layer in `shared/modules/*`.
|
||||||
* **Database Inspection**: Integrated Drizzle Studio access.
|
- Runtime game config is loaded from the `game_settings` table into `shared/lib/config.ts`.
|
||||||
* **WebSocket Support**: Real-time event streaming for live updates.
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## Getting started
|
||||||
|
|
||||||
Aurora uses a **Single Process Monolith** architecture to maximize performance and simplify resource sharing.
|
|
||||||
|
|
||||||
* **Unified Runtime**: Both the Discord Client and the REST API run within the same Bun process.
|
|
||||||
* **Shared State**: This allows the API to access live bot memory (caches, gateways) directly without complex inter-process communication (IPC).
|
|
||||||
* **Simplified Deployment**: You only need to deploy a single Docker container.
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
|
||||||
|
|
||||||
* **Runtime**: [Bun](https://bun.sh/)
|
|
||||||
* **Bot Framework**: [Discord.js](https://discord.js.org/)
|
|
||||||
* **API Framework**: Bun HTTP Server (REST API)
|
|
||||||
* **UI**: Discord embeds and components
|
|
||||||
* **Database**: [PostgreSQL](https://www.postgresql.org/)
|
|
||||||
* **ORM**: [Drizzle ORM](https://orm.drizzle.team/)
|
|
||||||
* **Validation**: [Zod](https://zod.dev/)
|
|
||||||
* **Containerization**: [Docker](https://www.docker.com/)
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
* [Bun](https://bun.sh/) (latest version)
|
- Bun
|
||||||
* [Docker](https://www.docker.com/) & Docker Compose
|
- Docker and Docker Compose
|
||||||
|
- A Discord application with bot token, client ID, and client secret
|
||||||
|
|
||||||
### Installation
|
### Setup
|
||||||
|
|
||||||
1. **Clone the repository**
|
1. Install dependencies.
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd aurora
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**
|
```bash
|
||||||
```bash
|
bun install
|
||||||
bun install
|
```
|
||||||
```
|
|
||||||
|
|
||||||
3. **Environment Setup**
|
2. Create your environment file.
|
||||||
Copy the example environment file and configure it:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
Edit `.env` with your Discord bot token, Client ID, and database credentials.
|
|
||||||
|
|
||||||
> **Note**: The `DATABASE_URL` in `.env.example` is pre-configured for Docker.
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
4. **Start the Database**
|
3. Start PostgreSQL.
|
||||||
Run the database service using Docker Compose:
|
|
||||||
```bash
|
|
||||||
docker compose up -d db
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run Migrations**
|
```bash
|
||||||
```bash
|
docker compose up -d db
|
||||||
bun run migrate
|
```
|
||||||
```
|
|
||||||
OR
|
|
||||||
```bash
|
|
||||||
bun run db:push
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running the Bot & API
|
4. Initialize the schema.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:push:local
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer running schema changes through Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Start the bot and API.
|
||||||
|
|
||||||
**Development Mode** (with hot reload):
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
* Bot: Online in Discord
|
|
||||||
* API: http://localhost:3000
|
|
||||||
|
|
||||||
**Production Mode**:
|
The Bun server listens on `http://localhost:3000`.
|
||||||
Build and run with Docker (recommended):
|
|
||||||
|
### Panel development
|
||||||
|
|
||||||
|
The Bun server can serve a built panel, but day-to-day panel work is done with Vite:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d app
|
bun run panel:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🔐 Accessing Production Services (SSH Tunnel)
|
The panel dev server runs on `http://localhost:5173` and proxies `/api`, `/auth`, `/assets`, and `/ws` to `http://localhost:3000`.
|
||||||
|
|
||||||
For security, the Production Database and API are **not exposed** to the public internet by default. They are only accessible via localhost on the server.
|
To build the panel for the integrated Bun server:
|
||||||
|
|
||||||
To access them from your local machine, use the included SSH tunnel script.
|
```bash
|
||||||
|
bun run panel:build
|
||||||
1. Add your VPS details to your local `.env` file:
|
|
||||||
```env
|
|
||||||
VPS_USER=root
|
|
||||||
VPS_HOST=123.45.67.89
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the remote connection script:
|
|
||||||
```bash
|
|
||||||
bun run remote
|
|
||||||
```
|
|
||||||
|
|
||||||
This will establish secure tunnels for:
|
|
||||||
* **API**: http://localhost:3000
|
|
||||||
* **Drizzle Studio**: http://localhost:4983
|
|
||||||
|
|
||||||
## 📜 Scripts
|
|
||||||
|
|
||||||
* `bun run dev`: Start the bot and API server in watch mode.
|
|
||||||
* `bun run remote`: Open SSH tunnel to production services.
|
|
||||||
* `bun run generate`: Generate Drizzle migrations.
|
|
||||||
* `bun run migrate`: Apply migrations (via Docker).
|
|
||||||
* `bun run db:studio`: Open Drizzle Studio to inspect the database.
|
|
||||||
* `bun test`: Run tests.
|
|
||||||
|
|
||||||
## 📂 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── bot # Discord Bot logic & entry point
|
|
||||||
├── web # REST API Server
|
|
||||||
├── shared # Shared code (Database, Config, Types)
|
|
||||||
├── drizzle # Drizzle migration files
|
|
||||||
├── scripts # Utility scripts
|
|
||||||
├── docker-compose.yml
|
|
||||||
└── package.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 Contributing
|
## Useful scripts
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
```bash
|
||||||
|
# App
|
||||||
|
bun run dev
|
||||||
|
docker compose up
|
||||||
|
docker compose up app
|
||||||
|
docker compose up db
|
||||||
|
|
||||||
## 📄 License
|
# Database
|
||||||
|
bun run db:push
|
||||||
|
bun run db:push:local
|
||||||
|
bun run db:generate
|
||||||
|
bun run db:migrate
|
||||||
|
bun run db:studio
|
||||||
|
bun run db:backup
|
||||||
|
bun run db:restore
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
# Panel
|
||||||
|
bun run panel:dev
|
||||||
|
bun run panel:build
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
bun test
|
||||||
|
bun run test
|
||||||
|
bun run test:ci
|
||||||
|
|
||||||
|
# Ops
|
||||||
|
bun run remote
|
||||||
|
bun run deploy
|
||||||
|
bun run deploy:remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment notes
|
||||||
|
|
||||||
|
The main variables you need in `.env` are:
|
||||||
|
|
||||||
|
- `DISCORD_BOT_TOKEN`
|
||||||
|
- `DISCORD_CLIENT_ID`
|
||||||
|
- `DISCORD_CLIENT_SECRET`
|
||||||
|
- `DISCORD_GUILD_ID`
|
||||||
|
- `ADMIN_USER_IDS`
|
||||||
|
- `DB_USER`
|
||||||
|
- `DB_PASSWORD`
|
||||||
|
- `DB_NAME`
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `PANEL_BASE_URL`
|
||||||
|
|
||||||
|
Players can authenticate into the panel only after they exist in the `users` table. Admin access is determined by `ADMIN_USER_IDS`.
|
||||||
|
|
||||||
|
## API and panel summary
|
||||||
|
|
||||||
|
- Public routes: `/auth/*`, `/api/health`
|
||||||
|
- Player-accessible API routes: `/api/stats`, `/api/health`, `/api/me`, `/api/me/inventory`
|
||||||
|
- Admin-only API routes: the rest of `/api/*`
|
||||||
|
- WebSocket: `/ws` with cookie-based auth
|
||||||
|
- Static assets: `/assets/*`
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
bot/
|
||||||
|
commands/
|
||||||
|
events/
|
||||||
|
lib/
|
||||||
|
modules/
|
||||||
|
|
||||||
|
api/
|
||||||
|
src/
|
||||||
|
routes/
|
||||||
|
games/
|
||||||
|
|
||||||
|
panel/
|
||||||
|
src/
|
||||||
|
|
||||||
|
shared/
|
||||||
|
db/
|
||||||
|
games/
|
||||||
|
lib/
|
||||||
|
modules/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [AGENTS.md](AGENTS.md): repo-wide implementation guidance
|
||||||
|
- [api/README.md](api/README.md): API surface and auth model
|
||||||
|
- [docs/new-design/DESIGN.md](docs/new-design/DESIGN.md): current panel design language
|
||||||
|
|||||||
132
api/README.md
132
api/README.md
@@ -1,30 +1,130 @@
|
|||||||
# Aurora Web API
|
# Aurora API
|
||||||
|
|
||||||
The web API provides a REST interface and WebSocket support for accessing Aurora bot data and configuration.
|
Aurora's API is a Bun server that runs inside the same process as the Discord bot. It serves REST routes, the authenticated WebSocket endpoint, static assets, and built panel files.
|
||||||
|
|
||||||
## API Endpoints
|
## Runtime model
|
||||||
|
|
||||||
- `GET /api/stats` - Real-time bot statistics
|
- Entry point: `api/src/server.ts`
|
||||||
- `GET /api/settings` - Bot configuration
|
- Route dispatcher: `api/src/routes/index.ts`
|
||||||
- `GET /api/users` - User data
|
- Auth: Discord OAuth with in-memory session cookies
|
||||||
- `GET /api/items` - Item catalog
|
- WebSocket: `/ws`
|
||||||
- `GET /api/quests` - Quest information
|
- Static assets: `/assets/*`
|
||||||
- `GET /api/transactions` - Economy data
|
- Built panel fallback: `panel/dist`
|
||||||
- `GET /api/health` - Health check
|
|
||||||
|
## Access model
|
||||||
|
|
||||||
|
Public:
|
||||||
|
|
||||||
|
- `GET /api/health`
|
||||||
|
- `/auth/discord`
|
||||||
|
- `/auth/callback`
|
||||||
|
- `POST /auth/logout`
|
||||||
|
- `GET /auth/me`
|
||||||
|
|
||||||
|
Player-accessible API routes:
|
||||||
|
|
||||||
|
- `GET /api/stats`
|
||||||
|
- `GET /api/health`
|
||||||
|
- `GET /api/me`
|
||||||
|
- `GET /api/me/inventory`
|
||||||
|
|
||||||
|
Admin-only API routes:
|
||||||
|
|
||||||
|
- everything else under `/api/*`
|
||||||
|
|
||||||
|
Admin vs player is derived from `ADMIN_USER_IDS`. A user must already exist in the `users` table to complete panel login.
|
||||||
|
|
||||||
|
## Route summary
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- `GET /auth/discord`
|
||||||
|
- `GET /auth/callback`
|
||||||
|
- `POST /auth/logout`
|
||||||
|
- `GET /auth/me`
|
||||||
|
|
||||||
|
### Dashboard and system
|
||||||
|
|
||||||
|
- `GET /api/health`
|
||||||
|
- `GET /api/stats`
|
||||||
|
- `GET /api/stats/activity`
|
||||||
|
- `POST /api/actions/reload-commands`
|
||||||
|
- `POST /api/actions/clear-cache`
|
||||||
|
- `POST /api/actions/maintenance-mode`
|
||||||
|
|
||||||
|
### Settings
|
||||||
|
|
||||||
|
- `GET /api/settings`
|
||||||
|
- `POST /api/settings`
|
||||||
|
- `GET /api/settings/meta`
|
||||||
|
- `GET /api/guilds/:guildId/settings`
|
||||||
|
- `PUT|PATCH /api/guilds/:guildId/settings`
|
||||||
|
- `DELETE /api/guilds/:guildId/settings`
|
||||||
|
|
||||||
|
### Users, classes, and inventory
|
||||||
|
|
||||||
|
- `GET /api/me`
|
||||||
|
- `GET /api/me/inventory`
|
||||||
|
- `GET /api/users`
|
||||||
|
- `GET /api/users/:id`
|
||||||
|
- `PUT /api/users/:id`
|
||||||
|
- `GET /api/users/:id/inventory`
|
||||||
|
- `POST /api/users/:id/inventory`
|
||||||
|
- `DELETE /api/users/:id/inventory/:itemId`
|
||||||
|
- `GET /api/classes`
|
||||||
|
- `POST /api/classes`
|
||||||
|
- `PUT /api/classes/:id`
|
||||||
|
- `DELETE /api/classes/:id`
|
||||||
|
|
||||||
|
### Game content
|
||||||
|
|
||||||
|
- `GET /api/items`
|
||||||
|
- `POST /api/items`
|
||||||
|
- `GET /api/items/:id`
|
||||||
|
- `PUT /api/items/:id`
|
||||||
|
- `DELETE /api/items/:id`
|
||||||
|
- `POST /api/items/:id/icon`
|
||||||
|
- `GET /api/quests`
|
||||||
|
- `POST /api/quests`
|
||||||
|
- `PUT /api/quests/:id`
|
||||||
|
- `DELETE /api/quests/:id`
|
||||||
|
- `GET /api/lootdrops`
|
||||||
|
- `POST /api/lootdrops`
|
||||||
|
- `DELETE /api/lootdrops/:messageId`
|
||||||
|
|
||||||
|
### Moderation and economy history
|
||||||
|
|
||||||
|
- `GET /api/moderation`
|
||||||
|
- `POST /api/moderation`
|
||||||
|
- `GET /api/transactions`
|
||||||
|
|
||||||
## WebSocket
|
## WebSocket
|
||||||
|
|
||||||
Connect to `/ws` for real-time updates:
|
`/ws` requires a valid `aurora_session` cookie.
|
||||||
- Stats broadcasts every 5 seconds
|
|
||||||
- Event notifications via system bus
|
Current behavior:
|
||||||
- PING/PONG heartbeat support
|
|
||||||
|
- dashboard clients subscribe to `dashboard`
|
||||||
|
- game clients also use lobby and room-scoped traffic through `GameServer`
|
||||||
|
- `PING` from the client returns `PONG`
|
||||||
|
- dashboard stats are broadcast every 5 seconds while at least one client is connected
|
||||||
|
- hard limits in `api/src/server.ts`:
|
||||||
|
- 200 concurrent connections
|
||||||
|
- 16 KB max payload
|
||||||
|
- 60 second idle timeout
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The API runs automatically when you start the bot:
|
Start the backend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The API will be available at `http://localhost:3000`
|
Optional panel dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run panel:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Panel dev runs on `http://localhost:5173` and proxies API/auth/assets/WebSocket requests to `http://localhost:3000`.
|
||||||
|
|||||||
@@ -1,31 +1,68 @@
|
|||||||
# API Layer
|
# API layer
|
||||||
|
|
||||||
## Server
|
## Server shape
|
||||||
- Bun's native `serve()` API — no Express/Fastify. Custom `handleRequest()` dispatcher with pathname prefix matching.
|
|
||||||
- Route modules export `{ name: string, handler: RouteHandler }`. Handlers return `null` for non-matching paths.
|
|
||||||
|
|
||||||
## Authentication
|
- Aurora uses Bun's native `serve()` API in `api/src/server.ts`.
|
||||||
- Discord OAuth2 with session cookies (`aurora_session`, HttpOnly, 7-day TTL).
|
- Route modules are aggregated in `api/src/routes/index.ts`.
|
||||||
- In-memory session store — sessions lost on restart.
|
- A route module returns `null` when it does not match so the dispatcher can continue.
|
||||||
- Role-based: `admin` vs `player` (admins set via `ADMIN_USER_IDS` env var).
|
- After route handling, the server tries `panel/dist` for SPA/static files.
|
||||||
- Non-enrolled users (not in DB) get 403 even with valid Discord auth.
|
|
||||||
- Call `getSession(req)` for all protected routes.
|
|
||||||
|
|
||||||
## Response Conventions
|
## Authentication and authorization
|
||||||
- Success: `jsonResponse(data, status)` — uses custom BigInt-safe JSON replacer.
|
|
||||||
- Error: `errorResponse(message, status, details?)` → `{ error, details? }`
|
- OAuth routes live in `api/src/routes/auth.routes.ts`.
|
||||||
- Validation: `validationErrorResponse(zodError)` → `{ error: "Invalid payload", issues: [...] }`
|
- Sessions are stored in memory and keyed by the `aurora_session` cookie.
|
||||||
- Zod schemas centralized in `schemas.ts`.
|
- Session TTL is 7 days.
|
||||||
|
- Login succeeds only for users already present in the `users` table.
|
||||||
|
- Role is `admin` if the Discord ID is in `ADMIN_USER_IDS`, otherwise `player`.
|
||||||
|
- Redirects after login are intentionally restricted to localhost or relative paths.
|
||||||
|
|
||||||
|
Current access rules from `api/src/routes/index.ts`:
|
||||||
|
|
||||||
|
- public: `/auth/*`, `/api/health`
|
||||||
|
- player allow-list: `/api/stats`, `/api/health`, `/api/me`
|
||||||
|
- everything else under `/api/*`: admin-only
|
||||||
|
|
||||||
|
`/api/me/inventory` is handled by `users.routes.ts` and still depends on a valid session.
|
||||||
|
|
||||||
|
## Response conventions
|
||||||
|
|
||||||
|
- `jsonResponse()` serializes `bigint` values as strings.
|
||||||
|
- `errorResponse()` returns `{ error, details? }`.
|
||||||
|
- `parseBody()` and `parseQuery()` validate with Zod and return a `Response` on failure.
|
||||||
|
- The API does not use a framework-level middleware stack; each route handles its own parsing and branching.
|
||||||
|
|
||||||
## WebSocket
|
## WebSocket
|
||||||
- Upgrade via `/ws` endpoint (requires auth).
|
|
||||||
- Pub/sub via Bun's `.publish()` / `.subscribe()` on channels: `dashboard`, `lobby`, `room:${roomId}`.
|
- Endpoint: `/ws`
|
||||||
- Dashboard stats broadcast every 5 seconds. Game events are room-scoped.
|
- Requires an authenticated session
|
||||||
- Hard limit: 200 concurrent WS connections (429 rejection), 16KB max payload, 60s idle timeout.
|
- Dashboard channel: `dashboard`
|
||||||
- Fire-and-forget broadcasts — no ack mechanism.
|
- Lobby channel: `lobby`
|
||||||
|
- Room-specific messaging is handled inside `GameServer`
|
||||||
|
- Dashboard broadcasts `STATS_UPDATE` every 5 seconds while clients are connected
|
||||||
|
- `NEW_EVENT` broadcasts are wired from `shared/lib/events`
|
||||||
|
|
||||||
|
Hard limits:
|
||||||
|
|
||||||
|
- max connections: 200
|
||||||
|
- max payload: 16 KB
|
||||||
|
- idle timeout: 60 seconds
|
||||||
|
|
||||||
|
## Static files
|
||||||
|
|
||||||
|
- Built panel assets are served from `panel/dist`
|
||||||
|
- `/assets/*` serves files from `bot/assets/graphics`
|
||||||
|
- `/api/*`, `/auth/*`, `/ws`, and `/assets/*` bypass the SPA fallback
|
||||||
|
|
||||||
|
## Route notes
|
||||||
|
|
||||||
|
- `items.routes.ts` supports both JSON and multipart form data for item creation.
|
||||||
|
- `settings.routes.ts` writes DB-backed game settings and emits the reload-commands event.
|
||||||
|
- `guild-settings.routes.ts` invalidates the guild config cache after writes.
|
||||||
|
- `lootdrops.routes.ts` delegates spawning/deletion to bot-side handlers because Discord message creation happens there.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
- All DB IDs are BigInt — JSON responses must use the custom `jsonReplacer`.
|
|
||||||
- No rate limiting on HTTP routes.
|
- Sessions and some caches are in-memory only and are lost on restart.
|
||||||
- Some routes accept multipart form data (e.g., item icon upload) — manual parsing, not abstracted.
|
- The server registers game plugins at startup; duplicate registration throws.
|
||||||
- Asset directories resolve relative to `import.meta.dir`.
|
- BigInt-safe JSON matters for nearly every domain route.
|
||||||
|
- The panel's auth flow depends on `PANEL_BASE_URL` matching the OAuth callback origin.
|
||||||
|
|||||||
@@ -1,116 +1,38 @@
|
|||||||
# Trivia - Components v2 Implementation
|
# Trivia UI
|
||||||
|
|
||||||
This trivia feature uses **Discord Components v2** for a premium visual experience.
|
The trivia command uses Discord Components v2 for the question, result, and timeout states.
|
||||||
|
|
||||||
## 🎨 Visual Features
|
## Files
|
||||||
|
|
||||||
### **Container with Accent Colors**
|
```text
|
||||||
Each trivia question is displayed in a Container with a colored accent bar that changes based on difficulty:
|
|
||||||
- **🟢 Easy**: Green accent bar (`0x57F287`)
|
|
||||||
- **🟡 Medium**: Yellow accent bar (`0xFEE75C`)
|
|
||||||
- **🔴 Hard**: Red accent bar (`0xED4245`)
|
|
||||||
|
|
||||||
### **Modern Layout Components**
|
|
||||||
- **TextDisplay** - Rich markdown formatting for question text
|
|
||||||
- **Separator** - Visual spacing between sections
|
|
||||||
- **Container** - Groups all content with difficulty-based styling
|
|
||||||
|
|
||||||
### **Interactive Features**
|
|
||||||
✅ **Give Up Button** - Players can forfeit if they're unsure
|
|
||||||
✅ **Disabled Answer Buttons** - After answering, buttons show:
|
|
||||||
- ✅ Green for correct answer
|
|
||||||
- ❌ Red for user's incorrect answer
|
|
||||||
- Gray for other options
|
|
||||||
|
|
||||||
✅ **Time Display** - Shows both relative time (`in 30s`) and seconds remaining
|
|
||||||
✅ **Stakes Preview** - Clear display: `50 AU ➜ 100 AU`
|
|
||||||
|
|
||||||
## 📁 File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
bot/modules/trivia/
|
bot/modules/trivia/
|
||||||
├── trivia.view.ts # Components v2 view functions
|
trivia.types.ts
|
||||||
├── trivia.interaction.ts # Button interaction handler
|
trivia.view.ts
|
||||||
└── README.md # This file
|
trivia.interaction.ts
|
||||||
|
|
||||||
bot/commands/economy/
|
bot/commands/economy/trivia.ts
|
||||||
└── trivia.ts # /trivia slash command
|
shared/modules/trivia/trivia.service.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Technical Details
|
## What the view layer does
|
||||||
|
|
||||||
### Components v2 Requirements
|
- renders the active question as a Components v2 container
|
||||||
- Uses `MessageFlags.IsComponentsV2` flag
|
- colors the container by difficulty
|
||||||
- No `embeds` or `content` fields (uses TextDisplay instead)
|
- renders answer buttons from the session's shuffled answers
|
||||||
- Numeric component types:
|
- renders separate result and timeout views with disabled buttons
|
||||||
- `1` - Action Row
|
|
||||||
- `2` - Button
|
|
||||||
- `10` - Text Display
|
|
||||||
- `14` - Separator
|
|
||||||
- `17` - Container
|
|
||||||
- Max 40 components per message (vs 5 for legacy)
|
|
||||||
|
|
||||||
### Button Styles
|
## Current interaction flow
|
||||||
- **Secondary (2)**: Gray - Used for answer buttons
|
|
||||||
- **Success (3)**: Green - Used for "True" and correct answers
|
|
||||||
- **Danger (4)**: Red - Used for "False", incorrect answers, and "Give Up"
|
|
||||||
|
|
||||||
## 🎮 User Experience Flow
|
1. `/trivia` checks cooldown before deferring.
|
||||||
|
2. `startTrivia()` deducts the entry fee and creates the session.
|
||||||
|
3. `getTriviaQuestionView()` renders the prompt.
|
||||||
|
4. `trivia.interaction.ts` compares the clicked answer with the session's `correctIndex`.
|
||||||
|
5. `submitAnswer()` finalizes the session and the view swaps to success, failure, or timeout output.
|
||||||
|
|
||||||
1. User runs `/trivia`
|
The command also schedules a timeout cleanup with a 5-second grace period after `config.trivia.timeoutSeconds`.
|
||||||
2. Sees question in a Container with difficulty-based accent color
|
|
||||||
3. Can choose to:
|
|
||||||
- Select an answer (A/B/C/D or True/False)
|
|
||||||
- Give up using the 🏳️ button
|
|
||||||
4. After answering, sees result with:
|
|
||||||
- Disabled buttons showing correct/incorrect answers
|
|
||||||
- Container with result-based accent color (green/red/yellow)
|
|
||||||
- Reward or penalty information
|
|
||||||
|
|
||||||
## 🌟 Visual Examples
|
## Custom IDs
|
||||||
|
|
||||||
### Question Display
|
- answer buttons: `TRIVIA_CUSTOM_IDS.ANSWER(sessionId, index)`
|
||||||
```
|
- give up: `TRIVIA_CUSTOM_IDS.GIVE_UP(sessionId)`
|
||||||
┌─[GREEN]─────────────────────────┐
|
- result/timeout buttons use non-interactive result IDs
|
||||||
│ # 🎯 Trivia Challenge │
|
|
||||||
│ 🟢 Easy • 📚 Geography │
|
|
||||||
│ ─────────────────────────── │
|
|
||||||
│ ### What is the capital of │
|
|
||||||
│ France? │
|
|
||||||
│ │
|
|
||||||
│ ⏱️ Time: in 30s (30s) │
|
|
||||||
│ 💰 Stakes: 50 AU ➜ 100 AU │
|
|
||||||
│ 👤 Player: Username │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
[🇦 A: Paris] [🇧 B: London]
|
|
||||||
[🇨 C: Berlin] [🇩 D: Madrid]
|
|
||||||
[🏳️ Give Up]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Result Display (Correct)
|
|
||||||
```
|
|
||||||
┌─[GREEN]─────────────────────────┐
|
|
||||||
│ # 🎉 Correct Answer! │
|
|
||||||
│ ### What is the capital of │
|
|
||||||
│ France? │
|
|
||||||
│ ─────────────────────────── │
|
|
||||||
│ ✅ Your answer: Paris │
|
|
||||||
│ │
|
|
||||||
│ 💰 Reward: +100 AU │
|
|
||||||
│ │
|
|
||||||
│ 🏆 Great job! Keep it up! │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
[✅ A: Paris] [❌ B: London]
|
|
||||||
[❌ C: Berlin] [❌ D: Madrid]
|
|
||||||
(all buttons disabled)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Future Enhancements
|
|
||||||
|
|
||||||
Potential improvements:
|
|
||||||
- [ ] Thumbnail images based on trivia category
|
|
||||||
- [ ] Progress bar for time remaining
|
|
||||||
- [ ] Streak counter display
|
|
||||||
- [ ] Category-specific accent colors
|
|
||||||
- [ ] Media Gallery for image-based questions
|
|
||||||
- [ ] Leaderboard integration in results
|
|
||||||
|
|||||||
@@ -1,84 +1,60 @@
|
|||||||
# Design System Specification: Stellar Editorial
|
# Stellar Editorial
|
||||||
|
|
||||||
## 1. Overview & Creative North Star: "The Celestial Curator"
|
This document now tracks the design language that is actually implemented in the panel, with a small amount of forward-looking guidance where the code is still catching up.
|
||||||
This design system is built to evoke the atmosphere of an elite, secretive astronomical academy. We are moving away from the "utility-first" aesthetic of standard Discord bots toward a "Digital Editorial" experience.
|
|
||||||
|
|
||||||
**Creative North Star: The Celestial Curator**
|
## Current implementation status
|
||||||
The UI should feel like a high-end, leather-bound astronomical ledger reimagined for the 22nd century. We achieve this through "The Void & The Light"—using deep, expansive dark spaces contrasted against precise, shimmering accents. We reject the rigid, boxed-in layouts of traditional dashboards in favor of intentional asymmetry, overlapping layers, and a profound sense of depth. Every element should feel curated, prestigious, and slightly mysterious.
|
|
||||||
|
|
||||||
---
|
Implemented today in `panel/src/index.css` and the active panel pages:
|
||||||
|
|
||||||
## 2. Colors: The Palette of the Night Sky
|
- dark "void and light" surface stack
|
||||||
The color philosophy is rooted in "Atmospheric Depth." We use a hierarchy of midnight tones to create a sense of infinite space, punctuated by gold and starlight.
|
- Celestial Gold primary color family
|
||||||
|
- Noto Serif, Manrope, Space Grotesk, and JetBrains Mono typography
|
||||||
|
- low-contrast "ghost border" treatment
|
||||||
|
- rounded surface hierarchy for cards, sidebars, and controls
|
||||||
|
|
||||||
### The "No-Line" Rule
|
Partially implemented or still aspirational:
|
||||||
**Explicit Instruction:** Designers are prohibited from using 1px solid borders to define sections or containers. Layout boundaries must be defined exclusively through background color shifts. Use `surface-container-low` against a `surface` background to create a subtle "carved" effect, or `surface-container-high` to create "lift."
|
|
||||||
|
|
||||||
### Surface Hierarchy & Nesting
|
- stronger asymmetry and editorial layouts across more pages
|
||||||
Instead of a flat grid, treat the UI as a series of nested celestial bodies.
|
- decorative constellation/nebula treatments
|
||||||
* **Base Layer:** `surface` (#0d1323) – The infinite void.
|
- more consistent premium component states across every admin screen
|
||||||
* **Secondary Sections:** `surface-container-low` (#151b2c) – Softly recessed areas.
|
|
||||||
* **Interactive Cards:** `surface-container-high` (#24293b) – Elevated content.
|
|
||||||
* **Floating Modals:** `surface-container-highest` (#2f3446) – The closest layer to the viewer.
|
|
||||||
|
|
||||||
### The "Glass & Gradient" Rule
|
## Design intent
|
||||||
To capture the "Stellar Academy" prestige, use Glassmorphism for floating elements.
|
|
||||||
* **Formula:** Apply `surface-variant` at 40% opacity with a `24px` backdrop blur.
|
|
||||||
* **Signature Textures:** For primary CTAs and hero headers, utilize a linear gradient: `primary` (#e9c349) to `primary-fixed-dim` (#e9c349) at a 135-degree angle. This provides a "metallic brass" sheen that flat colors cannot replicate.
|
|
||||||
|
|
||||||
---
|
Aurora's panel should feel like an elite astronomical academy rather than a default admin dashboard. The codebase already follows that direction through the theme tokens and typography system; new UI work should continue that tone instead of falling back to generic SaaS styling.
|
||||||
|
|
||||||
## 3. Typography: Academic Authority
|
## Core tokens
|
||||||
The type system pairs the intellectual weight of a classic Serif with the technical precision of a modern Sans-Serif.
|
|
||||||
|
|
||||||
* **Display & Headlines (Noto Serif):** These are the "Ancient Manuscripts." Use `display-lg` to `headline-sm` for titles and major headers. This font conveys the "Elite Academy" prestige.
|
Surface hierarchy:
|
||||||
* **UI & Body (Manrope):** This is the "Modern Faculty." Use `title-lg` down to `body-sm` for all functional UI elements, descriptions, and system feedback.
|
|
||||||
* **Technical Labels (Space Grotesk):** Use `label-md` and `label-sm` for status tags, metadata, and numerical RPG stats. This adds a subtle "astronomical instrument" feel.
|
|
||||||
|
|
||||||
**Editorial Tip:** Use exaggerated tracking (0.1em) on `label-sm` in all-caps to enhance the high-end, premium feel.
|
- `--color-background`: `#0d1323`
|
||||||
|
- `--color-surface-container-low`: `#151b2c`
|
||||||
|
- `--color-surface-container-high`: `#24293b`
|
||||||
|
- `--color-surface-container-highest`: `#2f3446`
|
||||||
|
|
||||||
---
|
Primary accents:
|
||||||
|
|
||||||
## 4. Elevation & Depth: Tonal Layering
|
- `--color-primary`: `#e9c349`
|
||||||
We do not use structural lines. We use light and shadow to imply existence.
|
- `--color-primary-fixed-dim`: `#d4af37`
|
||||||
|
- `--color-primary-container`: `#3d2e00`
|
||||||
|
|
||||||
* **The Layering Principle:** Depth is achieved by stacking. Place a `surface-container-lowest` card on a `surface-container-low` section. This creates a "soft-edge" transition that feels organic rather than digital.
|
Typography:
|
||||||
* **Ambient Shadows:** For floating elements, use a shadow with a 40px blur, 0px offset, and 6% opacity using the `on-surface` color. This creates a "glow" rather than a "drop shadow," mimicking how light behaves in a dark vacuum.
|
|
||||||
* **The "Ghost Border" Fallback:** If a boundary is strictly required for accessibility, use a "Ghost Border": `outline-variant` (#45464c) at 15% opacity. Never use 100% opaque borders.
|
|
||||||
* **The Aurora Glow:** Use `primary-container` as a very large, soft radial gradient (600px+) behind key UI elements to create a "nebula" effect, grounding the component in space.
|
|
||||||
|
|
||||||
---
|
- display: Noto Serif
|
||||||
|
- body: Manrope
|
||||||
|
- labels: Space Grotesk
|
||||||
|
- mono: JetBrains Mono
|
||||||
|
|
||||||
## 5. Components: Precision & Prestige
|
## Component guidance
|
||||||
|
|
||||||
### Buttons
|
- Prefer tonal separation over heavy borders.
|
||||||
* **Primary:** A gradient-filled container (`primary` to `primary-fixed-dim`). No border. `label-md` (Space Grotesk) text in `on-primary`.
|
- Use gold as a focused accent, not a flood color.
|
||||||
* **Secondary:** A "Ghost Border" container with `secondary` text. Upon hover, the background fills with 5% `secondary` white.
|
- Keep text contrast high and metadata quieter.
|
||||||
* **Tertiary:** Text-only in `primary`, but with a small `2px` gold dot (star) preceding the label.
|
- Sidebar, cards, tables, and game views should feel like the same product family.
|
||||||
|
- Avoid plain white cards, Discord blurple defaults, and generic component-library styling.
|
||||||
|
|
||||||
### Cards & Lists
|
## Practical rules for future work
|
||||||
* **Rule:** Forbid the use of divider lines.
|
|
||||||
* **Separation:** Use `spacing-6` (2rem) of vertical white space to separate list items. If items must be grouped, use a subtle background shift to `surface-container-low`.
|
|
||||||
* **Visual Interest:** In the top-right corner of a "Highest" elevation card, place a subtle, 10% opacity constellation pattern SVG.
|
|
||||||
|
|
||||||
### Input Fields
|
- Start from the CSS tokens in `panel/src/index.css` instead of inventing new one-off colors.
|
||||||
* **Styling:** Inputs should not be boxes. Use a `surface-container-lowest` background with a `2px` bottom-only border in `outline-variant`.
|
- Preserve the current font roles unless there is a strong reason to change them.
|
||||||
* **Focus State:** The bottom border transitions to `primary` (gold) with a subtle `primary-container` outer glow.
|
- Use gradients, glow, or tonal depth sparingly and intentionally.
|
||||||
|
- Keep mobile behavior first-class; the existing layout already has mobile drawer behavior that new pages should respect.
|
||||||
### New Component: "The Astral Badge"
|
|
||||||
A specialized chip for RPG ranks.
|
|
||||||
* **Style:** `label-sm` text inside a `surface-container-highest` pill. It features a 1px "Ghost Border" and a tiny radial glow in the center-left using the `primary` color to represent a "guiding star."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Do's and Don'ts
|
|
||||||
|
|
||||||
### Do:
|
|
||||||
* **Embrace Asymmetry:** Align text to the left but place decorative "constellation" elements floating on the right to break the grid.
|
|
||||||
* **Use Generous Spacing:** Premium design requires "breathing room." Use `spacing-10` and `spacing-12` for section margins.
|
|
||||||
* **Tint Your Greys:** Ensure all "neutrals" are pulled from the `secondary` and `tertiary` tokens, which are infused with midnight blue and silver.
|
|
||||||
|
|
||||||
### Don't:
|
|
||||||
* **Don't use pure black (#000):** It kills the "depth" of the midnight blue theme. Use `surface-container-lowest`.
|
|
||||||
* **Don't use standard Discord "Blurple":** This system must feel like a custom application that exists outside the standard Discord ecosystem.
|
|
||||||
* **Don't use hard corners:** Stick strictly to the `Roundedness Scale`. Most containers should use `xl` (0.75rem) or `lg` (0.5rem) to maintain a sophisticated, approachable feel.
|
|
||||||
|
|||||||
104
panel/AGENTS.md
104
panel/AGENTS.md
@@ -1,31 +1,87 @@
|
|||||||
# Panel (Admin Dashboard)
|
# Panel
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- React 19 + React Router v7 + Tailwind CSS v4 (Vite plugin) + Lucide icons.
|
|
||||||
- No component library (no Radix, no shadcn). All styling is inline Tailwind.
|
|
||||||
- No external state management. All state via custom hooks (`useAuth`, `useUsers`, `useItems`, `useDashboard`, `useGameRoom`).
|
|
||||||
|
|
||||||
## API Client
|
- React 19
|
||||||
- Thin `fetch` wrapper in `src/lib/api.ts` with typed generics: `get`, `post`, `put`, `del`.
|
- React Router 7
|
||||||
- Credentials: `same-origin` only — CORS requests will fail.
|
- Vite 6
|
||||||
- 401 responses redirect to Discord auth. No retry logic.
|
- Tailwind CSS v4 via `@tailwindcss/vite`
|
||||||
- 204 / empty responses return `undefined`.
|
- Local utilities: `clsx`, `tailwind-merge`, `class-variance-authority`
|
||||||
|
- Icons: `lucide-react`
|
||||||
|
|
||||||
## WebSocket
|
The panel lives in `panel/src` and is built to `panel/dist`.
|
||||||
- Singleton pattern in `useWebSocket()`. Global handler Set — multiple hooks share one connection.
|
|
||||||
- Auto-reconnects with exponential backoff (max 30s).
|
|
||||||
- `send()` and `subscribe()` API for components.
|
|
||||||
|
|
||||||
## Patterns
|
## Dev and runtime
|
||||||
- **Draft editing:** Pages use `selectedUser → userDraft` flow. Changes aren't auto-saved. `saveDraft()` commits, `discardDraft()` reverts, `isDirty()` detects changes.
|
|
||||||
- **Debounced search:** 300ms debounce on search inputs.
|
|
||||||
- **No cache invalidation:** After mutations, manually call `refetch()` to update lists.
|
|
||||||
|
|
||||||
## Routing
|
- `bun run panel:dev` starts Vite on `http://localhost:5173`
|
||||||
- Role-based: player routes (`/dashboard`, `/games`, `/leaderboards`) and admin routes (`/admin/*`).
|
- Vite proxies `/api`, `/auth`, `/assets`, and `/ws` to `http://localhost:3000`
|
||||||
- Sidebar auto-hides admin routes for non-admins.
|
- The Bun server serves the built panel from `panel/dist` in integrated mode
|
||||||
|
|
||||||
## Theme
|
## Auth model
|
||||||
- Dark theme with "Celestial Gold" primary (`#e9c349`).
|
|
||||||
- Semantic colors: destructive, success, warning, info.
|
- `useAuth()` calls `GET /auth/me`
|
||||||
- 4-tier surface hierarchy. Utility: `cn()` from `clsx + tailwind-merge`.
|
- unauthenticated users are sent through `/auth/discord`
|
||||||
|
- logout uses `POST /auth/logout`
|
||||||
|
- non-enrolled users see the `NotEnrolled` page
|
||||||
|
|
||||||
|
Roles:
|
||||||
|
|
||||||
|
- `admin`: admin routes plus player/game routes
|
||||||
|
- `player`: player/game routes only
|
||||||
|
|
||||||
|
## Active routes
|
||||||
|
|
||||||
|
- `/dashboard`
|
||||||
|
- `/leaderboards`
|
||||||
|
- `/games`
|
||||||
|
- `/:gameSlug/:roomId`
|
||||||
|
- `/admin`
|
||||||
|
- `/admin/users`
|
||||||
|
- `/admin/items`
|
||||||
|
- `/admin/classes`
|
||||||
|
- `/admin/quests`
|
||||||
|
- `/admin/lootdrops`
|
||||||
|
- `/admin/moderation`
|
||||||
|
- `/admin/transactions`
|
||||||
|
- `/admin/settings`
|
||||||
|
|
||||||
|
## Data layer
|
||||||
|
|
||||||
|
Shared hooks in `panel/src/lib`:
|
||||||
|
|
||||||
|
- `useAuth`
|
||||||
|
- `useDashboard`
|
||||||
|
- `useUsers`
|
||||||
|
- `useItems`
|
||||||
|
- `useSettings`
|
||||||
|
- `useWebSocket`
|
||||||
|
- `useGameRoom`
|
||||||
|
|
||||||
|
`panel/src/lib/api.ts` is a thin fetch wrapper:
|
||||||
|
|
||||||
|
- base path is empty because the panel is usually same-origin with the Bun server
|
||||||
|
- 401 triggers a redirect back into `/auth/discord`
|
||||||
|
- 204 and empty responses return `undefined`
|
||||||
|
|
||||||
|
## WebSocket and games
|
||||||
|
|
||||||
|
- `useWebSocket()` keeps a singleton browser WebSocket connection
|
||||||
|
- reconnects with exponential backoff up to 30 seconds
|
||||||
|
- `useGameRoom()` multiplexes room traffic over that shared socket
|
||||||
|
- current built-in game UIs are chess and blackjack
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
- Theme tokens live in `panel/src/index.css`
|
||||||
|
- Fonts currently loaded:
|
||||||
|
- Noto Serif
|
||||||
|
- Manrope
|
||||||
|
- Space Grotesk
|
||||||
|
- JetBrains Mono
|
||||||
|
- The implemented visual system is the "Stellar Editorial" direction documented in `docs/new-design/DESIGN.md`
|
||||||
|
|
||||||
|
## Current patterns
|
||||||
|
|
||||||
|
- Admin pages tend to use explicit `refetch()` after mutations instead of a shared cache layer
|
||||||
|
- Search inputs use a 300 ms debounce in hooks such as `useUsers()` and `useItems()`
|
||||||
|
- Layout and sidebar ownership lives in `panel/src/components/Layout.tsx`
|
||||||
|
|||||||
@@ -1,40 +1,79 @@
|
|||||||
# Database Layer
|
# Database layer
|
||||||
|
|
||||||
## Column Types
|
Aurora uses Drizzle ORM with PostgreSQL. Docker Compose currently runs PostgreSQL 17.
|
||||||
- **Bigint** (`mode: 'bigint'`): All Discord snowflake IDs (`userId`, `guildId`, `roleId`, `channelId`), currency amounts (`balance`, `xp`, transaction `amount`, item `price`, inventory `quantity`). Use `0n` BigInt literals, never `Number()`.
|
|
||||||
- **Integer**: Small counts and internal IDs — `level`, `daily_streak`, `progress`, `reward_amount` in lootdrops, auto-increment `serial('id')` for items.
|
|
||||||
|
|
||||||
## Composite Primary Keys
|
## Schema modules
|
||||||
- `inventory(userId, itemId)` — one stack per user per item
|
|
||||||
- `userQuests(userId, questId)` — one assignment per user per quest
|
|
||||||
- `userTimers(userId, type, key)` — one timer per user per type/key combo
|
|
||||||
|
|
||||||
## Constraints
|
- `users.ts`
|
||||||
- `inventory.quantity > 0` check constraint — never store zero-quantity rows
|
- `inventory.ts`
|
||||||
- `classes.name` and `items.name` are unique
|
- `economy.ts`
|
||||||
- Cascade deletes on user FK; set null on `relatedUserId` (preserves transaction history)
|
- `quests.ts`
|
||||||
|
- `moderation.ts`
|
||||||
|
- `feature-flags.ts`
|
||||||
|
- `guild-settings.ts`
|
||||||
|
- `game-settings.ts`
|
||||||
|
|
||||||
## JSON Columns (JSONB)
|
`shared/db/schema/index.ts` re-exports the full schema surface.
|
||||||
- `quests.requirements`: `{ target: number }`
|
|
||||||
- `quests.rewards`: `{ xp?: number, balance?: number }`
|
|
||||||
- `gameSettings` fields: Typed via `.$type<T>()` — `LevelingConfig`, `EconomyConfig`, etc.
|
|
||||||
- `guildSettings.featureOverrides`: `Record<string, boolean>` (sparse)
|
|
||||||
- `guildSettings.colorRoleIds`: `string[]`
|
|
||||||
- `users.settings`, `userTimers.metadata`, `items.usageData`: Untyped JSONB, default `{}`
|
|
||||||
|
|
||||||
## Enums
|
## Numeric conventions
|
||||||
Defined as TypeScript enums in `shared/lib/constants.ts`, **not** as database enums. Schema columns use `varchar` with length constraints. Key enum types:
|
|
||||||
- `TimerType`: COOLDOWN, EFFECT, ACCESS, EXAM_SYSTEM, TRIVIA_COOLDOWN
|
|
||||||
- `TransactionType`: TRANSFER_IN, DAILY_REWARD, etc. (8 values)
|
|
||||||
- `ModerationCaseType`: warn, timeout, kick, ban, note, prune
|
|
||||||
- `ItemType`: MATERIAL, CONSUMABLE, EQUIPMENT, QUEST
|
|
||||||
|
|
||||||
## Notable Indexes
|
- Discord IDs, balances, XP, quantities, and transaction amounts are stored as `bigint`
|
||||||
- `user_timers_lookup_idx`: Composite on (userId, type, key) — fast timer checks
|
- many API responses serialize those `bigint` values to strings
|
||||||
- `user_timers_expires_at_idx`: Expiry-based cleanup queries
|
- JSON config blobs inside `game_settings` use strings for values that become `bigint` at runtime, for example:
|
||||||
- `users_balance_idx` / `users_level_xp_idx`: Leaderboard queries
|
- `economy.daily.amount`
|
||||||
|
- `economy.daily.streakBonus`
|
||||||
|
- `economy.daily.weeklyBonus`
|
||||||
|
- `economy.transfers.minAmount`
|
||||||
|
- `inventory.maxStackSize`
|
||||||
|
- `trivia.entryFee`
|
||||||
|
|
||||||
## Client Setup
|
## Important tables
|
||||||
- `DrizzleClient` is a singleton in `shared/db/DrizzleClient.ts` (postgres-js driver, no prefetch).
|
|
||||||
- `withTransaction` utility in `bot/lib/db.ts` wraps Drizzle transactions and tracks count for graceful shutdown. Accepts optional existing `tx` for nested calls.
|
- `users`
|
||||||
- No soft deletes anywhere. `moderationCases` uses `active: boolean` + `resolvedAt`/`resolvedBy` for lifecycle, but rows are never deleted.
|
- `classes`
|
||||||
|
- `transactions`
|
||||||
|
- `item_transactions`
|
||||||
|
- `items`
|
||||||
|
- `inventory`
|
||||||
|
- `quests`
|
||||||
|
- `user_quests`
|
||||||
|
- `user_timers`
|
||||||
|
- `moderation_cases`
|
||||||
|
- `lootdrops`
|
||||||
|
- `feature_flags`
|
||||||
|
- `feature_flag_access`
|
||||||
|
- `guild_settings`
|
||||||
|
- `game_settings`
|
||||||
|
|
||||||
|
## Composite keys and constraints
|
||||||
|
|
||||||
|
- `inventory(userId, itemId)`
|
||||||
|
- `userQuests(userId, questId)`
|
||||||
|
- `userTimers(userId, type, key)`
|
||||||
|
- `inventory.quantity > 0`
|
||||||
|
- `items.name` is unique
|
||||||
|
- `feature_flags.name` is unique
|
||||||
|
|
||||||
|
## JSON columns
|
||||||
|
|
||||||
|
- `items.usageData`
|
||||||
|
- `users.settings`
|
||||||
|
- `userTimers.metadata`
|
||||||
|
- `quests.requirements`
|
||||||
|
- `quests.rewards`
|
||||||
|
- `guildSettings.colorRoleIds`
|
||||||
|
- `guildSettings.featureOverrides`
|
||||||
|
- every main section in `gameSettings`
|
||||||
|
|
||||||
|
## Relations and deletion behavior
|
||||||
|
|
||||||
|
- inventory rows cascade on user/item deletion
|
||||||
|
- `transactions.relatedUserId` and `item_transactions.relatedUserId` use `set null`
|
||||||
|
- feature flag access rows cascade when a feature flag is deleted
|
||||||
|
- moderation cases are not soft-deleted; lifecycle is represented by `active`, `resolvedAt`, and `resolvedBy`
|
||||||
|
|
||||||
|
## Client setup
|
||||||
|
|
||||||
|
- `shared/db/DrizzleClient.ts` exports the singleton DB client
|
||||||
|
- `bot/lib/db.ts` exports `withTransaction()`
|
||||||
|
- shared services normally accept an optional existing transaction so nested operations stay atomic
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
# Economy Module
|
# Economy module
|
||||||
|
|
||||||
- All currency values are `bigint`. Never use `Number()` for arithmetic on balances -- use BigInt literals (e.g., `0n`, `500n`) and `sql` template expressions for DB updates.
|
This area is split across three services:
|
||||||
- `modifyUserBalance` is the canonical way to change a user's balance. It checks for insufficient funds on negative amounts, logs a transaction record, and emits `BALANCE_CHANGED` for quest progression. Bypass it only if you have a very good reason.
|
|
||||||
- Daily rewards reset at **UTC midnight**, not 24h from last claim. The cooldown `expiresAt` is set to the next UTC 00:00:00. Streak breaks if the user misses an entire 24h window after the cooldown expired.
|
- `economy.service.ts`
|
||||||
- Daily reward is capped at `MAX_DAILY_REWARD = 500n` regardless of streak/weekly bonus.
|
- `exam.service.ts`
|
||||||
- The streak has a grace period: if a user's timer record is missing (e.g., DB migration), the code allows one "free" increment to avoid unfair resets.
|
- `lootdrop.service.ts`
|
||||||
- Weekly bonus triggers every 7th consecutive day (streak % 7 === 0).
|
|
||||||
- **Exam system**: a weekly check-in that rewards users based on XP gained since their last exam. The reward uses scaled BigInt arithmetic (`* 10000 / 10000n`) to avoid floating-point precision loss. Exams are locked to a specific day of the week set at registration time. Missing your exam day means zero reward -- there is no retroactive claim.
|
## Core rules
|
||||||
- Lootdrops use **in-memory state** (`Map`s for channel activity and cooldowns). This state is lost on restart. The DB stores only spawned/claimed drops. Claiming uses an atomic `UPDATE ... WHERE claimedBy IS NULL` to prevent race conditions.
|
|
||||||
- Lootdrops expire after 10 minutes and are cleaned up by a 60-second interval.
|
- Currency values are `bigint`
|
||||||
|
- `modifyUserBalance()` is the canonical balance mutator for most features because it logs a transaction and emits `BALANCE_CHANGED`
|
||||||
|
- direct balance updates still exist in a few flows where the service owns the full transaction, such as trivia entry/win and exam payout
|
||||||
|
|
||||||
|
## Daily rewards
|
||||||
|
|
||||||
|
- `claimDaily()` uses a UTC-midnight cooldown, not a rolling 24-hour timer
|
||||||
|
- streak bonus is linear from `config.economy.daily.streakBonus`
|
||||||
|
- weekly bonus applies every seventh claim
|
||||||
|
- total daily reward is capped at `500n`
|
||||||
|
- missing more than 24 hours after the cooldown expired resets the streak
|
||||||
|
|
||||||
|
## Weekly exam
|
||||||
|
|
||||||
|
- stored in `user_timers` with type `EXAM_SYSTEM`
|
||||||
|
- registration locks the user to the current weekday
|
||||||
|
- payout is based on XP gained since the previous exam snapshot
|
||||||
|
- missing the assigned weekday rolls the timer forward and pays nothing
|
||||||
|
|
||||||
|
## Lootdrops
|
||||||
|
|
||||||
|
- channel activity and cooldowns are kept in memory
|
||||||
|
- spawned drops are persisted in the `lootdrops` table
|
||||||
|
- claiming uses an atomic update where `claimedBy IS NULL`
|
||||||
|
- expired drops and stale activity are cleaned every 60 seconds
|
||||||
|
- spawned drops expire after 10 minutes
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
# Feature Flags Module
|
# Feature flags module
|
||||||
|
|
||||||
- **No caching.** Every `isFlagEnabled()` and `hasAccess()` call hits the database directly.
|
## Behavior
|
||||||
- `isFlagEnabled(flagName)` checks global on/off state. `hasAccess(flagName, context)` checks both global state AND per-entity access records (guild, user, or role).
|
|
||||||
- Access logic: flag must be globally enabled AND user must have an explicit access grant. Grants can target guildId, userId, or roleId independently.
|
- no in-memory caching; each check reads from the database
|
||||||
- Commands declare `beta: true` and optionally `featureFlag: string` in the Command interface. `CommandHandler` intercepts beta commands and calls `hasAccess()` before execution.
|
- a flag must exist and be globally enabled before any access grant matters
|
||||||
- If a command has no explicit `featureFlag`, the command name (`interaction.commandName`) is used as the flag name fallback.
|
- grants can target a guild, a user, or a role
|
||||||
- Flag names are case-sensitive. Convention is snake_case or camelCase — no enforcement.
|
|
||||||
- Admin management via `/featureflags` command: CRUD on flags and access grants/revokes.
|
## Main entrypoints
|
||||||
|
|
||||||
|
- `isFlagEnabled(flagName)`
|
||||||
|
- `hasAccess(flagName, { guildId, userId, memberRoles })`
|
||||||
|
- `createFlag(name, description?)`
|
||||||
|
- `setFlagEnabled(name, enabled)`
|
||||||
|
- `grantAccess(flagName, { guildId?, userId?, roleId? })`
|
||||||
|
- `revokeAccess(accessId)`
|
||||||
|
- `listFlags()`
|
||||||
|
- `listAccess(flagName)`
|
||||||
|
- `deleteFlag(name)`
|
||||||
|
|
||||||
|
## Command integration
|
||||||
|
|
||||||
|
- `bot/lib/handlers/CommandHandler.ts` checks beta commands through this service
|
||||||
|
- if a command does not set `featureFlag`, the slash command name is used as the fallback flag name
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
# Guild Settings Module
|
# Guild settings module
|
||||||
|
|
||||||
- `updateSetting()` uses a hardcoded `keyMap` to map friendly key names to DB columns. Use exact key names (e.g., `"studentRole"` not `"studentRoleId"`). Unknown keys throw `UserError`.
|
## Responsibilities
|
||||||
- Type coercion per column: Discord IDs → BigInt automatically; `colorRoleIds` must be array; `featureOverrides` must be object; `moderationDmOnWarn` must be boolean; `moderationAutoTimeoutThreshold` must be number. Null values set columns to NULL.
|
|
||||||
- **Caching:** `getGuildConfig()` (in `shared/lib/config.ts`) caches transformed settings for 60 seconds. Every mutation (`upsertSettings`, `updateSetting`, `addColorRole`, `removeColorRole`) calls `invalidateGuildConfigCache(guildId)` immediately.
|
- store raw per-guild settings in `guild_settings`
|
||||||
- If settings don't exist for a guild, the cache returns safe defaults — no errors thrown.
|
- convert DB rows to string-friendly objects for the API
|
||||||
- `featureOverrides` is a sparse `Record<string, boolean>` — no keys are predefined. Consumers must check key existence.
|
- support the cached runtime view returned by `shared/lib/config.ts`
|
||||||
- **No Discord validation:** The service does not verify that role/channel IDs actually exist in Discord. Invalid IDs are stored silently.
|
|
||||||
- `addColorRole()` / `removeColorRole()` fetch the full settings, mutate the array in JS, then upsert — this is not atomic and can race under concurrent requests.
|
## Main methods
|
||||||
- `terminalMessageId` and `terminalChannelId` are separate DB columns but grouped as `terminal: { channelId, messageId }` in the cached config. Setting one without the other can create orphaned data.
|
|
||||||
|
- `getSettings(guildId)`
|
||||||
|
- `upsertSettings({ guildId, ...fields })`
|
||||||
|
- `updateSetting(guildId, key, value)`
|
||||||
|
- `deleteSettings(guildId)`
|
||||||
|
- `addColorRole(guildId, roleId)`
|
||||||
|
- `removeColorRole(guildId, roleId)`
|
||||||
|
|
||||||
|
## Runtime cache
|
||||||
|
|
||||||
|
- `shared/lib/config.ts` caches `getGuildConfig()` results for 60 seconds
|
||||||
|
- API writes invalidate that cache immediately
|
||||||
|
- the cached runtime shape is not identical to the DB shape:
|
||||||
|
- `studentRoleId` -> `studentRole`
|
||||||
|
- `visitorRoleId` -> `visitorRole`
|
||||||
|
- `colorRoleIds` -> `colorRoles`
|
||||||
|
- terminal fields are grouped under `terminal`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `updateSetting()` accepts friendly keys like `studentRole`, `welcomeChannel`, and `terminalMessage`
|
||||||
|
- Discord IDs are stored as `bigint` in the DB and exposed as strings from the service
|
||||||
|
- `addColorRole()` and `removeColorRole()` read-modify-write the whole array, so they are not atomic under concurrent updates
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
# Inventory Module
|
# Inventory module
|
||||||
|
|
||||||
- Inventory has two hard limits from config: **max slots** (distinct item types) and **max stack size** (quantity per item). Both are enforced in `addItem` and will throw `UserError` if exceeded.
|
## Main methods
|
||||||
- When quantity reaches 0, the inventory row is **deleted** (not kept with quantity 0). This means slot count = row count.
|
|
||||||
- `buyItem` delegates balance deduction to `economyService.modifyUserBalance` within the same transaction to ensure atomicity. Never deduct balance directly when purchasing.
|
- `addItem()`
|
||||||
- Item usage is driven by a JSON `usageData` field on the item record. Items without `usageData.effects` cannot be used. The `consume` flag in `usageData` controls whether the item is removed after use.
|
- `removeItem()`
|
||||||
- **Effect system**: effects are validated at runtime via Zod (`EffectPayloadSchema`) before execution. The registry maps effect type strings to handler functions. Adding a new effect type requires: (1) add to `EffectType` enum in constants, (2) add Zod schema variant in `effect.types.ts`, (3) add handler in `effect.handlers.ts`, (4) register in `effect.registry.ts`.
|
- `getInventory()`
|
||||||
- `XP_BOOST` and `TEMP_ROLE` effects use `userTimers` with upsert -- activating while already active **replaces** the timer (does not stack or extend).
|
- `buyItem()`
|
||||||
- `TEMP_ROLE` only records the timer in DB; actual Discord role assignment must happen in the bot command layer.
|
- `getItem()`
|
||||||
- `LOOTBOX` effect uses weighted random selection. Weights are relative, not percentages. A `NOTHING` loot type is valid and intentional.
|
- `useItem()`
|
||||||
- The `getAutocompleteItems` method filters to only show items that have usable effects, so non-usable items won't appear in the `/use` autocomplete.
|
- `getAutocompleteItems()`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- max slots and max stack size come from runtime config
|
||||||
|
- removing the last quantity deletes the inventory row
|
||||||
|
- `buyItem()` uses `economyService.modifyUserBalance()` and `addItem()` in one transaction
|
||||||
|
|
||||||
|
## Item usage
|
||||||
|
|
||||||
|
- item behavior is driven by `items.usageData`
|
||||||
|
- items without `usageData.effects` are not usable
|
||||||
|
- `usageData.consume` controls whether the item is removed after use
|
||||||
|
- effect execution is routed through `effect.registry.ts`
|
||||||
|
|
||||||
|
To add a new effect type, update:
|
||||||
|
|
||||||
|
- `shared/lib/constants.ts`
|
||||||
|
- `effect.types.ts`
|
||||||
|
- `effect.handlers.ts`
|
||||||
|
- `effect.registry.ts`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- XP boost and temp-role effects are timer-based and overwrite existing timers rather than stacking
|
||||||
|
- temp-role effects only write timer data; actual Discord role assignment is handled outside this service
|
||||||
|
- autocomplete only returns usable items
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
# Leveling Module
|
# Leveling module
|
||||||
|
|
||||||
- **Level is derived, not stored.** Total XP is the source of truth. `getLevelFromXp()` recalculates level from cumulative XP on every `addXp()` call. Levels are monotonic — they never decrease.
|
## Model
|
||||||
- XP curve is a power law: `xpForLevel(n) = floor(base * n^exponent)` where defaults are `base: 100`, `exponent: 1.5`. Config comes from `gameSettingsService` (30s cache TTL).
|
|
||||||
- Chat XP (`processChatXp()`) awards random XP between `minXp` (5) and `maxXp` (15) per message, gated by a 60-second per-user cooldown (`TimerType.COOLDOWN`, key `TimerKey.CHAT_XP`). The cooldown is upserted atomically.
|
- total XP is the source of truth
|
||||||
- Quest/reward XP uses `addXp()` directly — it bypasses the chat cooldown.
|
- level is derived from XP on each award
|
||||||
- XP boost multipliers come from active `TimerType.EFFECT` timers with key `'xp_boost'` (metadata field: `multiplier`).
|
- XP curve is driven by `config.leveling.base` and `config.leveling.exponent`
|
||||||
- All XP values are `bigint` in the DB but converted to `Number` for arithmetic. Watch for overflow at extremely high XP values.
|
|
||||||
- `addXp()` and `processChatXp()` run inside transactions. They emit `XP_GAINED` (fire-and-forget) which the quest system listens to — the weight equals the XP amount.
|
## Main methods
|
||||||
|
|
||||||
|
- `getXpToReachLevel(level)`
|
||||||
|
- `getLevelFromXp(totalXp)`
|
||||||
|
- `getXpForNextLevel(currentLevel)`
|
||||||
|
- `addXp(userId, amount)`
|
||||||
|
- `processChatXp(userId)`
|
||||||
|
|
||||||
|
## Chat XP
|
||||||
|
|
||||||
|
- gated by a `user_timers` cooldown on `TimerKey.CHAT_XP`
|
||||||
|
- base award is random between `config.leveling.chat.minXp` and `maxXp`
|
||||||
|
- active XP boost timers can multiply the award
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `addXp()` emits `XP_GAINED` for quest progression
|
||||||
|
- the level curve currently converts `bigint` XP to `number` for the math loop, so extremely large totals would be the stress point to watch
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
# Moderation Module
|
# Moderation module
|
||||||
|
|
||||||
- Case IDs are sequential strings formatted as `CASE-XXXX` (zero-padded to 4 digits). Generated by querying the latest case and incrementing. Not a DB sequence -- concurrent inserts could theoretically collide, but in practice moderation actions are low-frequency.
|
## Responsibilities
|
||||||
- Only `WARN` type cases are created with `active: true`. All other case types (TIMEOUT, BAN, KICK, NOTE) default to `active: false`. The `active` flag is specifically for tracking unresolved warnings.
|
|
||||||
- `issueWarning` has two side effects beyond creating the case:
|
- create and query moderation case records
|
||||||
- **DM notification**: sends the user a warning embed via DM. Fails silently if the user has DMs disabled. Controlled by `config.dmOnWarn` (defaults to true if unset).
|
- manage active warning state
|
||||||
- **Auto-timeout**: if active warning count >= `autoTimeoutThreshold`, the user is automatically timed out for 24 hours and a separate `TIMEOUT` case is created with `moderatorId: "0"` (system). The timeout target (Discord GuildMember) is passed in from the command layer.
|
- optionally DM warned users
|
||||||
- `clearCase` sets `active: false` and records who cleared it and why. It works on any case type, not just warnings.
|
- optionally auto-timeout when warning thresholds are reached
|
||||||
- The service does **not** perform Discord actions (kick, ban, timeout) directly -- it only manages database records. The bot command layer is responsible for calling Discord APIs and then recording cases here. The one exception is auto-timeout in `issueWarning`, where the Discord member object is passed in.
|
|
||||||
- `searchCases` supports pagination via `limit`/`offset` (default limit 50).
|
## Main methods
|
||||||
|
|
||||||
|
- `createCase()`
|
||||||
|
- `issueWarning()`
|
||||||
|
- `getCaseById()`
|
||||||
|
- `getUserCases()`
|
||||||
|
- `getUserWarnings()`
|
||||||
|
- `getUserNotes()`
|
||||||
|
- `clearCase()`
|
||||||
|
- `searchCases()`
|
||||||
|
- `getActiveWarningCount()`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- case IDs are generated in application code as `CASE-0001`, `CASE-0002`, and so on
|
||||||
|
- warnings are the main case type that starts as `active: true`
|
||||||
|
- `issueWarning()` can DM the user and can create a follow-up timeout case when the configured threshold is hit
|
||||||
|
- the service stores moderation records; it does not generally execute Discord actions itself
|
||||||
|
- the auto-timeout path is the exception because a timeout target can be passed into `issueWarning()`
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
# Quest Module
|
# Quest module
|
||||||
|
|
||||||
- Quests are **event-driven**. The `handleEvent` method is called by system event listeners (not by commands directly). It matches events by exact name or prefix (e.g., trigger `ITEM_COLLECT` matches event `ITEM_COLLECT:101`), enabling both generic and specific quest triggers.
|
## Model
|
||||||
- Max active quests is controlled by `gameSettingsService`, not hardcoded. Default is 3.
|
|
||||||
- `assignQuest` uses `onConflictDoNothing` -- re-assigning an already-assigned quest silently no-ops. This is intentional to avoid duplicate quest entries.
|
- quests are event-driven
|
||||||
- Quest progress is a simple integer counter. The `weight` parameter in `handleEvent` allows a single event to advance progress by more than 1 (useful for bulk actions).
|
- user progress is stored in `user_quests`
|
||||||
- Quest completion is **automatic**: when progress >= target during `handleEvent`, `completeQuest` is called within the same transaction. There is no manual "turn in" step.
|
- completion is automatic once progress reaches the quest target
|
||||||
- Rewards (xp and balance) are distributed via `economyService` and `levelingService` inside the completion transaction. The `QUEST.COMPLETED` event is emitted with `systemEvents.emit` (fire-and-forget, not async) for bot-layer notifications.
|
|
||||||
- `requirements` and `rewards` are stored as JSON columns. Always expect `{ target: number }` for requirements and `{ xp?: number, balance?: number }` for rewards.
|
## Main methods
|
||||||
- Completed quests are never deleted -- they stay in `userQuests` with a `completedAt` timestamp. `getAvailableQuests` excludes any quest the user has ever been assigned (completed or not).
|
|
||||||
|
- `assignQuest()`
|
||||||
|
- `updateProgress()`
|
||||||
|
- `handleEvent()`
|
||||||
|
- `completeQuest()`
|
||||||
|
- `getUserQuests()`
|
||||||
|
- `getAvailableQuests()`
|
||||||
|
- `createQuest()`
|
||||||
|
- `getAllQuests()`
|
||||||
|
- `deleteQuest()`
|
||||||
|
- `updateQuest()`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- max active quests comes from `gameSettingsService`
|
||||||
|
- `assignQuest()` uses `onConflictDoNothing()`
|
||||||
|
- `handleEvent()` matches either exact trigger names or `trigger:` prefixes, for example `ITEM_COLLECTED:42`
|
||||||
|
- rewards can include balance and XP and are paid inside the completion transaction
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- completed assignments remain in `user_quests`
|
||||||
|
- `EVENTS.QUEST.COMPLETED` is emitted for the bot/UI layer after reward distribution
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
# Trade Module
|
# Trade module
|
||||||
|
|
||||||
- Trade sessions are stored **in-memory only** (a `Map` keyed by thread ID). Sessions are lost on restart. There is no persistence or recovery mechanism.
|
## Model
|
||||||
- The trade uses a **two-phase lock** pattern: both users must `toggleLock` (accept) before `executeTrade` can proceed. Any offer modification (add/remove item, change money) automatically **unlocks both users**, forcing re-confirmation. This prevents bait-and-switch.
|
|
||||||
- `executeTrade` wraps both directions of transfer in a single DB transaction. If any part fails (e.g., insufficient funds, inventory full), the entire trade rolls back.
|
- trade sessions are in-memory only
|
||||||
- Money and item transfers go through `economyService.modifyUserBalance` and `inventoryService.addItem`/`removeItem`, which means all their validation (balance checks, stack limits, slot limits) and side effects (transaction logging, quest events) apply.
|
- sessions are keyed by Discord thread ID
|
||||||
- Item transactions are logged separately in the `itemTransactions` table (distinct from currency `transactions`), with `TRADE_IN`/`TRADE_OUT` types.
|
- there is no persistence or restart recovery
|
||||||
- The trade types are defined in `bot/modules/trade/trade.types.ts` but the service lives in `shared/modules/trade/`. The import uses `@/modules/trade/trade.types` (bot alias). This cross-boundary import works because both run in the same process.
|
|
||||||
- `_sessions` is exposed on the service object for testing purposes only.
|
## Main methods
|
||||||
|
|
||||||
|
- `createSession()`
|
||||||
|
- `getSession()`
|
||||||
|
- `endSession()`
|
||||||
|
- `updateMoney()`
|
||||||
|
- `addItem()`
|
||||||
|
- `removeItem()`
|
||||||
|
- `toggleLock()`
|
||||||
|
- `executeTrade()`
|
||||||
|
- `clearSessions()`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- any change to money or items unlocks both participants
|
||||||
|
- `executeTrade()` requires both users to be locked
|
||||||
|
- money transfer goes through `economyService.modifyUserBalance()`
|
||||||
|
- item transfer goes through `inventoryService.removeItem()` and `addItem()`
|
||||||
|
- item transfers are also logged in `item_transactions`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `_sessions` is intentionally exposed for tests
|
||||||
|
- type definitions live under `bot/modules/trade/trade.types.ts`, while the service stays in `shared/modules/trade`
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
# Trivia Module
|
# Trivia module
|
||||||
|
|
||||||
- Trivia is an **economic sink**: the entry fee is deducted immediately when starting, before the question is fetched. If the API call fails after payment, the user loses the fee. This is by design (prevents free retries).
|
## Model
|
||||||
- Questions come from the **OpenTDB API** with base64 encoding to avoid HTML entity issues. The service decodes all fields from base64 before returning.
|
|
||||||
- Sessions are in-memory (`Map` keyed by `userId_timestamp`). Lost on restart. Expired sessions are cleaned up every 30 seconds.
|
- `triviaService` is a class-backed singleton
|
||||||
- The cooldown is set **at session start**, not on answer submission. This means a user is on cooldown even if they never answer.
|
- active sessions live in memory
|
||||||
- Answer correctness (`isCorrect`) is determined by the **caller** (interaction handler), not the service. The `submitAnswer` method trusts the `isCorrect` boolean. The session stores `correctIndex` for the UI layer to compare.
|
- expired sessions are cleaned every 30 seconds
|
||||||
- Reward calculation: `potentialReward = entryFee * rewardMultiplier`. The multiplier comes from config. Wrong answers get 0 (the entry fee is already gone).
|
|
||||||
- Unlike most services, `TriviaService` is a **class instance** (not a plain object). This is because it needs constructor logic for the cleanup interval. The singleton is exported as `triviaService`.
|
## Flow
|
||||||
- The reward payment in `submitAnswer` reads the current balance and sets it directly (not using `sql` addition). This is a potential race condition under extreme concurrency but acceptable given the per-user cooldown.
|
|
||||||
- Session is deleted before processing the reward to prevent double-submit, even if the reward transaction fails.
|
1. `canPlayTrivia()` checks the cooldown timer.
|
||||||
|
2. `startTrivia()` deducts the entry fee, fetches a question from OpenTDB, creates the session, and sets the cooldown.
|
||||||
|
3. The bot view layer renders answer buttons using the stored shuffled answers and `correctIndex`.
|
||||||
|
4. `submitAnswer()` removes the session and pays the reward only if the caller says the answer was correct.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- questions are fetched from OpenTDB with `encode=base64` and decoded server-side
|
||||||
|
- entry fee is deducted before the question fetch completes
|
||||||
|
- cooldown is applied when the session starts, not when the answer is submitted
|
||||||
|
- `submitAnswer()` trusts the caller's `isCorrect` boolean
|
||||||
|
- reward payment currently reads and writes the balance directly inside the transaction instead of using `modifyUserBalance()`
|
||||||
|
|||||||
Reference in New Issue
Block a user