diff --git a/.github/workflows/railway-preview.yml b/.github/workflows/railway-preview.yml
new file mode 100644
index 000000000..c6dd50f9a
--- /dev/null
+++ b/.github/workflows/railway-preview.yml
@@ -0,0 +1,149 @@
+name: Railway Preview Deploy
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches: [main]
+
+concurrency:
+ group: railway-preview-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ deploy-preview:
+ name: Deploy to Railway Preview
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.head.repo.full_name == github.repository
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6.0.2
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6.2.0
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build web dashboard
+ run: cd web && pnpm build
+ env:
+ NEXT_PUBLIC_BOT_API_URL: ${{ secrets.NEXT_PUBLIC_BOT_API_URL }}
+ NEXT_PUBLIC_DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
+ NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL_PREVIEW || 'https://preview.volvox.dev' }}
+ NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
+ DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
+ DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
+
+ - name: Install Railway CLI
+ run: npm install -g @railway/cli
+
+ - name: Deploy to Railway Preview
+ id: railway-deploy
+ run: |
+ # Generate a unique preview environment name based on PR number
+ PREVIEW_ENV="pr-${{ github.event.number }}"
+ echo "Deploying to preview environment: $PREVIEW_ENV"
+
+ # Deploy using Railway CLI
+ railway up --service=volvox-bot --environment=$PREVIEW_ENV --detach
+
+ # Get the deployment URL
+ DEPLOY_URL=$(railway domain --service=volvox-bot --environment=$PREVIEW_ENV)
+ echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT
+ env:
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
+
+ - name: Comment PR with preview URL
+ uses: actions/github-script@v7.0.1
+ with:
+ script: |
+ const deployUrl = '${{ steps.railway-deploy.outputs.deploy_url }}';
+ const prNumber = context.issue.number;
+
+ // Find existing comment
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ });
+
+ const botComment = comments.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('๐ Railway Preview Deployment')
+ );
+
+ const body = `## ๐ Railway Preview Deployment
+
+ Your PR has been deployed to a preview environment!
+
+ **Preview URL:** ${deployUrl}
+
+ **Environment:** \`pr-${prNumber}\`
+
+ This deployment will be updated automatically when you push new commits.
+
+ ---
+ *Last updated: ${new Date().toISOString()}*`;
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: body,
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: body,
+ });
+ }
+
+ cleanup-preview:
+ name: Cleanup Railway Preview
+ runs-on: ubuntu-latest
+ if: github.event.action == 'closed'
+
+ steps:
+ - name: Install Railway CLI
+ run: npm install -g @railway/cli
+
+ - name: Remove Preview Environment
+ run: |
+ PREVIEW_ENV="pr-${{ github.event.number }}"
+ echo "Removing preview environment: $PREVIEW_ENV"
+ railway environment delete $PREVIEW_ENV --yes || true
+ env:
+ RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
+
+ - name: Comment PR about cleanup
+ uses: actions/github-script@v7.0.1
+ with:
+ script: |
+ const prNumber = context.issue.number;
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: `## ๐งน Preview Environment Cleaned Up
+
+ The Railway preview environment for this PR has been removed.
+
+ **Environment:** \`pr-${prNumber}\``,
+ });
diff --git a/.gitignore b/.gitignore
index d9a3dd071..8ab5a75cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,5 @@ web/.env.local
web/.env.*.local
web/tsconfig.tsbuildinfo
-# Worktree marker
+# OpenClaw
+openclaw-studio/
diff --git a/docs/railway.toml b/docs/railway.toml
index 8c5492404..db46e804d 100644
--- a/docs/railway.toml
+++ b/docs/railway.toml
@@ -1,6 +1,5 @@
[build]
builder = "DOCKERFILE"
-dockerfilePath = "Dockerfile"
[deploy]
restartPolicyType = "ON_FAILURE"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cee4d8562..b29c06a7a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -106,6 +106,9 @@ importers:
diff:
specifier: ^8.0.3
version: 8.0.3
+ framer-motion:
+ specifier: ^12.34.5
+ version: 12.34.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
lucide-react:
specifier: ^0.525.0
version: 0.525.0(react@19.2.4)
@@ -3273,6 +3276,20 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
+ framer-motion@12.34.5:
+ resolution: {integrity: sha512-Z2dQ+o7BsfpJI3+u0SQUNCrN+ajCKJen1blC4rCHx1Ta2EOHs+xKJegLT2aaD9iSMbU3OoX+WabQXkloUbZmJQ==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -3950,6 +3967,12 @@ packages:
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+ motion-dom@12.34.5:
+ resolution: {integrity: sha512-k33CsnxO2K3gBRMUZT+vPmc4Utlb5menKdG0RyVNLtlqRaaJPRWlE9fXl8NTtfZ5z3G8TDvqSu0MENLqSTaHZA==}
+
+ motion-utils@12.29.2:
+ resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -8288,6 +8311,15 @@ snapshots:
forwarded@0.2.0: {}
+ framer-motion@12.34.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ motion-dom: 12.34.5
+ motion-utils: 12.29.2
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
fresh@2.0.0: {}
fs-constants@1.0.0: {}
@@ -9022,6 +9054,12 @@ snapshots:
moment@2.30.1: {}
+ motion-dom@12.34.5:
+ dependencies:
+ motion-utils: 12.29.2
+
+ motion-utils@12.29.2: {}
+
ms@2.1.3: {}
mustache@4.2.0: {}
diff --git a/web/next-env.d.ts b/web/next-env.d.ts
index 9edff1c7c..c4b7818fb 100644
--- a/web/next-env.d.ts
+++ b/web/next-env.d.ts
@@ -1,6 +1,6 @@
///
- The AI-powered Discord bot for the Volvox community. Moderation, AI chat, dynamic - welcomes, spam detection, and a fully configurable web dashboard. -
-- A full-featured Discord bot with a modern web dashboard. -
-- Add Bill Bot to your Discord server and manage everything from this dashboard. -
-{feature.description}
++ Everything you need, nothing you don't. Built by developers who actually use Discord. +
++ From hobby projects to enterprise guilds. +
+ + {/* Toggle */} +{tier.description}
++ Save ${tier.price.monthly * 12 - tier.price.annual}/year +
+ )} +{t.quote}
++ Trusted by teams at leading tech companies and thousands of open-source communities +
+