diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3729b3e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ +## Summary + + +## Changes + +- + +## Testing + +- + +## Checklist +- [ ] Branched off `main` +- [ ] One focused change (small PR) +- [ ] Commits include a `Co-authored-by:` trailer where applicable +- [ ] Verified locally (no `npm run build` while the dev server is running) diff --git a/.kilo/kilo.json b/.kilo/kilo.json new file mode 100644 index 0000000..b27fd20 --- /dev/null +++ b/.kilo/kilo.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://app.kilo.ai/config.json", + "skills": { + "paths": ["/home/m25/.agents/skills"] + } +} \ No newline at end of file diff --git a/.zscripts/build.sh b/.zscripts/build.sh deleted file mode 100755 index f661c7f..0000000 --- a/.zscripts/build.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash - -# 将 stderr 重定向到 stdout,避免 execute_command 因为 stderr 输出而报错 -exec 2>&1 - -set -e - -# 获取脚本所在目录(.zscripts 目录,即 workspace-agent/.zscripts) -# 使用 $0 获取脚本路径(兼容 sh 和 bash) -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# Next.js 项目路径 -NEXTJS_PROJECT_DIR="/home/z/my-project" - -# 检查 Next.js 项目目录是否存在 -if [ ! -d "$NEXTJS_PROJECT_DIR" ]; then - echo "❌ 错误: Next.js 项目目录不存在: $NEXTJS_PROJECT_DIR" - exit 1 -fi - -echo "🚀 开始构建 Next.js 应用和 mini-services..." -echo "📁 Next.js 项目路径: $NEXTJS_PROJECT_DIR" - -# 切换到 Next.js 项目目录 -cd "$NEXTJS_PROJECT_DIR" || exit 1 - -# 设置环境变量 -export NEXT_TELEMETRY_DISABLED=1 - -BUILD_DIR="/tmp/build_fullstack_$BUILD_ID" -echo "📁 清理并创建构建目录: $BUILD_DIR" -mkdir -p "$BUILD_DIR" - -# 安装依赖 -echo "📦 安装依赖..." -bun install - -# 构建 Next.js 应用 -echo "🔨 构建 Next.js 应用..." -bun run build - -# 构建 mini-services -# 检查 Next.js 项目目录下是否有 mini-services 目录 -if [ -d "$NEXTJS_PROJECT_DIR/mini-services" ]; then - echo "🔨 构建 mini-services..." - # 使用 workspace-agent 目录下的 mini-services 脚本 - sh "$SCRIPT_DIR/mini-services-install.sh" - sh "$SCRIPT_DIR/mini-services-build.sh" - - # 复制 mini-services-start.sh 到 mini-services-dist 目录 - echo " - 复制 mini-services-start.sh 到 $BUILD_DIR" - cp "$SCRIPT_DIR/mini-services-start.sh" "$BUILD_DIR/mini-services-start.sh" - chmod +x "$BUILD_DIR/mini-services-start.sh" -else - echo "ℹ️ mini-services 目录不存在,跳过" -fi - -# 将所有构建产物复制到临时构建目录 -echo "📦 收集构建产物到 $BUILD_DIR..." - -# 复制 Next.js standalone 构建输出 -if [ -d ".next/standalone" ]; then - echo " - 复制 .next/standalone" - cp -r .next/standalone "$BUILD_DIR/next-service-dist/" -fi - -# 复制 Next.js 静态文件 -if [ -d ".next/static" ]; then - echo " - 复制 .next/static" - mkdir -p "$BUILD_DIR/next-service-dist/.next" - cp -r .next/static "$BUILD_DIR/next-service-dist/.next/" -fi - -# 复制 public 目录 -if [ -d "public" ]; then - echo " - 复制 public" - cp -r public "$BUILD_DIR/next-service-dist/" -fi - -# 将测试环境数据库复制到构建产物中,生产环境直接使用这份数据库 -if [ -f "./db/custom.db" ]; then - echo "🗄️ 复制测试环境数据库到构建产物..." - mkdir -p "$BUILD_DIR/db" - cp -r ./db/. "$BUILD_DIR/db/" - - echo "🗄️ 同步构建产物中的数据库结构..." - DATABASE_URL="file:$BUILD_DIR/db/custom.db" bun run db:push - echo "✅ 构建产物数据库已准备完成" - ls -lah "$BUILD_DIR/db" -else - echo "❌ 未找到测试环境数据库文件 ./db/custom.db,无法继续构建生产包" - exit 1 -fi - -# 复制 Caddyfile(如果存在) -if [ -f "Caddyfile" ]; then - echo " - 复制 Caddyfile" - cp Caddyfile "$BUILD_DIR/" -else - echo "ℹ️ Caddyfile 不存在,跳过" -fi - -# 复制 start.sh 脚本 -echo " - 复制 start.sh 到 $BUILD_DIR" -cp "$SCRIPT_DIR/start.sh" "$BUILD_DIR/start.sh" -chmod +x "$BUILD_DIR/start.sh" - -# 打包到 $BUILD_DIR.tar.gz -PACKAGE_FILE="${BUILD_DIR}.tar.gz" -echo "" -echo "📦 打包构建产物到 $PACKAGE_FILE..." -cd "$BUILD_DIR" || exit 1 -tar -czf "$PACKAGE_FILE" . -cd - > /dev/null || exit 1 - -# # 清理临时目录 -# rm -rf "$BUILD_DIR" - -echo "" -echo "✅ 构建完成!所有产物已打包到 $PACKAGE_FILE" -echo "📊 打包文件大小:" -ls -lh "$PACKAGE_FILE" diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid deleted file mode 100644 index 47d947c..0000000 --- a/.zscripts/dev.pid +++ /dev/null @@ -1 +0,0 @@ -23340 diff --git a/.zscripts/dev.sh b/.zscripts/dev.sh deleted file mode 100755 index 8705793..0000000 --- a/.zscripts/dev.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# 获取脚本所在目录(.zscripts) -# 使用 $0 获取脚本路径(与 build.sh 保持一致) -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" - -log_step_start() { - local step_name="$1" - echo "==========================================" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting: $step_name" - echo "==========================================" - export STEP_START_TIME - STEP_START_TIME=$(date +%s) -} - -log_step_end() { - local step_name="${1:-Unknown step}" - local end_time - end_time=$(date +%s) - local duration=$((end_time - STEP_START_TIME)) - echo "==========================================" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Completed: $step_name" - echo "[LOG] Step: $step_name | Duration: ${duration}s" - echo "==========================================" - echo "" -} - -start_mini_services() { - local mini_services_dir="$PROJECT_DIR/mini-services" - local started_count=0 - - log_step_start "Starting mini-services" - if [ ! -d "$mini_services_dir" ]; then - echo "Mini-services directory not found, skipping..." - log_step_end "Starting mini-services" - return 0 - fi - - echo "Found mini-services directory, scanning for sub-services..." - - for service_dir in "$mini_services_dir"/*; do - if [ ! -d "$service_dir" ]; then - continue - fi - - local service_name - service_name=$(basename "$service_dir") - echo "Checking service: $service_name" - - if [ ! -f "$service_dir/package.json" ]; then - echo "[$service_name] No package.json found, skipping..." - continue - fi - - if ! grep -q '"dev"' "$service_dir/package.json"; then - echo "[$service_name] No dev script found, skipping..." - continue - fi - - echo "Starting $service_name in background..." - ( - cd "$service_dir" - echo "[$service_name] Installing dependencies..." - bun install - echo "[$service_name] Running bun run dev..." - exec bun run dev - ) >"$PROJECT_DIR/.zscripts/mini-service-${service_name}.log" 2>&1 & - - local service_pid=$! - echo "[$service_name] Started in background (PID: $service_pid)" - echo "[$service_name] Log: $PROJECT_DIR/.zscripts/mini-service-${service_name}.log" - disown "$service_pid" 2>/dev/null || true - started_count=$((started_count + 1)) - done - - echo "Mini-services startup completed. Started $started_count service(s)." - log_step_end "Starting mini-services" -} - -wait_for_service() { - local host="$1" - local port="$2" - local service_name="$3" - local max_attempts="${4:-60}" - local attempt=1 - - echo "Waiting for $service_name to be ready on $host:$port..." - - while [ "$attempt" -le "$max_attempts" ]; do - if curl -s --connect-timeout 2 --max-time 5 "http://$host:$port" >/dev/null 2>&1; then - echo "$service_name is ready!" - return 0 - fi - - echo "Attempt $attempt/$max_attempts: $service_name not ready yet, waiting..." - sleep 1 - attempt=$((attempt + 1)) - done - - echo "ERROR: $service_name failed to start within $max_attempts seconds" - return 1 -} - -cleanup() { - if [ -n "${DEV_PID:-}" ] && kill -0 "$DEV_PID" >/dev/null 2>&1; then - echo "Stopping Next.js dev server (PID: $DEV_PID)..." - kill "$DEV_PID" >/dev/null 2>&1 || true - fi -} - -trap cleanup EXIT INT TERM - -cd "$PROJECT_DIR" - -if ! command -v bun >/dev/null 2>&1; then - echo "ERROR: bun is not installed or not in PATH" - exit 1 -fi - -log_step_start "bun install" -echo "[BUN] Installing dependencies..." -bun install -log_step_end "bun install" - -log_step_start "bun run db:push" -echo "[BUN] Setting up database..." -bun run db:push -log_step_end "bun run db:push" - -log_step_start "Starting Next.js dev server" -echo "[BUN] Starting development server..." -bun run dev & -DEV_PID=$! -log_step_end "Starting Next.js dev server" - -log_step_start "Waiting for Next.js dev server" -wait_for_service "localhost" "3000" "Next.js dev server" -log_step_end "Waiting for Next.js dev server" - -log_step_start "Health check" -echo "[BUN] Performing health check..." -curl -fsS localhost:3000 >/dev/null -echo "[BUN] Health check passed" -log_step_end "Health check" - -start_mini_services - -echo "Next.js dev server is running in background (PID: $DEV_PID)." -echo "Use 'kill $DEV_PID' to stop it." -disown "$DEV_PID" 2>/dev/null || true -unset DEV_PID diff --git a/.zscripts/mini-services-build.sh b/.zscripts/mini-services-build.sh deleted file mode 100755 index 1e3d39d..0000000 --- a/.zscripts/mini-services-build.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -# 配置项 -ROOT_DIR="/home/z/my-project/mini-services" -DIST_DIR="/tmp/build_fullstack_$BUILD_ID/mini-services-dist" - -main() { - echo "🚀 开始批量构建..." - - # 检查 rootdir 是否存在 - if [ ! -d "$ROOT_DIR" ]; then - echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过构建" - return - fi - - # 创建输出目录(如果不存在) - mkdir -p "$DIST_DIR" - - # 统计变量 - success_count=0 - fail_count=0 - - # 遍历 mini-services 目录下的所有文件夹 - for dir in "$ROOT_DIR"/*; do - # 检查是否是目录且包含 package.json - if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then - project_name=$(basename "$dir") - - # 智能查找入口文件 (按优先级查找) - entry_path="" - for entry in "src/index.ts" "index.ts" "src/index.js" "index.js"; do - if [ -f "$dir/$entry" ]; then - entry_path="$dir/$entry" - break - fi - done - - if [ -z "$entry_path" ]; then - echo "⚠️ 跳过 $project_name: 未找到入口文件 (index.ts/js)" - continue - fi - - echo "" - echo "📦 正在构建: $project_name..." - - # 使用 bun build CLI 构建 - output_file="$DIST_DIR/mini-service-$project_name.js" - - if bun build "$entry_path" \ - --outfile "$output_file" \ - --target bun \ - --minify; then - echo "✅ $project_name 构建成功 -> $output_file" - success_count=$((success_count + 1)) - else - echo "❌ $project_name 构建失败" - fail_count=$((fail_count + 1)) - fi - fi - done - - if [ -f ./.zscripts/mini-services-start.sh ]; then - cp ./.zscripts/mini-services-start.sh "$DIST_DIR/mini-services-start.sh" - chmod +x "$DIST_DIR/mini-services-start.sh" - fi - - echo "" - echo "🎉 所有任务完成!" - if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then - echo "✅ 成功: $success_count 个" - if [ $fail_count -gt 0 ]; then - echo "❌ 失败: $fail_count 个" - fi - fi -} - -main - diff --git a/.zscripts/mini-services-install.sh b/.zscripts/mini-services-install.sh deleted file mode 100755 index 91ff56b..0000000 --- a/.zscripts/mini-services-install.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# 配置项 -ROOT_DIR="/home/z/my-project/mini-services" - -main() { - echo "🚀 开始批量安装依赖..." - - # 检查 rootdir 是否存在 - if [ ! -d "$ROOT_DIR" ]; then - echo "ℹ️ 目录 $ROOT_DIR 不存在,跳过安装" - return - fi - - # 统计变量 - success_count=0 - fail_count=0 - failed_projects="" - - # 遍历 mini-services 目录下的所有文件夹 - for dir in "$ROOT_DIR"/*; do - # 检查是否是目录且包含 package.json - if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then - project_name=$(basename "$dir") - echo "" - echo "📦 正在安装依赖: $project_name..." - - # 进入项目目录并执行 bun install - if (cd "$dir" && bun install); then - echo "✅ $project_name 依赖安装成功" - success_count=$((success_count + 1)) - else - echo "❌ $project_name 依赖安装失败" - fail_count=$((fail_count + 1)) - if [ -z "$failed_projects" ]; then - failed_projects="$project_name" - else - failed_projects="$failed_projects $project_name" - fi - fi - fi - done - - # 汇总结果 - echo "" - echo "==================================================" - if [ $success_count -gt 0 ] || [ $fail_count -gt 0 ]; then - echo "🎉 安装完成!" - echo "✅ 成功: $success_count 个" - if [ $fail_count -gt 0 ]; then - echo "❌ 失败: $fail_count 个" - echo "" - echo "失败的项目:" - for project in $failed_projects; do - echo " - $project" - done - fi - else - echo "ℹ️ 未找到任何包含 package.json 的项目" - fi - echo "==================================================" -} - -main - diff --git a/.zscripts/mini-services-start.sh b/.zscripts/mini-services-start.sh deleted file mode 100755 index e0af64d..0000000 --- a/.zscripts/mini-services-start.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/sh - -# 配置项 -DIST_DIR="./mini-services-dist" - -# 存储所有子进程的 PID -pids="" - -# 清理函数:优雅关闭所有服务 -cleanup() { - echo "" - echo "🛑 正在关闭所有服务..." - - # 发送 SIGTERM 信号给所有子进程 - for pid in $pids; do - if kill -0 "$pid" 2>/dev/null; then - service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown") - echo " 关闭进程 $pid ($service_name)..." - kill -TERM "$pid" 2>/dev/null - fi - done - - # 等待所有进程退出(最多等待 5 秒) - sleep 1 - for pid in $pids; do - if kill -0 "$pid" 2>/dev/null; then - # 如果还在运行,等待最多 4 秒 - timeout=4 - while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do - sleep 1 - timeout=$((timeout - 1)) - done - # 如果仍然在运行,强制关闭 - if kill -0 "$pid" 2>/dev/null; then - echo " 强制关闭进程 $pid..." - kill -KILL "$pid" 2>/dev/null - fi - fi - done - - echo "✅ 所有服务已关闭" -} - -main() { - echo "🚀 开始启动所有 mini services..." - - # 检查 dist 目录是否存在 - if [ ! -d "$DIST_DIR" ]; then - echo "ℹ️ 目录 $DIST_DIR 不存在" - return - fi - - # 查找所有 mini-service-*.js 文件 - service_files="" - for file in "$DIST_DIR"/mini-service-*.js; do - if [ -f "$file" ]; then - if [ -z "$service_files" ]; then - service_files="$file" - else - service_files="$service_files $file" - fi - fi - done - - # 计算服务文件数量 - service_count=0 - for file in $service_files; do - service_count=$((service_count + 1)) - done - - if [ $service_count -eq 0 ]; then - echo "ℹ️ 未找到任何 mini service 文件" - return - fi - - echo "📦 找到 $service_count 个服务,开始启动..." - echo "" - - # 启动每个服务 - for file in $service_files; do - service_name=$(basename "$file" .js | sed 's/mini-service-//') - echo "▶️ 启动服务: $service_name..." - - # 使用 bun 运行服务(后台运行) - bun "$file" & - pid=$! - if [ -z "$pids" ]; then - pids="$pid" - else - pids="$pids $pid" - fi - - # 等待一小段时间检查进程是否成功启动 - sleep 0.5 - if ! kill -0 "$pid" 2>/dev/null; then - echo "❌ $service_name 启动失败" - # 从字符串中移除失败的 PID - pids=$(echo "$pids" | sed "s/\b$pid\b//" | sed 's/ */ /g' | sed 's/^ *//' | sed 's/ *$//') - else - echo "✅ $service_name 已启动 (PID: $pid)" - fi - done - - # 计算运行中的服务数量 - running_count=0 - for pid in $pids; do - if kill -0 "$pid" 2>/dev/null; then - running_count=$((running_count + 1)) - fi - done - - echo "" - echo "🎉 所有服务已启动!共 $running_count 个服务正在运行" - echo "" - echo "💡 按 Ctrl+C 停止所有服务" - echo "" - - # 等待所有后台进程 - wait -} - -main - diff --git a/.zscripts/start.sh b/.zscripts/start.sh deleted file mode 100755 index 33a2984..0000000 --- a/.zscripts/start.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/sh - -set -e - -# 获取脚本所在目录 -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -BUILD_DIR="$SCRIPT_DIR" - -# 存储所有子进程的 PID -pids="" - -# 清理函数:优雅关闭所有服务 -cleanup() { - echo "" - echo "🛑 正在关闭所有服务..." - - # 发送 SIGTERM 信号给所有子进程 - for pid in $pids; do - if kill -0 "$pid" 2>/dev/null; then - service_name=$(ps -p "$pid" -o comm= 2>/dev/null || echo "unknown") - echo " 关闭进程 $pid ($service_name)..." - kill -TERM "$pid" 2>/dev/null - fi - done - - # 等待所有进程退出(最多等待 5 秒) - sleep 1 - for pid in $pids; do - if kill -0 "$pid" 2>/dev/null; then - # 如果还在运行,等待最多 4 秒 - timeout=4 - while [ $timeout -gt 0 ] && kill -0 "$pid" 2>/dev/null; do - sleep 1 - timeout=$((timeout - 1)) - done - # 如果仍然在运行,强制关闭 - if kill -0 "$pid" 2>/dev/null; then - echo " 强制关闭进程 $pid..." - kill -KILL "$pid" 2>/dev/null - fi - fi - done - - echo "✅ 所有服务已关闭" - exit 0 -} - -echo "🚀 开始启动所有服务..." -echo "" - -# 切换到构建目录 -cd "$BUILD_DIR" || exit 1 - -ls -lah - -DEFAULT_PACKAGED_DB_PATH="/app/db/custom.db" -DEFAULT_PACKAGED_DATABASE_URL="file:$DEFAULT_PACKAGED_DB_PATH" - -# 启动 Next.js 服务器 -if [ -f "./next-service-dist/server.js" ]; then - echo "🚀 启动 Next.js 服务器..." - cd next-service-dist/ || exit 1 - - # 设置环境变量 - export NODE_ENV=production - export PORT="${PORT:-3000}" - export HOSTNAME="${HOSTNAME:-0.0.0.0}" - export DATABASE_URL="${DATABASE_URL:-$DEFAULT_PACKAGED_DATABASE_URL}" - - if [ "$DATABASE_URL" = "$DEFAULT_PACKAGED_DATABASE_URL" ]; then - if [ ! -f "$DEFAULT_PACKAGED_DB_PATH" ]; then - echo "❌ 未找到打包后的数据库文件 $DEFAULT_PACKAGED_DB_PATH" - echo " 为避免生产环境启动到空数据库,启动已终止" - exit 1 - fi - - echo "🗄️ 当前使用打包数据库: $DEFAULT_PACKAGED_DB_PATH" - else - echo "🗄️ 当前使用外部指定数据库: $DATABASE_URL" - fi - - # 后台启动 Next.js - bun server.js & - NEXT_PID=$! - pids="$NEXT_PID" - - # 等待一小段时间检查进程是否成功启动 - sleep 1 - if ! kill -0 "$NEXT_PID" 2>/dev/null; then - echo "❌ Next.js 服务器启动失败" - exit 1 - else - echo "✅ Next.js 服务器已启动 (PID: $NEXT_PID, Port: $PORT)" - fi - - cd ../ -else - echo "⚠️ 未找到 Next.js 服务器文件: ./next-service-dist/server.js" -fi - -# 启动 mini-services -if [ -f "./mini-services-start.sh" ]; then - echo "🚀 启动 mini-services..." - - # 运行启动脚本(从根目录运行,脚本内部会处理 mini-services-dist 目录) - sh ./mini-services-start.sh & - MINI_PID=$! - pids="$pids $MINI_PID" - - # 等待一小段时间检查进程是否成功启动 - sleep 1 - if ! kill -0 "$MINI_PID" 2>/dev/null; then - echo "⚠️ mini-services 可能启动失败,但继续运行..." - else - echo "✅ mini-services 已启动 (PID: $MINI_PID)" - fi -elif [ -d "./mini-services-dist" ]; then - echo "⚠️ 未找到 mini-services 启动脚本,但目录存在" -else - echo "ℹ️ mini-services 目录不存在,跳过" -fi - -# 启动 Caddy(如果存在 Caddyfile) -echo "🚀 启动 Caddy..." - -# Caddy 作为前台进程运行(主进程) -echo "✅ Caddy 已启动(前台运行)" -echo "" -echo "🎉 所有服务已启动!" -echo "" -echo "💡 按 Ctrl+C 停止所有服务" -echo "" - -# Caddy 作为主进程运行 -exec caddy run --config Caddyfile --adapter caddyfile diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8ff1bf2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,24 @@ +# SalonPro — Agent Guide + +Multi-tenant SaaS for salon management in Rwanda/East Africa. Next.js (App Router) + TypeScript + Tailwind + shadcn/ui + Prisma/Postgres. Read the relevant `/context` doc before working in an area. + +## Context map +- **What it does, stack, status, gaps** → [context/PROJECT_OVERVIEW.md](./context/PROJECT_OVERVIEW.md) +- **Folders, routing, tenancy, auth, API, state, data fetching** → [context/ARCHITECTURE.md](./context/ARCHITECTURE.md) +- **Design tokens + full component inventory (check before building UI)** → [context/DESIGN_SYSTEM.md](./context/DESIGN_SYSTEM.md) +- **Naming, TS, imports, API & domain conventions** → [context/CODING_STANDARDS.md](./context/CODING_STANDARDS.md) +- **Prisma models + types + API surface** → [context/DATA_MODELS.md](./context/DATA_MODELS.md) +- **Roadmap + cleanup backlog** → [context/TODO.md](./context/TODO.md) +- **Design/product source of truth (intent)** → `specs/ui/` + `specs/product-tour-plan.md` + +## Non-negotiables +- **UI**: never hardcode colors/radii/fonts — use the semantic tokens in `globals.css` / Tailwind classes. Check the component inventory before creating anything new. One solid pink (`primary`) button per screen; accent ≤10% of a screen. +- **Multi-tenancy**: every query scopes by `auth.salonId` (from `requireAuth`); never trust `salonId` from the request body. +- **Data client**: import the Prisma singleton from `@/lib/db` (not the empty `lib/prisma.ts`). +- **Money**: stored as plain numbers; format via `formatMoney()`/`formatRWF()`; currency comes from salon settings. +- **Toasts**: use `sonner`. The radix `ui/toaster` is legacy/being retired. + +## Working style +- Don't run `npm run dev` from the agent, and don't `npm run build` while the dev server runs (shared `.next`). Verify visually in the user's browser, not via automation. +- Always `git status` before assuming state — the repo may be edited in parallel. +- `main` is up to date with `feature/saas-multi-tenancy` (merged 2026-06-13). diff --git a/agent-ctx/2-c-crud-views-redesign.md b/agent-ctx/2-c-crud-views-redesign.md deleted file mode 100644 index 04e27a1..0000000 --- a/agent-ctx/2-c-crud-views-redesign.md +++ /dev/null @@ -1,29 +0,0 @@ -# Task 2-c: CRUD Views Redesign - -## Agent: CRUD Views Redesign Agent - -## Summary -Redesigned 4 CRUD view components (CustomersView, StaffView, ServicesView, AppointmentDialog) with proper shadcn/ui components, sonner toast, loading states, and role-based UI. - -## Files Modified -1. `/home/z/my-project/src/components/salon/CustomersView.tsx` - Complete redesign -2. `/home/z/my-project/src/components/salon/StaffView.tsx` - Complete redesign -3. `/home/z/my-project/src/components/salon/ServicesView.tsx` - Complete redesign -4. `/home/z/my-project/src/components/salon/AppointmentDialog.tsx` - Complete redesign - -## Key Changes -- Migrated all toast notifications from `@/hooks/use-toast` to `sonner` (toast.success/toast.error) -- Added proper shadcn components: Table, ToggleGroup, Avatar, ScrollArea, Separator, DialogDescription, DialogFooter -- Added loading skeletons for all views -- Added Loader2 spinners for async operations -- Added form validation -- Separated active/inactive items in Staff and Services views -- Role-based UI: admin (full), receptionist (limited), stylist (read-only) -- Consistent emerald/teal color scheme -- RWF currency formatting -- Responsive design - -## Verification -- ESLint: 0 errors -- App responds with HTTP 200 -- No compilation errors diff --git a/agent-ctx/3-b-salon-frontend.md b/agent-ctx/3-b-salon-frontend.md deleted file mode 100644 index 97495db..0000000 --- a/agent-ctx/3-b-salon-frontend.md +++ /dev/null @@ -1,70 +0,0 @@ -# Worklog - Salon Management System - -## Task 3-b: Build Complete Salon Management Frontend - -### What was done: -1. **Updated globals.css** - Changed the theme from default neutral to emerald/teal color scheme appropriate for a salon management system. Updated all CSS variables (primary, secondary, accent, sidebar, etc.) to use emerald/teal tones. - -2. **Created API Routes** (8 endpoints): - - `/api/customers` - GET (with search), POST, PUT, DELETE - - `/api/staff` - GET (with active filter), POST, PUT, DELETE - - `/api/services` - GET (with active filter), POST, PUT, DELETE - - `/api/appointments` - GET (with date/staffId/status/from/to filters), POST, PUT, DELETE - - `/api/payments` - GET (with status/method filters), POST, PUT - - `/api/dashboard` - GET (today's aggregated data) - - `/api/reports` - GET (with period/from/to params, returns revenue charts, top services/customers, payment/status breakdowns) - - `/api/seed` - POST (seeds demo data with 8 customers, 4 staff, 8 services, 16 appointments with payments) - -3. **Pushed Prisma schema** and seeded database with demo data - -4. **Created Salon Components** (9 components): - - `Sidebar.tsx` - Navigation sidebar with emerald/teal dark theme, collapsible on mobile with hamburger menu - - `DashboardView.tsx` - Dashboard with 4 stat cards (appointments, revenue, pending payments, pending amount), status breakdown badges, staff workload cards, today's appointment list - - `QuickBookingForm.tsx` - Fast appointment creation form with customer search/autocomplete, new customer creation, service/staff/time selection - - `AppointmentDialog.tsx` - Full appointment detail dialog with status change buttons (booked→confirmed→in_progress→completed, or no_show), payment management (status/method/amount), notes editing, cancel option - - `AppointmentsView.tsx` - Appointments view with day/week toggle, time slot calendar (8AM-7PM), week view with 7-column layout, color-coded appointment cards, legend - - `CustomersView.tsx` - Customer management with search, customer cards with visit count, add/detail dialogs, editable profile, visit history - - `StaffView.tsx` - Staff management with role badges (stylist/receptionist), active/inactive toggle switch, add/edit dialog - - `ServicesView.tsx` - Service management with price (RWF), duration, active/inactive toggle, add/edit dialog - - `ReportsView.tsx` - Reports with period selector (daily/weekly/monthly), custom date range, revenue bar chart (recharts), top services/customers tables, payment method pie chart, appointment status pie chart - -5. **Updated page.tsx** - Main entry point with sidebar + content area + sticky footer layout -6. **Updated layout.tsx** - Updated metadata for SalonPro Rwanda - -### Key Design Decisions: -- Used emerald/teal color scheme throughout (NOT blue/indigo as specified) -- RWF currency formatting using `Intl.NumberFormat('en-RW')` -- Mobile-first responsive design with collapsible sidebar -- Color-coded status badges (booked=blue, confirmed=emerald, in_progress=amber, completed=green, no_show=red) -- Payment methods include Rwanda-specific options (MTN MoMo, Airtel Money, Cash) -- Sticky footer with "SalonPro Rwanda" branding -- All components are 'use client' for interactivity -- Used shadcn/ui components throughout (Card, Dialog, Select, Badge, Switch, etc.) - -### Files Created/Modified: -- `src/app/globals.css` - Modified (emerald/teal theme) -- `src/app/layout.tsx` - Modified (updated metadata) -- `src/app/page.tsx` - Modified (main salon app layout) -- `src/app/api/customers/route.ts` - Created -- `src/app/api/staff/route.ts` - Created -- `src/app/api/services/route.ts` - Created -- `src/app/api/appointments/route.ts` - Created -- `src/app/api/payments/route.ts` - Created -- `src/app/api/dashboard/route.ts` - Created -- `src/app/api/reports/route.ts` - Created -- `src/app/api/seed/route.ts` - Created -- `src/components/salon/Sidebar.tsx` - Created -- `src/components/salon/DashboardView.tsx` - Created -- `src/components/salon/QuickBookingForm.tsx` - Created -- `src/components/salon/AppointmentDialog.tsx` - Created -- `src/components/salon/AppointmentsView.tsx` - Created -- `src/components/salon/CustomersView.tsx` - Created -- `src/components/salon/StaffView.tsx` - Created -- `src/components/salon/ServicesView.tsx` - Created -- `src/components/salon/ReportsView.tsx` - Created - -### Verification: -- ESLint passes with 0 errors -- All API routes return 200 with correct data -- Database seeded with 8 customers, 4 staff, 8 services, 16 appointments -- Dev server running successfully on port 3000 diff --git a/appointments-dark-fixed.png b/appointments-dark-fixed.png deleted file mode 100644 index 553f6a1..0000000 Binary files a/appointments-dark-fixed.png and /dev/null differ diff --git a/appointments-dark.png b/appointments-dark.png deleted file mode 100644 index 160479f..0000000 Binary files a/appointments-dark.png and /dev/null differ diff --git a/context/ACTIVITY-LOG-context.md b/context/ACTIVITY-LOG-context.md new file mode 100644 index 0000000..de5a4fc --- /dev/null +++ b/context/ACTIVITY-LOG-context.md @@ -0,0 +1,159 @@ +# SalonPro — Activity Log (Phase 2) · Context / Handoff + +Use this to resume cold. It captures the goal, the locked decisions from the +architect session, the data model, the capture points, and the build order. +This is the **spec** the owner asked to lock before any code. + +--- + +## 1. Goal + +Give the owner **operational transparency**: a feed of "who did what, when" +inside their salon — who confirmed an appointment, who took a payment, who added +a team member. Append-only, written as the actions happen, read by admins/owners +on a dedicated **Activity** page. + +This is an *owner-transparency* feed, **not** a compliance/legal audit and **not** +a developer/debug log. A rare missing row is acceptable; never block a real action +to write one. + +--- + +## 2. Language (agreed) + +- **Activity log** — append-only record of meaningful business actions, surfaced + to the owner. +- **Actor** — who acted. Either a staff `User` (admin/receptionist/stylist) **or** + an `Owner` (no `User` row). Stored as a **denormalized snapshot** (type + id + + name + role at the time), never an FK — so the trail survives renames/deletes. +- **Event / activity** — one logged action = one row (action type, target, + human-readable summary, optional metadata). +- **Surface to the owner** — a dedicated admin/owner-only Activity page, near + Reports, rendered as a reverse-chronological feed. + +--- + +## 3. Decisions locked + +1. **Scope (v1)** — log: + - **Appointments**: created · status changed · deleted + - **Payments**: recorded · updated + - **Staff & users**: user added · user updated (deactivate / role change) · + staff added / updated / removed + - **Deferred** (same mechanism, add later): logins, customer CRUD, service edits. +2. **Capture** — explicit `logActivity()` helper called inside each mutation + route. No middleware/wrapper magic; matches the hand-written-handler idiom. +3. **Durability** — **best-effort**. The log write is *awaited* (serverless can't + reliably fire-and-forget) but wrapped in try/catch. A logging failure emits a + `console.error` and the business action still succeeds. **Never** put the log + write in the same transaction as the mutation. +4. **Visibility** — admin + owner only (owners resolve to `admin` via + `requireAuth`), same gating as Settings. Gated by a new + `canViewActivityLog` permission (admin-only). +5. **Row shape** — store structured fields **and** a human-readable summary built + at write-time. Renders with no joins; survives deleted targets; structured + fields enable future filtering/deep-links. + +--- + +## 4. Data model — `ActivityLog` + +``` +model ActivityLog { + id String @id @default(cuid()) + salonId String // tenant scope (cascade) + actorType String // 'staff' | 'owner' + actorId String // User.id or Owner.id (NOT an FK) + actorName String // snapshot at write-time + actorRole String // snapshot: admin | receptionist | stylist | owner + action String // e.g. 'appointment.confirmed' (see ACTIVITY_ACTIONS) + targetType String? // 'appointment' | 'payment' | 'user' | 'staff' + targetId String? + summary String // human-readable sentence, built at write-time + metadata Json? // optional details: { from, to } | { amount, method } | ... + createdAt DateTime @default(now()) + + salon Salon @relation(fields: [salonId], references: [id], onDelete: Cascade) + + @@index([salonId, createdAt]) +} +``` +- `Salon` gets an `activityLogs ActivityLog[]` back-relation. +- Sync via **`npm run db:push`** + `db:generate` (this repo's workflow — migrations + are baselined; `migrate dev` would offer a destructive reset). +- Retention: unbounded in v1; feed is cursor-paginated. A purge/retention job is + future work. + +--- + +## 5. Helper — `src/lib/activity.ts` + +- `ACTIVITY_ACTIONS` — constant map of action keys → labels (display + filtering). +- `logActivity(auth, { action, targetType, targetId, summary, metadata })`: + - reads the actor snapshot from `auth.user` (`kind`→actorType, id, name, role; + owners → actorRole `'owner'`), + - inserts the row, **awaited inside try/catch**; on failure `console.error` and + return (never throw). +- Per-type metadata convention: + - appointment status → `{ from, to }`; summary names the customer + status. + - payment → `{ amount, method, status }`. + - user/staff → `{ role, name }` (+ `{ from, to }` for role changes). + +--- + +## 6. Capture points (routes to instrument) + +| Route | Verb | Action(s) | +|---|---|---| +| `api/appointments` | POST | `appointment.created` | +| `api/appointments` | PUT | `appointment.status_changed` (only when status changes) / `appointment.updated` | +| `api/appointments` | DELETE | `appointment.deleted` | +| `api/payments` | POST | `payment.recorded` | +| `api/payments` | PUT | `payment.updated` | +| `api/users` | POST | `user.added` | +| `api/users/[id]` | PATCH | `user.updated` (deactivate / role change / edit) | +| `api/staff` | POST/PUT/DELETE | `staff.added` / `staff.updated` / `staff.removed` | + +Note: the owner-provisioned onboarding auto-creates a `Staff` slot inside the +`api/users` POST transaction — log the **user.added** event (the staff slot is an +implementation detail of that one action, not a second event). + +--- + +## 7. Read surface + +- **API** `src/app/api/activity/route.ts` — `GET`, `requireAuth(req, + 'canViewActivityLog')`, scoped by `auth.salonId`, `orderBy createdAt desc`, + cursor pagination (`?cursor=&take=`). +- **Page** `src/app/(app)/activity/page.tsx` — thin wrapper → `ActivityView`. +- **View** `src/components/salon/ActivityView.tsx` — client; fetches its own data; + reverse-chron feed grouped by day; design tokens only; sonner for errors; + access-gated on `permissions?.canViewActivityLog`. +- **Nav** add to `nav-items.ts` with `roles: ['admin']` (covers owners). + +--- + +## 8. Build order + +1. `ActivityLog` model + `Salon` back-relation → `db:push` + `db:generate`. +2. `lib/activity.ts` (`ACTIVITY_ACTIONS` + `logActivity`). +3. `canViewActivityLog` in `Permissions` / `ROLE_PERMISSIONS` (admin-only) + + `PERMISSION_MATRIX_ROWS`. +4. Instrument the routes in §6. +5. `GET /api/activity`. +6. Activity page + view + nav entry. +7. `tsc --noEmit`; user verifies in browser (no automation, per project rules). + +Commit incrementally per step (commit-after-every-change convention). + +--- + +## 9. Deferred / future + +- Logins, customer CRUD, service edits (same mechanism). +- Filtering by actor / action type / date range; deep-links from a row to its + target record. +- Retention/purge job (table is unbounded in v1). +- Per-record history view (rejected for v1 in favor of the central feed). + + diff --git a/context/ARCHITECTURE.md b/context/ARCHITECTURE.md new file mode 100644 index 0000000..fee53c6 --- /dev/null +++ b/context/ARCHITECTURE.md @@ -0,0 +1,100 @@ +# Architecture — SalonPro + +## Folder structure +``` +src/ + app/ Next.js App Router + (app)/ authed route group + layout.tsx SERVER: resolve subdomain→salon + notFound() before auth + AppShell.tsx client shell: sidebar + topbar + ⌘K + mobile tab bar + auth gate + dashboard|appointments|customers|staff|services|reports|settings|billing/page.tsx + api/ route handlers (see "API" below) + book/[subdomain]/ public self-booking page (path-param tenant; not Host-resolved) + login/ UnifiedLogin: one email+password screen on every host (no PINs) + signup/ create salon + owner account + page.tsx marketing landing ("/") + layout.tsx root layout: fonts, theme bootstrap script, + globals.css design tokens (single source of truth) + tour CSS + icon.svg, not-found.tsx, global-error.tsx + components/ + ui/ shadcn primitives (47) — themed via tokens + salon/ feature views + shared app components (see DESIGN_SYSTEM.md) + booking/BookingFlow.tsx public booking UI + marketing/ LandingPage.tsx + scoped landing.css (own theme) + theme-toggle.tsx light/dark switch + hooks/ use-mobile, use-toast (legacy) + lib/ data, auth, domain logic (see "Key modules") + middleware.ts Host → x-salon-subdomain header on the request (all tenant routes) +prisma/schema.prisma data model (see DATA_MODELS.md) +specs/ design/product source-of-truth docs +``` + +## Routing +- **App Router** only (no `pages/`). +- **`(app)` route group** holds every authed screen behind one shared `layout.tsx`. Page files are thin wrappers — they render a view component: `export default () => `. +- **Views are URLs, not state.** Navigation goes through real routes; the sidebar, ⌘K palette, and dashboard links all `router.push`. Zustand no longer holds nav state. +- Public/unauthed: `/` (marketing), `/login`, `/signup`, `/book/[subdomain]`. +- Full route table + shell mapping: `specs/ui/routes.md` and `specs/ui/app-shell.md`. + +## Multi-tenancy (Host is the authority) +The operating salon is derived from the **request Host**, never the token or request body. Two-stage resolution: +1. **Edge (middleware, no DB):** `lib/subdomain.ts` extracts the subdomain *label* from `Host` vs `ROOT_DOMAIN` (apex/`www`/reserved → no tenant; dev `*.localhost` + dev-only `?salon=` fallback). It's forwarded on the **request** header `x-salon-subdomain` for all tenant routes — *except* the path-param public booking surface (`/book/[subdomain]`, `/api/public/booking/[subdomain]`). +2. **Node (Prisma):** the `(app)` **server** layout and `requireAuth`/`/api/auth/me` look up `subdomain → salon` and `notFound()` (pages) / 404 (API) unknown subdomains. +- `requireAuth` reads `salonId` from the resolved Host and **verifies membership** (see below); a per-request indexed `salon.findUnique` (no cache yet — invalidation is the real work when one is added). `ROOT_DOMAIN` env: `salonpro.me` prod / `localhost:3000` dev. +- **Every** domain model carries `salonId` and every query is scoped by `auth.salonId`. Cascade deletes from `Salon`. + +## Authentication & authorization (two surfaces) +Cookie-only (the Bearer/`localStorage` channel was retired). The `salonpro_session` cookie is **host-only** (no `Domain`), so it's naturally isolated per subdomain. Logic in `src/lib/auth.ts`; guard in `src/lib/auth-guard.ts` (`requireAuth(req?, permission?)`). `AUTH_SECRET` is **required** (no fallback — fails loudly). + +- **Staff** — per-salon `User`, **email + scrypt password** (`lib/password.ts`; email unique per salon), login at the tenant host (`/login` → `/api/auth/login`). PINs were retired. Roles: `admin`, `receptionist`, `stylist` (`src/lib/permissions.ts`, `ROLE_PERMISSIONS`, client-safe). +- **Owner** — global `Owner` (email + **scrypt** password, `lib/password.ts`), logs in at the **root** host (`/login`), gets a short-lived root-owner cookie (`salonpro_owner`), picks a salon, and is handed off to the subdomain via a **single-use exchange token** (`/api/owner/select` → `/api/auth/exchange`). An owner has **admin** rights to any salon it's linked to via `OwnerSalon` — **no `User` row required**. +- **Single login UI** (`src/app/login/UnifiedLogin.tsx`, every host): email → password, no PINs. A **tenant host** signs staff in against that salon (`/api/auth/login`); the **apex** is the owner flow (`/api/owner/login` → picker → handoff) plus a "Go to your salon" link that sends a team member to `/login`. The email step makes **no backend call**; credentials are only verified on submit, failing generically, so the screen never leaks whether an email exists. +- **`requireAuth`** decodes the cookie (discriminated `kind: 'staff' | 'owner'`), resolves the Host salon, then verifies membership: staff → active `User` in that salon; owner → `OwnerSalon` link. Two failures: unknown subdomain → 404; valid subdomain + non-member → **401 identical to no-session** (no existence leak). Role/name come from the fresh DB row. +- **Auth gating is client-side** in `AppShell` (`/api/auth/me`); the **server** `(app)` layout only does salon-resolution + `notFound()` (a logged-out visitor briefly sees the shell before the client redirect — known-deferred). Owners have no `User`, so `/api/auth/me` and `users/me/tour-complete` branch on `kind`. +- Server routes enforce via `requireAuth`; nav visibility via `nav-items.ts` (display only — does **not** guard routes). + +## API layer +Route handlers under `src/app/api/*/route.ts`. Convention (see `api/customers/route.ts` as the reference): +```ts +export async function GET(req: NextRequest) { + const auth = await requireAuth(req) // or requireAuth(req, 'canDeleteRecords') + if (!auth.authorized) return auth.error + const where = { salonId: auth.salonId, ... } // auth.salonId = Host-resolved salon + ... + return NextResponse.json(data) // POST returns { status: 201 } +} +``` +- Standard verbs per resource: `GET` (list/filter), `POST` (create), `PUT` (update by `id` in body), `DELETE` (`?id=`). +- Role checks inline (`auth.user?.role === 'stylist'` → 403) and via the permission arg to `requireAuth`. +- **Free-plan limits** enforced in create handlers (e.g. max 100 customers, 5 staff) → 403 with upgrade message. +- Resources: appointments, customers, staff, services, payments, dashboard, reports, salons, seed; `auth/*` (login, logout, me, signup, **exchange**), `owner/*` (**login, me, select** — root-host owner flow), `billing/*` (mock), `salon/settings`, `users/*` (+ `users/me/tour-complete`), `public/booking/[subdomain]` (+ `/slots`). + +## Data fetching +- **Client-side fetch.** `(app)` pages are server components that render `'use client'` view components; each view fetches its own data with `useState` + `useEffect` + raw `fetch()`. No props passed down from the page. +- No data-fetching library — `@tanstack/react-query` was removed (unused). Cookie auth rides along automatically on same-origin requests; `authFetch` is now a thin `fetch` wrapper. + +## State management +- **Zustand** (`src/lib/salon-store.ts`): current salon info, selected date, ⌘K open state, mobile sidebar open. Nav is URL-based, not in the store. +- **React Context** (`src/lib/auth-context.tsx`): current user, salon, role permissions; wraps the `(app)` tree (and the landing's CTA). + +## Key `lib/` modules +| File | Purpose | +|---|---| +| `db.ts` | Prisma client singleton (`db`). **Use this.** | +| `auth.ts` | session create/verify (staff + owner, discriminated `kind`), root-owner + single-use exchange tokens, `AUTH_SECRET` (required) | +| `password.ts` | scrypt `hashPassword`/`verifyPassword` (all account passwords — staff `User` and `Owner`) | +| `subdomain.ts` | edge-safe `getSubdomainLabel(host, ROOT_DOMAIN)` + `SALON_SUBDOMAIN_HEADER` | +| `auth-guard.ts` | `requireAuth(req?, permission?)` → `{ authorized, user, permissions, salonId, error }`; salonId from Host + membership verify | +| `permissions.ts` | `ROLE_PERMISSIONS`, `ROLE_LABELS`, `PERMISSION_MATRIX_ROWS` (client-safe) | +| `salon-settings.ts` | `SalonSettings` type + `parseSalonSettings()` (always read settings through it), currencies, business-hours defaults | +| `salon-store.ts` | Zustand store | +| `auth-context.tsx` | auth React context/provider | +| `constants.ts` | `STATUS_CONFIG`, `PAYMENT_STATUS_CONFIG` + status types (token-based classes) | +| `utils.ts` | `cn()`, `formatMoney()`, `formatRWF()` | +| `tour.ts` | Driver.js tour steps/config | +| `stripe.ts` | placeholder stub (mock billing) | + +## Conventions to preserve +- New authed data screen → add a page under `(app)/`, a view in `components/salon/`, a `salonId`-scoped API route, and a nav entry in `nav-items.ts` if it needs navigation. +- Every API query/mutation is scoped by `auth.salonId`. Never trust a `salonId` from the request body. +- Currency is display-only formatting (`formatMoney`); amounts stored as plain numbers, no conversion. diff --git a/context/CODING_STANDARDS.md b/context/CODING_STANDARDS.md new file mode 100644 index 0000000..537c7a2 --- /dev/null +++ b/context/CODING_STANDARDS.md @@ -0,0 +1,45 @@ +# Coding Standards — SalonPro + +Derived from the existing codebase. When in doubt, match the surrounding file. + +## Language & types +- **TypeScript everywhere**, `strict` (see `tsconfig.json`). No `any` — prefer explicit interfaces or `unknown` + narrowing (e.g. `Record` for dynamic Prisma `where`). +- Export shared types from where the data lives: domain unions in `lib/constants.ts` (`AppointmentStatus`, `PaymentStatus`), permissions in `lib/permissions.ts` (`UserRole`, `Permissions`), settings in `lib/salon-settings.ts`. +- Derive types from data with `as const` + `keyof typeof` (see `STATUS_CONFIG`). Don't hand-maintain parallel union lists. +- Component props: inline `interface XProps` above the component, or inline `{ ... }: { ... }` for tiny ones. Default values via destructuring defaults. + +## Naming & files +- **Components**: PascalCase files and exports. Feature components in `src/components/salon/` are PascalCase (`CustomersView.tsx`); shadcn primitives in `src/components/ui/` are kebab-case (`dropdown-menu.tsx`) — keep that split. +- **Non-component modules**: kebab-case (`auth-guard.ts`, `salon-settings.ts`, `nav-items.ts`). +- **Hooks**: `use-*.ts` (`use-mobile.ts`). +- **Route handlers**: always `route.ts`; folder name is the URL segment; dynamic segments `[param]`. +- Variables/functions camelCase; constants/config objects SCREAMING_SNAKE or `as const` objects (`ROLE_PERMISSIONS`, `STATUS_CONFIG`, `FREE_PLAN_LIMITS`). + +## Imports +- Use the **`@/` alias** for everything under `src/` (`@/components/ui/button`, `@/lib/utils`). No deep relative `../../`. +- Rough order (as seen in files): external packages → `next/*` → `@/components/*` → `@/lib/*` / `@/hooks/*` → types. Group related imports. +- Aliases (from `components.json`): `@/components`, `@/components/ui`, `@/lib`, `@/hooks`, `@/lib/utils`. + +## React / Next +- Mark interactive components `'use client'` at the top. `(app)` page files stay server components that just render a client view. +- Data fetching in client views: `useState` + `useEffect` + `fetch()`; wrap callbacks in `useCallback` where they're deps. Show `Skeleton` while loading and `Loader2` spinners for async actions — **no full-page spinners**. +- Class names: compose with **`cn()`** (`@/lib/utils`) — never string-concatenate conditional classes. +- Icons from `lucide-react`. Use semantic Tailwind tokens for color (see [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md)) — no raw hex in JSX. + +## API route conventions +- First line of every handler: `const auth = await requireAuth(req[, 'permissionKey']); if (!auth.authorized) return auth.error`. +- **Scope every query by `auth.salonId`.** Never accept `salonId` from the request body. +- Verbs: `GET` list/filter (query params), `POST` create (→ `201`), `PUT` update (`id` in body), `DELETE` (`?id=`). +- Errors: `NextResponse.json({ error: 'message' }, { status })` — `400` bad input, `403` permission/plan limit, `404` not found. +- Enforce plan limits in create handlers (pattern in `api/customers/route.ts`). + +## Domain rules +- **Money is display-only**: store plain numbers; format with `formatMoney(amount, currency)` / `formatRWF()`. Currency comes from salon settings, not hardcoded. +- **Salon settings**: always read via `parseSalonSettings()` so partial/missing JSON falls back to defaults. +- **Roles**: check against `ROLE_PERMISSIONS` (server) — don't scatter literal role strings; nav visibility via `navItemsForRole()`. +- Sentence case for user-facing labels/buttons. + +## Quality gates +- `npm run lint` (ESLint flat config, `eslint-config-next`) must be clean — 0 errors before committing. +- `tsc` clean (no type errors). +- Don't run `npm run build` while the dev server is running (they share `.next`). diff --git a/context/DATA_MODELS.md b/context/DATA_MODELS.md new file mode 100644 index 0000000..363ccde --- /dev/null +++ b/context/DATA_MODELS.md @@ -0,0 +1,123 @@ +# Data Models — SalonPro + +Source of truth: **`prisma/schema.prisma`** (PostgreSQL via Neon serverless adapter). All IDs are `cuid()`; every domain model has `createdAt`/`updatedAt` and a `salonId` FK with `onDelete: Cascade`. + +## Entity overview +``` +Owner >──< Salon (via OwnerSalon join; global owner identity, admin per link) + │ +Salon ──┬─< Customer ──< Appointment >── Service + ├─< Staff ─────< Appointment │ + ├─< Service └── Payment (1:1) + ├─< Appointment + ├─< Payment + └─< User >── Staff (optional link) (per-salon staff auth) + +OneTimeToken standalone — single-use owner cross-domain login handoff +``` + +**Two auth identities:** `Owner` (global, email+password, admin via `OwnerSalon`) vs `User` (per-salon staff, email+password). Both use scrypt; PINs were retired. See ARCHITECTURE.md → Authentication. + +## Models + +### Salon (tenant root) +| Field | Type | Notes | +|---|---|---| +| `id` | String | PK | +| `name` | String | | +| `subdomain` | String | **unique** — tenant resolution. Rules below. | +| `plan` | String | `free` \| `pro` (default `free`) | +| `stripeCustomerId` | String? | | +| `stripeSubscriptionId` | String? | | +| `settings` | Json? | `SalonSettings`: businessHours, slotIntervalMinutes, publicBookingEnabled, currency — read via `parseSalonSettings()` | +| relations | — | customers, staff, services, appointments, payments, users, owners (OwnerSalon) | + +#### Subdomain rules (single source of truth: `src/lib/constants.ts`) +A salon's `subdomain` is validated by **`validateSubdomain()`** in `lib/constants.ts` — the one place these rules live, shared by the signup page, the availability endpoint (`GET /api/salons?subdomain=`), and the create endpoint (`POST /api/salons`). The module is client-safe, so the UI and the API can never disagree. + +- **Charset**: lowercase `a–z`, digits `0–9`, hyphen. Must start and end alphanumeric (no leading/trailing hyphen). Pattern: `^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$`. +- **Length**: `SUBDOMAIN_MIN_LENGTH` 3 … `SUBDOMAIN_MAX_LENGTH` 30. +- **Reserved**: not in `RESERVED_SUBDOMAINS` (infra/product names — `www`, `api`, `app`, `admin`, `billing`, `salonpro`, … ~35 total). `demo` is intentionally **not** reserved (legitimate seed tenant; the unique constraint already protects it). +- **Uniqueness / race**: the DB `@unique` constraint is authoritative. `POST /api/salons` keeps a fast pre-check for a friendly 409, and also catches Prisma **P2002** on `subdomain` (and on owner `email`) → 409 (prevents the two-simultaneous-signups race from surfacing as a 500). + +When adding a reserved name or changing the rules, edit `RESERVED_SUBDOMAINS` / `validateSubdomain()` in `lib/constants.ts` and update this section. + +### Customer +`name`, `phone`, `notes` (default ''), `salonId`, `appointments[]`. Indexes: `phone`, `name`, `salonId`. + +### Staff +`name`, `phone` (''), `role` (`stylist` \| `receptionist`, default `stylist`), `active` (Bool, default true), `salonId`, `appointments[]`, `users[]`. Indexes: `role`, `salonId`. + +### Service +`name`, `price` (Float, salon currency), `duration` (Int, minutes), `active` (Bool), `salonId`, `appointments[]`. Indexes: `name`, `salonId`. + +### Appointment +| Field | Type | Notes | +|---|---|---| +| `date` | String | `YYYY-MM-DD` (string for easy querying) | +| `startTime` / `endTime` | String | `HH:mm` | +| `status` | String | `booked` \| `confirmed` \| `in_progress` \| `completed` \| `no_show` (default `booked`) | +| `notes` | String | '' | +| FKs | — | `salonId`, `customerId`, `staffId`, `serviceId` (all cascade) | +| `payment` | Payment? | 1:1 | +Indexes: `date`, `status`, `staffId`, `customerId`, `salonId`. + +### Payment (1:1 with Appointment) +`status` (`unpaid` \| `partial` \| `paid`, default `unpaid`), `method` (`cash` \| `mtn_momo` \| `airtel_money`, default `cash`), `amount` (Float, default 0), `salonId`, `appointmentId` (**unique**). Indexes: `status`, `method`, `salonId`. (`PAYMENT_STATUS_CONFIG` in `lib/constants.ts` is keyed `partial` to match.) + +### User (per-salon staff auth) +`name`, `email` (lowercased), `passwordHash` (**scrypt** `salt:hash`, `lib/password.ts`), `mustResetPassword` (Bool, default false), `role` (`admin` \| `receptionist` \| `stylist`, default `receptionist`), `active` (Bool), `tourCompleted` (Bool), `staffId?` (optional link to Staff, `onDelete: SetNull`), `salonId`. **`@@unique([salonId, email])`** — email is the login handle, unique within a salon. Indexes: `role`, `salonId`. Belongs to exactly one salon; login is email + password at the tenant host (`/api/auth/login`). PINs were retired. + +`mustResetPassword` is **set true** by `/api/users` POST (admin gives a temporary password) and **enforced**: `AppShell` gates such staff behind a forced "Set your password" screen. `POST /api/auth/change-password` (staff only) verifies the current password, sets a new one (≥8, must differ), and clears the flag. Staff can also change their password anytime from the sidebar; owners are rejected (they manage theirs at the apex). + +### Owner (global owner identity) +| Field | Type | Notes | +|---|---|---| +| `id` | String | PK | +| `email` | String | **unique**, stored lowercased — the global login handle | +| `passwordHash` | String | **scrypt** `salt:hash` (`lib/password.ts`) — same scheme as staff `User` | +| `name` | String | | +| `mustResetPassword` | Boolean | default false. Set by the backfill script; **enforcement deferred** — currently an unenforced marker (login works with the temp password). | +| relations | — | `salons` (OwnerSalon[]) | + +An owner sits **above** `User` and carries admin rights to each linked salon **directly** — it does not need a `User` row. Sign-in: email+password at the root host → exchange handoff to a subdomain (see ARCHITECTURE.md). + +### OwnerSalon (owner ↔ salon membership) +`ownerId` (→ Owner, cascade), `salonId` (→ Salon, cascade), `createdAt`. **`@@unique([ownerId, salonId])`**, index on `salonId`. One owner may run several salons; one salon may have several owners. This link is what grants an owner admin to a salon (verified per request by `requireAuth`). + +### OneTimeToken (owner login handoff nonce) +`id` (PK = the `jti` carried in the signed exchange token), `ownerId`, `salonId`, `expiresAt`, `consumedAt?`, `createdAt`. Index on `expiresAt`. Single-use: `/api/auth/exchange` consumes it atomically (`updateMany` guarded by `consumedAt: null` + unexpired) before setting the owner session cookie. 60s TTL. + +## Application-level types (not in DB, but canonical) +| Type | Location | Purpose | +|---|---|---| +| `AppointmentStatus`, `PaymentStatus` | `lib/constants.ts` | status unions + display config | +| `UserRole`, `Permissions`, `ROLE_PERMISSIONS` | `lib/permissions.ts` | role → capability matrix | +| `SalonSettings`, `BusinessHours`, `DayHours`, `SupportedCurrency` | `lib/salon-settings.ts` | the `Salon.settings` JSON shape (currencies: RWF/USD/KES/UGX) | +| `NavItem` | `components/salon/nav-items.ts` | nav config | + +## API surface (handlers in `src/app/api/`) +All authed routes scoped by `salonId` via `requireAuth` (salonId from the Host, not the body/token). Verb convention: `GET` list/filter · `POST` create · `PUT` update (`id` in body) · `DELETE` (`?id=`). + +| Route | Purpose | +|---|---| +| `auth/login`, `auth/logout`, `auth/me` | staff session lifecycle (email + password), host-aware | +| `auth/exchange` | consume single-use owner token (on subdomain) → set owner session cookie | +| `auth/signup` | add a staff `User` to the current salon (requires auth) | +| `owner/login`, `owner/me`, `owner/select` | owner email/password login + salon picker + mint exchange token (root host) | +| `customers` | CRUD + `?q=` search; free-plan cap 100 | +| `staff` | CRUD + active filter; free-plan cap 5 | +| `services` | CRUD + active filter | +| `appointments` | CRUD + filters (date/staffId/status/from/to); conflict detection | +| `payments` | GET (status/method filters), POST, PUT | +| `dashboard` | today's aggregated stats | +| `reports` | revenue/top-services/customers/breakdowns (period/from/to) | +| `salon/settings` | read/update `Salon.settings` | +| `users`, `users/[id]`, `users/me/tour-complete` | account management + tour flag | +| `billing/checkout`, `billing/webhook` | **mock** Pro upgrade (no real Stripe) | +| `public/booking/[subdomain]`, `.../slots` | public self-booking (gated by settings) | +| `salons` | create salon: unauth → new `Owner`+`Salon`+link; authed owner → link only; **staff → 403**. GET = subdomain availability | +| `seed` | demo data (8 customers, 4 staff, 8 services, 16 appointments) | + +## DB workflow +`npm run db:push` (sync schema — **this repo's workflow**; migrations are baselined, so `db:migrate`/`migrate dev` would offer a destructive reset) · `db:generate` (client) · `db:seed` · `db:reset` · `db:deploy`. `prisma generate` runs on `postinstall` and in `build`. One-off scripts (e.g. `scripts/backfill-owners.ts`) run via `npx tsx Vercel: Build and deploy the best web experiences with the AI CloudSkip to content
\"\"
Events

Ship 26 is coming to 5 cities

Build and deploy on the AI Cloud.

Vercel provides the developer tools and cloud infrastructure to build, scale, and secure a faster, more personalized web.

Build and deploy on the AI Cloud.

Vercel provides the developer tools and cloud infrastructure to build, scale, and secure a faster, more personalized web.

\"Runway\"\"Runway\"\"LeonardoAi\"\"LeonardoAi\"\"Zapier\"\"Zapier\"\"Adobe\"\"Adobe\"\"Typefully\"\"Typefully\"\"Neo\"\"Neo\"\"Ruggable\"\"Ruggable\"\"Sonos\"\"Sonos\"\"Chicos\"\"Chicos\"\"Stripe\"\"Stripe\"\"Box\"\"Box\"\"Hydrow\"\"Hydrow\"\"Super\"\"Super\"\"Hashnode\"\"Hashnode\"
\"Runway\"\"Runway\" build times went from 7m to 40s.\"LeonardoAi\"\"LeonardoAi\" saw a 95% reduction in page load times. \"Zapier\"\"Zapier\" saw 24x faster builds.

Get started using our pre-built templates. Easily stream long-running LLM responses for a better user experience with zero-config infrastructure that's always globally performant.

Fast load times, zero overhead with Vercel's highly optimized infrastructure and CDN, reducing bounce rates and improving SEO. Streamline content creation & publishing with built-in previews.

Deploy AI Apps in seconds\n \n

Your product, delivered.

Security, speed, and AI included, so you can focus on your user.

Agents

Deliver more value to users by executing complex workflows.

\"\"
\"Agent\"Agent

AI Apps

Enrich any product or feature with the latest models and tools.

\"\"
  • Fluid
  • AISDK
  • AI Gateway
  • Workflow
  • Sandbox
  • BotID

Web Apps

Ship beautiful interfaces that don’t compromise speed or functionality.

\"\"
\"Web\"Web

Composable Commerce

Increase conversion with fast, branded storefronts.

\"\"
\"Commerce\"Commerce

Multi-tenant Platform

Serve millions securely across isolated environments.

\"\"
customer.domain.com
project.domain.com
jane.domain.com
joe.domain.com
\n \n \n \n Svelte
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Vite
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Next.js
Nuxt
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n Turbopack
\n \n \n \n \n \n \n \n

Framework-Defined Infrastructure

From code to infrastructure in one git push. Vercel deeply understands your app to provision the right resources and optimize for high-performance apps.

Scale your

Enterprise

without compromising

Security

Deploy once, deliver everywhere.

When you push code to Vercel, we make it instantly available across the globe.

Nodes on the globe are sending out small pulses to indicate activity

Fluid Compute

A compute model for all workloads. With Active CPU pricing.

Learn more
\"Fluid\"Fluid

\n \n AI Gateway

The AI Gateway For Developers. Effortlessly access and deploy hundreds of AI models from one interface.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
import { streamText } from 'ai'\n\nconst result = streamText({\n  model: 'openai/gpt-5.5',\n  prompt: 'Why is the sky blue?'\n})

Use it with

andmany more

Top models on Jun 2, 2026

1

Gemini 3 Flash

20.6%

2

DeepSeek V4 Flash

12.6%

3

DeepSeek V4 Pro

8.7%

4

Claude Sonnet 4.6

7.7%

5

Claude Opus 4.8

7.5%

6

Claude Opus 4.6

3.9%

7

GPT 5.4 Mini

2.7%

8

Claude Haiku 4.5

2.5%

9

MiniMax M3

2.5%

10

Qwen 3.7 Max

2.3%

Deploy your first app in seconds.

  • Deploy automatically from git or with our CLI
  • Wide range support for the most popular frameworks
  • Previews for every push
  • Automatic HTTPS for all your domains






Next.js Templates






\n \n \n
React Templates






Astro Templates






\n \n \n \n
Svelte Templates






Nuxt Templates






\n \n \n \n \n \n \n \n \n \n \n
Python Templates
", - "metadata": { - "apple-mobile-web-app-status-bar-style": "default", - "apple-mobile-web-app-title": "Vercel", - "color-scheme": "dark light", - "content-id": "1DrU8MS0JjmoIieqMNAbRU", - "description": "Vercel gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web.", - "lang": "en", - "mobile-web-app-capable": "yes", - "og:description": "Vercel gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web.", - "og:image": "https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/4JmubmYDJnFtstwHbaZPev/0c3576832aae5b1a4d98c8c9f98863c3/Vercel_Home_OG.png", - "og:locale": "en_US", - "og:site_name": "Vercel", - "og:title": "Vercel: Build and deploy the best web experiences with the AI Cloud – Vercel", - "og:type": "website", - "og:url": "https://vercel.com/home", - "robots": "index, max-image-preview:large", - "theme-color": [ - "#FAFAFA", - "#FAFAFA" - ], - "trace-attributes": "{\"next.appdir\":true}", - "twitter:card": "summary_large_image", - "twitter:description": "Vercel gives developers the frameworks, workflows, and infrastructure to build a faster, more personalized web.", - "twitter:image": "https://assets.vercel.com/image/upload/contentful/image/e5382hct74si/4JmubmYDJnFtstwHbaZPev/0c3576832aae5b1a4d98c8c9f98863c3/Vercel_Home_OG.png", - "twitter:image:height": "628", - "twitter:image:width": "1200", - "twitter:title": "Vercel: Build and deploy the best web experiences with the AI Cloud", - "viewport": "width=device-width, initial-scale=1, maximum-scale=1" - }, - "title": "Vercel: Build and deploy the best web experiences with the AI Cloud – Vercel", - "url": "https://vercel.com/", - "usage": { - "tokens": 331296 - } - }, - "meta": { - "usage": { - "tokens": 331296 - } - }, - "status": 20000 -} \ No newline at end of file diff --git a/vercel-reference.png b/vercel-reference.png deleted file mode 100644 index 5b62685..0000000 Binary files a/vercel-reference.png and /dev/null differ diff --git a/vercel-style-dark.png b/vercel-style-dark.png deleted file mode 100644 index 90be457..0000000 Binary files a/vercel-style-dark.png and /dev/null differ diff --git a/vercel-style-light.png b/vercel-style-light.png deleted file mode 100644 index b844b5a..0000000 Binary files a/vercel-style-light.png and /dev/null differ diff --git a/worklog-new.md b/worklog-new.md deleted file mode 100644 index 8dac9c4..0000000 --- a/worklog-new.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -Task ID: 4 -Agent: Main -Task: Fix sidebar not being affected by dark/light theme toggle - -Work Log: -- Identified root cause: sidebar had hardcoded `!important` CSS rules in globals.css forcing always-dark background -- Identified secondary cause: Sidebar.tsx used hardcoded neutral-* color classes (bg-neutral-800, text-white, border-neutral-800, etc.) instead of theme-aware CSS variable classes -- Fixed globals.css: Removed `[data-sidebar="sidebar"]` `!important` override rules -- Fixed globals.css: Updated `:root` sidebar CSS variables from dark colors to light colors (sidebar: oklch(0.985 0 0) light gray-white, sidebar-foreground: dark text, etc.) -- Rewrote Sidebar.tsx: Replaced all hardcoded colors with theme-aware CSS variable classes -- Fixed remaining hardcoded colors across other components (CustomersView, AppointmentsView, AppointmentDialog, DashboardView) -- Verified with Agent Browser: Light mode sidebar = near-white, Dark mode sidebar = near-black -- Lint passes clean, no console errors - -Stage Summary: -- Sidebar now properly responds to dark/light theme toggle -- Light mode: light gray-white sidebar with dark text and subtle borders -- Dark mode: near-black sidebar with light text and dark borders -- All hardcoded neutral-/gray- colors replaced with theme-aware CSS variable classes -- 6 files modified: globals.css, Sidebar.tsx, CustomersView.tsx, AppointmentsView.tsx, AppointmentDialog.tsx, DashboardView.tsx diff --git a/worklog-responsive.md b/worklog-responsive.md deleted file mode 100644 index 16c0f26..0000000 --- a/worklog-responsive.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -Task ID: 1 -Agent: Main Agent -Task: Fix responsiveness for all devices across all SalonPro Rwanda components - -Work Log: -- Audited all 11 salon components for responsiveness issues -- Fixed Main Layout (page.tsx): sticky footer, responsive header text, responsive padding -- Fixed DashboardView: responsive title, compact buttons, responsive stat numbers, tighter time column -- Fixed AppointmentsView: responsive time grid, icon-only Today button, touch targets, overflow handling -- Fixed QuickBookingForm: better medium-screen grid, larger mobile button, bigger autocomplete -- Fixed AppointmentDialog: stacking grids on mobile for all info/payment sections -- Fixed CustomersView: touch targets, responsive dialog width, visit history gaps -- Fixed StaffView: hidden labels on mobile, touch targets for switches -- Fixed ServicesView: hidden labels on mobile, touch targets, responsive dialog grid -- Fixed ReportsView: full-width controls on mobile, responsive date inputs, smaller text -- Fixed LoginPage: compact logo/title, tighter margins, touch targets for demo buttons - -Stage Summary: -- All 11 components now responsive across mobile/tablet/desktop -- Lint passes clean, dev server running correctly -- Footer sticky at bottom, all touch targets 44px minimum diff --git a/worklog.md b/worklog.md deleted file mode 100644 index 4e80db4..0000000 --- a/worklog.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -Task ID: 1 -Agent: Main -Task: Fix dashboard conflict and update color scheme - -Work Log: -- Investigated dashboard "conflict" - found duplicate fetch logic in DashboardView.tsx -- Fixed duplicate fetch: useEffect was independently reimplementing the same fetch as fetchDashboard callback -- Restructured initial fetch to use direct authFetch in useEffect with cancellation support -- Fixed lint error (set-state-in-effect) by separating initial load from retry logic -- Updated mobile stat cards from single-row (icon+value+arrow) to stacked layout (icon+label on top, value below) -- Removed truncate class from revenue/amount values on mobile -- Updated entire color scheme across all 11 salon components to match common Next.js/SaaS patterns: - - Sidebar: Dark zinc/neutral palette instead of emerald-green - - Dashboard: Uses primary (theme-aware) colors instead of hardcoded emerald - - Quick action buttons: primary/10 backgrounds with primary text - - Stat cards: icon+label layout with proper mobile sizing - - Status badges: Softer pastel tones (sky-50 for booked, zinc-100 for completed) - - Buttons: Use default shadcn primary styling instead of hardcoded emerald-600 - - Login page: Uses primary color instead of emerald gradient - - Appointments: Calendar uses primary color for selected/today states - - All components: Replaced bg-emerald-600/hover:bg-emerald-700 with default Button styling - -Stage Summary: -- Dashboard conflict (duplicate fetch) fixed -- Mobile text truncation on stat cards fixed with new stacked card layout -- Full color scheme update to match modern Next.js/SaaS conventions -- All 11 salon components updated: DashboardView, Sidebar, LoginPage, AppointmentsView, QuickBookingForm, AppointmentDialog, CustomersView, StaffView, ServicesView, ReportsView, page.tsx -- Lint passes clean -- Verified on both desktop (1440x900) and mobile (390x844) viewports - ---- -Task ID: 2 -Agent: Main -Task: Add dark/light theme support with next-themes - -Work Log: -- Added ThemeProvider wrapper from next-themes to layout.tsx -- Created theme-toggle.tsx component with Sun/Moon/Monitor dropdown -- Added theme toggle to app header and login page -- Fixed hardcoded colors in all 6 view components with dark: variants -- Fixed sidebar with dark mode variants for all zinc colors -- Updated globals.css with dark sidebar gradient -- Fixed TabsContent runtime error in AppointmentsView (was outside Tabs component) -- Verified both light and dark modes with Agent Browser + VLM - -Stage Summary: -- Full dark/light theme support with system detection -- Theme toggle in header and login page -- All status badges, role badges, and colored elements have dark mode variants - ---- -Task ID: 3 -Agent: Main -Task: Redesign UI with Vercel-inspired design system - -Work Log: -- Analyzed Vercel.com design via browser screenshots and VLM -- Identified key Vercel characteristics: near-black bg (#0a0a0a), white surfaces, black/white primary, thin borders, minimal color, Geist font -- Rewrote globals.css with complete Vercel color system: - - Light: white bg, black primary (#000), #e5e5e5 borders, #0a0a0a sidebar - - Dark: #0a0a0a bg, white primary (#fff), #2a2a2a borders, #080808 sidebar - - Removed emerald/teal custom colors from @theme inline block -- Redesigned Sidebar: Triangle logo, minimal nav, white badge on black, mono version text -- Redesigned page.tsx: 44px header with triangle icon, minimal footer with mono font -- Redesigned LoginPage: Vercel-style centered card, black triangle logo, subtle demo accounts -- Updated all 6 view components with Vercel-style subtle status colors (bg-*-500/10 pattern) -- Replaced bold pastel badges with transparent overlay badges -- Role badges now use emerald/violet with /10 opacity -- Reports card colors updated to use /10 overlays instead of solid backgrounds -- Fixed TabsContent error in AppointmentsView -- Verified both light and dark modes with Agent Browser + VLM — confirmed Vercel-like aesthetic - -Stage Summary: -- Complete Vercel-inspired design system across entire app -- Dark mode: #0a0a0a background with white primary — signature Vercel look -- Light mode: clean white with black primary and dark sidebar -- Status colors use subtle /10 opacity overlays -- Triangle logo for Vercel branding alignment -- Login page matches Vercel's minimal auth style