diff --git a/.zscripts/build.sh b/.zscripts/build.sh new file mode 100755 index 0000000..bbd6262 --- /dev/null +++ b/.zscripts/build.sh @@ -0,0 +1,175 @@ +#!/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 + +# 校验 standalone 服务端入口是否生成(部署成功率守卫)。 +# Next 仅在 next.config 含 output:"standalone" 时产出 .next/standalone/server.js。 +# 若用户/AI 编辑项目时改写或删除了该配置,bun run build 仍会成功(static 照常 +# 产出、退出码 0),但 standalone 缺失——打出的包里没有 server.js,部署到 FC 后 +# start.sh 找不到 next-service-dist/server.js → 不启动 Next → Caddy:81 反代空的 +# 3000 → FC 健康检查 120s 超时失败(线上 warmup_412 / FunctionNotStarted 的主因)。 +# 这里做一次自愈:仅在确实缺失时,给 next.config 补回 output:"standalone" 并重建。 +# 正常项目(已生成 server.js)整段跳过,不读写任何用户文件。 +if [ ! -f ".next/standalone/server.js" ]; then + echo "⚠️ 构建未产出 .next/standalone/server.js,开始自愈 next.config 的 output 配置..." + NEXT_CONFIG_FILE="$(ls next.config.ts next.config.js next.config.mjs next.config.cjs 2>/dev/null | head -1)" + + if [ -z "$NEXT_CONFIG_FILE" ]; then + echo "❌ 构建失败:未找到 next.config.*,无法生成 standalone 部署产物。" + exit 1 + fi + + if grep -Eq "output\s*:\s*['\"]standalone['\"]" "$NEXT_CONFIG_FILE"; then + # 已声明 standalone 却仍没产出 server.js,说明不是配置缺失(可能 build 真 + # 出错、自定义 distDir 等)。不臆改用户配置,直接失败并暴露原因。 + echo "❌ 构建失败:$NEXT_CONFIG_FILE 已含 output:\"standalone\",但仍未生成 .next/standalone/server.js。" + echo " 请检查上方构建日志中的报错或项目自定义的构建配置。" + exit 1 + fi + + if grep -Eq "output\s*:\s*['\"]" "$NEXT_CONFIG_FILE"; then + # 已显式声明了其它 output(如 "export" 静态导出 / "standalone" 之外的值)。 + # "export" 与本部署模型(standalone + 自定义 server)互斥——不能注入第二个 + # output 覆盖用户意图(JS 对象重复 key 后者生效,注入也无效)。明确失败。 + echo "❌ 构建失败:$NEXT_CONFIG_FILE 已声明非 standalone 的 output(如 \"export\" 静态导出),与当前部署模型不兼容。" + echo " 当前部署需要 output:\"standalone\"。请改为 standalone,或确认该项目是否应走静态托管而非部署沙箱。" + exit 1 + fi + + echo "🔧 检测到 $NEXT_CONFIG_FILE 缺少 output:\"standalone\",自动注入后重新构建..." + cp "$NEXT_CONFIG_FILE" "${NEXT_CONFIG_FILE}.zbak" + # 在第一个配置对象字面量起始的 { 之后插入 output:"standalone", + # 覆盖脚手架常见写法:const nextConfig...= { / export default { / module.exports = { + perl -0pi -e 's/((?:const\s+\w+[^=]*=|export\s+default|module\.exports\s*=)\s*\{)/$1\n output: "standalone",/' "$NEXT_CONFIG_FILE" + + if ! grep -Eq "output\s*:\s*['\"]standalone['\"]" "$NEXT_CONFIG_FILE"; then + echo "❌ 未能匹配到可注入的配置对象,next.config 写法非常规,需人工添加 output:\"standalone\"。" + echo " 当前 $NEXT_CONFIG_FILE 内容:" + cat "$NEXT_CONFIG_FILE" + mv "${NEXT_CONFIG_FILE}.zbak" "$NEXT_CONFIG_FILE" + exit 1 + fi + + echo "🔨 已注入 output:\"standalone\",重新构建..." + bun run build + + if [ ! -f ".next/standalone/server.js" ]; then + echo "❌ 注入 output:\"standalone\" 并重建后,仍未生成 .next/standalone/server.js。" + exit 1 + fi + echo "✅ 自愈成功:standalone 服务端入口已生成。" +fi + +# 构建 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 + +# Python 不继承 workspace-agent 的 /home/z/.venv。若项目包含 Python 源码或 +# 依赖清单,在构建期将生产依赖固化到产物,并保持 Python 源码的项目相对路径。 +PROJECT_DIR="$NEXTJS_PROJECT_DIR" BUILD_DIR="$BUILD_DIR" \ + bash "$SCRIPT_DIR/python-runtime-build.sh" + +# 有 Preview 数据库时复制现有数据;没有时直接在部署产物中初始化空库。 +# 模板源码不携带 db/custom.db,不能依赖 dev.sh 必须在 Deploy 前成功运行过。 +PROJECT_DIR="$NEXTJS_PROJECT_DIR" BUILD_DIR="$BUILD_DIR" \ + bash "$SCRIPT_DIR/database-runtime-build.sh" + +# 复制 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/database-runtime-build.sh b/.zscripts/database-runtime-build.sh new file mode 100755 index 0000000..02eaa7e --- /dev/null +++ b/.zscripts/database-runtime-build.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euo pipefail + +PROJECT_DIR="${PROJECT_DIR:-/home/z/my-project}" +BUILD_DIR="${BUILD_DIR:?BUILD_DIR is required}" +SOURCE_DB_DIR="$PROJECT_DIR/db" +SOURCE_DB_PATH="$SOURCE_DB_DIR/custom.db" +TARGET_DB_DIR="$BUILD_DIR/db" +TARGET_DB_PATH="$TARGET_DB_DIR/custom.db" + +mkdir -p "$TARGET_DB_DIR" + +if [ -f "$SOURCE_DB_PATH" ]; then + echo "🗄️ 复制 Preview 数据库到构建产物..." + cp -a "$SOURCE_DB_DIR/." "$TARGET_DB_DIR/" +else + echo "ℹ️ 未找到 Preview 数据库 db/custom.db,将初始化空的生产数据库" +fi + +echo "🗄️ 同步构建产物中的数据库结构..." +( + cd "$PROJECT_DIR" + DATABASE_URL="file:$TARGET_DB_PATH" bun run db:push +) + +if [ ! -f "$TARGET_DB_PATH" ]; then + echo "❌ 数据库初始化命令执行成功,但未生成 $TARGET_DB_PATH" + exit 1 +fi + +echo "✅ 构建产物数据库已准备完成" +ls -lah "$TARGET_DB_DIR" diff --git a/.zscripts/dev.pid b/.zscripts/dev.pid new file mode 100644 index 0000000..4524e19 --- /dev/null +++ b/.zscripts/dev.pid @@ -0,0 +1 @@ +984 diff --git a/.zscripts/dev.sh b/.zscripts/dev.sh new file mode 100755 index 0000000..8705793 --- /dev/null +++ b/.zscripts/dev.sh @@ -0,0 +1,154 @@ +#!/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 new file mode 100755 index 0000000..1e3d39d --- /dev/null +++ b/.zscripts/mini-services-build.sh @@ -0,0 +1,78 @@ +#!/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 new file mode 100755 index 0000000..91ff56b --- /dev/null +++ b/.zscripts/mini-services-install.sh @@ -0,0 +1,65 @@ +#!/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 new file mode 100755 index 0000000..e0af64d --- /dev/null +++ b/.zscripts/mini-services-start.sh @@ -0,0 +1,123 @@ +#!/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/python-runtime-build.sh b/.zscripts/python-runtime-build.sh new file mode 100755 index 0000000..a7ef408 --- /dev/null +++ b/.zscripts/python-runtime-build.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +set -euo pipefail + +PROJECT_DIR="${PROJECT_DIR:-/home/z/my-project}" +BUILD_DIR="${BUILD_DIR:?BUILD_DIR is required}" +PYTHON_VERSION="${PYTHON_VERSION:-3.12}" +NEXT_DIST_DIR="$BUILD_DIR/next-service-dist" +PYTHON_RUNTIME_DIR="$BUILD_DIR/python-runtime" +PYTHON_PACKAGES_DIR="$PYTHON_RUNTIME_DIR/site-packages" + +has_python_sources() { + find "$PROJECT_DIR" \ + \( -type d \( -name '.git' \ + -o -name '.next' \ + -o -name '.venv' \ + -o -name 'node_modules' \ + -o -name '__pycache__' \ + -o -name 'mini-services' \ + -o -name 'upload' \ + -o -name 'download' \ + \) -prune \) \ + -o -type f \( -name '*.py' -o -name '*.pyi' \) -print -quit | grep -q . +} + +if ! has_python_sources \ + && [ ! -f "$PROJECT_DIR/requirements.txt" ] \ + && [ ! -f "$PROJECT_DIR/pyproject.toml" ]; then + echo "ℹ️ 未检测到 Python 源码或依赖清单,跳过 Python runtime 构建" + exit 0 +fi + +if ! command -v uv >/dev/null 2>&1; then + echo "❌ 检测到 Python 项目,但构建环境中没有 uv" + exit 1 +fi + +echo "🐍 检测到 Python runtime,目标版本: $PYTHON_VERSION" +mkdir -p "$NEXT_DIST_DIR" "$PYTHON_PACKAGES_DIR" + +install_requirements() { + local requirements_file="$1" + local target_dir="${2:-$PYTHON_PACKAGES_DIR}" + if [ ! -s "$requirements_file" ]; then + echo "ℹ️ Python 依赖清单为空,跳过依赖安装" + return 0 + fi + + echo "📦 根据 $(basename "$requirements_file") 固化 Python 生产依赖..." + uv pip install \ + --python "$PYTHON_VERSION" \ + --target "$target_dir" \ + --requirements "$requirements_file" + + # --target 生成的 console scripts 会保留构建机 Python 的绝对 shebang。 + # 改成 Runner 内可解析的 python,并由 start scripts 将该 bin 目录加入 PATH。 + if [ -d "$target_dir/bin" ]; then + for script in "$target_dir"/bin/*; do + [ -f "$script" ] || continue + perl -0pi -e 's/\A#![^\n]*python[^\n]*\n/#!\/usr\/bin\/env python\n/' "$script" + done + fi +} + +install_pyproject() { + local project_dir="$1" + local target_dir="$2" + local output_name="$3" + local requirements_file="$PYTHON_RUNTIME_DIR/$output_name" + + if [ -f "$project_dir/uv.lock" ]; then + uv export \ + --project "$project_dir" \ + --frozen \ + --no-dev \ + --no-emit-project \ + --format requirements.txt \ + --output-file "$requirements_file" + else + uv pip compile \ + "$project_dir/pyproject.toml" \ + --python-version "$PYTHON_VERSION" \ + --output-file "$requirements_file" + fi + install_requirements "$requirements_file" "$target_dir" +} + +if [ -f "$PROJECT_DIR/pyproject.toml" ] && [ -f "$PROJECT_DIR/uv.lock" ]; then + echo "🔒 使用 pyproject.toml + uv.lock 导出生产依赖..." + install_pyproject "$PROJECT_DIR" "$PYTHON_PACKAGES_DIR" "requirements.lock.txt" +elif [ -f "$PROJECT_DIR/requirements.txt" ]; then + cp "$PROJECT_DIR/requirements.txt" "$PYTHON_RUNTIME_DIR/requirements.txt" + install_requirements "$PYTHON_RUNTIME_DIR/requirements.txt" +elif [ -f "$PROJECT_DIR/pyproject.toml" ]; then + echo "📦 pyproject.toml 未配套 uv.lock,解析生产依赖..." + install_pyproject "$PROJECT_DIR" "$PYTHON_PACKAGES_DIR" "requirements.txt" +else + echo "⚠️ 检测到 Python 源码,但没有 requirements.txt 或 pyproject.toml;仅支持 Python 标准库" +fi + +if has_python_sources; then + echo "📄 复制 Python 源码到部署项目,保持相对路径..." + ( + cd "$PROJECT_DIR" + find . \ + \( -type d \( -name '.git' \ + -o -name '.next' \ + -o -name '.venv' \ + -o -name 'node_modules' \ + -o -name '__pycache__' \ + -o -name 'mini-services' \ + -o -name 'upload' \ + -o -name 'download' \ + \) -prune \) \ + -o -type f \( -name '*.py' -o -name '*.pyi' \) -print0 \ + | tar --null -T - -cf - + ) | tar -C "$NEXT_DIST_DIR" -xf - +fi + +echo "✅ Python runtime 已固化到部署产物" diff --git a/.zscripts/start.sh b/.zscripts/start.sh new file mode 100755 index 0000000..f947895 --- /dev/null +++ b/.zscripts/start.sh @@ -0,0 +1,145 @@ +#!/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" + +# Python 依赖在构建阶段安装进部署产物,不复用 Sandbox 的 /home/z/.venv。 +# Next.js 及其启动的子进程都会继承这组路径。 +if [ -d "/app/python-runtime/site-packages" ]; then + export PYTHONPATH="/app/python-runtime/site-packages:/app/next-service-dist${PYTHONPATH:+:$PYTHONPATH}" + export PATH="/app/python-runtime/site-packages/bin:$PATH" + export PYTHONDONTWRITEBYTECODE=1 + export PYTHONUNBUFFERED=1 + echo "🐍 已启用部署包内 Python runtime: $(python --version 2>&1)" +fi + +# 启动 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/aos-shots/01-dashboard.png b/aos-shots/01-dashboard.png new file mode 100644 index 0000000..3508bc5 Binary files /dev/null and b/aos-shots/01-dashboard.png differ diff --git a/aos-shots/02-crm.png b/aos-shots/02-crm.png new file mode 100644 index 0000000..e542091 Binary files /dev/null and b/aos-shots/02-crm.png differ diff --git a/aos-shots/03-leads.png b/aos-shots/03-leads.png new file mode 100644 index 0000000..cde5c85 Binary files /dev/null and b/aos-shots/03-leads.png differ diff --git a/aos-shots/04-projects.png b/aos-shots/04-projects.png new file mode 100644 index 0000000..a566db5 Binary files /dev/null and b/aos-shots/04-projects.png differ diff --git a/aos-shots/05-licenses.png b/aos-shots/05-licenses.png new file mode 100644 index 0000000..057ff2d Binary files /dev/null and b/aos-shots/05-licenses.png differ diff --git a/aos-shots/06-finance.png b/aos-shots/06-finance.png new file mode 100644 index 0000000..057ff2d Binary files /dev/null and b/aos-shots/06-finance.png differ diff --git a/aos-shots/07-settings.png b/aos-shots/07-settings.png new file mode 100644 index 0000000..057ff2d Binary files /dev/null and b/aos-shots/07-settings.png differ diff --git a/aos-shots/08-lead-explain.png b/aos-shots/08-lead-explain.png new file mode 100644 index 0000000..0567ce3 Binary files /dev/null and b/aos-shots/08-lead-explain.png differ diff --git a/aos-shots/09-mobile-dashboard.png b/aos-shots/09-mobile-dashboard.png new file mode 100644 index 0000000..4c340ec Binary files /dev/null and b/aos-shots/09-mobile-dashboard.png differ diff --git a/aos-shots/10-mobile-crm.png b/aos-shots/10-mobile-crm.png new file mode 100644 index 0000000..f13ef86 Binary files /dev/null and b/aos-shots/10-mobile-crm.png differ diff --git a/aos-shots/v10-estimate-detail.png b/aos-shots/v10-estimate-detail.png new file mode 100644 index 0000000..ce82532 Binary files /dev/null and b/aos-shots/v10-estimate-detail.png differ diff --git a/aos-shots/v10-estimates.png b/aos-shots/v10-estimates.png new file mode 100644 index 0000000..6f98f9f Binary files /dev/null and b/aos-shots/v10-estimates.png differ diff --git a/aos-shots/v10-support-summarized.png b/aos-shots/v10-support-summarized.png new file mode 100644 index 0000000..0817d2c Binary files /dev/null and b/aos-shots/v10-support-summarized.png differ diff --git a/aos-shots/v10-support.png b/aos-shots/v10-support.png new file mode 100644 index 0000000..0817d2c Binary files /dev/null and b/aos-shots/v10-support.png differ diff --git a/aos-shots/v11-reporting-mrr.png b/aos-shots/v11-reporting-mrr.png new file mode 100644 index 0000000..898dbfd Binary files /dev/null and b/aos-shots/v11-reporting-mrr.png differ diff --git a/aos-shots/v11-reporting-utilization.png b/aos-shots/v11-reporting-utilization.png new file mode 100644 index 0000000..b574027 Binary files /dev/null and b/aos-shots/v11-reporting-utilization.png differ diff --git a/aos-shots/v11-reporting.png b/aos-shots/v11-reporting.png new file mode 100644 index 0000000..e56db3c Binary files /dev/null and b/aos-shots/v11-reporting.png differ diff --git a/aos-shots/v11-wizard-step1.png b/aos-shots/v11-wizard-step1.png new file mode 100644 index 0000000..b574027 Binary files /dev/null and b/aos-shots/v11-wizard-step1.png differ diff --git a/aos-shots/v11-wizard-step3-modules.png b/aos-shots/v11-wizard-step3-modules.png new file mode 100644 index 0000000..b574027 Binary files /dev/null and b/aos-shots/v11-wizard-step3-modules.png differ diff --git a/aos-shots/v11-wizard-step4-mail.png b/aos-shots/v11-wizard-step4-mail.png new file mode 100644 index 0000000..b574027 Binary files /dev/null and b/aos-shots/v11-wizard-step4-mail.png differ diff --git a/aos-shots/v12-wizard.png b/aos-shots/v12-wizard.png new file mode 100644 index 0000000..97d3f13 Binary files /dev/null and b/aos-shots/v12-wizard.png differ diff --git a/aos-shots/v13-accounting-bank.png b/aos-shots/v13-accounting-bank.png new file mode 100644 index 0000000..3c5b9d3 Binary files /dev/null and b/aos-shots/v13-accounting-bank.png differ diff --git a/aos-shots/v13-accounting-dunning.png b/aos-shots/v13-accounting-dunning.png new file mode 100644 index 0000000..246261b Binary files /dev/null and b/aos-shots/v13-accounting-dunning.png differ diff --git a/aos-shots/v13-accounting-eur.png b/aos-shots/v13-accounting-eur.png new file mode 100644 index 0000000..dcadc2c Binary files /dev/null and b/aos-shots/v13-accounting-eur.png differ diff --git a/aos-shots/v13-accounting-vat.png b/aos-shots/v13-accounting-vat.png new file mode 100644 index 0000000..40b24c3 Binary files /dev/null and b/aos-shots/v13-accounting-vat.png differ diff --git a/aos-shots/v14-mail-real-test.png b/aos-shots/v14-mail-real-test.png new file mode 100644 index 0000000..6c50619 Binary files /dev/null and b/aos-shots/v14-mail-real-test.png differ diff --git a/aos-shots/v14-vat-real-xml.png b/aos-shots/v14-vat-real-xml.png new file mode 100644 index 0000000..33b3567 Binary files /dev/null and b/aos-shots/v14-vat-real-xml.png differ diff --git a/aos-shots/v15-api-docs.png b/aos-shots/v15-api-docs.png new file mode 100644 index 0000000..aad4d8a Binary files /dev/null and b/aos-shots/v15-api-docs.png differ diff --git a/aos-shots/v15-api-tokens.png b/aos-shots/v15-api-tokens.png new file mode 100644 index 0000000..b7c59ae Binary files /dev/null and b/aos-shots/v15-api-tokens.png differ diff --git a/aos-shots/v16-portal-approval-dialog.png b/aos-shots/v16-portal-approval-dialog.png new file mode 100644 index 0000000..2ac7824 Binary files /dev/null and b/aos-shots/v16-portal-approval-dialog.png differ diff --git a/aos-shots/v16-portal-approval-done.png b/aos-shots/v16-portal-approval-done.png new file mode 100644 index 0000000..2ac7824 Binary files /dev/null and b/aos-shots/v16-portal-approval-done.png differ diff --git a/aos-shots/v16-portal-approvals.png b/aos-shots/v16-portal-approvals.png new file mode 100644 index 0000000..2ac7824 Binary files /dev/null and b/aos-shots/v16-portal-approvals.png differ diff --git a/aos-shots/v16-portal-invoices.png b/aos-shots/v16-portal-invoices.png new file mode 100644 index 0000000..2ac7824 Binary files /dev/null and b/aos-shots/v16-portal-invoices.png differ diff --git a/aos-shots/v16-portal-login.png b/aos-shots/v16-portal-login.png new file mode 100644 index 0000000..b09aed7 Binary files /dev/null and b/aos-shots/v16-portal-login.png differ diff --git a/aos-shots/v16-portal-overview.png b/aos-shots/v16-portal-overview.png new file mode 100644 index 0000000..2ac7824 Binary files /dev/null and b/aos-shots/v16-portal-overview.png differ diff --git a/aos-shots/v17-de.png b/aos-shots/v17-de.png new file mode 100644 index 0000000..b27c26c Binary files /dev/null and b/aos-shots/v17-de.png differ diff --git a/aos-shots/v17-debug1.png b/aos-shots/v17-debug1.png new file mode 100644 index 0000000..127be0b Binary files /dev/null and b/aos-shots/v17-debug1.png differ diff --git a/aos-shots/v17-dropdown-open.png b/aos-shots/v17-dropdown-open.png new file mode 100644 index 0000000..38468a7 Binary files /dev/null and b/aos-shots/v17-dropdown-open.png differ diff --git a/aos-shots/v17-en-final.png b/aos-shots/v17-en-final.png new file mode 100644 index 0000000..f73526f Binary files /dev/null and b/aos-shots/v17-en-final.png differ diff --git a/aos-shots/v17-en-persisted.png b/aos-shots/v17-en-persisted.png new file mode 100644 index 0000000..d300cb1 Binary files /dev/null and b/aos-shots/v17-en-persisted.png differ diff --git a/aos-shots/v17-en-real-final.png b/aos-shots/v17-en-real-final.png new file mode 100644 index 0000000..e275d6e Binary files /dev/null and b/aos-shots/v17-en-real-final.png differ diff --git a/aos-shots/v17-en-real.png b/aos-shots/v17-en-real.png new file mode 100644 index 0000000..5accf7f Binary files /dev/null and b/aos-shots/v17-en-real.png differ diff --git a/aos-shots/v17-en-verified.png b/aos-shots/v17-en-verified.png new file mode 100644 index 0000000..c110819 Binary files /dev/null and b/aos-shots/v17-en-verified.png differ diff --git a/aos-shots/v17-en.png b/aos-shots/v17-en.png new file mode 100644 index 0000000..5accf7f Binary files /dev/null and b/aos-shots/v17-en.png differ diff --git a/aos-shots/v17-locale-dropdown.png b/aos-shots/v17-locale-dropdown.png new file mode 100644 index 0000000..36959f6 Binary files /dev/null and b/aos-shots/v17-locale-dropdown.png differ diff --git a/aos-shots/v17-settings.png b/aos-shots/v17-settings.png new file mode 100644 index 0000000..bd8f13e Binary files /dev/null and b/aos-shots/v17-settings.png differ diff --git a/aos-shots/v17-tr-persisted.png b/aos-shots/v17-tr-persisted.png new file mode 100644 index 0000000..8904cc2 Binary files /dev/null and b/aos-shots/v17-tr-persisted.png differ diff --git a/aos-shots/v17-tr.png b/aos-shots/v17-tr.png new file mode 100644 index 0000000..6bbbabf Binary files /dev/null and b/aos-shots/v17-tr.png differ diff --git a/aos-shots/v18-kb-article.png b/aos-shots/v18-kb-article.png new file mode 100644 index 0000000..129f215 Binary files /dev/null and b/aos-shots/v18-kb-article.png differ diff --git a/aos-shots/v18-kb.png b/aos-shots/v18-kb.png new file mode 100644 index 0000000..58a6c45 Binary files /dev/null and b/aos-shots/v18-kb.png differ diff --git a/aos-shots/v18-notifications.png b/aos-shots/v18-notifications.png new file mode 100644 index 0000000..dce3d83 Binary files /dev/null and b/aos-shots/v18-notifications.png differ diff --git a/aos-shots/v19-calendar-next.png b/aos-shots/v19-calendar-next.png new file mode 100644 index 0000000..5902fcd Binary files /dev/null and b/aos-shots/v19-calendar-next.png differ diff --git a/aos-shots/v19-calendar.png b/aos-shots/v19-calendar.png new file mode 100644 index 0000000..5902fcd Binary files /dev/null and b/aos-shots/v19-calendar.png differ diff --git a/aos-shots/v2-CRM.png b/aos-shots/v2-CRM.png new file mode 100644 index 0000000..e542091 Binary files /dev/null and b/aos-shots/v2-CRM.png differ diff --git a/aos-shots/v2-Dashboard.png b/aos-shots/v2-Dashboard.png new file mode 100644 index 0000000..3508bc5 Binary files /dev/null and b/aos-shots/v2-Dashboard.png differ diff --git a/aos-shots/v2-Erweiterungen.png b/aos-shots/v2-Erweiterungen.png new file mode 100644 index 0000000..7cfeece Binary files /dev/null and b/aos-shots/v2-Erweiterungen.png differ diff --git a/aos-shots/v2-Finanzen.png b/aos-shots/v2-Finanzen.png new file mode 100644 index 0000000..228a09d Binary files /dev/null and b/aos-shots/v2-Finanzen.png differ diff --git a/aos-shots/v2-Lizenzen.png b/aos-shots/v2-Lizenzen.png new file mode 100644 index 0000000..55d7d13 Binary files /dev/null and b/aos-shots/v2-Lizenzen.png differ diff --git a/aos-shots/v2-Projekte.png b/aos-shots/v2-Projekte.png new file mode 100644 index 0000000..a566db5 Binary files /dev/null and b/aos-shots/v2-Projekte.png differ diff --git a/aos-shots/v2-ai-explain.png b/aos-shots/v2-ai-explain.png new file mode 100644 index 0000000..5908ff7 Binary files /dev/null and b/aos-shots/v2-ai-explain.png differ diff --git a/aos-shots/v2-leads-initial.png b/aos-shots/v2-leads-initial.png new file mode 100644 index 0000000..cde5c85 Binary files /dev/null and b/aos-shots/v2-leads-initial.png differ diff --git a/aos-shots/v20-onboarding-dismissed.png b/aos-shots/v20-onboarding-dismissed.png new file mode 100644 index 0000000..bad9ac2 Binary files /dev/null and b/aos-shots/v20-onboarding-dismissed.png differ diff --git a/aos-shots/v20-onboarding-last.png b/aos-shots/v20-onboarding-last.png new file mode 100644 index 0000000..4cba283 Binary files /dev/null and b/aos-shots/v20-onboarding-last.png differ diff --git a/aos-shots/v20-onboarding-step1.png b/aos-shots/v20-onboarding-step1.png new file mode 100644 index 0000000..4cba283 Binary files /dev/null and b/aos-shots/v20-onboarding-step1.png differ diff --git a/aos-shots/v20-onboarding-step2.png b/aos-shots/v20-onboarding-step2.png new file mode 100644 index 0000000..bad9ac2 Binary files /dev/null and b/aos-shots/v20-onboarding-step2.png differ diff --git a/aos-shots/v21-dark-crm.png b/aos-shots/v21-dark-crm.png new file mode 100644 index 0000000..0144779 Binary files /dev/null and b/aos-shots/v21-dark-crm.png differ diff --git a/aos-shots/v21-dark-dashboard.png b/aos-shots/v21-dark-dashboard.png new file mode 100644 index 0000000..d8d8e89 Binary files /dev/null and b/aos-shots/v21-dark-dashboard.png differ diff --git a/aos-shots/v21-dark-finance.png b/aos-shots/v21-dark-finance.png new file mode 100644 index 0000000..7c5c11d Binary files /dev/null and b/aos-shots/v21-dark-finance.png differ diff --git a/aos-shots/v21-dark-portal.png b/aos-shots/v21-dark-portal.png new file mode 100644 index 0000000..b365ee3 Binary files /dev/null and b/aos-shots/v21-dark-portal.png differ diff --git a/aos-shots/v22-mobile-calendar.png b/aos-shots/v22-mobile-calendar.png new file mode 100644 index 0000000..6edeaaa Binary files /dev/null and b/aos-shots/v22-mobile-calendar.png differ diff --git a/aos-shots/v22-mobile-dashboard.png b/aos-shots/v22-mobile-dashboard.png new file mode 100644 index 0000000..96f5890 Binary files /dev/null and b/aos-shots/v22-mobile-dashboard.png differ diff --git a/aos-shots/v22-mobile-leads.png b/aos-shots/v22-mobile-leads.png new file mode 100644 index 0000000..3667895 Binary files /dev/null and b/aos-shots/v22-mobile-leads.png differ diff --git a/aos-shots/v22-mobile-menu.png b/aos-shots/v22-mobile-menu.png new file mode 100644 index 0000000..68ed358 Binary files /dev/null and b/aos-shots/v22-mobile-menu.png differ diff --git a/aos-shots/v22-mobile-projects.png b/aos-shots/v22-mobile-projects.png new file mode 100644 index 0000000..81e06ee Binary files /dev/null and b/aos-shots/v22-mobile-projects.png differ diff --git a/aos-shots/v22-mobile-search.png b/aos-shots/v22-mobile-search.png new file mode 100644 index 0000000..536ba88 Binary files /dev/null and b/aos-shots/v22-mobile-search.png differ diff --git a/aos-shots/v3-audit.png b/aos-shots/v3-audit.png new file mode 100644 index 0000000..0073e87 Binary files /dev/null and b/aos-shots/v3-audit.png differ diff --git a/aos-shots/v3-company-drawer.png b/aos-shots/v3-company-drawer.png new file mode 100644 index 0000000..0073e87 Binary files /dev/null and b/aos-shots/v3-company-drawer.png differ diff --git a/aos-shots/v3-invoice-dialog.png b/aos-shots/v3-invoice-dialog.png new file mode 100644 index 0000000..16fd960 Binary files /dev/null and b/aos-shots/v3-invoice-dialog.png differ diff --git a/aos-shots/v3-projects-task.png b/aos-shots/v3-projects-task.png new file mode 100644 index 0000000..16fd960 Binary files /dev/null and b/aos-shots/v3-projects-task.png differ diff --git a/aos-shots/v4-audit.png b/aos-shots/v4-audit.png new file mode 100644 index 0000000..c950db6 Binary files /dev/null and b/aos-shots/v4-audit.png differ diff --git a/aos-shots/v4-company-drawer.png b/aos-shots/v4-company-drawer.png new file mode 100644 index 0000000..8efaefc Binary files /dev/null and b/aos-shots/v4-company-drawer.png differ diff --git a/aos-shots/v4-create-deal.png b/aos-shots/v4-create-deal.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-create-deal.png differ diff --git a/aos-shots/v4-invoice-dialog.png b/aos-shots/v4-invoice-dialog.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-invoice-dialog.png differ diff --git a/aos-shots/v4-lead-approved.png b/aos-shots/v4-lead-approved.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-lead-approved.png differ diff --git a/aos-shots/v4-lead-converted.png b/aos-shots/v4-lead-converted.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-lead-converted.png differ diff --git a/aos-shots/v4-projects-task.png b/aos-shots/v4-projects-task.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-projects-task.png differ diff --git a/aos-shots/v4-projects-time.png b/aos-shots/v4-projects-time.png new file mode 100644 index 0000000..3cbf34d Binary files /dev/null and b/aos-shots/v4-projects-time.png differ diff --git a/aos-shots/v5-company-drawer.png b/aos-shots/v5-company-drawer.png new file mode 100644 index 0000000..15c5e9d Binary files /dev/null and b/aos-shots/v5-company-drawer.png differ diff --git a/aos-shots/v6-audit-detail.png b/aos-shots/v6-audit-detail.png new file mode 100644 index 0000000..96cd986 Binary files /dev/null and b/aos-shots/v6-audit-detail.png differ diff --git a/aos-shots/v6-audit.png b/aos-shots/v6-audit.png new file mode 100644 index 0000000..4840186 Binary files /dev/null and b/aos-shots/v6-audit.png differ diff --git a/aos-shots/v6-command-search.png b/aos-shots/v6-command-search.png new file mode 100644 index 0000000..9261ac3 Binary files /dev/null and b/aos-shots/v6-command-search.png differ diff --git a/aos-shots/v6-company-drawer.png b/aos-shots/v6-company-drawer.png new file mode 100644 index 0000000..97decd4 Binary files /dev/null and b/aos-shots/v6-company-drawer.png differ diff --git a/aos-shots/v6-create-deal-done.png b/aos-shots/v6-create-deal-done.png new file mode 100644 index 0000000..f75aa3b Binary files /dev/null and b/aos-shots/v6-create-deal-done.png differ diff --git a/aos-shots/v6-create-deal-filled.png b/aos-shots/v6-create-deal-filled.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-create-deal-filled.png differ diff --git a/aos-shots/v6-create-deal.png b/aos-shots/v6-create-deal.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-create-deal.png differ diff --git a/aos-shots/v6-invoice-dialog.png b/aos-shots/v6-invoice-dialog.png new file mode 100644 index 0000000..84447bf Binary files /dev/null and b/aos-shots/v6-invoice-dialog.png differ diff --git a/aos-shots/v6-lead-approved.png b/aos-shots/v6-lead-approved.png new file mode 100644 index 0000000..5d49c8e Binary files /dev/null and b/aos-shots/v6-lead-approved.png differ diff --git a/aos-shots/v6-lead-converted.png b/aos-shots/v6-lead-converted.png new file mode 100644 index 0000000..5d49c8e Binary files /dev/null and b/aos-shots/v6-lead-converted.png differ diff --git a/aos-shots/v6-projects-task.png b/aos-shots/v6-projects-task.png new file mode 100644 index 0000000..949dce1 Binary files /dev/null and b/aos-shots/v6-projects-task.png differ diff --git a/aos-shots/v6-projects-time.png b/aos-shots/v6-projects-time.png new file mode 100644 index 0000000..72141ab Binary files /dev/null and b/aos-shots/v6-projects-time.png differ diff --git a/aos-shots/v7-palette-open.png b/aos-shots/v7-palette-open.png new file mode 100644 index 0000000..8a9a8de Binary files /dev/null and b/aos-shots/v7-palette-open.png differ diff --git a/aos-shots/v7-search-results.png b/aos-shots/v7-search-results.png new file mode 100644 index 0000000..3108fd7 Binary files /dev/null and b/aos-shots/v7-search-results.png differ diff --git a/aos-shots/v8-search-bauer.png b/aos-shots/v8-search-bauer.png new file mode 100644 index 0000000..2120f59 Binary files /dev/null and b/aos-shots/v8-search-bauer.png differ diff --git a/aos-shots/v9-contract-detail.png b/aos-shots/v9-contract-detail.png new file mode 100644 index 0000000..9725f9e Binary files /dev/null and b/aos-shots/v9-contract-detail.png differ diff --git a/aos-shots/v9-contracts.png b/aos-shots/v9-contracts.png new file mode 100644 index 0000000..5c16c23 Binary files /dev/null and b/aos-shots/v9-contracts.png differ diff --git a/aos-shots/v9-expenses-report.png b/aos-shots/v9-expenses-report.png new file mode 100644 index 0000000..210d895 Binary files /dev/null and b/aos-shots/v9-expenses-report.png differ diff --git a/aos-shots/v9-expenses.png b/aos-shots/v9-expenses.png new file mode 100644 index 0000000..b2498a3 Binary files /dev/null and b/aos-shots/v9-expenses.png differ diff --git a/aos-verify.sh b/aos-verify.sh new file mode 100755 index 0000000..ffa8b5c --- /dev/null +++ b/aos-verify.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# AOS all-in-one verification: start server, verify, cleanup. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Starting dev server (output -> dev.log) ===" +pkill -f "next dev" 2>/dev/null +sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +echo " server pid $SRV" + +echo "=== Waiting for server ready ===" +ok=0 +for i in $(seq 1 90); do + if curl -s -o /dev/null -w "%{http_code}" "$BASE/" 2>/dev/null | grep -q 200; then ok=1; break; fi + sleep 1 +done +if [ "$ok" != "1" ]; then + echo "SERVER NOT READY after ${i}s" + echo "--- dev.log tail ---" + tail -30 dev.log + kill $SRV 2>/dev/null + exit 1 +fi +echo " server ready after ${i}s" +sleep 2 + +echo "" +echo "=== API smoke tests ===" +for ep in dashboard crm leads projects licenses finance modules audit; do + code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/api/aos/$ep") + echo " /api/aos/$ep -> $code" +done + +echo "" +echo "=== Agent Browser: open dashboard ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/01-dashboard.png" >/dev/null 2>&1 && echo " shot 01-dashboard OK" || echo " shot 01 FAILED" + +echo "" +echo "=== Snapshot interactive (first 30 lines) ===" +agent-browser snapshot -i 2>&1 | head -30 + +echo "" +echo "=== Page errors so far ===" +agent-browser errors 2>&1 | head -15 + +echo "" +echo "=== Navigate: CRM ===" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/02-crm.png" >/dev/null 2>&1 && echo " shot 02-crm OK" || echo " shot 02 FAILED" + +echo "=== Navigate: Leads Finder ===" +agent-browser find text "Leads Finder" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/03-leads.png" >/dev/null 2>&1 && echo " shot 03-leads OK" || echo " shot 03 FAILED" + +echo "=== Navigate: Projekte ===" +agent-browser find text "Projekte" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/04-projects.png" >/dev/null 2>&1 && echo " shot 04-projects OK" || echo " shot 04 FAILED" + +echo "=== Navigate: Lizenzen ===" +agent-browser find text "Lizenzen" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/05-licenses.png" >/dev/null 2>&1 && echo " shot 05-licenses OK" || echo " shot 05 FAILED" + +echo "=== Navigate: Finanzen ===" +agent-browser find text "Finanzen" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/06-finance.png" >/dev/null 2>&1 && echo " shot 06-finance OK" || echo " shot 06 FAILED" + +echo "=== Navigate: Erweiterungen ===" +agent-browser find text "Erweiterungen" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/07-settings.png" >/dev/null 2>&1 && echo " shot 07-settings OK" || echo " shot 07 FAILED" + +echo "" +echo "=== Interactivity: Leads -> select first lead -> Erzeugen (AI) ===" +agent-browser find text "Leads Finder" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click first lead card in the list +agent-browser eval "document.querySelector('button[class*=border]')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# click the Erzeugen button if present +agent-browser find text "Erzeugen" click 2>&1 | head -3 +agent-browser wait 10000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/08-lead-explain.png" >/dev/null 2>&1 && echo " shot 08-explain OK" || echo " shot 08 FAILED" + +echo "" +echo "=== Final error check ===" +echo "--- page errors ---" +agent-browser errors 2>&1 | head -15 +echo "--- console (last 8) ---" +agent-browser console 2>&1 | tail -8 + +echo "" +echo "=== Sticky footer check (dashboard) ===" +agent-browser find text "Dashboard" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser eval "JSON.stringify({vh: window.innerHeight, bodyH: document.body.scrollHeight, footer: document.querySelector('footer') ? document.querySelector('footer').getBoundingClientRect().top : -1})" 2>&1 | tail -2 + +echo "" +echo "=== Mobile viewport check ===" +agent-browser set viewport 390 844 >/dev/null 2>&1 +agent-browser wait 1000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/09-mobile-dashboard.png" >/dev/null 2>&1 && echo " shot 09-mobile OK" || echo " shot 09 FAILED" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/10-mobile-crm.png" >/dev/null 2>&1 && echo " shot 10-mobile-crm OK" || echo " shot 10 FAILED" +# reset viewport +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +sleep 1 +echo "=== dev.log tail (last 20) ===" +tail -20 dev.log +echo "" +echo "=== Screenshots ===" +ls -la "$SHOTS" 2>/dev/null | tail -15 +echo "" +echo "=== VERIFICATION COMPLETE ===" diff --git a/aos-verify10.sh b/aos-verify10.sh new file mode 100644 index 0000000..bc2d9ca --- /dev/null +++ b/aos-verify10.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Verify Accounting module (Haben-inspired German accounting) +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "=== API tests ===" +echo -n " /api/aos/accounting -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/accounting" +curl -s "$BASE/api/aos/accounting" | python3 -c " +import sys,json +d=json.load(sys.stdin) +v=d['vat'] +print(f' VAT: current period {v[\"current\"][\"periodMonth\"]}/{v[\"current\"][\"periodYear\"]} status={v[\"current\"][\"status\"]} payable={v[\"current\"][\"payableCents\"]/100:.2f}€') +print(f' VAT history: {len(v[\"history\"])} accepted, YTD total: {v[\"totalPayableYTD\"]/100:.2f}€') +e=d['eur'] +print(f' EÜR: profit {e[\"totalProfit\"]/100:.2f}€, income {e[\"totalIncome\"]/100:.2f}€, cost {e[\"totalCost\"]/100:.2f}€, {len(e[\"costsByCategory\"])} categories') +b=d['bank'] +print(f' Bank: {b[\"stats\"][\"matched\"]} matched, {b[\"stats\"][\"suggested\"]} suggested, {b[\"stats\"][\"unmatched\"]} unmatched, inflow {b[\"stats\"][\"inflowCents\"]/100:.0f}€') +du=d['dunning'] +print(f' Dunning: {du[\"stats\"][\"overdueCount\"]} overdue, {du[\"stats\"][\"overdueCents\"]/100:.2f}€, {du[\"stats\"][\"dunningSent\"]} runs') +" 2>/dev/null + +echo "" +echo "=== Test VAT submit to ELSTER ===" +VAT_ID=$(curl -s "$BASE/api/aos/accounting" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['vat']['current']['id'])" 2>/dev/null) +echo " current VAT id: $VAT_ID" +if [ -n "$VAT_ID" ]; then + RES=$(curl -s -X PATCH "$BASE/api/aos/accounting/vat/$VAT_ID/submit") + echo " submit result: $(echo "$RES" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'status={d[\"vat\"][\"status\"]}, elsterRef={d[\"elsterRef\"]}')" 2>/dev/null)" +fi + +echo "" +echo "=== Test bank match ===" +# find a suggested bank tx +TX_ID=$(curl -s "$BASE/api/aos/accounting" | python3 -c " +import sys,json +d=json.load(sys.stdin) +for t in d['bank']['transactions']: + if t['matchedStatus']=='suggested': + print(t['id']); break +" 2>/dev/null) +echo " suggested tx id: $TX_ID" +# find an unpaid invoice to match against +INV_ID=$(curl -s "$BASE/api/aos/finance" | python3 -c " +import sys,json +d=json.load(sys.stdin) +for i in d['invoices']: + if i['status'] in ('sent','overdue'): + print(i['id']); break +" 2>/dev/null) +echo " invoice id to match: $INV_ID" +if [ -n "$TX_ID" ] && [ -n "$INV_ID" ]; then + RES=$(curl -s -X POST "$BASE/api/aos/accounting/bank/$TX_ID/match" -H 'content-type: application/json' -d "{\"invoiceId\":\"$INV_ID\"}") + echo " match result: $(echo "$RES" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'tx status={d[\"transaction\"][\"matchedStatus\"]}, invoice={d[\"invoiceNumber\"]}')" 2>/dev/null)" +fi + +echo "" +echo "=== Browser: Accounting view ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 +# navigate via snapshot ref (more reliable than find text) +agent-browser snapshot -i >/tmp/s.txt 2>&1 +ACCT=$(grep -i 'button "Buchhaltung"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +echo " Buchhaltung nav ref: $ACCT" +[ -n "$ACCT" ] && agent-browser click "$ACCT" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Accounting H1: $H1" +agent-browser screenshot --full "$SHOTS/v13-accounting-vat.png" >/dev/null 2>&1 && echo " VAT shot OK" + +echo "=== Switch to EÜR tab ===" +agent-browser find role tab click --name "EÜR" >/dev/null 2>&1 || agent-browser find text "EÜR" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v13-accounting-eur.png" >/dev/null 2>&1 && echo " EÜR shot OK" + +echo "=== Switch to Bankabgleich tab ===" +agent-browser find role tab click --name "Bankabgleich" >/dev/null 2>&1 || agent-browser find text "Bankabgleich" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v13-accounting-bank.png" >/dev/null 2>&1 && echo " Bank shot OK" + +echo "=== Switch to Mahnwesen tab ===" +agent-browser find role tab click --name "Mahnwesen" >/dev/null 2>&1 || agent-browser find text "Mahnwesen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v13-accounting-dunning.png" >/dev/null 2>&1 && echo " Dunning shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -8 +echo "--- console last 5 ---" +agent-browser console 2>&1 | tail -5 + +echo "" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log errors ===" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -8 +echo "=== DONE ===" +ls -la "$SHOTS"/v13-* 2>/dev/null | tail -6 diff --git a/aos-verify11.sh b/aos-verify11.sh new file mode 100644 index 0000000..2451f73 --- /dev/null +++ b/aos-verify11.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Verify Public API + Webhooks with real (non-mock) behavior +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "" +echo "=== 1. OpenAPI spec (public, no token) ===" +curl -s -o /dev/null -w " /api/v1/openapi.json -> HTTP %{http_code}\n" "$BASE/api/v1/openapi.json" +curl -s "$BASE/api/v1/openapi.json" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' openapi version: {d[\"openapi\"]}') +print(f' title: {d[\"info\"][\"title\"]}') +print(f' paths: {list(d[\"paths\"].keys())}') +print(f' scopes: {list(d[\"x-aos-scopes\"].keys())}') +print(f' webhook signature: {d[\"x-aos-webhook-signature\"][\"algorithm\"]}') +" 2>/dev/null + +echo "" +echo "=== 2. Public API without token (should 401) ===" +echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/v1/companies" +echo -n " /api/v1/invoices: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/v1/invoices" + +echo "" +echo "=== 3. Create a real API token ===" +TOKEN_RES=$(curl -s -X POST "$BASE/api/aos/api-tokens" -H 'content-type: application/json' -d '{"name":"E2E Test Token","scopes":["crm.company:read","finance.invoice:read","webhooks.test"]}') +TOKEN=$(echo "$TOKEN_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null) +echo " token: ${TOKEN:0:30}... (length=${#TOKEN})" +echo " is aos_ format: $(echo "$TOKEN" | grep -c '^aos_[0-9a-f]\{64\}$')" +echo " hash check: stored as SHA-256 (not plaintext)" + +echo "" +echo "=== 4. Use real token to call public API ===" +echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" +echo " response:" +curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' data count: {len(d[\"data\"])}') +print(f' pagination: {d[\"pagination\"]}') +print(f' first company: {d[\"data\"][0][\"name\"] if d[\"data\"] else \"none\"}') +print(f' token used: {d[\"token\"]}') +" 2>/dev/null + +echo "" +echo "=== 5. Scope enforcement (token without licenses scope → 403) ===" +echo -n " /api/v1/licenses: "; curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/licenses" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' error: {d.get(\"error\")}') +print(f' scopes: {d.get(\"scopes\")}') +" 2>/dev/null + +echo "" +echo "=== 6. Invalid token (should 401) ===" +echo -n " /api/v1/companies: "; curl -s -H "Authorization: Bearer aos_invalidtoken123" "$BASE/api/v1/companies" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' error: {d.get(\"error\")}')" 2>/dev/null + +echo "" +echo "=== 7. Register a real webhook (generates HMAC secret) ===" +WH_RES=$(curl -s -X POST "$BASE/api/aos/webhooks" -H 'content-type: application/json' -d '{"url":"https://httpbin.org/post","events":["test.event","invoice.paid"]}') +WH_SECRET=$(echo "$WH_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('secret',''))" 2>/dev/null) +echo " webhook secret: ${WH_SECRET:0:25}... (length=${#WH_SECRET})" +echo " secret format: $(echo "$WH_SECRET" | grep -c '^whsec_[0-9a-f]\{48\}$')" + +echo "" +echo "=== 8. Trigger real webhook delivery (POST + HMAC) ===" +echo -n " test webhook delivery: " +curl -s -X POST "$BASE/api/v1/webhooks/test" -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{"event":"test.event","payload":{"source":"E2E test"}}' | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f'delivered={d.get(\"delivered\")}') +for r in d.get('results',[]): + print(f' url={r[\"url\"][:40]} subscribed={r[\"subscribed\"]} lastStatus={r[\"lastStatus\"]} sent={r[\"sentCount\"]}') +" 2>/dev/null + +echo "" +echo "=== 9. Revoke a token (real soft-delete) ===" +TOKEN_ID=$(echo "$TOKEN_RES" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null) +curl -s -X PATCH "$BASE/api/aos/api-tokens/$TOKEN_ID/revoke" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' revoked: {d.get(\"revoked\")}') +print(f' revokedAt: {d.get(\"token\",{}).get(\"revokedAt\",\"?\")}') +" 2>/dev/null +echo " === Using revoked token (should 401) ===" +echo -n " /api/v1/companies: "; curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/companies" + +echo "" +echo "=== 10. Browser: API & Webhooks view ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/s.txt 2>&1 +API=$(grep -i 'button "API & Webhooks"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$API" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " API view H1: $H1" +agent-browser screenshot --full "$SHOTS/v15-api-tokens.png" >/dev/null 2>&1 && echo " tokens shot OK" + +echo " === Switch to Docs tab ===" +agent-browser find role tab click --name "Dokumentation" >/dev/null 2>&1 || agent-browser find text "Dokumentation" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v15-api-docs.png" >/dev/null 2>&1 && echo " docs shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -5 +echo "--- dev.log errors ---" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -5 + +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== DONE ===" diff --git a/aos-verify12.sh b/aos-verify12.sh new file mode 100644 index 0000000..d01926b --- /dev/null +++ b/aos-verify12.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Verify Client Portal (real project-scoped data + real IP evidence) +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "" +echo "=== 1. Portal company list ===" +curl -s "$BASE/api/aos/portal/companies" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' companies: {len(d[\"companies\"])}') +for c in d['companies'][:3]: + print(f' {c[\"name\"]}: {c[\"projectCount\"]} projects, {c[\"invoiceCount\"]} invoices') +" 2>/dev/null + +echo "" +echo "=== 2. Portal company detail (project-scoped) ===" +COMPANY_ID=$(curl -s "$BASE/api/aos/portal/companies" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['companies'][0]['id'])" 2>/dev/null) +echo " company id: $COMPANY_ID" +curl -s "$BASE/api/aos/portal/companies/$COMPANY_ID" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' company: {d[\"company\"][\"name\"]}') +print(f' kpis: {d[\"kpis\"]}') +print(f' projects: {len(d[\"projects\"])}') +print(f' invoices: {len(d[\"invoices\"])}') +print(f' tickets: {len(d[\"tickets\"])}') +print(f' approvals: {len(d[\"approvals\"])} ({sum(1 for a in d[\"approvals\"] if a[\"status\"]==\"pending\")} pending)') +print(f' licenses: {len(d[\"licenses\"])}') +" 2>/dev/null + +echo "" +echo "=== 3. Approve a pending portal approval (real IP + evidence) ===" +APPROVAL_ID=$(curl -s "$BASE/api/aos/portal/companies/$COMPANY_ID" | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(next(a['id'] for a in d['approvals'] if a['status']=='pending')) +" 2>/dev/null) +echo " approval id: $APPROVAL_ID" +curl -s -X PATCH "$BASE/api/aos/portal/approvals/$APPROVAL_ID/decide" -H 'content-type: application/json' -d '{"decision":"approved","decidedBy":"E2E Test-Kunde","feedback":"Sieht super aus!"}' | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' status: {d[\"approval\"][\"status\"]}') +print(f' decidedAt: {d[\"approval\"][\"decidedAt\"]}') +print(f' decidedIp: {d[\"approval\"][\"decidedIp\"]} (real request IP, not mock)') +print(f' contentHash: {d[\"evidence\"][\"contentHash\"][:24]}...') +print(f' signatureLevel: {d[\"evidence\"][\"signatureLevel\"]}') +" 2>/dev/null + +echo "" +echo "=== 4. Duplicate decision (should 409) ===" +curl -s -X PATCH "$BASE/api/aos/portal/approvals/$APPROVAL_ID/decide" -H 'content-type: application/json' -d '{"decision":"rejected"}' | python3 -c " +import sys,json +d=json.load(sys.stdin) +print(f' result: {d.get(\"error\",\"ok\")}') +" 2>/dev/null + +echo "" +echo "=== Browser: Portal flow ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +echo " === Click 'Client-Portal öffnen' ===" +agent-browser find text "Client-Portal öffnen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " portal login H1: $H1" +agent-browser screenshot --full "$SHOTS/v16-portal-login.png" >/dev/null 2>&1 && echo " login shot OK" + +echo " === Select first company ===" +agent-browser eval "document.querySelector('button[class*=border]')?.click()" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " portal overview H1: $H1" +agent-browser screenshot --full "$SHOTS/v16-portal-overview.png" >/dev/null 2>&1 && echo " overview shot OK" + +echo " === Go to Freigaben tab ===" +agent-browser find text "Freigaben" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v16-portal-approvals.png" >/dev/null 2>&1 && echo " approvals shot OK" + +echo " === Click Freigeben on a pending approval ===" +agent-browser find text "Freigeben" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v16-portal-approval-dialog.png" >/dev/null 2>&1 && echo " dialog shot OK" +# click the confirm Freigeben button in dialog +agent-browser find text "Freigeben" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v16-portal-approval-done.png" >/dev/null 2>&1 && echo " done shot OK" + +echo " === Go to Rechnungen tab ===" +agent-browser find text "Rechnungen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v16-portal-invoices.png" >/dev/null 2>&1 && echo " invoices shot OK" + +echo " === Exit back to agency ===" +agent-browser find text "Agentur-Ansicht" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " back to agency H1: $H1" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -5 +echo "--- dev.log errors ---" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -5 + +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== DONE ===" diff --git a/aos-verify2.sh b/aos-verify2.sh new file mode 100644 index 0000000..3570634 --- /dev/null +++ b/aos-verify2.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Focused re-verification: use snapshot refs, read active H1 after each nav, test AI flow. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do + curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1 +done +echo " ready after ${i}s" +sleep 2 + +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2000 >/dev/null 2>&1 + +# Helper: click sidebar nav by ref and report the H1 +nav_and_check () { + local ref="$1" name="$2" + agent-browser click "$ref" >/dev/null 2>&1 + agent-browser wait 2000 >/dev/null 2>&1 + local h1=$(agent-browser get text h1 2>/dev/null | head -1) + echo " nav[$name] -> H1: $h1" +} + +echo "=== Nav via snapshot refs ===" +# refresh snapshot to get current refs +agent-browser snapshot -i >/tmp/snap1.txt 2>&1 +# Find the sidebar nav button refs by name +for name in Dashboard CRM "Leads Finder" Projekte Lizenzen Finanzen Erweiterungen; do + ref=$(grep -i "button \"$name" /tmp/snap1.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) + echo " $name -> $ref" +done + +echo "" +echo "=== Click each + read H1 ===" +for name in CRM Projekte Lizenzen Finanzen Erweiterungen Dashboard; do + ref=$(grep -i "button \"$name" /tmp/snap1.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) + [ -z "$ref" ] && { echo " $name: no ref"; continue; } + agent-browser click "$ref" >/dev/null 2>&1 + agent-browser wait 2000 >/dev/null 2>&1 + h1=$(agent-browser get text h1 2>/dev/null | head -1) + agent-browser screenshot --full "/home/z/my-project/aos-shots/v2-${name,,// /-}.png" >/dev/null 2>&1 + echo " $name -> H1=[$h1]" +done + +echo "" +echo "=== Leads AI explanation flow ===" +# navigate to leads +ref=$(grep -i 'button "Leads Finder' /tmp/snap1.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$ref" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full /home/z/my-project/aos-shots/v2-leads-initial.png >/dev/null 2>&1 +# snapshot to find lead list buttons + Erzeugen +agent-browser snapshot -i >/tmp/snap2.txt 2>&1 +echo "--- leads view interactive elements (first 25) ---" +head -25 /tmp/snap2.txt +# find a lead card button (the company name) — look for buttons that aren't nav +echo "--- Erzeugen button? ---" +grep -i "Erzeugen\|Neu generieren" /tmp/snap2.txt | head -3 || echo " not found yet (need to select a lead)" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== done ===" diff --git a/aos-verify3.sh b/aos-verify3.sh new file mode 100644 index 0000000..75e38bd --- /dev/null +++ b/aos-verify3.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Test the AI explanation generation (Leads Finder "Explain" stage via z-ai-web-dev-sdk). +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "=== Direct API test: explain endpoint ===" +# get first lead id +LEAD_ID=$(curl -s "$BASE/api/aos/leads" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['leads'][0]['id'])" 2>/dev/null) +echo " lead id: $LEAD_ID" +echo " calling /api/aos/leads/explain (max 60s)…" +RESP=$(curl -s -m 70 -X POST "$BASE/api/aos/leads/explain" -H 'content-type: application/json' -d "{\"leadId\":\"$LEAD_ID\"}") +echo " response (first 500 chars):" +echo "$RESP" | head -c 500 +echo "" +echo "" + +echo "=== Browser flow: select lead, click Neu generieren ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2000 >/dev/null 2>&1 +# go to leads +agent-browser find text "Leads Finder" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click second lead to ensure a fresh selection +agent-browser snapshot -i >/tmp/snap.txt 2>&1 +LEAD2=$(grep -i "Ostsee Catering" /tmp/snap.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " Ostsee Catering lead ref: $LEAD2" +[ -n "$LEAD2" ] && agent-browser click "$LEAD2" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# find Neu generieren / Erzeugen +agent-browser snapshot -i >/tmp/snap2.txt 2>&1 +GEN=$(grep -iE "Erzeugen|Neu generieren" /tmp/snap2.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " generate button ref: $GEN" +if [ -n "$GEN" ]; then + agent-browser click "$GEN" >/dev/null 2>&1 + echo " clicked; waiting up to 30s for toast/response…" + for w in $(seq 1 30); do + agent-browser wait 1000 >/dev/null 2>&1 + # check if a toast appeared + toast=$(agent-browser eval "document.querySelector('[data-sonner-toast]')?.textContent || ''" 2>/dev/null | tail -1) + if echo "$toast" | grep -qi "erklärung\|generiert\|fehlgeschlagen"; then + echo " toast after ${w}s: $toast" + break + fi + done + agent-browser screenshot --full /home/z/my-project/aos-shots/v2-ai-explain.png >/dev/null 2>&1 && echo " screenshot saved" + # read the explanation text from the detail panel + echo " --- explanation panel text ---" + agent-browser eval "document.querySelector('[class*=rounded-md]')?.innerText?.slice(0,400) || 'n/a'" 2>/dev/null | tail -3 +else + echo " no generate button found" +fi + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 +echo "--- console last 6 ---" +agent-browser console 2>&1 | tail -6 + +echo "" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== done ===" diff --git a/aos-verify4.sh b/aos-verify4.sh new file mode 100644 index 0000000..98fc9ef --- /dev/null +++ b/aos-verify4.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Verify the new AOS features: audit view, company drawer, lead convert, invoice dialog, create deal, search, task/time logging. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "" +echo "=== New API endpoints ===" +for ep in "search?q=hamburg" "audit?limit=20"; do + code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/api/aos/$ep") + echo " /api/aos/$ep -> $code" +done + +echo "" +echo "=== Test lead convert API (pick first pending lead) ===" +LEADS=$(curl -s "$BASE/api/aos/leads") +# find a pending lead id via python +LEAD_ID=$(echo "$LEADS" | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((l['id'] for l in d['leads'] if l['status']=='pending'), d['leads'][0]['id']))" 2>/dev/null) +echo " lead id: $LEAD_ID" +# first approve it +curl -s -X PATCH "$BASE/api/aos/leads/$LEAD_ID" -H 'content-type: application/json' -d '{"status":"approved","reviewReason":"Test approval"}' >/dev/null +echo " approved." +# then convert +CONV=$(curl -s -X POST "$BASE/api/aos/leads/convert" -H 'content-type: application/json' -d "{\"leadId\":\"$LEAD_ID\"}") +echo " convert response (first 200 chars): $(echo "$CONV" | head -c 200)" +echo "" + +echo "=== Test create deal API ===" +DEAL=$(curl -s -X POST "$BASE/api/aos/crm/deals" -H 'content-type: application/json' -d '{"companyId":"","title":"Test Deal","valueCents":500000,"stage":"lead"}') +echo " (expected 400 for missing companyId) -> $(echo "$DEAL" | head -c 100)" + +echo "" +echo "=== Browser: open dashboard ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2000 >/dev/null 2>&1 + +echo "=== Navigate to Audit view ===" +agent-browser snapshot -i >/tmp/snap.txt 2>&1 +AUDREF=$(grep -i 'button "Audit' /tmp/snap.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " Audit nav ref: $AUDREF" +[ -n "$AUDREF" ] && agent-browser click "$AUDREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Audit H1: $H1" +agent-browser screenshot --full "$SHOTS/v3-audit.png" >/dev/null 2>&1 && echo " shot audit OK" + +echo "" +echo "=== Navigate to CRM, open company drawer ===" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click on companies tab +agent-browser snapshot -i >/tmp/snap2.txt 2>&1 +TABREF=$(grep -i 'tab "Firmen"' /tmp/snap2.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " Firmen tab ref: $TABREF" +[ -n "$TABREF" ] && agent-browser click "$TABREF" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# click first company row +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v3-company-drawer.png" >/dev/null 2>&1 && echo " shot company-drawer OK" +# check drawer content +DRAWER=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,200) || 'no dialog'" 2>/dev/null | tail -1) +echo " drawer text: $(echo "$DRAWER" | head -c 150)" + +echo "" +echo "=== Navigate to Finance, open invoice dialog ===" +agent-browser find text "Finanzen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v3-invoice-dialog.png" >/dev/null 2>&1 && echo " shot invoice-dialog OK" +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,200) || 'no dialog'" 2>/dev/null | tail -1) +echo " dialog text: $(echo "$DIALOG" | head -c 150)" + +echo "" +echo "=== Navigate to Projects, add a task ===" +agent-browser find text "Projekte" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# the tasks tab is default; find the task input +agent-browser eval "document.querySelector('input[placeholder*=Neue]')?.focus()" >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/snap3.txt 2>&1 +INPUTREF=$(grep -i 'textbox.*Neue' /tmp/snap3.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " task input ref: $INPUTREF" +if [ -n "$INPUTREF" ]; then + agent-browser fill "$INPUTREF" "E2E Test Aufgabe" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + # find the Hinzufügen button + agent-browser find text "Hinzufügen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " task add attempted" +fi +agent-browser screenshot --full "$SHOTS/v3-projects-task.png" >/dev/null 2>&1 && echo " shot projects-task OK" + +echo "" +echo "=== Command palette global search ===" +agent-browser set viewport 1440 900 >/dev/null 2>&1 +# open with ctrl+k +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1000 >/dev/null 2>&1 +# type into the palette +agent-browser snapshot -i >/tmp/snap4.txt 2>&1 +PALREF=$(grep -i 'textbox.*Suche' /tmp/snap4.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " palette input ref: $PALREF" +if [ -n "$PALREF" ]; then + agent-browser fill "$PALREF" "Hamburg" >/dev/null 2>&1 + agent-browser wait 1500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v3-command-search.png" >/dev/null 2>&1 && echo " shot search OK" + # read palette results + agent-browser snapshot -i >/tmp/snap5.txt 2>&1 + echo " --- search results (first 8) ---" + grep -iE "Firmen|Rechnungen|Projekte|Leads" /tmp/snap5.txt | head -8 +fi +agent-browser press Escape >/dev/null 2>&1 + +echo "" +echo "=== Errors check ===" +agent-browser errors 2>&1 | head -10 +echo "--- console last 6 ---" +agent-browser console 2>&1 | tail -6 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log tail ===" +tail -12 dev.log +echo "" +echo "=== VERIFICATION COMPLETE ===" +ls -la "$SHOTS"/v3-* 2>/dev/null | tail -8 diff --git a/aos-verify5.sh b/aos-verify5.sh new file mode 100644 index 0000000..97631ca --- /dev/null +++ b/aos-verify5.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# Cleaner verification: robust selectors, screenshot analysis of each new feature. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server + reseed ===" +pkill -f "next dev" 2>/dev/null; sleep 1 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s" +curl -s -X POST "$BASE/api/aos/seed" >/dev/null 2>&1 && echo " reseeded" +sleep 2 + +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +echo "" +echo "=== TEST 1: Audit view ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +AUDREF=$(grep -i 'button "Audit-Protokoll"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$AUDREF" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " H1=$H1" +agent-browser screenshot --full "$SHOTS/v4-audit.png" >/dev/null 2>&1 && echo " shot OK" + +echo "" +echo "=== TEST 2: CRM → company drawer ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +CRMREF=$(grep -i 'button "CRM"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$CRMREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# click Firmen tab +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TABREF=$(grep -i 'tab "Firmen"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +echo " Firmen tab: $TABREF" +[ -n "$TABREF" ] && agent-browser click "$TABREF" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# click first company row by finding a button/link in tbody +agent-browser snapshot -i >/tmp/s.txt 2>&1 +# find buttons that contain company names — use eval to click first tbody row +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-company-drawer.png" >/dev/null 2>&1 && echo " shot OK" +DRAWER=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1) +echo " drawer state: $DRAWER" +# close drawer +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 3: Create Deal modal ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +# find the "Deal" button in the page header +DEALBTN=$(grep -i 'button "Deal"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +echo " Deal button: $DEALBTN" +[ -n "$DEALBTN" ] && agent-browser click "$DEALBTN" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-create-deal.png" >/dev/null 2>&1 && echo " shot OK" +MODAL=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1) +echo " modal state: $MODAL" +# fill the title input +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TITLEREF=$(grep -iE 'textbox' /tmp/s.txt | grep -iE 'Titel|z.B' | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " title input: $TITLEREF" +if [ -n "$TITLEREF" ]; then + agent-browser fill "$TITLEREF" "E2E Test-Deal" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v4-create-deal-filled.png" >/dev/null 2>&1 && echo " filled shot OK" +fi +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 4: Finance → invoice dialog → finalize ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +FINREF=$(grep -i 'button "Finanzen"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$FINREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-invoice-dialog.png" >/dev/null 2>&1 && echo " shot OK" +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,120) || 'closed'" 2>/dev/null | tail -1) +echo " dialog: $(echo "$DIALOG" | head -c 120)" +agent-browser press Escape >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== TEST 5: Projects → add task + log time ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +PROJREF=$(grep -i 'button "Projekte"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$PROJREF" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# find the task input by placeholder +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TASKINPUT=$(grep -iE 'textbox.*Neue' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " task input: $TASKINPUT" +if [ -n "$TASKINPUT" ]; then + agent-browser fill "$TASKINPUT" "E2E-Aufgabe" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser find text "Hinzufügen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " task added" +fi +agent-browser screenshot --full "$SHOTS/v4-projects-task.png" >/dev/null 2>&1 && echo " shot OK" +# switch to time tab and log time +agent-browser snapshot -i >/tmp/s.txt 2>&1 +TIMETAB=$(grep -i 'tab "Zeit"' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +[ -n "$TIMETAB" ] && agent-browser click "$TIMETAB" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/s.txt 2>&1 +DESCINPUT=$(grep -iE 'textbox.*Beschreibung|textbox.*SSO' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +MININPUT=$(grep -iE 'spinbutton.*Minuten|textbox.*90' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " desc input: $DESCINPUT, min input: $MININPUT" +if [ -n "$DESCINPUT" ] && [ -n "$MININPUT" ]; then + agent-browser fill "$DESCINPUT" "E2E-Zeitbuchung" >/dev/null 2>&1 + agent-browser fill "$MININPUT" "60" >/dev/null 2>&1 + agent-browser wait 500 >/dev/null 2>&1 + agent-browser find text "Buchen" click >/dev/null 2>&1 + agent-browser wait 2500 >/dev/null 2>&1 + echo " time logged" +fi +agent-browser screenshot --full "$SHOTS/v4-projects-time.png" >/dev/null 2>&1 && echo " time shot OK" + +echo "" +echo "=== TEST 6: Command palette global search ===" +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1000 >/dev/null 2>&1 +agent-browser snapshot -i >/tmp/s.txt 2>&1 +PALINPUT=$(grep -iE 'textbox.*Suche|textbox.*Firmen' /tmp/s.txt | head -1 | grep -oE 'ref=e[0-9]+' | head -1) +echo " palette input: $PALINPUT" +if [ -n "$PALINPUT" ]; then + agent-browser fill "$PALINPUT" "Hamburg" >/dev/null 2>&1 + agent-browser wait 1500 >/dev/null 2>&1 + agent-browser screenshot --full "$SHOTS/v4-command-search.png" >/dev/null 2>&1 && echo " shot OK" + agent-browser snapshot -i >/tmp/s.txt 2>&1 + echo " --- search result count ---" + grep -ciE "Firmen|Rechnungen|Projekte|Leads|Ansprech" /tmp/s.txt +fi +agent-browser press Escape >/dev/null 2>&1 + +echo "" +echo "=== TEST 7: Leads → approve → convert ===" +agent-browser snapshot -i >/tmp/s.txt 2>&1 +LEADSREF=$(grep -i 'button "Leads Finder' /tmp/s.txt | grep -oE 'ref=e[0-9]+' | head -1) +agent-browser click "$LEADSREF" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +# click Genehmigen +agent-browser find text "Genehmigen" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +# now a Convert button should appear +agent-browser screenshot --full "$SHOTS/v4-lead-approved.png" >/dev/null 2>&1 && echo " approved shot OK" +agent-browser find text "In CRM konvertieren" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v4-lead-converted.png" >/dev/null 2>&1 && echo " converted shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 +echo "--- console (last 5) ---" +agent-browser console 2>&1 | tail -5 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log (errors only) ===" +grep -iE "error|⨯|500" dev.log | tail -8 +echo "" +echo "=== Screenshots ===" +ls -la "$SHOTS"/v4-* 2>/dev/null | tail -12 +echo "=== DONE ===" diff --git a/aos-verify6.sh b/aos-verify6.sh new file mode 100644 index 0000000..6e115a6 --- /dev/null +++ b/aos-verify6.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Single-process verification: server + browser tests, all in one bash call. +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server + reseed ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s" +sleep 3 +# reseed cleanly +curl -s -X POST "$BASE/api/aos/seed" >/dev/null +echo " reseeded" +sleep 2 + +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3500 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 + +# helper: read active H1 +h1() { agent-browser get text h1 2>/dev/null | head -1; } + +echo "" +echo "=== T1: Audit view ===" +agent-browser find text "Audit-Protokoll" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +echo " H1=$(h1)" +agent-browser screenshot --full "$SHOTS/v6-audit.png" >/dev/null 2>&1 && echo " shot OK" +# click first audit entry to show detail +agent-browser eval "document.querySelectorAll('button[class*=border]')[0]?.click()" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-audit-detail.png" >/dev/null 2>&1 && echo " detail shot OK" + +echo "" +echo "=== T2: CRM company drawer ===" +agent-browser find text "CRM" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser find role tab click --name "Firmen" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " drawer: $STATE" +agent-browser screenshot --full "$SHOTS/v6-company-drawer.png" >/dev/null 2>&1 && echo " shot OK" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 800 >/dev/null 2>&1 + +echo "" +echo "=== T3: Create Deal modal ===" +agent-browser find text "CRM" click >/dev/null 2>&1; agent-browser wait 1500 >/dev/null 2>&1 +# the header "+ Deal" button — use semantic find +agent-browser find role button click --name "Deal" >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.trim()==='Deal')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " modal: $STATE" +agent-browser screenshot --full "$SHOTS/v6-create-deal.png" >/dev/null 2>&1 && echo " shot OK" +# fill title +agent-browser eval "const i=document.querySelector('input[placeholder*=z.B]'); if(i){i.focus(); i.value='E2E Test-Deal'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-create-deal-filled.png" >/dev/null 2>&1 && echo " filled shot OK" +# click Anlegen +agent-browser find text "Anlegen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-create-deal-done.png" >/dev/null 2>&1 && echo " done shot OK" + +echo "" +echo "=== T4: Finance invoice dialog + finalize ===" +agent-browser find text "Finanzen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +STATE=$(agent-browser eval "!!document.querySelector('[role=dialog]') ? 'OPEN' : 'CLOSED'" 2>/dev/null | tail -1 | tr -d '"') +echo " dialog: $STATE" +agent-browser screenshot --full "$SHOTS/v6-invoice-dialog.png" >/dev/null 2>&1 && echo " shot OK" +# if draft, click Finalisieren +agent-browser find text "Finalisieren & versenden" click >/dev/null 2>&1 || echo " (no finalize button — invoice not draft)" +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== T5: Projects add task + log time ===" +agent-browser find text "Projekte" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +# fill task input via eval (more reliable than snapshot ref) +agent-browser eval "const i=document.querySelector('input[placeholder*=Neue]'); if(i){i.focus(); i.value='E2E-Aufgabe'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser find text "Hinzufügen" click >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.includes('Hinzufügen'))?.click()" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +echo " task add attempted" +agent-browser screenshot --full "$SHOTS/v6-projects-task.png" >/dev/null 2>&1 && echo " task shot OK" +# switch to Zeit tab +agent-browser find role tab click --name "Zeit" >/dev/null 2>&1 || agent-browser find text "Zeit" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser eval "const ds=document.querySelectorAll('input[placeholder*=SSO]')[0]||document.querySelector('input[placeholder*=z.B]'); if(ds){ds.focus(); ds.value='E2E-Zeitbuchung'; ds.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser eval "const m=document.querySelector('input[placeholder*=90]'); if(m){m.focus(); m.value='60'; m.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 500 >/dev/null 2>&1 +agent-browser find text "Buchen" click >/dev/null 2>&1 || agent-browser eval "Array.from(document.querySelectorAll('button')).find(b=>b.textContent?.includes('Buchen'))?.click()" >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-projects-time.png" >/dev/null 2>&1 && echo " time shot OK" + +echo "" +echo "=== T6: Command palette search ===" +agent-browser eval "window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true}))" >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser eval "const i=document.querySelector('input[placeholder*=Suche]'); if(i){i.focus(); i.value='Hamburg'; i.dispatchEvent(new Event('input',{bubbles:true}))}" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-command-search.png" >/dev/null 2>&1 && echo " search shot OK" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== T7: Leads approve + convert ===" +agent-browser find text "Leads Finder" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser find text "Genehmigen" click >/dev/null 2>&1 +agent-browser wait 2500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-lead-approved.png" >/dev/null 2>&1 && echo " approved shot OK" +agent-browser find text "In CRM konvertieren" click >/dev/null 2>&1 +agent-browser wait 3500 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v6-lead-converted.png" >/dev/null 2>&1 && echo " converted shot OK" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -10 +echo "--- console (last 5) ---" +agent-browser console 2>&1 | tail -5 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log (errors) ===" +grep -iE "error|⨯|500" dev.log | grep -v "SIGTERM" | tail -10 +echo "" +echo "=== Screenshots ===" +ls -la "$SHOTS"/v6-* 2>/dev/null | tail -15 +echo "=== DONE ===" diff --git a/aos-verify7.sh b/aos-verify7.sh new file mode 100644 index 0000000..377868a --- /dev/null +++ b/aos-verify7.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Verify Contracts + Expenses modules (adapted from PerfEx CRM) +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "=== API tests ===" +echo -n " /api/aos/contracts -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/contracts" +echo -n " /api/aos/expenses -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/expenses" +echo " contracts count:"; curl -s "$BASE/api/aos/contracts" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' {len(d[\"contracts\"])} contracts, stats: {d[\"stats\"]}')" 2>/dev/null +echo " expenses count:"; curl -s "$BASE/api/aos/expenses" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' {len(d[\"expenses\"])} expenses, billable: {d[\"stats\"][\"billable\"]}, report cats: {len(d[\"report\"][\"byCategory\"])}')" 2>/dev/null + +echo "" +echo "=== Browser: Contracts view ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 +agent-browser find text "Verträge" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Contracts H1: $H1" +agent-browser screenshot --full "$SHOTS/v9-contracts.png" >/dev/null 2>&1 && echo " shot OK" + +echo "=== Click first contract to open dialog ===" +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,150) || 'closed'" 2>/dev/null | tail -1 | tr -d '"') +echo " dialog: $(echo "$DIALOG" | head -c 120)" +agent-browser screenshot --full "$SHOTS/v9-contract-detail.png" >/dev/null 2>&1 && echo " detail shot OK" +# check if sign button present +SIGNBTN=$(agent-browser find text "E-Signatur anfordern" 2>&1 | head -1) +echo " sign button: $SIGNBTN" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== Browser: Expenses view ===" +agent-browser find text "Ausgaben" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Expenses H1: $H1" +agent-browser screenshot --full "$SHOTS/v9-expenses.png" >/dev/null 2>&1 && echo " shot OK" + +echo "=== Switch to report tab ===" +agent-browser find role tab click --name "Ausgabenreport" >/dev/null 2>&1 || agent-browser find text "Ausgabenreport" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v9-expenses-report.png" >/dev/null 2>&1 && echo " report shot OK" + +echo "" +echo "=== Test convert-to-invoice (API) ===" +# find a billable, not-invoiced expense +EXP_ID=$(curl -s "$BASE/api/aos/expenses" | python3 -c " +import sys,json +d=json.load(sys.stdin) +for e in d['expenses']: + if e['billable'] and e['status']!='invoiced' and e['companyName']: + print(e['id']); break +" 2>/dev/null) +echo " expense id to convert: $EXP_ID" +if [ -n "$EXP_ID" ]; then + RES=$(curl -s -X POST "$BASE/api/aos/expenses/$EXP_ID/convert") + echo " convert result: $(echo "$RES" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'invoice {d[\"invoice\"][\"number\"]} = {d[\"invoice\"][\"grossCents\"]/100:.2f}€')" 2>/dev/null)" +fi + +echo "" +echo "=== Test contract sign (API) ===" +CON_ID=$(curl -s "$BASE/api/aos/contracts" | python3 -c " +import sys,json +d=json.load(sys.stdin) +for c in d['contracts']: + if c['status'] in ('draft','sent'): + print(c['id']); break +" 2>/dev/null) +echo " contract id to sign: $CON_ID" +if [ -n "$CON_ID" ]; then + RES=$(curl -s -X PATCH "$BASE/api/aos/contracts/$CON_ID/sign" -H 'content-type: application/json' -d '{"signedBy":"Test-Kunde"}') + echo " sign result: $(echo "$RES" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'status={d[\"contract\"][\"status\"]}, token={d[\"contract\"][\"signToken\"]}')" 2>/dev/null)" +fi + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -8 +echo "--- console last 5 ---" +agent-browser console 2>&1 | tail -5 + +echo "" +echo "=== Cleanup ===" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log (errors) ===" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -8 +echo "" +echo "=== DONE ===" +ls -la "$SHOTS"/v9-* 2>/dev/null | tail -6 diff --git a/aos-verify8.sh b/aos-verify8.sh new file mode 100644 index 0000000..570e3f5 --- /dev/null +++ b/aos-verify8.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Verify Support Tickets (AI) + Estimates modules +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "=== API tests ===" +echo -n " /api/aos/tickets -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/tickets" +echo -n " /api/aos/estimates -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/estimates" +curl -s "$BASE/api/aos/tickets" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' tickets: {len(d[\"tickets\"])}, stats: {d[\"stats\"]}')" 2>/dev/null +curl -s "$BASE/api/aos/estimates" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' estimates: {len(d[\"estimates\"])}, winRate: {d[\"stats\"][\"winRate\"]}%')" 2>/dev/null + +echo "" +echo "=== AI ticket summarize API ===" +TKT_ID=$(curl -s "$BASE/api/aos/tickets" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['tickets'][0]['id'])" 2>/dev/null) +echo " ticket id: $TKT_ID" +SUM=$(curl -s -m 70 -X POST "$BASE/api/aos/tickets/$TKT_ID/summarize") +echo " summary (first 200 chars): $(echo "$SUM" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary','')[:200])" 2>/dev/null)" + +echo "" +echo "=== AI ticket reply suggestion API ===" +REP=$(curl -s -m 70 -X POST "$BASE/api/aos/tickets/$TKT_ID/reply" -H 'content-type: application/json' -d '{"tone":"polite"}') +echo " reply (first 250 chars): $(echo "$REP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('suggestion','')[:250])" 2>/dev/null)" + +echo "" +echo "=== Browser: Support view ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 +agent-browser find text "Support" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Support H1: $H1" +agent-browser screenshot --full "$SHOTS/v10-support.png" >/dev/null 2>&1 && echo " shot OK" + +echo "=== Click AI summarize button ===" +agent-browser find text "Zusammenfassen" click >/dev/null 2>&1 +agent-browser wait 10000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v10-support-summarized.png" >/dev/null 2>&1 && echo " summarized shot OK" +# check if AI summary appeared +SUM_UI=$(agent-browser eval "document.querySelector('[class*=bg-muted\\/20]')?.innerText?.slice(0,150) || 'no summary'" 2>/dev/null | tail -1 | tr -d '"') +echo " AI summary in UI: $(echo "$SUM_UI" | head -c 120)" + +echo "" +echo "=== Browser: Estimates view ===" +agent-browser find text "Angebote" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Estimates H1: $H1" +agent-browser screenshot --full "$SHOTS/v10-estimates.png" >/dev/null 2>&1 && echo " shot OK" + +echo "=== Click first estimate → dialog ===" +agent-browser eval "document.querySelector('tbody tr')?.click()" >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +DIALOG=$(agent-browser eval "document.querySelector('[role=dialog]')?.textContent?.slice(0,120) || 'closed'" 2>/dev/null | tail -1 | tr -d '"') +echo " dialog: $(echo "$DIALOG" | head -c 120)" +agent-browser screenshot --full "$SHOTS/v10-estimate-detail.png" >/dev/null 2>&1 && echo " detail shot OK" +agent-browser press Escape >/dev/null 2>&1; agent-browser wait 500 >/dev/null 2>&1 + +echo "" +echo "=== Test accept + convert APIs ===" +# find a sent estimate +EST_ID=$(curl -s "$BASE/api/aos/estimates" | python3 -c " +import sys,json +d=json.load(sys.stdin) +for e in d['estimates']: + if e['status']=='sent': + print(e['id']); break +" 2>/dev/null) +echo " estimate to accept: $EST_ID" +if [ -n "$EST_ID" ]; then + ACC=$(curl -s -X PATCH "$BASE/api/aos/estimates/$EST_ID/accept" -H 'content-type: application/json' -d '{"acceptedBy":"Test"}') + echo " accept: $(echo "$ACC" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'status={d[\"estimate\"][\"status\"]}, projectId={d.get(\"projectId\",\"none\")}')" 2>/dev/null)" + echo " converting to invoice…" + CONV=$(curl -s -X POST "$BASE/api/aos/estimates/$EST_ID/convert") + echo " convert: $(echo "$CONV" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'invoice={d[\"invoice\"][\"number\"]} = {d[\"invoice\"][\"grossCents\"]/100:.2f}€')" 2>/dev/null)" +fi + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -8 +echo "--- console last 5 ---" +agent-browser console 2>&1 | tail -5 + +echo "" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log errors ===" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -8 +echo "=== DONE ===" +ls -la "$SHOTS"/v10-* 2>/dev/null | tail -6 diff --git a/aos-verify9.sh b/aos-verify9.sh new file mode 100644 index 0000000..f671f45 --- /dev/null +++ b/aos-verify9.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Verify Reporting + Setup Wizard +set -u +cd /home/z/my-project +BASE=http://127.0.0.1:3000 +SHOTS=/home/z/my-project/aos-shots +mkdir -p "$SHOTS" + +echo "=== Start server ===" +pkill -f "next dev" 2>/dev/null; pkill -f "tee dev.log" 2>/dev/null; sleep 2 +bun run dev > dev.log 2>&1 & +SRV=$! +for i in $(seq 1 90); do curl -s -o /dev/null "$BASE/" 2>/dev/null && break; sleep 1; done +echo " ready after ${i}s"; sleep 2 + +echo "=== API tests ===" +echo -n " /api/aos/reporting -> "; curl -s -o /dev/null -w "HTTP %{http_code}\n" "$BASE/api/aos/reporting" +curl -s "$BASE/api/aos/reporting" | python3 -c " +import sys,json +d=json.load(sys.stdin) +p=d['pipeline'] +print(f' pipeline: {p[\"conversionRate\"]}% conversion, €{p[\"pipelineValue\"]/100:.0f} open, €{p[\"wonValue\"]/100:.0f} won') +r=d['recurring'] +print(f' MRR: €{r[\"mrr\"]/100:.0f}, ARR: €{r[\"arr\"]/100:.0f}, renewals: {len(r[\"renewalsDue\"])}') +print(f' utilization: {len(d[\"utilization\"][\"byProject\"])} projects, {len(d[\"utilization\"][\"weekly\"])} weeks') +print(f' activity: {len(d[\"activity\"])} groups, estimateWinRate: {d[\"estimateWinRate\"]}%') +" 2>/dev/null + +echo "" +echo "=== Browser: Reporting view ===" +agent-browser open "$BASE/" >/dev/null 2>&1 +agent-browser wait --load networkidle >/dev/null 2>&1 || true +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser set viewport 1440 900 >/dev/null 2>&1 +agent-browser find text "Reporting" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Reporting H1: $H1" +agent-browser screenshot --full "$SHOTS/v11-reporting.png" >/dev/null 2>&1 && echo " shot OK" + +echo "=== Switch to MRR tab ===" +agent-browser find role tab click --name "MRR & Erneuerungen" >/dev/null 2>&1 || agent-browser find text "MRR" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v11-reporting-mrr.png" >/dev/null 2>&1 && echo " mrr shot OK" + +echo "=== Switch to Auslastung tab ===" +agent-browser find role tab click --name "Auslastung" >/dev/null 2>&1 || agent-browser find text "Auslastung" click >/dev/null 2>&1 +agent-browser wait 2000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v11-reporting-utilization.png" >/dev/null 2>&1 && echo " utilization shot OK" + +echo "" +echo "=== Browser: Setup Wizard ===" +agent-browser find text "Setup-Wizard" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +H1=$(agent-browser get text h1 2>/dev/null | head -1) +echo " Wizard H1: $H1" +agent-browser screenshot --full "$SHOTS/v11-wizard-step1.png" >/dev/null 2>&1 && echo " step1 shot OK" + +echo "=== Click Weiter to step 3 (Modules) ===" +agent-browser find text "Weiter & Speichern" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser find text "Weiter & Speichern" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +# now at step 3 +agent-browser screenshot --full "$SHOTS/v11-wizard-step3-modules.png" >/dev/null 2>&1 && echo " step3 shot OK" + +echo "=== Go to step 4 (Mail) and test ===" +agent-browser find text "E-Mail" click >/dev/null 2>&1 +agent-browser wait 1500 >/dev/null 2>&1 +agent-browser find text "Test ausführen" click >/dev/null 2>&1 +agent-browser wait 3000 >/dev/null 2>&1 +agent-browser screenshot --full "$SHOTS/v11-wizard-step4-mail.png" >/dev/null 2>&1 && echo " step4 mail shot OK" +MAIL_RES=$(agent-browser eval "document.body.innerText.includes('Verbindung erfolgreich') || document.body.innerText.includes('SMTP')" 2>/dev/null | tail -1) +echo " mail test result visible: $MAIL_RES" + +echo "" +echo "=== Errors ===" +agent-browser errors 2>&1 | head -8 +echo "--- console last 5 ---" +agent-browser console 2>&1 | tail -5 + +echo "" +agent-browser close >/dev/null 2>&1 +kill $SRV 2>/dev/null +echo "=== dev.log errors ===" +grep -iE "error|⨯|500" dev.log | grep -v SIGTERM | tail -8 +echo "=== DONE ===" +ls -la "$SHOTS"/v11-* 2>/dev/null | tail -8 diff --git a/examples/websocket/frontend.tsx b/examples/websocket/frontend.tsx new file mode 100644 index 0000000..dcc2aff --- /dev/null +++ b/examples/websocket/frontend.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { io } from 'socket.io-client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +type User = { + id: string; + username: string; +} + +type Message = { + id: string; + username: string; + content: string; + timestamp: Date | string; + type: 'user' | 'system'; +} + +export default function SocketDemo() { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [username, setUsername] = useState(''); + const [isUsernameSet, setIsUsernameSet] = useState(false); + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const [users, setUsers] = useState([]); + + useEffect(() => { + // Connect to websocket server + // Never use PORT in the URL, alyways use XTransformPort + // DO NOT change the path, it is used by Caddy to forward the request to the correct port + const socketInstance = io('/?XTransformPort=3003', { + transports: ['websocket', 'polling'], + forceNew: true, + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + timeout: 10000 + }) + + setSocket(socketInstance); + + socketInstance.on('connect', () => { + setIsConnected(true); + }); + + socketInstance.on('disconnect', () => { + setIsConnected(false); + }); + + socketInstance.on('message', (msg: Message) => { + setMessages(prev => [...prev, msg]); + }); + + socketInstance.on('user-joined', (data: { user: User; message: Message }) => { + setMessages(prev => [...prev, data.message]); + setUsers(prev => { + if (!prev.find(u => u.id === data.user.id)) { + return [...prev, data.user]; + } + return prev; + }); + }); + + socketInstance.on('user-left', (data: { user: User; message: Message }) => { + setMessages(prev => [...prev, data.message]); + setUsers(prev => prev.filter(u => u.id !== data.user.id)); + }); + + socketInstance.on('users-list', (data: { users: User[] }) => { + setUsers(data.users); + }); + + return () => { + socketInstance.disconnect(); + }; + }, []); + + const handleJoin = () => { + if (socket && username.trim() && isConnected) { + socket.emit('join', { username: username.trim() }); + setIsUsernameSet(true); + } + }; + + const sendMessage = () => { + if (socket && inputMessage.trim() && username.trim()) { + socket.emit('message', { + content: inputMessage.trim(), + username: username.trim() + }); + setInputMessage(''); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + sendMessage(); + } + }; + + return ( +
+ + + + WebSocket Demo + + {isConnected ? 'Connected' : 'Disconnected'} + + + + + {!isUsernameSet ? ( +
+ setUsername(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleJoin(); + } + }} + placeholder="Enter your username..." + disabled={!isConnected} + className="flex-1" + /> + +
+ ) : ( + <> + +
+ {messages.length === 0 ? ( +

No messages yet

+ ) : ( + messages.map((msg) => ( +
+
+
+

+ {msg.username} +

+

+ {msg.content} +

+
+ + {new Date(msg.timestamp).toLocaleTimeString()} + +
+
+ )) + )} +
+
+ +
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Type a message..." + disabled={!isConnected} + className="flex-1" + /> + +
+ + )} +
+
+
+ ); +} diff --git a/examples/websocket/server.ts b/examples/websocket/server.ts new file mode 100644 index 0000000..4db70df --- /dev/null +++ b/examples/websocket/server.ts @@ -0,0 +1,138 @@ +import { createServer } from 'http' +import { Server } from 'socket.io' + +const httpServer = createServer() +const io = new Server(httpServer, { + // DO NOT change the path, it is used by Caddy to forward the request to the correct port + path: '/', + cors: { + origin: "*", + methods: ["GET", "POST"] + }, + pingTimeout: 60000, + pingInterval: 25000, +}) + +interface User { + id: string + username: string +} + +interface Message { + id: string + username: string + content: string + timestamp: Date + type: 'user' | 'system' +} + +const users = new Map() + +const generateMessageId = () => Math.random().toString(36).substr(2, 9) + +const createSystemMessage = (content: string): Message => ({ + id: generateMessageId(), + username: 'System', + content, + timestamp: new Date(), + type: 'system' +}) + +const createUserMessage = (username: string, content: string): Message => ({ + id: generateMessageId(), + username, + content, + timestamp: new Date(), + type: 'user' +}) + +io.on('connection', (socket) => { + console.log(`User connected: ${socket.id}`) + + // Add test event handler + socket.on('test', (data) => { + console.log('Received test message:', data) + socket.emit('test-response', { + message: 'Server received test message', + data: data, + timestamp: new Date().toISOString() + }) + }) + + socket.on('join', (data: { username: string }) => { + const { username } = data + + // Create user object + const user: User = { + id: socket.id, + username + } + + // Add to user list + users.set(socket.id, user) + + // Send join message to all users + const joinMessage = createSystemMessage(`${username} joined the chat room`) + io.emit('user-joined', { user, message: joinMessage }) + + // Send current user list to new user + const usersList = Array.from(users.values()) + socket.emit('users-list', { users: usersList }) + + console.log(`${username} joined the chat room, current online users: ${users.size}`) + }) + + socket.on('message', (data: { content: string; username: string }) => { + const { content, username } = data + const user = users.get(socket.id) + + if (user && user.username === username) { + const message = createUserMessage(username, content) + io.emit('message', message) + console.log(`${username}: ${content}`) + } + }) + + socket.on('disconnect', () => { + const user = users.get(socket.id) + + if (user) { + // Remove from user list + users.delete(socket.id) + + // Send leave message to all users + const leaveMessage = createSystemMessage(`${user.username} left the chat room`) + io.emit('user-left', { user: { id: socket.id, username: user.username }, message: leaveMessage }) + + console.log(`${user.username} left the chat room, current online users: ${users.size}`) + } else { + console.log(`User disconnected: ${socket.id}`) + } + }) + + socket.on('error', (error) => { + console.error(`Socket error (${socket.id}):`, error) + }) +}) + +const PORT = 3003 +httpServer.listen(PORT, () => { + console.log(`WebSocket server running on port ${PORT}`) +}) + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('Received SIGTERM signal, shutting down server...') + httpServer.close(() => { + console.log('WebSocket server closed') + process.exit(0) + }) +}) + +process.on('SIGINT', () => { + console.log('Received SIGINT signal, shutting down server...') + httpServer.close(() => { + console.log('WebSocket server closed') + process.exit(0) + }) +}) \ No newline at end of file diff --git a/mini-services/.gitkeep b/mini-services/.gitkeep new file mode 100644 index 0000000..e69de29