From 5430dc03f49e3e40dbdca943dd4c66e4da754419 Mon Sep 17 00:00:00 2001 From: Cccc_ Date: Fri, 2 Jan 2026 01:57:25 +0800 Subject: [PATCH] refactor: see details below - Fix emailing. - Updated manage.sh to enhance command handling and service management for backend and frontend. - Introduced utility functions for better code organization and readability. - Added support for checking Node.js version and ensuring the virtual environment is set up. - Implemented improved logging with color-coded output for better visibility. - Created a new nginx.conf.example file for easy Nginx configuration setup for the application. --- .env.example | 2 +- backend/services/task_service.py | 7 +- backend/workers/check_in_worker.py | 24 +- backend/workers/email_notifier.py | 390 +++++- ...ice.example => checkin-app.service.example | 58 +- deployment/DEPLOYMENT.md | 474 ------- deployment/README.md | 307 ----- deployment/deploy.sh | 358 ----- deployment/nginx.conf.example | 216 --- frontend/src/views/TaskRecordsView.vue | 19 +- frontend/src/views/TasksView.vue | 29 +- frontend/vite.config.js | 1 + manage.bat | 81 +- manage.sh | 1171 +++++++++-------- nginx.conf.example | 143 ++ 15 files changed, 1257 insertions(+), 2023 deletions(-) rename deployment/checkin-app.service.example => checkin-app.service.example (51%) delete mode 100644 deployment/DEPLOYMENT.md delete mode 100644 deployment/README.md delete mode 100644 deployment/deploy.sh delete mode 100644 deployment/nginx.conf.example create mode 100644 nginx.conf.example diff --git a/.env.example b/.env.example index 4c2299d..4754c48 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ # DATABASE_URL=postgresql://user:password@localhost/checkin # CORS 允许的前端域名(逗号分隔,生产环境必须修改) -CORS_ORIGINS=http://localhost:5173,http://localhost:3000 +CORS_ORIGINS=http://localhost:3000 # 日志级别(可选:DEBUG, INFO, WARNING, ERROR) LOG_LEVEL=INFO diff --git a/backend/services/task_service.py b/backend/services/task_service.py index 1aca8d6..dcfe0e1 100644 --- a/backend/services/task_service.py +++ b/backend/services/task_service.py @@ -336,9 +336,10 @@ class TaskService: job_id = f"task_{task.id}" # 先移除旧的任务(如果存在) - if scheduler.get_job(job_id): + existing_job = scheduler.get_job(job_id) + if existing_job: scheduler.remove_job(job_id) - logger.debug(f"从调度器移除旧任务: {job_id}") + logger.info(f"从调度器移除旧任务: {job_id}") # 如果任务启用且有有效的 cron 表达式,添加新任务 if task.is_scheduled_enabled: @@ -354,7 +355,7 @@ class TaskService: args=[task.id], replace_existing=True ) - logger.info(f"✅ 任务 {task.id} 已添加到调度器: {cron_str}") + logger.info(f"✅ 任务 {task.id} 已重新加载到调度器: {cron_str}") else: logger.warning(f"任务 {task.id} 的 cron 表达式无效: {cron_str}") else: diff --git a/backend/workers/check_in_worker.py b/backend/workers/check_in_worker.py index 48c57b5..eb35630 100644 --- a/backend/workers/check_in_worker.py +++ b/backend/workers/check_in_worker.py @@ -211,12 +211,22 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]: "error_message": "" } - # 情况2: 不在打卡时间范围 → 标记为时间范围外 - # 支持多种匹配方式:直接文本匹配、JSON Data 字段、Description 字段 + # 情况2: 已经提交过了(重复提交)→ 视为成功,但不发送邮件 + # 匹配 "已被提交" 或 "已经打卡" + elif ("已被提交" in response_text or "已经打卡" in response_text or + "重复提交" in response_text): + logger.info(f"✅ 检测到'已被提交',本次打卡已完成(重复提交,不发送邮件)") + return { + "success": True, + "status": "success", + "response_text": response_text, + "error_message": "" + } + + # 情况3: 不在打卡时间范围 → 标记为时间范围外 + # 匹配 Data 或 Description 中的内容 elif ("不在打卡时间范围" in response_text or - "不在打卡时间" in response_text or - '"Data":"不在打卡时间范围"' in response_text or - '"Description":"不在打卡时间范围"' in response_text): + "不在打卡时间" in response_text): logger.warning(f"⏰ 检测到'不在打卡时间范围',打卡时间不符") return { "success": False, @@ -225,7 +235,7 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]: "error_message": "不在打卡时间范围内" } - # 情况3: Token 失效的特征标识 → 失败 + # 情况4: Token 失效的特征标识 → 失败 elif ("登录" in response_text): logger.warning(f"⚠️ 检测到登录失败关键字,Token 可能已失效") if email: @@ -237,7 +247,7 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]: "error_message": "Token 已失效,需要重新授权" } - # 情况4: 其他响应 → 需要人工确认(标记为异常) + # 情况5: 其他响应 → 需要人工确认(标记为异常) else: logger.warning(f"⚠️ 未识别的响应内容,请检查: {response_text[:200]}...") # 标记为未知状态,记录完整响应供后续分析 diff --git a/backend/workers/email_notifier.py b/backend/workers/email_notifier.py index 5bb5c0c..680b332 100644 --- a/backend/workers/email_notifier.py +++ b/backend/workers/email_notifier.py @@ -3,8 +3,6 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import time import logging -import configparser -from pathlib import Path from backend.config import settings @@ -14,79 +12,344 @@ logger = logging.getLogger(__name__) EXPIRATION_HTML_TEMPLATE = """ - + - Token 到期通知 + + Token 到期提醒 -

注意!

-
-

{name},请注意!

-

您的 token 已经到期,请尽快重新刷新您的 token,否则您的自动打卡功能将会失效。

-

到期时间: {exp_time}

+
+
+

⚠️ Token 即将到期提醒

+
+
+

您好,

+
+

您的接龙打卡系统 Token 即将到期,为避免影响自动打卡功能,请尽快刷新您的 Token。

+
+
+ 到期时间:{exp_time} +
+
+ 通知时间:{send_time} +
+

+ 请登录系统,前往 用户设置 页面刷新您的 Token,以确保自动打卡功能正常运行。 +

+
+
- """ SUCCESS_HTML_TEMPLATE = """ - + + 打卡成功通知 -

打卡成功!

-
-

{name},您好!

-

系统已于 {send_time} 成功为您完成自动打卡。

-

您无需进行任何操作,此邮件仅作通知。

+
+
+

✅ 打卡成功

+
+
+
🎉
+

您好,

+
+

自动打卡已成功完成!

+
+
+ 打卡时间:{send_time} +
+
+ 打卡状态:成功 ✓ +
+

+ 您无需进行任何操作,系统已自动为您完成打卡。此邮件仅作通知。 +

+
+
- """ FAILURE_HTML_TEMPLATE = """ - + + 打卡失败通知 -

通知:自动打卡失败!

-
-

{name},您好!

-

系统于 {send_time} 尝试为您自动打卡时失败。

-

失败原因: 服务器返回 "需要登录",这通常意味着您的 Token 已失效

-

请您立即刷新您的 Token,以确保后续打卡能够成功。

+
+
+

❌ 打卡失败通知

+
+
+
⚠️
+

您好,

+
+

自动打卡失败,需要您的关注!

+
+
+ 失败时间:{send_time} +
+
+ 失败原因:Token 已失效(需要登录) +
+
+

📋 需要您执行以下操作:

+
    +
  • 登录接龙自动打卡系统
  • +
  • 刷新您的 Authorization Token
  • +
  • 确认 Token 更新成功
  • +
+
+

+ Token 失效是正常现象,通常在一段时间后会自动过期。刷新 Token 后,系统将恢复自动打卡功能。 +

+
+
- """ @@ -94,29 +357,31 @@ FAILURE_HTML_TEMPLATE = """ def get_email_settings(): """ - 从 config.ini 读取邮件配置 + 从环境变量读取邮件配置 + + 如果 SMTP_SERVER、SMTP_PORT 或 SMTP_SENDER_EMAIL 有任一为空,则禁用邮件功能 Returns: - dict: 邮件配置,如果配置文件不存在则返回 None + dict: 邮件配置,如果配置不完整则返回 None """ - if not settings.EMAIL_CONFIG_FILE.exists(): - logger.warning("找不到 config.ini,无法发送邮件") + # 检查必要的邮件配置是否存在 + if not settings.SMTP_SERVER or not settings.SMTP_SENDER_EMAIL: + logger.debug("邮件配置未完成(SMTP_SERVER 或 SMTP_SENDER_EMAIL 为空),邮件发送功能已禁用") return None - try: - config_parser = configparser.ConfigParser() - config_parser.read(settings.EMAIL_CONFIG_FILE, encoding='utf-8') - - if 'Email' not in config_parser: - logger.warning("config.ini 中缺少 [Email] 配置段") - return None - - return config_parser['Email'] - - except Exception as e: - logger.error(f"读取邮件配置失败: {e}") + if not settings.SMTP_PORT: + logger.debug("邮件配置未完成(SMTP_PORT 为空),邮件发送功能已禁用") return None + # 返回配置字典 + return { + 'smtpserver': settings.SMTP_SERVER, + 'smtpport': settings.SMTP_PORT, + 'senderemail': settings.SMTP_SENDER_EMAIL, + 'senderpassword': settings.SMTP_SENDER_PASSWORD, + 'use_ssl': settings.SMTP_USE_SSL + } + def _send_email(to_email: str, subject: str, html_content: str, email_settings: dict) -> bool: """ @@ -138,9 +403,16 @@ def _send_email(to_email: str, subject: str, html_content: str, email_settings: msg["Subject"] = subject msg.attach(MIMEText(html_content, 'html', 'utf-8')) - with smtplib.SMTP_SSL(email_settings['smtpserver'], int(email_settings['smtpport'])) as server: - server.login(email_settings['senderemail'], email_settings['senderpassword']) - server.sendmail(msg["From"], msg["To"], msg.as_string()) + # 根据配置选择使用 SSL 或普通 SMTP + if email_settings.get('use_ssl', True): + with smtplib.SMTP_SSL(email_settings['smtpserver'], int(email_settings['smtpport'])) as server: + server.login(email_settings['senderemail'], email_settings['senderpassword']) + server.sendmail(msg["From"], msg["To"], msg.as_string()) + else: + with smtplib.SMTP(email_settings['smtpserver'], int(email_settings['smtpport'])) as server: + server.starttls() + server.login(email_settings['senderemail'], email_settings['senderpassword']) + server.sendmail(msg["From"], msg["To"], msg.as_string()) logger.info(f"已成功向 {to_email} 发送邮件,主题: {subject}") return True diff --git a/deployment/checkin-app.service.example b/checkin-app.service.example similarity index 51% rename from deployment/checkin-app.service.example rename to checkin-app.service.example index 2631a77..1191257 100644 --- a/deployment/checkin-app.service.example +++ b/checkin-app.service.example @@ -22,53 +22,32 @@ # ============================================================================== [Unit] -# Service description Description=CheckIn App V2 - Backend API Service -Documentation=https://github.com/your-repo/checkin-app - -# Start after network and database are available +Documentation=https://github.com/Cccc-owo/CheckInApp After=network.target Wants=network-online.target [Service] -# Service type -Type=simple +Type=forking -# User and Group -# IMPORTANT: Replace 'www-data' with your actual user -# Create a dedicated user: sudo useradd -r -s /bin/false checkin -User=www-data -Group=www-data +# CHANGE THIS: Replace with your actual installation path +# Example: /home/username/CheckInApp +WorkingDirectory=/path/to/CheckInApp -# Working directory -# IMPORTANT: Replace with your actual installation path -WorkingDirectory=/opt/checkin-app +# PID file written by manage.sh +PIDFile=/path/to/CheckInApp/backend.pid -# Environment variables -Environment="PATH=/opt/checkin-app/venv/bin:/usr/local/bin:/usr/bin:/bin" -Environment="PYTHONPATH=/opt/checkin-app" +# Start backend using manage.sh script +ExecStart=/path/to/CheckInApp/manage.sh start backend -# Load environment variables from .env file (optional) -EnvironmentFile=-/opt/checkin-app/.env - -# Command to start the service -# Using uvicorn directly for production -ExecStart=/opt/checkin-app/venv/bin/python /opt/checkin-app/run_daemon.py - -# Alternative: Using uvicorn directly with more control -# ExecStart=/opt/checkin-app/venv/bin/uvicorn backend.main:app \ -# --host 0.0.0.0 \ -# --port 8000 \ -# --workers 4 \ -# --log-level info \ -# --access-log \ -# --proxy-headers +# Stop backend using manage.sh script +ExecStop=/path/to/CheckInApp/manage.sh stop backend # Restart policy Restart=always RestartSec=10 -# Kill signal +# Kill settings KillSignal=SIGTERM KillMode=mixed @@ -76,18 +55,10 @@ KillMode=mixed TimeoutStartSec=60 TimeoutStopSec=30 -# Resource limits (optional) +# Resource limits LimitNOFILE=65535 -# LimitNPROC=4096 -# MemoryLimit=2G -# CPUQuota=200% -# Security settings (optional but recommended) -# Restrict access to the filesystem -# ReadWritePaths=/opt/checkin-app/data /opt/checkin-app/logs /opt/checkin-app/sessions -# ReadOnlyPaths=/opt/checkin-app - -# Prevent privilege escalation +# Security settings NoNewPrivileges=true # Logging @@ -96,5 +67,4 @@ StandardError=journal SyslogIdentifier=checkin-app [Install] -# Start on boot WantedBy=multi-user.target diff --git a/deployment/DEPLOYMENT.md b/deployment/DEPLOYMENT.md deleted file mode 100644 index 26c5561..0000000 --- a/deployment/DEPLOYMENT.md +++ /dev/null @@ -1,474 +0,0 @@ -# CheckIn App V2 - Deployment Guide - -This guide explains how to deploy CheckIn App V2 to a production server using Nginx and systemd. - -## Table of Contents - -1. [Prerequisites](#prerequisites) -2. [Server Setup](#server-setup) -3. [Application Setup](#application-setup) -4. [Nginx Configuration](#nginx-configuration) -5. [Systemd Service Setup](#systemd-service-setup) -6. [SSL/TLS Certificate](#ssltls-certificate) -7. [Monitoring and Logs](#monitoring-and-logs) -8. [Troubleshooting](#troubleshooting) - ---- - -## Prerequisites - -- **Operating System**: Ubuntu 20.04+ or similar Linux distribution -- **Python**: 3.9 or higher -- **Node.js**: 16+ (for building frontend) -- **Nginx**: 1.18 or higher -- **Domain name** (optional but recommended for SSL) - -### Install Required Packages - -```bash -# Update system -sudo apt update && sudo apt upgrade -y - -# Install Python and tools -sudo apt install -y python3 python3-pip python3-venv - -# Install Node.js (using NodeSource) -curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - -sudo apt install -y nodejs - -# Install Nginx -sudo apt install -y nginx - -# Install other dependencies -sudo apt install -y git curl wget -``` - ---- - -## Server Setup - -### 1. Create Application User - -```bash -# Create a dedicated user for the application -sudo useradd -r -m -s /bin/bash checkin -sudo usermod -aG www-data checkin -``` - -### 2. Create Application Directory - -```bash -# Create directory structure -sudo mkdir -p /opt/checkin-app -sudo chown -R checkin:www-data /opt/checkin-app - -# Create required subdirectories -sudo -u checkin mkdir -p /opt/checkin-app/{data,logs,sessions} -``` - ---- - -## Application Setup - -### 1. Clone Repository - -```bash -# Switch to application user -sudo su - checkin - -# Clone the repository -cd /opt/checkin-app -git clone https://github.com/your-repo/checkin-app.git . - -# Or upload your files using scp/rsync -``` - -### 2. Setup Backend - -```bash -# Create virtual environment -python3 -m venv venv - -# Activate virtual environment -source venv/bin/activate - -# Install Python dependencies -pip install -r backend/requirements.txt - -# Create .env file -cp .env.example .env - -# Edit .env and configure your settings -nano .env -``` - -**Important Environment Variables:** - -```env -# Database -DATABASE_URL=sqlite:///./data/checkin.db - -# Security -SECRET_KEY=your-secret-key-here-change-this -ALLOWED_ORIGINS=https://your-domain.com - -# QQ Login (if applicable) -QQ_APPID=your-qq-appid -QQ_APPSECRET=your-qq-appsecret - -# Email notifications (optional) -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER=your-email@gmail.com -SMTP_PASSWORD=your-app-password -ADMIN_EMAIL=admin@your-domain.com -``` - -### 3. Initialize Database - -```bash -# Run database migrations if needed -# Example: -# alembic upgrade head - -# Or run initialization script -python backend/scripts/create_admin.py -``` - -### 4. Build Frontend - -```bash -# Install frontend dependencies -cd frontend -npm install - -# Build for production -npm run build - -# Verify build output -ls -lh dist/ -``` - -### 5. Set Permissions - -```bash -# Exit from checkin user -exit - -# Set proper permissions -sudo chown -R checkin:www-data /opt/checkin-app -sudo chmod -R 755 /opt/checkin-app -sudo chmod -R 775 /opt/checkin-app/{data,logs,sessions} -``` - ---- - -## Nginx Configuration - -### 1. Copy Configuration - -```bash -# Copy example configuration -sudo cp /opt/checkin-app/deployment/nginx.conf.example /etc/nginx/sites-available/checkin-app - -# Edit configuration -sudo nano /etc/nginx/sites-available/checkin-app -``` - -### 2. Update Configuration - -Replace the following placeholders: - -- `your-domain.com` → Your actual domain name -- `/opt/checkin-app` → Your installation path (if different) - -### 3. Enable Site - -```bash -# Create symbolic link -sudo ln -s /etc/nginx/sites-available/checkin-app /etc/nginx/sites-enabled/ - -# Remove default site (optional) -sudo rm /etc/nginx/sites-enabled/default - -# Test Nginx configuration -sudo nginx -t - -# Reload Nginx -sudo systemctl reload nginx -``` - ---- - -## Systemd Service Setup - -### 1. Copy Service File - -```bash -# Copy example service file -sudo cp /opt/checkin-app/deployment/checkin-app.service.example /etc/systemd/system/checkin-app.service - -# Edit service file -sudo nano /etc/systemd/system/checkin-app.service -``` - -### 2. Update Service File - -Replace placeholders: - -- `User=www-data` → `User=checkin` (if using dedicated user) -- `WorkingDirectory=/opt/checkin-app` → Your installation path -- Adjust paths in `ExecStart` if needed - -### 3. Enable and Start Service - -```bash -# Reload systemd -sudo systemctl daemon-reload - -# Enable service (start on boot) -sudo systemctl enable checkin-app.service - -# Start service -sudo systemctl start checkin-app.service - -# Check status -sudo systemctl status checkin-app.service - -# View logs -sudo journalctl -u checkin-app -f -``` - ---- - -## SSL/TLS Certificate - -### Using Let's Encrypt (Recommended) - -```bash -# Install Certbot -sudo apt install -y certbot python3-certbot-nginx - -# Obtain certificate -sudo certbot --nginx -d your-domain.com -d www.your-domain.com - -# Follow the prompts to configure SSL - -# Test auto-renewal -sudo certbot renew --dry-run -``` - -The Certbot will automatically update your Nginx configuration with SSL settings. - -### Manual Certificate Setup - -If you have your own SSL certificate: - -```bash -# Copy certificate files -sudo mkdir -p /etc/nginx/ssl -sudo cp your-cert.crt /etc/nginx/ssl/ -sudo cp your-key.key /etc/nginx/ssl/ - -# Set permissions -sudo chmod 600 /etc/nginx/ssl/your-key.key - -# Update Nginx configuration with certificate paths -``` - ---- - -## Monitoring and Logs - -### Service Logs - -```bash -# View service logs -sudo journalctl -u checkin-app -f - -# View last 100 lines -sudo journalctl -u checkin-app -n 100 - -# View logs since yesterday -sudo journalctl -u checkin-app --since yesterday -``` - -### Application Logs - -```bash -# Backend logs -tail -f /opt/checkin-app/logs/backend.log - -# Nginx access logs -sudo tail -f /var/log/nginx/checkin-app-access.log - -# Nginx error logs -sudo tail -f /var/log/nginx/checkin-app-error.log -``` - -### Service Status - -```bash -# Check service status -sudo systemctl status checkin-app - -# Check if port is listening -sudo netstat -tlnp | grep :8000 - -# Check process -ps aux | grep python -``` - ---- - -## Troubleshooting - -### Service Won't Start - -```bash -# Check service logs -sudo journalctl -u checkin-app -xe - -# Check if port is already in use -sudo lsof -i :8000 - -# Verify permissions -ls -la /opt/checkin-app/ - -# Test manual start -sudo -u checkin /opt/checkin-app/venv/bin/python /opt/checkin-app/run_daemon.py -``` - -### Nginx Errors - -```bash -# Test Nginx configuration -sudo nginx -t - -# Check error logs -sudo tail -f /var/log/nginx/error.log - -# Verify backend is running -curl http://localhost:8000/health -``` - -### Database Issues - -```bash -# Check database file permissions -ls -la /opt/checkin-app/data/ - -# Check if database is locked -fuser /opt/checkin-app/data/checkin.db - -# Backup database -cp /opt/checkin-app/data/checkin.db /opt/checkin-app/data/checkin.db.backup -``` - -### Frontend Not Loading - -```bash -# Verify build exists -ls -la /opt/checkin-app/frontend/dist/ - -# Check Nginx configuration for root path -grep -n "root" /etc/nginx/sites-available/checkin-app - -# Clear browser cache or test with curl -curl -I https://your-domain.com/ -``` - ---- - -## Updating the Application - -### Update Backend - -```bash -# Switch to application user -sudo su - checkin -cd /opt/checkin-app - -# Pull latest changes -git pull - -# Activate virtual environment -source venv/bin/activate - -# Update dependencies -pip install -r backend/requirements.txt - -# Run migrations if needed -# alembic upgrade head - -# Exit and restart service -exit -sudo systemctl restart checkin-app -``` - -### Update Frontend - -```bash -sudo su - checkin -cd /opt/checkin-app/frontend - -# Pull latest changes -git pull - -# Install dependencies -npm install - -# Build -npm run build - -# Exit -exit - -# No need to restart - Nginx serves static files -``` - ---- - -## Security Recommendations - -1. **Firewall**: Use `ufw` to restrict access - ```bash - sudo ufw allow 22/tcp # SSH - sudo ufw allow 80/tcp # HTTP - sudo ufw allow 443/tcp # HTTPS - sudo ufw enable - ``` - -2. **Regular Updates**: Keep system and packages updated - ```bash - sudo apt update && sudo apt upgrade - ``` - -3. **Backup**: Regular backups of database and configuration - ```bash - # Create backup script - sudo nano /opt/checkin-app/backup.sh - ``` - -4. **Monitoring**: Consider using monitoring tools like Prometheus, Grafana, or Uptime Kuma - -5. **Rate Limiting**: Configure Nginx rate limiting for API endpoints - ---- - -## Additional Resources - -- [Nginx Documentation](https://nginx.org/en/docs/) -- [Systemd Documentation](https://www.freedesktop.org/software/systemd/man/) -- [Let's Encrypt](https://letsencrypt.org/) -- [FastAPI Deployment](https://fastapi.tiangolo.com/deployment/) - ---- - -## Support - -For issues or questions, please: -- Check the logs first -- Review this guide carefully -- Open an issue on GitHub -- Contact system administrator diff --git a/deployment/README.md b/deployment/README.md deleted file mode 100644 index 0360f3c..0000000 --- a/deployment/README.md +++ /dev/null @@ -1,307 +0,0 @@ -# Deployment Files - -This directory contains configuration files and scripts for deploying CheckIn App V2 to a production server. - -## Files - -- **`nginx.conf.example`** - Nginx reverse proxy configuration -- **`checkin-app.service.example`** - Systemd service file -- **`deploy.sh`** - Automated deployment script -- **`DEPLOYMENT.md`** - Comprehensive deployment guide - -## Quick Start - -### Option 1: Automated Deployment (Recommended) - -```bash -# Make script executable -chmod +x deployment/deploy.sh - -# Run installation -sudo deployment/deploy.sh install -``` - -### Option 2: Manual Deployment - -Follow the step-by-step guide in [DEPLOYMENT.md](./DEPLOYMENT.md). - -## Deployment Script Usage - -The `deploy.sh` script provides three main commands: - -### 1. Install (First-time deployment) - -```bash -sudo deployment/deploy.sh install -``` - -This will: -- Check system dependencies -- Create application user -- Setup virtual environment -- Install Python dependencies -- Build frontend -- Configure systemd service -- Configure Nginx -- Start all services - -### 2. Update (Update existing installation) - -```bash -sudo deployment/deploy.sh update -``` - -This will: -- Backup database -- Pull latest changes (if using git) -- Update Python dependencies -- Rebuild frontend -- Restart services - -### 3. Rollback (Revert to previous version) - -```bash -sudo deployment/deploy.sh rollback -``` - -This will: -- Stop services -- Restore database from latest backup -- Restart services - -## Configuration Files - -### Nginx Configuration - -Edit `/etc/nginx/sites-available/checkin-app` and update: - -- `server_name` - Your domain name -- `ssl_certificate` and `ssl_certificate_key` - SSL certificate paths -- `root` - Frontend build directory path (usually `/opt/checkin-app/frontend/dist`) - -### Systemd Service - -Edit `/etc/systemd/system/checkin-app.service` and update: - -- `User` and `Group` - Application user (default: `checkin`) -- `WorkingDirectory` - Application directory (default: `/opt/checkin-app`) -- `ExecStart` - Path to Python executable and run script - -### Environment Variables - -Create and configure `.env` file in the application root: - -```bash -sudo nano /opt/checkin-app/.env -``` - -Required variables: -```env -# Database -DATABASE_URL=sqlite:///./data/checkin.db - -# Security -SECRET_KEY=your-secret-key-here -ALLOWED_ORIGINS=https://your-domain.com - -# QQ Login -QQ_APPID=your-appid -QQ_APPSECRET=your-appsecret -``` - -## SSL Certificate Setup - -### Using Let's Encrypt (Recommended) - -```bash -# Install Certbot -sudo apt install certbot python3-certbot-nginx - -# Obtain certificate -sudo certbot --nginx -d your-domain.com - -# Auto-renewal is configured automatically -``` - -### Manual Certificate - -If you have your own SSL certificate: - -1. Copy certificate files to `/etc/nginx/ssl/` -2. Update Nginx configuration with correct paths -3. Reload Nginx: `sudo systemctl reload nginx` - -## Service Management - -### Start Service - -```bash -sudo systemctl start checkin-app -``` - -### Stop Service - -```bash -sudo systemctl stop checkin-app -``` - -### Restart Service - -```bash -sudo systemctl restart checkin-app -``` - -### Check Status - -```bash -sudo systemctl status checkin-app -``` - -### View Logs - -```bash -# Application logs -sudo journalctl -u checkin-app -f - -# Nginx access logs -sudo tail -f /var/log/nginx/checkin-app-access.log - -# Nginx error logs -sudo tail -f /var/log/nginx/checkin-app-error.log -``` - -## Directory Structure - -After deployment, the application structure should look like: - -``` -/opt/checkin-app/ -├── backend/ # Backend Python code -│ ├── api/ -│ ├── models/ -│ ├── services/ -│ └── ... -├── frontend/ # Frontend source code -│ ├── src/ -│ ├── dist/ # Built static files (served by Nginx) -│ └── ... -├── venv/ # Python virtual environment -├── data/ # SQLite database -├── logs/ # Application logs -├── sessions/ # Session data -├── deployment/ # Deployment files (this directory) -├── .env # Environment variables -└── run_daemon.py # Application entry point -``` - -## Troubleshooting - -### Service won't start - -```bash -# Check logs -sudo journalctl -u checkin-app -xe - -# Verify configuration -sudo -u checkin /opt/checkin-app/venv/bin/python /opt/checkin-app/run_daemon.py -``` - -### Nginx configuration errors - -```bash -# Test configuration -sudo nginx -t - -# Check error logs -sudo tail -f /var/log/nginx/error.log -``` - -### Database locked - -```bash -# Check what's using the database -sudo fuser /opt/checkin-app/data/checkin.db - -# Kill the process if needed -sudo fuser -k /opt/checkin-app/data/checkin.db -``` - -### Permission issues - -```bash -# Fix ownership -sudo chown -R checkin:www-data /opt/checkin-app - -# Fix permissions -sudo chmod -R 755 /opt/checkin-app -sudo chmod -R 775 /opt/checkin-app/{data,logs,sessions} -``` - -## Security Best Practices - -1. **Keep system updated** - ```bash - sudo apt update && sudo apt upgrade - ``` - -2. **Use firewall** - ```bash - sudo ufw allow 22/tcp # SSH - sudo ufw allow 80/tcp # HTTP - sudo ufw allow 443/tcp # HTTPS - sudo ufw enable - ``` - -3. **Regular backups** - ```bash - # Backup database - sudo -u checkin cp /opt/checkin-app/data/checkin.db /backup/checkin-$(date +%Y%m%d).db - ``` - -4. **Monitor logs** - ```bash - # Setup log rotation - sudo nano /etc/logrotate.d/checkin-app - ``` - -5. **Use strong passwords** and **secure SECRET_KEY** - -## Performance Tuning - -### Nginx - -- Enable gzip compression (already configured) -- Configure caching headers (already configured) -- Adjust worker processes based on CPU cores - -### Backend - -- Increase uvicorn workers in service file: - ``` - ExecStart=/opt/checkin-app/venv/bin/uvicorn backend.main:app --workers 4 - ``` - -- Consider using Gunicorn with uvicorn workers for production - -### Database - -- For high traffic, consider switching to PostgreSQL -- Regular VACUUM for SQLite - -## Monitoring - -Consider setting up monitoring tools: - -- **Uptime monitoring**: Uptime Kuma, UptimeRobot -- **Log aggregation**: Loki, ELK Stack -- **Metrics**: Prometheus + Grafana -- **Error tracking**: Sentry - -## Support - -For detailed deployment instructions, see [DEPLOYMENT.md](./DEPLOYMENT.md). - -For issues or questions: -- Check application logs -- Review troubleshooting section -- Open an issue on GitHub diff --git a/deployment/deploy.sh b/deployment/deploy.sh deleted file mode 100644 index c6696cb..0000000 --- a/deployment/deploy.sh +++ /dev/null @@ -1,358 +0,0 @@ -#!/bin/bash -# ============================================================================== -# CheckIn App V2 - Quick Deployment Script -# ============================================================================== -# -# This script automates the deployment process for CheckIn App V2 -# -# Usage: -# sudo ./deploy.sh [install|update|rollback] -# -# ============================================================================== - -set -e - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Configuration -APP_NAME="checkin-app" -APP_USER="checkin" -APP_DIR="/opt/checkin-app" -SERVICE_NAME="checkin-app.service" -NGINX_CONFIG="checkin-app" - -# Functions -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -check_root() { - if [ "$EUID" -ne 0 ]; then - log_error "This script must be run as root (use sudo)" - exit 1 - fi -} - -check_dependencies() { - log_info "Checking dependencies..." - - local missing_deps=() - - # Check Python - if ! command -v python3 &> /dev/null; then - missing_deps+=("python3") - fi - - # Check Node.js - if ! command -v node &> /dev/null; then - missing_deps+=("nodejs") - fi - - # Check Nginx - if ! command -v nginx &> /dev/null; then - missing_deps+=("nginx") - fi - - if [ ${#missing_deps[@]} -ne 0 ]; then - log_error "Missing dependencies: ${missing_deps[*]}" - log_info "Please install them first:" - log_info " sudo apt install -y python3 python3-pip python3-venv nodejs nginx" - exit 1 - fi - - log_info "All dependencies are installed" -} - -create_user() { - if id "$APP_USER" &>/dev/null; then - log_info "User $APP_USER already exists" - else - log_info "Creating user $APP_USER..." - useradd -r -m -s /bin/bash "$APP_USER" - usermod -aG www-data "$APP_USER" - log_info "User $APP_USER created" - fi -} - -create_directories() { - log_info "Creating application directories..." - - mkdir -p "$APP_DIR" - chown -R "$APP_USER:www-data" "$APP_DIR" - - sudo -u "$APP_USER" mkdir -p "$APP_DIR"/{data,logs,sessions} - - log_info "Directories created" -} - -setup_backend() { - log_info "Setting up backend..." - - cd "$APP_DIR" - - # Create virtual environment - if [ ! -d "venv" ]; then - log_info "Creating virtual environment..." - sudo -u "$APP_USER" python3 -m venv venv - fi - - # Install dependencies - log_info "Installing Python dependencies..." - sudo -u "$APP_USER" bash -c "source venv/bin/activate && pip install --upgrade pip && pip install -r backend/requirements.txt" - - # Create .env if not exists - if [ ! -f ".env" ]; then - log_warn ".env file not found, please create one from .env.example" - if [ -f ".env.example" ]; then - sudo -u "$APP_USER" cp .env.example .env - log_info "Created .env from .env.example - please configure it" - fi - fi - - log_info "Backend setup complete" -} - -build_frontend() { - log_info "Building frontend..." - - cd "$APP_DIR/frontend" - - # Install dependencies - if [ ! -d "node_modules" ]; then - log_info "Installing Node.js dependencies..." - sudo -u "$APP_USER" npm install - fi - - # Build - log_info "Building frontend for production..." - sudo -u "$APP_USER" npm run build - - if [ -d "dist" ]; then - log_info "Frontend built successfully" - else - log_error "Frontend build failed - dist directory not found" - exit 1 - fi -} - -setup_systemd() { - log_info "Setting up systemd service..." - - if [ -f "$APP_DIR/deployment/checkin-app.service.example" ]; then - # Copy service file - cp "$APP_DIR/deployment/checkin-app.service.example" "/etc/systemd/system/$SERVICE_NAME" - - # Reload systemd - systemctl daemon-reload - - # Enable service - systemctl enable "$SERVICE_NAME" - - log_info "Systemd service configured" - else - log_error "Service file not found: $APP_DIR/deployment/checkin-app.service.example" - exit 1 - fi -} - -setup_nginx() { - log_info "Setting up Nginx configuration..." - - if [ -f "$APP_DIR/deployment/nginx.conf.example" ]; then - # Copy Nginx config - cp "$APP_DIR/deployment/nginx.conf.example" "/etc/nginx/sites-available/$NGINX_CONFIG" - - # Create symlink - if [ ! -L "/etc/nginx/sites-enabled/$NGINX_CONFIG" ]; then - ln -s "/etc/nginx/sites-available/$NGINX_CONFIG" "/etc/nginx/sites-enabled/$NGINX_CONFIG" - fi - - # Test Nginx config - if nginx -t; then - log_info "Nginx configuration is valid" - else - log_error "Nginx configuration test failed" - exit 1 - fi - - log_warn "Please edit /etc/nginx/sites-available/$NGINX_CONFIG and configure your domain" - else - log_error "Nginx config file not found: $APP_DIR/deployment/nginx.conf.example" - exit 1 - fi -} - -start_services() { - log_info "Starting services..." - - # Start application - systemctl start "$SERVICE_NAME" - - # Reload Nginx - systemctl reload nginx - - # Check status - sleep 2 - if systemctl is-active --quiet "$SERVICE_NAME"; then - log_info "Application service started successfully" - else - log_error "Application service failed to start" - systemctl status "$SERVICE_NAME" - exit 1 - fi - - log_info "All services started" -} - -install() { - log_info "Starting installation..." - - check_root - check_dependencies - create_user - create_directories - setup_backend - build_frontend - setup_systemd - setup_nginx - - # Set permissions - chown -R "$APP_USER:www-data" "$APP_DIR" - chmod -R 755 "$APP_DIR" - chmod -R 775 "$APP_DIR"/{data,logs,sessions} - - start_services - - echo "" - log_info "================================================" - log_info "Installation complete!" - log_info "================================================" - echo "" - log_info "Next steps:" - log_info "1. Configure .env file: sudo nano $APP_DIR/.env" - log_info "2. Configure Nginx: sudo nano /etc/nginx/sites-available/$NGINX_CONFIG" - log_info "3. Set up SSL certificate: sudo certbot --nginx -d your-domain.com" - log_info "4. Restart services: sudo systemctl restart $SERVICE_NAME nginx" - echo "" - log_info "Useful commands:" - log_info " Status: sudo systemctl status $SERVICE_NAME" - log_info " Logs: sudo journalctl -u $SERVICE_NAME -f" - log_info " Restart: sudo systemctl restart $SERVICE_NAME" - echo "" -} - -update() { - log_info "Updating application..." - - check_root - - cd "$APP_DIR" - - # Backup database - if [ -f "data/checkin.db" ]; then - log_info "Backing up database..." - sudo -u "$APP_USER" cp data/checkin.db "data/checkin.db.backup.$(date +%Y%m%d_%H%M%S)" - fi - - # Pull latest changes (if using git) - if [ -d ".git" ]; then - log_info "Pulling latest changes..." - sudo -u "$APP_USER" git pull - fi - - # Update backend - log_info "Updating backend dependencies..." - sudo -u "$APP_USER" bash -c "source venv/bin/activate && pip install -r backend/requirements.txt" - - # Rebuild frontend - build_frontend - - # Restart service - log_info "Restarting service..." - systemctl restart "$SERVICE_NAME" - - # Check status - sleep 2 - if systemctl is-active --quiet "$SERVICE_NAME"; then - log_info "Update completed successfully" - else - log_error "Service failed to start after update" - systemctl status "$SERVICE_NAME" - exit 1 - fi -} - -rollback() { - log_info "Rolling back to previous version..." - - check_root - - cd "$APP_DIR" - - # Find latest backup - LATEST_BACKUP=$(ls -t data/checkin.db.backup.* 2>/dev/null | head -n 1) - - if [ -z "$LATEST_BACKUP" ]; then - log_error "No database backup found" - exit 1 - fi - - log_info "Found backup: $LATEST_BACKUP" - - # Stop service - systemctl stop "$SERVICE_NAME" - - # Restore database - log_info "Restoring database..." - sudo -u "$APP_USER" cp "$LATEST_BACKUP" data/checkin.db - - # Rollback git (if using git) - if [ -d ".git" ]; then - log_warn "Please manually rollback git to the desired commit" - log_info "Example: git reset --hard " - fi - - # Start service - systemctl start "$SERVICE_NAME" - - log_info "Rollback completed" -} - -# Main -case "${1:-}" in - install) - install - ;; - update) - update - ;; - rollback) - rollback - ;; - *) - echo "CheckIn App V2 - Deployment Script" - echo "" - echo "Usage: $0 {install|update|rollback}" - echo "" - echo "Commands:" - echo " install - Full installation (first time)" - echo " update - Update existing installation" - echo " rollback - Rollback to previous version" - echo "" - exit 1 - ;; -esac - -exit 0 diff --git a/deployment/nginx.conf.example b/deployment/nginx.conf.example deleted file mode 100644 index 4060883..0000000 --- a/deployment/nginx.conf.example +++ /dev/null @@ -1,216 +0,0 @@ -# ============================================================================== -# CheckIn App V2 - Nginx Configuration Example -# ============================================================================== -# -# Usage: -# 1. Copy this file: sudo cp nginx.conf.example /etc/nginx/sites-available/checkin-app -# 2. Edit the file and replace placeholders with your actual values -# 3. Create symlink: sudo ln -s /etc/nginx/sites-available/checkin-app /etc/nginx/sites-enabled/ -# 4. Test config: sudo nginx -t -# 5. Reload Nginx: sudo systemctl reload nginx -# -# ============================================================================== - -# Upstream backend API server -upstream checkin_backend { - # Backend FastAPI server running on port 8000 - server 127.0.0.1:8000; - - # Optional: Add more backend servers for load balancing - # server 127.0.0.1:8001; - # server 127.0.0.1:8002; - - # Keep alive connections - keepalive 32; -} - -# HTTP Server - Redirect to HTTPS (optional) -server { - listen 80; - listen [::]:80; - server_name your-domain.com www.your-domain.com; - - # Redirect all HTTP traffic to HTTPS - return 301 https://$server_name$request_uri; -} - -# HTTPS Server -server { - # SSL Configuration - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name your-domain.com www.your-domain.com; - - # SSL Certificate (Let's Encrypt recommended) - ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; - - # SSL Configuration (Modern) - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; - ssl_prefer_server_ciphers off; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - - # Security Headers - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Root directory for frontend static files - root /opt/checkin-app/frontend/dist; - index index.html; - - # Access and Error Logs - access_log /var/log/nginx/checkin-app-access.log; - error_log /var/log/nginx/checkin-app-error.log; - - # Gzip Compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; - - # Client body size (for file uploads) - client_max_body_size 10M; - - # ========================================== - # API Proxy Configuration - # ========================================== - location /api/ { - # Proxy to backend - proxy_pass http://checkin_backend; - - # Proxy headers - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support (if needed) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - - # Buffering - proxy_buffering off; - proxy_request_buffering off; - } - - # API Documentation - location /docs { - proxy_pass http://checkin_backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /redoc { - proxy_pass http://checkin_backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /openapi.json { - proxy_pass http://checkin_backend; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # ========================================== - # Frontend Static Files - # ========================================== - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - try_files $uri =404; - } - - # Frontend routes (SPA) - location / { - try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - # Favicon - location = /favicon.ico { - log_not_found off; - access_log off; - } - - # Robots.txt - location = /robots.txt { - log_not_found off; - access_log off; - } - - # Health check endpoint - location /health { - proxy_pass http://checkin_backend; - access_log off; - } -} - -# ============================================================================== -# Alternative: HTTP-only configuration (for development/internal use) -# ============================================================================== -# Uncomment below if you don't need HTTPS - -# server { -# listen 80; -# listen [::]:80; -# server_name your-domain.com; -# -# root /opt/checkin-app/frontend/dist; -# index index.html; -# -# access_log /var/log/nginx/checkin-app-access.log; -# error_log /var/log/nginx/checkin-app-error.log; -# -# gzip on; -# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; -# -# client_max_body_size 10M; -# -# # API Proxy -# location /api/ { -# proxy_pass http://127.0.0.1:8000; -# proxy_set_header Host $host; -# proxy_set_header X-Real-IP $remote_addr; -# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -# proxy_set_header X-Forwarded-Proto $scheme; -# proxy_http_version 1.1; -# proxy_buffering off; -# } -# -# # API Documentation -# location ~ ^/(docs|redoc|openapi.json) { -# proxy_pass http://127.0.0.1:8000; -# proxy_set_header Host $host; -# proxy_set_header X-Real-IP $remote_addr; -# } -# -# # Static files -# location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { -# expires 1y; -# add_header Cache-Control "public"; -# } -# -# # Frontend routes -# location / { -# try_files $uri $uri/ /index.html; -# } -# } diff --git a/frontend/src/views/TaskRecordsView.vue b/frontend/src/views/TaskRecordsView.vue index 9fc285e..01c54dd 100644 --- a/frontend/src/views/TaskRecordsView.vue +++ b/frontend/src/views/TaskRecordsView.vue @@ -23,7 +23,7 @@ - 接龙 ID: {{ currentTask.thread_id }} + 接龙 ID: {{ getThreadId(currentTask) }} {{ currentTask.is_active ? '启用中' : '已禁用' }} @@ -148,8 +148,8 @@ v-else class="status-error" >❌ 打卡失败 - - {{ record.trigger_type === 'scheduler' ? '自动触发' : '手动触发' }} + + {{ record.trigger_type === 'scheduled' ? '自动触发' : '手动触发' }}
@@ -239,6 +239,19 @@ const recordStats = computed(() => { } }) +// 从 payload_config 中提取 ThreadId +const getThreadId = (task) => { + if (!task || !task.payload_config) return '未知' + + try { + const payload = JSON.parse(task.payload_config) + return payload.ThreadId || '未知' + } catch (e) { + console.error('解析 payload_config 失败:', e) + return '未知' + } +} + // 获取任务详情 const fetchTaskDetail = async () => { try { diff --git a/frontend/src/views/TasksView.vue b/frontend/src/views/TasksView.vue index 424ab8d..2c8e5f5 100644 --- a/frontend/src/views/TasksView.vue +++ b/frontend/src/views/TasksView.vue @@ -110,7 +110,7 @@ - 接龙ID: {{ task.thread_id || '未知' }} + 接龙ID: {{ getThreadId(task) }}
@@ -550,6 +550,19 @@ const handleModeChange = (mode) => { templateTaskForm.field_values = {} } +// 从 payload_config 中提取 ThreadId +const getThreadId = (task) => { + if (!task.payload_config) return '未知' + + try { + const payload = JSON.parse(task.payload_config) + return payload.ThreadId || '未知' + } catch (e) { + console.error('解析 payload_config 失败:', e) + return '未知' + } +} + // 加载任务列表 const fetchTasks = async () => { loading.value = true @@ -570,12 +583,22 @@ const viewTask = (task) => { // 编辑任务 const editTask = (task) => { editingTask.value = task + + // 从 payload_config 中提取 thread_id + let threadId = '' + try { + const payload = JSON.parse(task.payload_config || '{}') + threadId = payload.ThreadId || '' + } catch (e) { + console.error('解析 payload_config 失败:', e) + } + Object.assign(taskForm, { name: task.name, - thread_id: task.thread_id, + thread_id: threadId, is_active: task.is_active, payload_config: task.payload_config || '{}', - cron_expression: task.cron_expression || '0 20 * * *', // 新增:加载 cron_expression + cron_expression: task.cron_expression || '0 20 * * *', }) showCreateDialog.value = true } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c7a50d7..0555872 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -13,6 +13,7 @@ export default defineConfig({ }, server: { + host: '0.0.0.0', // Listen on all network interfaces for LAN access port: 3000, proxy: { '/api': { diff --git a/manage.bat b/manage.bat index 8349309..b3f7804 100644 --- a/manage.bat +++ b/manage.bat @@ -87,7 +87,7 @@ if exist "%BACKEND_PID_FILE%" ( echo [WARNING] Backend is already running (PID: !PID!) exit /b 0 ) else ( - echo [INFO] Backend PID file exists but process not running, cleaning up... + REM Silently clean up stale PID file (don't show message) del "%BACKEND_PID_FILE%" >nul 2>&1 ) ) @@ -106,7 +106,7 @@ if not exist "sessions" mkdir sessions echo [INFO] Starting backend service in background... -REM Create a VBS script to run Python invisibly +REM Create a VBS script to run Python invisibly (using venv's python.exe directly) set VBS_FILE=%TEMP%\start_backend.vbs echo Set WshShell = CreateObject("WScript.Shell") > "%VBS_FILE%" echo WshShell.Run """%PYTHON_EXE%"" ""%APP_DIR%run_daemon.py""", 0, False >> "%VBS_FILE%" @@ -167,7 +167,7 @@ if exist "%FRONTEND_PID_FILE%" ( echo [WARNING] Frontend is already running (PID: !PID!) exit /b 0 ) else ( - echo [INFO] Frontend PID file exists but process not running, cleaning up... + REM Silently clean up stale PID file (don't show message) del "%FRONTEND_PID_FILE%" >nul 2>&1 ) ) @@ -211,7 +211,7 @@ del "%VBS_FILE%" >nul 2>&1 echo [INFO] Waiting for frontend to start... timeout /t 3 /nobreak >nul -REM Check if port 3000 is listening (Vite configured port) +REM Check if port 5000 is listening (Vite configured port) set SERVICE_RUNNING=0 for /L %%i in (1,1,10) do ( netstat -ano | findstr ":3000" | findstr "LISTENING" >nul 2>&1 @@ -289,8 +289,12 @@ set BACKEND_KILLED=0 for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8000" ^| findstr "LISTENING"') do ( taskkill /F /PID %%a >nul 2>&1 if "!ERRORLEVEL!"=="0" ( - echo [OK] Backend stopped (PID: %%a) + echo [OK] Backend stopped ^(PID: %%a^) set BACKEND_KILLED=1 + REM Delete PID file immediately after successful kill + if exist "%BACKEND_PID_FILE%" ( + del "%BACKEND_PID_FILE%" >nul 2>&1 + ) ) ) @@ -301,17 +305,24 @@ if "%BACKEND_KILLED%"=="0" ( tasklist /FI "PID eq !PID!" 2>NUL | find /I /N "python.exe">NUL if "!ERRORLEVEL!"=="0" ( taskkill /F /T /PID !PID! >nul 2>&1 - echo [OK] Backend stopped (PID: !PID!) + if "!ERRORLEVEL!"=="0" ( + echo [OK] Backend stopped ^(PID: !PID!^) + set BACKEND_KILLED=1 + REM Delete PID file immediately after successful kill + del "%BACKEND_PID_FILE%" >nul 2>&1 + ) ) else ( - echo [WARNING] Backend not running (process does not exist) + REM Process doesn't exist, just clean up the stale PID file + del "%BACKEND_PID_FILE%" >nul 2>&1 ) - ) else ( - echo [WARNING] Backend not running (no process found) + ) + + REM Only show warning if nothing was stopped + if "%BACKEND_KILLED%"=="0" ( + echo [WARNING] Backend not running ) ) -REM Clean up PID file -del "%BACKEND_PID_FILE%" >nul 2>&1 exit /b 0 REM --- Frontend Stop Logic --- @@ -323,12 +334,16 @@ set FRONTEND_KILLED=0 for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":3000" ^| findstr "LISTENING"') do ( taskkill /F /T /PID %%a >nul 2>&1 if "!ERRORLEVEL!"=="0" ( - echo [OK] Frontend stopped (PID: %%a) + echo [OK] Frontend stopped ^(PID: %%a^) set FRONTEND_KILLED=1 + REM Delete PID file immediately after successful kill + if exist "%FRONTEND_PID_FILE%" ( + del "%FRONTEND_PID_FILE%" >nul 2>&1 + ) ) ) -REM Also check ports 3001-3010 (Vite fallback ports) +REM Also check ports 5001-5010 (Vite fallback ports) if "%FRONTEND_KILLED%"=="0" ( for /L %%p in (3001,1,3010) do ( for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":%%p" ^| findstr "LISTENING"') do ( @@ -337,8 +352,12 @@ if "%FRONTEND_KILLED%"=="0" ( if "!ERRORLEVEL!"=="0" ( taskkill /F /T /PID %%a >nul 2>&1 if "!ERRORLEVEL!"=="0" ( - echo [OK] Frontend stopped (PID: %%a, Port: %%p) + echo [OK] Frontend stopped ^(PID: %%a, Port: %%p^) set FRONTEND_KILLED=1 + REM Delete PID file immediately after successful kill + if exist "%FRONTEND_PID_FILE%" ( + del "%FRONTEND_PID_FILE%" >nul 2>&1 + ) ) ) ) @@ -352,17 +371,24 @@ if "%FRONTEND_KILLED%"=="0" ( tasklist /FI "PID eq !PID!" 2>NUL | find /I /N "node.exe">NUL if "!ERRORLEVEL!"=="0" ( taskkill /F /T /PID !PID! >nul 2>&1 - echo [OK] Frontend stopped (PID: !PID!) + if "!ERRORLEVEL!"=="0" ( + echo [OK] Frontend stopped ^(PID: !PID!^) + set FRONTEND_KILLED=1 + REM Delete PID file immediately after successful kill + del "%FRONTEND_PID_FILE%" >nul 2>&1 + ) ) else ( - echo [WARNING] Frontend not running (process does not exist) + REM Process doesn't exist, just clean up the stale PID file + del "%FRONTEND_PID_FILE%" >nul 2>&1 ) - ) else ( - echo [WARNING] Frontend not running (no process found) + ) + + REM Only show warning if nothing was stopped + if "%FRONTEND_KILLED%"=="0" ( + echo [WARNING] Frontend not running ) ) -REM Clean up PID file -del "%FRONTEND_PID_FILE%" >nul 2>&1 exit /b 0 REM ============================================ @@ -373,6 +399,19 @@ echo [INFO] Restarting %TARGET%... echo. call :stop timeout /t 2 /nobreak >nul + +REM Force cleanup of any remaining PID files before starting +if "%TARGET%"=="backend" ( + del "%BACKEND_PID_FILE%" >nul 2>&1 +) +if "%TARGET%"=="frontend" ( + del "%FRONTEND_PID_FILE%" >nul 2>&1 +) +if "%TARGET%"=="all" ( + del "%BACKEND_PID_FILE%" >nul 2>&1 + del "%FRONTEND_PID_FILE%" >nul 2>&1 +) + call :start goto end @@ -586,7 +625,7 @@ echo build - Build frontend for production echo. echo Targets: echo backend - Backend API service (default port: 8000) -echo frontend - Frontend dev server (default port: 3000) +echo frontend - Frontend dev server (default port: 5000) echo all - Both services (default) echo. echo Examples: diff --git a/manage.sh b/manage.sh index c3e0c9b..1144abb 100644 --- a/manage.sh +++ b/manage.sh @@ -1,632 +1,749 @@ #!/bin/bash # ============================================================================== -# CheckIn App V2 - Unified Process Manager (Linux/Mac) -# Usage: ./manage.sh {start|stop|restart|status|log} [backend|frontend|all] +# CheckIn App V2 - Unified Service Manager (Linux/macOS) +# ============================================================================== +# Description: Manages backend and frontend services with unified interface +# Usage: ./manage.sh COMMAND [TARGET] +# Commands: start, stop, restart, status, log, build +# Targets: backend, frontend, all (default) # ============================================================================== -set -e - -# Get script directory -APP_DIR=$(cd "$(dirname "$0")" && pwd) +set -eu +# Enable pipefail if supported (bash 3+) +if set -o | grep -q pipefail; then + set -o pipefail +fi +# ============================================================================== # Configuration -VENV_DIR="$APP_DIR/venv" -BACKEND_PID_FILE="$APP_DIR/backend.pid" -FRONTEND_PID_FILE="$APP_DIR/frontend.pid" -BACKEND_LOG_FILE="$APP_DIR/logs/backend.log" -FRONTEND_LOG_FILE="$APP_DIR/logs/frontend.log" -PYTHON_EXE="$VENV_DIR/bin/python" +# ============================================================================== +readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +readonly APP_DIR="${SCRIPT_DIR}" +readonly VENV_DIR="${APP_DIR}/venv" +readonly PYTHON_BIN="${VENV_DIR}/bin/python" -# Parse command and target -COMMAND=$1 -TARGET=${2:-all} +readonly BACKEND_PID="${APP_DIR}/backend.pid" +readonly FRONTEND_PID="${APP_DIR}/frontend.pid" +readonly BACKEND_LOG="${APP_DIR}/logs/backend.log" +readonly FRONTEND_LOG="${APP_DIR}/logs/frontend.log" -# Color codes for better output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color +readonly BACKEND_PORT=8000 +readonly FRONTEND_PORT=3000 + +# Colors +readonly C_RESET='\033[0m' +readonly C_RED='\033[0;31m' +readonly C_GREEN='\033[0;32m' +readonly C_YELLOW='\033[1;33m' +readonly C_BLUE='\033[0;34m' +readonly C_CYAN='\033[0;36m' + +# ============================================================================== +# Utility Functions +# ============================================================================== +print_header() { + local text="$1" + printf "\n" + printf "${C_CYAN}========================================${C_RESET}\n" + printf "${C_CYAN}%s${C_RESET}\n" "$text" + printf "${C_CYAN}========================================${C_RESET}\n" +} -# Helper functions log_info() { - echo -e "${GREEN}[INFO]${NC} $1" + printf "${C_GREEN}[INFO]${C_RESET} %s\n" "$1" +} + +log_success() { + printf "${C_GREEN}[OK]${C_RESET} %s\n" "$1" } log_warn() { - echo -e "${YELLOW}[WARNING]${NC} $1" + printf "${C_YELLOW}[WARNING]${C_RESET} %s\n" "$1" } log_error() { - echo -e "${RED}[ERROR]${NC} $1" + printf "${C_RED}[ERROR]${C_RESET} %s\n" "$1" } -log_ok() { - echo -e "${GREEN}[OK]${NC} $1" +log_debug() { + printf "${C_BLUE}[DEBUG]${C_RESET} %s\n" "$1" } -# ============================================ -# START COMMAND -# ============================================ -start_command() { - case $TARGET in - backend) - echo "========================================" - echo "CheckIn App V2 - Starting Backend" - echo "========================================" - echo "" - start_backend - ;; - frontend) - echo "========================================" - echo "CheckIn App V2 - Starting Frontend" - echo "========================================" - echo "" - start_frontend - ;; - all) - echo "========================================" - echo "CheckIn App V2 - Starting All Services" - echo "========================================" - echo "" - start_backend - echo "" - start_frontend - echo "" - echo "========================================" - echo "All Services Started!" - echo "========================================" - echo "" - echo "Backend API: http://localhost:8000" - echo "API Docs: http://localhost:8000/docs" - echo "Frontend App: http://localhost:3000" - echo "" - ;; - *) - log_error "Invalid target: $TARGET" - usage - ;; - esac +# Check if a process is running by PID +is_process_alive() { + local pid="$1" + kill -0 "$pid" 2>/dev/null } -# Backend start logic +# Get PID from PID file if exists +get_pid_from_file() { + local pid_file="$1" + if [ -f "$pid_file" ]; then + cat "$pid_file" + else + echo "" + fi +} + +# Get PID listening on a port +get_pid_by_port() { + local port="$1" + local pid="" + + # Try lsof first + if command -v lsof >/dev/null 2>&1; then + pid=$(lsof -ti ":$port" 2>/dev/null | head -n1) + if [ -n "$pid" ]; then + echo "$pid" + return 0 + fi + fi + + # Fall back to netstat + ps + if command -v netstat >/dev/null 2>&1; then + local line + line=$(netstat -tlnp 2>/dev/null | grep ":$port " | head -n1) + if [ -n "$line" ]; then + pid=$(echo "$line" | awk '{print $NF}' | cut -d'/' -f1) + if [ -n "$pid" ] && [ "$pid" != "-" ]; then + echo "$pid" + return 0 + fi + fi + fi + + echo "" + return 1 +} + +# Check Node.js version (returns 0 for valid, 1 for invalid) +check_node_version() { + local node_cmd="$1" + local node_version + node_version=$($node_cmd --version 2>/dev/null | sed 's/v//') + local major_version + major_version=$(echo "$node_version" | cut -d. -f1) + + # Vite requires Node.js 20.19+ or 22.12+ + if [ "$major_version" -lt 20 ]; then + return 1 + fi + return 0 +} + +# Detect Node.js binary +find_node() { + local node_cmd="" + + if command -v node &>/dev/null; then + node_cmd="node" + elif [ -x /usr/bin/node ]; then + node_cmd="/usr/bin/node" + elif [ -x /usr/local/bin/node ]; then + node_cmd="/usr/local/bin/node" + else + return 1 + fi + + echo "$node_cmd" + return 0 +} + +# Wait for port to be listening +wait_for_port() { + local port="$1" + local max_wait="${2:-10}" + local count=0 + + while [ $count -lt $max_wait ]; do + local pid + pid=$(get_pid_by_port "$port") + if [ -n "$pid" ]; then + return 0 + fi + sleep 1 + count=$((count + 1)) + done + return 1 +} + +# ============================================================================== +# Backend Management +# ============================================================================== start_backend() { + log_info "Starting backend service..." + # Check if already running - if [ -f "$BACKEND_PID_FILE" ]; then - PID=$(cat "$BACKEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - log_warn "Backend is already running (PID: $PID)" - return 0 - else - log_info "Backend PID file exists but process not running, cleaning up..." - rm -f "$BACKEND_PID_FILE" - fi + local pid + pid=$(get_pid_from_file "$BACKEND_PID") + if [ -n "$pid" ] && is_process_alive "$pid"; then + log_warn "Backend already running (PID: $pid)" + return 0 fi - # Check virtual environment + # Clean stale PID file + [ -f "$BACKEND_PID" ] && rm -f "$BACKEND_PID" + + # Verify virtual environment if [ ! -d "$VENV_DIR" ]; then - log_error "Virtual environment does not exist: $VENV_DIR" - log_info "Please run first: python3 -m venv venv" - exit 1 + log_error "Virtual environment not found: $VENV_DIR" + log_info "Create it with: python3 -m venv venv" + return 1 fi - # Check required directories - mkdir -p data logs sessions + if [ ! -x "$PYTHON_BIN" ]; then + log_error "Python executable not found: $PYTHON_BIN" + return 1 + fi - log_info "Starting backend service in background..." + # Create required directories + mkdir -p "${APP_DIR}/data" "${APP_DIR}/logs" "${APP_DIR}/sessions" - # Start backend using run_daemon.py - nohup "$PYTHON_EXE" "$APP_DIR/run_daemon.py" > "$BACKEND_LOG_FILE" 2>&1 & - PID=$! - echo $PID > "$BACKEND_PID_FILE" + # Start backend daemon + log_info "Launching backend daemon..." + nohup "$PYTHON_BIN" "${APP_DIR}/run_daemon.py" >"$BACKEND_LOG" 2>&1 & + local daemon_pid=$! + echo "$daemon_pid" >"$BACKEND_PID" - log_info "Waiting for backend to start..." - sleep 3 - - # Check if port 8000 is listening - SERVICE_RUNNING=0 - for i in {1..10}; do - if lsof -i :8000 > /dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q :8000; then - SERVICE_RUNNING=1 - break - fi - sleep 1 - done - - if [ $SERVICE_RUNNING -eq 1 ]; then - # Get actual PID from port 8000 - ACTUAL_PID=$(lsof -ti :8000 2>/dev/null | head -n 1) - if [ -n "$ACTUAL_PID" ]; then - echo $ACTUAL_PID > "$BACKEND_PID_FILE" - log_ok "Backend started successfully (PID: $ACTUAL_PID)" - echo " API Docs: http://localhost:8000/docs" - echo " Log: $BACKEND_LOG_FILE" + # Wait for service to be ready + log_info "Waiting for backend to be ready..." + if wait_for_port "$BACKEND_PORT" 15; then + # Update PID with actual process on port + local actual_pid + actual_pid=$(get_pid_by_port "$BACKEND_PORT") + if [ -n "$actual_pid" ]; then + echo "$actual_pid" >"$BACKEND_PID" + log_success "Backend started (PID: $actual_pid)" else - log_ok "Backend started successfully (PID: $PID)" - echo " API Docs: http://localhost:8000/docs" - echo " Log: $BACKEND_LOG_FILE" + log_success "Backend started (PID: $daemon_pid)" fi + printf " ${C_BLUE}API:${C_RESET} http://localhost:%d\n" "$BACKEND_PORT" + printf " ${C_BLUE}Docs:${C_RESET} http://localhost:%d/docs\n" "$BACKEND_PORT" + printf " ${C_BLUE}Log:${C_RESET} %s\n" "$BACKEND_LOG" + return 0 else - log_error "Backend failed to start - port 8000 not listening" - log_info "Check log: $BACKEND_LOG_FILE" - echo "" - echo "[DEBUG] Last 10 lines of log:" - if [ -f "$BACKEND_LOG_FILE" ]; then - tail -n 10 "$BACKEND_LOG_FILE" - else - echo "Log file not found" - fi - rm -f "$BACKEND_PID_FILE" - exit 1 + log_error "Backend failed to start - port $BACKEND_PORT not listening" + log_info "Check logs: tail -f $BACKEND_LOG" + rm -f "$BACKEND_PID" + return 1 fi } -# Frontend start logic -start_frontend() { - # Check if already running - if [ -f "$FRONTEND_PID_FILE" ]; then - PID=$(cat "$FRONTEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - log_warn "Frontend is already running (PID: $PID)" - return 0 - else - log_info "Frontend PID file exists but process not running, cleaning up..." - rm -f "$FRONTEND_PID_FILE" - fi - fi - - # Check Node.js - if ! command -v node &> /dev/null; then - log_error "Node.js not found" - log_info "Please install Node.js from https://nodejs.org/" - exit 1 - fi - - # Check frontend directory - if [ ! -d "frontend" ]; then - log_error "Frontend directory not found" - exit 1 - fi - - # Check node_modules - if [ ! -d "frontend/node_modules" ]; then - log_info "Installing frontend dependencies..." - cd frontend - npm install - cd .. - fi - - log_info "Starting frontend service in background..." - - # Start frontend in background - cd frontend - nohup npm run dev > "$FRONTEND_LOG_FILE" 2>&1 & - PID=$! - cd .. - echo $PID > "$FRONTEND_PID_FILE" - - log_info "Waiting for frontend to start..." - sleep 3 - - # Check if port 3000 is listening - SERVICE_RUNNING=0 - for i in {1..10}; do - if lsof -i :3000 > /dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q :3000; then - SERVICE_RUNNING=1 - break - fi - sleep 1 - done - - if [ $SERVICE_RUNNING -eq 1 ]; then - # Get actual PID from port 3000 - ACTUAL_PID=$(lsof -ti :3000 2>/dev/null | head -n 1) - if [ -n "$ACTUAL_PID" ]; then - echo $ACTUAL_PID > "$FRONTEND_PID_FILE" - log_ok "Frontend started successfully (PID: $ACTUAL_PID)" - echo " URL: http://localhost:3000" - echo " Log: $FRONTEND_LOG_FILE" - else - log_ok "Frontend started successfully (PID: $PID)" - echo " URL: http://localhost:3000" - echo " Log: $FRONTEND_LOG_FILE" - fi - else - log_error "Frontend failed to start - port 3000 not listening" - log_info "Check log: $FRONTEND_LOG_FILE" - echo "" - echo "[DEBUG] Last 10 lines of log:" - if [ -f "$FRONTEND_LOG_FILE" ]; then - tail -n 10 "$FRONTEND_LOG_FILE" - else - echo "Log file not found" - fi - rm -f "$FRONTEND_PID_FILE" - exit 1 - fi -} - -# ============================================ -# STOP COMMAND -# ============================================ -stop_command() { - case $TARGET in - backend) - stop_backend - ;; - frontend) - stop_frontend - ;; - all) - echo "========================================" - echo "CheckIn App V2 - Stopping All Services" - echo "========================================" - echo "" - stop_backend - echo "" - stop_frontend - ;; - *) - log_error "Invalid target: $TARGET" - usage - ;; - esac -} - -# Backend stop logic stop_backend() { log_info "Stopping backend..." - # First try to kill by port - BACKEND_KILLED=0 - PIDS=$(lsof -ti :8000 2>/dev/null) - if [ -n "$PIDS" ]; then - for pid in $PIDS; do - kill -TERM $pid 2>/dev/null || kill -9 $pid 2>/dev/null - if [ $? -eq 0 ]; then - log_ok "Backend stopped (PID: $pid)" - BACKEND_KILLED=1 - fi - done - fi + local stopped=false - # Then try PID file if port method didn't work - if [ $BACKEND_KILLED -eq 0 ]; then - if [ -f "$BACKEND_PID_FILE" ]; then - PID=$(cat "$BACKEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - kill -TERM $PID 2>/dev/null || kill -9 $PID 2>/dev/null - if [ $? -eq 0 ]; then - log_ok "Backend stopped (PID: $PID)" - fi - else - log_warn "Backend not running (process does not exist)" - fi - else - log_warn "Backend not running (no process found)" + # Try to stop by port first + local pid + pid=$(get_pid_by_port "$BACKEND_PORT") + if [ -n "$pid" ]; then + if kill -TERM "$pid" 2>/dev/null; then + log_success "Backend stopped (PID: $pid)" + stopped=true fi fi - # Clean up PID file - rm -f "$BACKEND_PID_FILE" + # Try PID file if not stopped yet + if [ "$stopped" = "false" ]; then + pid=$(get_pid_from_file "$BACKEND_PID") + if [ -n "$pid" ] && is_process_alive "$pid"; then + if kill -TERM "$pid" 2>/dev/null; then + log_success "Backend stopped (PID: $pid)" + stopped=true + fi + fi + fi + + # Cleanup PID file + rm -f "$BACKEND_PID" + + if [ "$stopped" = "false" ]; then + log_warn "Backend not running" + fi + + return 0 +} + +status_backend() { + printf "\n${C_CYAN}[Backend Service]${C_RESET}\n" + + local pid + pid=$(get_pid_from_file "$BACKEND_PID") + + if [ -z "$pid" ] || ! is_process_alive "$pid"; then + printf " Status: ${C_RED}NOT RUNNING${C_RESET}\n" + rm -f "$BACKEND_PID" + return 1 + fi + + printf " Status: ${C_GREEN}RUNNING${C_RESET}\n" + printf " PID: %s\n" "$pid" + printf " URL: http://localhost:%d\n" "$BACKEND_PORT" + printf " Docs: http://localhost:%d/docs\n" "$BACKEND_PORT" + printf " Log: %s\n" "$BACKEND_LOG" + + # Show port info if lsof available + if command -v lsof &>/dev/null; then + local port_info + port_info=$(lsof -i ":$BACKEND_PORT" 2>/dev/null | grep LISTEN || echo "N/A") + printf " Port: %s\n" "$port_info" + fi + + return 0 +} + +# ============================================================================== +# Frontend Management +# ============================================================================== +start_frontend() { + log_info "Starting frontend service..." + + # Check if already running + local pid + pid=$(get_pid_from_file "$FRONTEND_PID") + if [ -n "$pid" ] && is_process_alive "$pid"; then + log_warn "Frontend already running (PID: $pid)" + return 0 + fi + + # Clean stale PID file + [ -f "$FRONTEND_PID" ] && rm -f "$FRONTEND_PID" + + # Verify Node.js exists + local node_bin + node_bin=$(find_node) + if [ $? -ne 0 ]; then + log_error "Node.js not found" + log_info "Install from: https://nodejs.org/" + return 1 + fi + + # Check Node.js version + if ! check_node_version "$node_bin"; then + local node_version + node_version=$($node_bin --version 2>/dev/null) + log_error "Node.js version $node_version is too old" + log_error "Vite requires Node.js 20.19+ or 22.12+" + log_info "Upgrade: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -" + log_info "Then: sudo apt-get install -y nodejs" + return 1 + fi + + # Verify frontend directory + if [ ! -d "${APP_DIR}/frontend" ]; then + log_error "Frontend directory not found" + return 1 + fi + + # Install dependencies if needed + if [ ! -d "${APP_DIR}/frontend/node_modules" ]; then + log_info "Installing frontend dependencies..." + (cd "${APP_DIR}/frontend" && npm install) + fi + + # Start frontend dev server + log_info "Launching frontend dev server..." + (cd "${APP_DIR}/frontend" && nohup npm run dev >"$FRONTEND_LOG" 2>&1 & echo $! >&3) 3>"$FRONTEND_PID" + + # Read PID from file + local npm_pid + npm_pid=$(cat "$FRONTEND_PID" 2>/dev/null || echo "unknown") + + # Wait for service to be ready + log_info "Waiting for frontend to be ready..." + if wait_for_port "$FRONTEND_PORT" 15; then + # Update PID with actual process on port + local actual_pid + actual_pid=$(get_pid_by_port "$FRONTEND_PORT") + if [ -n "$actual_pid" ]; then + echo "$actual_pid" >"$FRONTEND_PID" + log_success "Frontend started (PID: $actual_pid)" + else + log_success "Frontend started (PID: $npm_pid)" + fi + printf " ${C_BLUE}URL:${C_RESET} http://localhost:%d\n" "$FRONTEND_PORT" + printf " ${C_BLUE}Log:${C_RESET} %s\n" "$FRONTEND_LOG" + return 0 + else + log_error "Frontend failed to start - port $FRONTEND_PORT not listening" + + # Show last 10 lines of log for debugging + if [ -f "$FRONTEND_LOG" ]; then + echo "" + log_warn "Last 10 lines from log:" + echo "----------------------------------------" + tail -n 10 "$FRONTEND_LOG" + echo "----------------------------------------" + fi + + log_info "Full log: tail -f $FRONTEND_LOG" + rm -f "$FRONTEND_PID" + return 1 + fi } -# Frontend stop logic stop_frontend() { log_info "Stopping frontend..." - # First try to kill by port - FRONTEND_KILLED=0 + local stopped=false - # Check port 3000 - PIDS=$(lsof -ti :3000 2>/dev/null) - if [ -n "$PIDS" ]; then - for pid in $PIDS; do - kill -TERM $pid 2>/dev/null || kill -9 $pid 2>/dev/null - if [ $? -eq 0 ]; then - log_ok "Frontend stopped (PID: $pid)" - FRONTEND_KILLED=1 - fi - done - fi - - # Also check ports 3001-3010 (Vite fallback ports) - if [ $FRONTEND_KILLED -eq 0 ]; then - for port in {3001..3010}; do - PIDS=$(lsof -ti :$port 2>/dev/null) - if [ -n "$PIDS" ]; then - for pid in $PIDS; do - # Check if it's a node process - if ps -p $pid -o comm= | grep -q node; then - kill -TERM $pid 2>/dev/null || kill -9 $pid 2>/dev/null - if [ $? -eq 0 ]; then - log_ok "Frontend stopped (PID: $pid, Port: $port)" - FRONTEND_KILLED=1 - fi - fi - done - fi - done - fi - - # Then try PID file if port method didn't work - if [ $FRONTEND_KILLED -eq 0 ]; then - if [ -f "$FRONTEND_PID_FILE" ]; then - PID=$(cat "$FRONTEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - kill -TERM $PID 2>/dev/null || kill -9 $PID 2>/dev/null - if [ $? -eq 0 ]; then - log_ok "Frontend stopped (PID: $PID)" + # Try common Vite ports (3000-3010) + for port in $(seq 3000 3010); do + local pid + pid=$(get_pid_by_port "$port") + if [ -n "$pid" ]; then + # Verify it's a node process + if ps -p "$pid" -o comm= 2>/dev/null | grep -q node; then + if kill -TERM "$pid" 2>/dev/null; then + log_success "Frontend stopped (PID: $pid, Port: $port)" + stopped=true fi - else - log_warn "Frontend not running (process does not exist)" fi - else - log_warn "Frontend not running (no process found)" + fi + done + + # Try PID file if not stopped yet + if [ "$stopped" = "false" ]; then + local pid + pid=$(get_pid_from_file "$FRONTEND_PID") + if [ -n "$pid" ] && is_process_alive "$pid"; then + if kill -TERM "$pid" 2>/dev/null; then + log_success "Frontend stopped (PID: $pid)" + stopped=true + fi fi fi - # Clean up PID file - rm -f "$FRONTEND_PID_FILE" -} + # Cleanup PID file + rm -f "$FRONTEND_PID" -# ============================================ -# RESTART COMMAND -# ============================================ -restart_command() { - log_info "Restarting $TARGET..." - echo "" - stop_command - sleep 2 - start_command -} - -# ============================================ -# STATUS COMMAND -# ============================================ -status_command() { - echo "========================================" - echo "CheckIn App V2 - Service Status" - echo "========================================" - echo "" - - case $TARGET in - backend) - status_backend - ;; - frontend) - status_frontend - ;; - all) - status_backend - echo "" - status_frontend - ;; - *) - log_error "Invalid target: $TARGET" - usage - ;; - esac -} - -# Backend status -status_backend() { - echo "[Backend Service]" - - if [ ! -f "$BACKEND_PID_FILE" ]; then - echo " Status: NOT RUNNING" - return 0 + if [ "$stopped" = "false" ]; then + log_warn "Frontend not running" fi - PID=$(cat "$BACKEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - echo " Status: RUNNING" - echo " PID: $PID" - echo " URL: http://localhost:8000/docs" - echo " Log: $BACKEND_LOG_FILE" - echo " Port: $(lsof -i :8000 2>/dev/null | grep LISTEN || echo 'N/A')" - else - echo " Status: NOT RUNNING" - rm -f "$BACKEND_PID_FILE" - fi + return 0 } -# Frontend status status_frontend() { - echo "[Frontend Service]" + printf "\n${C_CYAN}[Frontend Service]${C_RESET}\n" - if [ ! -f "$FRONTEND_PID_FILE" ]; then - echo " Status: NOT RUNNING" - return 0 + local pid + pid=$(get_pid_from_file "$FRONTEND_PID") + + if [ -z "$pid" ] || ! is_process_alive "$pid"; then + printf " Status: ${C_RED}NOT RUNNING${C_RESET}\n" + rm -f "$FRONTEND_PID" + return 1 fi - PID=$(cat "$FRONTEND_PID_FILE") - if ps -p $PID > /dev/null 2>&1; then - echo " Status: RUNNING" - echo " PID: $PID" - echo " URL: http://localhost:3000" - echo " Log: $FRONTEND_LOG_FILE" - echo " Port: $(lsof -i :3000 2>/dev/null | grep LISTEN || echo 'N/A')" - else - echo " Status: NOT RUNNING" - rm -f "$FRONTEND_PID_FILE" + printf " Status: ${C_GREEN}RUNNING${C_RESET}\n" + printf " PID: %s\n" "$pid" + printf " URL: http://localhost:%d\n" "$FRONTEND_PORT" + printf " Log: %s\n" "$FRONTEND_LOG" + + # Show port info if lsof available + if command -v lsof &>/dev/null; then + local port_info + port_info=$(lsof -i ":$FRONTEND_PORT" 2>/dev/null | grep LISTEN || echo "N/A") + printf " Port: %s\n" "$port_info" fi + + return 0 } -# ============================================ -# BUILD COMMAND -# ============================================ -build_command() { - echo "========================================" - echo "CheckIn App V2 - Building Frontend" - echo "========================================" - echo "" +# ============================================================================== +# Build Command +# ============================================================================== +build_frontend() { + print_header "CheckIn App V2 - Building Frontend" - # Check Node.js - if ! command -v node &> /dev/null; then + # Verify Node.js exists + local node_bin + node_bin=$(find_node) + if [ $? -ne 0 ]; then log_error "Node.js not found" - log_info "Please install Node.js from https://nodejs.org/" - exit 1 + log_info "Install from: https://nodejs.org/" + return 1 fi - # Check frontend directory - if [ ! -d "frontend" ]; then + # Check Node.js version + if ! check_node_version "$node_bin"; then + local node_version + node_version=$($node_bin --version 2>/dev/null) + log_error "Node.js version $node_version is too old" + log_error "Vite requires Node.js 20.19+ or 22.12+" + log_info "Upgrade: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -" + log_info "Then: sudo apt-get install -y nodejs" + return 1 + fi + + # Verify frontend directory + if [ ! -d "${APP_DIR}/frontend" ]; then log_error "Frontend directory not found" - exit 1 + return 1 fi - # Check node_modules - if [ ! -d "frontend/node_modules" ]; then - log_info "Installing frontend dependencies first..." - cd frontend - npm install - if [ $? -ne 0 ]; then + # Install dependencies if needed + if [ ! -d "${APP_DIR}/frontend/node_modules" ]; then + log_info "Installing dependencies first..." + (cd "${APP_DIR}/frontend" && npm install) || { log_error "Failed to install dependencies" - exit 1 - fi - cd .. - echo "" + return 1 + } + echo fi log_info "Building frontend for production..." - echo "" + echo - # Build frontend - cd frontend - npm run build - BUILD_EXIT_CODE=$? - cd .. + # Build + (cd "${APP_DIR}/frontend" && npm run build) + local exit_code=$? - if [ $BUILD_EXIT_CODE -eq 0 ]; then - echo "" - log_ok "Frontend built successfully!" + if [ $exit_code -eq 0 ]; then + echo + log_success "Frontend built successfully!" - # Check if dist directory exists - if [ -d "frontend/dist" ]; then - DIST_SIZE=$(du -sh frontend/dist | cut -f1) - echo "" - echo "Build output:" - echo " Location: $APP_DIR/frontend/dist" - echo " Size: $DIST_SIZE" - echo "" - echo "File structure:" - ls -lh frontend/dist/ - echo "" - log_info "You can now deploy the 'frontend/dist' directory to your web server" + # Show build info + if [ -d "${APP_DIR}/frontend/dist" ]; then + local dist_size + dist_size=$(du -sh "${APP_DIR}/frontend/dist" 2>/dev/null | cut -f1 || echo "unknown") + + echo + printf "${C_CYAN}Build Output:${C_RESET}\n" + printf " Location: %s/frontend/dist\n" "$APP_DIR" + printf " Size: %s\n" "$dist_size" + echo + printf "${C_CYAN}File Structure:${C_RESET}\n" + ls -lh "${APP_DIR}/frontend/dist/" 2>/dev/null || echo " (unable to list files)" + echo + log_info "Deploy 'frontend/dist' to your web server" else log_warn "Build succeeded but dist directory not found" fi + return 0 else - echo "" + echo log_error "Frontend build failed" - log_info "Check the output above for error details" - exit 1 + log_info "Check output above for details" + return 1 fi } -# ============================================ -# LOG COMMAND -# ============================================ -log_command() { - case $TARGET in +# ============================================================================== +# Command Handlers +# ============================================================================== +cmd_start() { + local target="${1:-all}" + + case "$target" in backend) - echo "========================================" - echo "Backend Real-time Logs (Press Ctrl+C to exit)" - echo "========================================" - echo "" - - if [ ! -f "$BACKEND_LOG_FILE" ]; then - log_error "Log file does not exist: $BACKEND_LOG_FILE" - exit 1 - fi - - tail -f "$BACKEND_LOG_FILE" + print_header "CheckIn App V2 - Starting Backend" + start_backend ;; frontend) - echo "========================================" - echo "Frontend Real-time Logs (Press Ctrl+C to exit)" - echo "========================================" - echo "" - - if [ ! -f "$FRONTEND_LOG_FILE" ]; then - log_error "Log file does not exist: $FRONTEND_LOG_FILE" - exit 1 - fi - - tail -f "$FRONTEND_LOG_FILE" + print_header "CheckIn App V2 - Starting Frontend" + start_frontend ;; all) - log_error "Cannot tail multiple logs simultaneously" - log_info "Use: ./manage.sh log backend OR ./manage.sh log frontend" - usage + print_header "CheckIn App V2 - Starting All Services" + echo + start_backend + echo + start_frontend + echo + printf "${C_GREEN}========================================${C_RESET}\n" + printf "${C_GREEN}All Services Started!${C_RESET}\n" + printf "${C_GREEN}========================================${C_RESET}\n" + echo + printf "Backend API: http://localhost:%d\n" "$BACKEND_PORT" + printf "API Docs: http://localhost:%d/docs\n" "$BACKEND_PORT" + printf "Frontend App: http://localhost:%d\n" "$FRONTEND_PORT" + echo ;; *) - log_error "Invalid target: $TARGET" - usage + log_error "Invalid target: $target" + show_usage + return 1 ;; esac } -# ============================================ -# USAGE -# ============================================ -usage() { - echo "CheckIn App V2 - Unified Process Manager" - echo "" - echo "Usage: $0 COMMAND [TARGET]" - echo "" - echo "Commands:" - echo " start [TARGET] - Start service(s)" - echo " stop [TARGET] - Stop service(s)" - echo " restart [TARGET] - Restart service(s)" - echo " status [TARGET] - View service(s) status" - echo " log TARGET - View real-time logs (backend or frontend only)" - echo " build - Build frontend for production" - echo "" - echo "Targets:" - echo " backend - Backend API service (default port: 8000)" - echo " frontend - Frontend dev server (default port: 3000)" - echo " all - Both services (default)" - echo "" - echo "Examples:" - echo " $0 start # Start both services" - echo " $0 start backend # Start backend only" - echo " $0 stop all # Stop all services" - echo " $0 status # View all services status" - echo " $0 log backend # View backend logs" - echo " $0 build # Build frontend static files" - echo " $0 restart frontend # Restart frontend" - exit 1 +cmd_stop() { + local target="${1:-all}" + + case "$target" in + backend) + print_header "CheckIn App V2 - Stopping Backend" + stop_backend + ;; + frontend) + print_header "CheckIn App V2 - Stopping Frontend" + stop_frontend + ;; + all) + print_header "CheckIn App V2 - Stopping All Services" + echo + stop_backend + echo + stop_frontend + ;; + *) + log_error "Invalid target: $target" + show_usage + return 1 + ;; + esac } -# ============================================ -# MAIN -# ============================================ -if [ -z "$COMMAND" ]; then - usage -fi +cmd_restart() { + local target="${1:-all}" -case $COMMAND in - start) - start_command - ;; - stop) - stop_command - ;; - restart) - restart_command - ;; - status) - status_command - ;; - log) - log_command - ;; - build) - build_command - ;; - *) - usage - ;; -esac + log_info "Restarting $target..." + echo + cmd_stop "$target" + sleep 2 + cmd_start "$target" +} -exit 0 +cmd_status() { + local target="${1:-all}" + + print_header "CheckIn App V2 - Service Status" + + case "$target" in + backend) + status_backend || true + ;; + frontend) + status_frontend || true + ;; + all) + status_backend || true + status_frontend || true + ;; + *) + log_error "Invalid target: $target" + show_usage + return 1 + ;; + esac + echo +} + +cmd_log() { + local target="${1:-}" + + if [ -z "$target" ] || [ "$target" = "all" ]; then + log_error "Cannot tail multiple logs simultaneously" + log_info "Use: $0 log backend OR $0 log frontend" + return 1 + fi + + case "$target" in + backend) + if [ ! -f "$BACKEND_LOG" ]; then + log_error "Log file not found: $BACKEND_LOG" + return 1 + fi + print_header "Backend Real-time Logs (Press Ctrl+C to exit)" + echo + tail -f "$BACKEND_LOG" + ;; + frontend) + if [ ! -f "$FRONTEND_LOG" ]; then + log_error "Log file not found: $FRONTEND_LOG" + return 1 + fi + print_header "Frontend Real-time Logs (Press Ctrl+C to exit)" + echo + tail -f "$FRONTEND_LOG" + ;; + *) + log_error "Invalid target: $target" + log_info "Use: backend or frontend" + return 1 + ;; + esac +} + +# ============================================================================== +# Help and Usage +# ============================================================================== +show_usage() { + echo + printf "${C_CYAN}CheckIn App V2 - Unified Service Manager${C_RESET}\n" + echo + printf "${C_YELLOW}USAGE:${C_RESET}\n" + echo " \$0 COMMAND [TARGET]" + echo + printf "${C_YELLOW}COMMANDS:${C_RESET}\n" + echo " start [TARGET] - Start service(s)" + echo " stop [TARGET] - Stop service(s)" + echo " restart [TARGET] - Restart service(s)" + echo " status [TARGET] - View service status" + echo " log TARGET - View real-time logs (backend or frontend)" + echo " build - Build frontend for production" + echo + printf "${C_YELLOW}TARGETS:${C_RESET}\n" + echo " backend - Backend API service (port $BACKEND_PORT)" + echo " frontend - Frontend dev server (port $FRONTEND_PORT)" + echo " all - Both services (default)" + echo + printf "${C_YELLOW}EXAMPLES:${C_RESET}\n" + echo " \$0 start # Start both services" + echo " \$0 start backend # Start backend only" + echo " \$0 stop all # Stop all services" + echo " \$0 status # View all service status" + echo " \$0 log backend # View backend logs" + echo " \$0 build # Build frontend static files" + echo " \$0 restart frontend # Restart frontend" + echo +} + +# ============================================================================== +# Main Entry Point +# ============================================================================== +main() { + local command="${1:-}" + local target="${2:-all}" + + if [ -z "$command" ]; then + show_usage + exit 1 + fi + + case "$command" in + start) + cmd_start "$target" + ;; + stop) + cmd_stop "$target" + ;; + restart) + cmd_restart "$target" + ;; + status) + cmd_status "$target" + ;; + log) + cmd_log "$target" + ;; + build) + build_frontend + ;; + help|--help|-h) + show_usage + exit 0 + ;; + *) + log_error "Unknown command: $command" + show_usage + exit 1 + ;; + esac +} + +# Run main function +main "$@" diff --git a/nginx.conf.example b/nginx.conf.example new file mode 100644 index 0000000..7d34cc9 --- /dev/null +++ b/nginx.conf.example @@ -0,0 +1,143 @@ +# ============================================================================== +# CheckIn App V2 - Nginx Configuration Example (HTTP Only) +# ============================================================================== +# +# Usage: +# 1. Copy this file: sudo cp nginx.conf.example /etc/nginx/sites-available/checkin-app +# 2. Edit the file and replace placeholders with your actual values +# 3. Create symlink: sudo ln -s /etc/nginx/sites-available/checkin-app /etc/nginx/sites-enabled/ +# 4. Test config: sudo nginx -t +# 5. Reload Nginx: sudo systemctl reload nginx +# +# ============================================================================== + +# HTTP Server - Production Configuration +server { + listen 80; + listen [::]:80; + server_name your-domain.com; # CHANGE THIS: Replace with your domain or IP + + # CHANGE THIS: Replace with your actual frontend build path + # Example: /home/username/CheckInApp/frontend/dist + root /path/to/CheckInApp/frontend/dist; + index index.html; + + # Access and Error Logs + access_log /var/log/nginx/checkin-app-access.log; + error_log /var/log/nginx/checkin-app-error.log; + + # Gzip Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/json application/javascript; + + # Client body size (for file uploads) + client_max_body_size 10M; + + # ========================================== + # API Proxy Configuration + # ========================================== + location /api/ { + # Proxy to backend FastAPI server + proxy_pass http://127.0.0.1:8000; + + # Proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # HTTP version and buffering + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # API Documentation endpoints + location ~ ^/(docs|redoc|openapi.json|health) { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # ========================================== + # Frontend Static Files + # ========================================== + + # Cache static assets (JS, CSS, images, fonts) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Frontend routes (SPA - Single Page Application) + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Favicon + location = /favicon.ico { + log_not_found off; + access_log off; + } + + # Robots.txt + location = /robots.txt { + log_not_found off; + access_log off; + } +} + +# ============================================================================== +# HTTPS Configuration (Currently Commented Out) +# ============================================================================== +# Uncomment and configure below when you need HTTPS with SSL certificate +# +# server { +# listen 80; +# listen [::]:80; +# server_name your-domain.com; +# +# # Redirect HTTP to HTTPS +# return 301 https://$server_name$request_uri; +# } +# +# server { +# listen 443 ssl http2; +# listen [::]:443 ssl http2; +# server_name your-domain.com; +# +# # SSL Certificate (use Let's Encrypt: certbot --nginx) +# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; +# +# # SSL Configuration +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; +# ssl_prefer_server_ciphers off; +# ssl_session_cache shared:SSL:10m; +# ssl_session_timeout 10m; +# +# # Security Headers +# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +# add_header X-Frame-Options "SAMEORIGIN" always; +# add_header X-Content-Type-Options "nosniff" always; +# +# # CHANGE THIS: Replace with your actual frontend build path +# root /path/to/CheckInApp/frontend/dist; +# index index.html; +# +# # ... (rest of the configuration same as HTTP version above) +# }