From 0cc295f902471e550ab045519e5e4b7a2cb00ded Mon Sep 17 00:00:00 2001
From: Shellishack <40737228+Shellishack@users.noreply.github.com>
Date: Mon, 6 Apr 2026 03:30:16 +0800
Subject: [PATCH 1/3] Replace nextjs with react+vite
---
packages/app/api/index.ts | 32 +-
packages/app/api/routes/tenant.ts | 1 +
packages/app/package.json | 13 +-
packages/app/shared/index.ts | 2 +
packages/app/web/AGENTS.md | 7 +-
packages/app/web/app/layout.tsx | 18 -
.../DashboardLayout.tsx} | 27 +-
packages/app/web/app/main.tsx | 39 +
.../page.tsx => pages/AiBackends.tsx} | 1 -
.../channels/page.tsx => pages/Channels.tsx} | 12 +-
.../page.tsx => pages/Conversations.tsx} | 16 +-
.../page.tsx => pages/Dashboard.tsx} | 5 +-
.../end-users/page.tsx => pages/EndUsers.tsx} | 1 -
.../app/{login/page.tsx => pages/Login.tsx} | 13 +-
.../{onboard/page.tsx => pages/Onboard.tsx} | 35 +-
.../page.tsx => pages/Onboarding.tsx} | 1 -
.../{register/page.tsx => pages/Register.tsx} | 13 +-
.../settings/page.tsx => pages/Settings.tsx} | 17 +-
.../users/page.tsx => pages/Users.tsx} | 1 -
packages/app/web/eslint.config.mjs | 13 +-
packages/app/web/index.html | 13 +
packages/app/web/lib/api.ts | 2 +-
packages/app/web/next.config.ts | 9 -
packages/app/web/postcss.config.mjs | 7 -
packages/app/web/public/file.svg | 1 -
packages/app/web/public/globe.svg | 1 -
packages/app/web/public/next.svg | 1 -
packages/app/web/public/vercel.svg | 1 -
packages/app/web/public/window.svg | 1 -
packages/app/web/tsconfig.json | 19 +-
packages/app/web/vite.config.ts | 18 +
pnpm-lock.yaml | 2743 +++--------------
32 files changed, 537 insertions(+), 2546 deletions(-)
delete mode 100644 packages/app/web/app/layout.tsx
rename packages/app/web/app/{(dashboard)/layout.tsx => layouts/DashboardLayout.tsx} (85%)
create mode 100644 packages/app/web/app/main.tsx
rename packages/app/web/app/{(dashboard)/ai-backends/page.tsx => pages/AiBackends.tsx} (99%)
rename packages/app/web/app/{(dashboard)/channels/page.tsx => pages/Channels.tsx} (98%)
rename packages/app/web/app/{(dashboard)/conversations/page.tsx => pages/Conversations.tsx} (93%)
rename packages/app/web/app/{(dashboard)/page.tsx => pages/Dashboard.tsx} (96%)
rename packages/app/web/app/{(dashboard)/end-users/page.tsx => pages/EndUsers.tsx} (99%)
rename packages/app/web/app/{login/page.tsx => pages/Login.tsx} (86%)
rename packages/app/web/app/{onboard/page.tsx => pages/Onboard.tsx} (94%)
rename packages/app/web/app/{(dashboard)/onboarding/page.tsx => pages/Onboarding.tsx} (99%)
rename packages/app/web/app/{register/page.tsx => pages/Register.tsx} (91%)
rename packages/app/web/app/{(dashboard)/settings/page.tsx => pages/Settings.tsx} (93%)
rename packages/app/web/app/{(dashboard)/users/page.tsx => pages/Users.tsx} (99%)
create mode 100644 packages/app/web/index.html
delete mode 100644 packages/app/web/next.config.ts
delete mode 100644 packages/app/web/postcss.config.mjs
delete mode 100644 packages/app/web/public/file.svg
delete mode 100644 packages/app/web/public/globe.svg
delete mode 100644 packages/app/web/public/next.svg
delete mode 100644 packages/app/web/public/vercel.svg
delete mode 100644 packages/app/web/public/window.svg
create mode 100644 packages/app/web/vite.config.ts
diff --git a/packages/app/api/index.ts b/packages/app/api/index.ts
index 493d7c4..893f156 100644
--- a/packages/app/api/index.ts
+++ b/packages/app/api/index.ts
@@ -57,14 +57,8 @@ app.use('/api/onboard', onboardRouter);
app.use('/gateway', gatewayRouter);
-// ─── Static file serving (production) ────────────────────────────────────────
+// ─── API/gateway 404 ─────────────────────────────────────────────────────────
-const webRoot = path.resolve(process.cwd(), 'web', 'out');
-app.use(express.static(webRoot));
-
-// ─── Fallback ─────────────────────────────────────────────────────────────────
-
-// API/gateway 404
app.use('/api', (_req, res) => {
res.status(404).json({ ok: false, error: 'Not found' });
});
@@ -75,10 +69,26 @@ app.use('/gateway', (_req, res) => {
res.status(404).json({ ok: false, error: 'Not found' });
});
-// SPA fallback — serve index.html for all other routes
-app.use((_req, res) => {
- res.sendFile(path.join(webRoot, 'index.html'));
-});
+// ─── Frontend serving ────────────────────────────────────────────────────────
+
+const isDev = process.env.NODE_ENV !== 'production';
+const webRoot = path.resolve(process.cwd(), 'web/dist');
+
+if (isDev) {
+ // Embed Vite dev server with HMR directly into Express
+ const { createServer: createViteServer } = await import('vite');
+ const vite = await createViteServer({
+ configFile: path.resolve(process.cwd(), 'web/vite.config.ts'),
+ server: { middlewareMode: true },
+ });
+ app.use(vite.middlewares);
+} else {
+ // Production: serve the static build
+ app.use(express.static(webRoot));
+ app.use((_req, res) => {
+ res.sendFile(path.join(webRoot, 'index.html'));
+ });
+}
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
diff --git a/packages/app/api/routes/tenant.ts b/packages/app/api/routes/tenant.ts
index 0172edf..293420e 100644
--- a/packages/app/api/routes/tenant.ts
+++ b/packages/app/api/routes/tenant.ts
@@ -9,6 +9,7 @@ const updateSettingsSchema = z.object({
name: z.string().min(2).max(80).optional(),
settings: z
.object({
+ siteTitle: z.string().max(100).optional(),
personaName: z.string().min(1).max(80).optional(),
personaPrompt: z.string().max(4000).optional(),
endUserAccess: z.enum(['anonymous', 'whitelist', 'blacklist']).optional(),
diff --git a/packages/app/package.json b/packages/app/package.json
index 98a0c2f..712f5ee 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -4,9 +4,9 @@
"private": true,
"type": "module",
"scripts": {
- "dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 tsx watch api/index.ts",
- "dev:web": "next dev web --port 4040",
- "build": "tsc -p tsconfig.json && next build web",
+ "dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 tsx api/index.ts",
+ "dev:web": "vite --config web/vite.config.ts",
+ "build": "tsc -p tsconfig.json && vite build --config web/vite.config.ts",
"start": "node dist/api/index.js",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
@@ -40,12 +40,12 @@
"matrix-js-sdk": "^41.2.0",
"morgan": "^1.10.1",
"nanoid": "^5.1.5",
- "next": "16.2.1",
"openai": "^6.33.0",
"pino": "^10.3.1",
"qrcode": "^1.5.4",
"react": "19.2.4",
"react-dom": "19.2.4",
+ "react-router-dom": "^7.14.0",
"slugify": "^1.6.6",
"tailwind-merge": "^3.5.0",
"ws": "^8.20.0",
@@ -53,7 +53,7 @@
"zod": "^3.24.4"
},
"devDependencies": {
- "@tailwindcss/postcss": "^4",
+ "@tailwindcss/vite": "^4.2.2",
"@types/bcryptjs": "^2.4.6",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.9",
@@ -63,14 +63,15 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/ws": "^8.18.1",
+ "@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^3.2.4",
"cross-env": "^10.1.0",
"eslint": "^10",
- "eslint-config-next": "16.2.1",
"prisma": "^6.6.0",
"tailwindcss": "^4",
"tsx": "^4.19.4",
"typescript": "^5.8.3",
+ "vite": "^8.0.3",
"vitest": "^3.1.3"
}
}
diff --git a/packages/app/shared/index.ts b/packages/app/shared/index.ts
index 71329b3..2ccf08e 100644
--- a/packages/app/shared/index.ts
+++ b/packages/app/shared/index.ts
@@ -49,6 +49,8 @@ export interface OnboardingBranding {
}
export interface TenantSettings {
+ /** Custom browser tab title (falls back to tenant name if not set) */
+ siteTitle?: string;
/** Display name for the AI persona shown to end-users */
personaName: string;
/** System prompt that defines the bot's behaviour */
diff --git a/packages/app/web/AGENTS.md b/packages/app/web/AGENTS.md
index 8bd0e39..64f55c2 100644
--- a/packages/app/web/AGENTS.md
+++ b/packages/app/web/AGENTS.md
@@ -1,5 +1,4 @@
-
-# This is NOT the Next.js you know
+# Web App
-This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
-
+Vite + React SPA with React Router for routing and Tailwind CSS v4 for styling.
+Express serves the built static files from `web/dist` in production.
diff --git a/packages/app/web/app/layout.tsx b/packages/app/web/app/layout.tsx
deleted file mode 100644
index aa488bf..0000000
--- a/packages/app/web/app/layout.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Metadata } from "next";
-import "./globals.css";
-
-export const metadata: Metadata = {
- title: "ClawScale",
- description: "ClawScale — multi-user, multi-agent IM chatbot gateway",
- icons: { icon: "/logo.png" },
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{ children: React.ReactNode }>) {
- return (
-
-
{children}
-
- );
-}
diff --git a/packages/app/web/app/(dashboard)/layout.tsx b/packages/app/web/app/layouts/DashboardLayout.tsx
similarity index 85%
rename from packages/app/web/app/(dashboard)/layout.tsx
rename to packages/app/web/app/layouts/DashboardLayout.tsx
index b2cfd7e..04b2128 100644
--- a/packages/app/web/app/(dashboard)/layout.tsx
+++ b/packages/app/web/app/layouts/DashboardLayout.tsx
@@ -1,8 +1,5 @@
-'use client';
import { useEffect, useState } from 'react';
-import Link from 'next/link';
-import { useRouter, usePathname } from 'next/navigation';
-import Image from 'next/image';
+import { Link, Outlet, useNavigate, useLocation } from 'react-router-dom';
import { LayoutDashboard, Users, UserCheck, Radio, Settings, LogOut, MessageSquare, BotMessageSquare, Globe } from 'lucide-react';
import { isAuthenticated, clearAuth, getUser, getTenant } from '@/lib/auth';
import { cn } from '@/lib/utils';
@@ -18,18 +15,20 @@ const navItems: { href: string; icon: typeof LayoutDashboard; label: string; exa
{ href: '/settings', icon: Settings, label: 'Settings' },
];
-export default function DashboardLayout({ children }: { children: React.ReactNode }) {
- const router = useRouter();
- const pathname = usePathname();
+export default function DashboardLayout() {
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
const [ready, setReady] = useState(false);
useEffect(() => {
if (!isAuthenticated()) {
- router.replace('/login');
+ navigate('/login', { replace: true });
} else {
setReady(true);
+ const t = getTenant();
+ if (t) document.title = t.settings?.siteTitle || `${t.name} — ClawScale`;
}
- }, [router]);
+ }, [navigate]);
if (!ready) return null;
@@ -38,14 +37,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
function handleLogout() {
clearAuth();
- router.push('/login');
+ navigate('/login');
}
return (