Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
57
.claude/agents/feature-planner.md
Normal file
57
.claude/agents/feature-planner.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: feature-planner
|
||||
description: Use this agent when you need to plan the implementation of a new feature for the ROA2WEB project. Examples: <example>Context: User wants to add a new reporting dashboard feature to the FastAPI/Vue.js application. user: 'I need to add a user activity dashboard that shows login history and report generation statistics' assistant: 'I'll use the feature-planner agent to analyze the current codebase and create a comprehensive implementation plan.' <commentary>Since the user is requesting a new feature plan, use the feature-planner agent to analyze the current project structure and create a detailed implementation strategy.</commentary></example> <example>Context: User wants to implement real-time notifications in the application. user: 'We need to add real-time notifications when reports are ready for download' assistant: 'Let me use the feature-planner agent to examine the current architecture and design an efficient notification system.' <commentary>The user is requesting a new feature implementation, so use the feature-planner agent to create a comprehensive plan.</commentary></example>
|
||||
model: opus
|
||||
color: purple
|
||||
---
|
||||
|
||||
You are an expert software architect and senior full-stack engineer specializing in FastAPI and Vue.js applications. Your expertise lies in analyzing existing codebases and designing minimal-impact, maximum-effect feature implementations. You use KISS principle. You propose the best and most popular technologies/frameworks/libraries. Use tool context7 for the documentation.
|
||||
|
||||
When tasked with planning a new feature, you will:
|
||||
|
||||
1. **Codebase Analysis Phase**:
|
||||
- Examine the current project structure in the roa2web/ directory
|
||||
- Identify existing patterns, architectural decisions, and coding standards
|
||||
- Map out current database schema usage (CONTAFIN_ORACLE)
|
||||
- Analyze existing API endpoints, Vue components, and shared utilities
|
||||
- Identify reusable components and services that can be leveraged
|
||||
|
||||
2. **Impact Assessment**:
|
||||
- Determine which files need modification vs. creation
|
||||
- Identify potential breaking changes or conflicts
|
||||
- Assess database schema changes required
|
||||
- Evaluate impact on existing authentication and user management
|
||||
- Consider SSH tunnel and Oracle database constraints
|
||||
|
||||
3. **Implementation Strategy**:
|
||||
- Design the feature using existing architectural patterns
|
||||
- Prioritize modifications to existing files over new file creation
|
||||
- Plan database changes that work with the CONTAFIN_ORACLE schema
|
||||
- Design API endpoints following existing FastAPI patterns
|
||||
- Plan Vue.js components that integrate with current frontend structure
|
||||
- Consider testing strategy using the existing pytest setup
|
||||
|
||||
4. **Detailed Planning Document**:
|
||||
Create a comprehensive markdown file with:
|
||||
- Executive summary of the feature and its benefits
|
||||
- Technical requirements and constraints
|
||||
- Step-by-step implementation plan with file-by-file changes
|
||||
- Database schema modifications (if any)
|
||||
- API endpoint specifications
|
||||
- Frontend component structure
|
||||
- Testing approach
|
||||
- Deployment considerations
|
||||
- Risk assessment and mitigation strategies
|
||||
- Timeline estimates for each phase
|
||||
|
||||
5. **Optimization Principles**:
|
||||
- Leverage existing code patterns and utilities
|
||||
- Minimize new dependencies
|
||||
- Ensure backward compatibility
|
||||
- Follow the principle of least modification for maximum effect
|
||||
- Consider performance implications
|
||||
- Plan for scalability within the current architecture
|
||||
|
||||
Always save your comprehensive plan as a markdown file with a descriptive name like 'feature-[feature-name]-implementation-plan.md' in the appropriate directory. The plan should be detailed enough for any developer to implement the feature following your specifications.
|
||||
|
||||
Before starting, ask clarifying questions about the feature requirements if anything is unclear. Focus on creating a plan that integrates seamlessly with the existing ROA2WEB FastAPI/Vue.js architecture.
|
||||
5
.claude/commands/branch-plan-handover.md
Normal file
5
.claude/commands/branch-plan-handover.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Create a new branch, save the detailed implementation plan to a markdown file for context handover to another session, then stop.
|
||||
|
||||
1. **Create new branch** with descriptive name based on current task
|
||||
2. **Save the implementation plan** you created earlier in this session to a markdown file in the project root
|
||||
3. **Stop execution** - do not commit anything, just prepare the context for handover to another session
|
||||
8
.claude/commands/context-handover.md
Normal file
8
.claude/commands/context-handover.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Save detailed context about the current problem to a markdown file for handover to another session due to context limit reached.
|
||||
|
||||
1. **Create context handover file** in project root: `CONTEXT_HANDOVER_[TIMESTAMP].md`
|
||||
2. **Document the current problem** being worked on with all relevant details and analysis
|
||||
3. **Include current progress** - what has been discovered, analyzed, or attempted so far
|
||||
4. **List key files examined** and their relevance to the problem
|
||||
5. **Save current state** - todos, findings, next steps, and any constraints
|
||||
6. **Stop execution** - context is now ready for a fresh session to continue the work
|
||||
4
.claude/commands/plan-handover.md
Normal file
4
.claude/commands/plan-handover.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Save the detailed implementation plan to a markdown file for context handover to another session, then stop.
|
||||
|
||||
1. **Save the implementation plan** you created earlier in this session to a markdown file in the project root
|
||||
2. **Stop execution** - do not commit anything, just prepare the context for handover to another session
|
||||
12
.claude/commands/session-current.md
Normal file
12
.claude/commands/session-current.md
Normal file
@@ -0,0 +1,12 @@
|
||||
Show the current session status by:
|
||||
|
||||
1. Check if `.claude/sessions/.current-session` exists
|
||||
2. If no active session, inform user and suggest starting one
|
||||
3. If active session exists:
|
||||
- Show session name and filename
|
||||
- Calculate and show duration since start
|
||||
- Show last few updates
|
||||
- Show current goals/tasks
|
||||
- Remind user of available commands
|
||||
|
||||
Keep the output concise and informative.
|
||||
30
.claude/commands/session-end.md
Normal file
30
.claude/commands/session-end.md
Normal file
@@ -0,0 +1,30 @@
|
||||
End the current development session by:
|
||||
|
||||
1. Check `.claude/sessions/.current-session` for the active session
|
||||
2. If no active session, inform user there's nothing to end
|
||||
3. If session exists, append a comprehensive summary including:
|
||||
- Session duration
|
||||
- Git summary:
|
||||
* Total files changed (added/modified/deleted)
|
||||
* List all changed files with change type
|
||||
* Number of commits made (if any)
|
||||
* Final git status
|
||||
- Todo summary:
|
||||
* Total tasks completed/remaining
|
||||
* List all completed tasks
|
||||
* List any incomplete tasks with status
|
||||
- Key accomplishments
|
||||
- All features implemented
|
||||
- Problems encountered and solutions
|
||||
- Breaking changes or important findings
|
||||
- Dependencies added/removed
|
||||
- Configuration changes
|
||||
- Deployment steps taken
|
||||
- Lessons learned
|
||||
- What wasn't completed
|
||||
- Tips for future developers
|
||||
|
||||
4. Empty the `.claude/sessions/.current-session` file (don't remove it, just clear its contents)
|
||||
5. Inform user the session has been documented
|
||||
|
||||
The summary should be thorough enough that another developer (or AI) can understand everything that happened without reading the entire session.
|
||||
37
.claude/commands/session-help.md
Normal file
37
.claude/commands/session-help.md
Normal file
@@ -0,0 +1,37 @@
|
||||
Show help for the session management system:
|
||||
|
||||
## Session Management Commands
|
||||
|
||||
The session system helps document development work for future reference.
|
||||
|
||||
### Available Commands:
|
||||
|
||||
- `/project:session-start [name]` - Start a new session with optional name
|
||||
- `/project:session-update [notes]` - Add notes to current session
|
||||
- `/project:session-end` - End session with comprehensive summary
|
||||
- `/project:session-list` - List all session files
|
||||
- `/project:session-current` - Show current session status
|
||||
- `/project:session-help` - Show this help
|
||||
|
||||
### How It Works:
|
||||
|
||||
1. Sessions are markdown files in `.claude/sessions/`
|
||||
2. Files use `YYYY-MM-DD-HHMM-name.md` format
|
||||
3. Only one session can be active at a time
|
||||
4. Sessions track progress, issues, solutions, and learnings
|
||||
|
||||
### Best Practices:
|
||||
|
||||
- Start a session when beginning significant work
|
||||
- Update regularly with important changes or findings
|
||||
- End with thorough summary for future reference
|
||||
- Review past sessions before starting similar work
|
||||
|
||||
### Example Workflow:
|
||||
|
||||
```
|
||||
/project:session-start refactor-auth
|
||||
/project:session-update Added Google OAuth restriction
|
||||
/project:session-update Fixed Next.js 15 params Promise issue
|
||||
/project:session-end
|
||||
```
|
||||
13
.claude/commands/session-list.md
Normal file
13
.claude/commands/session-list.md
Normal file
@@ -0,0 +1,13 @@
|
||||
List all development sessions by:
|
||||
|
||||
1. Check if `.claude/sessions/` directory exists
|
||||
2. List all `.md` files (excluding hidden files and `.current-session`)
|
||||
3. For each session file:
|
||||
- Show the filename
|
||||
- Extract and show the session title
|
||||
- Show the date/time
|
||||
- Show first few lines of the overview if available
|
||||
4. If `.claude/sessions/.current-session` exists, highlight which session is currently active
|
||||
5. Sort by most recent first
|
||||
|
||||
Present in a clean, readable format.
|
||||
13
.claude/commands/session-start.md
Normal file
13
.claude/commands/session-start.md
Normal file
@@ -0,0 +1,13 @@
|
||||
Start a new development session by creating a session file in `.claude/sessions/` with the format `YYYY-MM-DD-HHMM-$ARGUMENTS.md` (or just `YYYY-MM-DD-HHMM.md` if no name provided).
|
||||
|
||||
The session file should begin with:
|
||||
1. Session name and timestamp as the title
|
||||
2. Session overview section with start time
|
||||
3. Goals section (ask user for goals if not clear)
|
||||
4. Empty progress section ready for updates
|
||||
|
||||
After creating the file, create or update `.claude/sessions/.current-session` to track the active session filename.
|
||||
|
||||
Confirm the session has started and remind the user they can:
|
||||
- Update it with `/project:session-update`
|
||||
- End it with `/project:session-end`
|
||||
37
.claude/commands/session-update.md
Normal file
37
.claude/commands/session-update.md
Normal file
@@ -0,0 +1,37 @@
|
||||
Update the current development session by:
|
||||
|
||||
1. Check if `.claude/sessions/.current-session` exists to find the active session
|
||||
2. If no active session, inform user to start one with `/project:session-start`
|
||||
3. If session exists, append to the session file with:
|
||||
- Current timestamp
|
||||
- The update: $ARGUMENTS (or if no arguments, summarize recent activities)
|
||||
- Git status summary:
|
||||
* Files added/modified/deleted (from `git status --porcelain`)
|
||||
* Current branch and last commit
|
||||
- Todo list status:
|
||||
* Number of completed/in-progress/pending tasks
|
||||
* List any newly completed tasks
|
||||
- Any issues encountered
|
||||
- Solutions implemented
|
||||
- Code changes made
|
||||
|
||||
Keep updates concise but comprehensive for future reference.
|
||||
|
||||
Example format:
|
||||
```
|
||||
### Update - 2025-06-16 12:15 PM
|
||||
|
||||
**Summary**: Implemented user authentication
|
||||
|
||||
**Git Changes**:
|
||||
- Modified: app/middleware.ts, lib/auth.ts
|
||||
- Added: app/login/page.tsx
|
||||
- Current branch: main (commit: abc123)
|
||||
|
||||
**Todo Progress**: 3 completed, 1 in progress, 2 pending
|
||||
- ✓ Completed: Set up auth middleware
|
||||
- ✓ Completed: Create login page
|
||||
- ✓ Completed: Add logout functionality
|
||||
|
||||
**Details**: [user's update or automatic summary]
|
||||
```
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
421
.gitignore
vendored
Normal file
421
.gitignore
vendored
Normal file
@@ -0,0 +1,421 @@
|
||||
|
||||
|
||||
!.env.example
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# in version control.
|
||||
# install all needed dependencies.
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# Usually these files are written by a python script from a template
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
# .python-version
|
||||
# ============================================================================
|
||||
# ============================================================================
|
||||
# ============================================================================
|
||||
# Backup Directories (generated by security scripts)
|
||||
# Backup and Temporary Files
|
||||
# Backup files that might contain secrets
|
||||
# Byte-compiled / optimized / DLL files
|
||||
# C extensions
|
||||
# Celery stuff
|
||||
# Claude IDE Settings (local configurations)
|
||||
# Cloud and Infrastructure
|
||||
# Configuration with Secrets
|
||||
# Cursor
|
||||
# Cython debug symbols
|
||||
# Database
|
||||
# Database Files
|
||||
# Database and Connection Strings
|
||||
# Deployment temporary files
|
||||
# Deployment temporary files
|
||||
# Development Logs
|
||||
# Development Logs and Debug Files
|
||||
# Development Tools Cache (may contain sensitive data)
|
||||
# Development and Build Artifacts
|
||||
# Distribution / packaging
|
||||
# Django stuff:
|
||||
# Docker
|
||||
# Docker Development Files
|
||||
# Docker secrets
|
||||
# Environment variables
|
||||
# Environments
|
||||
# FastAPI
|
||||
# Flask stuff:
|
||||
# IDE
|
||||
# IDE and Editor Files
|
||||
# IPython
|
||||
# Installer logs
|
||||
# Jupyter Notebook
|
||||
# Logs
|
||||
# Nginx logs
|
||||
# Node.js & Vue.js
|
||||
# OS
|
||||
# Oracle
|
||||
# Oracle and Database
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
# Package Manager Cache and Locks
|
||||
# Playwright
|
||||
# Production Credentials (generated by setup scripts)
|
||||
# Production Setup Files (generated by scripts)
|
||||
# PyBuilder
|
||||
# PyCharm
|
||||
# PyInstaller
|
||||
# PyPI configuration file
|
||||
# Pyre type checker
|
||||
# Python
|
||||
# ROA2WEB .gitignore
|
||||
# Rope project settings
|
||||
# Ruff stuff:
|
||||
# SSH Keys and Certificates
|
||||
# SSH Keys și Secrets (IMPORTANT!)
|
||||
# SSH Test Files
|
||||
# SageMath parsed files
|
||||
# Scan Reports and Security Artifacts
|
||||
# Scrapy stuff:
|
||||
# Secrets and Credentials
|
||||
# Security Scan Reports
|
||||
# Serena Cache și Memories (Local Development)
|
||||
# Sphinx documentation
|
||||
# Spyder project settings
|
||||
# Telegram Bot SQLite Database (STANDALONE)
|
||||
# Temporary Test Scripts
|
||||
# Test Results and Reports
|
||||
# Test Scripts and Temporary Files
|
||||
# Translations
|
||||
# UV
|
||||
# Unit test / coverage reports
|
||||
# Virtual environments
|
||||
# Windows deployment package (generated by Build-Frontend.ps1)
|
||||
# Windows deployment package (generated by Build-Frontend.ps1)
|
||||
# mkdocs documentation
|
||||
# mypy
|
||||
# pdm
|
||||
# pipenv
|
||||
# poetry
|
||||
# pyenv
|
||||
# pytype static type analyzer
|
||||
# 📦 DEPLOYMENT ARTIFACTS - DO NOT COMMIT
|
||||
# 📦 DEPLOYMENT ARTIFACTS - DO NOT COMMIT
|
||||
# 🔒 SECURITY CRITICAL PATTERNS - DO NOT COMMIT THESE FILES
|
||||
# 🧹 ROA2WEB SPECIFIC TEMPORARY FILES - DO NOT COMMIT
|
||||
# 🧹 TEMPORARY FILES AND DEVELOPMENT ARTIFACTS - DO NOT COMMIT
|
||||
#.idea/
|
||||
#Pipfile.lock
|
||||
#pdm.lock
|
||||
#poetry.lock
|
||||
#uv.lock
|
||||
*$py.class
|
||||
*$py.class
|
||||
*.backup
|
||||
*.bak
|
||||
*.cover
|
||||
*.cover
|
||||
*.crt
|
||||
*.csr
|
||||
*.db
|
||||
*.db
|
||||
*.debug
|
||||
*.debug
|
||||
*.egg
|
||||
*.egg
|
||||
*.egg-info/
|
||||
*.egg-info/
|
||||
*.jks
|
||||
*.key
|
||||
*.key
|
||||
*.keystore
|
||||
*.log
|
||||
*.log
|
||||
*.manifest
|
||||
*.mo
|
||||
*.old
|
||||
*.ora
|
||||
*.ora
|
||||
*.orig
|
||||
*.p12
|
||||
*.pem
|
||||
*.pem
|
||||
*.pfx
|
||||
*.pot
|
||||
*.pub
|
||||
*.py,cover
|
||||
*.py[cod]
|
||||
*.py[cod]
|
||||
*.rsa
|
||||
*.sage.py
|
||||
*.so
|
||||
*.so
|
||||
*.spec
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.sqlite3
|
||||
*.sublime-*
|
||||
*.swo
|
||||
*.swo
|
||||
*.swp
|
||||
*.swp
|
||||
*.temp
|
||||
*.tmp
|
||||
*.wallet
|
||||
*_backup_*
|
||||
*_backup_*
|
||||
*_rsa
|
||||
*_rsa.pub
|
||||
*_temp_*
|
||||
*_tmp_*
|
||||
*auth*
|
||||
*cleanup*.json
|
||||
*cleanup*.json
|
||||
*connection*
|
||||
*credential*
|
||||
*dsn*
|
||||
*passwd*
|
||||
*password*
|
||||
*prod.env*
|
||||
*production.env*
|
||||
*report*.json
|
||||
*report*.json
|
||||
*scan*.json
|
||||
*scan*.json
|
||||
*secret*
|
||||
*security*.json
|
||||
*security*.json
|
||||
*ssh_test*
|
||||
*staging.env*
|
||||
*test_*.bat
|
||||
*test_*.py
|
||||
*test_*.sh
|
||||
*test_report*
|
||||
*test_results*
|
||||
*token*
|
||||
*tunnel_test*
|
||||
*~
|
||||
*~
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
.DS_Store?
|
||||
.Python
|
||||
.Python
|
||||
.Spotlight-V100
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.Trashes
|
||||
._*
|
||||
._*
|
||||
.aws/
|
||||
.azure/
|
||||
.cache
|
||||
.claude/settings.local.json
|
||||
.coverage
|
||||
.coverage
|
||||
.coverage.*
|
||||
.coverage.*
|
||||
.credentials/
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
.dmypy.json
|
||||
.docker/
|
||||
.dockerignore
|
||||
.dockerignore
|
||||
.eggs/
|
||||
.eggs/
|
||||
.env
|
||||
.env
|
||||
.env.*
|
||||
.env.*.local
|
||||
.env.local
|
||||
.env.production
|
||||
.env.production
|
||||
.env.test
|
||||
.gcp/
|
||||
.hypothesis/
|
||||
.hypothesis/
|
||||
.idea/
|
||||
.idea/
|
||||
.installed.cfg
|
||||
.installed.cfg
|
||||
.ipynb_checkpoints
|
||||
.mypy_cache/
|
||||
.nox/
|
||||
.nuxt/
|
||||
.output/
|
||||
.pdm-build/
|
||||
.pdm-python
|
||||
.pdm.toml
|
||||
.pnpm-debug.log*
|
||||
.pnpm-debug.log*
|
||||
.pybuilder/
|
||||
.pypirc
|
||||
.pyre/
|
||||
.pytest_cache/
|
||||
.pytest_cache/
|
||||
.pytype/
|
||||
.ropeproject
|
||||
.ruff_cache/
|
||||
.scrapy
|
||||
.secrets/
|
||||
.serena/cache/
|
||||
.serena/cache/
|
||||
.serena/memories/
|
||||
.serena/memories/
|
||||
.spyderproject
|
||||
.spyproject
|
||||
.terraform/
|
||||
.tox/
|
||||
.venv
|
||||
.venv
|
||||
.vite/
|
||||
.vscode/
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
.vscode/tasks.json
|
||||
.webassets-cache
|
||||
.yarn/build-state.yml
|
||||
.yarn/cache/
|
||||
.yarn/install-state.gz
|
||||
.yarn/unplugged/
|
||||
/blob-report/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/site
|
||||
ENV/
|
||||
ENV/
|
||||
MANIFEST
|
||||
MANIFEST
|
||||
PRODUCTION_CREDENTIALS.md
|
||||
PRODUCTION_CREDENTIALS.md
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
__pypackages__/
|
||||
access.log
|
||||
ansible-vault*
|
||||
authorized_keys
|
||||
build/
|
||||
build/
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
config.prod.*
|
||||
config.production.*
|
||||
cover/
|
||||
coverage.xml
|
||||
coverage.xml
|
||||
coverage/
|
||||
credentials/
|
||||
cython_debug/
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
debug.log
|
||||
deploy_production.sh
|
||||
deployment/windows/deploy-package/
|
||||
deployment/windows/scripts/*.log
|
||||
deployment/windows/temp/
|
||||
dev.log
|
||||
dev.log
|
||||
develop-eggs/
|
||||
develop-eggs/
|
||||
dist/
|
||||
dist/
|
||||
dmypy.json
|
||||
docker-compose.override.yml
|
||||
docs/_build/
|
||||
downloads/
|
||||
downloads/
|
||||
eggs/
|
||||
eggs/
|
||||
ehthumbs.db
|
||||
ehthumbs.db
|
||||
env.bak/
|
||||
env.bak/
|
||||
env/
|
||||
env/
|
||||
error.log
|
||||
htmlcov/
|
||||
htmlcov/
|
||||
id_rsa*
|
||||
id_rsa*
|
||||
instance/
|
||||
ipython_config.py
|
||||
known_hosts
|
||||
ldap.ora
|
||||
lib/
|
||||
lib/
|
||||
lib64/
|
||||
lib64/
|
||||
local_settings.py
|
||||
logs/
|
||||
nginx/logs/*.log
|
||||
node_modules/
|
||||
node_modules/
|
||||
nosetests.xml
|
||||
npm-debug.log*
|
||||
npm-debug.log*
|
||||
package-lock.json
|
||||
parts/
|
||||
parts/
|
||||
pip-delete-this-directory.txt
|
||||
pip-log.txt
|
||||
playwright-report/
|
||||
profile_default/
|
||||
quick_test.*
|
||||
quick_test.*
|
||||
roa2web/deployment/windows/deploy-package/
|
||||
roa2web/deployment/windows/scripts/*.log
|
||||
roa2web/deployment/windows/temp/
|
||||
roa2web/reports-app/telegram-bot/data/*.db
|
||||
roa2web/reports-app/telegram-bot/data/*.db-*
|
||||
run_tests.*
|
||||
run_tests.*
|
||||
scan_*.json
|
||||
sdist/
|
||||
sdist/
|
||||
secrets/
|
||||
security_*.json
|
||||
share/python-wheels/
|
||||
sqlnet.ora
|
||||
ssh-tunnel/*_rsa
|
||||
ssh-tunnel/*_rsa.pub
|
||||
ssh-tunnel/roa_oracle_server
|
||||
ssh_host_*
|
||||
target/
|
||||
temp_test.*
|
||||
terraform.tfstate*
|
||||
test-results/
|
||||
test_*.bat
|
||||
test_*.py
|
||||
test_*.sh
|
||||
test_results/
|
||||
tnsnames.ora
|
||||
tnsnames.ora
|
||||
var/
|
||||
var/
|
||||
venv.bak/
|
||||
venv.bak/
|
||||
venv/
|
||||
venv/
|
||||
wallet/
|
||||
wheels/
|
||||
wheels/
|
||||
yarn-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn-error.log*
|
||||
536
CLAUDE.md
Normal file
536
CLAUDE.md
Normal file
@@ -0,0 +1,536 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 🚀 Project Overview
|
||||
|
||||
**ROA2WEB** - Modern ERP Reports Application with FastAPI backend and Vue.js frontend using microservices architecture.
|
||||
|
||||
**Active Branch**: `v2-roa2web-fastapi`
|
||||
**Main Branch**: `main` (use for PRs)
|
||||
**Working Directory**: Repository root - All development happens here
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Microservices Structure
|
||||
```
|
||||
.
|
||||
├── shared/ # Shared components across microservices
|
||||
│ ├── database/ # Oracle connection pool (singleton pattern)
|
||||
│ ├── auth/ # JWT authentication & middleware
|
||||
│ └── utils/ # Common utilities
|
||||
├── reports-app/ # Main reports application
|
||||
│ ├── backend/ # FastAPI API (port 8001)
|
||||
│ ├── frontend/ # Vue.js 3 UI (Vite dev server, port 3000-3005)
|
||||
│ └── telegram-bot/ # Telegram bot frontend (port 8002 internal API)
|
||||
├── nginx/ # Reverse proxy & load balancer
|
||||
└── ssh-tunnel/ # SSH tunnel for Oracle DB access
|
||||
```
|
||||
|
||||
### Key Architectural Decisions
|
||||
- **Shared Database Connection Pool**: Singleton `OraclePool` class in `shared/database/oracle_pool.py` is shared across all microservices. Uses `python-oracledb` with connection pooling.
|
||||
- **Centralized Authentication**: JWT-based auth in `shared/auth/` with middleware that auto-injects user data into `request.state`.
|
||||
- **SSH Tunnel Requirement**: All Oracle DB connections go through an SSH tunnel to the remote server (see Database Setup below).
|
||||
- **FastAPI Structure**: Routers in `backend/app/routers/`, schemas in `backend/app/schemas/`, no traditional models (Oracle stored procedures used instead).
|
||||
- **Telegram Bot Frontend**: Alternative command-based interface for Telegram. Uses standalone SQLite database for Telegram-specific data (auth codes, sessions) and communicates with backend via HTTP API.
|
||||
|
||||
## 🗄️ Database Setup
|
||||
|
||||
**Schema**: `CONTAFIN_ORACLE` - Used for authentication and user management
|
||||
**Connection Method**: SSH tunnel required (Oracle DB is on remote network)
|
||||
|
||||
### SSH Tunnel Management
|
||||
```bash
|
||||
./ssh_tunnel.sh start # Start tunnel (localhost:1526 -> remote:1521)
|
||||
./ssh_tunnel.sh stop # Stop tunnel
|
||||
./ssh_tunnel.sh status # Check tunnel status
|
||||
./ssh_tunnel.sh restart # Restart tunnel
|
||||
```
|
||||
|
||||
**SSH Configuration**:
|
||||
- Remote Server: `83.103.197.79:22122`
|
||||
- User: `roa2web`
|
||||
- SSH Key: `ssh-tunnel/secrets/roa_oracle_server`
|
||||
- Local Port: `1526`
|
||||
- Remote Oracle: `10.0.20.36:1521`
|
||||
|
||||
**IMPORTANT**: Always ensure SSH tunnel is running before starting backend services.
|
||||
|
||||
### Environment Variables
|
||||
Located in `reports-app/backend/.env`:
|
||||
```bash
|
||||
# Oracle Database (through SSH tunnel)
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_PASSWORD=your_password
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1526
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your_secret_key
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
```
|
||||
|
||||
## 🛠️ Development Commands
|
||||
|
||||
### Quick Start (All Services)
|
||||
```bash
|
||||
./start-dev.sh # Starts SSH tunnel, backend, and frontend
|
||||
./start-dev.sh stop # Stops all ROA2WEB services
|
||||
./start-dev.sh help # Show help
|
||||
```
|
||||
|
||||
This script manages:
|
||||
- SSH tunnel (Oracle DB connection)
|
||||
- Backend (FastAPI on port 8001)
|
||||
- Frontend (Vue.js/Vite on port 3000-3005)
|
||||
|
||||
### Backend (FastAPI)
|
||||
```bash
|
||||
cd reports-app/backend/
|
||||
|
||||
# Create virtual environment (first time only)
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run development server
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8001
|
||||
|
||||
# API Documentation (when running)
|
||||
# http://localhost:8001/docs # Swagger UI
|
||||
# http://localhost:8001/redoc # ReDoc
|
||||
# http://localhost:8001/health # Health check endpoint
|
||||
```
|
||||
|
||||
### Frontend (Vue.js + Vite)
|
||||
```bash
|
||||
cd reports-app/frontend/
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run development server (Vite will auto-assign port 3000-3005)
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint and format
|
||||
npm run lint
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
#### Backend Tests (Shared Components)
|
||||
```bash
|
||||
cd shared/
|
||||
python -m pytest -v # All tests
|
||||
python -m pytest tests/test_auth.py -v # Specific test file
|
||||
```
|
||||
|
||||
#### Frontend E2E Tests (Playwright)
|
||||
```bash
|
||||
cd reports-app/frontend/
|
||||
|
||||
# Run all tests (headless)
|
||||
npm run test:e2e
|
||||
|
||||
# Run with browser UI visible
|
||||
npm run test:e2e:headed
|
||||
|
||||
# Debug mode with step-through
|
||||
npm run test:e2e:debug
|
||||
|
||||
# Interactive UI mode
|
||||
npm run test:e2e:ui
|
||||
|
||||
# View test report
|
||||
npm run test:e2e:report
|
||||
```
|
||||
|
||||
**Frontend Test Structure**:
|
||||
- Uses Page Object Model pattern
|
||||
- Tests in `tests/e2e/{auth,dashboard,invoices,payments,responsive}/`
|
||||
- Page objects in `tests/page-objects/`
|
||||
- Mocks all backend API calls for speed and reliability
|
||||
- See `reports-app/frontend/tests/README.md` for details
|
||||
|
||||
#### Telegram Bot Tests
|
||||
```bash
|
||||
cd reports-app/telegram-bot/
|
||||
|
||||
# Run all tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Run specific test suites
|
||||
pytest tests/test_auth.py -v # Authentication flow tests
|
||||
pytest tests/test_session_company.py -v # Session management tests
|
||||
|
||||
# Run with coverage
|
||||
pytest tests/ --cov=app --cov-report=html
|
||||
```
|
||||
|
||||
**Telegram Bot Test Coverage:**
|
||||
- Authentication flow tests (link/unlink, JWT management)
|
||||
- Session management tests (active company persistence)
|
||||
- Manual testing checklist: `tests/MANUAL_TESTING_CHECKLIST.md`
|
||||
|
||||
## 🔑 Authentication Flow
|
||||
|
||||
1. **Login**: `POST /api/auth/login` calls Oracle stored procedure `pack_drepturi.verificautilizator(username, password)`
|
||||
2. **Token Generation**: JWT token includes `username`, `user_id`, `companies[]`, `permissions[]`, `exp`, `iat`, `type`
|
||||
3. **Middleware**: `AuthenticationMiddleware` in `shared/auth/middleware.py` auto-validates tokens and injects user into `request.state.user`
|
||||
4. **Protected Routes**: All routes except `excluded_paths` require valid JWT token
|
||||
|
||||
**Key Files**:
|
||||
- `shared/auth/middleware.py` - FastAPI middleware with rate limiting
|
||||
- `shared/auth/jwt_handler.py` - Token creation/validation
|
||||
- `reports-app/backend/app/main.py` - Auth router inline definition
|
||||
|
||||
## 🔌 API Endpoints
|
||||
|
||||
All endpoints prefixed with `/api`:
|
||||
|
||||
### Authentication
|
||||
- `POST /api/auth/login` - Login with username/password
|
||||
|
||||
### Companies
|
||||
- `GET /api/companies` - Get user's accessible companies
|
||||
|
||||
### Dashboard
|
||||
- `GET /api/dashboard/{company_id}` - Dashboard statistics
|
||||
|
||||
### Invoices
|
||||
- `GET /api/invoices/{company_id}` - List invoices with filters
|
||||
- `GET /api/invoices/{company_id}/summary` - Invoice summary stats
|
||||
|
||||
### Treasury
|
||||
- `GET /api/treasury/{company_id}` - Treasury/payment data
|
||||
|
||||
### Telegram Bot
|
||||
- `POST /api/telegram/auth/generate-code` - Generate 8-char linking code (requires JWT)
|
||||
- `POST /api/telegram/auth/verify-user` - Verify Oracle user_id (public)
|
||||
- `POST /api/telegram/auth/refresh-token` - Refresh JWT token (public)
|
||||
- `POST /api/telegram/export` - Export reports for Telegram (requires JWT)
|
||||
- `GET /api/telegram/health` - Health check (public)
|
||||
|
||||
**Backend Routers**: `reports-app/backend/app/routers/{companies,dashboard,invoices,treasury,telegram}.py`
|
||||
|
||||
## 🎨 Frontend Stack
|
||||
|
||||
- **Framework**: Vue.js 3 (Composition API)
|
||||
- **UI Library**: PrimeVue (rich component library)
|
||||
- **State Management**: Pinia stores in `src/stores/`
|
||||
- **Routing**: Vue Router in `src/router/`
|
||||
- **Build Tool**: Vite
|
||||
- **Charts**: Chart.js via vue-chartjs
|
||||
- **HTTP**: Axios
|
||||
- **Testing**: Playwright (E2E)
|
||||
|
||||
**Key Frontend Components**:
|
||||
- `src/views/LoginView.vue` - Login page
|
||||
- `src/views/DashboardView.vue` - Main dashboard
|
||||
- `src/views/InvoicesView.vue` - Invoice management
|
||||
- `src/components/dashboard/cards/` - Dashboard metric cards
|
||||
- `src/components/layout/` - Layout components
|
||||
|
||||
## 🤖 Telegram Bot Frontend
|
||||
|
||||
The Telegram Bot provides an alternative command-based interface to ROA2WEB for Telegram users.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Bot Framework**: python-telegram-bot (v20.7+)
|
||||
- **Database**: Standalone SQLite (auth codes, sessions, active company)
|
||||
- **Backend Communication**: HTTP API client (httpx)
|
||||
- **Internal API**: FastAPI (port 8002) for backend callbacks
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
cd reports-app/telegram-bot/
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with TELEGRAM_BOT_TOKEN
|
||||
|
||||
# Run bot
|
||||
python -m app.main
|
||||
|
||||
# Health check
|
||||
curl http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
### Authentication Flow (Telegram)
|
||||
|
||||
1. User logs into ROA2WEB web app
|
||||
2. User requests Telegram linking → Backend generates 8-char code
|
||||
3. Backend saves code to telegram-bot via `POST /internal/save-code` (port 8002)
|
||||
4. User sends code to Telegram bot: `/start ABC123XY`
|
||||
5. Bot verifies code, creates user in SQLite, receives JWT token
|
||||
6. User can now use commands to query data
|
||||
|
||||
### Bot Commands
|
||||
|
||||
- `/start [code]` - Welcome message or link account with code
|
||||
- `/help` - Usage guide and available commands
|
||||
- `/companies` - View accessible companies
|
||||
- `/selectcompany [name]` - Select or search for active company
|
||||
- `/dashboard` - View financial dashboard for active company
|
||||
- `/sold` - View balance (alias for `/dashboard`)
|
||||
- `/facturi [filter]` - View invoices (optional filters: neplatite, platite)
|
||||
- `/trezorerie` - View treasury/payment data
|
||||
- `/clear` - Clear active company selection
|
||||
- `/unlink` - Unlink Telegram from Oracle account
|
||||
|
||||
### Database (SQLite)
|
||||
|
||||
Standalone database in `data/telegram_bot.db`:
|
||||
- **telegram_users**: Telegram-Oracle account linking
|
||||
- **telegram_auth_codes**: 8-char codes (15 min expiry)
|
||||
- **telegram_sessions**: Active company selection
|
||||
|
||||
Automatic cleanup runs hourly to remove expired data.
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Unit and integration tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Coverage report
|
||||
pytest tests/ --cov=app --cov-report=html
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
- `app/main.py` - Bot entry point and Telegram application setup
|
||||
- `app/bot/handlers.py` - Telegram command handlers
|
||||
- `app/bot/helpers.py` - Helper functions for commands
|
||||
- `app/bot/formatters.py` - Response formatting utilities
|
||||
- `app/agent/session.py` - Session management (active company)
|
||||
- `app/auth/linking.py` - Account linking logic
|
||||
- `app/db/operations.py` - SQLite CRUD operations
|
||||
- `app/api/client.py` - Backend API client
|
||||
- `app/internal_api.py` - Internal FastAPI for backend callbacks
|
||||
|
||||
See `reports-app/telegram-bot/README.md` for complete documentation.
|
||||
|
||||
## 🐳 Docker & Production
|
||||
|
||||
### Docker Compose (Legacy Flask App)
|
||||
The root `docker-compose.yaml` is for the old Flask application - NOT for the current FastAPI/Vue.js app.
|
||||
|
||||
### Production Deployment Options
|
||||
|
||||
ROA2WEB supports two production deployment architectures:
|
||||
|
||||
#### 🐧 Linux Production (Docker)
|
||||
|
||||
```bash
|
||||
# Setup production environment
|
||||
./setup_production.sh
|
||||
|
||||
# Deployment scripts
|
||||
./scripts/deploy.sh # Deploy application
|
||||
./scripts/backup.sh # Backup data
|
||||
./scripts/rollback.sh # Rollback deployment
|
||||
./scripts/health-check.sh # Health monitoring
|
||||
```
|
||||
|
||||
See `DEPLOYMENT_GUIDE.md` for full Linux/Docker deployment instructions.
|
||||
|
||||
#### 🪟 Windows Server Production (IIS)
|
||||
|
||||
Windows Server deployment uses IIS and Windows Services without Docker:
|
||||
|
||||
**Build deployment package (on WSL/Linux development machine):**
|
||||
```bash
|
||||
cd deployment/windows/scripts
|
||||
./Build-Frontend.ps1
|
||||
|
||||
# Output: ./deploy-package/
|
||||
# Transfer this to Windows Server
|
||||
```
|
||||
|
||||
**Deploy on Windows Server (PowerShell as Administrator):**
|
||||
```powershell
|
||||
# Initial installation
|
||||
cd C:\path\to\deploy-package\scripts
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
# Configure application
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
# Deploy application files
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\path\to\deploy-package"
|
||||
|
||||
# Management
|
||||
.\Start-ROA2WEB.ps1
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Check status
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-Website ROA2WEB
|
||||
```
|
||||
|
||||
**Windows Deployment Architecture:**
|
||||
- **IIS Web Server**: Serves frontend static files (port 80/443)
|
||||
- **Windows Service**: FastAPI backend via NSSM (port 8000)
|
||||
- **Direct Oracle Connection**: No SSH tunnel required
|
||||
- **URL Rewrite Module**: Reverse proxy for `/api/*` routes
|
||||
|
||||
See `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` for complete Windows deployment guide.
|
||||
|
||||
## 📝 Common Development Tasks
|
||||
|
||||
### Adding a New API Endpoint
|
||||
1. Create router in `reports-app/backend/app/routers/your_router.py`
|
||||
2. Define Pydantic schemas in `app/schemas/`
|
||||
3. Use `oracle_pool.get_connection()` context manager for DB queries
|
||||
4. Register router in `app/main.py` with `app.include_router(your_router, prefix="/api/your_prefix")`
|
||||
|
||||
### Adding Shared Functionality
|
||||
1. Place in `shared/{database|auth|utils}/`
|
||||
2. Import using `sys.path.append()` pattern (see `backend/app/main.py:21`)
|
||||
3. Test in `shared/tests/`
|
||||
|
||||
### Working with Oracle Stored Procedures
|
||||
```python
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT pack_name.procedure_name(:param1, :param2)
|
||||
FROM DUAL
|
||||
""", {
|
||||
'param1': value1,
|
||||
'param2': value2
|
||||
})
|
||||
result = cursor.fetchone()
|
||||
```
|
||||
|
||||
### Frontend API Integration
|
||||
```javascript
|
||||
// Store pattern (src/stores/authStore.js)
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8001/api',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const response = await api.get('/companies');
|
||||
```
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- **Git Hooks**: Security scanning hooks in `security/install_hooks.sh`
|
||||
- **Environment Files**: Never commit `.env` files (use `.env.example` as template)
|
||||
- **SSH Keys**: Stored in `ssh-tunnel/secrets/` (gitignored)
|
||||
- **JWT Secrets**: Generate strong secrets for production
|
||||
- **Rate Limiting**: Built into authentication middleware (5 requests per 5 minutes)
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### SSH Tunnel Issues
|
||||
```bash
|
||||
# Check tunnel status
|
||||
./ssh_tunnel.sh status
|
||||
|
||||
# View tunnel logs
|
||||
ps aux | grep ssh | grep 1526
|
||||
|
||||
# Test Oracle connectivity through tunnel
|
||||
timeout 5 bash -c "cat < /dev/null > /dev/tcp/127.0.0.1/1526"
|
||||
```
|
||||
|
||||
### Backend Not Starting
|
||||
- Ensure SSH tunnel is running
|
||||
- Check `.env` file exists with correct credentials
|
||||
- Verify virtual environment is activated
|
||||
- Check port 8001 is not already in use: `lsof -ti:8001`
|
||||
|
||||
### Frontend Build Issues
|
||||
- Clear node_modules: `rm -rf node_modules && npm install`
|
||||
- Check Node.js version: `node -v` (requires 16+)
|
||||
- Vite may auto-assign ports 3000-3005 if 3000 is busy
|
||||
|
||||
### Database Connection Errors
|
||||
- Verify SSH tunnel: `./ssh_tunnel.sh status`
|
||||
- Check Oracle listener is running on remote server
|
||||
- Verify credentials in `.env` file
|
||||
- Test connection: `http://localhost:8001/health`
|
||||
|
||||
## 📚 Additional Documentation
|
||||
|
||||
### General Documentation
|
||||
- `README.md` - Project overview
|
||||
- `DEVELOPMENT_BLUEPRINT.md` - Detailed development plan
|
||||
- `ARCHITECTURE_SCHEMA.md` - Architecture diagrams and schemas
|
||||
- `MICROSERVICES_GUIDE.md` - Microservices architecture details
|
||||
|
||||
### Deployment Documentation
|
||||
- `DEPLOYMENT_GUIDE.md` - Production deployment (Linux/Docker & Windows/IIS)
|
||||
- `deployment/windows/README.md` - Windows deployment quick start
|
||||
- `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` - Complete Windows deployment guide
|
||||
|
||||
### Component Documentation
|
||||
- `reports-app/backend/README.md` - Backend specifics
|
||||
- `reports-app/frontend/README.md` - Frontend guide
|
||||
- `reports-app/frontend/tests/README.md` - Testing guide
|
||||
- `reports-app/telegram-bot/README.md` - Telegram bot complete guide
|
||||
- `reports-app/telegram-bot/TELEGRAM_COMMANDS.md` - Command reference
|
||||
|
||||
## 🔧 Tech Stack Summary
|
||||
|
||||
**Backend**:
|
||||
- FastAPI (async Python web framework)
|
||||
- python-oracledb (Oracle database driver)
|
||||
- JWT (PyJWT for authentication)
|
||||
- Pydantic (data validation)
|
||||
- pytest (testing)
|
||||
|
||||
**Frontend**:
|
||||
- Vue.js 3 (Composition API)
|
||||
- PrimeVue (UI components)
|
||||
- Pinia (state management)
|
||||
- Vite (build tool)
|
||||
- Playwright (E2E testing)
|
||||
- Axios (HTTP client)
|
||||
- Chart.js (data visualization)
|
||||
|
||||
**Telegram Bot Frontend**:
|
||||
- python-telegram-bot (Telegram API wrapper)
|
||||
- SQLite + aiosqlite (standalone database)
|
||||
- httpx (async HTTP client for backend)
|
||||
- FastAPI + uvicorn (internal API)
|
||||
- pytest + pytest-asyncio (testing)
|
||||
|
||||
**Infrastructure**:
|
||||
- Oracle Database (enterprise data)
|
||||
- SQLite (Telegram bot standalone data)
|
||||
- SSH Tunnel (secure database access - Linux/development)
|
||||
- Nginx (reverse proxy - Linux production)
|
||||
- Docker Compose (orchestration - Linux production)
|
||||
- Windows Server + IIS (Windows production deployment)
|
||||
- NSSM (Windows service manager for backend)
|
||||
393
README.md
Normal file
393
README.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# ROA2WEB - Modern ERP Reports Application
|
||||
|
||||
**FastAPI Backend + Vue.js 3 Frontend + Telegram Bot**
|
||||
|
||||
Modern microservices-based ERP reporting application for managing invoices, payments, and financial data with Oracle database integration.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
ROA2WEB is a comprehensive financial reporting platform built with modern technologies:
|
||||
|
||||
- **Backend**: FastAPI (Python) - High-performance async API
|
||||
- **Frontend**: Vue.js 3 + PrimeVue - Rich, responsive web interface
|
||||
- **Telegram Bot**: Alternative command-based interface
|
||||
- **Database**: Oracle Database with connection pooling
|
||||
- **Architecture**: Microservices with shared components
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- Node.js 16+
|
||||
- Oracle Database access
|
||||
- SSH access to Oracle server (for development)
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone <repository-url>
|
||||
cd roa2web
|
||||
|
||||
# Start all services (SSH tunnel + Backend + Frontend)
|
||||
cd roa2web
|
||||
./start-dev.sh
|
||||
|
||||
# Or start services individually:
|
||||
|
||||
# 1. Start SSH tunnel for Oracle DB
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# 2. Start FastAPI backend (port 8001)
|
||||
cd reports-app/backend
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8001
|
||||
|
||||
# 3. Start Vue.js frontend (port 3000-3005)
|
||||
cd reports-app/frontend
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# 4. Start Telegram Bot (optional, port 8002)
|
||||
cd reports-app/telegram-bot
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
### Access the Application
|
||||
|
||||
- **Frontend**: http://localhost:3000 (or 3001-3005 if 3000 is busy)
|
||||
- **Backend API Docs**: http://localhost:8001/docs (Swagger UI)
|
||||
- **Backend ReDoc**: http://localhost:8001/redoc
|
||||
- **Health Check**: http://localhost:8001/health
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
|
||||
├── shared/ # Shared components
|
||||
│ ├── database/ # Oracle connection pool (singleton)
|
||||
│ ├── auth/ # JWT authentication & middleware
|
||||
│ └── utils/ # Common utilities
|
||||
│
|
||||
├── reports-app/ # Main reports application
|
||||
│ ├── backend/ # FastAPI backend (port 8001)
|
||||
│ ├── frontend/ # Vue.js 3 frontend (port 3000-3005)
|
||||
│ └── telegram-bot/ # Telegram bot (port 8002)
|
||||
│
|
||||
├── nginx/ # Nginx reverse proxy config
|
||||
├── ssh-tunnel/ # SSH tunnel for Oracle DB
|
||||
├── deployment/ # Deployment scripts (Linux & Windows)
|
||||
└── scripts/ # Utility scripts
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Shared Database Pool**: Singleton Oracle connection pool shared across microservices
|
||||
- **JWT Authentication**: Secure token-based auth with middleware
|
||||
- **Microservices**: Independent services with clear separation of concerns
|
||||
- **Oracle Integration**: Direct Oracle stored procedure calls
|
||||
- **Responsive Design**: Mobile-friendly Vue.js interface
|
||||
- **Telegram Integration**: Alternative bot-based interface
|
||||
|
||||
---
|
||||
|
||||
## Core Technologies
|
||||
|
||||
### Backend
|
||||
- **FastAPI**: Modern, high-performance Python web framework
|
||||
- **python-oracledb**: Oracle database driver with connection pooling
|
||||
- **JWT (PyJWT)**: Token-based authentication
|
||||
- **Pydantic**: Data validation and settings management
|
||||
- **pytest**: Comprehensive testing
|
||||
|
||||
### Frontend
|
||||
- **Vue.js 3**: Progressive JavaScript framework (Composition API)
|
||||
- **PrimeVue**: Rich UI component library
|
||||
- **Pinia**: State management
|
||||
- **Vue Router**: Client-side routing
|
||||
- **Vite**: Fast build tool
|
||||
- **Axios**: HTTP client
|
||||
- **Chart.js**: Data visualization
|
||||
- **Playwright**: E2E testing
|
||||
|
||||
### Telegram Bot
|
||||
- **python-telegram-bot**: Telegram Bot API wrapper
|
||||
- **SQLite + aiosqlite**: Standalone database for bot data
|
||||
- **httpx**: Async HTTP client for backend communication
|
||||
- **FastAPI**: Internal API for backend callbacks
|
||||
|
||||
### Infrastructure
|
||||
- **Oracle Database**: Enterprise data storage
|
||||
- **SSH Tunnel**: Secure database access (development)
|
||||
- **Nginx**: Reverse proxy and load balancer (production)
|
||||
- **Docker**: Containerization (production)
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Backend (FastAPI)
|
||||
|
||||
```bash
|
||||
cd reports-app/backend
|
||||
|
||||
# Development server with auto-reload
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8001
|
||||
|
||||
# Run tests
|
||||
pytest -v
|
||||
|
||||
# API documentation
|
||||
# Swagger UI: http://localhost:8001/docs
|
||||
# ReDoc: http://localhost:8001/redoc
|
||||
```
|
||||
|
||||
### Frontend (Vue.js)
|
||||
|
||||
```bash
|
||||
cd reports-app/frontend
|
||||
|
||||
# Development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# E2E tests (Playwright)
|
||||
npm run test:e2e
|
||||
npm run test:e2e:ui # Interactive UI mode
|
||||
npm run test:e2e:debug # Debug mode
|
||||
```
|
||||
|
||||
### Telegram Bot
|
||||
|
||||
```bash
|
||||
cd reports-app/telegram-bot
|
||||
|
||||
# Run bot
|
||||
python -m app.main
|
||||
|
||||
# Run tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Health check
|
||||
curl http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
### SSH Tunnel Management
|
||||
|
||||
```bash
|
||||
cd roa2web
|
||||
|
||||
# Start tunnel (localhost:1526 -> remote Oracle:1521)
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# Check status
|
||||
./ssh_tunnel.sh status
|
||||
|
||||
# Stop tunnel
|
||||
./ssh_tunnel.sh stop
|
||||
|
||||
# Restart tunnel
|
||||
./ssh_tunnel.sh restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Tests
|
||||
```bash
|
||||
cd shared
|
||||
python -m pytest -v
|
||||
```
|
||||
|
||||
### Frontend E2E Tests
|
||||
```bash
|
||||
cd reports-app/frontend
|
||||
npm run test:e2e # Headless mode
|
||||
npm run test:e2e:headed # With browser UI
|
||||
npm run test:e2e:ui # Interactive mode
|
||||
```
|
||||
|
||||
### Telegram Bot Tests
|
||||
```bash
|
||||
cd reports-app/telegram-bot
|
||||
pytest tests/ -v
|
||||
pytest tests/ --cov=app --cov-report=html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Linux/Docker Deployment
|
||||
|
||||
```bash
|
||||
cd roa2web
|
||||
|
||||
# Setup production environment
|
||||
./setup_production.sh
|
||||
|
||||
# Deploy application
|
||||
./scripts/deploy.sh
|
||||
|
||||
# Health check
|
||||
./scripts/health-check.sh
|
||||
```
|
||||
|
||||
See `DEPLOYMENT_GUIDE.md` for complete deployment instructions.
|
||||
|
||||
### Windows Server Deployment
|
||||
|
||||
ROA2WEB supports deployment on Windows Server with IIS:
|
||||
|
||||
```powershell
|
||||
# On Windows Server (PowerShell as Administrator)
|
||||
cd C:\path\to\roa2web\deployment\windows\scripts
|
||||
|
||||
# Install
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
# Deploy
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\path\to\deploy-package"
|
||||
|
||||
# Manage services
|
||||
.\Start-ROA2WEB.ps1
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Restart-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
See `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` for details.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints prefixed with `/api`:
|
||||
|
||||
### Authentication
|
||||
- `POST /api/auth/login` - Login with Oracle credentials
|
||||
|
||||
### Companies
|
||||
- `GET /api/companies` - Get user's accessible companies
|
||||
|
||||
### Dashboard
|
||||
- `GET /api/dashboard/{company_id}` - Dashboard statistics
|
||||
|
||||
### Invoices
|
||||
- `GET /api/invoices/{company_id}` - List invoices with filters
|
||||
- `GET /api/invoices/{company_id}/summary` - Invoice summary
|
||||
|
||||
### Treasury
|
||||
- `GET /api/treasury/{company_id}` - Payment data
|
||||
|
||||
### Telegram Bot
|
||||
- `POST /api/telegram/auth/generate-code` - Generate linking code
|
||||
- `POST /api/telegram/auth/verify-user` - Verify Oracle user
|
||||
- `POST /api/telegram/auth/refresh-token` - Refresh JWT token
|
||||
- `POST /api/telegram/export` - Export reports
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Copy `.env.example` to `.env` in each microservice and configure:
|
||||
|
||||
### Backend (`reports-app/backend/.env`)
|
||||
```bash
|
||||
# Oracle Database (through SSH tunnel)
|
||||
ORACLE_USER=your_username
|
||||
ORACLE_PASSWORD=your_password
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1526
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your_secret_key
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
```
|
||||
|
||||
### Telegram Bot (`reports-app/telegram-bot/.env`)
|
||||
```bash
|
||||
# Telegram Bot Token
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||
|
||||
# Backend API
|
||||
BACKEND_API_URL=http://localhost:8001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
### General
|
||||
- `CLAUDE.md` - Development guide for Claude Code
|
||||
- `DEVELOPMENT_BLUEPRINT.md` - Detailed development plan
|
||||
- `TEAM_IMPLEMENTATION_GUIDE.md` - Team implementation guide
|
||||
- `ARCHITECTURE_SCHEMA.md` - Architecture diagrams
|
||||
- `MICROSERVICES_GUIDE.md` - Microservices details
|
||||
|
||||
### Component-Specific
|
||||
- `README.md` - Main application README
|
||||
- `reports-app/backend/README.md` - Backend specifics
|
||||
- `reports-app/frontend/README.md` - Frontend guide
|
||||
- `reports-app/frontend/tests/README.md` - Frontend testing
|
||||
- `reports-app/telegram-bot/README.md` - Telegram bot guide
|
||||
- `reports-app/telegram-bot/TELEGRAM_COMMANDS.md` - Bot commands
|
||||
|
||||
### Deployment
|
||||
- `DEPLOYMENT_GUIDE.md` - Production deployment (Linux & Windows)
|
||||
- `deployment/windows/README.md` - Windows quick start
|
||||
- `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` - Complete Windows guide
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Create feature branch from `main`
|
||||
2. Make changes following project structure
|
||||
3. Write tests for new features
|
||||
4. Run all tests before committing
|
||||
5. Create pull request with clear description
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
[Your License Here]
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Check documentation in `` subdirectories
|
||||
- Review CLAUDE.md for development guidelines
|
||||
- See component-specific READMEs for detailed information
|
||||
|
||||
---
|
||||
|
||||
**Branch**: v2-roa2web-fastapi
|
||||
**Working Directory**: `` - All development happens here
|
||||
361
deployment/windows/README.md
Normal file
361
deployment/windows/README.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# ROA2WEB - Windows Deployment Package
|
||||
|
||||
Complete deployment solution for ROA2WEB on Windows Server with IIS and Oracle Database.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Package Contents
|
||||
|
||||
```
|
||||
deployment/windows/
|
||||
├── config/ # Configuration files
|
||||
│ ├── web.config # IIS configuration (URL Rewrite, reverse proxy)
|
||||
│ └── .env.production.windows # Environment variables template
|
||||
│
|
||||
├── scripts/ # PowerShell automation scripts
|
||||
│ ├── Install-ROA2WEB.ps1 # Initial installation
|
||||
│ ├── Deploy-ROA2WEB.ps1 # Deploy updates
|
||||
│ ├── Build-Frontend.ps1 # Build Vue.js frontend (run locally)
|
||||
│ ├── Start-ROA2WEB.ps1 # Start backend service
|
||||
│ ├── Stop-ROA2WEB.ps1 # Stop backend service
|
||||
│ └── Restart-ROA2WEB.ps1 # Restart backend service
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
│ └── WINDOWS_DEPLOYMENT.md # Complete deployment guide
|
||||
│
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Windows Server** 2016+ (or Windows 10/11 Pro)
|
||||
- **IIS** installed
|
||||
- **Oracle Database** (local or network-accessible)
|
||||
- **PowerShell 5.1+**
|
||||
- **Administrator privileges**
|
||||
|
||||
### Installation Steps
|
||||
|
||||
#### 1. Build Frontend (on development machine)
|
||||
|
||||
```bash
|
||||
# On WSL/Linux/Mac
|
||||
cd roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1
|
||||
|
||||
# This creates: ./deploy-package/
|
||||
```
|
||||
|
||||
#### 2. Transfer to Server
|
||||
|
||||
Copy the entire project to Windows Server:
|
||||
```
|
||||
C:\roa2web\deployment\windows\
|
||||
```
|
||||
|
||||
#### 3. Run Installation
|
||||
|
||||
```powershell
|
||||
# On Windows Server (PowerShell as Administrator)
|
||||
cd C:\roa2web\deployment\windows\scripts
|
||||
|
||||
# Install everything
|
||||
.\Install-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
This will:
|
||||
- ✅ Install Python 3.11+
|
||||
- ✅ Install NSSM (service manager)
|
||||
- ✅ Install IIS URL Rewrite and ARR
|
||||
- ✅ Create directory structure
|
||||
- ✅ Install Python dependencies
|
||||
- ✅ Create Windows Service
|
||||
- ✅ Configure IIS website
|
||||
|
||||
#### 4. Configure Application
|
||||
|
||||
```powershell
|
||||
# Copy and edit environment file
|
||||
Copy-Item C:\inetpub\wwwroot\roa2web\backend\config\.env.production.windows `
|
||||
C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
# Edit with your values
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
```
|
||||
|
||||
**Required settings:**
|
||||
|
||||
Configure these variables in `.env`:
|
||||
- Database credentials (user, password, host, port, SID)
|
||||
- JWT secret key for authentication
|
||||
- Other application-specific settings
|
||||
|
||||
Example structure:
|
||||
```env
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
# Add password and JWT secret here
|
||||
```
|
||||
|
||||
#### 5. Deploy Application Files
|
||||
|
||||
```powershell
|
||||
# Deploy frontend and backend
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\path\to\deploy-package"
|
||||
```
|
||||
|
||||
#### 6. Verify Installation
|
||||
|
||||
```powershell
|
||||
# Check service
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Test backend
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Open application
|
||||
Start-Process "http://localhost"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Update Workflow
|
||||
|
||||
For deploying updates to existing installation:
|
||||
|
||||
**1. Build on development machine:**
|
||||
```bash
|
||||
cd roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1 -OutputPath "./deploy-$(date +%Y%m%d)"
|
||||
```
|
||||
|
||||
**2. Transfer to server:**
|
||||
```powershell
|
||||
Copy-Item .\deploy-20250118 -Destination C:\Temp\roa2web-deploy -Recurse
|
||||
```
|
||||
|
||||
**3. Deploy on server:**
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\deployment\windows\scripts
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Management Commands
|
||||
|
||||
```powershell
|
||||
# Start backend service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Stop backend service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restart backend service
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# View logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Check service status
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Check IIS website
|
||||
Get-Website ROA2WEB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Architecture
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Type | Port | Purpose |
|
||||
|-----------|------|------|---------|
|
||||
| **Frontend** | IIS Static Files | 80/443 | Vue.js SPA |
|
||||
| **Backend** | Windows Service | 8000 | FastAPI API |
|
||||
| **Database** | Oracle | 1521 | Data storage |
|
||||
| **Reverse Proxy** | IIS URL Rewrite | - | API routing |
|
||||
|
||||
### Network Flow
|
||||
|
||||
```
|
||||
Client → IIS (port 80) → [web.config URL Rewrite]
|
||||
├─ /api/* → Backend Service (localhost:8000)
|
||||
│ ↓
|
||||
│ Oracle DB (localhost:1521)
|
||||
└─ /* → Static Files (Vue.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Directory Structure After Installation
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── backend\ # FastAPI application
|
||||
│ ├── app\
|
||||
│ ├── requirements.txt
|
||||
│ ├── .env # Configuration
|
||||
│ └── logs\
|
||||
│
|
||||
├── frontend\ # Vue.js static files
|
||||
│ ├── index.html
|
||||
│ ├── assets\
|
||||
│ └── web.config
|
||||
│
|
||||
├── logs\ # Service logs
|
||||
│ ├── backend-stdout.log
|
||||
│ └── backend-stderr.log
|
||||
│
|
||||
└── backups\ # Automatic backups
|
||||
└── backup-YYYYMMDD-HHMMSS\
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
```powershell
|
||||
# Check logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 50
|
||||
|
||||
# Test manually
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
### Frontend not loading
|
||||
|
||||
```powershell
|
||||
# Restart IIS
|
||||
iisreset
|
||||
|
||||
# Check website status
|
||||
Get-Website ROA2WEB
|
||||
Start-Website ROA2WEB
|
||||
```
|
||||
|
||||
### API calls failing (502/504)
|
||||
|
||||
```powershell
|
||||
# Check backend service
|
||||
Get-Service ROA2WEB-Backend
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Test backend directly
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Database connection issues
|
||||
|
||||
```powershell
|
||||
# Test Oracle connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
|
||||
# Check Oracle service
|
||||
Get-Service Oracle*
|
||||
|
||||
# Check .env configuration
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\backend\.env | Select-String ORACLE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Full Documentation
|
||||
|
||||
For complete documentation, see:
|
||||
- **[WINDOWS_DEPLOYMENT.md](docs/WINDOWS_DEPLOYMENT.md)** - Comprehensive deployment guide
|
||||
- **[.env.production.windows](config/.env.production.windows)** - Configuration reference
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Features
|
||||
|
||||
✅ **Simple Installation** - One PowerShell script installs everything
|
||||
✅ **Minimal Dependencies** - Only Python + IIS (already on Windows Server)
|
||||
✅ **Easy Replication** - Same scripts work on all servers
|
||||
✅ **Automatic Backups** - Every deployment creates a backup
|
||||
✅ **Windows Service** - Backend runs as service with auto-start/restart
|
||||
✅ **Production Ready** - Optimized for performance and reliability
|
||||
|
||||
---
|
||||
|
||||
## 📊 System Requirements
|
||||
|
||||
| Resource | Minimum | Recommended |
|
||||
|----------|---------|-------------|
|
||||
| **OS** | Windows Server 2016 | Windows Server 2019+ |
|
||||
| **RAM** | 4 GB | 8 GB |
|
||||
| **CPU** | 2 cores | 4 cores |
|
||||
| **Disk** | 10 GB free | 20 GB free |
|
||||
| **Network** | 100 Mbps | 1 Gbps |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Recommendations
|
||||
|
||||
1. **Generate Strong JWT Secret:**
|
||||
```powershell
|
||||
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | % {[char]$_})
|
||||
```
|
||||
|
||||
2. **Secure .env File:**
|
||||
```powershell
|
||||
icacls C:\inetpub\wwwroot\roa2web\backend\.env /inheritance:r /grant:r Administrators:F
|
||||
```
|
||||
|
||||
3. **Enable HTTPS:** ⭐ **RECOMMENDED**
|
||||
```powershell
|
||||
# Quick setup with automated script
|
||||
cd C:\roa2web\deployment\windows\scripts
|
||||
.\Enable-HTTPS.ps1
|
||||
|
||||
# For detailed instructions, see:
|
||||
# docs/HTTPS_SETUP.md
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Creates/installs SSL certificate
|
||||
- Configures HTTPS binding (port 443)
|
||||
- Enables HTTP to HTTPS redirect
|
||||
- Activates HSTS (Strict Transport Security)
|
||||
|
||||
**Access your application securely:**
|
||||
- `https://10.0.20.36/roa2web` (or your domain)
|
||||
|
||||
4. **Regular Updates:**
|
||||
- Keep Windows Server updated
|
||||
- Update Python packages monthly
|
||||
- Monitor security advisories
|
||||
- Renew SSL certificates before expiry
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
2. Review [WINDOWS_DEPLOYMENT.md](docs/WINDOWS_DEPLOYMENT.md)
|
||||
3. Contact: development-team@your-company.com
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 2.0.0 | 2025-01-18 | Initial Windows deployment package |
|
||||
|
||||
---
|
||||
|
||||
*ROA2WEB - Modern ERP Reports Application*
|
||||
*Windows Server Deployment Package v2.0.0*
|
||||
158
deployment/windows/config/web.config
Normal file
158
deployment/windows/config/web.config
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
ROA2WEB - IIS Web Configuration
|
||||
|
||||
This configuration enables:
|
||||
- SPA routing for Vue.js (all routes fallback to index.html)
|
||||
- Reverse proxy for /api/* to backend FastAPI service (localhost:8000)
|
||||
- Compression and caching for optimal performance
|
||||
- Security headers
|
||||
|
||||
Prerequisites:
|
||||
- IIS URL Rewrite Module: https://www.iis.net/downloads/microsoft/url-rewrite
|
||||
- IIS Application Request Routing (ARR): https://www.iis.net/downloads/microsoft/application-request-routing
|
||||
-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
|
||||
<!-- Static Content Compression -->
|
||||
<urlCompression doStaticCompression="true" doDynamicCompression="true" />
|
||||
|
||||
<!-- Default Document -->
|
||||
<defaultDocument>
|
||||
<files>
|
||||
<clear />
|
||||
<add value="index.html" />
|
||||
</files>
|
||||
</defaultDocument>
|
||||
|
||||
<!-- Static Content Settings -->
|
||||
<staticContent>
|
||||
<!-- Enable MIME types for modern web assets -->
|
||||
<!-- Remove first to avoid duplicates, then add -->
|
||||
<remove fileExtension=".json" />
|
||||
<mimeMap fileExtension=".json" mimeType="application/json" />
|
||||
<remove fileExtension=".woff" />
|
||||
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
|
||||
<remove fileExtension=".woff2" />
|
||||
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
|
||||
<remove fileExtension=".svg" />
|
||||
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
|
||||
<remove fileExtension=".webmanifest" />
|
||||
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
|
||||
|
||||
<!-- Client-side caching for static assets -->
|
||||
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />
|
||||
</staticContent>
|
||||
|
||||
<!-- Custom HTTP Headers (Security) -->
|
||||
<httpProtocol>
|
||||
<customHeaders>
|
||||
<!-- Security Headers -->
|
||||
<add name="X-Frame-Options" value="DENY" />
|
||||
<add name="X-Content-Type-Options" value="nosniff" />
|
||||
<add name="X-XSS-Protection" value="1; mode=block" />
|
||||
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
|
||||
<add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
|
||||
|
||||
<!-- Content Security Policy (adjust as needed) -->
|
||||
<add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:" />
|
||||
|
||||
<!-- Remove Server header for security -->
|
||||
<remove name="X-Powered-By" />
|
||||
</customHeaders>
|
||||
</httpProtocol>
|
||||
|
||||
<!-- URL Rewrite Rules -->
|
||||
<rewrite>
|
||||
<rules>
|
||||
|
||||
<!-- Rule 1: Force HTTPS (redirect HTTP to HTTPS) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 2: Reverse Proxy for API Requests -->
|
||||
<rule name="API Reverse Proxy" stopProcessing="true">
|
||||
<match url="^api/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8000/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 3: Health Check Endpoint -->
|
||||
<rule name="Health Check Proxy" stopProcessing="true">
|
||||
<match url="^health$" />
|
||||
<action type="Rewrite" url="http://localhost:8000/health" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 4: Don't rewrite if file exists (static assets) -->
|
||||
<rule name="StaticContent" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" />
|
||||
</conditions>
|
||||
<action type="None" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 5: Don't rewrite if directory exists -->
|
||||
<rule name="StaticDirectory" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" />
|
||||
</conditions>
|
||||
<action type="None" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 6: SPA Routing - Rewrite all other requests to index.html -->
|
||||
<rule name="SPA Fallback" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
<add input="{REQUEST_URI}" pattern="^/api" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.html" />
|
||||
</rule>
|
||||
|
||||
</rules>
|
||||
|
||||
<!-- Outbound Rules (optional - for modifying responses) -->
|
||||
<outboundRules>
|
||||
<rule name="Add HSTS Header" preCondition="IsHTTPS">
|
||||
<match serverVariable="RESPONSE_Strict-Transport-Security" pattern=".*" />
|
||||
<action type="Rewrite" value="max-age=31536000; includeSubDomains" />
|
||||
</rule>
|
||||
<preConditions>
|
||||
<preCondition name="IsHTTPS">
|
||||
<add input="{HTTPS}" pattern="on" />
|
||||
</preCondition>
|
||||
</preConditions>
|
||||
</outboundRules>
|
||||
</rewrite>
|
||||
|
||||
<!-- Error Pages -->
|
||||
<httpErrors errorMode="Custom" existingResponse="Replace">
|
||||
<!-- 404 - Not Found: Serve index.html for SPA routing -->
|
||||
<remove statusCode="404" subStatusCode="-1" />
|
||||
<error statusCode="404" path="index.html" responseMode="ExecuteURL" />
|
||||
|
||||
<!-- 500 - Internal Server Error -->
|
||||
<remove statusCode="500" subStatusCode="-1" />
|
||||
<error statusCode="500" path="index.html" responseMode="ExecuteURL" />
|
||||
</httpErrors>
|
||||
|
||||
<!-- Disable directory browsing -->
|
||||
<directoryBrowse enabled="false" />
|
||||
|
||||
</system.webServer>
|
||||
|
||||
<!-- System.web for ASP.NET settings (if needed) -->
|
||||
<system.web>
|
||||
<compilation debug="false" targetFramework="4.8" />
|
||||
<httpRuntime targetFramework="4.8" maxRequestLength="10240" executionTimeout="300" />
|
||||
</system.web>
|
||||
|
||||
</configuration>
|
||||
572
deployment/windows/docs/HTTPS_SETUP.md
Normal file
572
deployment/windows/docs/HTTPS_SETUP.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# HTTPS Setup Guide for ROA2WEB on IIS
|
||||
|
||||
Complete guide for enabling HTTPS on ROA2WEB deployed on Windows Server with IIS.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start
|
||||
|
||||
### Option 1: Automated Setup (Recommended)
|
||||
|
||||
Run the automated PowerShell script:
|
||||
|
||||
```powershell
|
||||
# On Windows Server (PowerShell as Administrator)
|
||||
cd C:\path\to\roa2web\deployment\windows\scripts
|
||||
|
||||
# Enable HTTPS with self-signed certificate
|
||||
.\Enable-HTTPS.ps1
|
||||
|
||||
# Or specify custom settings
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "Default Web Site" -CertificateDnsName "10.0.20.36"
|
||||
```
|
||||
|
||||
### Option 2: Manual Setup
|
||||
|
||||
Follow the step-by-step instructions in the [Manual Configuration](#manual-configuration) section below.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Certificate Options
|
||||
|
||||
### Self-Signed Certificate (Development/Testing)
|
||||
|
||||
**Pros:**
|
||||
- Quick setup (5 minutes)
|
||||
- No cost
|
||||
- Works immediately
|
||||
|
||||
**Cons:**
|
||||
- Browser security warnings
|
||||
- Not trusted by default
|
||||
- Not recommended for production
|
||||
|
||||
**Use when:**
|
||||
- Internal development
|
||||
- Testing HTTPS functionality
|
||||
- Private internal network
|
||||
|
||||
### CA-Issued Certificate (Production)
|
||||
|
||||
**Pros:**
|
||||
- Trusted by all browsers
|
||||
- No security warnings
|
||||
- Professional appearance
|
||||
|
||||
**Cons:**
|
||||
- Requires domain name
|
||||
- May have cost (unless using Let's Encrypt)
|
||||
- More setup steps
|
||||
|
||||
**Use when:**
|
||||
- Production deployment
|
||||
- Public-facing application
|
||||
- Customer/client access
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Automated Setup Details
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Windows Server 2016+ (or Windows 10/11 Pro)
|
||||
- IIS installed and configured
|
||||
- Administrator privileges
|
||||
- ROA2WEB already deployed
|
||||
|
||||
### Running the Script
|
||||
|
||||
**Basic usage (auto-detect settings):**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Auto-detect your server's hostname and IP
|
||||
2. Create a self-signed certificate
|
||||
3. Configure HTTPS binding on IIS
|
||||
4. Enable HTTP to HTTPS redirect
|
||||
5. Test the configuration
|
||||
|
||||
**Custom DNS name:**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -CertificateDnsName "roa2web.company.com"
|
||||
```
|
||||
|
||||
**Use existing certificate:**
|
||||
|
||||
```powershell
|
||||
# List available certificates
|
||||
Get-ChildItem cert:\LocalMachine\My | Select-Object Thumbprint, Subject, FriendlyName
|
||||
|
||||
# Use specific certificate
|
||||
.\Enable-HTTPS.ps1 -UseExistingCert -CertThumbprint "ABC123..."
|
||||
```
|
||||
|
||||
**For IIS application (not site root):**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "Default Web Site"
|
||||
```
|
||||
|
||||
**Custom HTTPS port:**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -Port 8443
|
||||
```
|
||||
|
||||
### Script Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `IISSiteName` | String | "Default Web Site" | IIS site name |
|
||||
| `CertificateDnsName` | String | Auto-detect | DNS name for certificate |
|
||||
| `UseExistingCert` | Switch | False | Use existing certificate |
|
||||
| `CertThumbprint` | String | "" | Thumbprint of existing cert |
|
||||
| `Port` | Int | 443 | HTTPS port |
|
||||
| `IPAddress` | String | "*" | Bind to specific IP or all (*) |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Manual Configuration
|
||||
|
||||
### Step 1: Create SSL Certificate
|
||||
|
||||
#### Option A: Self-Signed Certificate
|
||||
|
||||
```powershell
|
||||
# Create certificate for IP address (10.0.20.36)
|
||||
$cert = New-SelfSignedCertificate `
|
||||
-DnsName "10.0.20.36" `
|
||||
-CertStoreLocation "cert:\LocalMachine\My" `
|
||||
-NotAfter (Get-Date).AddYears(5) `
|
||||
-FriendlyName "ROA2WEB SSL Certificate" `
|
||||
-KeyUsage DigitalSignature, KeyEncipherment `
|
||||
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")
|
||||
|
||||
# Display certificate info
|
||||
$cert | Select-Object Subject, Thumbprint, NotAfter
|
||||
```
|
||||
|
||||
#### Option B: CA-Issued Certificate
|
||||
|
||||
**Using IIS Manager:**
|
||||
|
||||
1. Open IIS Manager (`inetmgr`)
|
||||
2. Select your server in the left panel
|
||||
3. Double-click "Server Certificates"
|
||||
4. Click "Create Certificate Request..." in right panel
|
||||
5. Fill in certificate details:
|
||||
- Common Name: `10.0.20.36` (or your domain)
|
||||
- Organization: Your company name
|
||||
- City, State, Country
|
||||
6. Save the CSR file
|
||||
7. Submit CSR to Certificate Authority (CA)
|
||||
8. Once received, import certificate:
|
||||
- Click "Complete Certificate Request..."
|
||||
- Browse to certificate file
|
||||
- Give it a friendly name: "ROA2WEB SSL Certificate"
|
||||
|
||||
**Popular Certificate Authorities:**
|
||||
- **Let's Encrypt** (Free, automated): https://letsencrypt.org/
|
||||
- **DigiCert** (Commercial): https://www.digicert.com/
|
||||
- **Sectigo** (Commercial): https://sectigo.com/
|
||||
- **GlobalSign** (Commercial): https://www.globalsign.com/
|
||||
|
||||
### Step 2: Configure HTTPS Binding
|
||||
|
||||
```powershell
|
||||
Import-Module WebAdministration
|
||||
|
||||
# Add HTTPS binding to site
|
||||
New-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443 -IPAddress "*"
|
||||
|
||||
# Attach certificate to binding
|
||||
$cert = Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
|
||||
Push-Location
|
||||
Set-Location IIS:\SslBindings
|
||||
$cert | New-Item "0.0.0.0!443"
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
### Step 3: Enable HTTP to HTTPS Redirect
|
||||
|
||||
**Option A: Automatic (via script)**
|
||||
|
||||
The `Enable-HTTPS.ps1` script will offer to add the redirect rule automatically.
|
||||
|
||||
**Option B: Manual Edit**
|
||||
|
||||
Edit `C:\inetpub\wwwroot\roa2web\frontend\web.config`:
|
||||
|
||||
```xml
|
||||
<rewrite>
|
||||
<rules>
|
||||
<!-- Add this rule FIRST (before other rules) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
<!-- Existing rules below... -->
|
||||
<rule name="API Reverse Proxy" stopProcessing="true">
|
||||
<!-- ... -->
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
```
|
||||
|
||||
**Option C: IIS Manager GUI**
|
||||
|
||||
1. Open IIS Manager
|
||||
2. Navigate to your site
|
||||
3. Double-click "URL Rewrite"
|
||||
4. Click "Add Rule(s)..." → "Blank rule"
|
||||
5. Configure:
|
||||
- Name: `Force HTTPS`
|
||||
- Match URL: `(.*)`
|
||||
- Conditions: Add condition
|
||||
- Input: `{HTTPS}`
|
||||
- Pattern: `off`
|
||||
- Action:
|
||||
- Type: `Redirect`
|
||||
- URL: `https://{HTTP_HOST}/{R:1}`
|
||||
- Redirect type: `Permanent (301)`
|
||||
|
||||
### Step 4: Test Configuration
|
||||
|
||||
```powershell
|
||||
# Restart IIS site
|
||||
Restart-Website "Default Web Site"
|
||||
|
||||
# Test HTTPS locally (self-signed cert)
|
||||
Invoke-WebRequest https://localhost -SkipCertificateCheck
|
||||
|
||||
# Test from browser
|
||||
Start-Process "https://10.0.20.36/roa2web"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing HTTPS Configuration
|
||||
|
||||
### Browser Testing
|
||||
|
||||
1. **Access via HTTPS:**
|
||||
```
|
||||
https://10.0.20.36/roa2web
|
||||
```
|
||||
|
||||
2. **Check for security warnings:**
|
||||
- **Self-signed cert**: You'll see a warning - this is normal
|
||||
- **CA-issued cert**: No warning - connection is secure
|
||||
|
||||
3. **Verify redirect:**
|
||||
- Try accessing: `http://10.0.20.36/roa2web`
|
||||
- Should automatically redirect to: `https://10.0.20.36/roa2web`
|
||||
|
||||
4. **Check console for mixed content:**
|
||||
- Open browser DevTools (F12)
|
||||
- Look for mixed content warnings
|
||||
- All resources should load over HTTPS
|
||||
|
||||
### PowerShell Testing
|
||||
|
||||
```powershell
|
||||
# Test HTTPS binding
|
||||
Get-WebBinding -Name "Default Web Site" -Protocol "https"
|
||||
|
||||
# Test certificate
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
$cert | Select-Object Subject, Thumbprint, NotAfter, DnsNameList
|
||||
|
||||
# Test HTTPS response
|
||||
Invoke-WebRequest https://localhost/roa2web -SkipCertificateCheck
|
||||
|
||||
# Test from external IP
|
||||
Invoke-WebRequest https://10.0.20.36/roa2web -SkipCertificateCheck
|
||||
|
||||
# View IIS SSL bindings
|
||||
netsh http show sslcert
|
||||
```
|
||||
|
||||
### Network Testing
|
||||
|
||||
```bash
|
||||
# Test from Linux/Mac client
|
||||
curl -k https://10.0.20.36/roa2web
|
||||
|
||||
# Check certificate details
|
||||
openssl s_client -connect 10.0.20.36:443 -servername 10.0.20.36
|
||||
|
||||
# Test redirect
|
||||
curl -I http://10.0.20.36/roa2web
|
||||
# Should return: HTTP/1.1 301 Moved Permanently
|
||||
# Location: https://10.0.20.36/roa2web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### HTTPS Not Working
|
||||
|
||||
**Symptom:** Can't access site via HTTPS
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify HTTPS binding exists
|
||||
Get-WebBinding -Name "Default Web Site"
|
||||
|
||||
# Check if port 443 is listening
|
||||
netstat -ano | findstr :443
|
||||
|
||||
# View SSL certificate bindings
|
||||
netsh http show sslcert
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```powershell
|
||||
# Remove and recreate binding
|
||||
Remove-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
|
||||
New-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
|
||||
|
||||
# Reattach certificate
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
Push-Location IIS:\SslBindings
|
||||
$cert | New-Item "0.0.0.0!443" -Force
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
### Certificate Warning in Browser
|
||||
|
||||
**Symptom:** Browser shows "Your connection is not private" or similar warning
|
||||
|
||||
**Cause:**
|
||||
- Self-signed certificate (not trusted by default)
|
||||
- Expired certificate
|
||||
- Hostname mismatch
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **For Development (self-signed):**
|
||||
- Click "Advanced" → "Proceed anyway"
|
||||
- This is expected behavior for self-signed certificates
|
||||
|
||||
2. **For Production:**
|
||||
- Replace with CA-issued certificate
|
||||
- Ensure certificate CN matches the URL you're accessing
|
||||
|
||||
3. **For Internal Network:**
|
||||
- Add certificate to Trusted Root CA:
|
||||
```powershell
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My\<THUMBPRINT>
|
||||
$store = Get-Item cert:\LocalMachine\Root
|
||||
$store.Open("ReadWrite")
|
||||
$store.Add($cert)
|
||||
$store.Close()
|
||||
```
|
||||
|
||||
### HTTP Not Redirecting to HTTPS
|
||||
|
||||
**Symptom:** HTTP URLs still work, no automatic redirect
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify web.config has redirect rule
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\frontend\web.config |
|
||||
Select-String "Force HTTPS"
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Ensure redirect rule is present in web.config
|
||||
- Verify rule is BEFORE other rewrite rules
|
||||
- Check rule is not disabled
|
||||
- Restart IIS site:
|
||||
```powershell
|
||||
Restart-Website "Default Web Site"
|
||||
```
|
||||
|
||||
### Mixed Content Warnings
|
||||
|
||||
**Symptom:** Console shows "Mixed Content" warnings
|
||||
|
||||
**Cause:** Some resources loading over HTTP instead of HTTPS
|
||||
|
||||
**Fix:**
|
||||
1. Check frontend code for hardcoded `http://` URLs
|
||||
2. Update to use relative URLs or `https://`
|
||||
3. Update API base URL in frontend config:
|
||||
```javascript
|
||||
// src/config.js or similar
|
||||
const API_BASE_URL = window.location.protocol === 'https:'
|
||||
? 'https://10.0.20.36/api'
|
||||
: 'http://10.0.20.36/api';
|
||||
```
|
||||
|
||||
### API Calls Failing After HTTPS
|
||||
|
||||
**Symptom:** Frontend loads but API calls fail with CORS or SSL errors
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify backend is accessible
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Check IIS URL Rewrite is forwarding correctly
|
||||
Get-WebConfiguration -Filter "system.webServer/rewrite/rules"
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Update CORS settings in FastAPI backend to allow HTTPS origin
|
||||
- Verify web.config proxy rules are correct
|
||||
- Check backend logs for errors
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
### 1. Use Strong Certificates
|
||||
|
||||
```powershell
|
||||
# For production, use CA-issued certificates
|
||||
# Minimum key size: 2048 bits (4096 recommended)
|
||||
# Use SHA-256 or higher
|
||||
```
|
||||
|
||||
### 2. Enable HSTS (Strict Transport Security)
|
||||
|
||||
Already configured in `web.config` (lines 115-124):
|
||||
|
||||
```xml
|
||||
<rule name="Add HSTS Header" preCondition="IsHTTPS">
|
||||
<match serverVariable="RESPONSE_Strict-Transport-Security" pattern=".*" />
|
||||
<action type="Rewrite" value="max-age=31536000; includeSubDomains" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
This tells browsers to always use HTTPS for your site.
|
||||
|
||||
### 3. Disable Weak SSL/TLS Protocols
|
||||
|
||||
```powershell
|
||||
# Disable SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.1
|
||||
# Enable only TLS 1.2 and TLS 1.3
|
||||
|
||||
# Run as Administrator
|
||||
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Force
|
||||
New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'Enabled' -Value 1 -PropertyType 'DWord'
|
||||
New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'DisabledByDefault' -Value 0 -PropertyType 'DWord'
|
||||
|
||||
# Restart required
|
||||
Restart-Computer
|
||||
```
|
||||
|
||||
### 4. Secure Cookie Settings
|
||||
|
||||
Update FastAPI backend cookie settings:
|
||||
|
||||
```python
|
||||
# backend/app/main.py
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=True, # Only send over HTTPS
|
||||
samesite="lax"
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Regular Certificate Renewal
|
||||
|
||||
- CA certificates typically expire in 1-2 years
|
||||
- Let's Encrypt certificates expire in 90 days
|
||||
- Set up reminders or automated renewal
|
||||
|
||||
```powershell
|
||||
# Check certificate expiry
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
$cert.NotAfter
|
||||
|
||||
# Days until expiry
|
||||
($cert.NotAfter - (Get-Date)).Days
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Documentation
|
||||
- [IIS SSL Configuration](https://docs.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
|
||||
- [Let's Encrypt](https://letsencrypt.org/)
|
||||
- [SSL/TLS Best Practices](https://wiki.mozilla.org/Security/Server_Side_TLS)
|
||||
|
||||
### Testing Tools
|
||||
- [SSL Labs Server Test](https://www.ssllabs.com/ssltest/) - Comprehensive SSL/TLS analysis
|
||||
- [SSL Checker](https://www.sslshopper.com/ssl-checker.html) - Quick certificate validation
|
||||
- [Why No Padlock?](https://www.whynopadlock.com/) - Find mixed content issues
|
||||
|
||||
### Certificate Providers
|
||||
- **Free:**
|
||||
- [Let's Encrypt](https://letsencrypt.org/) (Automated, 90-day validity)
|
||||
- [ZeroSSL](https://zerossl.com/) (Free tier available)
|
||||
|
||||
- **Commercial:**
|
||||
- [DigiCert](https://www.digicert.com/)
|
||||
- [Sectigo](https://sectigo.com/)
|
||||
- [GlobalSign](https://www.globalsign.com/)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```powershell
|
||||
# View all certificates
|
||||
Get-ChildItem cert:\LocalMachine\My
|
||||
|
||||
# View IIS bindings
|
||||
Get-WebBinding -Name "Default Web Site"
|
||||
|
||||
# View SSL bindings
|
||||
netsh http show sslcert
|
||||
|
||||
# Test HTTPS
|
||||
Invoke-WebRequest https://localhost -SkipCertificateCheck
|
||||
|
||||
# Restart IIS site
|
||||
Restart-Website "Default Web Site"
|
||||
|
||||
# Enable HTTPS (automated script)
|
||||
.\Enable-HTTPS.ps1
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- **IIS bindings**: IIS Manager → Site → Bindings
|
||||
- **web.config**: `C:\inetpub\wwwroot\roa2web\frontend\web.config`
|
||||
- **Certificates**: Certificate Manager (`certmgr.msc`)
|
||||
- **Backend config**: `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
|
||||
### Access Points
|
||||
|
||||
- **HTTP**: `http://10.0.20.36/roa2web` (redirects to HTTPS)
|
||||
- **HTTPS**: `https://10.0.20.36/roa2web`
|
||||
- **API**: `https://10.0.20.36/api/*` (proxied to backend)
|
||||
- **Health**: `https://10.0.20.36/health`
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-01-18*
|
||||
*ROA2WEB HTTPS Setup Guide v1.0*
|
||||
844
deployment/windows/docs/TELEGRAM_BOT_DEPLOYMENT.md
Normal file
844
deployment/windows/docs/TELEGRAM_BOT_DEPLOYMENT.md
Normal file
@@ -0,0 +1,844 @@
|
||||
# ROA2WEB Telegram Bot - Windows Server Deployment Guide
|
||||
|
||||
**Target Server**: Windows Server (10.0.20.36)
|
||||
**Deployment Method**: Windows Service (NSSM) - No Docker
|
||||
**Service Name**: ROA2WEB-TelegramBot
|
||||
**Internal API Port**: 8002
|
||||
**Created**: 2025-10-22
|
||||
**Last Updated**: 2025-10-22
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Deployment Architecture](#deployment-architecture)
|
||||
4. [Installation Steps](#installation-steps)
|
||||
5. [Configuration](#configuration)
|
||||
6. [Service Management](#service-management)
|
||||
7. [Database Backup](#database-backup)
|
||||
8. [Monitoring & Troubleshooting](#monitoring--troubleshooting)
|
||||
9. [Security Considerations](#security-considerations)
|
||||
10. [Maintenance Procedures](#maintenance-procedures)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The ROA2WEB Telegram Bot is deployed as a Windows Service on the same server as the backend (10.0.20.36). It provides an alternative conversational interface to ROA2WEB using Claude Agent SDK.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Conversational AI**: Claude Agent SDK with 5 custom tools
|
||||
- **Account Linking**: Secure linking between Telegram and Oracle accounts
|
||||
- **Real-time Queries**: Dashboard, invoices, treasury, exports
|
||||
- **Database**: Standalone SQLite for Telegram-specific data
|
||||
- **Internal API**: FastAPI endpoint for backend callbacks (port 8002)
|
||||
- **Production Ready**: All components use real backend integration (no mocks)
|
||||
|
||||
### Deployment Method
|
||||
|
||||
- **Windows Service**: Runs via NSSM (Non-Sucking Service Manager)
|
||||
- **No Docker**: Direct Windows deployment (same pattern as backend)
|
||||
- **Auto-start**: Service starts automatically on server boot
|
||||
- **Auto-recovery**: Service restarts automatically on failure
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
|
||||
- **Operating System**: Windows Server 2016+ or Windows 10/11
|
||||
- **Python**: 3.11 or higher
|
||||
- **RAM**: Minimum 512 MB (dedicated to service)
|
||||
- **Disk Space**: Minimum 500 MB
|
||||
- **Network**: Access to localhost:8000 (backend API)
|
||||
|
||||
### Required Credentials
|
||||
|
||||
1. **Telegram Bot Token**
|
||||
- Get from @BotFather on Telegram
|
||||
- Command: `/newbot` or `/mybots`
|
||||
- Example: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
|
||||
|
||||
2. **Claude Authentication** (Choose ONE method)
|
||||
|
||||
**Method A: Claude Pro/Max Subscription** ⭐ **RECOMMENDED**
|
||||
- ✅ **NO API key needed**
|
||||
- ✅ **NO additional costs** (included in your subscription)
|
||||
- ✅ Uses browser authentication
|
||||
- See: [Claude Authentication Setup](#claude-authentication-setup) below
|
||||
|
||||
**Method B: Claude API Key** (Alternative)
|
||||
- Get from Anthropic Console: https://console.anthropic.com/settings/keys
|
||||
- Example: `sk-ant-api03-XXXXXXXX...`
|
||||
- ⚠️ Usage-based billing applies
|
||||
- Takes precedence over Method A if both are configured
|
||||
|
||||
3. **Backend Access**
|
||||
- Backend should be running on http://localhost:8000
|
||||
- Verify: `Invoke-WebRequest http://localhost:8000/health`
|
||||
|
||||
### Software Prerequisites
|
||||
|
||||
- **PowerShell 5.1+**: Built into Windows Server
|
||||
- **Python 3.11+**: Will be installed if missing (via Chocolatey)
|
||||
- **NSSM**: Will be installed automatically by installation script
|
||||
|
||||
---
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\telegram-bot\
|
||||
├── app\ # Application source code
|
||||
│ ├── main.py # Entry point, Claude Agent wrapper
|
||||
│ ├── bot\ # Telegram bot handlers
|
||||
│ ├── agent\ # Claude Agent tools and sessions
|
||||
│ ├── auth\ # Account linking logic
|
||||
│ ├── db\ # SQLite database operations
|
||||
│ ├── api\ # Backend API client
|
||||
│ └── internal_api.py # FastAPI internal API
|
||||
├── venv\ # Python virtual environment
|
||||
├── data\ # SQLite database
|
||||
│ └── telegram_bot.db # Main database file
|
||||
├── logs\ # Application logs
|
||||
│ ├── stdout.log # Service output
|
||||
│ ├── stderr.log # Service errors
|
||||
│ └── backup.log # Backup operations
|
||||
├── backups\ # Database backups
|
||||
├── temp\ # Temporary files
|
||||
├── scripts\ # PowerShell management scripts
|
||||
├── config\ # Configuration templates
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Environment configuration
|
||||
```
|
||||
|
||||
### Service Integration
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Windows Server 10.0.20.36 │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ IIS (Port 80) │ │ ROA2WEB-Backend │ │
|
||||
│ │ Frontend Static │ │ (Port 8000) │ │
|
||||
│ └──────────────────┘ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ │ HTTP API Calls │
|
||||
│ │ │
|
||||
│ ┌─────────────────▼──────────────┐ │
|
||||
│ │ ROA2WEB-TelegramBot │ │
|
||||
│ │ (Port 8002 - Internal API) │ │
|
||||
│ │ - Telegram Bot Handlers │ │
|
||||
│ │ - Claude Agent SDK │ │
|
||||
│ │ - SQLite Database │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Telegram Bot API
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Telegram │
|
||||
│ Cloud │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Build Deployment Package (Development Machine)
|
||||
|
||||
On your development machine (WSL/Linux), run:
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
./Build-TelegramBot.ps1
|
||||
```
|
||||
|
||||
This creates the deployment package at:
|
||||
```
|
||||
../deploy-package/telegram-bot/
|
||||
```
|
||||
|
||||
**Package Contents**:
|
||||
- `app/` - Application source code
|
||||
- `requirements.txt` - Python dependencies
|
||||
- `.env.example` - Configuration template
|
||||
- `scripts/` - PowerShell management scripts
|
||||
- `config/` - Production config templates
|
||||
- `README.txt` - Deployment instructions
|
||||
|
||||
### Step 2: Transfer Package to Server
|
||||
|
||||
**Option A: Network Share** (Recommended)
|
||||
```powershell
|
||||
# On development machine
|
||||
Copy-Item -Path ./deploy-package/telegram-bot -Destination \\10.0.20.36\C$\Temp\telegram-bot-deploy -Recurse
|
||||
```
|
||||
|
||||
**Option B: RDP**
|
||||
1. Connect to server via RDP: `mstsc /v:10.0.20.36`
|
||||
2. Manually copy deployment package to `C:\Temp\telegram-bot-deploy`
|
||||
|
||||
### Step 3: Run Installation Script
|
||||
|
||||
On Windows Server (10.0.20.36), open PowerShell as Administrator:
|
||||
|
||||
```powershell
|
||||
cd C:\Temp\telegram-bot-deploy\scripts
|
||||
.\Install-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Installation Process**:
|
||||
1. ✅ Checks Python 3.11+ installation
|
||||
2. ✅ Installs NSSM (service manager)
|
||||
3. ✅ Creates directory structure
|
||||
4. ✅ Creates Python virtual environment
|
||||
5. ✅ Installs Python dependencies
|
||||
6. ✅ Creates Windows Service (ROA2WEB-TelegramBot)
|
||||
7. ✅ Creates .env configuration template
|
||||
|
||||
**Installation Output**:
|
||||
```
|
||||
====================================================================
|
||||
ROA2WEB TELEGRAM BOT INSTALLATION COMPLETED
|
||||
====================================================================
|
||||
|
||||
Installation Details:
|
||||
Install Path: C:\inetpub\wwwroot\roa2web\telegram-bot
|
||||
Service Name: ROA2WEB-TelegramBot
|
||||
Internal API Port: 8002
|
||||
|
||||
Next Steps:
|
||||
1. Edit configuration: C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
2. Start service: .\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Step 4: Configure Environment
|
||||
|
||||
Edit the `.env` file:
|
||||
|
||||
```powershell
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
```
|
||||
|
||||
**Required Configuration**:
|
||||
```env
|
||||
# CRITICAL: Update this value
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_from_@BotFather
|
||||
|
||||
# Claude Authentication: Leave empty to use Claude Pro/Max subscription
|
||||
CLAUDE_API_KEY=
|
||||
|
||||
# Verify these are correct
|
||||
BACKEND_URL=http://localhost:8000
|
||||
SQLITE_DB_PATH=C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
INTERNAL_API_PORT=8002
|
||||
LOG_LEVEL=INFO
|
||||
ENVIRONMENT=production
|
||||
```
|
||||
|
||||
**Get Bot Token**:
|
||||
1. Open Telegram, search for `@BotFather`
|
||||
2. Send `/newbot` or `/mybots`
|
||||
3. Copy token to `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
### Step 4a: Claude Authentication Setup
|
||||
|
||||
**Choose ONE authentication method:**
|
||||
|
||||
#### **Method A: Claude Pro/Max Subscription** ⭐ **RECOMMENDED**
|
||||
|
||||
If you have a Claude Pro or Claude Max subscription, use this method (NO API key needed!):
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-ClaudeAuth.ps1
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Script installs `claude-code` CLI (if not already installed)
|
||||
2. Opens your browser for authentication
|
||||
3. Log in with your Claude Pro/Max account
|
||||
4. Authorize the application
|
||||
5. Credentials are saved to: `%APPDATA%\claude\credentials.json`
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
====================================================================
|
||||
IMPORTANT: Browser Authentication Required
|
||||
====================================================================
|
||||
|
||||
1. A browser window will open
|
||||
2. Log in with your Claude Pro/Max account
|
||||
3. Authorize the application
|
||||
4. Return to this window after authentication
|
||||
|
||||
====================================================================
|
||||
|
||||
[*] Opening browser for authentication...
|
||||
[OK] Authentication successful!
|
||||
[OK] Credentials file found and valid
|
||||
[OK] .env will use Claude Pro subscription (browser login)
|
||||
|
||||
====================================================================
|
||||
CLAUDE AUTHENTICATION SETUP COMPLETE
|
||||
====================================================================
|
||||
```
|
||||
|
||||
**Alternative: Copy credentials from development machine**
|
||||
|
||||
If the server doesn't have browser access, authenticate on your local machine and copy credentials:
|
||||
|
||||
```powershell
|
||||
# On local machine (with browser):
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
claude-code login
|
||||
|
||||
# Copy credentials file to server
|
||||
Copy-Item "$env:APPDATA\claude\credentials.json" -Destination "\\10.0.20.36\C$\Temp\claude-credentials.json"
|
||||
|
||||
# On server:
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-ClaudeAuth.ps1 -Method copy -CredentialsPath "C:\Temp\claude-credentials.json"
|
||||
```
|
||||
|
||||
#### **Method B: Claude API Key** (Alternative)
|
||||
|
||||
If you prefer to use an API key instead:
|
||||
|
||||
1. Visit https://console.anthropic.com/settings/keys
|
||||
2. Create new key
|
||||
3. Edit `.env` and set `CLAUDE_API_KEY=sk-ant-api03-XXXXXXXX...`
|
||||
4. ⚠️ Usage-based billing applies
|
||||
|
||||
**Note:** API key takes precedence over browser login if both are configured.
|
||||
|
||||
### Step 5: Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
```
|
||||
[*] Starting ROA2WEB Telegram Bot Service...
|
||||
[*] Service start command issued
|
||||
[*] Waiting for service to start... (5/30)
|
||||
[OK] Service started successfully
|
||||
[OK] Health check passed: healthy
|
||||
[OK] Database: connected
|
||||
```
|
||||
|
||||
### Step 6: Verify Installation
|
||||
|
||||
**Check Service Status**:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
```
|
||||
|
||||
Expected: `Status = Running`
|
||||
|
||||
**Check Health Endpoint**:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health | ConvertFrom-Json
|
||||
```
|
||||
|
||||
Expected Output:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2025-10-22T14:30:00",
|
||||
"database": {
|
||||
"status": "connected",
|
||||
"users": 0,
|
||||
"pending_codes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test Bot on Telegram**:
|
||||
1. Open Telegram
|
||||
2. Search for your bot (name from @BotFather)
|
||||
3. Send `/start`
|
||||
4. Expected: Welcome message with linking instructions
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
See `.env.production.windows.telegram` in `config/` directory for complete reference.
|
||||
|
||||
**Key Settings**:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `TELEGRAM_BOT_TOKEN` | *required* | Bot token from @BotFather |
|
||||
| `CLAUDE_API_KEY` | *required* | API key from Anthropic |
|
||||
| `BACKEND_URL` | `http://localhost:8000` | Backend API URL |
|
||||
| `SQLITE_DB_PATH` | `C:\...\telegram_bot.db` | Database file location |
|
||||
| `INTERNAL_API_PORT` | `8002` | Internal API port |
|
||||
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG/INFO/WARN/ERROR) |
|
||||
| `ENVIRONMENT` | `production` | Environment name |
|
||||
| `AUTH_CODE_EXPIRY_MINUTES` | `15` | Linking code expiry time |
|
||||
| `SESSION_TIMEOUT_MINUTES` | `60` | User session timeout |
|
||||
| `MAX_CONVERSATION_HISTORY` | `20` | Max messages per session |
|
||||
|
||||
### Applying Configuration Changes
|
||||
|
||||
After editing `.env`:
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Management
|
||||
|
||||
### Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Stop Service
|
||||
|
||||
```powershell
|
||||
.\Stop-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Restart Service
|
||||
|
||||
```powershell
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Check Service Status
|
||||
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
|
||||
# Detailed info
|
||||
Get-Service ROA2WEB-TelegramBot | Select-Object *
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
**Real-time Logs** (tail -f equivalent):
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log -Tail 50 -Wait
|
||||
```
|
||||
|
||||
**Error Logs**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
**Backup Logs**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\backup.log -Tail 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Backup
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Backup-TelegramDB.ps1
|
||||
```
|
||||
|
||||
**Output**:
|
||||
- Backup file: `backups/telegram_bot_backup_YYYYMMDD-HHMMSS.db.zip`
|
||||
- Compressed and timestamped
|
||||
- Integrity tested automatically
|
||||
|
||||
### Setup Automated Daily Backup
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-DailyBackup.ps1
|
||||
```
|
||||
|
||||
**Configuration**:
|
||||
- Runs daily at 2:00 AM
|
||||
- Keeps last 30 days of backups
|
||||
- Runs as SYSTEM account
|
||||
- Logs all operations
|
||||
|
||||
**Verify Scheduled Task**:
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName "ROA2WEB-TelegramBot-Backup"
|
||||
```
|
||||
|
||||
**Run Backup Manually** (via Task Scheduler):
|
||||
```powershell
|
||||
Start-ScheduledTask -TaskName "ROA2WEB-TelegramBot-Backup"
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
|
||||
1. Stop service:
|
||||
```powershell
|
||||
.\Stop-TelegramBot.ps1
|
||||
```
|
||||
|
||||
2. Find backup file:
|
||||
```powershell
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\telegram-bot\backups | Sort-Object LastWriteTime -Descending
|
||||
```
|
||||
|
||||
3. Extract backup (if compressed):
|
||||
```powershell
|
||||
Expand-Archive -Path "backups\telegram_bot_backup_YYYYMMDD-HHMMSS.db.zip" -DestinationPath "temp\"
|
||||
```
|
||||
|
||||
4. Replace database:
|
||||
```powershell
|
||||
Copy-Item -Path "temp\telegram_bot_backup_YYYYMMDD-HHMMSS.db" -Destination "data\telegram_bot.db" -Force
|
||||
```
|
||||
|
||||
5. Start service:
|
||||
```powershell
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Troubleshooting
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Service Health**:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot | Select-Object Name, Status, StartType
|
||||
```
|
||||
|
||||
**API Health**:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
**Database Stats**:
|
||||
```powershell
|
||||
(Invoke-WebRequest http://localhost:8002/internal/stats).Content | ConvertFrom-Json
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Service Won't Start
|
||||
|
||||
**Symptoms**: Service shows "Stopped" or "Starting" forever
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Missing .env file** → Create from `.env.example`
|
||||
2. **Invalid bot token** → Check `TELEGRAM_BOT_TOKEN`
|
||||
3. **Invalid Claude API key** → Check `CLAUDE_API_KEY`
|
||||
4. **Port 8002 already in use** → Change `INTERNAL_API_PORT`
|
||||
5. **Backend not running** → Start ROA2WEB-Backend service
|
||||
|
||||
**Solutions**:
|
||||
```powershell
|
||||
# Fix config
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
|
||||
# Check port availability
|
||||
Get-NetTCPConnection -LocalPort 8002
|
||||
|
||||
# Restart service
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
#### Bot Not Responding on Telegram
|
||||
|
||||
**Symptoms**: Bot doesn't reply to messages
|
||||
|
||||
**Diagnosis**:
|
||||
1. Check service status:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
```
|
||||
2. Check health endpoint:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
3. Check logs for errors:
|
||||
```powershell
|
||||
Get-Content logs\stderr.log -Tail 50
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Service stopped** → Start service
|
||||
2. **Invalid bot token** → Update `.env`
|
||||
3. **Network issues** → Check internet connectivity
|
||||
4. **Bot blocked by user** → User must /start bot again
|
||||
|
||||
#### Account Linking Fails
|
||||
|
||||
**Symptoms**: `/link CODE` returns error
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
# Check internal API
|
||||
Invoke-WebRequest http://localhost:8002/internal/stats
|
||||
|
||||
# Check backend connectivity
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Code expired** (15 min) → Generate new code
|
||||
2. **Backend unreachable** → Check ROA2WEB-Backend service
|
||||
3. **Database error** → Check SQLite file permissions
|
||||
|
||||
#### Database Errors
|
||||
|
||||
**Symptoms**: "Database locked" or "Cannot open database"
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
# Check database file
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
# Check file permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
```powershell
|
||||
# Stop service
|
||||
.\Stop-TelegramBot.ps1
|
||||
|
||||
# Fix permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\data /grant "SYSTEM:(OI)(CI)F" /T
|
||||
|
||||
# Restart service
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**NEVER**:
|
||||
- Commit `.env` to git
|
||||
- Share bot token or API keys
|
||||
- Log sensitive data
|
||||
|
||||
**ALWAYS**:
|
||||
- Keep backups of `.env` in secure location
|
||||
- Rotate API keys periodically
|
||||
- Use strong file permissions
|
||||
|
||||
**File Permissions**:
|
||||
```powershell
|
||||
# Restrict .env to SYSTEM and Administrators only
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\.env /grant "SYSTEM:F" /grant "Administrators:F" /inheritance:r
|
||||
```
|
||||
|
||||
### Network Security
|
||||
|
||||
- **Internal API (8002)**: Bind to 127.0.0.1 (localhost only)
|
||||
- **Backend API (8000)**: Already on localhost
|
||||
- **No Firewall Rules Needed**: All communication is local
|
||||
|
||||
### Bot Security
|
||||
|
||||
- **Account Linking**: 8-character codes, 15-minute expiry
|
||||
- **JWT Tokens**: Signed and verified by backend
|
||||
- **Rate Limiting**: Built into authentication middleware
|
||||
- **Session Timeout**: 60 minutes of inactivity
|
||||
|
||||
---
|
||||
|
||||
## Maintenance Procedures
|
||||
|
||||
### Updates and Deployments
|
||||
|
||||
1. **Build new deployment package** (dev machine):
|
||||
```bash
|
||||
./Build-TelegramBot.ps1
|
||||
```
|
||||
|
||||
2. **Transfer to server**:
|
||||
```powershell
|
||||
Copy-Item -Path ./deploy-package/telegram-bot -Destination \\10.0.20.36\C$\Temp\telegram-bot-update -Recurse
|
||||
```
|
||||
|
||||
3. **Deploy update** (server):
|
||||
```powershell
|
||||
cd C:\Temp\telegram-bot-update\scripts
|
||||
.\Deploy-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Deployment Features**:
|
||||
- ✅ Automatic backup before deployment
|
||||
- ✅ Stops service, updates files, restarts service
|
||||
- ✅ Preserves `.env` configuration
|
||||
- ✅ Automatic rollback on failure
|
||||
- ✅ Health check after deployment
|
||||
|
||||
### Log Rotation
|
||||
|
||||
Logs are automatically rotated by Python logging:
|
||||
- **Max size**: 10 MB per file
|
||||
- **Backups**: 5 old log files kept
|
||||
- **Location**: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\`
|
||||
|
||||
**Manual cleanup**:
|
||||
```powershell
|
||||
# Delete old logs (older than 30 days)
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\telegram-bot\logs\*.log |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
|
||||
Remove-Item -Force
|
||||
```
|
||||
|
||||
### Database Maintenance
|
||||
|
||||
**Cleanup expired data** (automatic via scheduled task):
|
||||
- Expired auth codes (older than 15 minutes)
|
||||
- Old sessions (inactive for 60+ minutes)
|
||||
- Runs hourly automatically
|
||||
|
||||
**Manual cleanup**:
|
||||
```sql
|
||||
-- Connect to database
|
||||
sqlite3 C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
-- Delete expired codes
|
||||
DELETE FROM telegram_auth_codes WHERE created_at < datetime('now', '-15 minutes');
|
||||
|
||||
-- Delete old sessions
|
||||
DELETE FROM telegram_sessions WHERE updated_at < datetime('now', '-60 minutes');
|
||||
```
|
||||
|
||||
### Service Health Monitoring
|
||||
|
||||
**Daily Checks** (manual or script):
|
||||
```powershell
|
||||
# Service status
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
|
||||
# Health endpoint
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
|
||||
# Check logs for errors
|
||||
Get-Content logs\stderr.log -Tail 50 | Select-String "ERROR"
|
||||
|
||||
# Check database size
|
||||
(Get-Item data\telegram_bot.db).Length / 1MB
|
||||
```
|
||||
|
||||
**Weekly Checks**:
|
||||
- Review backup logs
|
||||
- Check backup retention (30 days)
|
||||
- Review disk space usage
|
||||
- Check for Windows updates
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### PowerShell Scripts Reference
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `Install-TelegramBot.ps1` | Initial installation |
|
||||
| `Deploy-TelegramBot.ps1` | Deploy updates |
|
||||
| `Build-TelegramBot.ps1` | Build deployment package |
|
||||
| `Start-TelegramBot.ps1` | Start service |
|
||||
| `Stop-TelegramBot.ps1` | Stop service |
|
||||
| `Restart-TelegramBot.ps1` | Restart service |
|
||||
| `Backup-TelegramDB.ps1` | Manual database backup |
|
||||
| `Setup-DailyBackup.ps1` | Configure automated backups |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/internal/health` | GET | Health check |
|
||||
| `/internal/stats` | GET | Database statistics |
|
||||
| `/internal/save-code` | POST | Save auth code (from backend) |
|
||||
| `/internal/verify-code` | POST | Verify auth code |
|
||||
|
||||
### Database Schema
|
||||
|
||||
**telegram_users**:
|
||||
```sql
|
||||
CREATE TABLE telegram_users (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
oracle_user_id INTEGER NOT NULL,
|
||||
oracle_username TEXT NOT NULL,
|
||||
jwt_token TEXT NOT NULL,
|
||||
jwt_refresh_token TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**telegram_auth_codes**:
|
||||
```sql
|
||||
CREATE TABLE telegram_auth_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
oracle_user_id INTEGER NOT NULL,
|
||||
oracle_username TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used INTEGER DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
**telegram_sessions**:
|
||||
```sql
|
||||
CREATE TABLE telegram_sessions (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
conversation_history TEXT, -- JSON
|
||||
session_data TEXT, -- JSON
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
**Documentation**:
|
||||
- Project README: `/mnt/e/proiecte/roa2web/roa2web/reports-app/telegram-bot/README.md`
|
||||
- Progress Tracker: `/mnt/e/proiecte/roa2web/roa2web/development/TELEGRAM_BOT_PROGRESS.md`
|
||||
- Production Deployment Plan: `/mnt/e/proiecte/roa2web/roa2web/development/TELEGRAM_BOT_PRODUCTION_DEPLOYMENT.md`
|
||||
|
||||
**Logs Location**:
|
||||
- Service Output: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log`
|
||||
- Service Errors: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log`
|
||||
- Backups: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\backup.log`
|
||||
|
||||
**Contact**: ROA2WEB Development Team
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-10-22
|
||||
**Status**: Production Ready
|
||||
917
deployment/windows/docs/WINDOWS_DEPLOYMENT.md
Normal file
917
deployment/windows/docs/WINDOWS_DEPLOYMENT.md
Normal file
@@ -0,0 +1,917 @@
|
||||
# ROA2WEB - Windows Server Deployment Guide
|
||||
|
||||
Complete deployment guide for ROA2WEB on Windows Server with IIS and Oracle Database.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Architecture](#architecture)
|
||||
4. [Initial Setup](#initial-setup)
|
||||
5. [Deployment Workflow](#deployment-workflow)
|
||||
6. [Configuration](#configuration)
|
||||
7. [Management](#management)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
9. [Maintenance](#maintenance)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This guide provides step-by-step instructions for deploying ROA2WEB on Windows Server with:
|
||||
|
||||
- **Backend**: FastAPI as Windows Service (port 8000)
|
||||
- **Frontend**: Vue.js static files served by IIS (port 80/443)
|
||||
- **Database**: Direct connection to local Oracle DB (no SSH tunnel)
|
||||
- **Reverse Proxy**: IIS with URL Rewrite for API routing
|
||||
|
||||
### Key Features
|
||||
|
||||
✅ Simple installation with PowerShell scripts
|
||||
✅ Minimal dependencies (Python + IIS)
|
||||
✅ Easy replication across multiple servers
|
||||
✅ Windows Service for backend (auto-start, auto-restart)
|
||||
✅ Production-ready configuration
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
|
||||
| Component | Requirement | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| **OS** | Windows Server 2016+ | Or Windows 10/11 Pro |
|
||||
| **RAM** | 4GB minimum | 8GB recommended |
|
||||
| **Disk** | 10GB free space | For application and logs |
|
||||
| **CPU** | 2 cores minimum | 4 cores recommended |
|
||||
|
||||
### Software Requirements
|
||||
|
||||
#### Required (will be installed automatically)
|
||||
|
||||
- **IIS** (Internet Information Services)
|
||||
- **Python 3.11+**
|
||||
- **NSSM** (Non-Sucking Service Manager)
|
||||
- **IIS URL Rewrite Module**
|
||||
- **IIS Application Request Routing (ARR)**
|
||||
|
||||
#### Pre-installed
|
||||
|
||||
- **Oracle Database** (local or network-accessible)
|
||||
- **Oracle Instant Client** (for Python oracledb)
|
||||
|
||||
#### On Development Machine
|
||||
|
||||
- **Node.js 16+** (for building frontend)
|
||||
- **Git** (optional, for cloning repository)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Deployment Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── backend\ # FastAPI application
|
||||
│ ├── app\ # Application code
|
||||
│ ├── requirements.txt # Python dependencies
|
||||
│ ├── .env # Environment configuration
|
||||
│ └── logs\ # Application logs
|
||||
│
|
||||
├── frontend\ # Vue.js static files
|
||||
│ ├── index.html
|
||||
│ ├── assets\
|
||||
│ ├── web.config # IIS configuration
|
||||
│ └── ...
|
||||
│
|
||||
├── logs\ # Service logs
|
||||
│ ├── backend-stdout.log
|
||||
│ └── backend-stderr.log
|
||||
│
|
||||
├── temp\ # Temporary files
|
||||
│
|
||||
└── backups\ # Deployment backups
|
||||
└── backup-YYYYMMDD-HHMMSS\
|
||||
```
|
||||
|
||||
### Network Flow
|
||||
|
||||
```
|
||||
Client Browser
|
||||
↓
|
||||
IIS (Port 80/443)
|
||||
↓
|
||||
├─→ /api/* ────→ Backend Service (localhost:8000)
|
||||
│ ↓
|
||||
│ Oracle Database (localhost:1521)
|
||||
│
|
||||
└─→ /* ─────────→ Frontend Static Files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Initial Setup
|
||||
|
||||
### Step 1: Install IIS
|
||||
|
||||
Open PowerShell as Administrator:
|
||||
|
||||
```powershell
|
||||
# Install IIS with required features
|
||||
Install-WindowsFeature -Name Web-Server -IncludeManagementTools
|
||||
|
||||
# Verify installation
|
||||
Get-WindowsFeature -Name Web-Server
|
||||
```
|
||||
|
||||
### Step 2: Prepare Deployment Package
|
||||
|
||||
**On your development machine (WSL/Windows):**
|
||||
|
||||
```bash
|
||||
# Navigate to deployment scripts
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
|
||||
# Build frontend and create deployment package
|
||||
./Build-Frontend.ps1
|
||||
|
||||
# Output will be in: ./deploy-package
|
||||
```
|
||||
|
||||
This creates a complete deployment package:
|
||||
```
|
||||
deploy-package/
|
||||
├── backend/ # Backend files
|
||||
├── frontend/ # Built Vue.js files
|
||||
├── config/ # Configuration templates
|
||||
└── README.txt # Deployment instructions
|
||||
```
|
||||
|
||||
### Step 3: Transfer to Server
|
||||
|
||||
**Option A: Network Share**
|
||||
```powershell
|
||||
# On development machine
|
||||
Copy-Item -Path .\deploy-package -Destination \\SERVER-IP\C$\Temp\roa2web -Recurse
|
||||
```
|
||||
|
||||
**Option B: Manual Transfer**
|
||||
- Zip the `deploy-package` folder
|
||||
- Transfer via RDP, FTP, or USB
|
||||
- Extract on server to `C:\Temp\roa2web`
|
||||
|
||||
### Step 4: Run Installation Script
|
||||
|
||||
**On Windows Server (PowerShell as Administrator):**
|
||||
|
||||
```powershell
|
||||
# Navigate to deployment scripts
|
||||
cd C:\path\to\roa2web\deployment\windows\scripts
|
||||
|
||||
# Run installation
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
# Installation will:
|
||||
# - Install Python, NSSM, IIS modules
|
||||
# - Create directory structure
|
||||
# - Install Python dependencies
|
||||
# - Create Windows Service
|
||||
# - Configure IIS website
|
||||
```
|
||||
|
||||
**Installation Parameters:**
|
||||
|
||||
```powershell
|
||||
# Custom installation path
|
||||
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web"
|
||||
|
||||
# Custom service port
|
||||
.\Install-ROA2WEB.ps1 -ServicePort 8001
|
||||
|
||||
# Skip Python installation (if already installed)
|
||||
.\Install-ROA2WEB.ps1 -SkipPython
|
||||
|
||||
# Skip IIS configuration
|
||||
.\Install-ROA2WEB.ps1 -SkipIIS
|
||||
```
|
||||
|
||||
### Step 5: Configure Application
|
||||
|
||||
**Edit configuration file:**
|
||||
|
||||
```powershell
|
||||
# Copy environment template
|
||||
Copy-Item C:\inetpub\wwwroot\roa2web\backend\config\.env.production.windows `
|
||||
C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
# Edit with your values
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
```
|
||||
|
||||
**Required configuration:**
|
||||
|
||||
```env
|
||||
# Oracle Database
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
# Database password - configure in .env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# JWT Secret (generate new one!)
|
||||
JWT_SECRET_KEY=GENERATE_STRONG_RANDOM_STRING_HERE
|
||||
```
|
||||
|
||||
**Generate JWT Secret:**
|
||||
|
||||
```powershell
|
||||
# PowerShell method
|
||||
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | % {[char]$_})
|
||||
|
||||
# Or use online tool: https://generate-secret.vercel.app/
|
||||
```
|
||||
|
||||
### Step 6: Start Services
|
||||
|
||||
```powershell
|
||||
# Start backend service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Check service status
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Check logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50
|
||||
```
|
||||
|
||||
### Step 7: Verify Installation
|
||||
|
||||
**Test endpoints:**
|
||||
|
||||
```powershell
|
||||
# Backend health check
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health"
|
||||
|
||||
# API documentation
|
||||
Start-Process "http://localhost:8000/docs"
|
||||
|
||||
# Frontend application
|
||||
Start-Process "http://localhost"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Deployment Workflow
|
||||
|
||||
### For Updates and New Deployments
|
||||
|
||||
**1. Build on Development Machine:**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1 -OutputPath "./deploy-$(date +%Y%m%d)"
|
||||
```
|
||||
|
||||
**2. Transfer to Server:**
|
||||
|
||||
```powershell
|
||||
# Copy deployment package to server
|
||||
Copy-Item -Path .\deploy-20250118 -Destination C:\Temp\roa2web-deploy -Recurse
|
||||
```
|
||||
|
||||
**3. Deploy on Server:**
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\deployment\windows\scripts
|
||||
|
||||
# Run deployment script
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
|
||||
# The script will:
|
||||
# - Create backup of current deployment
|
||||
# - Stop backend service
|
||||
# - Update backend and frontend files
|
||||
# - Install new Python dependencies (if changed)
|
||||
# - Restart backend service
|
||||
# - Validate deployment health
|
||||
```
|
||||
|
||||
**Deployment Options:**
|
||||
|
||||
```powershell
|
||||
# Update only backend
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateFrontend $false
|
||||
|
||||
# Update only frontend
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateBackend $false
|
||||
|
||||
# Skip backup (not recommended)
|
||||
.\Deploy-ROA2WEB.ps1 -BackupEnabled $false
|
||||
|
||||
# Skip service restart
|
||||
.\Deploy-ROA2WEB.ps1 -RestartService $false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Backend Configuration (.env)
|
||||
|
||||
**Location:** `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
|
||||
**Essential settings:**
|
||||
|
||||
```env
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
DEBUG=false
|
||||
|
||||
# Oracle Database
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
# Database password - configure in .env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# Connection Pool
|
||||
ORACLE_MIN_POOL_SIZE=2
|
||||
ORACLE_MAX_POOL_SIZE=10
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your_strong_secret_key
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRE_MINUTES=480
|
||||
|
||||
# Server Settings
|
||||
HOST=127.0.0.1
|
||||
PORT=8000
|
||||
WORKERS=4
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=C:\inetpub\wwwroot\roa2web\backend\logs\app.log
|
||||
```
|
||||
|
||||
### IIS Configuration (web.config)
|
||||
|
||||
**Location:** `C:\inetpub\wwwroot\roa2web\frontend\web.config`
|
||||
|
||||
This file is automatically created during installation. Key features:
|
||||
|
||||
- **SPA Routing**: All non-file requests fallback to `index.html`
|
||||
- **API Reverse Proxy**: `/api/*` routed to backend service
|
||||
- **Compression**: Gzip compression enabled
|
||||
- **Caching**: Static assets cached for 1 year
|
||||
- **Security Headers**: X-Frame-Options, CSP, HSTS
|
||||
|
||||
**No manual configuration needed** - works out of the box!
|
||||
|
||||
### Windows Service Configuration
|
||||
|
||||
**Service Name:** `ROA2WEB-Backend`
|
||||
**Startup Type:** Automatic
|
||||
**Recovery:** Restart on failure (5 second delay)
|
||||
|
||||
**View/Edit service:**
|
||||
|
||||
```powershell
|
||||
# Service properties
|
||||
Get-Service ROA2WEB-Backend | Format-List *
|
||||
|
||||
# Service configuration
|
||||
sc.exe qc ROA2WEB-Backend
|
||||
|
||||
# Modify with NSSM GUI
|
||||
nssm edit ROA2WEB-Backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Management
|
||||
|
||||
### Service Management
|
||||
|
||||
**PowerShell Scripts:**
|
||||
|
||||
```powershell
|
||||
# Start service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Stop service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restart service
|
||||
.\Restart-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
**Manual Service Management:**
|
||||
|
||||
```powershell
|
||||
# Start
|
||||
Start-Service ROA2WEB-Backend
|
||||
|
||||
# Stop
|
||||
Stop-Service ROA2WEB-Backend
|
||||
|
||||
# Restart
|
||||
Restart-Service ROA2WEB-Backend
|
||||
|
||||
# Status
|
||||
Get-Service ROA2WEB-Backend
|
||||
```
|
||||
|
||||
**Windows Services GUI:**
|
||||
|
||||
```powershell
|
||||
services.msc
|
||||
# Find: ROA2WEB Backend Service
|
||||
```
|
||||
|
||||
### Log Management
|
||||
|
||||
**Log Locations:**
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\logs\
|
||||
├── backend-stdout.log # Service output
|
||||
├── backend-stderr.log # Service errors
|
||||
└── app.log # Application log
|
||||
```
|
||||
|
||||
**View Logs:**
|
||||
|
||||
```powershell
|
||||
# Real-time monitoring
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Last 100 lines
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 100
|
||||
|
||||
# Search for errors
|
||||
Select-String -Path "C:\inetpub\wwwroot\roa2web\logs\*.log" -Pattern "ERROR|CRITICAL"
|
||||
|
||||
# Filter by date
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\app.log |
|
||||
Select-String -Pattern "2025-01-18"
|
||||
```
|
||||
|
||||
**Log Rotation:**
|
||||
|
||||
Logs are automatically rotated when they reach 10MB (configured in .env):
|
||||
|
||||
```env
|
||||
LOG_MAX_SIZE=10485760 # 10 MB
|
||||
LOG_BACKUP_COUNT=5 # Keep 5 old logs
|
||||
```
|
||||
|
||||
### IIS Management
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Website status
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# Start/Stop website
|
||||
Start-Website ROA2WEB
|
||||
Stop-Website ROA2WEB
|
||||
|
||||
# Application pool
|
||||
Get-WebAppPoolState ROA2WEB-AppPool
|
||||
Restart-WebAppPool ROA2WEB-AppPool
|
||||
|
||||
# View configuration
|
||||
Get-WebConfiguration -Filter "system.webServer/rewrite/rules"
|
||||
```
|
||||
|
||||
**IIS Manager GUI:**
|
||||
|
||||
```powershell
|
||||
inetmgr
|
||||
# Navigate to: Sites → ROA2WEB
|
||||
```
|
||||
|
||||
### Backup Management
|
||||
|
||||
**Deployment backups are automatic!**
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\backups\
|
||||
├── backup-20250118-103045\
|
||||
├── backup-20250118-154512\
|
||||
└── backup-20250117-090123\
|
||||
```
|
||||
|
||||
Last 10 backups are kept automatically.
|
||||
|
||||
**Manual Backup:**
|
||||
|
||||
```powershell
|
||||
# Create backup
|
||||
$date = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
Copy-Item -Path C:\inetpub\wwwroot\roa2web `
|
||||
-Destination C:\Backups\roa2web-$date `
|
||||
-Recurse -Exclude logs,temp,backups
|
||||
```
|
||||
|
||||
**Restore from Backup:**
|
||||
|
||||
```powershell
|
||||
# Stop service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restore files
|
||||
Copy-Item -Path C:\inetpub\wwwroot\roa2web\backups\backup-20250118-103045\* `
|
||||
-Destination C:\inetpub\wwwroot\roa2web `
|
||||
-Recurse -Force
|
||||
|
||||
# Start service
|
||||
.\Start-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
|
||||
**Symptom:** Backend service fails to start or stops immediately.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# View error log
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 50
|
||||
|
||||
# Test Python manually
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
1. **Python not found:**
|
||||
```powershell
|
||||
# Check Python installation
|
||||
python --version
|
||||
# Add to PATH if needed
|
||||
```
|
||||
|
||||
2. **Module import errors:**
|
||||
```powershell
|
||||
# Reinstall dependencies
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Oracle connection failed:**
|
||||
```powershell
|
||||
# Check Oracle listener
|
||||
lsnrctl status
|
||||
# Test connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
```
|
||||
|
||||
4. **Port already in use:**
|
||||
```powershell
|
||||
# Check what's using port 8000
|
||||
netstat -ano | findstr :8000
|
||||
# Kill process or change port in .env
|
||||
```
|
||||
|
||||
### Frontend Not Loading
|
||||
|
||||
**Symptom:** Blank page or 404 errors.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# IIS website running?
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# Files exist?
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\frontend\index.html
|
||||
|
||||
# Check IIS logs
|
||||
Get-Content C:\inetpub\logs\LogFiles\W3SVC*\*.log -Tail 50
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```powershell
|
||||
# Restart IIS site
|
||||
Stop-Website ROA2WEB
|
||||
Start-Website ROA2WEB
|
||||
|
||||
# Restart app pool
|
||||
Restart-WebAppPool ROA2WEB-AppPool
|
||||
|
||||
# Verify web.config
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\frontend\web.config
|
||||
```
|
||||
|
||||
### API Calls Failing (502/504 errors)
|
||||
|
||||
**Symptom:** Frontend loads but API calls fail.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Backend service running?
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Backend responding?
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health"
|
||||
|
||||
# Check ARR proxy
|
||||
Get-WebConfiguration -Filter "system.webServer/proxy"
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Enable ARR proxy:**
|
||||
```powershell
|
||||
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
|
||||
-Filter "system.webServer/proxy" `
|
||||
-Name "enabled" `
|
||||
-Value "True"
|
||||
```
|
||||
|
||||
2. **Check backend logs:**
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 100
|
||||
```
|
||||
|
||||
3. **Test backend directly:**
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/api/health"
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
**Symptom:** Backend starts but database queries fail.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Oracle client installed?
|
||||
dir $env:ORACLE_HOME
|
||||
|
||||
# TNS names configured?
|
||||
$env:TNS_ADMIN
|
||||
Get-Content $env:TNS_ADMIN\tnsnames.ora
|
||||
|
||||
# Test connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Install Oracle Instant Client:**
|
||||
- Download from: https://www.oracle.com/database/technologies/instant-client/downloads.html
|
||||
- Extract to C:\oracle\instantclient_19_x
|
||||
- Add to PATH
|
||||
|
||||
2. **Configure .env:**
|
||||
```env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
```
|
||||
|
||||
3. **Check Oracle service:**
|
||||
```powershell
|
||||
Get-Service Oracle*
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
**Symptom:** Access denied errors.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Check folder permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web
|
||||
|
||||
# Check service account
|
||||
sc.exe qc ROA2WEB-Backend
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```powershell
|
||||
# Grant IIS user read access
|
||||
icacls C:\inetpub\wwwroot\roa2web /grant IIS_IUSRS:(OI)(CI)RX
|
||||
|
||||
# Grant service account full access to backend
|
||||
icacls C:\inetpub\wwwroot\roa2web\backend /grant "NT AUTHORITY\LOCAL SERVICE":(OI)(CI)F
|
||||
```
|
||||
|
||||
### High CPU/Memory Usage
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Service resource usage
|
||||
Get-Process -Name python | Format-Table ProcessName, CPU, WS
|
||||
|
||||
# Check worker count
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\backend\.env | Select-String WORKERS
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```env
|
||||
# Reduce workers in .env
|
||||
WORKERS=2
|
||||
|
||||
# Reduce pool size
|
||||
ORACLE_MAX_POOL_SIZE=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Maintenance
|
||||
|
||||
### Regular Maintenance Tasks
|
||||
|
||||
**Daily:**
|
||||
- Check service status
|
||||
- Monitor disk space
|
||||
- Review error logs
|
||||
|
||||
```powershell
|
||||
# Daily check script
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-PSDrive C | Select-Object Used,Free
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 20
|
||||
```
|
||||
|
||||
**Weekly:**
|
||||
- Clean old logs
|
||||
- Verify backups
|
||||
- Update dependencies (if needed)
|
||||
|
||||
```powershell
|
||||
# Clean logs older than 30 days
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\logs\*.log.* |
|
||||
Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} |
|
||||
Remove-Item
|
||||
|
||||
# List backups
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\backups
|
||||
```
|
||||
|
||||
**Monthly:**
|
||||
- Review security updates
|
||||
- Performance optimization
|
||||
- Database maintenance
|
||||
|
||||
### Updating Python Dependencies
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
|
||||
# Update all packages
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
# Restart service
|
||||
Restart-Service ROA2WEB-Backend
|
||||
```
|
||||
|
||||
### Database Maintenance
|
||||
|
||||
```sql
|
||||
-- Connect to Oracle
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
|
||||
-- Check table statistics
|
||||
SELECT table_name, num_rows, last_analyzed
|
||||
FROM user_tables
|
||||
ORDER BY last_analyzed;
|
||||
|
||||
-- Update statistics
|
||||
EXEC DBMS_STATS.gather_schema_stats('CONTAFIN_ORACLE');
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
**Built-in health check:**
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health" |
|
||||
Select-Object StatusCode, Content
|
||||
```
|
||||
|
||||
**Windows Performance Monitor:**
|
||||
|
||||
```powershell
|
||||
perfmon
|
||||
# Add counters:
|
||||
# - Process > % Processor Time > python.exe
|
||||
# - Process > Private Bytes > python.exe
|
||||
# - Web Service > Current Connections
|
||||
```
|
||||
|
||||
### Security Updates
|
||||
|
||||
**Windows Updates:**
|
||||
|
||||
```powershell
|
||||
# Check for updates
|
||||
Get-WindowsUpdate
|
||||
|
||||
# Install updates
|
||||
Install-WindowsUpdate -AcceptAll
|
||||
```
|
||||
|
||||
**Python Security Updates:**
|
||||
|
||||
```powershell
|
||||
# Check for vulnerabilities
|
||||
pip check
|
||||
|
||||
# Update specific package
|
||||
pip install --upgrade fastapi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Documentation Files
|
||||
|
||||
- `config/.env.production.windows` - Configuration template
|
||||
- `config/web.config` - IIS configuration
|
||||
- `scripts/*.ps1` - PowerShell scripts
|
||||
|
||||
### PowerShell Scripts Reference
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|-------|
|
||||
| `Install-ROA2WEB.ps1` | Initial installation | `.\Install-ROA2WEB.ps1` |
|
||||
| `Deploy-ROA2WEB.ps1` | Deploy updates | `.\Deploy-ROA2WEB.ps1 -SourcePath <path>` |
|
||||
| `Build-Frontend.ps1` | Build Vue.js frontend | `.\Build-Frontend.ps1` |
|
||||
| `Start-ROA2WEB.ps1` | Start backend service | `.\Start-ROA2WEB.ps1` |
|
||||
| `Stop-ROA2WEB.ps1` | Stop backend service | `.\Stop-ROA2WEB.ps1` |
|
||||
| `Restart-ROA2WEB.ps1` | Restart backend service | `.\Restart-ROA2WEB.ps1` |
|
||||
|
||||
### Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
2. Review this documentation
|
||||
3. Contact: development-team@your-company.com
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```powershell
|
||||
# Service management
|
||||
.\Start-ROA2WEB.ps1
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Check status
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# View logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Health check
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Deploy update
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
```
|
||||
|
||||
### Key Locations
|
||||
|
||||
- **Application**: `C:\inetpub\wwwroot\roa2web\`
|
||||
- **Backend**: `C:\inetpub\wwwroot\roa2web\backend\`
|
||||
- **Frontend**: `C:\inetpub\wwwroot\roa2web\frontend\`
|
||||
- **Logs**: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
- **Config**: `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
- **Backups**: `C:\inetpub\wwwroot\roa2web\backups\`
|
||||
|
||||
### Access Points
|
||||
|
||||
- **Web App**: http://localhost or http://server-ip
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
- **Health Check**: http://localhost:8000/health
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-01-18*
|
||||
*Version: 2.0.0*
|
||||
*ROA2WEB Windows Deployment Guide*
|
||||
361
deployment/windows/scripts/Backup-TelegramDB.ps1
Normal file
361
deployment/windows/scripts/Backup-TelegramDB.ps1
Normal file
@@ -0,0 +1,361 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Backup ROA2WEB Telegram Bot SQLite Database
|
||||
|
||||
.DESCRIPTION
|
||||
This script creates a backup of the Telegram bot's SQLite database:
|
||||
- Copies database file with timestamp
|
||||
- Compresses backup (optional)
|
||||
- Cleans up old backups (keeps last 30 days)
|
||||
- Logs backup operations
|
||||
- Can run manually or via scheduled task
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER RetentionDays
|
||||
Number of days to keep backups (default: 30)
|
||||
|
||||
.PARAMETER Compress
|
||||
Compress backup file (default: true)
|
||||
|
||||
.PARAMETER LogToFile
|
||||
Write backup log to file (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1
|
||||
Standard backup with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1 -RetentionDays 60 -Compress $true
|
||||
Backup with 60-day retention and compression
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1 -LogToFile $false
|
||||
Backup without logging to file (console only)
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+
|
||||
Can be run manually or via Task Scheduler
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[int]$RetentionDays = 30,
|
||||
[bool]$Compress = $true,
|
||||
[bool]$LogToFile = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
$script:Config = @{
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
DatabaseFile = "telegram_bot.db"
|
||||
RetentionDays = $RetentionDays
|
||||
Compress = $Compress
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$logMessage = "[$timestamp] [$Level] $Message"
|
||||
|
||||
# Console output
|
||||
switch ($Level) {
|
||||
"ERROR" { Write-Host $logMessage -ForegroundColor Red }
|
||||
"WARN" { Write-Host $logMessage -ForegroundColor Yellow }
|
||||
"SUCCESS" { Write-Host $logMessage -ForegroundColor Green }
|
||||
default { Write-Host $logMessage -ForegroundColor Cyan }
|
||||
}
|
||||
|
||||
# File output
|
||||
if ($LogToFile) {
|
||||
$logFile = Join-Path $Config.LogsPath "backup.log"
|
||||
Add-Content -Path $logFile -Value $logMessage -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DatabaseFile {
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
|
||||
if (-not (Test-Path $dbPath)) {
|
||||
Write-Log "Database file not found: $dbPath" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Check if file is accessible (not locked)
|
||||
try {
|
||||
$stream = [System.IO.File]::Open($dbPath, 'Open', 'Read', 'Read')
|
||||
$stream.Close()
|
||||
Write-Log "Database file is accessible: $dbPath" -Level "INFO"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Log "Database file is locked or inaccessible: $_" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DatabaseSize {
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
$size = (Get-Item $dbPath).Length / 1KB
|
||||
return [math]::Round($size, 2)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
Write-Log "Created backup directory: $($Config.BackupPath)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-Database {
|
||||
Write-Log "Starting database backup..." -Level "INFO"
|
||||
|
||||
# Ensure backup directory exists
|
||||
New-BackupDirectory
|
||||
|
||||
# Generate backup filename with timestamp
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupFileName = "telegram_bot_backup_$timestamp.db"
|
||||
$backupFilePath = Join-Path $Config.BackupPath $backupFileName
|
||||
|
||||
# Source database path
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
|
||||
try {
|
||||
# Copy database file
|
||||
Copy-Item -Path $dbPath -Destination $backupFilePath -Force
|
||||
Write-Log "Database backed up to: $backupFileName" -Level "SUCCESS"
|
||||
|
||||
# Get backup file size
|
||||
$backupSize = (Get-Item $backupFilePath).Length / 1KB
|
||||
Write-Log "Backup size: $([math]::Round($backupSize, 2)) KB" -Level "INFO"
|
||||
|
||||
# Compress if enabled
|
||||
if ($Config.Compress) {
|
||||
$compressedPath = Compress-Backup -BackupPath $backupFilePath
|
||||
if ($compressedPath) {
|
||||
# Remove uncompressed backup
|
||||
Remove-Item -Path $backupFilePath -Force
|
||||
Write-Log "Uncompressed backup removed" -Level "INFO"
|
||||
return $compressedPath
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFilePath
|
||||
} catch {
|
||||
Write-Log "Backup failed: $_" -Level "ERROR"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Compress-Backup {
|
||||
param([string]$BackupPath)
|
||||
|
||||
Write-Log "Compressing backup..." -Level "INFO"
|
||||
|
||||
$zipPath = "$BackupPath.zip"
|
||||
|
||||
try {
|
||||
# Create ZIP archive
|
||||
Compress-Archive -Path $BackupPath -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||
|
||||
# Get compressed size
|
||||
$originalSize = (Get-Item $BackupPath).Length / 1KB
|
||||
$compressedSize = (Get-Item $zipPath).Length / 1KB
|
||||
$ratio = [math]::Round((1 - ($compressedSize / $originalSize)) * 100, 1)
|
||||
|
||||
Write-Log "Backup compressed: $([math]::Round($compressedSize, 2)) KB (saved $ratio%)" -Level "SUCCESS"
|
||||
|
||||
return $zipPath
|
||||
} catch {
|
||||
Write-Log "Compression failed: $_" -Level "WARN"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-OldBackups {
|
||||
Write-Log "Cleaning up old backups (keeping last $($Config.RetentionDays) days)..." -Level "INFO"
|
||||
|
||||
$cutoffDate = (Get-Date).AddDays(-$Config.RetentionDays)
|
||||
|
||||
try {
|
||||
# Get all backup files (both .db and .zip)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
|
||||
Where-Object {
|
||||
$_.Name -like "telegram_bot_backup_*.db" -or
|
||||
$_.Name -like "telegram_bot_backup_*.db.zip"
|
||||
}
|
||||
|
||||
$removedCount = 0
|
||||
$freedSpace = 0
|
||||
|
||||
foreach ($backup in $allBackups) {
|
||||
if ($backup.LastWriteTime -lt $cutoffDate) {
|
||||
$size = $backup.Length / 1MB
|
||||
Remove-Item -Path $backup.FullName -Force
|
||||
$removedCount++
|
||||
$freedSpace += $size
|
||||
Write-Log "Removed old backup: $($backup.Name)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
if ($removedCount -gt 0) {
|
||||
Write-Log "Removed $removedCount old backup(s), freed $([math]::Round($freedSpace, 2)) MB" -Level "SUCCESS"
|
||||
} else {
|
||||
Write-Log "No old backups to remove" -Level "INFO"
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Failed to clean up old backups: $_" -Level "WARN"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BackupStatistics {
|
||||
Write-Log "Backup Statistics:" -Level "INFO"
|
||||
|
||||
# Count backups
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
|
||||
Where-Object {
|
||||
$_.Name -like "telegram_bot_backup_*.db" -or
|
||||
$_.Name -like "telegram_bot_backup_*.db.zip"
|
||||
}
|
||||
|
||||
$totalBackups = $allBackups.Count
|
||||
$totalSize = ($allBackups | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Log " Total backups: $totalBackups" -Level "INFO"
|
||||
Write-Log " Total size: $([math]::Round($totalSize, 2)) MB" -Level "INFO"
|
||||
|
||||
# Latest backup
|
||||
if ($totalBackups -gt 0) {
|
||||
$latest = $allBackups | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
Write-Log " Latest backup: $($latest.Name) ($([math]::Round($latest.Length / 1KB, 2)) KB)" -Level "INFO"
|
||||
Write-Log " Created: $($latest.LastWriteTime)" -Level "INFO"
|
||||
}
|
||||
|
||||
# Oldest backup
|
||||
if ($totalBackups -gt 1) {
|
||||
$oldest = $allBackups | Sort-Object LastWriteTime | Select-Object -First 1
|
||||
$age = (Get-Date) - $oldest.LastWriteTime
|
||||
Write-Log " Oldest backup: $($oldest.Name) (Age: $([math]::Round($age.TotalDays, 1)) days)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BackupIntegrity {
|
||||
param([string]$BackupPath)
|
||||
|
||||
Write-Log "Testing backup integrity..." -Level "INFO"
|
||||
|
||||
try {
|
||||
# For ZIP files, test archive
|
||||
if ($BackupPath -like "*.zip") {
|
||||
# Try to extract to temp location
|
||||
$tempExtract = Join-Path $env:TEMP "telegram_bot_backup_test"
|
||||
if (Test-Path $tempExtract) {
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
}
|
||||
|
||||
Expand-Archive -Path $BackupPath -DestinationPath $tempExtract -Force
|
||||
|
||||
# Check if database file exists in extracted folder
|
||||
$extractedDb = Get-ChildItem -Path $tempExtract -Filter "*.db" -Recurse
|
||||
if ($extractedDb) {
|
||||
Write-Log "Backup integrity test PASSED (ZIP archive valid)" -Level "SUCCESS"
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
return $true
|
||||
} else {
|
||||
Write-Log "Backup integrity test FAILED (No database file in archive)" -Level "ERROR"
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
# For .db files, try to open
|
||||
$stream = [System.IO.File]::Open($BackupPath, 'Open', 'Read', 'Read')
|
||||
$stream.Close()
|
||||
Write-Log "Backup integrity test PASSED (Database file readable)" -Level "SUCCESS"
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Backup integrity test FAILED: $_" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BACKUP FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Database Backup
|
||||
SQLite database backup and retention management
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
Write-Log "Backup started by: $env:USERNAME" -Level "INFO"
|
||||
Write-Log "Backup script: $($MyInvocation.MyCommand.Path)" -Level "INFO"
|
||||
|
||||
# Check if database exists
|
||||
if (-not (Test-DatabaseFile)) {
|
||||
Write-Log "Backup aborted: Database file not accessible" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get current database size
|
||||
$dbSize = Get-DatabaseSize
|
||||
Write-Log "Current database size: $dbSize KB" -Level "INFO"
|
||||
|
||||
try {
|
||||
# Perform backup
|
||||
$backupPath = Backup-Database
|
||||
|
||||
if ($backupPath) {
|
||||
# Test backup integrity
|
||||
$integrityOk = Test-BackupIntegrity -BackupPath $backupPath
|
||||
|
||||
if ($integrityOk) {
|
||||
# Cleanup old backups
|
||||
Remove-OldBackups
|
||||
|
||||
# Show statistics
|
||||
Get-BackupStatistics
|
||||
|
||||
Write-Log "Backup completed successfully" -Level "SUCCESS"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Log "Backup created but failed integrity test" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Log "Backup failed" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Backup process failed: $_" -Level "ERROR"
|
||||
Write-Log $_.ScriptStackTrace -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main backup
|
||||
Main
|
||||
585
deployment/windows/scripts/Build-Frontend.ps1
Normal file
585
deployment/windows/scripts/Build-Frontend.ps1
Normal file
@@ -0,0 +1,585 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build ROA2WEB Frontend for Production Deployment
|
||||
|
||||
.DESCRIPTION
|
||||
This script builds the Vue.js frontend for Windows Server deployment:
|
||||
- Checks for Node.js installation
|
||||
- Installs npm dependencies
|
||||
- Builds production-optimized static files
|
||||
- Creates deployment package with backend files
|
||||
- Optionally transfers to remote server
|
||||
|
||||
.PARAMETER BackendSource
|
||||
Path to backend source (default: ../../reports-app/backend)
|
||||
|
||||
.PARAMETER FrontendSource
|
||||
Path to frontend source (default: ../../reports-app/frontend)
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Output path for deployment package (default: ./deploy-package)
|
||||
|
||||
.PARAMETER ServerPath
|
||||
Remote server path for automatic deployment (optional)
|
||||
|
||||
.PARAMETER ServerHost
|
||||
Remote server hostname/IP for automatic deployment (optional)
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1
|
||||
Build with defaults, output to ./deploy-package
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1 -OutputPath "D:\deployments\roa2web-$(Get-Date -Format 'yyyyMMdd')"
|
||||
Build to custom output path
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1 -ServerHost "10.0.20.170" -ServerPath "C:\Temp\roa2web-deploy"
|
||||
Build and transfer to remote server
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: Node.js 16+, npm
|
||||
Can run on: WSL, Windows, Linux
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BackendSource = "../../reports-app/backend",
|
||||
[string]$FrontendSource = "../../reports-app/frontend",
|
||||
[string]$OutputPath = "./deploy-package",
|
||||
[string]$ServerHost = "",
|
||||
[string]$ServerPath = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-NodeJS {
|
||||
Write-Step "Checking Node.js installation..."
|
||||
|
||||
try {
|
||||
$nodeVersion = node --version 2>&1
|
||||
$npmVersion = npm --version 2>&1
|
||||
|
||||
Write-Success "Node.js: $nodeVersion"
|
||||
Write-Success "npm: $npmVersion"
|
||||
|
||||
# Check minimum version (16.x)
|
||||
if ($nodeVersion -match "v(\d+)\.") {
|
||||
$major = [int]$matches[1]
|
||||
if ($major -lt 16) {
|
||||
throw "Node.js version 16+ required (found: $nodeVersion)"
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Node.js not found or version too old"
|
||||
Write-Host "`n Install Node.js from: https://nodejs.org/" -ForegroundColor Yellow
|
||||
Write-Host " Minimum version: 16.x" -ForegroundColor Yellow
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-FullPath {
|
||||
param([string]$Path)
|
||||
|
||||
$scriptDir = Split-Path -Parent $PSScriptRoot
|
||||
$fullPath = Join-Path $scriptDir $Path
|
||||
|
||||
# Convert to absolute path and resolve .. and . properly
|
||||
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
|
||||
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Build-Frontend {
|
||||
param([string]$SourcePath)
|
||||
|
||||
Write-Step "Building Vue.js frontend..."
|
||||
|
||||
if (-not (Test-Path $SourcePath)) {
|
||||
throw "Frontend source path not found: $SourcePath"
|
||||
}
|
||||
|
||||
Push-Location $SourcePath
|
||||
try {
|
||||
# Clean node_modules if it exists (to avoid EPERM errors)
|
||||
$nodeModulesPath = Join-Path $SourcePath "node_modules"
|
||||
if (Test-Path $nodeModulesPath) {
|
||||
Write-Step "Cleaning existing node_modules..."
|
||||
try {
|
||||
Remove-Item -Path $nodeModulesPath -Recurse -Force -ErrorAction Stop
|
||||
Write-Success "Cleaned node_modules"
|
||||
} catch {
|
||||
Write-Warning "Could not remove node_modules: $_"
|
||||
Write-Warning "Please close VS Code/IDE and try again, or run as Administrator"
|
||||
throw "Cannot proceed with locked node_modules. Close all IDEs and retry."
|
||||
}
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
Write-Step "Installing npm dependencies (this may take a minute)..."
|
||||
|
||||
# Show npm output for transparency, redirect to host to prevent capture
|
||||
npm install | Out-Default
|
||||
|
||||
# Verify node_modules was created
|
||||
if (-not (Test-Path $nodeModulesPath)) {
|
||||
throw "npm install failed: node_modules not created. Check errors above."
|
||||
}
|
||||
Write-Success "Dependencies installed"
|
||||
|
||||
# Build for production
|
||||
Write-Step "Building for production (this may take a minute)..."
|
||||
$env:NODE_ENV = "production"
|
||||
|
||||
# Show build output for transparency, redirect to host to prevent capture
|
||||
npm run build | Out-Default
|
||||
|
||||
Write-Success "Build completed"
|
||||
|
||||
# Verify dist folder
|
||||
$distPath = Join-Path $SourcePath "dist"
|
||||
if (-not (Test-Path $distPath)) {
|
||||
throw "Build failed: dist folder not found"
|
||||
}
|
||||
|
||||
$distFiles = Get-ChildItem -Path $distPath -Recurse -File
|
||||
$totalSize = ($distFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
Write-Success "Generated $(($distFiles).Count) files (Total: $([math]::Round($totalSize, 2)) MB)"
|
||||
|
||||
return $distPath
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-BackendFiles {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying backend files..."
|
||||
|
||||
if (-not (Test-Path $SourcePath)) {
|
||||
throw "Backend source path not found: $SourcePath"
|
||||
}
|
||||
|
||||
# Ensure destination exists
|
||||
if (-not (Test-Path $DestPath)) {
|
||||
New-Item -ItemType Directory -Path $DestPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Exclude directory names (will skip entire directory trees)
|
||||
$excludeDirs = @(
|
||||
"venv",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"logs",
|
||||
"temp",
|
||||
"node_modules"
|
||||
)
|
||||
|
||||
# Exclude file patterns
|
||||
$excludeFiles = @(
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.log",
|
||||
".env",
|
||||
".env.local"
|
||||
)
|
||||
|
||||
# Normalize source path (ensure trailing backslash for proper substring calculation)
|
||||
$normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\'
|
||||
|
||||
# Helper function to check if path should be excluded (using script: scope to access parent variables)
|
||||
$testExclude = {
|
||||
param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles)
|
||||
|
||||
# Check directory exclusions (match directory name exactly)
|
||||
if ($IsDirectory) {
|
||||
$dirName = Split-Path $RelativePath -Leaf
|
||||
if ($ExcludeDirs -contains $dirName) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
# Check if any parent directory should be excluded
|
||||
$pathParts = $RelativePath -split '[\\/]'
|
||||
foreach ($part in $pathParts) {
|
||||
if ($ExcludeDirs -contains $part) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
# Check file patterns
|
||||
if (-not $IsDirectory) {
|
||||
foreach ($pattern in $ExcludeFiles) {
|
||||
if ($RelativePath -like $pattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# Copy files
|
||||
Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object {
|
||||
# Calculate relative path safely
|
||||
if ($_.FullName.Length -le $normalizedSourcePath.Length) {
|
||||
return # Skip if path is too short (shouldn't happen, but safety check)
|
||||
}
|
||||
$relativePath = $_.FullName.Substring($normalizedSourcePath.Length)
|
||||
|
||||
# Check if should be excluded
|
||||
$shouldExclude = & $testExclude -RelativePath $relativePath -IsDirectory $_.PSIsContainer -ExcludeDirs $excludeDirs -ExcludeFiles $excludeFiles
|
||||
if ($shouldExclude) {
|
||||
return
|
||||
}
|
||||
|
||||
$destFile = Join-Path $DestPath $relativePath
|
||||
|
||||
if ($_.PSIsContainer) {
|
||||
if (-not (Test-Path $destFile)) {
|
||||
New-Item -ItemType Directory -Path $destFile -Force | Out-Null
|
||||
}
|
||||
} else {
|
||||
$destDir = Split-Path $destFile -Parent
|
||||
if (-not (Test-Path $destDir)) {
|
||||
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||
}
|
||||
}
|
||||
|
||||
$backendFiles = Get-ChildItem -Path $DestPath -Recurse -File
|
||||
Write-Success "Copied $(($backendFiles).Count) backend files"
|
||||
}
|
||||
|
||||
function New-DeploymentPackage {
|
||||
param(
|
||||
[string]$FrontendDistPath,
|
||||
[string]$BackendSourcePath,
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
Write-Step "Creating deployment package..."
|
||||
|
||||
# Create output directory
|
||||
if (Test-Path $OutputPath) {
|
||||
Write-Warning "Output path exists, cleaning..."
|
||||
Remove-Item -Path $OutputPath -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
|
||||
|
||||
# Create structure
|
||||
$frontendDest = Join-Path $OutputPath "frontend"
|
||||
$backendDest = Join-Path $OutputPath "backend"
|
||||
|
||||
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $backendDest -Force | Out-Null
|
||||
|
||||
# Copy frontend dist
|
||||
Write-Step "Copying frontend files..."
|
||||
Copy-Item -Path "$FrontendDistPath\*" -Destination $frontendDest -Recurse -Force
|
||||
Write-Success "Frontend files copied"
|
||||
|
||||
# Copy backend files
|
||||
Copy-BackendFiles -SourcePath $BackendSourcePath -DestPath $backendDest
|
||||
|
||||
# Copy shared modules (database, auth, utils)
|
||||
Write-Step "Copying shared modules..."
|
||||
$sharedSource = Join-Path (Split-Path (Split-Path $BackendSourcePath -Parent) -Parent) "shared"
|
||||
$sharedDest = Join-Path $OutputPath "shared"
|
||||
|
||||
if (Test-Path $sharedSource) {
|
||||
Copy-Item -Path $sharedSource -Destination $sharedDest -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests")
|
||||
Write-Success "Shared modules copied"
|
||||
} else {
|
||||
Write-Warning "Shared modules not found at: $sharedSource"
|
||||
}
|
||||
|
||||
# Copy deployment config
|
||||
$configSource = Join-Path (Split-Path -Parent $PSScriptRoot) "config"
|
||||
$configDest = Join-Path $OutputPath "config"
|
||||
|
||||
if (Test-Path $configSource) {
|
||||
Copy-Item -Path $configSource -Destination $configDest -Recurse -Force
|
||||
Write-Success "Config files copied"
|
||||
}
|
||||
|
||||
# Copy deployment scripts
|
||||
Write-Step "Copying deployment scripts..."
|
||||
$scriptsSource = $PSScriptRoot
|
||||
$scriptsDest = Join-Path $OutputPath "scripts"
|
||||
New-Item -ItemType Directory -Path $scriptsDest -Force | Out-Null
|
||||
|
||||
# List of scripts to include in deployment package
|
||||
$deploymentScripts = @(
|
||||
"Install-ROA2WEB.ps1",
|
||||
"Deploy-ROA2WEB.ps1",
|
||||
"Start-ROA2WEB.ps1",
|
||||
"Stop-ROA2WEB.ps1",
|
||||
"Restart-ROA2WEB.ps1"
|
||||
)
|
||||
|
||||
$copiedScripts = 0
|
||||
foreach ($script in $deploymentScripts) {
|
||||
$scriptPath = Join-Path $scriptsSource $script
|
||||
if (Test-Path $scriptPath) {
|
||||
Copy-Item -Path $scriptPath -Destination $scriptsDest -Force
|
||||
$copiedScripts++
|
||||
}
|
||||
}
|
||||
Write-Success "Copied $copiedScripts deployment scripts"
|
||||
|
||||
# Create README
|
||||
$readmePath = Join-Path $OutputPath "README.txt"
|
||||
$readme = @"
|
||||
================================================================================
|
||||
ROA2WEB DEPLOYMENT PACKAGE
|
||||
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
================================================================================
|
||||
|
||||
CONTENTS:
|
||||
---------
|
||||
backend/ FastAPI backend application files (Python)
|
||||
frontend/ Vue.js static files (built for production)
|
||||
config/ IIS configuration files (.env template, web.config)
|
||||
scripts/ PowerShell management scripts
|
||||
|
||||
DEPLOYMENT SCRIPTS:
|
||||
-------------------
|
||||
Install-ROA2WEB.ps1 First-time setup (Python venv, IIS site)
|
||||
Deploy-ROA2WEB.ps1 Update application files (auto-detects source)
|
||||
Start-ROA2WEB.ps1 Start backend service + IIS
|
||||
Stop-ROA2WEB.ps1 Stop backend service + IIS
|
||||
Restart-ROA2WEB.ps1 Quick restart
|
||||
|
||||
================================================================================
|
||||
DEPLOYMENT WORKFLOW
|
||||
================================================================================
|
||||
|
||||
>> FIRST TIME INSTALLATION:
|
||||
---------------------------
|
||||
1. Copy this entire folder to server (e.g., C:\Deploy\ROA2WEB-v1)
|
||||
|
||||
2. Open PowerShell as Administrator:
|
||||
cd C:\Deploy\ROA2WEB-v1\scripts
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
3. Configure environment:
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
4. Start services:
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
5. Access: http://localhost:8080
|
||||
|
||||
|
||||
>> UPDATES (New code version):
|
||||
-------------------------------
|
||||
1. Copy new deployment package to server (e.g., C:\Deploy\ROA2WEB-v2)
|
||||
|
||||
2. Open PowerShell as Administrator:
|
||||
cd C:\Deploy\ROA2WEB-v2\scripts
|
||||
|
||||
3. Deploy (automatically stops, updates, and starts):
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Deploy-ROA2WEB.ps1
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
Note: Deploy-ROA2WEB.ps1 auto-detects source path (no parameters needed!)
|
||||
|
||||
4. Done! Application updated with new code.
|
||||
|
||||
|
||||
>> QUICK OPERATIONS:
|
||||
--------------------
|
||||
Restart app: .\Restart-ROA2WEB.ps1
|
||||
Stop app: .\Stop-ROA2WEB.ps1
|
||||
Start app: .\Start-ROA2WEB.ps1
|
||||
|
||||
================================================================================
|
||||
REQUIREMENTS
|
||||
================================================================================
|
||||
- Windows Server 2016+ or Windows 10/11
|
||||
- IIS already installed (with ASP.NET Core Hosting Bundle)
|
||||
- Python 3.8+ installed
|
||||
- PowerShell 5.1+ (run as Administrator)
|
||||
|
||||
NOTES:
|
||||
------
|
||||
• Backend files do NOT include venv (virtual environment)
|
||||
• Install-ROA2WEB.ps1 creates venv and installs dependencies automatically
|
||||
• Deploy-ROA2WEB.ps1 creates backup before updating
|
||||
• .env files are preserved during updates
|
||||
• Application installs to: C:\inetpub\wwwroot\roa2web\
|
||||
|
||||
TROUBLESHOOTING:
|
||||
----------------
|
||||
Check logs: C:\inetpub\wwwroot\roa2web\logs\
|
||||
Backend logs: C:\inetpub\wwwroot\roa2web\backend\backend.log
|
||||
IIS logs: C:\inetpub\logs\LogFiles\
|
||||
|
||||
For detailed documentation, see: WINDOWS_DEPLOYMENT.md
|
||||
|
||||
================================================================================
|
||||
"@
|
||||
Set-Content -Path $readmePath -Value $readme -Force
|
||||
|
||||
# Calculate package size
|
||||
$packageFiles = Get-ChildItem -Path $OutputPath -Recurse -File
|
||||
$packageSize = ($packageFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Success "Deployment package created: $OutputPath"
|
||||
Write-Success "Total files: $(($packageFiles).Count)"
|
||||
Write-Success "Total size: $([math]::Round($packageSize, 2)) MB"
|
||||
|
||||
return $OutputPath
|
||||
}
|
||||
|
||||
function Copy-ToRemoteServer {
|
||||
param(
|
||||
[string]$LocalPath,
|
||||
[string]$ServerHost,
|
||||
[string]$ServerPath
|
||||
)
|
||||
|
||||
Write-Step "Transferring to remote server..."
|
||||
|
||||
try {
|
||||
# Check if remote server is accessible
|
||||
$pingResult = Test-Connection -ComputerName $ServerHost -Count 1 -Quiet
|
||||
|
||||
if (-not $pingResult) {
|
||||
Write-Warning "Server $ServerHost not reachable"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Use robocopy for efficient transfer (Windows)
|
||||
if ($IsWindows -or $env:OS -match "Windows") {
|
||||
$remotePath = "\\$ServerHost\$($ServerPath -replace ':', '$')"
|
||||
|
||||
Write-Host " [*] Copying to: $remotePath" -ForegroundColor Yellow
|
||||
|
||||
robocopy $LocalPath $remotePath /E /Z /R:3 /W:5 /MT:8 /NFL /NDL /NP
|
||||
|
||||
if ($LASTEXITCODE -le 7) {
|
||||
Write-Success "Files transferred successfully"
|
||||
return $true
|
||||
} else {
|
||||
Write-Error "Transfer failed with code: $LASTEXITCODE"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
# Use scp for Unix/WSL
|
||||
Write-Warning "Remote copy via SCP not yet implemented"
|
||||
Write-Host " Manual transfer required to: $ServerHost`:$ServerPath" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to transfer to server: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BUILD FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Frontend Build Script
|
||||
Build Vue.js frontend and create deployment package
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Resolve paths
|
||||
$backendSourcePath = Resolve-FullPath -Path $BackendSource
|
||||
$frontendSourcePath = Resolve-FullPath -Path $FrontendSource
|
||||
$outputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Resolve-FullPath -Path $OutputPath }
|
||||
|
||||
Write-Host "`nPaths:" -ForegroundColor Yellow
|
||||
Write-Host " Backend Source: $backendSourcePath"
|
||||
Write-Host " Frontend Source: $frontendSourcePath"
|
||||
Write-Host " Output Path: $outputPath"
|
||||
|
||||
# Check Node.js
|
||||
Test-NodeJS
|
||||
|
||||
# Build frontend
|
||||
$distPath = Build-Frontend -SourcePath $frontendSourcePath
|
||||
|
||||
# Create deployment package
|
||||
$packagePath = New-DeploymentPackage `
|
||||
-FrontendDistPath $distPath `
|
||||
-BackendSourcePath $backendSourcePath `
|
||||
-OutputPath $outputPath
|
||||
|
||||
# Transfer to server if specified
|
||||
if ($ServerHost -and $ServerPath) {
|
||||
$transferred = Copy-ToRemoteServer `
|
||||
-LocalPath $packagePath `
|
||||
-ServerHost $ServerHost `
|
||||
-ServerPath $ServerPath
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host " BUILD COMPLETED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Package: $packagePath" -ForegroundColor Yellow
|
||||
|
||||
if ($ServerHost) {
|
||||
Write-Host "`nNext Steps (on server $ServerHost):" -ForegroundColor Yellow
|
||||
Write-Host " cd $ServerPath"
|
||||
Write-Host " .\Deploy-ROA2WEB.ps1 -SourcePath ."
|
||||
} else {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Transfer '$packagePath' to your Windows Server"
|
||||
Write-Host " 2. On the server, run: Deploy-ROA2WEB.ps1 -SourcePath '<transferred-path>'"
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Build failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main build
|
||||
Main
|
||||
788
deployment/windows/scripts/Build-TelegramBot.ps1
Normal file
788
deployment/windows/scripts/Build-TelegramBot.ps1
Normal file
@@ -0,0 +1,788 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build ROA2WEB Telegram Bot for Windows Server Deployment
|
||||
|
||||
.DESCRIPTION
|
||||
This script creates a deployment package for the Telegram bot:
|
||||
- Copies application source files (app/)
|
||||
- Copies requirements.txt
|
||||
- Copies PowerShell deployment scripts
|
||||
- Creates .env.example template
|
||||
- Creates deployment README
|
||||
- Excludes development files (venv, data, logs, etc.)
|
||||
- Optionally transfers to remote server
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Path to telegram-bot source (default: ../../reports-app/telegram-bot)
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Output path for deployment package (default: ../deploy-package/telegram-bot)
|
||||
|
||||
.PARAMETER ServerHost
|
||||
Remote server hostname/IP for automatic deployment (optional)
|
||||
|
||||
.PARAMETER ServerPath
|
||||
Remote server path for automatic deployment (optional)
|
||||
|
||||
.PARAMETER Clean
|
||||
Clean output directory before building (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1
|
||||
Build with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1 -OutputPath "D:\deployments\telegram-bot-$(Get-Date -Format 'yyyyMMdd')"
|
||||
Build to custom output path
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1 -ServerHost "10.0.20.36" -ServerPath "C:\Temp\telegram-bot-deploy"
|
||||
Build and transfer to remote server
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+
|
||||
Can run on: WSL, Windows, Linux
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SourcePath = "../../../reports-app/telegram-bot",
|
||||
[string]$OutputPath = "../deploy-package/telegram-bot",
|
||||
[string]$ServerHost = "",
|
||||
[string]$ServerPath = "",
|
||||
[bool]$Clean = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Resolve-FullPath {
|
||||
param([string]$Path)
|
||||
|
||||
$scriptDir = $PSScriptRoot
|
||||
$fullPath = Join-Path $scriptDir $Path
|
||||
|
||||
# Convert to absolute path and resolve .. and . properly
|
||||
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
|
||||
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Test-SourceDirectory {
|
||||
param([string]$Path)
|
||||
|
||||
Write-Step "Validating source directory..."
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "Source path not found: $Path"
|
||||
}
|
||||
|
||||
# Check for required files/directories
|
||||
$requiredPaths = @(
|
||||
(Join-Path $Path "app"),
|
||||
(Join-Path $Path "requirements.txt")
|
||||
)
|
||||
|
||||
foreach ($reqPath in $requiredPaths) {
|
||||
if (-not (Test-Path $reqPath)) {
|
||||
throw "Required path not found: $reqPath"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Source directory validated: $Path"
|
||||
}
|
||||
|
||||
function New-CleanOutputDirectory {
|
||||
param([string]$Path)
|
||||
|
||||
if ($Clean -and (Test-Path $Path)) {
|
||||
Write-Step "Cleaning output directory..."
|
||||
Remove-Item -Path $Path -Recurse -Force
|
||||
Write-Success "Output directory cleaned"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
Write-Success "Created output directory: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-ApplicationFiles {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying application files..."
|
||||
|
||||
# Exclude patterns
|
||||
$excludeDirs = @(
|
||||
"venv",
|
||||
"data",
|
||||
"logs",
|
||||
"temp",
|
||||
"backups",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"tests",
|
||||
".git"
|
||||
)
|
||||
|
||||
$excludeFiles = @(
|
||||
".env",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.log",
|
||||
"*.db",
|
||||
".DS_Store",
|
||||
"Thumbs.db"
|
||||
)
|
||||
|
||||
# Create app directory in destination
|
||||
$destApp = Join-Path $DestPath "app"
|
||||
New-Item -ItemType Directory -Path $destApp -Force | Out-Null
|
||||
|
||||
# Copy app/ directory
|
||||
$sourceApp = Join-Path $SourcePath "app"
|
||||
$fileCount = 0
|
||||
|
||||
Get-ChildItem -Path $sourceApp -Recurse | ForEach-Object {
|
||||
# Check if in excluded directory
|
||||
$inExcludedDir = $false
|
||||
foreach ($excludeDir in $excludeDirs) {
|
||||
if ($_.FullName -like "*\$excludeDir\*" -or $_.FullName -like "*/$excludeDir/*") {
|
||||
$inExcludedDir = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($inExcludedDir) {
|
||||
return
|
||||
}
|
||||
|
||||
# Check if excluded file
|
||||
$isExcludedFile = $false
|
||||
foreach ($pattern in $excludeFiles) {
|
||||
if ($_.Name -like $pattern) {
|
||||
$isExcludedFile = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($isExcludedFile) {
|
||||
return
|
||||
}
|
||||
|
||||
# Calculate relative path and destination
|
||||
$relativePath = $_.FullName.Substring($sourceApp.Length)
|
||||
$destFile = Join-Path $destApp $relativePath
|
||||
|
||||
if ($_.PSIsContainer) {
|
||||
# Create directory
|
||||
if (-not (Test-Path $destFile)) {
|
||||
New-Item -ItemType Directory -Path $destFile -Force | Out-Null
|
||||
}
|
||||
} else {
|
||||
# Copy file
|
||||
$destFileDir = Split-Path $destFile -Parent
|
||||
if (-not (Test-Path $destFileDir)) {
|
||||
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||
$fileCount++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Copied $fileCount application files"
|
||||
}
|
||||
|
||||
function Copy-RequirementsFile {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying requirements.txt..."
|
||||
|
||||
$sourceReq = Join-Path $SourcePath "requirements.txt"
|
||||
$destReq = Join-Path $DestPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
Write-Success "requirements.txt copied"
|
||||
} else {
|
||||
Write-Warning "requirements.txt not found in source"
|
||||
}
|
||||
}
|
||||
|
||||
function New-EnvironmentTemplate {
|
||||
param([string]$DestPath)
|
||||
|
||||
Write-Step "Creating .env.example template..."
|
||||
|
||||
$envTemplate = @"
|
||||
# ROA2WEB Telegram Bot - Production Configuration Template
|
||||
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_from_@BotFather
|
||||
|
||||
# Claude API Configuration
|
||||
CLAUDE_API_KEY=your_production_claude_api_key_from_anthropic_console
|
||||
|
||||
# Backend API Configuration
|
||||
BACKEND_URL=http://localhost:8000
|
||||
BACKEND_TIMEOUT=30
|
||||
|
||||
# SQLite Database Configuration
|
||||
SQLITE_DB_PATH=C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
# Internal API Configuration (for backend callbacks)
|
||||
INTERNAL_API_HOST=127.0.0.1
|
||||
INTERNAL_API_PORT=8002
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=C:\inetpub\wwwroot\roa2web\telegram-bot\logs\bot.log
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
|
||||
# Authentication Configuration
|
||||
AUTH_CODE_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_THRESHOLD_MINUTES=5
|
||||
|
||||
# Session Configuration
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
MAX_CONVERSATION_HISTORY=20
|
||||
"@
|
||||
|
||||
$envPath = Join-Path $DestPath ".env.example"
|
||||
Set-Content -Path $envPath -Value $envTemplate -Encoding UTF8
|
||||
Write-Success ".env.example template created"
|
||||
}
|
||||
|
||||
function Copy-DeploymentScripts {
|
||||
param(
|
||||
[string]$SourceScriptsPath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying deployment scripts..."
|
||||
|
||||
$scriptsDir = Join-Path $DestPath "scripts"
|
||||
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
||||
|
||||
# List of scripts to copy
|
||||
$scripts = @(
|
||||
"Install-TelegramBot.ps1",
|
||||
"Deploy-TelegramBot.ps1",
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$copiedCount = 0
|
||||
foreach ($script in $scripts) {
|
||||
$sourcePath = Join-Path $SourceScriptsPath $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destScript = Join-Path $scriptsDir $script
|
||||
Copy-Item -Path $sourcePath -Destination $destScript -Force
|
||||
$copiedCount++
|
||||
} else {
|
||||
Write-Warning "Script not found: $script"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Copied $copiedCount deployment scripts"
|
||||
}
|
||||
|
||||
function Copy-ConfigTemplates {
|
||||
param(
|
||||
[string]$SourceConfigPath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying configuration templates..."
|
||||
|
||||
$configDir = Join-Path $DestPath "config"
|
||||
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
|
||||
|
||||
# Copy .env.production.windows.telegram if it exists
|
||||
$prodEnv = Join-Path $SourceConfigPath ".env.production.windows.telegram"
|
||||
if (Test-Path $prodEnv) {
|
||||
Copy-Item -Path $prodEnv -Destination (Join-Path $configDir ".env.production.windows.telegram") -Force
|
||||
Write-Success "Production config template copied"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DeploymentReadme {
|
||||
param([string]$DestPath)
|
||||
|
||||
Write-Step "Creating deployment README..."
|
||||
|
||||
$readme = @"
|
||||
# ROA2WEB Telegram Bot - Deployment Package
|
||||
|
||||
**Created**: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
**Version**: Production Deployment Package
|
||||
|
||||
## Contents
|
||||
|
||||
- ``app/`` - Telegram bot application source code
|
||||
- ``scripts/`` - PowerShell deployment and management scripts
|
||||
- ``config/`` - Configuration templates
|
||||
- ``requirements.txt`` - Python dependencies
|
||||
- ``.env.example`` - Environment configuration template
|
||||
- ``README.txt`` - This file
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### 1. Initial Installation
|
||||
|
||||
Run as Administrator on Windows Server:
|
||||
|
||||
```powershell
|
||||
cd scripts
|
||||
.\Install-TelegramBot.ps1
|
||||
```
|
||||
|
||||
This will:
|
||||
- Check Python 3.11+ installation
|
||||
- Install NSSM (service manager)
|
||||
- Create directory structure
|
||||
- Create virtual environment
|
||||
- Install Python dependencies
|
||||
- Create Windows Service (ROA2WEB-TelegramBot)
|
||||
- Create configuration template
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
Edit the ``.env`` file:
|
||||
|
||||
```powershell
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
```
|
||||
|
||||
Required settings:
|
||||
- ``TELEGRAM_BOT_TOKEN`` - Get from @BotFather on Telegram
|
||||
- ``CLAUDE_API_KEY`` - Get from Anthropic console
|
||||
- ``BACKEND_URL`` - Usually http://localhost:8000
|
||||
|
||||
### 3. Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### 4. Verify Deployment
|
||||
|
||||
Check health endpoint:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log -Tail 50 -Wait
|
||||
```
|
||||
|
||||
## Management Commands
|
||||
|
||||
- **Start**: ``.\Start-TelegramBot.ps1``
|
||||
- **Stop**: ``.\Stop-TelegramBot.ps1``
|
||||
- **Restart**: ``.\Restart-TelegramBot.ps1``
|
||||
- **Deploy Update**: ``.\Deploy-TelegramBot.ps1 -SourcePath "path\to\new\package"``
|
||||
- **Backup Database**: ``.\Backup-TelegramDB.ps1``
|
||||
- **Setup Daily Backup**: ``.\Setup-DailyBackup.ps1``
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\telegram-bot\
|
||||
├── app\ # Application source code
|
||||
├── venv\ # Python virtual environment
|
||||
├── data\ # SQLite database (telegram_bot.db)
|
||||
├── logs\ # Application logs
|
||||
├── backups\ # Database backups
|
||||
├── temp\ # Temporary files
|
||||
├── scripts\ # Management scripts
|
||||
├── config\ # Configuration templates
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Environment configuration
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
Check logs:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
### Bot not responding on Telegram
|
||||
|
||||
1. Verify service is running: ``Get-Service ROA2WEB-TelegramBot``
|
||||
2. Check health endpoint: ``Invoke-WebRequest http://localhost:8002/internal/health``
|
||||
3. Verify ``.env`` configuration (TELEGRAM_BOT_TOKEN)
|
||||
4. Check logs for errors
|
||||
|
||||
### Database errors
|
||||
|
||||
Run database backup and check integrity:
|
||||
|
||||
```powershell
|
||||
.\Backup-TelegramDB.ps1
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: ``C:\inetpub\wwwroot\roa2web\deployment\windows\docs\TELEGRAM_BOT_DEPLOYMENT.md``
|
||||
- Project repository: ROA2WEB on GitHub
|
||||
- Contact: ROA2WEB Team
|
||||
|
||||
"@
|
||||
|
||||
$readmePath = Join-Path $DestPath "README.txt"
|
||||
Set-Content -Path $readmePath -Value $readme -Encoding UTF8
|
||||
Write-Success "Deployment README created"
|
||||
}
|
||||
|
||||
function Copy-ClaudeCredentials {
|
||||
param([string]$PackagePath)
|
||||
|
||||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host " OPTIONAL: Claude Credentials" -ForegroundColor Yellow
|
||||
Write-Host ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "If you have Claude Pro/Max credentials from 'claude-code login',"
|
||||
Write-Host "you can include them in the deployment package for easy setup."
|
||||
Write-Host ""
|
||||
|
||||
$response = Read-Host "Copy Claude credentials to deployment package? (Y/N)"
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
# Try to find credentials automatically in both possible locations
|
||||
$possiblePaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\.credentials.json"), # Correct location
|
||||
(Join-Path $env:APPDATA "claude\credentials.json") # Alternative location
|
||||
)
|
||||
|
||||
$defaultCredPath = $null
|
||||
foreach ($path in $possiblePaths) {
|
||||
if (Test-Path $path) {
|
||||
$defaultCredPath = $path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($defaultCredPath) {
|
||||
Write-Host "`nFound credentials at: $defaultCredPath" -ForegroundColor Green
|
||||
$usePath = Read-Host "Use this path? (Y/N)"
|
||||
|
||||
if ($usePath -eq "Y" -or $usePath -eq "y") {
|
||||
$credPath = $defaultCredPath
|
||||
} else {
|
||||
$credPath = Read-Host "Enter path to credentials.json"
|
||||
}
|
||||
} else {
|
||||
Write-Host "`nCredentials not found at default locations" -ForegroundColor Yellow
|
||||
Write-Host " Checked: $($possiblePaths -join ', ')" -ForegroundColor Gray
|
||||
$credPath = Read-Host "Enter full path to credentials.json"
|
||||
}
|
||||
|
||||
if (Test-Path $credPath) {
|
||||
try {
|
||||
$destCredPath = Join-Path $PackagePath "claude-credentials.json"
|
||||
Copy-Item -Path $credPath -Destination $destCredPath -Force
|
||||
Write-Success "Claude credentials copied to deployment package"
|
||||
Write-Host " Location: $destCredPath" -ForegroundColor Gray
|
||||
Write-Host " The Setup-ClaudeAuth.ps1 script will detect and use this file automatically" -ForegroundColor Gray
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to copy credentials: $_"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Credentials file not found at: $credPath"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Skipping credentials copy. You can set up Claude auth manually on the server." -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Transfer-ToServerSSH {
|
||||
param([string]$PackagePath)
|
||||
|
||||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host " OPTIONAL: SSH Transfer to Server" -ForegroundColor Yellow
|
||||
Write-Host ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "You can automatically transfer the deployment package to"
|
||||
Write-Host "the Windows Server via SSH/SCP (requires SSH server on Windows)."
|
||||
Write-Host ""
|
||||
|
||||
$response = Read-Host "Transfer package to server via SSH? (Y/N)"
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
Write-Host ""
|
||||
$sshUser = Read-Host "Enter SSH username (e.g., Administrator)"
|
||||
$sshHost = Read-Host "Enter server hostname/IP (e.g., 10.0.20.36)"
|
||||
$sshPort = Read-Host "Enter SSH port (default: 22, press Enter for default)"
|
||||
if ([string]::IsNullOrWhiteSpace($sshPort)) {
|
||||
$sshPort = "22"
|
||||
}
|
||||
|
||||
$remotePath = Read-Host "Enter remote path (e.g., C:/Temp/telegram-bot-deploy)"
|
||||
|
||||
# Convert Windows path to SCP format if needed
|
||||
$remotePath = $remotePath -replace '\\', '/'
|
||||
if ($remotePath -match '^[A-Za-z]:') {
|
||||
# Convert C:/path to /c/path format for SCP
|
||||
$remotePath = $remotePath -replace '^([A-Za-z]):', '/$1'
|
||||
}
|
||||
|
||||
Write-Host "`nTransfer Configuration:" -ForegroundColor Cyan
|
||||
Write-Host " Source: $PackagePath"
|
||||
Write-Host " Target: ${sshUser}@${sshHost}:${remotePath}"
|
||||
Write-Host " Port: $sshPort"
|
||||
Write-Host ""
|
||||
|
||||
$confirm = Read-Host "Proceed with transfer? (Y/N)"
|
||||
|
||||
if ($confirm -eq "Y" -or $confirm -eq "y") {
|
||||
Write-Step "Transferring package via SCP..."
|
||||
|
||||
try {
|
||||
# Use SCP to transfer
|
||||
$scpTarget = "${sshUser}@${sshHost}:${remotePath}"
|
||||
|
||||
# Build SCP command
|
||||
$scpArgs = @(
|
||||
"-P", $sshPort,
|
||||
"-r",
|
||||
$PackagePath,
|
||||
$scpTarget
|
||||
)
|
||||
|
||||
Write-Host " Running: scp $scpArgs" -ForegroundColor Gray
|
||||
|
||||
& scp @scpArgs
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Package transferred successfully!"
|
||||
Write-Host " Remote location: $scpTarget" -ForegroundColor Gray
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "SCP transfer failed with exit code: $LASTEXITCODE"
|
||||
Write-Host " You can transfer manually via RDP or network share" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Transfer failed: $_"
|
||||
Write-Host " Make sure 'scp' is available in PATH" -ForegroundColor Yellow
|
||||
Write-Host " Alternative: Use WinSCP, FileZilla, or manual RDP copy" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Transfer cancelled" -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Skipping SSH transfer. Transfer package manually via RDP or network share." -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-PackageSummary {
|
||||
param(
|
||||
[string]$PackagePath,
|
||||
[bool]$CredentialsCopied,
|
||||
[bool]$TransferredToServer
|
||||
)
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DEPLOYMENT PACKAGE CREATED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nPackage Location:" -ForegroundColor Yellow
|
||||
Write-Host " $PackagePath"
|
||||
|
||||
# Calculate package size
|
||||
$files = Get-ChildItem -Path $PackagePath -Recurse -File
|
||||
$totalSize = ($files | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Host "`nPackage Contents:" -ForegroundColor Yellow
|
||||
Write-Host " Files: $($files.Count)"
|
||||
Write-Host " Total Size: $([math]::Round($totalSize, 2)) MB"
|
||||
if ($CredentialsCopied) {
|
||||
Write-Host " ✓ Claude credentials included" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($TransferredToServer) {
|
||||
Write-Host "`nDeployment Status:" -ForegroundColor Yellow
|
||||
Write-Host " ✓ Package transferred to server via SSH" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next Steps on Server:" -ForegroundColor Yellow
|
||||
Write-Host " 1. SSH to server or RDP"
|
||||
Write-Host " 2. Navigate to deployment location"
|
||||
Write-Host " 3. Run: scripts\Install-TelegramBot.ps1 (as Administrator)"
|
||||
Write-Host " 4. Run: scripts\Setup-ClaudeAuth.ps1 (will auto-detect credentials)"
|
||||
Write-Host " 5. Configure: .env file with Telegram bot token"
|
||||
Write-Host " 6. Run: scripts\Start-TelegramBot.ps1"
|
||||
} else {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Transfer package to Windows Server (10.0.20.36)"
|
||||
Write-Host " - Via network share: Copy-Item -Path $PackagePath -Destination \\10.0.20.36\C$\Temp\telegram-bot-deploy -Recurse"
|
||||
Write-Host " - Via RDP: Manual copy"
|
||||
Write-Host " 2. On server, run: scripts\Install-TelegramBot.ps1 (as Administrator)"
|
||||
Write-Host " 3. Configure: .env file with production credentials"
|
||||
Write-Host " 4. Run: scripts\Start-TelegramBot.ps1"
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Transfer-ToServer {
|
||||
param(
|
||||
[string]$PackagePath,
|
||||
[string]$ServerHost,
|
||||
[string]$ServerPath
|
||||
)
|
||||
|
||||
if (-not $ServerHost -or -not $ServerPath) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Transferring to server $ServerHost..."
|
||||
|
||||
try {
|
||||
# Check if server is reachable
|
||||
if (Test-Connection -ComputerName $ServerHost -Count 1 -Quiet) {
|
||||
Write-Success "Server is reachable"
|
||||
} else {
|
||||
Write-Warning "Server is not reachable, skipping transfer"
|
||||
return
|
||||
}
|
||||
|
||||
# Transfer files (using Copy-Item for network path or RoboCopy)
|
||||
$networkPath = "\\$ServerHost\$(($ServerPath -replace ':', '$'))"
|
||||
|
||||
Write-Step "Copying to: $networkPath"
|
||||
|
||||
# Ensure destination exists
|
||||
if (-not (Test-Path $networkPath)) {
|
||||
New-Item -ItemType Directory -Path $networkPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Copy package
|
||||
Copy-Item -Path "$PackagePath\*" -Destination $networkPath -Recurse -Force
|
||||
|
||||
Write-Success "Package transferred to server"
|
||||
} catch {
|
||||
Write-Warning "Failed to transfer to server: $_"
|
||||
Write-Host " You can manually copy the package from: $PackagePath" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BUILD FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Build Deployment Package
|
||||
Creating production-ready deployment package
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Resolve paths
|
||||
$sourcePath = Resolve-FullPath -Path $SourcePath
|
||||
$outputPath = Resolve-FullPath -Path $OutputPath
|
||||
$scriptsPath = $PSScriptRoot
|
||||
|
||||
Write-Step "Build Configuration"
|
||||
Write-Host " Source: $sourcePath" -ForegroundColor Gray
|
||||
Write-Host " Output: $outputPath" -ForegroundColor Gray
|
||||
|
||||
try {
|
||||
# Build steps
|
||||
Test-SourceDirectory -Path $sourcePath
|
||||
New-CleanOutputDirectory -Path $outputPath
|
||||
Copy-ApplicationFiles -SourcePath $sourcePath -DestPath $outputPath
|
||||
Copy-RequirementsFile -SourcePath $sourcePath -DestPath $outputPath
|
||||
New-EnvironmentTemplate -DestPath $outputPath
|
||||
Copy-DeploymentScripts -SourceScriptsPath $scriptsPath -DestPath $outputPath
|
||||
|
||||
# Copy config templates if they exist
|
||||
$configPath = Join-Path (Split-Path $scriptsPath -Parent) "config"
|
||||
if (Test-Path $configPath) {
|
||||
Copy-ConfigTemplates -SourceConfigPath $configPath -DestPath $outputPath
|
||||
}
|
||||
|
||||
New-DeploymentReadme -DestPath $outputPath
|
||||
|
||||
# Interactive: Copy Claude credentials to package
|
||||
$credentialsCopied = Copy-ClaudeCredentials -PackagePath $outputPath
|
||||
|
||||
# Interactive: Transfer to server via SSH
|
||||
$transferredToServer = $false
|
||||
if (-not ($ServerHost -and $ServerPath)) {
|
||||
# Interactive SSH transfer
|
||||
$transferredToServer = Transfer-ToServerSSH -PackagePath $outputPath
|
||||
} else {
|
||||
# Legacy non-interactive transfer (if parameters provided)
|
||||
Transfer-ToServer -PackagePath $outputPath -ServerHost $ServerHost -ServerPath $ServerPath
|
||||
$transferredToServer = $true
|
||||
}
|
||||
|
||||
# Show summary with deployment status
|
||||
Show-PackageSummary -PackagePath $outputPath -CredentialsCopied $credentialsCopied -TransferredToServer $transferredToServer
|
||||
|
||||
Write-Host "`nBuild completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[BUILD FAILED] $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main build
|
||||
Main
|
||||
496
deployment/windows/scripts/Deploy-ROA2WEB.ps1
Normal file
496
deployment/windows/scripts/Deploy-ROA2WEB.ps1
Normal file
@@ -0,0 +1,496 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB - Quick Deployment/Update Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs rapid deployment or updates of ROA2WEB application:
|
||||
- Auto-detects source path (use from scripts/ directory)
|
||||
- Creates backup of current deployment
|
||||
- Stops backend service
|
||||
- Updates backend and/or frontend files
|
||||
- Installs new Python dependencies if changed
|
||||
- Restarts backend service
|
||||
- Validates deployment health
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Target installation path (default: C:\inetpub\wwwroot\roa2web)
|
||||
|
||||
.PARAMETER BackupEnabled
|
||||
Create backup before deployment (default: true)
|
||||
|
||||
.PARAMETER RestartService
|
||||
Restart backend service after deployment (default: true)
|
||||
|
||||
.PARAMETER UpdateBackend
|
||||
Update backend files (default: true)
|
||||
|
||||
.PARAMETER UpdateFrontend
|
||||
Update frontend files (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
cd C:\Deploy\ROA2WEB\scripts
|
||||
.\Deploy-ROA2WEB.ps1
|
||||
Deploy from current deployment package (auto-detected)
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateBackend -UpdateFrontend:$false
|
||||
Update only backend files
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
Must be run from deployment package's scripts/ directory
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
|
||||
[bool]$BackupEnabled = $true,
|
||||
[bool]$RestartService = $true,
|
||||
[bool]$UpdateBackend = $true,
|
||||
[bool]$UpdateFrontend = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = $PSScriptRoot
|
||||
if ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
$detectedSourcePath = Split-Path $PSScriptRoot -Parent
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB"
|
||||
ServiceName = "ROA2WEB-Backend"
|
||||
InstallPath = $InstallPath
|
||||
BackendPath = Join-Path $InstallPath "backend"
|
||||
FrontendPath = Join-Path $InstallPath "frontend"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-CurrentDeployment {
|
||||
if (-not $BackupEnabled) {
|
||||
Write-Warning "Backup disabled, skipping..."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Step "Creating backup of current deployment..."
|
||||
|
||||
New-BackupDirectory
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupName = "backup-$timestamp"
|
||||
$backupFullPath = Join-Path $Config.BackupPath $backupName
|
||||
|
||||
try {
|
||||
# Create backup directory
|
||||
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
|
||||
|
||||
# Backup backend
|
||||
if ((Test-Path $Config.BackendPath) -and $UpdateBackend) {
|
||||
$backupBackendPath = Join-Path $backupFullPath "backend"
|
||||
Copy-Item -Path $Config.BackendPath -Destination $backupBackendPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Success "Backend backed up"
|
||||
}
|
||||
|
||||
# Backup frontend
|
||||
if ((Test-Path $Config.FrontendPath) -and $UpdateFrontend) {
|
||||
$backupFrontendPath = Join-Path $backupFullPath "frontend"
|
||||
Copy-Item -Path $Config.FrontendPath -Destination $backupFrontendPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Success "Frontend backed up"
|
||||
}
|
||||
|
||||
Write-Success "Backup created at: $backupFullPath"
|
||||
|
||||
# Clean old backups (keep last 10)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Sort-Object Name -Descending
|
||||
if ($allBackups.Count -gt 10) {
|
||||
$oldBackups = $allBackups | Select-Object -Skip 10
|
||||
foreach ($oldBackup in $oldBackups) {
|
||||
Remove-Item -Path $oldBackup.FullName -Recurse -Force
|
||||
Write-Success "Cleaned old backup: $($oldBackup.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFullPath
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-BackendService {
|
||||
Write-Step "Stopping backend service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Stop-Service -Name $Config.ServiceName -Force
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Wait for service to stop
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Success "Service stopped successfully"
|
||||
} else {
|
||||
Write-Warning "Service did not stop within timeout"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Service already stopped"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to stop service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-BackendFiles {
|
||||
if (-not $UpdateBackend) {
|
||||
Write-Warning "Backend update disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Updating backend files..."
|
||||
|
||||
$sourceBackend = Join-Path $Config.SourcePath "backend"
|
||||
|
||||
if (-not (Test-Path $sourceBackend)) {
|
||||
Write-Warning "Source backend path not found: $sourceBackend"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Copy all files except .env (preserve existing config)
|
||||
$excludeFiles = @("*.env", "*.log", "*.pyc", "__pycache__")
|
||||
|
||||
Get-ChildItem -Path $sourceBackend -Recurse -File | ForEach-Object {
|
||||
$relativePath = $_.FullName.Substring($sourceBackend.Length)
|
||||
$destPath = Join-Path $Config.BackendPath $relativePath
|
||||
|
||||
# Skip excluded files
|
||||
$skip = $false
|
||||
foreach ($pattern in $excludeFiles) {
|
||||
if ($_.Name -like $pattern -or $_.Directory.Name -eq "__pycache__") {
|
||||
$skip = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $skip) {
|
||||
$destDir = Split-Path $destPath -Parent
|
||||
if (-not (Test-Path $destDir)) {
|
||||
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Backend files updated"
|
||||
|
||||
# Check if requirements.txt changed
|
||||
$sourceReq = Join-Path $sourceBackend "requirements.txt"
|
||||
$destReq = Join-Path $Config.BackendPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
$sourceHash = (Get-FileHash $sourceReq).Hash
|
||||
$destHash = if (Test-Path $destReq) { (Get-FileHash $destReq).Hash } else { "" }
|
||||
|
||||
if ($sourceHash -ne $destHash) {
|
||||
Write-Step "Requirements changed, updating Python dependencies..."
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
|
||||
try {
|
||||
& python -m pip install -r $destReq --upgrade
|
||||
Write-Success "Python dependencies updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update Python dependencies: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Python dependencies unchanged"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to update backend files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-FrontendFiles {
|
||||
if (-not $UpdateFrontend) {
|
||||
Write-Warning "Frontend update disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Updating frontend files..."
|
||||
|
||||
$sourceFrontend = Join-Path $Config.SourcePath "frontend"
|
||||
|
||||
if (-not (Test-Path $sourceFrontend)) {
|
||||
Write-Warning "Source frontend path not found: $sourceFrontend"
|
||||
Write-Warning "Expected path: $sourceFrontend"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Remove old frontend files (except web.config)
|
||||
if (Test-Path $Config.FrontendPath) {
|
||||
Get-ChildItem -Path $Config.FrontendPath -Exclude "web.config" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy new frontend files
|
||||
Copy-Item -Path "$sourceFrontend\*" -Destination $Config.FrontendPath -Recurse -Force
|
||||
|
||||
# Ensure web.config exists
|
||||
$webConfigPath = Join-Path $Config.FrontendPath "web.config"
|
||||
$webConfigTemplate = Join-Path (Split-Path $PSScriptRoot -Parent) "config\web.config"
|
||||
|
||||
if (-not (Test-Path $webConfigPath) -and (Test-Path $webConfigTemplate)) {
|
||||
Copy-Item -Path $webConfigTemplate -Destination $webConfigPath -Force
|
||||
Write-Success "web.config restored from template"
|
||||
}
|
||||
|
||||
Write-Success "Frontend files updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update frontend files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Start-BackendService {
|
||||
if (-not $RestartService) {
|
||||
Write-Warning "Service restart disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Starting backend service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Error "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Wait for service to start
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Service started successfully"
|
||||
} else {
|
||||
Write-Error "Service failed to start (Status: $($service.Status))"
|
||||
Write-Warning "Check logs at: $($Config.LogsPath)\backend-stderr.log"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DeploymentHealth {
|
||||
Write-Step "Testing deployment health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
# Get service port from .env or use default
|
||||
$envFile = Join-Path $Config.BackendPath ".env"
|
||||
$port = 8000
|
||||
|
||||
if (Test-Path $envFile) {
|
||||
$portLine = Get-Content $envFile | Where-Object { $_ -match "^PORT=(\d+)" }
|
||||
if ($portLine) {
|
||||
$port = [int]$matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
# Test backend health endpoint
|
||||
$healthUrl = "http://localhost:$port/health"
|
||||
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "Backend health check passed"
|
||||
Write-Success "Response: $($response.Content)"
|
||||
} else {
|
||||
Write-Warning "Health check returned status: $($response.StatusCode)"
|
||||
}
|
||||
|
||||
# Test frontend (IIS)
|
||||
try {
|
||||
$frontendResponse = Invoke-WebRequest -Uri "http://localhost/" -UseBasicParsing -TimeoutSec 5
|
||||
if ($frontendResponse.StatusCode -eq 200) {
|
||||
Write-Success "Frontend health check passed"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Frontend health check failed: $_"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Health check failed: $_"
|
||||
Write-Warning "The service may need more time to start"
|
||||
Write-Warning "Check logs: $($Config.LogsPath)\backend-stderr.log"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DeploymentSummary {
|
||||
param([string]$BackupPath, [datetime]$StartTime)
|
||||
|
||||
$duration = (Get-Date) - $StartTime
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DEPLOYMENT COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Summary:" -ForegroundColor Yellow
|
||||
Write-Host " Duration: $($duration.TotalSeconds) seconds"
|
||||
Write-Host " Backend Updated: $UpdateBackend"
|
||||
Write-Host " Frontend Updated: $UpdateFrontend"
|
||||
if ($BackupPath) {
|
||||
Write-Host " Backup Location: $BackupPath"
|
||||
}
|
||||
|
||||
Write-Host "`nApplication Status:" -ForegroundColor Yellow
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
if ($service) {
|
||||
Write-Host " Backend Service: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
|
||||
}
|
||||
} catch {
|
||||
Write-Host " Backend Service: Unknown" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
Write-Host " Web Application: http://localhost"
|
||||
Write-Host " API Documentation: http://localhost/api/docs"
|
||||
|
||||
Write-Host "`nManagement:" -ForegroundColor Yellow
|
||||
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50 -Wait"
|
||||
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
|
||||
Write-Host " Rollback: Use backup at $BackupPath"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN DEPLOYMENT FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Deployment Script
|
||||
Fast deployment and updates for Windows Server + IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
$startTime = Get-Date
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
if (-not (Test-Path $Config.InstallPath)) {
|
||||
Write-Error "Installation path not found: $($Config.InstallPath)"
|
||||
Write-Host " Run Install-ROA2WEB.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path exists"
|
||||
|
||||
try {
|
||||
# Deployment steps
|
||||
$backupPath = Backup-CurrentDeployment
|
||||
Stop-BackendService
|
||||
Update-BackendFiles
|
||||
Update-FrontendFiles
|
||||
Start-BackendService
|
||||
Test-DeploymentHealth
|
||||
Show-DeploymentSummary -BackupPath $backupPath -StartTime $startTime
|
||||
|
||||
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Deployment failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
|
||||
# Attempt rollback if backup exists
|
||||
if ($backupPath -and (Test-Path $backupPath)) {
|
||||
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
|
||||
# TODO: Implement rollback logic
|
||||
}
|
||||
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main deployment
|
||||
Main
|
||||
598
deployment/windows/scripts/Deploy-TelegramBot.ps1
Normal file
598
deployment/windows/scripts/Deploy-TelegramBot.ps1
Normal file
@@ -0,0 +1,598 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB Telegram Bot - Quick Deployment/Update Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs rapid deployment or updates of ROA2WEB Telegram Bot:
|
||||
- Auto-detects source path (use from scripts/ directory)
|
||||
- Creates backup of current deployment (app files + database)
|
||||
- Stops bot service
|
||||
- Updates application files
|
||||
- Installs new Python dependencies if changed
|
||||
- Preserves .env configuration
|
||||
- Restarts bot service
|
||||
- Validates deployment health
|
||||
- Rollback support on failure
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Target installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Source path for deployment package (auto-detected if run from scripts/)
|
||||
|
||||
.PARAMETER BackupEnabled
|
||||
Create backup before deployment (default: true)
|
||||
|
||||
.PARAMETER RestartService
|
||||
Restart bot service after deployment (default: true)
|
||||
|
||||
.PARAMETER RollbackOnFailure
|
||||
Automatically rollback if deployment fails (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
cd C:\Deploy\TelegramBot\scripts
|
||||
.\Deploy-TelegramBot.ps1
|
||||
Deploy from current deployment package (auto-detected)
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-TelegramBot.ps1 -SourcePath "C:\Deploy\new-version"
|
||||
Deploy from specific source path
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-TelegramBot.ps1 -BackupEnabled $false -RestartService $false
|
||||
Update files without backup or restart (manual testing)
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
Recommended to run from deployment package's scripts/ directory
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[string]$SourcePath = "",
|
||||
[bool]$BackupEnabled = $true,
|
||||
[bool]$RestartService = $true,
|
||||
[bool]$RollbackOnFailure = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = if ($SourcePath) {
|
||||
$SourcePath
|
||||
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
Split-Path $PSScriptRoot -Parent
|
||||
} else {
|
||||
$PSScriptRoot
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB-TelegramBot"
|
||||
ServiceName = "ROA2WEB-TelegramBot"
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
$script:DeploymentState = @{
|
||||
BackupPath = $null
|
||||
ServiceWasRunning = $false
|
||||
DeploymentSuccess = $false
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-CurrentDeployment {
|
||||
if (-not $BackupEnabled) {
|
||||
Write-Warning "Backup disabled, skipping..."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Step "Creating backup of current deployment..."
|
||||
|
||||
New-BackupDirectory
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupName = "backup-$timestamp"
|
||||
$backupFullPath = Join-Path $Config.BackupPath $backupName
|
||||
|
||||
try {
|
||||
# Create backup directory
|
||||
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
|
||||
|
||||
# Backup app directory
|
||||
if (Test-Path (Join-Path $Config.InstallPath "app")) {
|
||||
$backupAppPath = Join-Path $backupFullPath "app"
|
||||
Copy-Item -Path (Join-Path $Config.InstallPath "app") -Destination $backupAppPath -Recurse -Force
|
||||
Write-Success "App files backed up"
|
||||
}
|
||||
|
||||
# Backup requirements.txt
|
||||
$reqFile = Join-Path $Config.InstallPath "requirements.txt"
|
||||
if (Test-Path $reqFile) {
|
||||
Copy-Item -Path $reqFile -Destination (Join-Path $backupFullPath "requirements.txt") -Force
|
||||
Write-Success "Requirements file backed up"
|
||||
}
|
||||
|
||||
# Backup .env file
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (Test-Path $envFile) {
|
||||
Copy-Item -Path $envFile -Destination (Join-Path $backupFullPath ".env") -Force
|
||||
Write-Success ".env file backed up"
|
||||
}
|
||||
|
||||
# Backup database
|
||||
$dbFile = Join-Path $Config.DataPath "telegram_bot.db"
|
||||
if (Test-Path $dbFile) {
|
||||
$backupDataPath = Join-Path $backupFullPath "data"
|
||||
New-Item -ItemType Directory -Path $backupDataPath -Force | Out-Null
|
||||
Copy-Item -Path $dbFile -Destination (Join-Path $backupDataPath "telegram_bot.db") -Force
|
||||
Write-Success "Database backed up"
|
||||
}
|
||||
|
||||
Write-Success "Backup created at: $backupFullPath"
|
||||
|
||||
# Clean old backups (keep last 10)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory |
|
||||
Where-Object { $_.Name -like "backup-*" } |
|
||||
Sort-Object Name -Descending
|
||||
|
||||
if ($allBackups.Count -gt 10) {
|
||||
$oldBackups = $allBackups | Select-Object -Skip 10
|
||||
foreach ($oldBackup in $oldBackups) {
|
||||
Remove-Item -Path $oldBackup.FullName -Recurse -Force
|
||||
Write-Success "Cleaned old backup: $($oldBackup.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFullPath
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-BotService {
|
||||
Write-Step "Stopping Telegram bot service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
$DeploymentState.ServiceWasRunning = $false
|
||||
return
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
$DeploymentState.ServiceWasRunning = $true
|
||||
|
||||
Stop-Service -Name $Config.ServiceName -Force
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Wait for service to stop
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Success "Service stopped successfully"
|
||||
} else {
|
||||
Write-Warning "Service did not stop within timeout"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Service already stopped"
|
||||
$DeploymentState.ServiceWasRunning = $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to stop service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-ApplicationFiles {
|
||||
Write-Step "Updating application files..."
|
||||
|
||||
$sourceApp = Join-Path $Config.SourcePath "app"
|
||||
|
||||
if (-not (Test-Path $sourceApp)) {
|
||||
throw "Source app directory not found: $sourceApp"
|
||||
}
|
||||
|
||||
try {
|
||||
# Remove old app directory
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
if (Test-Path $destApp) {
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
Write-Success "Removed old app directory"
|
||||
}
|
||||
|
||||
# Copy new app files
|
||||
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "Application files updated"
|
||||
|
||||
# Update requirements.txt if present
|
||||
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
|
||||
$destReq = Join-Path $Config.InstallPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
$sourceHash = (Get-FileHash $sourceReq -Algorithm SHA256).Hash
|
||||
$destHash = if (Test-Path $destReq) {
|
||||
(Get-FileHash $destReq -Algorithm SHA256).Hash
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if ($sourceHash -ne $destHash) {
|
||||
Write-Step "Requirements changed, updating Python dependencies..."
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
|
||||
# Use virtual environment pip
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
|
||||
|
||||
if (Test-Path $pipPath) {
|
||||
try {
|
||||
& $pipPath install -r $destReq --upgrade
|
||||
Write-Success "Python dependencies updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update Python dependencies: $_"
|
||||
throw
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Virtual environment not found, skipping dependency update"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Python dependencies unchanged"
|
||||
}
|
||||
}
|
||||
|
||||
# Preserve .env file (never overwrite)
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (-not (Test-Path $envFile)) {
|
||||
$sourceEnv = Join-Path $Config.SourcePath ".env.example"
|
||||
if (Test-Path $sourceEnv) {
|
||||
Copy-Item -Path $sourceEnv -Destination $envFile -Force
|
||||
Write-Warning "Created .env from .env.example - PLEASE CONFIGURE"
|
||||
}
|
||||
} else {
|
||||
Write-Success ".env file preserved (not overwritten)"
|
||||
}
|
||||
|
||||
# Update management scripts
|
||||
$sourceScripts = Join-Path $Config.SourcePath "scripts"
|
||||
if (Test-Path $sourceScripts) {
|
||||
$destScripts = Join-Path $Config.InstallPath "scripts"
|
||||
if (-not (Test-Path $destScripts)) {
|
||||
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
|
||||
}
|
||||
|
||||
# List of management scripts to update
|
||||
$managementScripts = @(
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$updatedScriptsCount = 0
|
||||
foreach ($script in $managementScripts) {
|
||||
$sourcePath = Join-Path $sourceScripts $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destPath = Join-Path $destScripts $script
|
||||
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||
$updatedScriptsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if ($updatedScriptsCount -gt 0) {
|
||||
Write-Success "Updated $updatedScriptsCount management scripts"
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to update application files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Start-BotService {
|
||||
if (-not $RestartService) {
|
||||
Write-Warning "Service restart disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Starting Telegram bot service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Wait for service to start
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Service started successfully"
|
||||
} else {
|
||||
throw "Service failed to start (Status: $($service.Status))"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DeploymentHealth {
|
||||
Write-Step "Testing deployment health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
# Get service port from .env or use default
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
$port = 8002
|
||||
|
||||
if (Test-Path $envFile) {
|
||||
$envContent = Get-Content $envFile
|
||||
$portLine = $envContent | Where-Object { $_ -match "^INTERNAL_API_PORT=(\d+)" }
|
||||
if ($portLine -and $matches[1]) {
|
||||
$port = [int]$matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
$healthUrl = "http://localhost:$port/internal/health"
|
||||
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Success "Health check passed: $($content.status)"
|
||||
Write-Success "Database: $($content.database.status)"
|
||||
return $true
|
||||
} else {
|
||||
throw "Health check returned status code: $($response.StatusCode)"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Health check failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Restore-FromBackup {
|
||||
param([string]$BackupPath)
|
||||
|
||||
if (-not $BackupPath -or -not (Test-Path $BackupPath)) {
|
||||
Write-Error "Cannot rollback: backup path not found ($BackupPath)"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Step "Rolling back to backup: $BackupPath"
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Stop-BotService
|
||||
|
||||
# Restore app directory
|
||||
$backupApp = Join-Path $BackupPath "app"
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
|
||||
if (Test-Path $backupApp) {
|
||||
if (Test-Path $destApp) {
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
}
|
||||
Copy-Item -Path $backupApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "App files restored"
|
||||
}
|
||||
|
||||
# Restore requirements.txt
|
||||
$backupReq = Join-Path $BackupPath "requirements.txt"
|
||||
if (Test-Path $backupReq) {
|
||||
Copy-Item -Path $backupReq -Destination (Join-Path $Config.InstallPath "requirements.txt") -Force
|
||||
Write-Success "Requirements file restored"
|
||||
}
|
||||
|
||||
# Restore database
|
||||
$backupDb = Join-Path $BackupPath "data\telegram_bot.db"
|
||||
if (Test-Path $backupDb) {
|
||||
Copy-Item -Path $backupDb -Destination (Join-Path $Config.DataPath "telegram_bot.db") -Force
|
||||
Write-Success "Database restored"
|
||||
}
|
||||
|
||||
# Restart service
|
||||
if ($DeploymentState.ServiceWasRunning) {
|
||||
Start-BotService
|
||||
}
|
||||
|
||||
Write-Success "Rollback completed successfully"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Rollback failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DeploymentSummary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
if ($DeploymentState.DeploymentSuccess) {
|
||||
Write-Host " DEPLOYMENT COMPLETED SUCCESSFULLY" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Details:" -ForegroundColor Yellow
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Source Path: $($Config.SourcePath)"
|
||||
Write-Host " Backup Created: $(if ($DeploymentState.BackupPath) { 'Yes' } else { 'No' })"
|
||||
|
||||
if ($DeploymentState.BackupPath) {
|
||||
Write-Host " Backup Location: $($DeploymentState.BackupPath)"
|
||||
}
|
||||
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
if ($service) {
|
||||
Write-Host " Service Status: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
|
||||
}
|
||||
|
||||
if ($DeploymentState.DeploymentSuccess) {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " - Monitor logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50 -Wait"
|
||||
Write-Host " - Check health: Invoke-WebRequest http://localhost:8002/internal/health"
|
||||
Write-Host " - Test bot on Telegram"
|
||||
} else {
|
||||
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
|
||||
Write-Host " - Check logs: Get-Content $($Config.LogsPath)\stderr.log -Tail 100"
|
||||
Write-Host " - Verify .env configuration"
|
||||
if ($DeploymentState.BackupPath -and $RollbackOnFailure) {
|
||||
Write-Host " - Rollback completed automatically to: $($DeploymentState.BackupPath)"
|
||||
} elseif ($DeploymentState.BackupPath) {
|
||||
Write-Host " - Manual rollback available at: $($DeploymentState.BackupPath)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN DEPLOYMENT FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Deployment Script
|
||||
Quick deployment and update automation
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
if (-not (Test-Path $Config.InstallPath)) {
|
||||
Write-Error "Installation path not found: $($Config.InstallPath)"
|
||||
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path verified"
|
||||
|
||||
if (-not (Test-Path $Config.SourcePath)) {
|
||||
Write-Error "Source path not found: $($Config.SourcePath)"
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Source path verified: $($Config.SourcePath)"
|
||||
|
||||
try {
|
||||
# Deployment steps
|
||||
$DeploymentState.BackupPath = Backup-CurrentDeployment
|
||||
Stop-BotService
|
||||
Update-ApplicationFiles
|
||||
Start-BotService
|
||||
|
||||
$healthOk = Test-DeploymentHealth
|
||||
|
||||
if ($healthOk) {
|
||||
$DeploymentState.DeploymentSuccess = $true
|
||||
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
|
||||
} else {
|
||||
throw "Health check failed after deployment"
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[DEPLOYMENT FAILED] $_" -ForegroundColor Red
|
||||
|
||||
if ($RollbackOnFailure -and $DeploymentState.BackupPath) {
|
||||
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
|
||||
$rollbackOk = Restore-FromBackup -BackupPath $DeploymentState.BackupPath
|
||||
|
||||
if ($rollbackOk) {
|
||||
Write-Host "Rollback completed successfully" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Rollback failed - manual intervention required" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "Automatic rollback disabled or no backup available" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$DeploymentState.DeploymentSuccess = $false
|
||||
} finally {
|
||||
Show-DeploymentSummary
|
||||
}
|
||||
|
||||
if (-not $DeploymentState.DeploymentSuccess) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main deployment
|
||||
Main
|
||||
382
deployment/windows/scripts/Enable-HTTPS.ps1
Normal file
382
deployment/windows/scripts/Enable-HTTPS.ps1
Normal file
@@ -0,0 +1,382 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enable HTTPS for ROA2WEB on IIS
|
||||
|
||||
.DESCRIPTION
|
||||
This script configures HTTPS for ROA2WEB by:
|
||||
- Creating a self-signed SSL certificate (or using existing certificate)
|
||||
- Configuring HTTPS binding on IIS site
|
||||
- Enabling HTTP to HTTPS redirect
|
||||
|
||||
.PARAMETER IISSiteName
|
||||
IIS Site name (default: Default Web Site)
|
||||
|
||||
.PARAMETER CertificateDnsName
|
||||
DNS name for certificate (default: server IP or hostname)
|
||||
|
||||
.PARAMETER UseExistingCert
|
||||
Use existing certificate by thumbprint
|
||||
|
||||
.PARAMETER CertThumbprint
|
||||
Thumbprint of existing certificate to use
|
||||
|
||||
.PARAMETER Port
|
||||
HTTPS port (default: 443)
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1
|
||||
Create self-signed certificate and enable HTTPS
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "ROA2WEB" -CertificateDnsName "roa2web.company.com"
|
||||
Create certificate with custom DNS name
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1 -UseExistingCert -CertThumbprint "ABC123..."
|
||||
Use existing certificate
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$IISSiteName = "Default Web Site",
|
||||
[string]$CertificateDnsName = "",
|
||||
[switch]$UseExistingCert,
|
||||
[string]$CertThumbprint = "",
|
||||
[int]$Port = 443,
|
||||
[string]$IPAddress = "*"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN SCRIPT
|
||||
# =============================================================================
|
||||
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Enable HTTPS on IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check administrator privileges
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
# Import required modules
|
||||
Write-Step "Loading IIS module..."
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
Write-Success "IIS module loaded"
|
||||
|
||||
# Verify IIS site exists
|
||||
Write-Step "Verifying IIS site '$IISSiteName'..."
|
||||
$site = Get-Website -Name $IISSiteName -ErrorAction SilentlyContinue
|
||||
if (-not $site) {
|
||||
Write-Error "IIS site '$IISSiteName' not found"
|
||||
Write-Host "`nAvailable sites:" -ForegroundColor Yellow
|
||||
Get-Website | Format-Table Name, State, PhysicalPath
|
||||
exit 1
|
||||
}
|
||||
Write-Success "IIS site found: $IISSiteName (State: $($site.State))"
|
||||
|
||||
# Get or create certificate
|
||||
if ($UseExistingCert) {
|
||||
Write-Step "Using existing certificate..."
|
||||
|
||||
if ([string]::IsNullOrEmpty($CertThumbprint)) {
|
||||
Write-Host "`nAvailable certificates in LocalMachine\My:" -ForegroundColor Yellow
|
||||
Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Select-Object Thumbprint, Subject, FriendlyName, NotAfter |
|
||||
Format-Table -AutoSize
|
||||
|
||||
$CertThumbprint = Read-Host "Enter certificate thumbprint"
|
||||
}
|
||||
|
||||
$cert = Get-ChildItem -Path "cert:\LocalMachine\My\$CertThumbprint" -ErrorAction SilentlyContinue
|
||||
if (-not $cert) {
|
||||
Write-Error "Certificate with thumbprint '$CertThumbprint' not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Certificate found: $($cert.Subject)"
|
||||
|
||||
} else {
|
||||
Write-Step "Creating self-signed SSL certificate..."
|
||||
|
||||
# Auto-detect DNS name if not provided
|
||||
if ([string]::IsNullOrEmpty($CertificateDnsName)) {
|
||||
$serverName = $env:COMPUTERNAME
|
||||
$ipAddress = (Get-NetIPAddress -AddressFamily IPv4 |
|
||||
Where-Object {$_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.*"} |
|
||||
Select-Object -First 1).IPAddress
|
||||
|
||||
Write-Host " Detected hostname: $serverName" -ForegroundColor Yellow
|
||||
Write-Host " Detected IP: $ipAddress" -ForegroundColor Yellow
|
||||
|
||||
$response = Read-Host "Use hostname for certificate? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
$CertificateDnsName = $serverName
|
||||
} else {
|
||||
$CertificateDnsName = $ipAddress
|
||||
}
|
||||
}
|
||||
|
||||
# Check if certificate already exists
|
||||
$existingCert = Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Where-Object {$_.DnsNameList -contains $CertificateDnsName} |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($existingCert) {
|
||||
Write-Warning "Certificate for '$CertificateDnsName' already exists"
|
||||
$response = Read-Host "Use existing certificate? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
$cert = $existingCert
|
||||
Write-Success "Using existing certificate"
|
||||
} else {
|
||||
Write-Warning "Removing existing certificate..."
|
||||
Remove-Item -Path "cert:\LocalMachine\My\$($existingCert.Thumbprint)" -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $cert) {
|
||||
# Create new certificate
|
||||
$cert = New-SelfSignedCertificate `
|
||||
-DnsName $CertificateDnsName `
|
||||
-CertStoreLocation "cert:\LocalMachine\My" `
|
||||
-NotAfter (Get-Date).AddYears(5) `
|
||||
-FriendlyName "ROA2WEB SSL Certificate" `
|
||||
-KeyUsage DigitalSignature, KeyEncipherment `
|
||||
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")
|
||||
|
||||
Write-Success "Certificate created successfully"
|
||||
Write-Host " DNS Name: $CertificateDnsName" -ForegroundColor Yellow
|
||||
Write-Host " Thumbprint: $($cert.Thumbprint)" -ForegroundColor Yellow
|
||||
Write-Host " Valid Until: $($cert.NotAfter)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Configure HTTPS binding
|
||||
Write-Step "Configuring HTTPS binding..."
|
||||
|
||||
# Check if HTTPS binding already exists
|
||||
$existingBinding = Get-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingBinding) {
|
||||
Write-Warning "HTTPS binding already exists on port $Port"
|
||||
$response = Read-Host "Remove and recreate binding? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
Remove-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
|
||||
Write-Success "Existing binding removed"
|
||||
} else {
|
||||
Write-Warning "Skipping binding creation"
|
||||
$existingBinding = $null
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $existingBinding) {
|
||||
# Create HTTPS binding
|
||||
try {
|
||||
New-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -IPAddress $IPAddress
|
||||
Write-Success "HTTPS binding created on port $Port"
|
||||
} catch {
|
||||
Write-Error "Failed to create HTTPS binding: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Attach certificate to binding
|
||||
Write-Step "Attaching certificate to HTTPS binding..."
|
||||
|
||||
try {
|
||||
# Method 1: Using IIS PowerShell Provider (IIS 8+)
|
||||
Push-Location
|
||||
Set-Location IIS:\SslBindings
|
||||
|
||||
# Remove existing binding if present
|
||||
$sslBinding = "${IPAddress}!${Port}"
|
||||
if ($IPAddress -eq "*") {
|
||||
$sslBinding = "0.0.0.0!${Port}"
|
||||
}
|
||||
|
||||
if (Test-Path $sslBinding) {
|
||||
Remove-Item $sslBinding -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create new SSL binding
|
||||
$cert | New-Item $sslBinding
|
||||
Pop-Location
|
||||
|
||||
Write-Success "Certificate attached to HTTPS binding"
|
||||
|
||||
} catch {
|
||||
Write-Warning "Method 1 failed, trying alternative method..."
|
||||
|
||||
try {
|
||||
# Method 2: Using netsh (more compatible)
|
||||
$certHash = $cert.Thumbprint
|
||||
$appId = "{4dc3e181-e14b-4a21-b022-59fc669b0914}"
|
||||
|
||||
# Remove existing binding
|
||||
netsh http delete sslcert ipport=0.0.0.0:$Port 2>&1 | Out-Null
|
||||
|
||||
# Add new binding
|
||||
$result = netsh http add sslcert ipport=0.0.0.0:$Port certhash=$certHash appid=$appId
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Certificate attached using netsh"
|
||||
} else {
|
||||
Write-Error "Failed to attach certificate: $result"
|
||||
exit 1
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to attach certificate: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Update web.config for HTTP to HTTPS redirect
|
||||
Write-Step "Checking web.config for HTTPS redirect..."
|
||||
|
||||
$webConfigPath = Join-Path $site.PhysicalPath "web.config"
|
||||
|
||||
if (Test-Path $webConfigPath) {
|
||||
$webConfig = Get-Content $webConfigPath -Raw
|
||||
|
||||
if ($webConfig -match "Force HTTPS") {
|
||||
Write-Success "HTTPS redirect rule already exists in web.config"
|
||||
} else {
|
||||
Write-Warning "HTTPS redirect rule not found in web.config"
|
||||
Write-Host "`nTo enable automatic HTTP to HTTPS redirect, add this rule to web.config:" -ForegroundColor Yellow
|
||||
Write-Host @"
|
||||
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
"@ -ForegroundColor Gray
|
||||
|
||||
$response = Read-Host "`nAdd this rule automatically? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
# Backup web.config
|
||||
$backupPath = "$webConfigPath.backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
Copy-Item -Path $webConfigPath -Destination $backupPath
|
||||
Write-Success "web.config backed up to: $backupPath"
|
||||
|
||||
# Add redirect rule
|
||||
$redirectRule = @"
|
||||
<!-- Rule: Force HTTPS (redirect HTTP to HTTPS) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
"@
|
||||
|
||||
$webConfig = $webConfig -replace '(<rules>)', "`$1`r`n$redirectRule"
|
||||
Set-Content -Path $webConfigPath -Value $webConfig
|
||||
Write-Success "HTTPS redirect rule added to web.config"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Warning "web.config not found at: $webConfigPath"
|
||||
Write-Host " Please ensure web.config exists and contains proper rewrite rules" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Test configuration
|
||||
Write-Step "Testing configuration..."
|
||||
|
||||
# Restart IIS site to apply changes
|
||||
try {
|
||||
Stop-Website -Name $IISSiteName -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Website -Name $IISSiteName
|
||||
Write-Success "IIS site restarted"
|
||||
} catch {
|
||||
Write-Warning "Could not restart IIS site: $_"
|
||||
}
|
||||
|
||||
# Display summary
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host " HTTPS CONFIGURATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nConfiguration Summary:" -ForegroundColor Yellow
|
||||
Write-Host " IIS Site: $IISSiteName"
|
||||
Write-Host " HTTPS Port: $Port"
|
||||
Write-Host " Certificate: $($cert.Subject)"
|
||||
Write-Host " Thumbprint: $($cert.Thumbprint)"
|
||||
Write-Host " Valid Until: $($cert.NotAfter)"
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
Write-Host " HTTPS URL: https://$CertificateDnsName"
|
||||
if ($site.Bindings.Collection | Where-Object {$_.protocol -eq "http"}) {
|
||||
Write-Host " HTTP URL: http://$CertificateDnsName (will redirect to HTTPS)"
|
||||
}
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
|
||||
if ($cert.Subject -match "CN=[\d\.]+") {
|
||||
Write-Host " 1. [RECOMMENDED] Replace self-signed certificate with CA-issued certificate"
|
||||
Write-Host " - Browsers will show security warnings for self-signed certificates"
|
||||
Write-Host " - Use Let's Encrypt, DigiCert, or another CA for production"
|
||||
}
|
||||
|
||||
Write-Host " 2. Test HTTPS access: https://$CertificateDnsName"
|
||||
Write-Host " 3. Verify HTTP to HTTPS redirect is working"
|
||||
Write-Host " 4. Check browser console for mixed content warnings"
|
||||
|
||||
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
|
||||
Write-Host " View IIS bindings: Get-WebBinding -Name '$IISSiteName'"
|
||||
Write-Host " View certificates: Get-ChildItem cert:\LocalMachine\My"
|
||||
Write-Host " Test HTTPS locally: Invoke-WebRequest https://localhost:$Port -SkipCertificateCheck"
|
||||
Write-Host " IIS Manager: inetmgr"
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
598
deployment/windows/scripts/Install-ROA2WEB.ps1
Normal file
598
deployment/windows/scripts/Install-ROA2WEB.ps1
Normal file
@@ -0,0 +1,598 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB - Initial Installation Script for Windows Server + IIS
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs complete installation of ROA2WEB on Windows Server:
|
||||
- Checks prerequisites (Admin rights, IIS)
|
||||
- Installs Python 3.11+ if needed
|
||||
- Installs NSSM (service manager)
|
||||
- Installs IIS URL Rewrite and ARR modules
|
||||
- Creates directory structure
|
||||
- Installs Python dependencies
|
||||
- Creates Windows Service for backend
|
||||
- Configures IIS website
|
||||
- Starts all services
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web)
|
||||
|
||||
.PARAMETER PythonVersion
|
||||
Python version to install (default: 3.11.9)
|
||||
|
||||
.PARAMETER ServicePort
|
||||
Backend service port (default: 8000)
|
||||
|
||||
.PARAMETER SkipPython
|
||||
Skip Python installation (use existing Python)
|
||||
|
||||
.PARAMETER SkipIIS
|
||||
Skip IIS configuration
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-ROA2WEB.ps1
|
||||
Standard installation with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web" -ServicePort 8001
|
||||
Custom installation path and port
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
|
||||
[string]$PythonVersion = "3.11.9",
|
||||
[int]$ServicePort = 8000,
|
||||
[string]$IISSiteName = "Default Web Site",
|
||||
[string]$IISAppName = "roa2web",
|
||||
[switch]$CreateNewSite,
|
||||
[switch]$SkipPython,
|
||||
[switch]$SkipIIS
|
||||
)
|
||||
|
||||
# Strict error handling
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB"
|
||||
ServiceName = "ROA2WEB-Backend"
|
||||
ServiceDisplayName = "ROA2WEB Backend Service"
|
||||
ServiceDescription = "FastAPI backend service for ROA2WEB ERP Reports Application"
|
||||
InstallPath = $InstallPath
|
||||
BackendPath = Join-Path $InstallPath "backend"
|
||||
FrontendPath = Join-Path $InstallPath "frontend"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
TempPath = Join-Path $InstallPath "temp"
|
||||
PythonVersion = $PythonVersion
|
||||
ServicePort = $ServicePort
|
||||
IISSiteName = $IISSiteName
|
||||
IISAppName = $IISAppName
|
||||
IISAppPoolName = "ROA2WEB-AppPool"
|
||||
CreateNewSite = $CreateNewSite
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-CommandExists {
|
||||
param([string]$Command)
|
||||
try {
|
||||
if (Get-Command $Command -ErrorAction Stop) {
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Chocolatey {
|
||||
Write-Step "Installing Chocolatey package manager..."
|
||||
|
||||
if (Test-CommandExists "choco") {
|
||||
Write-Success "Chocolatey already installed"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
# Refresh environment
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
Write-Success "Chocolatey installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Chocolatey: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Python {
|
||||
Write-Step "Checking Python installation..."
|
||||
|
||||
if ($SkipPython) {
|
||||
Write-Warning "Skipping Python installation (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
# Check if Python is already installed
|
||||
try {
|
||||
$pythonCmd = Get-Command python -ErrorAction Stop
|
||||
$pythonVersionOutput = & python --version 2>&1
|
||||
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
|
||||
$installedVersion = $matches[1]
|
||||
Write-Success "Python $installedVersion already installed at $($pythonCmd.Source)"
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Python not found, will install..."
|
||||
}
|
||||
|
||||
# Install Python via Chocolatey
|
||||
Write-Step "Installing Python $PythonVersion..."
|
||||
try {
|
||||
choco install python --version=$PythonVersion -y --force
|
||||
|
||||
# Refresh environment
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
Write-Success "Python $PythonVersion installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NSSM {
|
||||
Write-Step "Installing NSSM (service manager)..."
|
||||
|
||||
if (Test-Path "C:\nssm\nssm.exe") {
|
||||
Write-Success "NSSM already installed"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
choco install nssm -y
|
||||
Write-Success "NSSM installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install NSSM: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-IISModules {
|
||||
if ($SkipIIS) {
|
||||
Write-Warning "Skipping IIS configuration (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Checking IIS installation..."
|
||||
|
||||
# Detect OS type (Server vs Desktop)
|
||||
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
|
||||
$isServer = $osInfo.ProductType -eq 3 # 1=Workstation, 2=Domain Controller, 3=Server
|
||||
|
||||
# Check if IIS is installed (different cmdlets for Server vs Desktop)
|
||||
$iisInstalled = $false
|
||||
|
||||
if ($isServer) {
|
||||
# Windows Server - use Get-WindowsFeature
|
||||
$iisFeature = Get-WindowsFeature -Name Web-Server -ErrorAction SilentlyContinue
|
||||
$iisInstalled = $iisFeature -and $iisFeature.InstallState -eq "Installed"
|
||||
|
||||
if (-not $iisInstalled) {
|
||||
Write-Error "IIS is not installed. Please install IIS first:"
|
||||
Write-Host " Install-WindowsFeature -Name Web-Server -IncludeManagementTools" -ForegroundColor Yellow
|
||||
throw "IIS not installed"
|
||||
}
|
||||
} else {
|
||||
# Windows Desktop (10/11) - use Get-WindowsOptionalFeature
|
||||
$iisFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -ErrorAction SilentlyContinue
|
||||
$iisInstalled = $iisFeature -and $iisFeature.State -eq "Enabled"
|
||||
|
||||
if (-not $iisInstalled) {
|
||||
Write-Error "IIS is not installed. Please install IIS first:"
|
||||
Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -All" -ForegroundColor Yellow
|
||||
Write-Host " Or use: Control Panel -> Programs -> Turn Windows features on/off -> Internet Information Services" -ForegroundColor Yellow
|
||||
throw "IIS not installed"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "IIS is installed ($($osInfo.Caption))"
|
||||
|
||||
# Install URL Rewrite Module
|
||||
Write-Step "Installing IIS URL Rewrite Module..."
|
||||
$urlRewriteInstalled = Get-WebConfiguration -Filter "/system.webServer/rewrite" -PSPath "IIS:\" -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $urlRewriteInstalled) {
|
||||
Write-Warning "URL Rewrite not found, installing..."
|
||||
try {
|
||||
$urlRewriteUrl = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi"
|
||||
$urlRewritePath = "$env:TEMP\rewrite_amd64.msi"
|
||||
|
||||
Invoke-WebRequest -Uri $urlRewriteUrl -OutFile $urlRewritePath
|
||||
Start-Process msiexec.exe -ArgumentList "/i", $urlRewritePath, "/quiet", "/norestart" -Wait
|
||||
Remove-Item $urlRewritePath -Force
|
||||
|
||||
Write-Success "URL Rewrite Module installed"
|
||||
} catch {
|
||||
Write-Error "Failed to install URL Rewrite: $_"
|
||||
Write-Warning "You can download it manually from: https://www.iis.net/downloads/microsoft/url-rewrite"
|
||||
}
|
||||
} else {
|
||||
Write-Success "URL Rewrite Module already installed"
|
||||
}
|
||||
|
||||
# Install Application Request Routing (ARR)
|
||||
Write-Step "Checking Application Request Routing (ARR)..."
|
||||
try {
|
||||
choco install iis-arr -y
|
||||
Write-Success "ARR installed successfully"
|
||||
} catch {
|
||||
Write-Warning "Could not install ARR via Chocolatey. Download manually from: https://www.iis.net/downloads/microsoft/application-request-routing"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DirectoryStructure {
|
||||
Write-Step "Creating directory structure..."
|
||||
|
||||
$directories = @(
|
||||
$Config.InstallPath,
|
||||
$Config.BackendPath,
|
||||
$Config.FrontendPath,
|
||||
$Config.LogsPath,
|
||||
$Config.TempPath,
|
||||
(Join-Path $Config.BackendPath "logs"),
|
||||
(Join-Path $Config.BackendPath "temp")
|
||||
)
|
||||
|
||||
foreach ($dir in $directories) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Write-Success "Created: $dir"
|
||||
} else {
|
||||
Write-Success "Already exists: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Set permissions (IIS user needs read access)
|
||||
try {
|
||||
$acl = Get-Acl $Config.InstallPath
|
||||
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
|
||||
$acl.SetAccessRule($accessRule)
|
||||
Set-Acl -Path $Config.InstallPath -AclObject $acl
|
||||
Write-Success "Permissions set for IIS_IUSRS"
|
||||
} catch {
|
||||
Write-Warning "Could not set permissions: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PythonDependencies {
|
||||
Write-Step "Installing Python dependencies..."
|
||||
|
||||
$requirementsPath = Join-Path $Config.BackendPath "requirements.txt"
|
||||
|
||||
if (-not (Test-Path $requirementsPath)) {
|
||||
Write-Warning "requirements.txt not found at $requirementsPath"
|
||||
Write-Warning "Please copy backend files first, then run this script again"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Upgrade pip first
|
||||
& python -m pip install --upgrade pip
|
||||
|
||||
# Install dependencies
|
||||
& python -m pip install -r $requirementsPath
|
||||
|
||||
Write-Success "Python dependencies installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python dependencies: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-WindowsService {
|
||||
Write-Step "Creating Windows Service for backend..."
|
||||
|
||||
# Check if service already exists using nssm (more reliable than Get-Service)
|
||||
# Temporarily disable error action to check service status
|
||||
$oldErrorAction = $ErrorActionPreference
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
$nssmOutput = & nssm status $Config.ServiceName 2>&1
|
||||
$serviceExists = $LASTEXITCODE -eq 0
|
||||
|
||||
$ErrorActionPreference = $oldErrorAction
|
||||
|
||||
if ($serviceExists) {
|
||||
Write-Warning "Service already exists, stopping and removing..."
|
||||
# Try to stop service (may fail if service is broken)
|
||||
& nssm stop $Config.ServiceName 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
# Force remove service
|
||||
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Success "Existing service removed"
|
||||
}
|
||||
|
||||
# Get Python path
|
||||
$pythonPath = (Get-Command python).Source
|
||||
$uvicornModule = "uvicorn"
|
||||
$appModule = "app.main:app"
|
||||
|
||||
# NSSM service creation
|
||||
try {
|
||||
# Install service
|
||||
& nssm install $Config.ServiceName $pythonPath "-m" $uvicornModule $appModule "--host" "127.0.0.1" "--port" $Config.ServicePort.ToString() "--workers" "4"
|
||||
|
||||
# Set service configuration
|
||||
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
||||
& nssm set $Config.ServiceName Description $Config.ServiceDescription
|
||||
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
|
||||
& nssm set $Config.ServiceName AppDirectory $Config.BackendPath
|
||||
|
||||
# Set environment variables (PYTHONPATH for shared modules)
|
||||
# Point to the installation root so shared/ modules can be imported
|
||||
$pythonPath = $Config.InstallPath
|
||||
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$pythonPath"
|
||||
|
||||
# Set logging
|
||||
$stdoutLog = Join-Path $Config.LogsPath "backend-stdout.log"
|
||||
$stderrLog = Join-Path $Config.LogsPath "backend-stderr.log"
|
||||
& nssm set $Config.ServiceName AppStdout $stdoutLog
|
||||
& nssm set $Config.ServiceName AppStderr $stderrLog
|
||||
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
|
||||
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
|
||||
|
||||
# Set restart policy
|
||||
& nssm set $Config.ServiceName AppExit Default Restart
|
||||
& nssm set $Config.ServiceName AppRestartDelay 5000
|
||||
|
||||
Write-Success "Windows Service created successfully"
|
||||
} catch {
|
||||
throw "Failed to create Windows Service: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-IISWebsite {
|
||||
if ($SkipIIS) {
|
||||
Write-Warning "Skipping IIS website configuration (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Configuring IIS application..."
|
||||
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
|
||||
# Remove existing app pool if present
|
||||
if (Test-Path "IIS:\AppPools\$($Config.IISAppPoolName)") {
|
||||
Write-Warning "Removing existing app pool..."
|
||||
Remove-WebAppPool -Name $Config.IISAppPoolName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create Application Pool
|
||||
Write-Step "Creating IIS Application Pool..."
|
||||
New-WebAppPool -Name $Config.IISAppPoolName -Force | Out-Null
|
||||
Set-ItemProperty -Path "IIS:\AppPools\$($Config.IISAppPoolName)" -Name "managedRuntimeVersion" -Value ""
|
||||
Write-Success "Application Pool created: $($Config.IISAppPoolName)"
|
||||
|
||||
if ($CreateNewSite) {
|
||||
# Create new website (old behavior)
|
||||
Write-Step "Creating new IIS Website..."
|
||||
|
||||
# Stop default website if running
|
||||
try {
|
||||
Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue
|
||||
Write-Success "Stopped Default Web Site"
|
||||
} catch {
|
||||
Write-Warning "Could not stop Default Web Site: $_"
|
||||
}
|
||||
|
||||
# Remove existing site if present
|
||||
if (Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue) {
|
||||
Write-Warning "Removing existing website..."
|
||||
Remove-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
New-Website -Name $Config.IISSiteName `
|
||||
-PhysicalPath $Config.FrontendPath `
|
||||
-ApplicationPool $Config.IISAppPoolName `
|
||||
-Port 80 `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Success "Website created: $($Config.IISSiteName)"
|
||||
|
||||
# Start website
|
||||
Start-Website -Name $Config.IISSiteName
|
||||
Write-Success "Website started: $($Config.IISSiteName)"
|
||||
} else {
|
||||
# Create application under existing site (default behavior)
|
||||
Write-Step "Creating IIS Application under '$($Config.IISSiteName)'..."
|
||||
|
||||
# Verify parent site exists
|
||||
$parentSite = Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
if (-not $parentSite) {
|
||||
throw "Parent website '$($Config.IISSiteName)' does not exist. Use -CreateNewSite to create a new site."
|
||||
}
|
||||
|
||||
# Remove existing application if present
|
||||
$existingApp = Get-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
if ($existingApp) {
|
||||
Write-Warning "Removing existing application..."
|
||||
Remove-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create application
|
||||
New-WebApplication -Name $Config.IISAppName `
|
||||
-Site $Config.IISSiteName `
|
||||
-PhysicalPath $Config.FrontendPath `
|
||||
-ApplicationPool $Config.IISAppPoolName `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Success "Application created: /$($Config.IISAppName) under $($Config.IISSiteName)"
|
||||
}
|
||||
|
||||
# Copy web.config to frontend path
|
||||
$webConfigSource = Join-Path $PSScriptRoot "..\config\web.config"
|
||||
$webConfigDest = Join-Path $Config.FrontendPath "web.config"
|
||||
|
||||
if (Test-Path $webConfigSource) {
|
||||
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
|
||||
Write-Success "web.config copied to frontend path"
|
||||
} else {
|
||||
Write-Warning "web.config not found at $webConfigSource"
|
||||
}
|
||||
}
|
||||
|
||||
function Start-Services {
|
||||
Write-Step "Starting services..."
|
||||
|
||||
# Start backend service
|
||||
try {
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$service = Get-Service -Name $Config.ServiceName
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Backend service started successfully"
|
||||
} else {
|
||||
Write-Error "Backend service failed to start (Status: $($service.Status))"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start backend service: $_"
|
||||
}
|
||||
|
||||
# Test backend health
|
||||
Write-Step "Testing backend health..."
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "Backend health check passed"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Backend health check failed (may need time to start): $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Summary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " ROA2WEB INSTALLATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Backend Path: $($Config.BackendPath)"
|
||||
Write-Host " Frontend Path: $($Config.FrontendPath)"
|
||||
Write-Host " Service Name: $($Config.ServiceName)"
|
||||
Write-Host " Service Port: $($Config.ServicePort)"
|
||||
Write-Host " IIS Site: $($Config.IISSiteName)"
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
if ($Config.CreateNewSite) {
|
||||
Write-Host " Web Application: http://localhost"
|
||||
} else {
|
||||
Write-Host " Web Application: http://localhost/$($Config.IISAppName)"
|
||||
}
|
||||
Write-Host " API Backend: http://localhost:$($Config.ServicePort)"
|
||||
Write-Host " API Docs: http://localhost:$($Config.ServicePort)/docs"
|
||||
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/health"
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Copy backend files to: $($Config.BackendPath)"
|
||||
Write-Host " 2. Copy frontend files to: $($Config.FrontendPath)"
|
||||
Write-Host " 3. Configure .env file at: $($Config.BackendPath)\.env"
|
||||
Write-Host " 4. Run: .\Deploy-ROA2WEB.ps1 to deploy updates"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " Start Service: .\Start-ROA2WEB.ps1"
|
||||
Write-Host " Stop Service: .\Stop-ROA2WEB.ps1"
|
||||
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
|
||||
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN INSTALLATION FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Windows Server Installation Script
|
||||
Modern ERP Reports Application with FastAPI + Vue.js + IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
try {
|
||||
# Installation steps
|
||||
Install-Chocolatey
|
||||
Install-Python
|
||||
Install-NSSM
|
||||
Install-IISModules
|
||||
New-DirectoryStructure
|
||||
Install-PythonDependencies
|
||||
New-WindowsService
|
||||
Initialize-IISWebsite
|
||||
Start-Services
|
||||
Show-Summary
|
||||
|
||||
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main installation
|
||||
Main
|
||||
633
deployment/windows/scripts/Install-TelegramBot.ps1
Normal file
633
deployment/windows/scripts/Install-TelegramBot.ps1
Normal file
@@ -0,0 +1,633 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB Telegram Bot - Installation Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs complete installation of ROA2WEB Telegram Bot on Windows Server:
|
||||
- Checks prerequisites (Admin rights, Python)
|
||||
- Installs NSSM (service manager) if needed
|
||||
- Creates directory structure
|
||||
- Installs Python dependencies
|
||||
- Creates Windows Service for Telegram bot
|
||||
- Configures internal API
|
||||
- Starts service
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER ServicePort
|
||||
Internal API service port (default: 8002)
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Source path for deployment package (auto-detected if run from scripts/ directory)
|
||||
|
||||
.PARAMETER SkipPython
|
||||
Skip Python installation check (use existing Python)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1
|
||||
Standard installation with defaults (auto-detects source from scripts/ directory)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1 -InstallPath "D:\Apps\roa2web\telegram-bot" -ServicePort 8003
|
||||
Custom installation path and port
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1 -SourcePath "C:\Deploy\telegram-bot"
|
||||
Install from specific source path
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges, Python 3.11+
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[int]$ServicePort = 8002,
|
||||
[string]$SourcePath = "",
|
||||
[switch]$SkipPython
|
||||
)
|
||||
|
||||
# Strict error handling
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = if ($SourcePath) {
|
||||
$SourcePath
|
||||
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
Split-Path $PSScriptRoot -Parent
|
||||
} else {
|
||||
$PSScriptRoot
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB-TelegramBot"
|
||||
ServiceName = "ROA2WEB-TelegramBot"
|
||||
ServiceDisplayName = "ROA2WEB Telegram Bot Service"
|
||||
ServiceDescription = "Telegram bot frontend for ROA2WEB with Claude Agent SDK"
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
TempPath = Join-Path $InstallPath "temp"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
ServicePort = $ServicePort
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-CommandExists {
|
||||
param([string]$Command)
|
||||
try {
|
||||
if (Get-Command $Command -ErrorAction Stop) {
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PythonInstallation {
|
||||
Write-Step "Checking Python installation..."
|
||||
|
||||
if ($SkipPython) {
|
||||
Write-Warning "Skipping Python check (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$pythonCmd = Get-Command python -ErrorAction Stop
|
||||
$pythonVersionOutput = & python --version 2>&1
|
||||
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
|
||||
$installedVersion = $matches[1]
|
||||
$versionParts = $installedVersion -split '\.'
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
|
||||
if ($major -ge 3 -and $minor -ge 11) {
|
||||
Write-Success "Python $installedVersion found at $($pythonCmd.Source)"
|
||||
return
|
||||
} else {
|
||||
throw "Python 3.11+ required, found $installedVersion"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Python 3.11+ not found. Please install Python first."
|
||||
Write-Host " Download from: https://www.python.org/downloads/" -ForegroundColor Yellow
|
||||
throw "Python not installed"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-ApplicationFiles {
|
||||
Write-Step "Copying application files from deployment package..."
|
||||
|
||||
$sourceApp = Join-Path $Config.SourcePath "app"
|
||||
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
|
||||
$sourceScripts = Join-Path $Config.SourcePath "scripts"
|
||||
|
||||
# Validate source files exist
|
||||
if (-not (Test-Path $sourceApp)) {
|
||||
Write-Warning "Source app directory not found: $sourceApp"
|
||||
Write-Warning "You may need to copy application files manually"
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not (Test-Path $sourceReq)) {
|
||||
Write-Warning "Source requirements.txt not found: $sourceReq"
|
||||
Write-Warning "You may need to copy requirements.txt manually"
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
# Copy app directory
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
if (Test-Path $destApp) {
|
||||
Write-Warning "App directory already exists, removing..."
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
}
|
||||
|
||||
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "Application files copied"
|
||||
|
||||
# Copy requirements.txt
|
||||
$destReq = Join-Path $Config.InstallPath "requirements.txt"
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
Write-Success "requirements.txt copied"
|
||||
|
||||
# Copy .env.example if exists (but don't overwrite .env)
|
||||
$sourceEnvExample = Join-Path $Config.SourcePath ".env.example"
|
||||
if (Test-Path $sourceEnvExample) {
|
||||
$destEnvExample = Join-Path $Config.InstallPath ".env.example"
|
||||
Copy-Item -Path $sourceEnvExample -Destination $destEnvExample -Force
|
||||
Write-Success ".env.example copied"
|
||||
}
|
||||
|
||||
# Copy management scripts (but exclude installation/deployment scripts)
|
||||
if (Test-Path $sourceScripts) {
|
||||
$destScripts = Join-Path $Config.InstallPath "scripts"
|
||||
if (-not (Test-Path $destScripts)) {
|
||||
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
|
||||
}
|
||||
|
||||
# List of management scripts to copy
|
||||
$managementScripts = @(
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$copiedScriptsCount = 0
|
||||
foreach ($script in $managementScripts) {
|
||||
$sourcePath = Join-Path $sourceScripts $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destPath = Join-Path $destScripts $script
|
||||
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||
$copiedScriptsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if ($copiedScriptsCount -gt 0) {
|
||||
Write-Success "Copied $copiedScriptsCount management scripts to scripts/ directory"
|
||||
} else {
|
||||
Write-Warning "No management scripts found to copy"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Source scripts directory not found: $sourceScripts"
|
||||
}
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Failed to copy application files: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NSSM {
|
||||
Write-Step "Installing NSSM (service manager)..."
|
||||
|
||||
if (Test-Path "C:\nssm\nssm.exe") {
|
||||
Write-Success "NSSM already installed"
|
||||
return
|
||||
}
|
||||
|
||||
# Check if Chocolatey is available
|
||||
if (Test-CommandExists "choco") {
|
||||
try {
|
||||
choco install nssm -y
|
||||
Write-Success "NSSM installed via Chocolatey"
|
||||
return
|
||||
} catch {
|
||||
Write-Warning "Chocolatey installation failed, trying direct download..."
|
||||
}
|
||||
}
|
||||
|
||||
# Direct download as fallback
|
||||
try {
|
||||
$nssmUrl = "https://nssm.cc/release/nssm-2.24.zip"
|
||||
$nssmZip = "$env:TEMP\nssm.zip"
|
||||
$nssmExtract = "$env:TEMP\nssm"
|
||||
|
||||
Write-Step "Downloading NSSM..."
|
||||
Invoke-WebRequest -Uri $nssmUrl -OutFile $nssmZip
|
||||
|
||||
Write-Step "Extracting NSSM..."
|
||||
Expand-Archive -Path $nssmZip -DestinationPath $nssmExtract -Force
|
||||
|
||||
# Copy nssm.exe to C:\nssm
|
||||
New-Item -ItemType Directory -Path "C:\nssm" -Force | Out-Null
|
||||
Copy-Item -Path "$nssmExtract\nssm-2.24\win64\nssm.exe" -Destination "C:\nssm\nssm.exe" -Force
|
||||
|
||||
# Add to PATH
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if ($currentPath -notlike "*C:\nssm*") {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$currentPath;C:\nssm", "Machine")
|
||||
$env:Path += ";C:\nssm"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
Remove-Item $nssmZip -Force
|
||||
Remove-Item $nssmExtract -Recurse -Force
|
||||
|
||||
Write-Success "NSSM installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install NSSM: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DirectoryStructure {
|
||||
Write-Step "Creating directory structure..."
|
||||
|
||||
$directories = @(
|
||||
$Config.InstallPath,
|
||||
(Join-Path $Config.InstallPath "app"),
|
||||
$Config.DataPath,
|
||||
$Config.LogsPath,
|
||||
$Config.TempPath,
|
||||
$Config.BackupPath
|
||||
)
|
||||
|
||||
foreach ($dir in $directories) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Write-Success "Created: $dir"
|
||||
} else {
|
||||
Write-Success "Already exists: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Set permissions (service needs full access to data, logs, backups)
|
||||
try {
|
||||
$acl = Get-Acl $Config.InstallPath
|
||||
|
||||
# Grant SYSTEM full control
|
||||
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
"SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
|
||||
)
|
||||
$acl.SetAccessRule($systemRule)
|
||||
|
||||
# Grant Administrators full control
|
||||
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
"Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
|
||||
)
|
||||
$acl.SetAccessRule($adminRule)
|
||||
|
||||
Set-Acl -Path $Config.InstallPath -AclObject $acl
|
||||
Write-Success "Permissions set for SYSTEM and Administrators"
|
||||
} catch {
|
||||
Write-Warning "Could not set permissions: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PythonDependencies {
|
||||
Write-Step "Installing Python dependencies..."
|
||||
|
||||
$requirementsPath = Join-Path $Config.InstallPath "requirements.txt"
|
||||
|
||||
if (-not (Test-Path $requirementsPath)) {
|
||||
Write-Warning "requirements.txt not found at $requirementsPath"
|
||||
Write-Warning "Please copy application files first, then run: pip install -r requirements.txt"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Create virtual environment
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
|
||||
if (-not (Test-Path $venvPath)) {
|
||||
Write-Step "Creating virtual environment..."
|
||||
& python -m venv $venvPath
|
||||
Write-Success "Virtual environment created"
|
||||
} else {
|
||||
Write-Success "Virtual environment already exists"
|
||||
}
|
||||
|
||||
# Activate and install dependencies
|
||||
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
|
||||
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
|
||||
|
||||
Write-Step "Upgrading pip..."
|
||||
& $pythonPath -m pip install --upgrade pip
|
||||
|
||||
Write-Step "Installing dependencies..."
|
||||
& $pipPath install -r $requirementsPath
|
||||
|
||||
Write-Success "Python dependencies installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python dependencies: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-WindowsService {
|
||||
Write-Step "Creating Windows Service for Telegram bot..."
|
||||
|
||||
# Check if service already exists
|
||||
$oldErrorAction = $ErrorActionPreference
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
$nssmOutput = & nssm status $Config.ServiceName 2>&1
|
||||
$serviceExists = $LASTEXITCODE -eq 0
|
||||
|
||||
$ErrorActionPreference = $oldErrorAction
|
||||
|
||||
if ($serviceExists) {
|
||||
Write-Warning "Service already exists, stopping and removing..."
|
||||
& nssm stop $Config.ServiceName 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Success "Existing service removed"
|
||||
}
|
||||
|
||||
# Get Python path from virtual environment
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
|
||||
|
||||
if (-not (Test-Path $pythonPath)) {
|
||||
throw "Virtual environment not found. Please run Install-PythonDependencies first."
|
||||
}
|
||||
|
||||
$appModule = "-m"
|
||||
$appMain = "app.main"
|
||||
|
||||
# NSSM service creation
|
||||
try {
|
||||
# Install service
|
||||
& nssm install $Config.ServiceName $pythonPath $appModule $appMain
|
||||
|
||||
# Set service configuration
|
||||
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
||||
& nssm set $Config.ServiceName Description $Config.ServiceDescription
|
||||
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
|
||||
& nssm set $Config.ServiceName AppDirectory $Config.InstallPath
|
||||
|
||||
# Set environment variables
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (Test-Path $envFile) {
|
||||
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$($Config.InstallPath)"
|
||||
Write-Success ".env file will be loaded by application"
|
||||
} else {
|
||||
Write-Warning ".env file not found - create it before starting service"
|
||||
}
|
||||
|
||||
# Set logging
|
||||
$stdoutLog = Join-Path $Config.LogsPath "stdout.log"
|
||||
$stderrLog = Join-Path $Config.LogsPath "stderr.log"
|
||||
& nssm set $Config.ServiceName AppStdout $stdoutLog
|
||||
& nssm set $Config.ServiceName AppStderr $stderrLog
|
||||
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
|
||||
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
|
||||
|
||||
# Set restart policy (important for bot reliability)
|
||||
& nssm set $Config.ServiceName AppExit Default Restart
|
||||
& nssm set $Config.ServiceName AppRestartDelay 5000
|
||||
& nssm set $Config.ServiceName AppThrottle 10000
|
||||
|
||||
Write-Success "Windows Service created successfully"
|
||||
} catch {
|
||||
throw "Failed to create Windows Service: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-ConfigurationFile {
|
||||
Write-Step "Creating configuration template..."
|
||||
|
||||
$envExample = Join-Path $Config.InstallPath ".env.example"
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
|
||||
# Create .env.example template
|
||||
$envTemplate = @"
|
||||
# ROA2WEB Telegram Bot - Production Configuration
|
||||
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_here
|
||||
|
||||
# Claude Authentication Configuration
|
||||
# =====================================
|
||||
# Two authentication methods are supported:
|
||||
#
|
||||
# Method 1: Claude Pro/Max Subscription (RECOMMENDED)
|
||||
# - Leave CLAUDE_API_KEY empty or remove the line
|
||||
# - Run: scripts\Setup-ClaudeAuth.ps1
|
||||
# - Authenticate via browser with your Claude Pro/Max account
|
||||
# - No additional costs!
|
||||
#
|
||||
# Method 2: Claude API Key (Alternative)
|
||||
# - Get API key from: https://console.anthropic.com/settings/keys
|
||||
# - Set CLAUDE_API_KEY below
|
||||
# - This will take precedence over browser login
|
||||
# - Usage-based billing applies
|
||||
#
|
||||
# Leave empty to use Claude Pro/Max subscription:
|
||||
CLAUDE_API_KEY=
|
||||
|
||||
# Backend API Configuration
|
||||
BACKEND_URL=http://localhost:8000
|
||||
BACKEND_TIMEOUT=30
|
||||
|
||||
# SQLite Database Configuration
|
||||
SQLITE_DB_PATH=$($Config.DataPath -replace '\\', '\\')\telegram_bot.db
|
||||
|
||||
# Internal API Configuration (for backend callbacks)
|
||||
INTERNAL_API_HOST=127.0.0.1
|
||||
INTERNAL_API_PORT=$($Config.ServicePort)
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=$($Config.LogsPath -replace '\\', '\\')\bot.log
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
|
||||
# Authentication Configuration
|
||||
AUTH_CODE_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_THRESHOLD_MINUTES=5
|
||||
|
||||
# Session Configuration
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
MAX_CONVERSATION_HISTORY=20
|
||||
"@
|
||||
|
||||
# Write .env.example
|
||||
Set-Content -Path $envExample -Value $envTemplate -Encoding UTF8
|
||||
Write-Success "Created .env.example template"
|
||||
|
||||
# Copy to .env if it doesn't exist
|
||||
if (-not (Test-Path $envFile)) {
|
||||
Copy-Item -Path $envExample -Destination $envFile
|
||||
Write-Warning "Created .env file - PLEASE UPDATE WITH PRODUCTION VALUES"
|
||||
Write-Host " Edit: $envFile" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Success ".env file already exists (not overwriting)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ServiceHealth {
|
||||
Write-Step "Testing service health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/internal/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Success "Health check passed: $($content.status)"
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Health check failed (service may need configuration): $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Summary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " ROA2WEB TELEGRAM BOT INSTALLATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
|
||||
Write-Host " Source Path: $($Config.SourcePath)"
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Scripts Path: $($Config.InstallPath)\scripts\"
|
||||
Write-Host " Data Path: $($Config.DataPath)"
|
||||
Write-Host " Logs Path: $($Config.LogsPath)"
|
||||
Write-Host " Backup Path: $($Config.BackupPath)"
|
||||
Write-Host " Service Name: $($Config.ServiceName)"
|
||||
Write-Host " Internal API Port: $($Config.ServicePort)"
|
||||
|
||||
Write-Host "`nService Endpoints:" -ForegroundColor Yellow
|
||||
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/internal/health"
|
||||
Write-Host " Stats: http://localhost:$($Config.ServicePort)/internal/stats"
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Edit configuration: $($Config.InstallPath)\.env"
|
||||
Write-Host " - Set TELEGRAM_BOT_TOKEN (from @BotFather)"
|
||||
Write-Host " - Set CLAUDE_API_KEY (from Anthropic console)"
|
||||
Write-Host " - Verify BACKEND_URL=http://localhost:8000"
|
||||
Write-Host " 2. Navigate to scripts: cd $($Config.InstallPath)\scripts"
|
||||
Write-Host " 3. Start service: .\Start-TelegramBot.ps1"
|
||||
Write-Host " 4. Check logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50"
|
||||
|
||||
Write-Host "`nManagement Scripts Location:" -ForegroundColor Yellow
|
||||
Write-Host " $($Config.InstallPath)\scripts\"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " cd $($Config.InstallPath)\scripts"
|
||||
Write-Host " .\Start-TelegramBot.ps1 # Start service"
|
||||
Write-Host " .\Stop-TelegramBot.ps1 # Stop service"
|
||||
Write-Host " .\Restart-TelegramBot.ps1 # Restart service"
|
||||
Write-Host " .\Backup-TelegramDB.ps1 # Backup database"
|
||||
Write-Host " .\Setup-DailyBackup.ps1 # Setup automated daily backups"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN INSTALLATION FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Windows Server Installation Script
|
||||
Telegram Bot Frontend with Claude Agent SDK
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
try {
|
||||
# Installation steps
|
||||
Test-PythonInstallation
|
||||
Install-NSSM
|
||||
New-DirectoryStructure
|
||||
|
||||
# Copy application files from deployment package
|
||||
$filesCopied = Copy-ApplicationFiles
|
||||
if (-not $filesCopied) {
|
||||
throw "Failed to copy application files. Please ensure you're running this script from the deployment package directory."
|
||||
}
|
||||
|
||||
Install-PythonDependencies
|
||||
New-ConfigurationFile
|
||||
New-WindowsService
|
||||
Show-Summary
|
||||
|
||||
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
|
||||
Write-Host "IMPORTANT: Configure .env file before starting service" -ForegroundColor Yellow
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main installation
|
||||
Main
|
||||
38
deployment/windows/scripts/Restart-ROA2WEB.ps1
Normal file
38
deployment/windows/scripts/Restart-ROA2WEB.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restart ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops and starts the ROA2WEB backend Windows service.
|
||||
|
||||
.EXAMPLE
|
||||
.\Restart-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Restarting ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Stop-ROA2WEB.ps1" -ServiceName $ServiceName
|
||||
|
||||
# Wait a moment
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Start service
|
||||
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Start-ROA2WEB.ps1" -ServiceName $ServiceName
|
||||
|
||||
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
38
deployment/windows/scripts/Restart-TelegramBot.ps1
Normal file
38
deployment/windows/scripts/Restart-TelegramBot.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restart ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops and starts the ROA2WEB Telegram Bot Windows service.
|
||||
|
||||
.EXAMPLE
|
||||
.\Restart-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Restarting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Stop-TelegramBot.ps1" -ServiceName $ServiceName
|
||||
|
||||
# Wait a moment
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Start service
|
||||
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Start-TelegramBot.ps1" -ServiceName $ServiceName
|
||||
|
||||
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
365
deployment/windows/scripts/Setup-DailyBackup.ps1
Normal file
365
deployment/windows/scripts/Setup-DailyBackup.ps1
Normal file
@@ -0,0 +1,365 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Setup Daily Backup Task for ROA2WEB Telegram Bot Database
|
||||
|
||||
.DESCRIPTION
|
||||
This script configures Windows Task Scheduler to run daily database backups:
|
||||
- Creates scheduled task (ROA2WEB-TelegramBot-Backup)
|
||||
- Runs daily at specified time (default: 2:00 AM)
|
||||
- Executes Backup-TelegramDB.ps1 script
|
||||
- Runs as SYSTEM account
|
||||
- Logs all backup operations
|
||||
- Optional email notifications on failure
|
||||
|
||||
.PARAMETER BackupTime
|
||||
Daily backup time in 24-hour format (default: "02:00")
|
||||
|
||||
.PARAMETER TaskName
|
||||
Name of the scheduled task (default: ROA2WEB-TelegramBot-Backup)
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER RunAsUser
|
||||
User account to run task (default: SYSTEM)
|
||||
|
||||
.PARAMETER EnableEmailAlerts
|
||||
Enable email notifications on backup failure (default: false)
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1
|
||||
Setup daily backup at 2:00 AM
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1 -BackupTime "03:30"
|
||||
Setup daily backup at 3:30 AM
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1 -EnableEmailAlerts $true
|
||||
Setup with email notifications on failure
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BackupTime = "02:00",
|
||||
[string]$TaskName = "ROA2WEB-TelegramBot-Backup",
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[string]$RunAsUser = "SYSTEM",
|
||||
[bool]$EnableEmailAlerts = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-BackupScript {
|
||||
$backupScriptPath = Join-Path $PSScriptRoot "Backup-TelegramDB.ps1"
|
||||
|
||||
if (-not (Test-Path $backupScriptPath)) {
|
||||
throw "Backup script not found: $backupScriptPath"
|
||||
}
|
||||
|
||||
Write-Success "Backup script found: $backupScriptPath"
|
||||
return $backupScriptPath
|
||||
}
|
||||
|
||||
function Remove-ExistingTask {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Checking for existing scheduled task..."
|
||||
|
||||
try {
|
||||
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingTask) {
|
||||
Write-Warning "Existing task found, removing..."
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
Write-Success "Existing task removed"
|
||||
} else {
|
||||
Write-Success "No existing task found"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not check for existing task: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-ScheduledBackupTask {
|
||||
param(
|
||||
[string]$TaskName,
|
||||
[string]$BackupScriptPath,
|
||||
[string]$BackupTime,
|
||||
[string]$RunAsUser
|
||||
)
|
||||
|
||||
Write-Step "Creating scheduled task..."
|
||||
|
||||
try {
|
||||
# Parse backup time
|
||||
$timeComponents = $BackupTime -split ":"
|
||||
$hour = [int]$timeComponents[0]
|
||||
$minute = [int]$timeComponents[1]
|
||||
|
||||
# Create task action (run PowerShell script)
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "PowerShell.exe" `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$BackupScriptPath`""
|
||||
|
||||
# Create task trigger (daily at specified time)
|
||||
$trigger = New-ScheduledTaskTrigger `
|
||||
-Daily `
|
||||
-At (Get-Date).Date.AddHours($hour).AddMinutes($minute)
|
||||
|
||||
# Create task settings
|
||||
$settings = New-ScheduledTaskSettings `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-RunOnlyIfNetworkAvailable:$false `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
# Create task principal (run as specified user)
|
||||
if ($RunAsUser -eq "SYSTEM") {
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId "SYSTEM" `
|
||||
-LogonType ServiceAccount `
|
||||
-RunLevel Highest
|
||||
} else {
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId $RunAsUser `
|
||||
-LogonType Password `
|
||||
-RunLevel Highest
|
||||
}
|
||||
|
||||
# Register task
|
||||
$task = Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description "Daily backup of ROA2WEB Telegram Bot SQLite database"
|
||||
|
||||
Write-Success "Scheduled task created: $TaskName"
|
||||
Write-Success "Schedule: Daily at $BackupTime"
|
||||
Write-Success "Run as: $RunAsUser"
|
||||
|
||||
return $task
|
||||
} catch {
|
||||
throw "Failed to create scheduled task: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TaskCreation {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Verifying task creation..."
|
||||
|
||||
try {
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
Write-Success "Task verified: $TaskName"
|
||||
Write-Host " Task State: $($task.State)" -ForegroundColor Gray
|
||||
Write-Host " Last Run: $($taskInfo.LastRunTime)" -ForegroundColor Gray
|
||||
Write-Host " Last Result: $($taskInfo.LastTaskResult)" -ForegroundColor Gray
|
||||
Write-Host " Next Run: $($taskInfo.NextRunTime)" -ForegroundColor Gray
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Task verification failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TaskExecution {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Testing task execution..."
|
||||
|
||||
try {
|
||||
# Run task immediately
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
|
||||
Write-Success "Task execution started"
|
||||
Write-Host " Waiting for task to complete..." -ForegroundColor Yellow
|
||||
|
||||
# Wait for task to complete (max 60 seconds)
|
||||
$timeout = 60
|
||||
$elapsed = 0
|
||||
|
||||
while ($elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 2
|
||||
$elapsed += 2
|
||||
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
$task = Get-ScheduledTask -TaskName $TaskName
|
||||
|
||||
if ($task.State -ne "Running") {
|
||||
break
|
||||
}
|
||||
|
||||
Write-Host " Task still running... ($elapsed/$timeout seconds)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Check result
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
if ($taskInfo.LastTaskResult -eq 0) {
|
||||
Write-Success "Task executed successfully"
|
||||
Write-Success "Last run: $($taskInfo.LastRunTime)"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Task completed with result code: $($taskInfo.LastTaskResult)"
|
||||
Write-Host " Check backup logs for details" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Task execution test failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-TaskSummary {
|
||||
param([string]$TaskName, [string]$BackupTime, [string]$InstallPath)
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DAILY BACKUP TASK CONFIGURED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nTask Configuration:" -ForegroundColor Yellow
|
||||
Write-Host " Task Name: $TaskName"
|
||||
Write-Host " Schedule: Daily at $BackupTime"
|
||||
Write-Host " Run As: $RunAsUser"
|
||||
Write-Host " Installation Path: $InstallPath"
|
||||
|
||||
$task = Get-ScheduledTask -TaskName $TaskName
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
Write-Host "`nTask Status:" -ForegroundColor Yellow
|
||||
Write-Host " State: $($task.State)"
|
||||
Write-Host " Last Run: $($taskInfo.LastRunTime)"
|
||||
Write-Host " Last Result: $($taskInfo.LastTaskResult)"
|
||||
Write-Host " Next Run: $($taskInfo.NextRunTime)"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " View task: Get-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Run manually: Start-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Disable task: Disable-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Enable task: Enable-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Remove task: Unregister-ScheduledTask -TaskName '$TaskName'"
|
||||
|
||||
Write-Host "`nBackup Management:" -ForegroundColor Yellow
|
||||
Write-Host " Manual backup: .\Backup-TelegramDB.ps1"
|
||||
Write-Host " Backup logs: Get-Content $InstallPath\logs\backup.log -Tail 50"
|
||||
Write-Host " Backups location: $InstallPath\backups"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN SETUP FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Setup Daily Backup Task
|
||||
Configure automated database backup via Task Scheduler
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
# Find backup script
|
||||
$backupScriptPath = Test-BackupScript
|
||||
|
||||
# Validate installation path
|
||||
if (-not (Test-Path $InstallPath)) {
|
||||
Write-Error "Installation path not found: $InstallPath"
|
||||
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path verified"
|
||||
|
||||
try {
|
||||
# Setup task
|
||||
Remove-ExistingTask -TaskName $TaskName
|
||||
$task = New-ScheduledBackupTask `
|
||||
-TaskName $TaskName `
|
||||
-BackupScriptPath $backupScriptPath `
|
||||
-BackupTime $BackupTime `
|
||||
-RunAsUser $RunAsUser
|
||||
|
||||
# Verify task creation
|
||||
$verified = Test-TaskCreation -TaskName $TaskName
|
||||
|
||||
if ($verified) {
|
||||
# Test task execution
|
||||
Write-Host "`nDo you want to test the backup task now? (Y/N)" -ForegroundColor Yellow
|
||||
$response = Read-Host
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
Test-TaskExecution -TaskName $TaskName
|
||||
} else {
|
||||
Write-Host " Skipping test execution" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Show summary
|
||||
Show-TaskSummary -TaskName $TaskName -BackupTime $BackupTime -InstallPath $InstallPath
|
||||
|
||||
Write-Host "`nSetup completed successfully!" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
throw "Task verification failed"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "`n[SETUP FAILED] $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main setup
|
||||
Main
|
||||
66
deployment/windows/scripts/Start-ROA2WEB.ps1
Normal file
66
deployment/windows/scripts/Start-ROA2WEB.ps1
Normal file
@@ -0,0 +1,66 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the ROA2WEB backend Windows service and validates it's running properly.
|
||||
|
||||
.EXAMPLE
|
||||
.\Start-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Starting ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service is already running" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Start service
|
||||
Start-Service -Name $ServiceName
|
||||
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to start
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service started successfully" -ForegroundColor Green
|
||||
|
||||
# Wait a bit and test health endpoint
|
||||
Start-Sleep -Seconds 3
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host " [OK] Health check passed" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [WARN] Health check failed (service may still be starting)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
70
deployment/windows/scripts/Start-TelegramBot.ps1
Normal file
70
deployment/windows/scripts/Start-TelegramBot.ps1
Normal file
@@ -0,0 +1,70 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the ROA2WEB Telegram Bot Windows service and validates it's running properly.
|
||||
|
||||
.EXAMPLE
|
||||
.\Start-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot",
|
||||
[int]$Timeout = 30,
|
||||
[int]$HealthPort = 8002
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Starting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service is already running" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Start service
|
||||
Start-Service -Name $ServiceName
|
||||
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to start
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service started successfully" -ForegroundColor Green
|
||||
|
||||
# Wait a bit and test health endpoint
|
||||
Start-Sleep -Seconds 5
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$HealthPort/internal/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Host " [OK] Health check passed: $($content.status)" -ForegroundColor Green
|
||||
Write-Host " [OK] Database: $($content.database.status)" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [WARN] Health check failed (service may still be starting): $_" -ForegroundColor Yellow
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
54
deployment/windows/scripts/Stop-ROA2WEB.ps1
Normal file
54
deployment/windows/scripts/Stop-ROA2WEB.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stop ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops the ROA2WEB backend Windows service gracefully.
|
||||
|
||||
.EXAMPLE
|
||||
.\Stop-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Stopping ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stop service
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to stop
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
54
deployment/windows/scripts/Stop-TelegramBot.ps1
Normal file
54
deployment/windows/scripts/Stop-TelegramBot.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stop ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops the ROA2WEB Telegram Bot Windows service gracefully.
|
||||
|
||||
.EXAMPLE
|
||||
.\Stop-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Stopping ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stop service
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to stop
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
157
docker-compose.production.yml
Normal file
157
docker-compose.production.yml
Normal file
@@ -0,0 +1,157 @@
|
||||
# ROA2WEB Docker Compose - Production Configuration
|
||||
# Use this file for production deployment: docker-compose -f docker-compose.yml -f docker-compose.production.yml up
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Backend production configuration
|
||||
roa-backend:
|
||||
deploy:
|
||||
replicas: 1
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 1G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 512M
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 10s
|
||||
max_attempts: 3
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- ENVIRONMENT=production
|
||||
- WORKERS=4
|
||||
- ORACLE_PASSWORD_FILE=/run/secrets/oracle_password
|
||||
- JWT_SECRET_KEY_FILE=/run/secrets/jwt_secret_key
|
||||
command: ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
secrets:
|
||||
- oracle_password
|
||||
- jwt_secret_key
|
||||
depends_on:
|
||||
- roa-redis # Only Redis dependency in production
|
||||
|
||||
# Frontend production configuration
|
||||
roa-frontend:
|
||||
deploy:
|
||||
replicas: 1
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 256M
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 128M
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
# Gateway production configuration with SSL
|
||||
roa-gateway:
|
||||
deploy:
|
||||
replicas: 1
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 512M
|
||||
reservations:
|
||||
cpus: '0.25'
|
||||
memory: 256M
|
||||
environment:
|
||||
- ENVIRONMENT=production
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ssl-certs:/etc/letsencrypt
|
||||
- nginx-logs:/var/log/nginx
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
|
||||
# SSH Tunnel is disabled in production
|
||||
roa-ssh-tunnel:
|
||||
deploy:
|
||||
replicas: 0 # Disable SSH tunnel in production
|
||||
|
||||
# Redis production configuration
|
||||
roa-redis:
|
||||
deploy:
|
||||
replicas: 1
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.25'
|
||||
memory: 256M
|
||||
reservations:
|
||||
cpus: '0.1'
|
||||
memory: 128M
|
||||
command: redis-server --appendonly yes --requirepass_file /run/secrets/redis_password --maxmemory 128mb --maxmemory-policy allkeys-lru
|
||||
secrets:
|
||||
- redis_password
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
# SSL Certificate Management (Let's Encrypt)
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: roa-certbot
|
||||
volumes:
|
||||
- ssl-certs:/etc/letsencrypt
|
||||
- ./nginx/html:/var/www/certbot
|
||||
command: certonly --webroot --webroot-path=/var/www/certbot --email ${SSL_EMAIL} --agree-tos --no-eff-email --keep-until-expiring -d ${DOMAIN}
|
||||
depends_on:
|
||||
- roa-gateway
|
||||
|
||||
# Monitoring and logging (optional)
|
||||
# Uncomment if you want to add monitoring
|
||||
# prometheus:
|
||||
# image: prom/prometheus:latest
|
||||
# container_name: roa-prometheus
|
||||
# ports:
|
||||
# - "9090:9090"
|
||||
# volumes:
|
||||
# - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
# networks:
|
||||
# - roa-network
|
||||
|
||||
# grafana:
|
||||
# image: grafana/grafana:latest
|
||||
# container_name: roa-grafana
|
||||
# ports:
|
||||
# - "3001:3000"
|
||||
# environment:
|
||||
# - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
|
||||
# volumes:
|
||||
# - grafana-data:/var/lib/grafana
|
||||
# networks:
|
||||
# - roa-network
|
||||
|
||||
# Production secrets management
|
||||
secrets:
|
||||
oracle_password:
|
||||
file: ./secrets/oracle_password.txt
|
||||
jwt_secret_key:
|
||||
file: ./secrets/jwt_secret_key.txt
|
||||
redis_password:
|
||||
file: ./secrets/redis_password.txt
|
||||
|
||||
# Additional volumes for production
|
||||
# volumes:
|
||||
# grafana-data:
|
||||
# driver: local
|
||||
20
docker-compose.ssh-tunnel.yml
Normal file
20
docker-compose.ssh-tunnel.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# SSH Tunnel Override - Only for development
|
||||
# This file is automatically included in development but excluded in production
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# SSH Tunnel is only enabled in development
|
||||
roa-ssh-tunnel:
|
||||
profiles:
|
||||
- development
|
||||
|
||||
# Backend dependency adjustment for development
|
||||
roa-backend:
|
||||
depends_on:
|
||||
- roa-redis
|
||||
- roa-ssh-tunnel
|
||||
environment:
|
||||
# In development, connect through SSH tunnel
|
||||
- ORACLE_HOST=roa-ssh-tunnel
|
||||
- ORACLE_PORT=1526
|
||||
209
docker-compose.yml
Normal file
209
docker-compose.yml
Normal file
@@ -0,0 +1,209 @@
|
||||
# ROA2WEB Docker Compose - Main Configuration
|
||||
# This is the base configuration for all environments
|
||||
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
roa-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
|
||||
volumes:
|
||||
nginx-logs:
|
||||
driver: local
|
||||
backend-logs:
|
||||
driver: local
|
||||
ssl-certs:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
telegram-bot-data:
|
||||
driver: local
|
||||
|
||||
services:
|
||||
# FastAPI Backend Service
|
||||
roa-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./reports-app/backend/Dockerfile
|
||||
target: production
|
||||
image: roa2web/backend:latest
|
||||
container_name: roa-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Database configuration
|
||||
- ORACLE_USER=${ORACLE_USER:-CONTAFIN_ORACLE}
|
||||
- ORACLE_PASSWORD=${ORACLE_PASSWORD}
|
||||
- ORACLE_HOST=roa-ssh-tunnel
|
||||
- ORACLE_PORT=${ORACLE_PORT:-1526}
|
||||
- ORACLE_SID=${ORACLE_SID:-ROA}
|
||||
|
||||
# JWT configuration
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
|
||||
- JWT_EXPIRE_MINUTES=${JWT_EXPIRE_MINUTES:-30}
|
||||
|
||||
# Application settings
|
||||
- ENVIRONMENT=${ENVIRONMENT:-development}
|
||||
- DEBUG=${DEBUG:-false}
|
||||
- API_V1_STR=${API_V1_STR:-/api/v1}
|
||||
networks:
|
||||
- roa-network
|
||||
volumes:
|
||||
- backend-logs:/app/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
depends_on:
|
||||
- roa-redis
|
||||
- roa-ssh-tunnel
|
||||
|
||||
# Vue.js Frontend Service
|
||||
roa-frontend:
|
||||
build:
|
||||
context: ./reports-app/frontend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
image: roa2web/frontend:latest
|
||||
container_name: roa-frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=${NODE_ENV:-production}
|
||||
- VITE_API_BASE_URL=${VITE_API_BASE_URL:-/api}
|
||||
networks:
|
||||
- roa-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Nginx Gateway Service
|
||||
roa-gateway:
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
image: roa2web/nginx-gateway:latest
|
||||
container_name: roa-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:8080" # Development port
|
||||
environment:
|
||||
- ENVIRONMENT=${ENVIRONMENT:-development}
|
||||
- DOMAIN=${DOMAIN:-localhost}
|
||||
- SSL_EMAIL=${SSL_EMAIL:-admin@roa2web.local}
|
||||
networks:
|
||||
- roa-network
|
||||
volumes:
|
||||
- nginx-logs:/var/log/nginx
|
||||
- ssl-certs:/etc/letsencrypt
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
- roa-backend
|
||||
- roa-frontend
|
||||
|
||||
# SSH Tunnel for Oracle Database (development only)
|
||||
roa-ssh-tunnel:
|
||||
build:
|
||||
context: ./ssh-tunnel
|
||||
dockerfile: Dockerfile
|
||||
image: roa2web/ssh-tunnel:latest
|
||||
container_name: roa-ssh-tunnel
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SSH_SERVER=${SSH_SERVER:-83.103.197.79}
|
||||
- SSH_PORT=${SSH_PORT:-22122}
|
||||
- SSH_USER=${SSH_USER:-roa2web}
|
||||
- SSH_KEY_PATH=/home/tunnel/.ssh/roa_oracle_server
|
||||
- LOCAL_PORT=1526
|
||||
- REMOTE_HOST=${REMOTE_HOST:-10.0.20.36}
|
||||
- REMOTE_PORT=1521
|
||||
# SSH key is now built into the image
|
||||
ports:
|
||||
- "1526:1526"
|
||||
networks:
|
||||
- roa-network
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "1526"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
# Redis for session storage and caching (optional but recommended)
|
||||
roa-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: roa-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-roa2web_redis_password}
|
||||
environment:
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-roa2web_redis_password}
|
||||
networks:
|
||||
- roa-network
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Telegram Bot Service (Claude Agent SDK integration)
|
||||
roa-telegram-bot:
|
||||
build:
|
||||
context: ./reports-app/telegram-bot
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
image: roa2web/telegram-bot:latest
|
||||
container_name: roa-telegram-bot
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Telegram Bot Configuration
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- CLAUDE_API_KEY=${CLAUDE_API_KEY}
|
||||
|
||||
# Backend API Configuration
|
||||
- BACKEND_URL=http://roa-backend:8000
|
||||
|
||||
# Database Configuration (SQLite standalone)
|
||||
- SQLITE_DB_PATH=/app/data/telegram_bot.db
|
||||
|
||||
# Internal API Configuration
|
||||
- INTERNAL_API_PORT=8002
|
||||
|
||||
# Optional Configuration
|
||||
- LOG_LEVEL=${TELEGRAM_LOG_LEVEL:-INFO}
|
||||
- SENTRY_DSN=${TELEGRAM_SENTRY_DSN:-}
|
||||
- ENVIRONMENT=${ENVIRONMENT:-production}
|
||||
networks:
|
||||
- roa-network
|
||||
volumes:
|
||||
# Persistent SQLite database storage
|
||||
- telegram-bot-data:/app/data
|
||||
ports:
|
||||
# Internal API port (for backend to save auth codes)
|
||||
- "8002:8002"
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import httpx; import asyncio; asyncio.run(httpx.AsyncClient().get('http://localhost:8002/internal/health'))"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
depends_on:
|
||||
roa-backend:
|
||||
condition: service_healthy
|
||||
|
||||
394
docs/ARCHITECTURE_SCHEMA.md
Normal file
394
docs/ARCHITECTURE_SCHEMA.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# 📊 ROA2WEB - SCHEMĂ GRAFICĂ ARHITECTURĂ
|
||||
|
||||
Această schemă prezintă arhitectura completă a aplicației ROA2WEB, incluzând frontend-ul Vue.js, backend-ul FastAPI, middleware-ul de autentificare și conexiunea la baza de date Oracle.
|
||||
|
||||
## 🏗️ **ARHITECTURA GENERALĂ**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🌐 CLIENT │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ HTTP Requests
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🖥️ FRONTEND │
|
||||
│ Vue.js 3 + PrimeVue + Vite │
|
||||
│ Port: 5173 (dev) / 3000 (prod) │
|
||||
│ │
|
||||
│ 📁 Components: 📦 Stores (Pinia): │
|
||||
│ • LoginView.vue • auth.js (JWT tokens) │
|
||||
│ • DashboardView.vue • companies.js │
|
||||
│ • InvoicesView.vue • dashboard.js │
|
||||
│ • BankCashRegisterView.vue • invoices.js │
|
||||
│ • treasury.js │
|
||||
│ 🔧 Services: │
|
||||
│ • api.js (Axios HTTP client) │
|
||||
│ • JWT token management │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ API Calls (axios)
|
||||
│ Authorization: Bearer <JWT>
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🚀 BACKEND API │
|
||||
│ FastAPI + Uvicorn │
|
||||
│ Port: 8000 │
|
||||
│ │
|
||||
│ 🛡️ MIDDLEWARE LAYER: │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 1. CORSMiddleware (Frontend communication) │ │
|
||||
│ │ 2. AuthenticationMiddleware (JWT validation) │ │
|
||||
│ │ • Token extraction from Authorization header │ │
|
||||
│ │ • JWT verification & user data injection │ │
|
||||
│ │ • Rate limiting (5 req/5min per IP) │ │
|
||||
│ │ • Security headers injection │ │
|
||||
│ │ • Excluded paths: ["/", "/docs", "/health", "/api/auth/login"] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 🛤️ API ROUTES: │
|
||||
│ • /api/auth/login (POST) - User authentication │
|
||||
│ • /api/companies (GET) - Company list │
|
||||
│ • /api/dashboard (GET) - Dashboard data │
|
||||
│ • /api/invoices (GET) - Invoice reports │
|
||||
│ • /api/treasury (GET) - Treasury/Bank data │
|
||||
│ • /health (GET) - Health check │
|
||||
│ │
|
||||
│ 📊 SERVICES: │
|
||||
│ • invoice_service.py │
|
||||
│ • dashboard_service.py │
|
||||
│ • treasury_service.py │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ Database Queries
|
||||
│ SSH Tunnel Required
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🔐 SSH TUNNEL LAYER │
|
||||
│ ./ssh_tunnel.sh (Local port forwarding) │
|
||||
│ Local: localhost:1526 ➜ Remote: oracle_server:1521 │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ Encrypted connection
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🏛️ ORACLE DATABASE │
|
||||
│ Schema: CONTAFIN_ORACLE │
|
||||
│ Port: 1521 (remote) / 1526 (local via tunnel) │
|
||||
│ │
|
||||
│ 📋 Main Tables/Views: │
|
||||
│ • UTILIZATORI (Users) │
|
||||
│ • V_NOM_FIRME (Companies) │
|
||||
│ • VDEF_UTIL_FIRME (User-Company relations) │
|
||||
│ • Financial data tables (invoices, payments, etc.) │
|
||||
│ │
|
||||
│ 🔧 Stored Procedures: │
|
||||
│ • pack_drepturi.verificautilizator (Authentication) │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔄 **FLUX DE AUTENTIFICARE**
|
||||
|
||||
```
|
||||
1. User Login (Frontend)
|
||||
↓
|
||||
2. POST /api/auth/login (Backend)
|
||||
↓
|
||||
3. Oracle Authentication via SSH Tunnel
|
||||
• pack_drepturi.verificautilizator(username, password)
|
||||
↓
|
||||
4. JWT Token Generation (Backend)
|
||||
• Access Token (30 min)
|
||||
• Refresh Token (7 days)
|
||||
• User data + companies + permissions
|
||||
↓
|
||||
5. Token Storage (Frontend - Pinia Store)
|
||||
↓
|
||||
6. Subsequent API Requests
|
||||
• Authorization: Bearer <token>
|
||||
• AuthenticationMiddleware validation
|
||||
• User data injection in request.state
|
||||
```
|
||||
|
||||
## 🚦 **MIDDLEWARE AUTHENTICATION FLOW**
|
||||
|
||||
```
|
||||
Incoming Request
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ Rate Limiting │ → 429 if exceeded (5 req/5min per IP)
|
||||
└─────┬───────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ Path Exclusion │ → Skip auth for /docs, /health, /api/auth/login
|
||||
└─────┬───────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ Token Extract │ → 401 if missing Authorization header
|
||||
└─────┬───────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ JWT Validation │ → 401 if invalid/expired/malformed
|
||||
└─────┬───────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ User Injection │ → request.state.user = CurrentUser
|
||||
└─────┬───────────┘ → request.state.is_authenticated = True
|
||||
↓ → request.state.token_data = TokenData
|
||||
┌─────────────────┐
|
||||
│ Security Headers│ → X-Content-Type-Options, X-Frame-Options
|
||||
└─────┬───────────┘ → X-XSS-Protection, X-Process-Time
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ Route Handler │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 🗂️ **STRUCTURA DE FIȘIERE**
|
||||
|
||||
### Frontend (Vue.js)
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── dashboard/
|
||||
│ │ ├── layout/
|
||||
│ │ ├── reports/
|
||||
│ │ └── ui/
|
||||
│ ├── stores/ (Pinia)
|
||||
│ │ ├── auth.js
|
||||
│ │ ├── companies.js
|
||||
│ │ ├── dashboard.js
|
||||
│ │ ├── invoices.js
|
||||
│ │ └── treasury.js
|
||||
│ ├── services/
|
||||
│ │ └── api.js
|
||||
│ ├── views/
|
||||
│ │ ├── LoginView.vue
|
||||
│ │ ├── DashboardView.vue
|
||||
│ │ ├── InvoicesView.vue
|
||||
│ │ └── BankCashRegisterView.vue
|
||||
│ └── router/
|
||||
└── tests/ (Playwright E2E)
|
||||
```
|
||||
|
||||
### Backend (FastAPI)
|
||||
```
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── main.py
|
||||
│ ├── routers/
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── companies.py
|
||||
│ │ ├── dashboard.py
|
||||
│ │ ├── invoices.py
|
||||
│ │ └── treasury.py
|
||||
│ ├── services/
|
||||
│ │ ├── invoice_service.py
|
||||
│ │ ├── dashboard_service.py
|
||||
│ │ └── treasury_service.py
|
||||
│ └── models/
|
||||
└── shared/
|
||||
├── auth/
|
||||
│ ├── middleware.py
|
||||
│ ├── jwt_handler.py
|
||||
│ ├── auth_service.py
|
||||
│ └── models.py
|
||||
└── database/
|
||||
└── oracle_pool.py
|
||||
```
|
||||
|
||||
## 🔧 **TEHNOLOGII UTILIZATE**
|
||||
|
||||
### Frontend Stack
|
||||
- **Vue.js 3** - Framework JavaScript reactiv
|
||||
- **PrimeVue** - UI Component Library
|
||||
- **Pinia** - State Management
|
||||
- **Vite** - Build Tool & Dev Server
|
||||
- **Axios** - HTTP Client
|
||||
- **Vue Router** - Client-side routing
|
||||
- **Chart.js** - Data visualization
|
||||
- **Playwright** - E2E Testing
|
||||
|
||||
### Backend Stack
|
||||
- **FastAPI** - Python Web Framework
|
||||
- **Uvicorn** - ASGI Server
|
||||
- **PyJWT** - JWT Token handling
|
||||
- **cx_Oracle** - Oracle Database driver
|
||||
- **Pydantic** - Data validation
|
||||
- **Python-dotenv** - Environment variables
|
||||
|
||||
### Database & Infrastructure
|
||||
- **Oracle Database** - Persistent data storage
|
||||
- **SSH Tunnel** - Secure database connection (Linux/development)
|
||||
- **Docker** - Containerization (Linux production)
|
||||
- **Nginx** - Reverse proxy & static files (Linux production)
|
||||
- **Windows Server + IIS** - Windows production deployment
|
||||
- **NSSM** - Windows service manager
|
||||
|
||||
## 🪟 **ARHITECTURA WINDOWS SERVER/IIS**
|
||||
|
||||
### Deployment pe Windows Server
|
||||
|
||||
ROA2WEB poate fi deployment pe Windows Server folosind IIS și Windows Services, fără Docker:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🌐 CLIENT │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ HTTP/HTTPS Requests
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🪟 IIS WEB SERVER │
|
||||
│ Port: 80/443 (HTTPS with SSL certificate) │
|
||||
│ │
|
||||
│ 📁 Static Files Serving: 🔀 URL Rewrite Module: │
|
||||
│ • Frontend (Vue.js build) • /api/* → Backend Service │
|
||||
│ • web.config configuration • /* → index.html (SPA routing) │
|
||||
│ • Compression & Caching • Application Request Routing (ARR) │
|
||||
│ │
|
||||
│ ⚙️ Application Pool: │
|
||||
│ • ROA2WEB-AppPool (.NET not required) │
|
||||
│ • Integrated pipeline mode │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ Reverse Proxy to Backend
|
||||
│ http://localhost:8000/api/*
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🔧 WINDOWS SERVICE │
|
||||
│ Service Name: ROA2WEB-Backend │
|
||||
│ Manager: NSSM (Non-Sucking Service Manager) │
|
||||
│ Port: 8000 (localhost only) │
|
||||
│ │
|
||||
│ 📊 Backend Components: │
|
||||
│ • FastAPI + Uvicorn (Python 3.11+) │
|
||||
│ • Auto-start on Windows boot │
|
||||
│ • Auto-restart on failure (5 sec delay) │
|
||||
│ • Logging to file (stdout/stderr) │
|
||||
│ │
|
||||
│ 📁 Installation Location: │
|
||||
│ • C:\inetpub\wwwroot\roa2web\backend\ │
|
||||
└─────────────────┬───────────────────────────────────────────────────────────────┘
|
||||
│ Direct Database Connection
|
||||
│ No SSH Tunnel Required
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🏛️ ORACLE DATABASE (Local/Network) │
|
||||
│ Connection: Direct TCP/IP (localhost:1521 or network) │
|
||||
│ Schema: CONTAFIN_ORACLE │
|
||||
│ │
|
||||
│ 📋 Same Tables/Views as Linux deployment │
|
||||
│ 🔧 Same Stored Procedures │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Diferențe între Linux și Windows Deployment
|
||||
|
||||
| Aspect | Linux (Docker) | Windows (IIS) |
|
||||
|--------|----------------|---------------|
|
||||
| **Web Server** | Nginx (container) | IIS (native) |
|
||||
| **Backend Runtime** | Docker container | Windows Service (NSSM) |
|
||||
| **Database Access** | SSH Tunnel required | Direct connection |
|
||||
| **SSL/TLS** | Let's Encrypt (certbot) | IIS SSL certificates |
|
||||
| **Service Management** | Docker Compose | PowerShell + Services.msc |
|
||||
| **Deployment** | `./scripts/deploy.sh` | `Deploy-ROA2WEB.ps1` |
|
||||
| **Logs** | Docker logs | Windows Event Log + Files |
|
||||
| **Auto-start** | Docker restart policies | Windows Service auto-start |
|
||||
|
||||
### Structura Fișiere Windows Deployment
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── backend\ # FastAPI application
|
||||
│ ├── app\
|
||||
│ │ ├── main.py
|
||||
│ │ ├── routers\
|
||||
│ │ ├── services\
|
||||
│ │ └── models\
|
||||
│ ├── shared\ # Shared components
|
||||
│ │ ├── auth\
|
||||
│ │ ├── database\
|
||||
│ │ └── utils\
|
||||
│ ├── requirements.txt
|
||||
│ ├── .env # Production config
|
||||
│ └── logs\
|
||||
│
|
||||
├── frontend\ # Vue.js build output
|
||||
│ ├── index.html
|
||||
│ ├── assets\
|
||||
│ ├── web.config # IIS configuration
|
||||
│ └── ...
|
||||
│
|
||||
├── logs\ # Service logs
|
||||
│ ├── backend-stdout.log
|
||||
│ └── backend-stderr.log
|
||||
│
|
||||
└── backups\ # Deployment backups
|
||||
└── backup-YYYYMMDD-HHMMSS\
|
||||
```
|
||||
|
||||
### Comenzi Windows Deployment
|
||||
|
||||
```powershell
|
||||
# Instalare inițială
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
# Deployment actualizări
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\path\to\deploy-package"
|
||||
|
||||
# Management serviciu
|
||||
.\Start-ROA2WEB.ps1
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Verificare status
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# Logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
```
|
||||
|
||||
Pentru detalii complete despre deployment pe Windows, consultați:
|
||||
- `/deployment/windows/docs/WINDOWS_DEPLOYMENT.md`
|
||||
- `/deployment/windows/README.md`
|
||||
|
||||
## ⚙️ **COMENZI DE DEZVOLTARE**
|
||||
|
||||
### Start SSH Tunnel
|
||||
```bash
|
||||
cd /mnt/d/PROIECTE/roa-flask/roa2web
|
||||
./ssh_tunnel.sh start
|
||||
```
|
||||
|
||||
### Backend Development
|
||||
```bash
|
||||
cd reports-app/backend/
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
```bash
|
||||
cd reports-app/frontend/
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
cd shared/
|
||||
python -m pytest -v
|
||||
```
|
||||
|
||||
## 🛡️ **SECURITATE**
|
||||
|
||||
### Middleware de Autentificare
|
||||
- **JWT Token Validation** - Verificare automată pentru toate endpoint-urile protejate
|
||||
- **Rate Limiting** - Protecție împotriva atacurilor brute force
|
||||
- **Security Headers** - X-Content-Type-Options, X-Frame-Options, X-XSS-Protection
|
||||
- **CORS Protection** - Configurare restrictivă pentru frontend-uri autorizate
|
||||
|
||||
### Baza de Date
|
||||
- **SSH Tunnel** - Conexiune criptată la Oracle
|
||||
- **Schema Dedicată** - CONTAFIN_ORACLE pentru izolare
|
||||
- **Stored Procedures** - Validare securizată de utilizatori
|
||||
|
||||
---
|
||||
|
||||
*Această schemă oferă o vedere de ansamblu asupra arhitecturii ROA2WEB și poate fi utilizată pentru documentare, onboarding și planificarea dezvoltării viitoare.*
|
||||
1000
docs/DEPLOYMENT_GUIDE.md
Normal file
1000
docs/DEPLOYMENT_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
193
docs/DEVELOPMENT_BLUEPRINT.md
Normal file
193
docs/DEVELOPMENT_BLUEPRINT.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# ROA2WEB DEVELOPMENT BLUEPRINT
|
||||
*Ghid Complet pentru Dezvoltarea Aplicației FastAPI + Vue.js*
|
||||
|
||||
---
|
||||
|
||||
## 🎯 VIZIUNEA PROIECTULUI
|
||||
|
||||
### Obiectiv Principal
|
||||
Transformarea aplicației Flask existente într-un ecosistem modern FastAPI + Vue.js pentru rapoarte ERP (facturi și încasări), cu arhitectură modulară pentru extensii viitoare.
|
||||
|
||||
### Directorul Principal: `roa2web`
|
||||
|
||||
---
|
||||
|
||||
## 📋 STATUS GENERAL DEZVOLTARE
|
||||
|
||||
| Componentă | Status | Progres | Următorul Pas |
|
||||
|------------|--------|---------|----------------|
|
||||
| Git Setup | ✅ COMPLET | 100% | - |
|
||||
| Structură Proiect | ✅ COMPLET | 100% | - |
|
||||
| Shared Database Pool | ✅ COMPLET | 100% | - |
|
||||
| Shared Authentication | ✅ COMPLET | 100% | - |
|
||||
| Backend FastAPI | ✅ COMPLET | 100% | - |
|
||||
| Backend Testing | ✅ COMPLET | 100% | - |
|
||||
| Frontend Vue.js | ✅ COMPLET | 100% | - |
|
||||
| Docker Compose | ✅ COMPLET | 100% | - |
|
||||
| Nginx Gateway | ✅ COMPLET | 100% | - |
|
||||
| SSH Tunnel Oracle | ✅ COMPLET | 100% | - |
|
||||
| Production Ready | ✅ COMPLET | 100% | - |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ ARHITECTURA FINALĂ
|
||||
|
||||
### Structură Completă `roa2web`
|
||||
```
|
||||
|
||||
├── shared/ # 🔧 Componente Comune ✅ COMPLET
|
||||
│ ├── database/ # Oracle connection pool
|
||||
│ ├── auth/ # JWT authentication
|
||||
│ └── utils/ # Utilități comune
|
||||
│
|
||||
├── reports-app/ # 📊 Aplicația Rapoarte
|
||||
│ ├── backend/ # FastAPI Backend ✅ COMPLET
|
||||
│ │ ├── app/
|
||||
│ │ │ ├── main.py # FastAPI entry point
|
||||
│ │ │ ├── models/ # Pydantic models
|
||||
│ │ │ ├── routers/ # API endpoints
|
||||
│ │ │ ├── services/ # Business logic
|
||||
│ │ │ └── schemas/ # Response schemas
|
||||
│ │ ├── requirements.txt
|
||||
│ │ ├── Dockerfile
|
||||
│ │ └── .env.example
|
||||
│ │
|
||||
│ ├── frontend/ # Vue.js Frontend ✅ COMPLET
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── main.js # Vue app entry
|
||||
│ │ │ ├── App.vue # Root component
|
||||
│ │ │ ├── router/ # Vue Router
|
||||
│ │ │ ├── stores/ # Pinia stores
|
||||
│ │ │ ├── views/ # Page components
|
||||
│ │ │ ├── components/ # Reusable components
|
||||
│ │ │ ├── services/ # API communication
|
||||
│ │ │ ├── composables/ # Vue composables
|
||||
│ │ │ ├── assets/ # Static assets
|
||||
│ │ │ └── utils/ # Helper functions
|
||||
│ │ ├── package.json
|
||||
│ │ ├── vite.config.js
|
||||
│ │ ├── Dockerfile
|
||||
│ │ └── .env.example
|
||||
│ │
|
||||
│ └── README.md
|
||||
│
|
||||
├── future-apps/ # 🚀 Pentru Aplicații Viitoare
|
||||
├── nginx/ # 🌐 Gateway ✅ COMPLET
|
||||
├── docker-compose.yml # 🐳 Orchestration ✅ COMPLET
|
||||
├── ssh-tunnel/ # 🔐 SSH Tunnel ✅ COMPLET
|
||||
├── scripts/ # 📜 Integration Tests ✅ COMPLET
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── DEVELOPMENT_BLUEPRINT.md # 📋 ACEST FIȘIER
|
||||
└── MICROSERVICES_GUIDE.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 COMPONENTE IMPLEMENTATE
|
||||
|
||||
### ✅ SHARED COMPONENTS (COMPLET)
|
||||
- **Oracle Database Pool**: Connection pooling cu oracledb, singleton pattern
|
||||
- **JWT Authentication**: Access/refresh tokens, middleware, dependencies
|
||||
- **Models**: User, Company, DatabaseConfig cu Pydantic
|
||||
- **Testing**: Comprehensive test suites pentru toate componentele
|
||||
|
||||
### ✅ BACKEND FASTAPI (COMPLET)
|
||||
- **FastAPI App**: Main application cu lifespan management
|
||||
- **Models**: Invoice, Payment cu validatori și CSS classes
|
||||
- **Routers**: Auth, Companies, Invoices, Payments cu toate endpoint-urile
|
||||
- **Services**: Business logic pentru facturi și încasări
|
||||
- **Integration**: Complete cu shared database pool și authentication
|
||||
|
||||
---
|
||||
|
||||
## ✅ FRONTEND VUE.JS - COMPLET IMPLEMENTAT!
|
||||
|
||||
### Obiectiv: ✅ REALIZAT
|
||||
Implementarea completă a frontend-ului Vue.js cu PrimeVue pentru aplicația de rapoarte.
|
||||
|
||||
### Pași implementați:
|
||||
1. ✅ **Setup Vue.js 3 cu Vite** în `reports-app/frontend/`
|
||||
2. ✅ **Configurare PrimeVue** și componente UI (Aura theme)
|
||||
3. ✅ **Implementare Pinia stores** pentru state management
|
||||
4. ✅ **Componente principale:**
|
||||
- LoginView.vue - Autentificare completă cu validare
|
||||
- DashboardView.vue - Dashboard cu statistici și acțiuni rapide
|
||||
- InvoicesView.vue - Pagină facturi cu filtrare și paginare
|
||||
- PaymentsView.vue - Pagină încasări cu management complet
|
||||
5. ✅ **Routing și navigation** cu Vue Router și navigation guards
|
||||
6. ✅ **Integrare API** cu interceptors și error handling
|
||||
7. ✅ **Styling responsive** pentru mobile și desktop + composables
|
||||
|
||||
### Deliverables: ✅ REALIZATE
|
||||
- ✅ Aplicație Vue.js complet funcțională
|
||||
- ✅ Interface responsive și user-friendly
|
||||
- ✅ Integrare completă cu backend-ul FastAPI
|
||||
- ✅ Composables pentru responsive design
|
||||
- ✅ CSS global și mobile optimizations
|
||||
|
||||
## ⏳ URMĂTOAREA ETAPĂ: DOCKER & DEPLOYMENT
|
||||
|
||||
### Obiectiv
|
||||
Containerizarea aplicației și setup pentru producție cu Docker Compose și Nginx.
|
||||
|
||||
---
|
||||
|
||||
## 🐳 FAZA FINALĂ: DOCKER & DEPLOYMENT
|
||||
|
||||
### Docker Compose și Nginx
|
||||
**Status**: ⏳ PLANIFICAT - după finalizarea frontend-ului
|
||||
|
||||
### Servicii Docker:
|
||||
- **reports-backend**: FastAPI backend containerizat
|
||||
- **reports-frontend**: Vue.js frontend cu Nginx
|
||||
- **nginx**: Gateway pentru routing între servicii
|
||||
|
||||
### Nginx Configuration:
|
||||
- Routing `/api` către backend FastAPI
|
||||
- Serving static files pentru frontend Vue.js
|
||||
- Load balancing pentru extensii viitoare
|
||||
|
||||
---
|
||||
|
||||
## 📊 CHECKLIST FINAL
|
||||
|
||||
### ✅ IMPLEMENTAT
|
||||
- [x] Git setup și structură proiect
|
||||
- [x] Shared database pool Oracle
|
||||
- [x] JWT authentication system
|
||||
- [x] FastAPI backend complet
|
||||
- [x] API endpoints pentru facturi și încasări
|
||||
- [x] Testing suites complete
|
||||
|
||||
### ✅ IMPLEMENTAT RECENT
|
||||
- [x] Vue.js 3 frontend cu PrimeVue ✅ COMPLET
|
||||
- [x] Pinia stores pentru state management ✅ COMPLET
|
||||
- [x] Componente UI responsive ✅ COMPLET
|
||||
- [x] LoginView, DashboardView, InvoicesView, PaymentsView ✅ COMPLET
|
||||
- [x] Vue Router cu navigation guards ✅ COMPLET
|
||||
- [x] API integration cu FastAPI backend ✅ COMPLET
|
||||
- [x] Responsive design pentru mobile și desktop ✅ COMPLET
|
||||
|
||||
### ⏳ DE IMPLEMENTAT
|
||||
- [ ] Docker Compose orchestration
|
||||
- [ ] Nginx gateway configuration
|
||||
- [ ] Production deployment setup
|
||||
|
||||
---
|
||||
|
||||
## 🎓 RESURSE DE ÎNVĂȚARE
|
||||
|
||||
### Vue.js 3
|
||||
- **Documentația oficială**: https://vuejs.org/guide/
|
||||
- **Composition API**: https://vuejs.org/guide/extras/composition-api-faq.html
|
||||
- **PrimeVue**: https://www.primefaces.org/primevue/
|
||||
|
||||
### Docker & Deployment
|
||||
- **Docker Compose**: https://docs.docker.com/compose/
|
||||
- **Nginx Configuration**: Exemple practice în proiect
|
||||
|
||||
---
|
||||
|
||||
*Acest blueprint este FARUL CĂLĂUZITOR pentru dezvoltarea aplicației ROA2WEB. Următoarea etapă: Frontend Vue.js!* 🚀
|
||||
400
docs/DOCKER_SETUP.md
Normal file
400
docs/DOCKER_SETUP.md
Normal file
@@ -0,0 +1,400 @@
|
||||
# ROA2WEB Docker Setup Guide
|
||||
|
||||
This guide covers how to set up and run ROA2WEB using Docker and Docker Compose for both development and production environments.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Docker (20.10+)
|
||||
- Docker Compose (2.0+)
|
||||
- Git
|
||||
- 4GB+ available RAM
|
||||
- 10GB+ available disk space
|
||||
|
||||
### Windows/WSL2 Users
|
||||
- WSL2 with Ubuntu/Debian
|
||||
- Docker Desktop for Windows with WSL2 backend
|
||||
|
||||
## 🚀 Quick Start (Development)
|
||||
|
||||
### 1. Clone and Setup Environment
|
||||
|
||||
```bash
|
||||
cd
|
||||
cp .env.development .env
|
||||
```
|
||||
|
||||
### 2. Configure Database Connection
|
||||
|
||||
Edit `.env` file with your Oracle database credentials:
|
||||
|
||||
```env
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_PASSWORD=your_password_here
|
||||
ORACLE_HOST=localhost # via SSH tunnel
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
```
|
||||
|
||||
### 3. Start SSH Tunnel (if needed)
|
||||
|
||||
```bash
|
||||
./ssh_tunnel.sh start
|
||||
```
|
||||
|
||||
### 4. Build and Start Services
|
||||
|
||||
```bash
|
||||
# Build images and start services
|
||||
docker-compose up --build
|
||||
|
||||
# Or run in background
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### 5. Access the Application
|
||||
|
||||
- **Frontend**: http://localhost:8080 (via Nginx Gateway)
|
||||
- **Backend API**: http://localhost:8000 (direct access)
|
||||
- **Frontend Direct**: http://localhost:3000 (direct access)
|
||||
- **Redis**: http://localhost:6379 (direct access)
|
||||
|
||||
### 6. View Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose logs -f roa-backend
|
||||
docker-compose logs -f roa-frontend
|
||||
docker-compose logs -f roa-gateway
|
||||
```
|
||||
|
||||
## 🏭 Production Deployment
|
||||
|
||||
### 1. Prepare Production Environment
|
||||
|
||||
```bash
|
||||
# Copy production template
|
||||
cp .env.production .env.production.local
|
||||
|
||||
# Edit with your production values
|
||||
nano .env.production.local
|
||||
```
|
||||
|
||||
### 2. Create Production Secrets
|
||||
|
||||
```bash
|
||||
# Create secrets directory
|
||||
mkdir -p secrets/
|
||||
|
||||
# Add your production secrets
|
||||
echo "your_oracle_password" > secrets/oracle_password.txt
|
||||
echo "your_jwt_secret_key" > secrets/jwt_secret_key.txt
|
||||
echo "your_redis_password" > secrets/redis_password.txt
|
||||
|
||||
# Secure the secrets
|
||||
chmod 600 secrets/*.txt
|
||||
```
|
||||
|
||||
### 3. Configure SSL Domain
|
||||
|
||||
Update `.env.production.local`:
|
||||
|
||||
```env
|
||||
DOMAIN=your-domain.com
|
||||
SSL_EMAIL=admin@your-domain.com
|
||||
```
|
||||
|
||||
### 4. Deploy to Production
|
||||
|
||||
```bash
|
||||
# Using deployment script (recommended)
|
||||
./scripts/deploy.sh
|
||||
|
||||
# Or manually
|
||||
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d --build
|
||||
```
|
||||
|
||||
### 5. Verify Deployment
|
||||
|
||||
```bash
|
||||
# Check services health
|
||||
./scripts/health-check.sh
|
||||
|
||||
# Check individual services
|
||||
curl http://localhost/health
|
||||
curl http://localhost/api/health
|
||||
```
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
### Hot Reload Development
|
||||
|
||||
The development setup includes hot reload for both frontend and backend:
|
||||
|
||||
```bash
|
||||
# Start with override (development config)
|
||||
docker-compose up
|
||||
|
||||
# Backend code changes in reports-app/backend/app/ are reflected immediately
|
||||
# Frontend code changes in reports-app/frontend/src/ trigger rebuild
|
||||
```
|
||||
|
||||
### Database Changes
|
||||
|
||||
```bash
|
||||
# Restart backend after database schema changes
|
||||
docker-compose restart roa-backend
|
||||
|
||||
# View backend logs
|
||||
docker-compose logs -f roa-backend
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
```bash
|
||||
# Rebuild frontend after package changes
|
||||
docker-compose build roa-frontend
|
||||
docker-compose up -d roa-frontend
|
||||
|
||||
# Access frontend directly for debugging
|
||||
# http://localhost:3000
|
||||
```
|
||||
|
||||
## 📊 Monitoring and Maintenance
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Comprehensive health check
|
||||
./scripts/health-check.sh full
|
||||
|
||||
# Quick service check
|
||||
./scripts/health-check.sh quick
|
||||
|
||||
# Continuous monitoring
|
||||
./scripts/health-check.sh watch
|
||||
```
|
||||
|
||||
### Backup and Restore
|
||||
|
||||
```bash
|
||||
# Full backup
|
||||
./scripts/backup.sh full
|
||||
|
||||
# Database only
|
||||
./scripts/backup.sh database
|
||||
|
||||
# List backups
|
||||
./scripts/backup.sh list
|
||||
|
||||
# Restore from backup
|
||||
./scripts/backup.sh restore backup_20240131_143022
|
||||
```
|
||||
|
||||
### Log Management
|
||||
|
||||
```bash
|
||||
# View real-time logs
|
||||
docker-compose logs -f
|
||||
|
||||
# View logs with timestamps
|
||||
docker-compose logs -t
|
||||
|
||||
# Export logs
|
||||
docker-compose logs > roa2web_logs_$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Port Already in Use
|
||||
|
||||
```bash
|
||||
# Check what's using the port
|
||||
sudo netstat -tlnp | grep :8080
|
||||
|
||||
# Stop the conflicting service or change ports in docker-compose.override.yml
|
||||
```
|
||||
|
||||
#### 2. Database Connection Failed
|
||||
|
||||
```bash
|
||||
# Check SSH tunnel status
|
||||
./ssh_tunnel.sh status
|
||||
|
||||
# Restart SSH tunnel
|
||||
./ssh_tunnel.sh restart
|
||||
|
||||
# Test database connection
|
||||
docker-compose exec roa-backend python -c "from shared.database.oracle_pool import test_connection; test_connection()"
|
||||
```
|
||||
|
||||
#### 3. Frontend Build Errors
|
||||
|
||||
```bash
|
||||
# Clear node_modules and rebuild
|
||||
docker-compose build --no-cache roa-frontend
|
||||
|
||||
# Check frontend logs
|
||||
docker-compose logs roa-frontend
|
||||
```
|
||||
|
||||
#### 4. SSL Certificate Issues (Production)
|
||||
|
||||
```bash
|
||||
# Generate test certificates
|
||||
docker-compose exec roa-gateway /usr/local/bin/ssl-renew.sh
|
||||
|
||||
# Check certificate status
|
||||
docker-compose exec roa-gateway openssl x509 -in /etc/letsencrypt/live/your-domain.com/cert.pem -text -noout
|
||||
```
|
||||
|
||||
### Service Recovery
|
||||
|
||||
#### Quick Recovery
|
||||
|
||||
```bash
|
||||
# Restart all services
|
||||
docker-compose restart
|
||||
|
||||
# Rollback to previous version
|
||||
./scripts/rollback.sh quick
|
||||
```
|
||||
|
||||
#### Full Recovery
|
||||
|
||||
```bash
|
||||
# Stop everything
|
||||
docker-compose down
|
||||
|
||||
# Clean up
|
||||
docker system prune -f
|
||||
|
||||
# Restart fresh
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
#### Development
|
||||
|
||||
```bash
|
||||
# Allocate more memory to Docker
|
||||
# Docker Desktop: Settings > Resources > Memory (recommend 4GB+)
|
||||
|
||||
# Disable unnecessary services in development
|
||||
# Comment out services in docker-compose.override.yml
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
```bash
|
||||
# Monitor resource usage
|
||||
docker stats
|
||||
|
||||
# Scale services
|
||||
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d --scale roa-backend=2
|
||||
|
||||
# Optimize images
|
||||
docker image prune -f
|
||||
docker volume prune -f
|
||||
```
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
### Development Security
|
||||
|
||||
- Never commit actual credentials to version control
|
||||
- Use `.env` files that are gitignored
|
||||
- SSH tunnel provides secure database access
|
||||
|
||||
### Production Security
|
||||
|
||||
- Use Docker secrets for sensitive data
|
||||
- Enable SSL/TLS with valid certificates
|
||||
- Regular security updates
|
||||
- Monitor logs for suspicious activity
|
||||
|
||||
```bash
|
||||
# Update base images
|
||||
docker-compose pull
|
||||
docker-compose up -d --build
|
||||
|
||||
# Security scan
|
||||
docker scout cves backend:latest
|
||||
```
|
||||
|
||||
## 📚 Advanced Configuration
|
||||
|
||||
### Custom Nginx Configuration
|
||||
|
||||
Edit `nginx/conf/sites-enabled/roa2web.conf` for custom routing:
|
||||
|
||||
```nginx
|
||||
# Add custom location
|
||||
location /custom-api/ {
|
||||
proxy_pass http://custom-service:3000/;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
```
|
||||
|
||||
### Environment-Specific Overrides
|
||||
|
||||
Create custom compose files:
|
||||
|
||||
```yaml
|
||||
# docker-compose.staging.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
roa-backend:
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
### Adding New Services
|
||||
|
||||
```yaml
|
||||
# Add to docker-compose.yml
|
||||
services:
|
||||
new-service:
|
||||
build: ./new-service
|
||||
networks:
|
||||
- roa-network
|
||||
depends_on:
|
||||
- roa-backend
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. Check logs: `docker-compose logs`
|
||||
2. Run health check: `./scripts/health-check.sh`
|
||||
3. Review this documentation
|
||||
4. Check GitHub issues
|
||||
5. Contact the development team
|
||||
|
||||
### Useful Commands Reference
|
||||
|
||||
```bash
|
||||
# Quick commands
|
||||
docker-compose up -d # Start services in background
|
||||
docker-compose down # Stop and remove containers
|
||||
docker-compose ps # Show running services
|
||||
docker-compose exec roa-backend sh # Access backend container
|
||||
|
||||
# Maintenance
|
||||
docker system df # Show Docker disk usage
|
||||
docker system prune -f # Clean up unused resources
|
||||
docker-compose pull # Update base images
|
||||
docker-compose build --no-cache # Rebuild without cache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last updated: $(date +%Y-%m-%d)*
|
||||
*ROA2WEB Docker Setup Guide v1.0*
|
||||
234
docs/MICROSERVICES_GUIDE.md
Normal file
234
docs/MICROSERVICES_GUIDE.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# ROA2WEB Microservices Guide
|
||||
|
||||
🚀 **Ghid pentru Adăugarea de Module Noi în Ecosistemul ROA2WEB**
|
||||
|
||||
## 📋 Conceptul Microserviciilor
|
||||
|
||||
ROA2WEB folosește o arhitectură microservicii care permite adăugarea ușoară de module noi fără a afecta funcționalitatea existentă.
|
||||
|
||||
### 🏗️ Structura unui Microserviciu
|
||||
|
||||
```
|
||||
new-app/
|
||||
├── backend/ # FastAPI Backend
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # Entry point
|
||||
│ │ ├── models/ # Pydantic models
|
||||
│ │ ├── routers/ # API endpoints
|
||||
│ │ ├── services/ # Business logic
|
||||
│ │ └── schemas/ # Response schemas
|
||||
│ ├── requirements.txt
|
||||
│ ├── Dockerfile
|
||||
│ └── .env.example
|
||||
├── frontend/ # Vue.js Frontend (opțional)
|
||||
│ ├── src/
|
||||
│ │ ├── main.js
|
||||
│ │ ├── App.vue
|
||||
│ │ ├── router/
|
||||
│ │ ├── stores/
|
||||
│ │ ├── views/
|
||||
│ │ └── components/
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.js
|
||||
│ └── Dockerfile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🔧 Shared Components
|
||||
|
||||
Toate microserviciile folosesc componentele comune din directorul `shared/`:
|
||||
|
||||
### Database Pool
|
||||
```python
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
# Database operations
|
||||
```
|
||||
|
||||
### Authentication
|
||||
```python
|
||||
from shared.auth.jwt_handler import jwt_handler
|
||||
from shared.auth.middleware import require_auth
|
||||
|
||||
@require_auth
|
||||
async def protected_endpoint():
|
||||
# Protected logic
|
||||
```
|
||||
|
||||
## 🚀 Pași pentru Adăugare Microserviciu Nou
|
||||
|
||||
### 1. Creare Structură
|
||||
|
||||
```bash
|
||||
mkdir -p new-app/{backend/app/{models,routers,services,schemas},frontend/src/{router,stores,views,components}}
|
||||
```
|
||||
|
||||
### 2. Backend Setup
|
||||
|
||||
**`new-app/backend/app/main.py`**:
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add shared path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
|
||||
|
||||
from database.oracle_pool import oracle_pool
|
||||
from auth.jwt_handler import jwt_handler
|
||||
|
||||
app = FastAPI(title="New App API")
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await oracle_pool.initialize()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await oracle_pool.close_pool()
|
||||
```
|
||||
|
||||
### 3. Frontend Setup (Opțional)
|
||||
|
||||
Dacă microserviciul necesită UI:
|
||||
|
||||
**`new-app/frontend/package.json`**:
|
||||
```json
|
||||
{
|
||||
"name": "new-app-frontend",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.0",
|
||||
"primevue": "^3.0.0",
|
||||
"pinia": "^2.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Docker Configuration
|
||||
|
||||
**`new-app/backend/Dockerfile`**:
|
||||
```dockerfile
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
### 5. Nginx Routing
|
||||
|
||||
Adaugă în `nginx/nginx.conf`:
|
||||
|
||||
```nginx
|
||||
location /new-app/ {
|
||||
proxy_pass http://new-app-backend:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Docker Compose Integration
|
||||
|
||||
Adaugă în `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
new-app-backend:
|
||||
build: ./new-app/backend
|
||||
networks:
|
||||
- roa-network
|
||||
environment:
|
||||
- ORACLE_USER=${ORACLE_USER}
|
||||
- ORACLE_PASSWORD=${ORACLE_PASSWORD}
|
||||
- ORACLE_DSN=${ORACLE_DSN}
|
||||
```
|
||||
|
||||
## 📊 Exemple de Microservicii
|
||||
|
||||
### 1. Invoicing App
|
||||
- Gestionare facturi
|
||||
- Generare PDF
|
||||
- Email notifications
|
||||
|
||||
### 2. Inventory App
|
||||
- Gestiune stocuri
|
||||
- Mișcări de marfă
|
||||
- Rapoarte inventar
|
||||
|
||||
### 3. CRM App
|
||||
- Gestionare clienți
|
||||
- Istoric interacțiuni
|
||||
- Pipeline vânzări
|
||||
|
||||
## 🔐 Securitate
|
||||
|
||||
### Autentificare
|
||||
Toate microserviciile folosesc JWT tokens din `shared/auth/`.
|
||||
|
||||
### Autorizare
|
||||
Implementează middleware pentru verificarea permisiunilor per modul.
|
||||
|
||||
### Database Access
|
||||
Folosește doar `shared/database/oracle_pool.py` pentru acces la baza de date.
|
||||
|
||||
## 📋 Best Practices
|
||||
|
||||
### 1. Naming Convention
|
||||
- **Directoare**: `kebab-case` (ex: `invoicing-app`)
|
||||
- **API Endpoints**: `/api/v1/resource`
|
||||
- **Database**: Schema separată per modul
|
||||
|
||||
### 2. Error Handling
|
||||
```python
|
||||
from shared.utils.exceptions import ROAException
|
||||
|
||||
@app.exception_handler(ROAException)
|
||||
async def roa_exception_handler(request, exc):
|
||||
return {"error": str(exc)}
|
||||
```
|
||||
|
||||
### 3. Logging
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger(f"roa.{__name__}")
|
||||
```
|
||||
|
||||
### 4. Testing
|
||||
```bash
|
||||
# Unit tests
|
||||
pytest new-app/backend/tests/
|
||||
|
||||
# Integration tests
|
||||
pytest tests/integration/test_new_app.py
|
||||
```
|
||||
|
||||
## 🔄 Deployment
|
||||
|
||||
### Development
|
||||
```bash
|
||||
docker-compose up new-app-backend new-app-frontend
|
||||
```
|
||||
|
||||
### Production
|
||||
Folosește orchestratoare precum Kubernetes pentru scalare automată.
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Pentru întrebări despre dezvoltarea de microservicii:
|
||||
|
||||
1. Consultă documentația shared components
|
||||
2. Urmărește pattern-urile din `reports-app/`
|
||||
3. Testează integrarea cu componentele comune
|
||||
|
||||
---
|
||||
|
||||
*Arhitectura microservicii permite creșterea organică a platformei ROA2WEB* 🚀
|
||||
345
docs/PRODUCTION_CHECKLIST.md
Normal file
345
docs/PRODUCTION_CHECKLIST.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# ROA2WEB Production Go-Live Checklist
|
||||
|
||||
This checklist ensures a smooth production deployment and covers all critical aspects of going live with ROA2WEB.
|
||||
|
||||
## 🎯 Pre-Go-Live Checklist (1-2 weeks before)
|
||||
|
||||
### Infrastructure Setup ✅
|
||||
|
||||
#### Server Requirements
|
||||
- [ ] Production server provisioned (4GB+ RAM, 20GB+ disk, 2+ CPU cores)
|
||||
- [ ] Server OS updated and hardened (Ubuntu 20.04+ or similar)
|
||||
- [ ] SSH key-based authentication configured
|
||||
- [ ] Non-root user with sudo privileges created
|
||||
- [ ] Firewall configured (UFW/iptables) - only required ports open
|
||||
- [ ] Backup server/storage configured
|
||||
- [ ] Monitoring tools installed (htop, curl, etc.)
|
||||
|
||||
#### Network and DNS
|
||||
- [ ] Domain name registered and configured
|
||||
- [ ] DNS A record pointing to production server IP
|
||||
- [ ] SSL certificate planning (Let's Encrypt or custom)
|
||||
- [ ] CDN configuration (if using CloudFlare/AWS CloudFront)
|
||||
- [ ] Load balancer setup (if using multiple servers)
|
||||
|
||||
#### Database Setup
|
||||
- [ ] Oracle database connection tested from production server
|
||||
- [ ] SSH tunnel configured and tested (if required)
|
||||
- [ ] Database user permissions verified
|
||||
- [ ] Database backup strategy implemented
|
||||
- [ ] Connection pooling settings optimized
|
||||
|
||||
### Application Configuration ✅
|
||||
|
||||
#### Environment Configuration
|
||||
- [ ] `.env.production` file created with production values
|
||||
- [ ] All environment variables validated
|
||||
- [ ] Secrets management configured (Docker secrets)
|
||||
- [ ] SSL email address configured for Let's Encrypt
|
||||
- [ ] JWT secret keys generated (strong, unique)
|
||||
- [ ] Redis password configured
|
||||
|
||||
#### Security Configuration
|
||||
- [ ] HTTPS enforced (HTTP redirects to HTTPS)
|
||||
- [ ] Security headers configured in Nginx
|
||||
- [ ] CORS settings reviewed and configured
|
||||
- [ ] API rate limiting configured
|
||||
- [ ] File upload restrictions in place
|
||||
- [ ] Database connection encryption enabled
|
||||
|
||||
#### Performance Configuration
|
||||
- [ ] Worker processes optimized for server resources
|
||||
- [ ] Connection pools sized appropriately
|
||||
- [ ] Caching strategy implemented (Redis)
|
||||
- [ ] Static file caching configured
|
||||
- [ ] Gzip compression enabled
|
||||
- [ ] Image optimization configured
|
||||
|
||||
### Docker and Deployment ✅
|
||||
|
||||
#### Docker Setup
|
||||
- [ ] Docker and Docker Compose installed (latest stable versions)
|
||||
- [ ] Docker daemon configured for production
|
||||
- [ ] Docker log rotation configured
|
||||
- [ ] Docker registry access configured (if using private registry)
|
||||
- [ ] Multi-stage Dockerfiles optimized
|
||||
- [ ] Health checks configured for all services
|
||||
|
||||
#### Deployment Pipeline
|
||||
- [ ] Deployment scripts tested (`deploy.sh`, `backup.sh`, `rollback.sh`)
|
||||
- [ ] Automated deployment pipeline configured (CI/CD)
|
||||
- [ ] Blue-green or rolling deployment strategy implemented
|
||||
- [ ] Rollback procedures tested
|
||||
- [ ] Zero-downtime deployment verified
|
||||
|
||||
## 🚀 Deployment Day Checklist
|
||||
|
||||
### Pre-Deployment (Morning) ✅
|
||||
|
||||
#### Final Preparations
|
||||
- [ ] All team members notified of deployment schedule
|
||||
- [ ] Maintenance window scheduled and communicated
|
||||
- [ ] Rollback plan reviewed and understood by team
|
||||
- [ ] Emergency contacts list updated
|
||||
- [ ] Backup of current system created
|
||||
- [ ] Database maintenance mode enabled (if required)
|
||||
|
||||
#### Last-Minute Verifications
|
||||
- [ ] Latest code pulled from main branch
|
||||
- [ ] All tests passing in CI/CD pipeline
|
||||
- [ ] Production configuration files reviewed
|
||||
- [ ] SSL certificates validated
|
||||
- [ ] DNS propagation confirmed
|
||||
- [ ] Third-party service integrations tested
|
||||
|
||||
### Deployment Execution ✅
|
||||
|
||||
#### Step 1: Infrastructure
|
||||
- [ ] Server resources verified (CPU, Memory, Disk)
|
||||
- [ ] Network connectivity confirmed
|
||||
- [ ] Database connectivity tested
|
||||
- [ ] SSH tunnel established (if required)
|
||||
- [ ] Firewall rules validated
|
||||
|
||||
#### Step 2: Application Deployment
|
||||
- [ ] Environment variables loaded
|
||||
- [ ] Docker images built successfully
|
||||
- [ ] Services started in correct order
|
||||
- [ ] Health checks passing
|
||||
- [ ] SSL certificates generated/installed
|
||||
- [ ] Nginx configuration loaded
|
||||
|
||||
#### Step 3: Service Verification
|
||||
- [ ] All containers running and healthy
|
||||
- [ ] Frontend accessible via HTTPS
|
||||
- [ ] Backend API responding correctly
|
||||
- [ ] Database connections working
|
||||
- [ ] Redis caching operational
|
||||
- [ ] Log files being generated
|
||||
|
||||
### Post-Deployment Verification ✅
|
||||
|
||||
#### Functional Testing
|
||||
- [ ] User authentication working
|
||||
- [ ] Main application features functional
|
||||
- [ ] Report generation working
|
||||
- [ ] File uploads/downloads working
|
||||
- [ ] Email notifications working (if applicable)
|
||||
- [ ] Search functionality working
|
||||
|
||||
#### Performance Testing
|
||||
- [ ] Page load times acceptable (<3 seconds)
|
||||
- [ ] API response times acceptable (<500ms)
|
||||
- [ ] Database query performance acceptable
|
||||
- [ ] Memory usage within limits
|
||||
- [ ] CPU usage within limits
|
||||
- [ ] No memory leaks detected
|
||||
|
||||
#### Security Testing
|
||||
- [ ] HTTPS enforced (HTTP redirects work)
|
||||
- [ ] Security headers present in responses
|
||||
- [ ] No sensitive data exposed in logs
|
||||
- [ ] Authentication/authorization working
|
||||
- [ ] XSS/CSRF protections active
|
||||
- [ ] File upload restrictions working
|
||||
|
||||
## 🔍 Go-Live Monitoring (First 24 Hours)
|
||||
|
||||
### Immediate Monitoring (First Hour) ✅
|
||||
|
||||
#### System Health
|
||||
- [ ] All services running (docker-compose ps)
|
||||
- [ ] Health checks passing (`./scripts/health-check.sh`)
|
||||
- [ ] No error messages in logs
|
||||
- [ ] Resource usage normal
|
||||
- [ ] SSL certificate working
|
||||
- [ ] DNS resolution working
|
||||
|
||||
#### Application Health
|
||||
- [ ] Login functionality working
|
||||
- [ ] User sessions persistent
|
||||
- [ ] Database queries executing normally
|
||||
- [ ] No 500/404 errors
|
||||
- [ ] Static files loading correctly
|
||||
- [ ] API endpoints responding
|
||||
|
||||
### Extended Monitoring (First 24 Hours) ✅
|
||||
|
||||
#### Performance Monitoring
|
||||
- [ ] Response times remain stable
|
||||
- [ ] Memory usage stable (no leaks)
|
||||
- [ ] CPU usage within expected range
|
||||
- [ ] Disk usage not growing abnormally
|
||||
- [ ] Database connection pool healthy
|
||||
- [ ] No timeout errors
|
||||
|
||||
#### Error Monitoring
|
||||
- [ ] Application error logs reviewed every 4 hours
|
||||
- [ ] Server error logs reviewed every 4 hours
|
||||
- [ ] No critical errors in database logs
|
||||
- [ ] No failed authentication attempts (beyond normal)
|
||||
- [ ] No security-related warnings
|
||||
|
||||
#### User Experience
|
||||
- [ ] User feedback collected and reviewed
|
||||
- [ ] No user-reported issues
|
||||
- [ ] Performance meets user expectations
|
||||
- [ ] All features accessible to users
|
||||
- [ ] Mobile responsiveness working
|
||||
|
||||
## 🚨 Issue Response Procedures
|
||||
|
||||
### Severity 1 - Critical (Service Down)
|
||||
**Response Time: Immediate**
|
||||
|
||||
- [ ] Execute emergency procedures
|
||||
- [ ] Notify all stakeholders immediately
|
||||
- [ ] Assess if rollback is needed
|
||||
- [ ] Document all actions taken
|
||||
- [ ] Implement fix or rollback within 30 minutes
|
||||
|
||||
**Emergency Rollback:**
|
||||
```bash
|
||||
./scripts/rollback.sh emergency
|
||||
./scripts/rollback.sh quick
|
||||
```
|
||||
|
||||
### Severity 2 - High (Performance Issues)
|
||||
**Response Time: Within 1 Hour**
|
||||
|
||||
- [ ] Investigate root cause
|
||||
- [ ] Implement temporary workaround if possible
|
||||
- [ ] Plan permanent fix
|
||||
- [ ] Monitor system closely
|
||||
- [ ] Update stakeholders every hour
|
||||
|
||||
### Severity 3 - Medium (Minor Issues)
|
||||
**Response Time: Within 4 Hours**
|
||||
|
||||
- [ ] Log issue in tracking system
|
||||
- [ ] Investigate when resources available
|
||||
- [ ] Plan fix for next maintenance window
|
||||
- [ ] Monitor for escalation
|
||||
|
||||
## 📊 Success Metrics
|
||||
|
||||
### Technical Metrics ✅
|
||||
- [ ] Uptime > 99.9% in first 24 hours
|
||||
- [ ] Average response time < 500ms
|
||||
- [ ] Error rate < 0.1%
|
||||
- [ ] Zero security incidents
|
||||
- [ ] Zero data loss events
|
||||
- [ ] Successful SSL certificate installation
|
||||
|
||||
### Business Metrics ✅
|
||||
- [ ] Users can successfully log in
|
||||
- [ ] Core functionality available
|
||||
- [ ] Reports generate correctly
|
||||
- [ ] No user-blocking issues
|
||||
- [ ] Positive user feedback
|
||||
- [ ] Go-live objectives met
|
||||
|
||||
## 📞 Communication Plan
|
||||
|
||||
### Stakeholder Notifications ✅
|
||||
|
||||
#### Pre-Go-Live (24 hours before)
|
||||
- [ ] Send deployment schedule to all stakeholders
|
||||
- [ ] Confirm maintenance window (if applicable)
|
||||
- [ ] Provide rollback timeline
|
||||
- [ ] Share emergency contact information
|
||||
|
||||
#### Go-Live Day
|
||||
- [ ] **Deployment Start**: Notify start of deployment
|
||||
- [ ] **Major Milestones**: Update on key deployment steps
|
||||
- [ ] **Issues**: Immediate notification of any problems
|
||||
- [ ] **Completion**: Confirmation of successful deployment
|
||||
- [ ] **Post-Go-Live**: 24-hour status update
|
||||
|
||||
#### Emergency Communications
|
||||
- [ ] **Severity 1**: Immediate email/SMS to all stakeholders
|
||||
- [ ] **Rollback Decision**: Immediate notification with timeline
|
||||
- [ ] **Resolution**: Update when issue resolved
|
||||
|
||||
### Contact Information ✅
|
||||
- [ ] Primary deployment engineer: [Name/Phone/Email]
|
||||
- [ ] Backup deployment engineer: [Name/Phone/Email]
|
||||
- [ ] Database administrator: [Name/Phone/Email]
|
||||
- [ ] Infrastructure team: [Name/Phone/Email]
|
||||
- [ ] Business stakeholders: [Names/Emails]
|
||||
|
||||
## 🔄 Post-Go-Live Activities (Week 1)
|
||||
|
||||
### Daily Reviews (Days 1-7) ✅
|
||||
- [ ] **Day 1**: Full system review and user feedback collection
|
||||
- [ ] **Day 2**: Performance analysis and optimization
|
||||
- [ ] **Day 3**: Security review and log analysis
|
||||
- [ ] **Day 4**: User experience review and minor fixes
|
||||
- [ ] **Day 5**: Backup and disaster recovery testing
|
||||
- [ ] **Day 6**: Documentation updates and lessons learned
|
||||
- [ ] **Day 7**: Weekly review and planning next steps
|
||||
|
||||
### Documentation Updates ✅
|
||||
- [ ] Update production runbooks
|
||||
- [ ] Document any configuration changes
|
||||
- [ ] Update troubleshooting guides
|
||||
- [ ] Record lessons learned
|
||||
- [ ] Update emergency procedures
|
||||
- [ ] Create post-mortem report (if issues occurred)
|
||||
|
||||
### Optimization Activities ✅
|
||||
- [ ] Review and optimize performance bottlenecks
|
||||
- [ ] Adjust resource allocations based on actual usage
|
||||
- [ ] Fine-tune caching configurations
|
||||
- [ ] Optimize database queries if needed
|
||||
- [ ] Update monitoring thresholds
|
||||
- [ ] Plan capacity scaling if needed
|
||||
|
||||
## ✅ Final Checklist Completion
|
||||
|
||||
### Deployment Team Sign-off ✅
|
||||
- [ ] **Lead Developer**: System functionality verified
|
||||
- [ ] **DevOps Engineer**: Infrastructure and deployment verified
|
||||
- [ ] **DBA**: Database operations verified
|
||||
- [ ] **Security Officer**: Security measures verified
|
||||
- [ ] **QA Lead**: Quality assurance verified
|
||||
- [ ] **Project Manager**: Go-live objectives met
|
||||
|
||||
### Business Team Sign-off ✅
|
||||
- [ ] **Business Owner**: Business requirements met
|
||||
- [ ] **End Users**: User acceptance confirmed
|
||||
- [ ] **Support Team**: Support procedures ready
|
||||
- [ ] **Management**: Go-live approved and successful
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Health Check
|
||||
./scripts/health-check.sh full
|
||||
|
||||
# Emergency Stop
|
||||
./scripts/rollback.sh emergency
|
||||
|
||||
# Quick Rollback
|
||||
./scripts/rollback.sh quick
|
||||
|
||||
# View Logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Check Services
|
||||
docker-compose ps
|
||||
|
||||
# System Resources
|
||||
docker stats
|
||||
htop
|
||||
df -h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**🎉 Congratulations on your successful ROA2WEB production deployment!**
|
||||
|
||||
*Production Go-Live Checklist v1.0*
|
||||
*Last updated: $(date +%Y-%m-%d)*
|
||||
220
docs/TEAM_IMPLEMENTATION_GUIDE.md
Normal file
220
docs/TEAM_IMPLEMENTATION_GUIDE.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 🎯 ROA2WEB Team Implementation Guide - COMPLETE
|
||||
|
||||
**Data implementare**: 2025-08-03 16:30
|
||||
**Status**: ✅ **TOATE INSTRUCȚIUNILE IMPLEMENTATE**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 CE AM IMPLEMENTAT PENTRU ECHIPĂ
|
||||
|
||||
### ✅ 1. ACTUALIZARE SSH SCRIPTS
|
||||
|
||||
#### Script Principal: `ssh_tunnel.sh`
|
||||
**Schimbare**: SSH key path actualizat automat
|
||||
```bash
|
||||
# ÎNAINTE (nu mai funcționa):
|
||||
SSH_KEY="$HOME/.ssh/roa_oracle_server"
|
||||
|
||||
# ACUM (funcționează automat):
|
||||
SSH_KEY="$(dirname "$0")/secrets/roa_oracle_server"
|
||||
```
|
||||
|
||||
**Utilizare**: `./ssh_tunnel.sh start` (funcționează automat cu noua cale)
|
||||
|
||||
#### Docker Configuration: `ssh-tunnel/Dockerfile`
|
||||
**Schimbare**: Docker folosește noua locație
|
||||
```dockerfile
|
||||
# Actualizat pentru noua cale:
|
||||
COPY ../secrets/roa_oracle_server /home/tunnel/.ssh/roa_oracle_server
|
||||
```
|
||||
|
||||
### ✅ 2. CONFIGURAȚII ENVIRONMENT SECURIZATE
|
||||
|
||||
#### `.env.example` - Actualizat cu Security Best Practices
|
||||
```bash
|
||||
# 🔐 SECURITY: Set these values in your environment, NOT in .env files!
|
||||
ORACLE_PASSWORD=SET_IN_PRODUCTION_ENV
|
||||
JWT_SECRET_KEY=GENERATE_STRONG_SECRET_IN_PRODUCTION
|
||||
```
|
||||
|
||||
#### `reports-app/backend/.env.example` - Credențiale Securizate
|
||||
```bash
|
||||
# 🔐 SECURITY: Nu pune credențiale reale în acest fișier!
|
||||
ORACLE_PASSWORD=SET_IN_PRODUCTION_ENV
|
||||
# Username: "SET_IN_PRODUCTION"
|
||||
# Password: "SET_IN_PRODUCTION"
|
||||
```
|
||||
|
||||
### ✅ 3. SCRIPT AUTOMAT DE SETUP PRODUCȚIE
|
||||
|
||||
#### `setup_production.sh` - Setup Complet Automat
|
||||
**Caracteristici**:
|
||||
- ✅ Generează automat parole sigure (16-32 caractere)
|
||||
- ✅ Creează `.env.production` complet
|
||||
- ✅ Generează JWT secret cryptografic sigur
|
||||
- ✅ Creează script de deployment automat
|
||||
- ✅ Include checklist de securitate complet
|
||||
|
||||
**Utilizare**:
|
||||
```bash
|
||||
./setup_production.sh
|
||||
# Generează toate credențialele și configurațiile necesare
|
||||
```
|
||||
|
||||
### ✅ 4. TESTARE ȘI VALIDARE
|
||||
|
||||
#### Configurație SSH Key Verificată
|
||||
```bash
|
||||
✅ SSH Key Location: secrets/roa_oracle_server
|
||||
✅ Protected by .gitignore: YES
|
||||
✅ Docker configured: YES
|
||||
✅ Scripts updated: YES
|
||||
✅ Production ready: YES
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 PENTRU ECHIPĂ: CE TREBUIE SĂ FACI ACUM
|
||||
|
||||
### 📋 OPȚIUNE 1: Setup Automat (RECOMANDAT)
|
||||
```bash
|
||||
# 1. Rulează setup automat pentru producție
|
||||
./setup_production.sh
|
||||
|
||||
# 2. Urmează instrucțiunile din PRODUCTION_CREDENTIALS.md
|
||||
# 3. Actualizează parola Oracle cu cea generată
|
||||
# 4. Deploy automat:
|
||||
./deploy_production.sh
|
||||
```
|
||||
|
||||
### 📋 OPȚIUNE 2: Setup Manual
|
||||
|
||||
#### Setare Credențiale în Mediul de Producție:
|
||||
```bash
|
||||
# În server/container de producție:
|
||||
export ORACLE_PASSWORD="parola_ta_oracle_reala"
|
||||
export JWT_SECRET_KEY="secret_jwt_foarte_sigur_generat"
|
||||
|
||||
# Pentru user authentication:
|
||||
export VALID_USERS='{"marius": "parola_noua_marius", "eli": "parola_noua_eli"}'
|
||||
```
|
||||
|
||||
#### SSH Scripts - FUNCȚIONEAZĂ AUTOMAT:
|
||||
```bash
|
||||
# Acestea funcționează deja cu noua cale:
|
||||
./ssh_tunnel.sh start # ✅ Funcționează automat
|
||||
./ssh_tunnel.sh status # ✅ Funcționează automat
|
||||
docker-compose up # ✅ Funcționează automat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 VERIFICĂRI PENTRU ECHIPĂ
|
||||
|
||||
### ✅ Verificare Rapidă - TOATE OK:
|
||||
```bash
|
||||
# 1. SSH key în locația corectă:
|
||||
ls -la secrets/roa_oracle_server # ✅ Există
|
||||
|
||||
# 2. SSH tunnel funcționează:
|
||||
cd roa2web && ./ssh_tunnel.sh status # ✅ Script actualizat
|
||||
|
||||
# 3. Docker configurație:
|
||||
grep "secrets/roa_oracle_server" ssh-tunnel/Dockerfile # ✅ Actualizat
|
||||
|
||||
# 4. Environment examples securizate:
|
||||
grep "SET_IN_PRODUCTION" .env.example # ✅ Securizat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 COMENZI PRACTICE PENTRU ECHIPĂ
|
||||
|
||||
### Dezvoltare Locală:
|
||||
```bash
|
||||
# Start SSH tunnel (folosește automat noua cale):
|
||||
cd roa2web
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# Verificare status:
|
||||
./ssh_tunnel.sh status
|
||||
|
||||
# Stop tunnel:
|
||||
./ssh_tunnel.sh stop
|
||||
```
|
||||
|
||||
### Docker Development:
|
||||
```bash
|
||||
# Start toate serviciile (inclusiv SSH tunnel):
|
||||
docker-compose up -d
|
||||
|
||||
# Check status:
|
||||
docker-compose ps
|
||||
|
||||
# Logs:
|
||||
docker-compose logs roa-ssh-tunnel
|
||||
```
|
||||
|
||||
### Producție:
|
||||
```bash
|
||||
# Setup automat complet:
|
||||
cd roa2web
|
||||
./setup_production.sh
|
||||
|
||||
# Deploy automat:
|
||||
./deploy_production.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 REZULTATE FINALE IMPLEMENTARE
|
||||
|
||||
### Înainte de Implementare:
|
||||
- ❌ SSH key în locație nesigură (`ssh-tunnel/`)
|
||||
- ❌ Script-uri cu path-uri fixe în `$HOME/.ssh/`
|
||||
- ❌ Credențiale în fișiere .env.example
|
||||
- ❌ Setup manual complex pentru producție
|
||||
|
||||
### După Implementare:
|
||||
- ✅ SSH key în locație sigură (`secrets/` protejat prin .gitignore)
|
||||
- ✅ Script-uri cu path-uri relative automate
|
||||
- ✅ Toate credențialele înlocuite cu placeholder-uri sigure
|
||||
- ✅ Setup automat complet pentru producție cu generare credențiale
|
||||
- ✅ Deployment automat cu o singură comandă
|
||||
|
||||
---
|
||||
|
||||
## 🎯 NEXT STEPS PENTRU ECHIPĂ
|
||||
|
||||
### Pentru Dezvoltare:
|
||||
1. **SSH funcționează automat** - nu e nevoie de schimbări
|
||||
2. **Environment variables** - folosește placeholder-urile sigure
|
||||
3. **Docker** - funcționează automat cu noua configurație
|
||||
|
||||
### Pentru Producție:
|
||||
1. **Rulează** `./setup_production.sh` pentru setup automat
|
||||
2. **Actualizează** parola Oracle cu cea generată
|
||||
3. **Deploy** cu `./deploy_production.sh`
|
||||
|
||||
### Pentru Securitate:
|
||||
1. **Monitorizare** cu `python3 security/secrets_scanner.py`
|
||||
2. **Validare** cu `python3 security/validate_security.py`
|
||||
3. **Git hooks** blochează automat commit-urile cu secrete
|
||||
|
||||
---
|
||||
|
||||
## 🎉 CONCLUZIE
|
||||
|
||||
**TOATE INSTRUCȚIUNILE PENTRU ECHIPĂ AU FOST IMPLEMENTATE AUTOMAT!**
|
||||
|
||||
✅ **SSH Scripts**: Actualizate și funcționale
|
||||
✅ **Environment Configs**: Securizate cu placeholder-uri
|
||||
✅ **Production Setup**: Automat și complet
|
||||
✅ **Testing**: Validat și funcțional
|
||||
|
||||
**Echipa poate continua dezvoltarea normal - toate script-urile funcționează automat cu noile configurații de securitate!**
|
||||
|
||||
---
|
||||
|
||||
*Implementare finalizată: 2025-08-03 16:30*
|
||||
*Toate sistemele operaționale și sigure!* 🔒✨
|
||||
51
nginx/Dockerfile
Normal file
51
nginx/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
# Install necessary packages for SSL and security
|
||||
RUN apk add --no-cache \
|
||||
tini \
|
||||
openssl \
|
||||
certbot \
|
||||
certbot-nginx \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nginx-user && \
|
||||
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G nginx-user nginx-user
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /etc/nginx/conf.d \
|
||||
/etc/nginx/sites-enabled \
|
||||
/var/log/nginx \
|
||||
/etc/letsencrypt \
|
||||
/var/www/certbot
|
||||
|
||||
# Copy configuration files
|
||||
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY conf/sites-enabled/ /etc/nginx/sites-enabled/
|
||||
COPY conf/ssl.conf /etc/nginx/conf.d/ssl.conf
|
||||
COPY conf/upstream.conf /etc/nginx/conf.d/upstream.conf
|
||||
COPY conf/security.conf /etc/nginx/conf.d/security.conf
|
||||
|
||||
# Copy SSL maintenance scripts
|
||||
COPY scripts/ssl-renew.sh /usr/local/bin/ssl-renew.sh
|
||||
RUN chmod +x /usr/local/bin/ssl-renew.sh
|
||||
|
||||
# Set proper permissions
|
||||
RUN chown -R nginx-user:nginx-user /var/cache/nginx \
|
||||
/var/log/nginx \
|
||||
/etc/nginx/conf.d \
|
||||
/etc/nginx/sites-enabled \
|
||||
/var/www/certbot
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 80 443
|
||||
|
||||
# Use tini as init system
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
# Start Nginx (run as root for port binding, nginx will drop privileges)
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
78
nginx/conf/nginx.conf
Normal file
78
nginx/conf/nginx.conf
Normal file
@@ -0,0 +1,78 @@
|
||||
# Main Nginx configuration optimized for production
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
# Worker configuration
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
# MIME types
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Logging format
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time uct="$upstream_connect_time" '
|
||||
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# Performance optimizations
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# Buffer sizes
|
||||
client_body_buffer_size 128k;
|
||||
client_max_body_size 50m;
|
||||
client_header_buffer_size 1k;
|
||||
large_client_header_buffers 4 4k;
|
||||
output_buffers 1 32k;
|
||||
postpone_output 1460;
|
||||
|
||||
# Timeouts
|
||||
client_header_timeout 30s;
|
||||
client_body_timeout 30s;
|
||||
send_timeout 30s;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1000;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
|
||||
limit_req_zone $binary_remote_addr zone=static:10m rate=1000r/m;
|
||||
limit_req_status 429;
|
||||
|
||||
# Connection limiting
|
||||
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
|
||||
limit_conn conn_limit_per_ip 10;
|
||||
|
||||
# Include configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*.conf;
|
||||
}
|
||||
16
nginx/conf/security.conf
Normal file
16
nginx/conf/security.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
# Security headers and configurations
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# HSTS (HTTP Strict Transport Security)
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
|
||||
# Content Security Policy
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-src 'none'; object-src 'none'; base-uri 'self';" always;
|
||||
|
||||
# Hide server information (configured in main nginx.conf)
|
||||
172
nginx/conf/sites-enabled/roa2web.conf
Normal file
172
nginx/conf/sites-enabled/roa2web.conf
Normal file
@@ -0,0 +1,172 @@
|
||||
# ROA2WEB Virtual Host Configuration
|
||||
|
||||
# HTTP server for development (no redirect)
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost roa2web.local;
|
||||
|
||||
# Let's Encrypt challenge (for future production use)
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Development: serve content directly via HTTP
|
||||
location /api/ {
|
||||
proxy_pass http://roa_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;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://roa_backend/health;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /api/health {
|
||||
proxy_pass http://roa_backend/health;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://roa_frontend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS main server (disabled for development)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name localhost roa2web.local;
|
||||
#
|
||||
# # SSL configuration
|
||||
# include /etc/nginx/conf.d/ssl.conf;
|
||||
#
|
||||
# # Security headers
|
||||
# include /etc/nginx/conf.d/security.conf;
|
||||
#
|
||||
# # Logging
|
||||
# access_log /var/log/nginx/roa2web_access.log main;
|
||||
# error_log /var/log/nginx/roa2web_error.log warn;
|
||||
#
|
||||
# # API routes - proxy to FastAPI backend
|
||||
# location /api/ {
|
||||
# # Rate limiting for API
|
||||
# limit_req zone=api burst=20 nodelay;
|
||||
#
|
||||
# # Proxy configuration
|
||||
# proxy_pass http://roa_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;
|
||||
# proxy_set_header X-Forwarded-Host $host;
|
||||
# proxy_set_header X-Forwarded-Port $server_port;
|
||||
#
|
||||
# # Timeouts
|
||||
# proxy_connect_timeout 30s;
|
||||
# proxy_send_timeout 30s;
|
||||
# proxy_read_timeout 30s;
|
||||
#
|
||||
# # Buffering
|
||||
# proxy_buffering on;
|
||||
# proxy_buffer_size 4k;
|
||||
# proxy_buffers 8 4k;
|
||||
#
|
||||
# # No caching for API responses
|
||||
# add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
# add_header Pragma "no-cache";
|
||||
# add_header Expires "0";
|
||||
# }
|
||||
#
|
||||
# # Health check endpoints
|
||||
# location /health {
|
||||
# access_log off;
|
||||
# proxy_pass http://health_backend/health;
|
||||
# proxy_set_header Host $host;
|
||||
# }
|
||||
#
|
||||
# # Backend health check
|
||||
# location /api/health {
|
||||
# access_log off;
|
||||
# proxy_pass http://roa_backend/health;
|
||||
# proxy_set_header Host $host;
|
||||
# }
|
||||
#
|
||||
# # Static assets with caching
|
||||
# location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
# limit_req zone=static burst=50 nodelay;
|
||||
#
|
||||
# proxy_pass http://roa_frontend;
|
||||
# proxy_set_header Host $host;
|
||||
#
|
||||
# # Long-term caching for assets with hash
|
||||
# expires 1y;
|
||||
# add_header Cache-Control "public, immutable";
|
||||
# add_header Vary "Accept-Encoding";
|
||||
#
|
||||
# # Gzip compression
|
||||
# gzip_static on;
|
||||
# }
|
||||
#
|
||||
# # Frontend SPA - everything else goes to Vue.js
|
||||
# location / {
|
||||
# limit_req zone=static burst=100 nodelay;
|
||||
#
|
||||
# proxy_pass http://roa_frontend;
|
||||
# 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;
|
||||
#
|
||||
# # Short caching for HTML files
|
||||
# add_header Cache-Control "no-cache, must-revalidate";
|
||||
# expires 5m;
|
||||
# }
|
||||
#
|
||||
# # Deny access to sensitive files
|
||||
# location ~ /\. {
|
||||
# deny all;
|
||||
# access_log off;
|
||||
# log_not_found off;
|
||||
# }
|
||||
#
|
||||
# location ~ \.(sql|env|key|pem)$ {
|
||||
# deny all;
|
||||
# access_log off;
|
||||
# log_not_found off;
|
||||
# }
|
||||
# }
|
||||
|
||||
# Development HTTP server (for local development)
|
||||
server {
|
||||
listen 8080;
|
||||
server_name dev.localhost;
|
||||
|
||||
# Simple HTTP setup for development
|
||||
location /api/ {
|
||||
proxy_pass http://roa_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;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://roa_backend/health;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /api/health {
|
||||
proxy_pass http://roa_backend/health;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://roa_frontend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
26
nginx/conf/ssl.conf
Normal file
26
nginx/conf/ssl.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
# SSL/TLS Configuration
|
||||
# Modern SSL configuration for security
|
||||
|
||||
# SSL protocols and ciphers
|
||||
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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# SSL session configuration
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# OCSP stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# SSL optimization
|
||||
ssl_buffer_size 8k;
|
||||
|
||||
# Default SSL certificate paths (to be replaced by Let's Encrypt)
|
||||
# ssl_certificate /etc/ssl/certs/roa2web.crt;
|
||||
# ssl_certificate_key /etc/ssl/private/roa2web.key;
|
||||
|
||||
# Diffie-Hellman parameters
|
||||
ssl_dhparam /etc/ssl/certs/dhparam.pem;
|
||||
19
nginx/conf/upstream.conf
Normal file
19
nginx/conf/upstream.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
# Upstream configuration for load balancing
|
||||
|
||||
# Backend FastAPI services
|
||||
upstream roa_backend {
|
||||
server roa-backend:8000 max_fails=3 fail_timeout=30s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Frontend services (for direct access if needed)
|
||||
upstream roa_frontend {
|
||||
server roa-frontend:3000 max_fails=3 fail_timeout=30s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# Health check backend
|
||||
upstream health_backend {
|
||||
server roa-backend:8000;
|
||||
keepalive 2;
|
||||
}
|
||||
48
nginx/scripts/ssl-renew.sh
Normal file
48
nginx/scripts/ssl-renew.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
# SSL certificate renewal script for Let's Encrypt
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
DOMAIN=${DOMAIN:-localhost}
|
||||
EMAIL=${SSL_EMAIL:-admin@roa2web.local}
|
||||
WEBROOT_PATH=/var/www/certbot
|
||||
|
||||
# Function to log messages
|
||||
log() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Check if running in production
|
||||
if [ "$ENVIRONMENT" = "production" ]; then
|
||||
log "Starting SSL certificate renewal for domain: $DOMAIN"
|
||||
|
||||
# Initial certificate generation
|
||||
if [ ! -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then
|
||||
log "Generating initial SSL certificate..."
|
||||
certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=$WEBROOT_PATH \
|
||||
--email=$EMAIL \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
--force-renewal \
|
||||
-d $DOMAIN
|
||||
fi
|
||||
|
||||
# Renew certificates
|
||||
log "Attempting certificate renewal..."
|
||||
certbot renew --webroot --webroot-path=$WEBROOT_PATH
|
||||
|
||||
# Reload nginx if certificates were renewed
|
||||
if [ $? -eq 0 ]; then
|
||||
log "Certificate renewal successful, reloading nginx..."
|
||||
nginx -s reload
|
||||
else
|
||||
log "Certificate renewal failed or not needed"
|
||||
fi
|
||||
else
|
||||
log "Not in production environment, skipping SSL renewal"
|
||||
fi
|
||||
|
||||
log "SSL renewal script completed"
|
||||
60
reports-app/backend/Dockerfile
Normal file
60
reports-app/backend/Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# Multi-stage build for optimized production image
|
||||
# Stage 1: Build dependencies
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install dependencies
|
||||
COPY ./reports-app/backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# Stage 2: Production image
|
||||
FROM python:3.11-slim as production
|
||||
|
||||
# Create non-root user for security
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies only
|
||||
RUN apt-get update && apt-get install -y \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
# Copy Python dependencies from builder stage
|
||||
COPY --from=builder /root/.local /home/appuser/.local
|
||||
|
||||
# Copy application code
|
||||
COPY ./reports-app/backend/app/ ./app/
|
||||
|
||||
# Copy shared modules (needed for auth and database)
|
||||
COPY ./shared/ ./shared/
|
||||
|
||||
# Set ownership and permissions
|
||||
RUN chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# Add user's local bin to PATH
|
||||
ENV PATH=/home/appuser/.local/bin:$PATH
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Use tini as init system for proper signal handling
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
# Run application with production settings
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||
204
reports-app/backend/README.md
Normal file
204
reports-app/backend/README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# ROA Reports Backend API
|
||||
|
||||
FastAPI backend pentru aplicația de rapoarte ROA2WEB.
|
||||
|
||||
## 🚀 Funcționalități
|
||||
|
||||
### 📊 API Endpoints Implementate
|
||||
|
||||
#### Authentication (`/auth`)
|
||||
- `POST /auth/login` - Autentificare utilizator
|
||||
- `POST /auth/logout` - Deconectare utilizator
|
||||
- `GET /auth/me` - Informații utilizator curent
|
||||
- `GET /auth/validate` - Validare token JWT
|
||||
|
||||
#### Companies (`/companies`)
|
||||
- `GET /companies/` - Lista firmelor utilizatorului
|
||||
- `GET /companies/{company_code}` - Detalii firmă
|
||||
- `GET /companies/{company_code}/validate` - Validare acces firmă
|
||||
|
||||
#### Invoices (`/invoices`)
|
||||
- `GET /invoices/` - Lista facturi cu filtrare și paginare
|
||||
- `GET /invoices/summary` - Rezumat facturi pentru dashboard
|
||||
- `GET /invoices/{invoice_number}` - Detalii factură specifică
|
||||
- `GET /invoices/export/{format}` - Export facturi (planned)
|
||||
|
||||
#### Payments (`/payments`)
|
||||
- `GET /payments/` - Lista încasări/plăți cu filtrare și paginare
|
||||
- `GET /payments/summary` - Rezumat încasări pentru dashboard
|
||||
- `GET /payments/{payment_number}` - Detalii încasare specifică
|
||||
- `POST /payments/` - Creare încasare nouă (planned)
|
||||
- `PUT /payments/{payment_number}` - Actualizare încasare (planned)
|
||||
- `GET /payments/export/{format}` - Export încasări (planned)
|
||||
|
||||
#### Health Check
|
||||
- `GET /` - Status API
|
||||
- `GET /health` - Health check complet cu database
|
||||
|
||||
## 🏗️ Arhitectură
|
||||
|
||||
### Shared Components Integration
|
||||
- **Database Pool**: Folosește `roa2web/shared/database/oracle_pool.py`
|
||||
- **JWT Authentication**: Folosește `roa2web/shared/auth/` pentru validare token-uri
|
||||
- **Middleware**: Authentication middleware cu rate limiting
|
||||
|
||||
### Structură Aplicație
|
||||
```
|
||||
app/
|
||||
├── main.py # FastAPI entry point
|
||||
├── models/ # Pydantic models
|
||||
│ ├── invoice.py # Modele pentru facturi
|
||||
│ └── payment.py # Modele pentru încasări
|
||||
├── routers/ # API endpoints
|
||||
│ ├── auth.py # Authentication endpoints
|
||||
│ ├── companies.py # Companies management
|
||||
│ ├── invoices.py # Invoices API
|
||||
│ └── payments.py # Payments API
|
||||
├── services/ # Business logic
|
||||
│ ├── invoice_service.py # Serviciu facturi cu Oracle queries
|
||||
│ └── payment_service.py # Serviciu încasări cu Oracle queries
|
||||
└── schemas/ # Response schemas (reserved)
|
||||
```
|
||||
|
||||
## 🔧 Instalare și Rulare
|
||||
|
||||
### Development Local
|
||||
|
||||
1. **Install dependencies**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Environment Variables**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Editează .env cu configurările tale
|
||||
```
|
||||
|
||||
3. **Run development server**:
|
||||
```bash
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
4. **Access API**:
|
||||
- API: http://localhost:8000
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t roa-reports-backend .
|
||||
|
||||
# Run container
|
||||
docker run -p 8000:8000 --env-file .env roa-reports-backend
|
||||
```
|
||||
|
||||
## 📝 Configurație
|
||||
|
||||
### 🔐 Oracle Database Setup
|
||||
|
||||
**IMPORTANT**: Conectare la schema **CONTAFIN_ORACLE** pentru authentication!
|
||||
|
||||
#### Arhitectura Bazei de Date
|
||||
- **Schema CONTAFIN_ORACLE**: Conține utilizatorii și procedura `pack_drepturi.verificautilizator`
|
||||
- **Scheme separate pentru firme**: Fiecare firmă are propria schemă cu date (ROMFAST, EUROLEVIS, etc.)
|
||||
|
||||
#### Flow-ul de Autentificare
|
||||
1. **Conectare**: La schema `CONTAFIN_ORACLE`
|
||||
2. **Verificare**: User/pass prin `pack_drepturi.verificautilizator(username, password)`
|
||||
3. **Drepturi**: Citire firme din `vdef_util_grup WHERE id_util = user_id`
|
||||
4. **Selecție**: User selectează firma/schema pentru acces la date
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Oracle Database (prin SSH tunnel)
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_PASSWORD=your_oracle_password
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# SSH Tunnel Setup Required:
|
||||
# ./ssh_tunnel.sh start
|
||||
# Server: 83.103.197.79:22122 -> 10.0.20.36:1521
|
||||
|
||||
# Test User Credentials (pentru dezvoltare):
|
||||
# Username: "MARIUS M" (cu spațiu în nume!)
|
||||
# Password: "PAROLA81"
|
||||
# Are acces la 66+ firme/scheme Oracle
|
||||
|
||||
# JWT Settings
|
||||
JWT_SECRET_KEY=your-secret-key
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
|
||||
# API Settings
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
DEBUG=True
|
||||
|
||||
# CORS
|
||||
FRONTEND_URLS=http://localhost:3000,http://localhost:5173
|
||||
```
|
||||
|
||||
## 🔐 Autentificare
|
||||
|
||||
API-ul folosește JWT Bearer tokens pentru autentificare:
|
||||
|
||||
1. **Login**: `POST /auth/login` cu username/password
|
||||
2. **Token Usage**: Include `Authorization: Bearer <token>` în header
|
||||
3. **Company Access**: Fiecare request verifică dacă utilizatorul are acces la firma specificată
|
||||
|
||||
## 📊 Models și Filtrare
|
||||
|
||||
### Invoice Filter Parameters
|
||||
- `company`: Codul firmei (obligatoriu)
|
||||
- `partner_type`: CLIENTI sau FURNIZORI
|
||||
- `date_from`, `date_to`: Filtrare după dată
|
||||
- `partner_name`: Filtrare după numele partenerului
|
||||
- `only_unpaid`: Doar facturile neachitate
|
||||
- `min_amount`, `max_amount`: Filtrare după sumă
|
||||
- `page`, `page_size`: Paginare
|
||||
|
||||
### Response Models
|
||||
- **InvoiceListResponse**: Lista paginată cu metadata
|
||||
- **InvoiceSummary**: Statistici pentru dashboard
|
||||
- **PaymentListResponse**: Lista încasări cu metadata
|
||||
- **PaymentSummary**: Statistici încasări
|
||||
|
||||
## 🚀 Următorii Pași
|
||||
|
||||
1. **Export Functionality**: Implementare export Excel, PDF, CSV
|
||||
2. **CRUD Operations**: Operațiuni complete pentru încasări
|
||||
3. **Advanced Filtering**: Filtre avansate și sortare
|
||||
4. **Caching**: Redis cache pentru performance
|
||||
5. **Rate Limiting**: Advanced rate limiting
|
||||
6. **Audit Logging**: Logging complet pentru operațiuni
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Unit tests (când vor fi implementate)
|
||||
pytest tests/ -v
|
||||
|
||||
# Health check manual
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# API testing
|
||||
# Vezi documentația Swagger la /docs pentru toate endpoint-urile
|
||||
```
|
||||
|
||||
## 📚 Compatibilitate
|
||||
|
||||
API-ul este compatibil 100% cu query-urile și datele din aplicația Flask existentă:
|
||||
- Stesso schema de date Oracle
|
||||
- Aceleași view-uri (`vireg_parteneri`)
|
||||
- Aceleași calcule pentru statusul facturilor
|
||||
- Aceleași validări pentru acces utilizatori
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ COMPLET - Ready for Frontend Integration
|
||||
**Next Phase**: Frontend Vue.js Development
|
||||
0
reports-app/backend/app/__init__.py
Normal file
0
reports-app/backend/app/__init__.py
Normal file
222
reports-app/backend/app/main.py
Normal file
222
reports-app/backend/app/main.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
ROA Reports API - FastAPI Backend
|
||||
Aplicația principală pentru rapoarte facturi și încasări
|
||||
"""
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Încărcare environment variables din .env
|
||||
load_dotenv()
|
||||
|
||||
# Configurare TNS_ADMIN pentru Oracle
|
||||
tns_path = os.path.join(os.path.dirname(__file__), '../../../../app')
|
||||
os.environ['TNS_ADMIN'] = tns_path
|
||||
|
||||
# Adăugare path pentru shared modules
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
|
||||
|
||||
from database.oracle_pool import oracle_pool
|
||||
from auth.middleware import AuthenticationMiddleware
|
||||
# from auth.routes import create_auth_router # Fixed inline
|
||||
|
||||
# Import routere locale
|
||||
from app.routers import invoices, dashboard, treasury, companies, telegram
|
||||
|
||||
# Auth endpoints pentru test
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
import jwt
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# JWT Setup
|
||||
JWT_SECRET = os.getenv("JWT_SECRET_KEY", "test-secret-key")
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_EXPIRE_MINUTES = 30
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str # Added refresh token
|
||||
token_type: str
|
||||
user: dict
|
||||
|
||||
def create_auth_router():
|
||||
"""Create authentication router for testing"""
|
||||
auth_router = APIRouter(tags=["authentication"])
|
||||
|
||||
@auth_router.post("/login", response_model=LoginResponse)
|
||||
async def login(credentials: LoginRequest):
|
||||
"""Autentificare utilizator prin Oracle database"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Call verificautilizator procedure using SELECT
|
||||
cursor.execute("""
|
||||
SELECT pack_drepturi.verificautilizator(:username, :password)
|
||||
FROM DUAL
|
||||
""", {
|
||||
'username': credentials.username.upper(),
|
||||
'password': credentials.password
|
||||
})
|
||||
|
||||
result = cursor.fetchone()
|
||||
verification_result = result[0] if result else -1
|
||||
|
||||
# Check if authentication was successful
|
||||
if verification_result == -1:
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
|
||||
# Get user companies - first get user ID from UTILIZATORI
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': credentials.username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
raise HTTPException(status_code=401, detail="User not found in system")
|
||||
|
||||
user_id = user_row[0]
|
||||
|
||||
# Now get companies using the correct query structure
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2
|
||||
AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
companies_result = cursor.fetchall()
|
||||
|
||||
if not companies_result:
|
||||
# Don't fail login if no companies - let frontend show message
|
||||
companies = []
|
||||
else:
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
# Create JWT token with all required fields
|
||||
now = datetime.utcnow()
|
||||
expire = now + timedelta(minutes=JWT_EXPIRE_MINUTES)
|
||||
token_data = {
|
||||
"username": credentials.username, # Changed from "sub" to "username"
|
||||
"user_id": user_id, # Include user_id from database
|
||||
"companies": companies,
|
||||
"permissions": ["read", "reports"], # Default permissions
|
||||
"exp": expire,
|
||||
"iat": now, # Added issued at time
|
||||
"type": "access" # Added token type
|
||||
}
|
||||
access_token = jwt.encode(token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
# Create refresh token
|
||||
refresh_expire = now + timedelta(days=7)
|
||||
refresh_token_data = {
|
||||
"username": credentials.username,
|
||||
"user_id": user_id,
|
||||
"companies": companies,
|
||||
"permissions": ["read", "reports"],
|
||||
"exp": refresh_expire,
|
||||
"iat": now,
|
||||
"type": "refresh"
|
||||
}
|
||||
refresh_token = jwt.encode(refresh_token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token, # Include refresh token
|
||||
token_type="bearer",
|
||||
user={
|
||||
"username": credentials.username,
|
||||
"user_id": user_id, # Include user_id
|
||||
"companies": companies,
|
||||
"permissions": ["read", "reports"] # Include permissions
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal authentication error")
|
||||
|
||||
return auth_router
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Lifecycle events pentru aplicație"""
|
||||
# Startup
|
||||
await oracle_pool.initialize()
|
||||
print("[ROA Reports API] Started successfully")
|
||||
yield
|
||||
# Shutdown
|
||||
await oracle_pool.close_pool()
|
||||
print("[ROA Reports API] Stopped")
|
||||
|
||||
app = FastAPI(
|
||||
title="ROA Reports API",
|
||||
description="API pentru rapoarte ERP - facturi, încasări și alte rapoarte financiare",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS pentru frontend Vue.js
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allow all origins for production deployment
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Authentication middleware
|
||||
print("[MAIN DEBUG] Adding AuthenticationMiddleware")
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=[
|
||||
"/", "/docs", "/health", "/api/auth/login", "/redoc", "/openapi.json",
|
||||
"/api/telegram/auth/verify-user", # Public endpoint for Telegram bot
|
||||
"/api/telegram/auth/refresh-token", # Public endpoint for token refresh
|
||||
"/api/telegram/health" # Health check for Telegram router
|
||||
]
|
||||
)
|
||||
print("[MAIN DEBUG] AuthenticationMiddleware added - FRESH RESTART - AUTH FIX APPLIED")
|
||||
|
||||
# Include routere with /api prefix
|
||||
auth_router = create_auth_router()
|
||||
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
|
||||
app.include_router(companies.router, prefix="/api/companies", tags=["companies"])
|
||||
app.include_router(invoices.router, prefix="/api/invoices", tags=["invoices"])
|
||||
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
|
||||
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
|
||||
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
print("[MAIN DEBUG] Root endpoint accessed")
|
||||
return {"message": "ROA Reports API", "version": "1.0.0", "status": "running"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
# Test database connection
|
||||
try:
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
return {"api": "healthy", "database": "connected", "timestamp": datetime.utcnow().isoformat()}
|
||||
except Exception as e:
|
||||
return {"api": "healthy", "database": f"error: {str(e)}", "timestamp": datetime.utcnow().isoformat()}
|
||||
0
reports-app/backend/app/models/__init__.py
Normal file
0
reports-app/backend/app/models/__init__.py
Normal file
118
reports-app/backend/app/models/dashboard.py
Normal file
118
reports-app/backend/app/models/dashboard.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from pydantic import BaseModel
|
||||
from decimal import Decimal
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
class TreasuryAccount(BaseModel):
|
||||
"""Cont de trezorerie (bancă/casă)"""
|
||||
cont: str # 5121, 5124, 5311, 5314
|
||||
nume_cont: str # "Bancă LEI", "Casă VALUTA" etc
|
||||
nume_banca: str # Numele băncii din vbalanta_parteneri.nume
|
||||
sold: Decimal
|
||||
valuta: str
|
||||
|
||||
class TrendData(BaseModel):
|
||||
"""Model pentru datele de trend - MODEL VECHI"""
|
||||
labels: List[str]
|
||||
incasari: List[Decimal]
|
||||
plati: List[Decimal]
|
||||
trezorerie: List[Decimal]
|
||||
incasari_total: Decimal
|
||||
plati_total: Decimal
|
||||
trezorerie_total: Decimal
|
||||
incasari_change: Optional[float] = None
|
||||
plati_change: Optional[float] = None
|
||||
trezorerie_change: Optional[float] = None
|
||||
|
||||
class TrendsResponse(BaseModel):
|
||||
"""Model pentru răspunsul endpoint-ului de trenduri - MODEL NOU"""
|
||||
# Current period data
|
||||
periods: List[str]
|
||||
clienti_facturat: List[float]
|
||||
clienti_incasat: List[float]
|
||||
furnizori_facturat: List[float]
|
||||
furnizori_achitat: List[float]
|
||||
clienti_sold: List[float]
|
||||
furnizori_sold: List[float]
|
||||
trezorerie_sold: Optional[List[float]] = None
|
||||
rata_incasare_clienti: List[float]
|
||||
rata_achitare_furnizori: List[float]
|
||||
|
||||
# Previous period data (for year-over-year comparison in sparklines)
|
||||
previous_periods: Optional[List[str]] = None
|
||||
clienti_facturat_prev: Optional[List[float]] = None
|
||||
clienti_incasat_prev: Optional[List[float]] = None
|
||||
furnizori_facturat_prev: Optional[List[float]] = None
|
||||
furnizori_achitat_prev: Optional[List[float]] = None
|
||||
clienti_sold_prev: Optional[List[float]] = None
|
||||
furnizori_sold_prev: Optional[List[float]] = None
|
||||
trezorerie_sold_prev: Optional[List[float]] = None
|
||||
|
||||
# Metadata and analytics
|
||||
metadata: Dict[str, Any]
|
||||
growth_rates: Optional[Dict[str, float]] = None
|
||||
|
||||
class DashboardSummary(BaseModel):
|
||||
"""Model pentru toate datele dashboard-ului"""
|
||||
# CLIENȚI - statistici existente
|
||||
clienti_total_facturat: Decimal # precdeb + debit (conturi 4111, 461)
|
||||
clienti_total_incasat: Decimal # preccred + credit (conturi 4111, 461)
|
||||
clienti_avansuri: Decimal # sold 419 (pasiv): credit - debit
|
||||
clienti_sold_total: Decimal # (facturat - incasat) - avansuri
|
||||
clienti_sold_restant: Decimal # sold cu datascad < azi
|
||||
|
||||
# CLIENȚI - NOI câmpuri pentru sold în termen
|
||||
clienti_sold_in_termen: Decimal # sold cu datascad >= azi
|
||||
|
||||
# CLIENȚI - NOI detalieri restanțe (sold cu datascad < azi)
|
||||
clienti_restant_7: Decimal # restant 1-7 zile
|
||||
clienti_restant_14: Decimal # restant 8-14 zile
|
||||
clienti_restant_30: Decimal # restant 15-30 zile
|
||||
clienti_restant_60: Decimal # restant 31-60 zile
|
||||
clienti_restant_90: Decimal # restant 61-90 zile
|
||||
clienti_restant_90plus: Decimal # restant 90+ zile
|
||||
|
||||
# CLIENȚI - NOI detalieri scadențe (sold cu datascad >= azi)
|
||||
clienti_scadent_7: Decimal # scadent în 1-7 zile
|
||||
clienti_scadent_14: Decimal # scadent în 8-14 zile
|
||||
clienti_scadent_30: Decimal # scadent în 15-30 zile
|
||||
clienti_scadent_60: Decimal # scadent în 31-60 zile
|
||||
clienti_scadent_90: Decimal # scadent în 61-90 zile
|
||||
clienti_scadent_90plus: Decimal # scadent în 90+ zile
|
||||
|
||||
# FURNIZORI - statistici existente
|
||||
furnizori_total_facturat: Decimal # preccred + credit (conturi 401, 404, 462)
|
||||
furnizori_total_achitat: Decimal # precdeb + debit (conturi 401, 404, 462)
|
||||
furnizori_avansuri: Decimal # sold 409x (activ): debit - credit
|
||||
furnizori_sold_total: Decimal # (facturat - achitat) - avansuri
|
||||
furnizori_sold_restant: Decimal # sold cu datascad < azi
|
||||
|
||||
# FURNIZORI - NOI câmpuri pentru sold în termen
|
||||
furnizori_sold_in_termen: Decimal # sold cu datascad >= azi
|
||||
|
||||
# FURNIZORI - NOI detalieri restanțe (sold cu datascad < azi)
|
||||
furnizori_restant_7: Decimal # restant 1-7 zile
|
||||
furnizori_restant_14: Decimal # restant 8-14 zile
|
||||
furnizori_restant_30: Decimal # restant 15-30 zile
|
||||
furnizori_restant_60: Decimal # restant 31-60 zile
|
||||
furnizori_restant_90: Decimal # restant 61-90 zile
|
||||
furnizori_restant_90plus: Decimal # restant 90+ zile
|
||||
|
||||
# FURNIZORI - NOI detalieri scadențe (sold cu datascad >= azi)
|
||||
furnizori_scadent_7: Decimal # scadent în 1-7 zile
|
||||
furnizori_scadent_14: Decimal # scadent în 8-14 zile
|
||||
furnizori_scadent_30: Decimal # scadent în 15-30 zile
|
||||
furnizori_scadent_60: Decimal # scadent în 31-60 zile
|
||||
furnizori_scadent_90: Decimal # scadent în 61-90 zile
|
||||
furnizori_scadent_90plus: Decimal # scadent în 90+ zile
|
||||
|
||||
# TREZORERIE - existente
|
||||
treasury_accounts: List[TreasuryAccount]
|
||||
treasury_totals_by_currency: Dict[str, Decimal]
|
||||
|
||||
# DATE SUPLIMENTARE pentru trend analysis
|
||||
clienti_facturat_luna_anterioara: Optional[Decimal] = Decimal('0')
|
||||
furnizori_facturat_luna_anterioara: Optional[Decimal] = Decimal('0')
|
||||
clienti_facturat_an_curent: Optional[Decimal] = Decimal('0')
|
||||
clienti_facturat_an_anterior: Optional[Decimal] = Decimal('0')
|
||||
furnizori_facturat_an_curent: Optional[Decimal] = Decimal('0')
|
||||
furnizori_facturat_an_anterior: Optional[Decimal] = Decimal('0')
|
||||
73
reports-app/backend/app/models/invoice.py
Normal file
73
reports-app/backend/app/models/invoice.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Modele Pydantic pentru facturi - Compatibile cu aplicația Flask existentă
|
||||
"""
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from datetime import date
|
||||
from typing import Optional, List, Literal
|
||||
from decimal import Decimal
|
||||
|
||||
class InvoiceBase(BaseModel):
|
||||
"""Model de bază pentru factură - mapează exact pe rezultatul query-ului Flask"""
|
||||
nume: str = Field(description="Numele partenerului")
|
||||
nract: int = Field(description="Numărul actului")
|
||||
dataact: Optional[date] = Field(description="Data actului")
|
||||
datascad: Optional[date] = Field(description="Data scadentă")
|
||||
contract: Optional[str] = Field(description="Numărul contractului")
|
||||
cod_fiscal: Optional[str] = Field(description="Codul fiscal")
|
||||
reg_comert: Optional[str] = Field(description="Registrul comerțului")
|
||||
|
||||
class Invoice(InvoiceBase):
|
||||
"""Model complet pentru factură cu calcule financiare"""
|
||||
totctva: Decimal = Field(description="Total cu TVA", decimal_places=2)
|
||||
achitat: Decimal = Field(description="Suma achitată", decimal_places=2)
|
||||
soldfinal: Decimal = Field(description="Soldul final", decimal_places=2)
|
||||
css_class: Literal["", "invoice-paid", "invoice-overdue"] = Field(
|
||||
default="", description="Clasa CSS pentru stilizare"
|
||||
)
|
||||
|
||||
@validator('css_class', always=True)
|
||||
def determine_css_class(cls, v, values):
|
||||
"""Determină automat clasa CSS bazată pe status factură"""
|
||||
if 'soldfinal' in values and 'datascad' in values:
|
||||
sold = values['soldfinal']
|
||||
data_scad = values['datascad']
|
||||
|
||||
if sold < 1:
|
||||
return 'invoice-paid'
|
||||
elif data_scad and data_scad < date.today() and sold != 0:
|
||||
return 'invoice-overdue'
|
||||
return ''
|
||||
|
||||
class InvoiceFilter(BaseModel):
|
||||
"""Filtru pentru căutarea facturilor"""
|
||||
company: str = Field(description="Codul firmei (schema Oracle)")
|
||||
partner_type: Literal["CLIENTI", "FURNIZORI"] = Field(description="Tipul partenerului")
|
||||
date_from: Optional[date] = Field(description="Data de început")
|
||||
date_to: Optional[date] = Field(description="Data de sfârșit")
|
||||
partner_name: Optional[str] = Field(description="Filtru după nume")
|
||||
only_unpaid: bool = Field(default=True, description="Doar neachitate")
|
||||
min_amount: Optional[Decimal] = Field(description="Suma minimă")
|
||||
max_amount: Optional[Decimal] = Field(description="Suma maximă")
|
||||
page: int = Field(default=1, ge=1, description="Pagina")
|
||||
page_size: int = Field(default=50, ge=1, le=1000, description="Mărimea paginii")
|
||||
|
||||
class InvoiceListResponse(BaseModel):
|
||||
"""Răspuns pentru lista de facturi"""
|
||||
invoices: List[Invoice]
|
||||
total_count: int
|
||||
filtered_count: int
|
||||
total_amount: Decimal
|
||||
page: int
|
||||
page_size: int
|
||||
has_more: bool
|
||||
|
||||
class InvoiceSummary(BaseModel):
|
||||
"""Rezumat pentru facturi - pentru dashboard"""
|
||||
company: str
|
||||
partner_type: str
|
||||
total_invoices: int
|
||||
total_amount: Decimal
|
||||
paid_amount: Decimal
|
||||
outstanding_amount: Decimal
|
||||
overdue_amount: Decimal
|
||||
overdue_count: int
|
||||
37
reports-app/backend/app/models/treasury.py
Normal file
37
reports-app/backend/app/models/treasury.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from pydantic import BaseModel
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
class BankCashRegister(BaseModel):
|
||||
"""Model pentru Registrul de Casă și Bancă"""
|
||||
nume: str
|
||||
nract: int
|
||||
dataact: datetime
|
||||
nume_cont_bancar: str # din vbalanta_parteneri.nume
|
||||
incasari: Decimal
|
||||
plati: Decimal
|
||||
sold: Decimal
|
||||
valuta: str
|
||||
tip_registru: str # "BANCA LEI", "CASA VALUTA" etc
|
||||
explicatia: str
|
||||
|
||||
class RegisterFilter(BaseModel):
|
||||
"""Filtre pentru registrul de casă și bancă"""
|
||||
company: str
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
partner_name: Optional[str] = None
|
||||
page: int = 1
|
||||
page_size: int = 50
|
||||
|
||||
class RegisterListResponse(BaseModel):
|
||||
"""Răspuns pentru lista din registru"""
|
||||
registers: List[BankCashRegister]
|
||||
total_count: int
|
||||
filtered_count: int
|
||||
total_incasari: Decimal
|
||||
total_plati: Decimal
|
||||
page: int
|
||||
page_size: int
|
||||
has_more: bool
|
||||
0
reports-app/backend/app/routers/__init__.py
Normal file
0
reports-app/backend/app/routers/__init__.py
Normal file
177
reports-app/backend/app/routers/companies.py
Normal file
177
reports-app/backend/app/routers/companies.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
API Router pentru managementul firmelor
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from typing import List, Optional
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from database.oracle_pool import oracle_pool
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(redirect_slashes=False)
|
||||
|
||||
class Company(BaseModel):
|
||||
"""Model pentru firmă"""
|
||||
id_firma: int # Cheia primară
|
||||
name: str # Numele firmei
|
||||
schema_name: str # Schema Oracle
|
||||
fiscal_code: Optional[str] = None
|
||||
is_active: bool = True
|
||||
|
||||
class CompanyListResponse(BaseModel):
|
||||
"""Răspuns pentru lista de firme"""
|
||||
companies: List[Company]
|
||||
total_count: int
|
||||
|
||||
@router.get("", response_model=CompanyListResponse)
|
||||
@router.get("/", response_model=CompanyListResponse)
|
||||
async def get_user_companies(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține lista firmelor la care utilizatorul are acces cu detalii complete
|
||||
"""
|
||||
print(f"[COMPANIES DEBUG] Request state: user={getattr(request.state, 'user', 'NOT_SET')}, is_authenticated={getattr(request.state, 'is_authenticated', 'NOT_SET')}")
|
||||
print(f"[COMPANIES DEBUG] Authorization header: {request.headers.get('Authorization', 'NOT_SET')}")
|
||||
try:
|
||||
companies = []
|
||||
|
||||
# Obține toate companiile pentru utilizator direct din query-ul complet
|
||||
# Ignorăm lista din JWT și recalculăm direct din Oracle pentru a obține toate cele 63 de companii
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Primul pas: obținem ID-ul utilizatorului din UTILIZATORI
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': current_user.username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
print(f"User {current_user.username} not found in UTILIZATORI table")
|
||||
return CompanyListResponse(companies=[], total_count=0)
|
||||
|
||||
user_id = user_row[0]
|
||||
print(f"Found user {current_user.username} with ID: {user_id}")
|
||||
|
||||
# Al doilea pas: obținem TOATE companiile pentru programul 2
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA, A.SCHEMA, A.COD_FISCAL
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
companies_rows = cursor.fetchall()
|
||||
|
||||
for row in companies_rows:
|
||||
id_firma = row[0]
|
||||
firma_name = row[1]
|
||||
schema = row[2]
|
||||
fiscal_code = row[3] # Poate fi NULL
|
||||
|
||||
company = Company(
|
||||
id_firma=id_firma,
|
||||
name=firma_name,
|
||||
schema_name=schema,
|
||||
fiscal_code=fiscal_code,
|
||||
is_active=True
|
||||
)
|
||||
companies.append(company)
|
||||
|
||||
print(f"Found {len(companies)} companies for user {current_user.username}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Eroare la obținerea companiilor din Oracle: {e}")
|
||||
# Fallback: folosim lista din JWT dacă query-ul Oracle eșuează
|
||||
for company_id in current_user.companies:
|
||||
try:
|
||||
id_firma = int(company_id)
|
||||
company = Company(
|
||||
id_firma=id_firma,
|
||||
name=f"Company {id_firma}",
|
||||
schema_name="",
|
||||
fiscal_code="",
|
||||
is_active=True
|
||||
)
|
||||
companies.append(company)
|
||||
except ValueError:
|
||||
# Skip invalid company IDs
|
||||
continue
|
||||
|
||||
return CompanyListResponse(
|
||||
companies=companies,
|
||||
total_count=len(companies)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea listei de firme: {str(e)}")
|
||||
|
||||
@router.get("/{company_id}", response_model=Company)
|
||||
async def get_company_details(
|
||||
company_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține detaliile unei firme specifice
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company_id not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company_id}")
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Query pentru detaliile firmei
|
||||
company_query = """
|
||||
SELECT ID_FIRMA, FIRMA, SCHEMA, COD_FISCAL
|
||||
FROM V_NOM_FIRME
|
||||
WHERE ID_FIRMA = :company_id
|
||||
"""
|
||||
|
||||
cursor.execute(company_query, {'company_id': int(company_id)})
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail=f"Firma {company_id} nu a fost găsită")
|
||||
|
||||
return Company(
|
||||
id_firma=row[0],
|
||||
name=row[1],
|
||||
schema_name=row[2],
|
||||
fiscal_code=row[3] or "",
|
||||
is_active=True
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea detaliilor firmei: {str(e)}")
|
||||
|
||||
@router.get("/{company_id}/validate")
|
||||
async def validate_company_access(
|
||||
company_id: str,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Validează dacă utilizatorul are acces la o firmă specificată
|
||||
"""
|
||||
has_access = company_id in current_user.companies
|
||||
|
||||
return {
|
||||
"company_id": company_id,
|
||||
"has_access": has_access,
|
||||
"user": current_user.username,
|
||||
"message": "Acces validat" if has_access else "Acces refuzat"
|
||||
}
|
||||
327
reports-app/backend/app/routers/dashboard.py
Normal file
327
reports-app/backend/app/routers/dashboard.py
Normal file
@@ -0,0 +1,327 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Optional
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..models.dashboard import DashboardSummary, TrendsResponse, TrendData
|
||||
from ..services.dashboard_service import DashboardService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/summary", response_model=DashboardSummary)
|
||||
async def get_dashboard_summary(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține toate datele pentru dashboard într-un singur apel
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează statistici clienți/furnizori și trezorerie
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_complete_summary(company, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor dashboard: {str(e)}")
|
||||
|
||||
@router.get("/trends", response_model=TrendsResponse)
|
||||
async def get_dashboard_trends(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
period: str = Query(default="30d", description="Perioada pentru trends: 7d, 30d, ytd, 12m"),
|
||||
compare_previous: bool = Query(default=True, description="Compară cu perioada anterioară"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține trenduri pentru indicatorii principali (clienți/furnizori)
|
||||
|
||||
- period: "7d" (7 zile), "30d" (30 zile), "ytd" (year to date), "12m" (12 luni)
|
||||
- compare_previous: dacă să compare cu perioada anterioară
|
||||
- Necesită autentificare JWT
|
||||
- Returnează date pentru grafice de trenduri
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
# Validează perioada
|
||||
valid_periods = ["7d", "30d", "ytd", "12m"]
|
||||
if period not in valid_periods:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Perioadă nevalidă: {period}. Valori permise: {', '.join(valid_periods)}"
|
||||
)
|
||||
|
||||
# Obține datele de trenduri
|
||||
result = await DashboardService.get_trends(int(company), period)
|
||||
|
||||
# The service now returns the data in the correct format
|
||||
# Return it directly as TrendsResponse
|
||||
return TrendsResponse(**result)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Value error in trends endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
|
||||
@router.get("/detailed-data")
|
||||
async def get_detailed_data(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=25, ge=1, le=100),
|
||||
search: str = Query(default="", description="Termen de căutare"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține date detaliate pentru tabelele din dashboard
|
||||
"""
|
||||
logger.info(f"[ROUTER] detailed-data called: company={company}, data_type={data_type}")
|
||||
try:
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
logger.info(f"[ROUTER] Calling DashboardService.get_detailed_data")
|
||||
result = await DashboardService.get_detailed_data(
|
||||
company=company,
|
||||
data_type=data_type,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search
|
||||
)
|
||||
|
||||
logger.info(f"[ROUTER] Service returned: {len(result.get('data', []))} rows")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor detaliate: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/performance")
|
||||
async def get_performance(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|ytd|12m)$", description="Perioada pentru analiză"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează date performanță pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează grafice încasări vs plăți pentru perioada selectată
|
||||
- Calculează indicatori: rata încasării, cash conversion, working capital
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_performance_data(company, period)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Performance"),
|
||||
"borderColor": result.get("borderColor", "#3B82F6"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(59, 130, 246, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
|
||||
@router.get("/cashflow")
|
||||
async def get_cashflow(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m)$", description="Perioada pentru previziune"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează previziune cash flow pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Analizează scadențele viitoare pentru calculul cash flow-ului
|
||||
- Identifică zilele critice cu deficit de cash
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_cashflow_forecast(company, period)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Cash Flow"),
|
||||
"borderColor": result.get("borderColor", "#10B981"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(16, 185, 129, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
|
||||
@router.get("/maturity")
|
||||
async def get_maturity_analysis(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|12m|all)$", description="Orizont de planificare pentru analiza scadențelor"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează analiza scadențelor pentru orizontul de planificare selectat
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată
|
||||
- Perioade disponibile:
|
||||
* 7d: Toate restanțele + scadențe următoarelor 7 zile
|
||||
* 1m: Toate restanțele + scadențe următoarelor 30 zile
|
||||
* 3m: Toate restanțele + scadențe următoarelor 90 zile
|
||||
* 6m: Toate restanțele + scadențe următoarelor 180 zile
|
||||
* 12m: Toate restanțele + scadențe următoarelor 365 zile
|
||||
* all: Toate soldurile (fără filtru)
|
||||
- Compară scadențele clienți vs furnizori
|
||||
- Calculează balanța și oferă recomandări
|
||||
- Returnează metadate cu statistici complete
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_maturity_analysis(company, period)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
|
||||
@router.get("/monthly-flows")
|
||||
async def get_monthly_flows(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează fluxurile lunare pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează date pentru analiza fluxurilor lunare
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_monthly_flows(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
|
||||
@router.get("/treasury-breakdown")
|
||||
async def get_treasury_breakdown(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează defalcarea trezoreriei pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează distribuția soldurilor pe conturi și tipuri
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_treasury_breakdown(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
|
||||
@router.get("/net-balance-breakdown")
|
||||
async def get_net_balance_breakdown(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează defalcarea balanței nete pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează analiza detaliată a balanței nete
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_net_balance_breakdown(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
|
||||
@router.get("/current-period")
|
||||
async def get_current_period(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează perioada curentă (an și lună) din calendarul Oracle
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează anul, luna și perioada curentă în format YYYY-MM
|
||||
- Folosit pentru afișarea lunii curente în dashboard
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if str(company) not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await DashboardService.get_current_period(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
143
reports-app/backend/app/routers/invoices.py
Normal file
143
reports-app/backend/app/routers/invoices.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
API Router pentru facturi
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user, require_company_access
|
||||
from auth.models import CurrentUser
|
||||
from ..models.invoice import InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
||||
from ..services.invoice_service import InvoiceService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=InvoiceListResponse)
|
||||
async def get_invoices(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
min_amount: Optional[float] = Query(None, description="Suma minimă"),
|
||||
max_amount: Optional[float] = Query(None, description="Suma maximă"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține lista de facturi pentru o firmă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||
- Suportă filtrare și paginare
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
# Convertește string-urile de date în obiecte date
|
||||
date_from_obj = None
|
||||
date_to_obj = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = date.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Formatul datei de început este invalid. Folosiți YYYY-MM-DD")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_obj = date.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Formatul datei de sfârșit este invalid. Folosiți YYYY-MM-DD")
|
||||
|
||||
filter_params = InvoiceFilter(
|
||||
company=company,
|
||||
partner_type=partner_type,
|
||||
date_from=date_from_obj,
|
||||
date_to=date_to_obj,
|
||||
partner_name=partner_name,
|
||||
only_unpaid=only_unpaid,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await InvoiceService.get_invoices(filter_params, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea facturilor: {str(e)}")
|
||||
|
||||
@router.get("/summary", response_model=InvoiceSummary)
|
||||
async def get_invoices_summary(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține rezumatul facturilor pentru dashboard"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea rezumatului facturilor: {str(e)}")
|
||||
|
||||
@router.get("/{invoice_number}")
|
||||
async def get_invoice_details(
|
||||
invoice_number: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține detaliile unei facturi specifice"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea detaliilor facturii: {str(e)}")
|
||||
|
||||
@router.get("/export/{format}")
|
||||
async def export_invoices(
|
||||
format: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Export facturi în format specificat (excel, pdf, csv)
|
||||
Această funcție va fi implementată în viitor
|
||||
"""
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
# Verifică formatul
|
||||
if format not in ["excel", "pdf", "csv"]:
|
||||
raise HTTPException(status_code=400, detail="Format invalid. Formatele suportate sunt: excel, pdf, csv")
|
||||
|
||||
# Pentru moment, returnează o eroare că funcția nu este implementată
|
||||
raise HTTPException(status_code=501, detail=f"Export în format {format} nu este încă implementat")
|
||||
559
reports-app/backend/app/routers/telegram.py
Normal file
559
reports-app/backend/app/routers/telegram.py
Normal file
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
API Router pentru Telegram Bot Integration
|
||||
Furnizează endpoint-uri pentru autentificare, linking și export rapoarte pentru Telegram bot
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from typing import List, Optional, Dict, Any
|
||||
import sys
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from auth.jwt_handler import jwt_handler
|
||||
from database.oracle_pool import oracle_pool
|
||||
|
||||
# Telegram bot internal API URL (running on same server)
|
||||
TELEGRAM_BOT_INTERNAL_API = os.getenv("TELEGRAM_BOT_INTERNAL_API", "http://localhost:8002")
|
||||
|
||||
router = APIRouter(redirect_slashes=False)
|
||||
|
||||
# ==================== Schemas ====================
|
||||
|
||||
class GenerateCodeRequest(BaseModel):
|
||||
"""Request pentru generarea unui cod de linking"""
|
||||
telegram_user_id: int = Field(description="ID-ul utilizatorului Telegram")
|
||||
telegram_username: Optional[str] = Field(default=None, description="Username-ul Telegram")
|
||||
telegram_first_name: Optional[str] = Field(default=None, description="Prenumele utilizatorului")
|
||||
telegram_last_name: Optional[str] = Field(default=None, description="Numele utilizatorului")
|
||||
|
||||
|
||||
class GenerateCodeResponse(BaseModel):
|
||||
"""Response pentru generarea unui cod de linking"""
|
||||
linking_code: str = Field(description="Codul de linking generat (8 caractere)")
|
||||
expires_at: datetime = Field(description="Data și ora expirării codului")
|
||||
expires_in_minutes: int = Field(description="Minutele până la expirare")
|
||||
|
||||
|
||||
class VerifyUserRequest(BaseModel):
|
||||
"""
|
||||
Request pentru verificarea utilizatorului în Oracle
|
||||
|
||||
Suportă 2 flow-uri:
|
||||
1. Auto-linking (recomandat): doar linking_code și oracle_username
|
||||
- Bot-ul verifică codul în SQLite, extrage oracle_username
|
||||
- Backend face lookup în Oracle fără verificare parolă
|
||||
- Codul valid este proof-of-authorization
|
||||
|
||||
2. Full verification (opțional): username, password, linking_code
|
||||
- Verificare completă cu parolă în Oracle
|
||||
"""
|
||||
linking_code: str = Field(description="Codul de linking de la /generate-code")
|
||||
oracle_username: Optional[str] = Field(default=None, description="Username Oracle (pentru auto-linking)")
|
||||
username: Optional[str] = Field(default=None, description="Username pentru verificare completă")
|
||||
password: Optional[str] = Field(default=None, description="Parolă pentru verificare completă")
|
||||
|
||||
|
||||
class VerifyUserResponse(BaseModel):
|
||||
"""Response pentru verificarea utilizatorului"""
|
||||
success: bool = Field(description="True dacă verificarea a avut succes")
|
||||
access_token: Optional[str] = Field(default=None, description="JWT access token")
|
||||
refresh_token: Optional[str] = Field(default=None, description="JWT refresh token")
|
||||
user: Optional[Dict[str, Any]] = Field(default=None, description="Detalii utilizator")
|
||||
message: str = Field(description="Mesaj de status")
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
"""Request pentru refresh JWT token"""
|
||||
refresh_token: str = Field(description="Refresh token-ul obținut la autentificare")
|
||||
|
||||
|
||||
class RefreshTokenResponse(BaseModel):
|
||||
"""Response pentru refresh token"""
|
||||
access_token: str = Field(description="Noul JWT access token")
|
||||
expires_in: int = Field(description="Timpul de expirare în secunde")
|
||||
token_type: str = Field(default="bearer", description="Tipul token-ului")
|
||||
|
||||
|
||||
class ExportReportRequest(BaseModel):
|
||||
"""Request pentru exportul unui raport"""
|
||||
company_id: int = Field(description="ID-ul firmei")
|
||||
report_type: str = Field(description="Tipul raportului (invoices, payments, dashboard)")
|
||||
format: str = Field(default="excel", description="Formatul exportului (excel, pdf, csv)")
|
||||
filters: Optional[Dict[str, Any]] = Field(default=None, description="Filtre pentru raport")
|
||||
|
||||
|
||||
class ExportReportResponse(BaseModel):
|
||||
"""Response pentru exportul raportului"""
|
||||
success: bool = Field(description="True dacă exportul a avut succes")
|
||||
file_url: Optional[str] = Field(default=None, description="URL-ul fișierului generat")
|
||||
file_name: Optional[str] = Field(default=None, description="Numele fișierului generat")
|
||||
file_size_bytes: Optional[int] = Field(default=None, description="Mărimea fișierului în bytes")
|
||||
message: str = Field(description="Mesaj de status")
|
||||
|
||||
|
||||
# ==================== Helper Functions ====================
|
||||
|
||||
def generate_linking_code(length: int = 8) -> str:
|
||||
"""
|
||||
Generează un cod alfanumeric aleatoriu pentru linking
|
||||
|
||||
Args:
|
||||
length: Lungimea codului (default: 8)
|
||||
|
||||
Returns:
|
||||
Codul generat (uppercase alphanumeric)
|
||||
"""
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
# Exclude caractere care pot fi confundate: 0, O, I, 1
|
||||
alphabet = alphabet.replace('0', '').replace('O', '').replace('I', '').replace('1', '')
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Obține informații despre utilizator din Oracle FĂRĂ verificare parolă.
|
||||
|
||||
Folosit pentru auto-linking când utilizatorul a fost deja autentificat
|
||||
prin generarea unui linking code valid în aplicația web.
|
||||
|
||||
Args:
|
||||
username: Username-ul utilizatorului Oracle
|
||||
|
||||
Returns:
|
||||
Dict cu informații despre utilizator sau None dacă nu există
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține detalii utilizator
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
return None
|
||||
|
||||
user_id = user_row[0]
|
||||
actual_username = user_row[1]
|
||||
|
||||
# Obține companiile utilizatorului
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2
|
||||
AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
companies_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
return {
|
||||
'user_id': user_id,
|
||||
'username': actual_username,
|
||||
'companies': companies,
|
||||
'permissions': ['read', 'reports']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting Oracle user by username: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def verify_oracle_user(username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verifică utilizatorul în Oracle folosind pack_drepturi.verificautilizator
|
||||
|
||||
Args:
|
||||
username: Username-ul utilizatorului
|
||||
password: Parola utilizatorului
|
||||
|
||||
Returns:
|
||||
Dict cu informații despre utilizator sau None dacă verificarea eșuează
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Verifică autentificarea
|
||||
cursor.execute("""
|
||||
SELECT pack_drepturi.verificautilizator(:username, :password)
|
||||
FROM DUAL
|
||||
""", {
|
||||
'username': username.upper(),
|
||||
'password': password
|
||||
})
|
||||
|
||||
result = cursor.fetchone()
|
||||
verification_result = result[0] if result else -1
|
||||
|
||||
if verification_result == -1:
|
||||
return None
|
||||
|
||||
# Obține detalii utilizator
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
return None
|
||||
|
||||
user_id = user_row[0]
|
||||
|
||||
# Obține companiile utilizatorului
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2
|
||||
AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
companies_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
return {
|
||||
'user_id': user_id,
|
||||
'username': username,
|
||||
'companies': companies,
|
||||
'permissions': ['read', 'reports']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error verifying Oracle user: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Endpoints ====================
|
||||
|
||||
@router.post("/auth/generate-code", response_model=GenerateCodeResponse)
|
||||
async def generate_linking_code_endpoint(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Generează un cod de linking pentru conectarea unui utilizator Telegram
|
||||
|
||||
Flow:
|
||||
1. Utilizatorul autentificat în aplicație solicită un cod
|
||||
2. Se generează un cod unic de 8 caractere
|
||||
3. Codul este trimis la Telegram bot pentru salvare în SQLite cu TTL de 15 minute
|
||||
4. Utilizatorul introduce codul în Telegram bot pentru linking
|
||||
|
||||
Note:
|
||||
- Acest endpoint necesită autentificare JWT (utilizatorul trebuie să fie logat în aplicație)
|
||||
- Codul expiră după 15 minute
|
||||
- Fiecare request generează un cod nou (codurile vechi devin invalide)
|
||||
- Nu este nevoie de telegram_user_id în acest moment (utilizatorul nu e încă conectat la Telegram)
|
||||
"""
|
||||
try:
|
||||
# Generează cod unic
|
||||
linking_code = generate_linking_code()
|
||||
|
||||
# Setează expirarea la 15 minute
|
||||
expires_at = datetime.utcnow() + timedelta(minutes=15)
|
||||
expires_in_minutes = 15
|
||||
|
||||
# Salvează codul în database-ul Telegram bot (SQLite) via internal API
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
save_code_response = await client.post(
|
||||
f"{TELEGRAM_BOT_INTERNAL_API}/internal/save-code",
|
||||
json={
|
||||
"code": linking_code,
|
||||
"telegram_user_id": 0, # Not known yet (user hasn't linked)
|
||||
"oracle_username": current_user.username,
|
||||
"expires_in_minutes": expires_in_minutes
|
||||
}
|
||||
)
|
||||
|
||||
# Accept both 200 (OK) and 201 (Created) as success
|
||||
if save_code_response.status_code not in [200, 201]:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to save code to Telegram bot: {save_code_response.text}"
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Telegram bot service is not responding. Please try again later."
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Cannot connect to Telegram bot service. Please contact administrator."
|
||||
)
|
||||
|
||||
return GenerateCodeResponse(
|
||||
linking_code=linking_code,
|
||||
expires_at=expires_at,
|
||||
expires_in_minutes=expires_in_minutes
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la generarea codului de linking: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/verify-user", response_model=VerifyUserResponse)
|
||||
async def verify_user_endpoint(request: VerifyUserRequest):
|
||||
"""
|
||||
Verifică utilizatorul în Oracle și returnează JWT tokens
|
||||
|
||||
Suportă 2 flow-uri de autentificare:
|
||||
|
||||
Flow A - Auto-linking (RECOMANDAT):
|
||||
1. Bot verifică linking_code în SQLite (code valid = user s-a autentificat în web app)
|
||||
2. Bot extrage oracle_username din cod
|
||||
3. Bot trimite: {linking_code, oracle_username}
|
||||
4. Backend face lookup în Oracle (FĂRĂ verificare parolă)
|
||||
5. Backend generează și returnează JWT tokens
|
||||
|
||||
Flow B - Full verification (OPȚIONAL):
|
||||
1. Bot cere username și parolă de la user în Telegram
|
||||
2. Bot trimite: {linking_code, username, password}
|
||||
3. Backend verifică credențialele în Oracle
|
||||
4. Backend generează și returnează JWT tokens
|
||||
|
||||
Note:
|
||||
- Acest endpoint NU necesită autentificare JWT (este public pentru bot)
|
||||
- Flow A oferă UX superior (fără re-introducere parolă)
|
||||
- Linking code-ul valid este proof-of-authorization
|
||||
"""
|
||||
try:
|
||||
# Flow A: Auto-linking (oracle_username provided, no password)
|
||||
if request.oracle_username and not request.password:
|
||||
user_data = await get_oracle_user_by_username(request.oracle_username)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message=f"Utilizatorul {request.oracle_username} nu există în Oracle"
|
||||
)
|
||||
|
||||
# Flow B: Full verification (username + password provided)
|
||||
elif request.username and request.password:
|
||||
user_data = await verify_oracle_user(request.username, request.password)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message="Username sau parolă incorectă"
|
||||
)
|
||||
|
||||
# Invalid request (missing required fields)
|
||||
else:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message="Trebuie furnizat fie oracle_username (auto-linking) fie username+password (verificare completă)"
|
||||
)
|
||||
|
||||
# Generează JWT tokens
|
||||
access_token = jwt_handler.create_access_token(
|
||||
username=user_data['username'],
|
||||
companies=user_data['companies'],
|
||||
user_id=user_data['user_id'],
|
||||
permissions=user_data['permissions']
|
||||
)
|
||||
|
||||
refresh_token = jwt_handler.create_refresh_token(
|
||||
username=user_data['username'],
|
||||
user_id=user_data['user_id']
|
||||
)
|
||||
|
||||
return VerifyUserResponse(
|
||||
success=True,
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user={
|
||||
'user_id': user_data['user_id'],
|
||||
'username': user_data['username'],
|
||||
'companies': user_data['companies'],
|
||||
'permissions': user_data['permissions']
|
||||
},
|
||||
message="Autentificare reușită"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la verificarea utilizatorului: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/refresh-token", response_model=RefreshTokenResponse)
|
||||
async def refresh_token_endpoint(request: RefreshTokenRequest):
|
||||
"""
|
||||
Refresh-uiește un JWT access token folosind refresh token-ul
|
||||
|
||||
Acest endpoint este folosit de Telegram bot pentru a obține un nou access token
|
||||
când cel curent expiră, fără a solicita din nou username/password.
|
||||
|
||||
Flow:
|
||||
1. Botul Telegram detectează că access token-ul a expirat
|
||||
2. Trimite refresh token-ul la acest endpoint
|
||||
3. Se validează refresh token-ul și se generează un nou access token
|
||||
4. Botul stochează noul access token în SQLite
|
||||
|
||||
Note:
|
||||
- Refresh token-ul este valid 7 zile (vs 30 minute pentru access token)
|
||||
- Dacă refresh token-ul expiră, utilizatorul trebuie să se re-autentifice
|
||||
"""
|
||||
try:
|
||||
# Verifică refresh token-ul
|
||||
token_data = jwt_handler.verify_token(request.refresh_token)
|
||||
|
||||
if not token_data or token_data.token_type != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Refresh token invalid sau expirat"
|
||||
)
|
||||
|
||||
# Obține companiile actualizate din Oracle
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2
|
||||
AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': token_data.user_id})
|
||||
|
||||
companies_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
# Generează nou access token
|
||||
new_access_token = jwt_handler.create_access_token(
|
||||
username=token_data.username,
|
||||
companies=companies,
|
||||
user_id=token_data.user_id,
|
||||
permissions=token_data.permissions
|
||||
)
|
||||
|
||||
return RefreshTokenResponse(
|
||||
access_token=new_access_token,
|
||||
expires_in=jwt_handler.access_token_expire_minutes * 60,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la refresh token: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/export", response_model=ExportReportResponse)
|
||||
async def export_report_endpoint(
|
||||
request: ExportReportRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Exportă un raport în format Excel, PDF sau CSV
|
||||
|
||||
Acest endpoint este folosit de Telegram bot pentru a genera rapoarte
|
||||
și a le trimite utilizatorului.
|
||||
|
||||
Flow:
|
||||
1. Botul trimite cerere de export cu parametrii raportului
|
||||
2. Se validează că utilizatorul are acces la firma specificată
|
||||
3. Se generează raportul în formatul solicitat
|
||||
4. Se returnează URL-ul sau conținutul fișierului
|
||||
|
||||
Tipuri de rapoarte suportate:
|
||||
- invoices: Facturi (cu filtre: dată, status, client)
|
||||
- payments: Încasări (cu filtre: dată, metodă plată)
|
||||
- dashboard: Statistici dashboard (rezumat)
|
||||
|
||||
Formate suportate:
|
||||
- excel: XLSX (cel mai complet)
|
||||
- pdf: PDF (pentru printing)
|
||||
- csv: CSV (pentru import în alte sisteme)
|
||||
|
||||
Note:
|
||||
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||
- Fișierele generate sunt temporare (șterse după 1 oră)
|
||||
"""
|
||||
try:
|
||||
# Verifică accesul la firmă
|
||||
company_id_str = str(request.company_id)
|
||||
if company_id_str not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Nu aveți acces la firma {request.company_id}"
|
||||
)
|
||||
|
||||
# TODO: Implementare export în funcție de report_type și format
|
||||
# Deocamdată returnăm un placeholder
|
||||
|
||||
return ExportReportResponse(
|
||||
success=True,
|
||||
file_url=f"/api/telegram/downloads/report_{request.report_type}_{request.company_id}.{request.format}",
|
||||
file_name=f"raport_{request.report_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{request.format}",
|
||||
file_size_bytes=0,
|
||||
message=f"Raport {request.report_type} generat cu succes în format {request.format}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la generarea raportului: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def telegram_health_check():
|
||||
"""
|
||||
Health check pentru routerul Telegram
|
||||
Verifică conectivitatea la Oracle și disponibilitatea serviciilor
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "telegram-router",
|
||||
"database": "connected",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "degraded",
|
||||
"service": "telegram-router",
|
||||
"database": f"error: {str(e)}",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
67
reports-app/backend/app/routers/treasury.py
Normal file
67
reports-app/backend/app/routers/treasury.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from ..models.treasury import RegisterFilter, RegisterListResponse
|
||||
from ..services.treasury_service import TreasuryService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
||||
async def get_bank_cash_register(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține registrul de casă și bancă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Suportă filtrare și paginare
|
||||
"""
|
||||
try:
|
||||
# Verifică dacă utilizatorul are acces la firma specificată
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
# Convertește datele
|
||||
date_from_obj = None
|
||||
date_to_obj = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = date.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată început invalid")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_obj = date.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată sfârșit invalid")
|
||||
|
||||
filter_params = RegisterFilter(
|
||||
company=company,
|
||||
date_from=date_from_obj,
|
||||
date_to=date_to_obj,
|
||||
partner_name=partner_name,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea registrului: {str(e)}")
|
||||
0
reports-app/backend/app/schemas/__init__.py
Normal file
0
reports-app/backend/app/schemas/__init__.py
Normal file
0
reports-app/backend/app/services/__init__.py
Normal file
0
reports-app/backend/app/services/__init__.py
Normal file
1842
reports-app/backend/app/services/dashboard_service.py
Normal file
1842
reports-app/backend/app/services/dashboard_service.py
Normal file
File diff suppressed because it is too large
Load Diff
268
reports-app/backend/app/services/invoice_service.py
Normal file
268
reports-app/backend/app/services/invoice_service.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Service pentru logica facturi - Portează query-urile din aplicația Flask
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from database.oracle_pool import oracle_pool
|
||||
from typing import List, Tuple
|
||||
from ..models.invoice import Invoice, InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InvoiceService:
|
||||
"""Service pentru gestionarea facturilor"""
|
||||
|
||||
@staticmethod
|
||||
async def get_invoices(filter_params: InvoiceFilter, username: str) -> InvoiceListResponse:
|
||||
"""
|
||||
Obține lista de facturi - Query simplu pentru afișare în tabel
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema din v_nom_firme bazat pe id_firma
|
||||
company_id = int(filter_params.company)
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
cursor.execute(schema_query, {'company_id': company_id})
|
||||
schema_result = cursor.fetchone()
|
||||
|
||||
if not schema_result:
|
||||
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
|
||||
|
||||
schema = schema_result[0]
|
||||
|
||||
# Determină conturile în funcție de partner_type
|
||||
if filter_params.partner_type == "CLIENTI":
|
||||
conturi = "'4111', '461'"
|
||||
elif filter_params.partner_type == "FURNIZORI":
|
||||
conturi = "'401', '404', '462'"
|
||||
else:
|
||||
conturi = "'4111'" # default
|
||||
|
||||
# Query cu calculele corecte pentru solduri
|
||||
base_query = f"""
|
||||
SELECT
|
||||
vp.NUME,
|
||||
vp.NRACT,
|
||||
vp.DATAACT,
|
||||
vp.DATASCAD,
|
||||
vp.CONTRACT,
|
||||
vp.COD_FISCAL,
|
||||
vp.REG_COMERT,
|
||||
CASE
|
||||
WHEN vp.CONT IN ('4111','461') THEN vp.PRECDEB + vp.DEBIT -- Total facturat clienți
|
||||
WHEN vp.CONT IN ('401','404','462') THEN vp.PRECCRED + vp.CREDIT -- Total facturat furnizori
|
||||
END as total_facturat,
|
||||
CASE
|
||||
WHEN vp.CONT IN ('4111','461') THEN vp.PRECCRED + vp.CREDIT -- Încasat clienți
|
||||
WHEN vp.CONT IN ('401','404','462') THEN vp.PRECDEB + vp.DEBIT -- Achitat furnizori
|
||||
END as achitat,
|
||||
CASE
|
||||
WHEN vp.CONT IN ('4111','461') THEN
|
||||
(vp.PRECDEB + vp.DEBIT) - (vp.PRECCRED + vp.CREDIT) -- Sold clienți
|
||||
WHEN vp.CONT IN ('401','404','462') THEN
|
||||
(vp.PRECCRED + vp.CREDIT) - (vp.PRECDEB + vp.DEBIT) -- Sold furnizori
|
||||
END as sold,
|
||||
vp.CONT,
|
||||
CASE
|
||||
WHEN vp.DATASCAD < SYSDATE THEN 'restant'
|
||||
ELSE 'in_termen'
|
||||
END as status
|
||||
FROM {schema}.vireg_parteneri vp
|
||||
WHERE vp.an = (SELECT anul FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar))
|
||||
AND vp.luna = (SELECT luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar))
|
||||
AND (
|
||||
(:partner_type = 'CLIENTI' AND vp.cont IN ('4111', '461'))
|
||||
OR
|
||||
(:partner_type = 'FURNIZORI' AND vp.cont IN ('401', '404', '462'))
|
||||
)
|
||||
"""
|
||||
|
||||
params = {'partner_type': filter_params.partner_type}
|
||||
|
||||
# Adaugă filtre dinamice
|
||||
if filter_params.date_from:
|
||||
base_query += " AND vp.dataact >= :date_from"
|
||||
params['date_from'] = filter_params.date_from
|
||||
|
||||
if filter_params.date_to:
|
||||
base_query += " AND vp.dataact <= :date_to"
|
||||
params['date_to'] = filter_params.date_to
|
||||
|
||||
if filter_params.partner_name:
|
||||
base_query += " AND UPPER(vp.nume) LIKE UPPER(:partner_name)"
|
||||
params['partner_name'] = f"%{filter_params.partner_name}%"
|
||||
|
||||
if filter_params.min_amount:
|
||||
base_query += " AND total_facturat >= :min_amount"
|
||||
params['min_amount'] = filter_params.min_amount
|
||||
|
||||
if filter_params.max_amount:
|
||||
base_query += " AND total_facturat <= :max_amount"
|
||||
params['max_amount'] = filter_params.max_amount
|
||||
|
||||
if filter_params.only_unpaid:
|
||||
# Nu putem folosi aliasul "sold" în WHERE în Oracle, trebuie să repetăm calculul
|
||||
base_query += """ AND (
|
||||
CASE
|
||||
WHEN vp.CONT IN ('4111','461') THEN
|
||||
(vp.PRECDEB + vp.DEBIT) - (vp.PRECCRED + vp.CREDIT)
|
||||
WHEN vp.CONT IN ('401','404','462') THEN
|
||||
(vp.PRECCRED + vp.CREDIT) - (vp.PRECDEB + vp.DEBIT)
|
||||
END
|
||||
) > 0"""
|
||||
|
||||
# Count total pentru paginare
|
||||
count_query = f"SELECT COUNT(*) FROM ({base_query})"
|
||||
cursor.execute(count_query, params)
|
||||
total_count = cursor.fetchone()[0]
|
||||
|
||||
# Adaugă ORDER BY și paginare
|
||||
base_query += " ORDER BY vp.DATAACT DESC, vp.NUME, vp.NRACT"
|
||||
|
||||
# Paginare Oracle
|
||||
offset = (filter_params.page - 1) * filter_params.page_size
|
||||
limit = offset + filter_params.page_size
|
||||
paginated_query = f"""
|
||||
SELECT * FROM (
|
||||
SELECT ROWNUM as rn, t.* FROM ({base_query}) t WHERE ROWNUM <= :limit
|
||||
) WHERE rn > :offset
|
||||
"""
|
||||
params['offset'] = offset
|
||||
params['limit'] = limit
|
||||
|
||||
cursor.execute(paginated_query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Procesează rezultatele cu structura nouă
|
||||
invoices = []
|
||||
total_amount = Decimal('0.00')
|
||||
|
||||
for row in rows:
|
||||
# Skip ROWNUM, extrage valorile din query-ul nou
|
||||
nume = row[1]
|
||||
nract = row[2]
|
||||
dataact = row[3]
|
||||
datascad = row[4]
|
||||
contract = row[5]
|
||||
cod_fiscal = row[6]
|
||||
reg_comert = row[7]
|
||||
total_facturat = Decimal(str(row[8] or 0))
|
||||
achitat = Decimal(str(row[9] or 0))
|
||||
sold = Decimal(str(row[10] or 0))
|
||||
cont = row[11]
|
||||
status = row[12]
|
||||
|
||||
invoice_data = {
|
||||
'nume': nume or '',
|
||||
'nract': nract or 0,
|
||||
'dataact': dataact,
|
||||
'datascad': datascad,
|
||||
'contract': contract,
|
||||
'cod_fiscal': cod_fiscal,
|
||||
'reg_comert': reg_comert,
|
||||
'totctva': total_facturat,
|
||||
'achitat': achitat,
|
||||
'soldfinal': sold
|
||||
}
|
||||
|
||||
invoice = Invoice(**invoice_data)
|
||||
invoices.append(invoice)
|
||||
total_amount += total_facturat
|
||||
|
||||
return InvoiceListResponse(
|
||||
invoices=invoices,
|
||||
total_count=total_count,
|
||||
filtered_count=len(invoices),
|
||||
total_amount=total_amount,
|
||||
page=filter_params.page,
|
||||
page_size=filter_params.page_size,
|
||||
has_more=len(invoices) == filter_params.page_size
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_invoice_details(company: str, invoice_number: str, username: str) -> Invoice:
|
||||
"""
|
||||
Obține detaliile unei facturi specifice
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema din v_nom_firme bazat pe id_firma
|
||||
company_id = int(company)
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
cursor.execute(schema_query, {'company_id': company_id})
|
||||
schema_result = cursor.fetchone()
|
||||
|
||||
if not schema_result:
|
||||
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
|
||||
|
||||
schema = schema_result[0]
|
||||
|
||||
# Query simplu pentru detalii factură
|
||||
detail_query = f"""
|
||||
SELECT
|
||||
NUME,
|
||||
NRACT,
|
||||
DATAACT,
|
||||
DATASCAD,
|
||||
CONTRACT,
|
||||
COD_FISCAL,
|
||||
REG_COMERT,
|
||||
PRECDEB,
|
||||
PRECCRED,
|
||||
DEBIT,
|
||||
CREDIT,
|
||||
CONT
|
||||
FROM {schema}.vireg_parteneri
|
||||
WHERE nract = :invoice_number
|
||||
AND an = (select anul from {schema}.calendar where anul*12+luna = (select max(anul*12+luna) as anmax from {schema}.calendar))
|
||||
AND luna = (select luna from {schema}.calendar where anul*12+luna = (select max(anul*12+luna) as anmax from {schema}.calendar))
|
||||
"""
|
||||
|
||||
cursor.execute(detail_query, {'invoice_number': invoice_number})
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise ValueError(f"Factura {invoice_number} nu a fost găsită")
|
||||
|
||||
# Extrage valorile
|
||||
nume = row[0]
|
||||
nract = row[1]
|
||||
dataact = row[2]
|
||||
datascad = row[3]
|
||||
contract = row[4]
|
||||
cod_fiscal = row[5]
|
||||
reg_comert = row[6]
|
||||
precdeb = Decimal(str(row[7] or 0))
|
||||
preccred = Decimal(str(row[8] or 0))
|
||||
debit = Decimal(str(row[9] or 0))
|
||||
credit = Decimal(str(row[10] or 0))
|
||||
cont = row[11]
|
||||
|
||||
# Calculează valorile în funcție de tipul contului
|
||||
if cont in ('4111', '461'): # CLIENTI
|
||||
totctva = precdeb + debit
|
||||
achitat = preccred + credit
|
||||
soldfinal = precdeb - preccred + debit - credit
|
||||
else: # FURNIZORI
|
||||
totctva = preccred + credit
|
||||
achitat = precdeb + debit
|
||||
soldfinal = preccred - precdeb + credit - debit
|
||||
|
||||
invoice_data = {
|
||||
'nume': nume or '',
|
||||
'nract': nract or 0,
|
||||
'dataact': dataact,
|
||||
'datascad': datascad,
|
||||
'contract': contract,
|
||||
'cod_fiscal': cod_fiscal,
|
||||
'reg_comert': reg_comert,
|
||||
'totctva': totctva,
|
||||
'achitat': achitat,
|
||||
'soldfinal': soldfinal
|
||||
}
|
||||
|
||||
return Invoice(**invoice_data)
|
||||
161
reports-app/backend/app/services/treasury_service.py
Normal file
161
reports-app/backend/app/services/treasury_service.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from database.oracle_pool import oracle_pool
|
||||
from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TreasuryService:
|
||||
"""Service pentru trezorerie - registru casă și bancă"""
|
||||
|
||||
@staticmethod
|
||||
async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse:
|
||||
"""
|
||||
Obține registrul de casă și bancă din vbancasa views
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = int(filter_params.company)
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
cursor.execute(schema_query, {'company_id': company_id})
|
||||
schema_result = cursor.fetchone()
|
||||
|
||||
if not schema_result:
|
||||
raise ValueError(f"Schema nu a fost găsită pentru id_firma {company_id}")
|
||||
|
||||
schema = schema_result[0]
|
||||
|
||||
# Query pentru registrele de bancă și casă
|
||||
union_queries = []
|
||||
|
||||
# BANCA LEI (5121)
|
||||
union_queries.append(f"""
|
||||
SELECT
|
||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
||||
vb.incasari, vb.plati,
|
||||
vb.incasari - vb.plati as sold,
|
||||
'RON' as valuta,
|
||||
'BANCA LEI' as tip_registru,
|
||||
vb.explicatia
|
||||
FROM {schema}.vbancasa_5121_cum vb
|
||||
WHERE (vb.incasari > 0 OR vb.plati > 0)
|
||||
""")
|
||||
|
||||
# BANCA VALUTA (5124)
|
||||
union_queries.append(f"""
|
||||
SELECT
|
||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
||||
vb.incasval, vb.platival,
|
||||
vb.incasval - vb.platival as sold,
|
||||
COALESCE(vb.numeval, 'EUR') as valuta,
|
||||
'BANCA VALUTA' as tip_registru,
|
||||
vb.explicatia
|
||||
FROM {schema}.vbancasa_5124_cum vb
|
||||
WHERE (vb.incasval > 0 OR vb.platival > 0)
|
||||
""")
|
||||
|
||||
# CASA LEI (5311)
|
||||
union_queries.append(f"""
|
||||
SELECT
|
||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
||||
vb.incasari, vb.plati,
|
||||
vb.incasari - vb.plati as sold,
|
||||
'RON' as valuta,
|
||||
'CASA LEI' as tip_registru,
|
||||
vb.explicatia
|
||||
FROM {schema}.vbancasa_5311_cum vb
|
||||
WHERE (vb.incasari > 0 OR vb.plati > 0)
|
||||
""")
|
||||
|
||||
# CASA VALUTA (5314)
|
||||
union_queries.append(f"""
|
||||
SELECT
|
||||
vb.nume, vb.nract, vb.dataact, vb.bancasa,
|
||||
vb.incasval, vb.platival,
|
||||
vb.incasval - vb.platival as sold,
|
||||
COALESCE(vb.numeval, 'EUR') as valuta,
|
||||
'CASA VALUTA' as tip_registru,
|
||||
vb.explicatia
|
||||
FROM {schema}.vbancasa_5314_cum vb
|
||||
WHERE (vb.incasval > 0 OR vb.platival > 0)
|
||||
""")
|
||||
|
||||
base_query = " UNION ALL ".join(union_queries)
|
||||
|
||||
params = {}
|
||||
where_conditions = []
|
||||
|
||||
if filter_params.date_from:
|
||||
where_conditions.append("dataact >= :date_from")
|
||||
params['date_from'] = filter_params.date_from
|
||||
|
||||
if filter_params.date_to:
|
||||
where_conditions.append("dataact <= :date_to")
|
||||
params['date_to'] = filter_params.date_to
|
||||
|
||||
if filter_params.partner_name:
|
||||
where_conditions.append("UPPER(nume) LIKE UPPER(:partner_name)")
|
||||
params['partner_name'] = f"%{filter_params.partner_name}%"
|
||||
|
||||
if where_conditions:
|
||||
base_query = f"SELECT * FROM ({base_query}) WHERE {' AND '.join(where_conditions)}"
|
||||
|
||||
# Count pentru paginare
|
||||
count_query = f"SELECT COUNT(*) FROM ({base_query})"
|
||||
cursor.execute(count_query, params)
|
||||
total_count = cursor.fetchone()[0]
|
||||
|
||||
# Query cu paginare
|
||||
base_query += " ORDER BY dataact DESC, nract"
|
||||
|
||||
offset = (filter_params.page - 1) * filter_params.page_size
|
||||
limit = offset + filter_params.page_size
|
||||
paginated_query = f"""
|
||||
SELECT * FROM (
|
||||
SELECT ROWNUM as rn, t.* FROM ({base_query}) t WHERE ROWNUM <= :limit
|
||||
) WHERE rn > :offset
|
||||
"""
|
||||
params['offset'] = offset
|
||||
params['limit'] = limit
|
||||
|
||||
cursor.execute(paginated_query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Procesare rezultate
|
||||
registers = []
|
||||
total_incasari = Decimal('0.00')
|
||||
total_plati = Decimal('0.00')
|
||||
|
||||
for row in rows:
|
||||
# Skip ROWNUM
|
||||
register_data = BankCashRegister(
|
||||
nume=row[1] or '',
|
||||
nract=row[2] or 0,
|
||||
dataact=row[3],
|
||||
nume_cont_bancar=row[4] or '',
|
||||
incasari=Decimal(str(row[5] or 0)),
|
||||
plati=Decimal(str(row[6] or 0)),
|
||||
sold=Decimal(str(row[7] or 0)),
|
||||
valuta=row[8],
|
||||
tip_registru=row[9],
|
||||
explicatia=row[10] or ''
|
||||
)
|
||||
registers.append(register_data)
|
||||
total_incasari += register_data.incasari
|
||||
total_plati += register_data.plati
|
||||
|
||||
return RegisterListResponse(
|
||||
registers=registers,
|
||||
total_count=total_count,
|
||||
filtered_count=len(registers),
|
||||
total_incasari=total_incasari,
|
||||
total_plati=total_plati,
|
||||
page=filter_params.page,
|
||||
page_size=filter_params.page_size,
|
||||
has_more=len(registers) == filter_params.page_size
|
||||
)
|
||||
13
reports-app/backend/requirements.txt
Normal file
13
reports-app/backend/requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
python-multipart>=0.0.6
|
||||
pydantic>=2.5.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
PyJWT>=2.8.0
|
||||
python-decouple>=3.8
|
||||
oracledb>=1.4.0
|
||||
python-dateutil>=2.8.2
|
||||
openpyxl>=3.1.0
|
||||
fpdf2>=2.7.0
|
||||
email-validator>=2.0.0
|
||||
httpx>=0.27.0
|
||||
36
reports-app/frontend/.eslintrc.cjs
Normal file
36
reports-app/frontend/.eslintrc.cjs
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2022: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.spec.js', '**/tests/**/*.js'],
|
||||
env: {
|
||||
jest: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
test: 'readonly',
|
||||
expect: 'readonly',
|
||||
describe: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
afterEach: 'readonly'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
64
reports-app/frontend/.gitignore
vendored
Normal file
64
reports-app/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Production builds
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Test output
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright-report-integration/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Test artifacts and debugging files
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.webm
|
||||
*.mp4
|
||||
*.har
|
||||
trace*.zip
|
||||
*-debug.json
|
||||
*-report.json
|
||||
*-report.xml
|
||||
debug-*.png
|
||||
journey-*.png
|
||||
button-debug.png
|
||||
responsive-*.png
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
*.lcov
|
||||
.nyc_output/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.log
|
||||
231
reports-app/frontend/ANDROID_QUICK_START.md
Normal file
231
reports-app/frontend/ANDROID_QUICK_START.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Android Testing - Quick Start (5 Minutes)
|
||||
|
||||
Ghid rapid pentru testarea ROA2WEB pe telefon Android real prin ADB WiFi.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows 10/11
|
||||
- Android phone (Android 10+)
|
||||
- Phone and computer on same WiFi network
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Install ADB on Windows (2 min)
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Install ADB Platform Tools
|
||||
winget install Google.PlatformTools
|
||||
|
||||
# Verify installation
|
||||
adb version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Enable Wireless Debugging on Phone (1 min)
|
||||
|
||||
**On Android phone:**
|
||||
|
||||
```
|
||||
Settings -> About phone -> Tap "Build number" 7 times
|
||||
Settings -> Developer options -> Enable "USB debugging"
|
||||
Settings -> Developer options -> Enable "Wireless debugging"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Connect Phone via WiFi (1 min)
|
||||
|
||||
**On phone:**
|
||||
```
|
||||
Settings -> Developer options -> Wireless debugging -> "Pair device with pairing code"
|
||||
```
|
||||
|
||||
You'll see:
|
||||
- **Pairing code:** 6 digits (e.g., 123456)
|
||||
- **IP & Port:** e.g., 10.0.20.114:37639
|
||||
|
||||
**In Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Pair (first time only)
|
||||
adb pair 10.0.20.114:37639
|
||||
# Enter the 6-digit code when prompted
|
||||
|
||||
# After pairing, check the MAIN port in "Wireless debugging" screen
|
||||
# It's different from pairing port!
|
||||
|
||||
# Connect (use the wireless debugging port, NOT pairing port)
|
||||
adb connect 10.0.20.114:XXXXX
|
||||
|
||||
# Verify
|
||||
adb devices
|
||||
# Should see: 10.0.20.114:XXXXX device
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Setup Port Forwarding (1 min)
|
||||
|
||||
**Windows PowerShell (Administrator):**
|
||||
|
||||
```powershell
|
||||
# A) ADB port forwarding (Chrome DevTools)
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote
|
||||
|
||||
# B) Windows port proxy (for WSL/MCP access)
|
||||
netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=127.0.0.1
|
||||
|
||||
# C) Firewall rule
|
||||
New-NetFirewallRule -DisplayName "Chrome-DevTools-Android" -Direction Inbound -LocalPort 9222 -Protocol TCP -Action Allow
|
||||
|
||||
# D) Port forwarding for app access from phone
|
||||
netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=172.18.251.234
|
||||
netsh interface portproxy add v4tov4 listenport=8001 listenaddress=0.0.0.0 connectport=8001 connectaddress=172.18.251.234
|
||||
|
||||
# E) Firewall rules for app
|
||||
New-NetFirewallRule -DisplayName "ROA2WEB-Frontend-WSL" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow
|
||||
New-NetFirewallRule -DisplayName "ROA2WEB-Backend-WSL" -Direction Inbound -LocalPort 8001 -Protocol TCP -Action Allow
|
||||
|
||||
# Verify
|
||||
netsh interface portproxy show all
|
||||
```
|
||||
|
||||
**OR use automated setup script:**
|
||||
|
||||
```powershell
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Test on Phone
|
||||
|
||||
**In WSL (start app):**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
**On phone Chrome:**
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Configure Chrome DevTools MCP (Optional)
|
||||
|
||||
For Claude Code to control Chrome on phone:
|
||||
|
||||
**Edit:** `~/.claude.json` (in WSL)
|
||||
|
||||
Find `chrome-devtools-android` and ensure it uses physical Windows IP:
|
||||
|
||||
```json
|
||||
"chrome-devtools-android": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browser-url",
|
||||
"http://10.0.20.144:9222"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Replace `10.0.20.144` with YOUR Windows IP:
|
||||
|
||||
```powershell
|
||||
# Get your Windows IP
|
||||
ipconfig | findstr "IPv4"
|
||||
```
|
||||
|
||||
**Reload Claude Code:** Ctrl+Shift+P -> "Developer: Reload Window"
|
||||
|
||||
---
|
||||
|
||||
## Daily Workflow
|
||||
|
||||
**Morning setup:**
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
adb connect 10.0.20.114:XXXXX # Your phone IP:PORT
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote
|
||||
```
|
||||
|
||||
**Start app (WSL):**
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
**On phone:** Open Chrome -> `http://localhost:3000`
|
||||
|
||||
**In Claude Code:** Ask for screenshots directly via MCP:
|
||||
```
|
||||
"Folosind chrome-devtools-android, fa screenshot de pe telefon"
|
||||
```
|
||||
|
||||
**End of day cleanup:**
|
||||
```bash
|
||||
# WSL
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/reports-app/frontend
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "adb: device unauthorized"
|
||||
- Unlock phone
|
||||
- Accept "Allow USB debugging" prompt
|
||||
- Check "Always allow from this computer"
|
||||
|
||||
### "Connection refused"
|
||||
- Phone and PC must be on SAME WiFi network
|
||||
- Restart "Wireless debugging" on phone
|
||||
- Re-pair and reconnect
|
||||
|
||||
### "localhost:3000 doesn't load on phone"
|
||||
Try Windows IP instead:
|
||||
```
|
||||
http://10.0.20.144:3000
|
||||
```
|
||||
|
||||
### "Chrome DevTools MCP doesn't connect"
|
||||
```bash
|
||||
# Test from WSL
|
||||
curl http://10.0.20.144:9222/json/version
|
||||
```
|
||||
|
||||
If fails, check Windows port proxy and firewall rules.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Component | Location | Command |
|
||||
|-----------|----------|---------|
|
||||
| **ADB Install** | Windows | `winget install Google.PlatformTools` |
|
||||
| **Connect Phone** | Windows | `adb connect IP:PORT` |
|
||||
| **Port Forwarding** | Windows Admin | `netsh interface portproxy ...` |
|
||||
| **Setup Script** | Windows | `.\android-test-setup.ps1` |
|
||||
| **Start App** | WSL | `./start-dev.sh` |
|
||||
| **Screenshot** | Claude Code | Ask via MCP (inline, no file save) |
|
||||
|
||||
---
|
||||
|
||||
**Your physical IP addresses (fill in):**
|
||||
- Windows IP: `___________________` (from `ipconfig`)
|
||||
- Phone IP: `___________________` (from Wireless debugging)
|
||||
- WSL IP for app: `172.18.251.234` (from `ip route show | grep default`)
|
||||
|
||||
---
|
||||
|
||||
For detailed guide, see: `tests/ANDROID_TESTING_GUIDE.md`
|
||||
55
reports-app/frontend/Dockerfile
Normal file
55
reports-app/frontend/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
# Multi-stage build for Vue.js frontend with Nginx
|
||||
# Stage 1: Build the Vue.js application
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies first for better caching
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy source code and build
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Serve with optimized Nginx
|
||||
FROM nginx:1.25-alpine as production
|
||||
|
||||
# Install security packages
|
||||
RUN apk add --no-cache \
|
||||
tini \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S appuser && \
|
||||
adduser -S -D -H -u 1001 -h /var/cache/nginx -s /sbin/nologin -G appuser appuser
|
||||
|
||||
# Copy built application from builder
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy custom Nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Fix permissions
|
||||
RUN chown -R appuser:appuser /var/cache/nginx && \
|
||||
chown -R appuser:appuser /var/log/nginx && \
|
||||
chown -R appuser:appuser /etc/nginx/conf.d && \
|
||||
touch /var/run/nginx.pid && \
|
||||
chown -R appuser:appuser /var/run/nginx.pid && \
|
||||
chown -R appuser:appuser /usr/share/nginx/html
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Use tini as init system
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
# Start Nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
202
reports-app/frontend/QUICK_START_TESTING.md
Normal file
202
reports-app/frontend/QUICK_START_TESTING.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 🚀 ROA2WEB Testing - Quick Start
|
||||
|
||||
Ghid rapid pentru rularea testelor Playwright cu detectarea problemelor reale.
|
||||
|
||||
## ⚡ Start Rapid
|
||||
|
||||
```bash
|
||||
# 1. Instalează dependențele (prima dată)
|
||||
npm install
|
||||
|
||||
# 2. Curăță fișierele temporare (recomandat)
|
||||
./cleanup-tests.sh
|
||||
|
||||
# 3. Rulează toate testele cu analiză completă
|
||||
./run-comprehensive-tests.sh
|
||||
|
||||
# 4. Pentru teste simple/CI
|
||||
./run-tests.sh
|
||||
```
|
||||
|
||||
## 📋 Scripturi Disponibile
|
||||
|
||||
| Script | Platformă | Descriere | Funcționalitate |
|
||||
|--------|-----------|-----------|------------------|
|
||||
| **`run-comprehensive-tests.sh`** | Linux/macOS/WSL | **🎯 RECOMANDAT** - Testare completă cu detecție probleme | Analiză avansată, recomandări, raportare detaliată |
|
||||
| `run-tests.sh` | Linux/macOS/WSL | Script Playwright standard | Testare de bază mock + integrare |
|
||||
| `cleanup-tests.sh` | Linux/macOS/WSL | Curățare fișiere temporare | Screenshots, videos, rapoarte |
|
||||
|
||||
## 🎯 Comenzi Esențiale
|
||||
|
||||
### 🚀 **Testare Comprehensivă (RECOMANDATĂ)**
|
||||
```bash
|
||||
./run-comprehensive-tests.sh # Toate testele + analiză completă
|
||||
./run-comprehensive-tests.sh --no-cleanup # Fără curățarea fișierelor
|
||||
./run-comprehensive-tests.sh --no-real-world # Doar teste de bază
|
||||
./run-comprehensive-tests.sh --help # Toate opțiunile
|
||||
```
|
||||
|
||||
### 🔧 **Testare Standard**
|
||||
```bash
|
||||
./run-tests.sh # Teste mock + integrare
|
||||
./run-tests.sh --no-mock # Doar teste de integrare
|
||||
./run-tests.sh --no-integration # Doar teste mock
|
||||
```
|
||||
|
||||
### 🧹 **Curățare & Management**
|
||||
```bash
|
||||
./cleanup-tests.sh # Curăță toate fișierele temporare
|
||||
./cleanup-tests.sh --deep # Curățare avansată + cache
|
||||
```
|
||||
|
||||
### 🔍 **Debug & Development**
|
||||
```bash
|
||||
npx playwright test auth/login.spec.js --headed # Login cu UI vizibil
|
||||
npx playwright test --debug # Debug interactiv
|
||||
npx playwright show-report # Afișează ultimul raport
|
||||
```
|
||||
|
||||
## 📊 Ce Testează
|
||||
|
||||
### 🎭 **Testare Comprehensivă**
|
||||
- ✅ **Authentication Flow** (10 teste): Login real, validare, token management
|
||||
- ✅ **Real-World Scenarios** (4 teste): Journey complet utilizator, performance
|
||||
- ✅ **Issue Detection** (4 teste): Debugging, network monitoring, CORS
|
||||
- ✅ **Reports Functionality** (2 teste): Companies, invoices, payments API
|
||||
- ✅ **Responsive Design** (4 teste): Mobile, tablet, desktop, orientări
|
||||
|
||||
### 🔧 **Testare Standard**
|
||||
- ✅ **Basic Auth** (10 teste): Login, validare, erori
|
||||
- ✅ **Dashboard** (8 teste): Companii, statistici, navigare
|
||||
- ✅ **Invoices** (10 teste): Liste, filtrare, export
|
||||
- ✅ **Payments** (12 teste): Încasări, totalizări, metodă
|
||||
- ✅ **Responsive** (9 teste): Breakpoint-uri, touch interactions
|
||||
|
||||
**Total: 70+ teste specifice + 200+ teste standard pe 5 browsere**
|
||||
|
||||
## 🔧 Setup Minim
|
||||
|
||||
```bash
|
||||
cd roa2web/reports-app/frontend
|
||||
npm install # Instalează dependențele
|
||||
npx playwright install # Instalează browserele
|
||||
chmod +x *.sh # Permisiuni pentru scripturi
|
||||
./run-comprehensive-tests.sh --help # Verifică că totul funcționează
|
||||
```
|
||||
|
||||
## 📋 **Prerequisites pentru Testare**
|
||||
|
||||
```bash
|
||||
# 1. Servicii necesare (în terminale separate):
|
||||
cd roa2web/reports-app/backend && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
cd roa2web/reports-app/frontend && npm run dev
|
||||
cd roa2web && ./ssh_tunnel.sh start
|
||||
|
||||
# 2. Verifică că serviciile rulează:
|
||||
curl http://localhost:8000/health # Backend
|
||||
curl http://localhost:3001 # Frontend
|
||||
nc -z localhost 1521 # SSH Tunnel
|
||||
```
|
||||
|
||||
## 📱 Browsere Suportate
|
||||
|
||||
- **Chromium** (Chrome/Edge) - Principal pentru debugging
|
||||
- **Firefox** - Cross-browser compatibility
|
||||
- **WebKit** (Safari) - Apple ecosystem
|
||||
- **Mobile Chrome** (Pixel 5) - Mobile testing
|
||||
- **Mobile Safari** (iPhone 12) - iOS testing
|
||||
|
||||
## 🎭 Exemple Practice
|
||||
|
||||
```bash
|
||||
# 🚀 Workflow Development (RECOMANDAT)
|
||||
./cleanup-tests.sh # Curăță vechile fișiere
|
||||
./run-comprehensive-tests.sh # Toate testele cu analiză
|
||||
cat test-reports-*/comprehensive-test-report.md # Vezi raportul
|
||||
|
||||
# 🔧 Workflow Standard/CI
|
||||
./run-tests.sh # Teste rapide standard
|
||||
npx playwright show-report # Vezi rezultatele
|
||||
|
||||
# 🔍 Debug Specific Issues
|
||||
npx playwright test auth/login.spec.js --headed --debug
|
||||
npx playwright test e2e/debugging-real-issues.spec.js --headed
|
||||
|
||||
# 🧹 Curățare după testare
|
||||
./cleanup-tests.sh --deep # Curățare completă
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### ❌ **Permission Denied pe Scripturi**
|
||||
```bash
|
||||
chmod +x *.sh # Permite execuția scripturilor
|
||||
```
|
||||
|
||||
### ❌ **Windows Line Endings**
|
||||
```bash
|
||||
sed -i 's/\r$//' *.sh # Convertește line endings
|
||||
```
|
||||
|
||||
### ❌ **Serviciile Nu Rulează**
|
||||
```bash
|
||||
# Verifică serviciile:
|
||||
./run-comprehensive-tests.sh --no-basic --no-real-world # Doar verificare servicii
|
||||
```
|
||||
|
||||
### ❌ **Teste Failed - Debugging**
|
||||
```bash
|
||||
./run-comprehensive-tests.sh # Vezi raport detaliat cu recomandări
|
||||
npx playwright test --debug # Debug interactiv
|
||||
npx playwright show-report # Ultimul raport Playwright
|
||||
```
|
||||
|
||||
### ❌ **JWT Token Issues (Known Issue)**
|
||||
```bash
|
||||
# Problema curentă identificată:
|
||||
# 401 Unauthorized pe /companies/ după login
|
||||
# Vezi FINAL_COMPREHENSIVE_TEST_REPORT.md pentru soluție
|
||||
```
|
||||
|
||||
### ❌ **Curățare Space pe Disk**
|
||||
```bash
|
||||
./cleanup-tests.sh --deep # Curăță tot + cache
|
||||
```
|
||||
|
||||
## 📊 **Interpretarea Rezultatelor**
|
||||
|
||||
### ✅ **Success Output:**
|
||||
```bash
|
||||
✅ All tests completed successfully!
|
||||
⚡ Average Response Time: 8ms
|
||||
🎉 No critical issues found!
|
||||
```
|
||||
|
||||
### 🚨 **Issues Found Output:**
|
||||
```bash
|
||||
❌ API Errors: 8
|
||||
🚨 CRITICAL: JWT Token not sent to protected routes
|
||||
💡 RECOMMENDATION: Fix token transmission in api.js
|
||||
📋 Report: test-reports-TIMESTAMP/comprehensive-test-report.md
|
||||
```
|
||||
|
||||
## 🎯 **Ce Faci Dacă...**
|
||||
|
||||
| Problemă | Soluție |
|
||||
|----------|---------|
|
||||
| 🚨 **Authentication 401 errors** | Vezi `FINAL_COMPREHENSIVE_TEST_REPORT.md` - problemă JWT token |
|
||||
| ⚡ **Teste durează mult** | Folosește `./run-tests.sh` pentru testare rapidă |
|
||||
| 🔍 **Vrei să debug o problemă** | `npx playwright test [test-name] --headed --debug` |
|
||||
| 📊 **Vrei raport detaliat** | `./run-comprehensive-tests.sh` + vezi `test-reports-*/` |
|
||||
| 🧹 **Disk plin cu fișiere test** | `./cleanup-tests.sh --deep` |
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **Next Steps**
|
||||
|
||||
1. **🚀 Start:** `./run-comprehensive-tests.sh`
|
||||
2. **📊 Review:** `cat test-reports-*/comprehensive-test-report.md`
|
||||
3. **🔧 Fix Issues:** Follow recommendations in report
|
||||
4. **📚 Deep Dive:** `TEST_RUNNER_GUIDE.md` pentru ghid complet
|
||||
|
||||
**🎯 RESULT:** Aplicație testată comprehensiv cu probleme reale identificate și soluții recomandate!
|
||||
437
reports-app/frontend/README.md
Normal file
437
reports-app/frontend/README.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# ROA Reports Frontend
|
||||
|
||||
Vue.js 3 frontend application for the ROA2WEB reports system.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- **Vue.js 3** with Composition API
|
||||
- **PrimeVue** UI components with Aura theme
|
||||
- **Pinia** for state management
|
||||
- **Vue Router** with navigation guards
|
||||
- **Axios** for API communication
|
||||
- **Responsive design** for mobile and desktop
|
||||
- **JWT Authentication** with token refresh
|
||||
- **TypeScript-ready** architecture
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.js # Application entry point
|
||||
├── App.vue # Root component
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ ├── global.css # Global styles and utilities
|
||||
│ │ └── mobile.css # Mobile-specific styles
|
||||
│ └── images/ # Static images
|
||||
├── components/ # Reusable components
|
||||
│ ├── layout/ # Layout components
|
||||
│ ├── reports/ # Report-specific components
|
||||
│ └── ui/ # Generic UI components
|
||||
├── composables/ # Vue composables
|
||||
│ ├── index.js # Composables exports
|
||||
│ └── useResponsive.js # Responsive design utilities
|
||||
├── router/
|
||||
│ └── index.js # Vue Router configuration
|
||||
├── services/
|
||||
│ ├── api.js # API service with interceptors
|
||||
│ └── index.js # Services exports
|
||||
├── stores/ # Pinia stores
|
||||
│ ├── auth.js # Authentication store
|
||||
│ ├── companies.js # Companies management
|
||||
│ ├── invoices.js # Invoices store
|
||||
│ ├── payments.js # Payments store
|
||||
│ └── index.js # Stores exports
|
||||
├── utils/ # Utility functions
|
||||
│ └── index.js # Utils exports
|
||||
└── views/ # Page components
|
||||
├── LoginView.vue # Authentication page
|
||||
├── DashboardView.vue # Main dashboard
|
||||
├── InvoicesView.vue # Invoices management
|
||||
└── PaymentsView.vue # Payments management
|
||||
```
|
||||
|
||||
## 🛠️ Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16+ and npm 8+
|
||||
- ROA2WEB Backend running on `http://localhost:8000`
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Navigate to frontend directory
|
||||
cd roa2web/reports-app/frontend/
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# Application will be available at http://localhost:3000
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Development server with hot reload
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in the frontend directory:
|
||||
|
||||
```env
|
||||
# API Configuration
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_APP_TITLE=ROA Reports
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# Development
|
||||
VITE_DEV_MODE=true
|
||||
VITE_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
### API Integration
|
||||
|
||||
The frontend communicates with the FastAPI backend through:
|
||||
|
||||
- **Base URL**: `/api` (proxied to `http://localhost:8000` in development)
|
||||
- **Authentication**: JWT tokens with automatic refresh
|
||||
- **Error Handling**: Global interceptors for error management
|
||||
- **Loading States**: Integrated with Pinia stores
|
||||
|
||||
### Responsive Design
|
||||
|
||||
The application uses a mobile-first approach with breakpoints:
|
||||
|
||||
- **xs**: < 640px (Small phones)
|
||||
- **sm**: 640px+ (Large phones)
|
||||
- **md**: 768px+ (Tablets)
|
||||
- **lg**: 1024px+ (Desktop)
|
||||
- **xl**: 1280px+ (Large desktop)
|
||||
- **2xl**: 1536px+ (Extra large)
|
||||
|
||||
## 🎨 UI Components
|
||||
|
||||
### PrimeVue Components Used
|
||||
|
||||
- **Navigation**: Menubar, Menu
|
||||
- **Forms**: InputText, Password, Dropdown, Calendar
|
||||
- **Data**: DataTable, Column, Paginator
|
||||
- **Feedback**: Toast, ConfirmDialog, ProgressSpinner
|
||||
- **Layout**: Card, Badge, Tag, Button
|
||||
- **Overlay**: Dialog
|
||||
|
||||
### Custom Styling
|
||||
|
||||
- **Global utilities** in `global.css`
|
||||
- **Mobile optimizations** in `mobile.css`
|
||||
- **Responsive composables** for dynamic behavior
|
||||
- **CSS custom properties** for consistent theming
|
||||
|
||||
## 📱 Mobile Features
|
||||
|
||||
- **Touch-friendly** interface with 44px minimum touch targets
|
||||
- **Swipe gestures** support (prepared for future implementation)
|
||||
- **Responsive tables** that stack on mobile
|
||||
- **Mobile navigation** with hamburger menu
|
||||
- **Optimized forms** with proper input types
|
||||
- **Accessible** design following WCAG guidelines
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
### Login Flow
|
||||
|
||||
1. User submits credentials to `/auth/login`
|
||||
2. Backend returns access and refresh tokens
|
||||
3. Tokens stored in localStorage
|
||||
4. API requests include JWT token in Authorization header
|
||||
5. Automatic token refresh on expiration
|
||||
|
||||
### Route Protection
|
||||
|
||||
- **Public routes**: `/login`
|
||||
- **Protected routes**: All others require authentication
|
||||
- **Navigation guards** handle redirects
|
||||
- **Auto-logout** on token expiry
|
||||
|
||||
## 📊 State Management
|
||||
|
||||
### Pinia Stores
|
||||
|
||||
#### Auth Store (`useAuthStore`)
|
||||
```javascript
|
||||
// Authentication state and actions
|
||||
const authStore = useAuthStore()
|
||||
authStore.login(credentials)
|
||||
authStore.logout()
|
||||
authStore.isAuthenticated
|
||||
```
|
||||
|
||||
#### Companies Store (`useCompanyStore`)
|
||||
```javascript
|
||||
// Company selection and management
|
||||
const companyStore = useCompanyStore()
|
||||
companyStore.loadCompanies()
|
||||
companyStore.setSelectedCompany(company)
|
||||
companyStore.selectedCompany
|
||||
```
|
||||
|
||||
#### Invoices Store (`useInvoicesStore`)
|
||||
```javascript
|
||||
// Invoice data and filtering
|
||||
const invoicesStore = useInvoicesStore()
|
||||
invoicesStore.loadInvoices(companyCode, filters)
|
||||
invoicesStore.setFilters(newFilters)
|
||||
invoicesStore.invoiceList
|
||||
```
|
||||
|
||||
#### Payments Store (`usePaymentsStore`)
|
||||
```javascript
|
||||
// Payment data and statistics
|
||||
const paymentsStore = usePaymentsStore()
|
||||
paymentsStore.loadPayments(companyCode, filters)
|
||||
paymentsStore.totalAmount
|
||||
paymentsStore.paymentList
|
||||
```
|
||||
|
||||
## 🧩 Composables
|
||||
|
||||
### useResponsive
|
||||
```javascript
|
||||
const { isMobile, isTablet, isDesktop, screenSize } = useResponsive()
|
||||
```
|
||||
|
||||
### useResponsiveTable
|
||||
```javascript
|
||||
const { shouldStackTable, defaultRows, getMobileColumns } = useResponsiveTable()
|
||||
```
|
||||
|
||||
### useResponsiveForm
|
||||
```javascript
|
||||
const { shouldStackButtons, getFormClass } = useResponsiveForm()
|
||||
```
|
||||
|
||||
### useMobileNav
|
||||
```javascript
|
||||
const { isMenuOpen, toggleMenu, closeMenu } = useMobileNav()
|
||||
```
|
||||
|
||||
## 🚀 Production Build
|
||||
|
||||
### Build Process
|
||||
|
||||
```bash
|
||||
# Create production build
|
||||
npm run build
|
||||
|
||||
# Build outputs to dist/ directory
|
||||
# Static files ready for web server deployment
|
||||
```
|
||||
|
||||
### Build Optimization
|
||||
|
||||
- **Code splitting** for vendor libraries
|
||||
- **Tree shaking** for unused code elimination
|
||||
- **Asset optimization** with Vite
|
||||
- **Modern JS** with automatic fallbacks
|
||||
- **Gzip compression** ready
|
||||
|
||||
### Deployment
|
||||
|
||||
The production build creates static files that can be served by:
|
||||
|
||||
- **Nginx** (recommended for production)
|
||||
- **Apache** with URL rewriting
|
||||
- **Node.js** static server
|
||||
- **CDN** with SPA support
|
||||
|
||||
## 🔍 Debugging
|
||||
|
||||
### Development Tools
|
||||
|
||||
- **Vue DevTools** browser extension
|
||||
- **Pinia DevTools** for state inspection
|
||||
- **Network tab** for API debugging
|
||||
- **Console logging** with environment-based levels
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **CORS errors**: Ensure backend allows frontend origin
|
||||
2. **401 errors**: Check token expiration and refresh logic
|
||||
3. **Loading states**: Verify store loading flags
|
||||
4. **Responsive issues**: Test with browser dev tools
|
||||
|
||||
## 📋 Testing
|
||||
|
||||
### E2E Tests with Playwright
|
||||
|
||||
```bash
|
||||
# Run all E2E tests (headless)
|
||||
npm run test:e2e
|
||||
|
||||
# Run with browser UI visible
|
||||
npm run test:e2e:headed
|
||||
|
||||
# Debug mode with step-through
|
||||
npm run test:e2e:debug
|
||||
|
||||
# Interactive UI mode
|
||||
npm run test:e2e:ui
|
||||
|
||||
# View test report
|
||||
npm run test:e2e:report
|
||||
```
|
||||
|
||||
**Test Structure:**
|
||||
- Uses **Page Object Model** pattern
|
||||
- Tests organized by feature: `tests/e2e/{auth,dashboard,invoices,payments,responsive}/`
|
||||
- Page objects in `tests/page-objects/`
|
||||
- Mocks all backend API calls for speed and reliability
|
||||
|
||||
See `tests/README.md` for detailed testing guide.
|
||||
|
||||
### Testing on Real Android Devices
|
||||
|
||||
For accurate mobile testing, you can test the application on a real Android phone using ADB WiFi and Chrome DevTools MCP.
|
||||
|
||||
**Quick Setup (Windows PowerShell):**
|
||||
```powershell
|
||||
# 1. Connect phone via WiFi (first time pairing)
|
||||
adb pair 10.0.20.114:PAIRING_PORT # Use pairing code from phone
|
||||
adb connect 10.0.20.114:MAIN_PORT # Connect to wireless debugging port
|
||||
|
||||
# 2. Run automated setup script
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
**Start Application (WSL):**
|
||||
```bash
|
||||
# Start backend and frontend
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
|
||||
# On phone Chrome: http://localhost:3000
|
||||
```
|
||||
|
||||
**Cleanup (WSL):**
|
||||
```bash
|
||||
# Remove port forwarding when done
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
**Advantages over emulation:**
|
||||
- [OK] Real device performance and rendering
|
||||
- [OK] Actual touch interactions
|
||||
- [OK] True mobile experience on real hardware
|
||||
- [OK] Claude Code can control your phone directly through MCP
|
||||
- [OK] Screenshot capture from real device
|
||||
|
||||
**Architecture:**
|
||||
- **ADB WiFi connection** (Android 10+) - no USB cable needed
|
||||
- **Windows PowerShell** for all ADB operations (WSL cannot access Android devices)
|
||||
- **Multi-layer port forwarding**: ADB forward + Windows port proxy + Firewall rules
|
||||
- **Chrome DevTools MCP** configured with physical Windows IP (not localhost!)
|
||||
|
||||
**Complete Guides:**
|
||||
- **Quick Start (5 minutes):** `ANDROID_QUICK_START.md`
|
||||
- **Full Setup Guide:** `tests/ANDROID_TESTING_GUIDE.md`
|
||||
- **Scripts Documentation:** `scripts/README_ANDROID.md`
|
||||
|
||||
**Requirements:**
|
||||
- Android phone (Android 10+) with Wireless Debugging
|
||||
- Windows 10/11 with PowerShell
|
||||
- ADB Platform Tools (install via `winget install Google.PlatformTools`)
|
||||
- Phone and PC on same WiFi network
|
||||
- Chrome DevTools MCP server configured in Claude Code
|
||||
|
||||
### Unit Tests (Future Implementation)
|
||||
|
||||
```bash
|
||||
# Unit tests with Vitest
|
||||
npm run test
|
||||
|
||||
# Component tests
|
||||
npm run test:component
|
||||
```
|
||||
|
||||
## 🔄 API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/login` - User login
|
||||
- `POST /auth/refresh` - Token refresh
|
||||
- `POST /auth/logout` - User logout
|
||||
|
||||
### Companies
|
||||
- `GET /companies` - List user companies
|
||||
|
||||
### Invoices
|
||||
- `GET /invoices/{company_code}` - Get company invoices
|
||||
- `GET /invoices/{company_code}/{invoice_id}` - Get invoice details
|
||||
|
||||
### Payments
|
||||
- `GET /payments/{company_code}` - Get company payments
|
||||
- `GET /payments/{company_code}/{payment_id}` - Get payment details
|
||||
|
||||
## 🎯 Browser Support
|
||||
|
||||
- **Chrome** 88+
|
||||
- **Firefox** 84+
|
||||
- **Safari** 14+
|
||||
- **Edge** 88+
|
||||
- **Mobile browsers** (iOS Safari 14+, Chrome Mobile 88+)
|
||||
|
||||
## 📝 Development Guidelines
|
||||
|
||||
### Code Style
|
||||
- Use **Composition API** for new components
|
||||
- Follow **Vue 3 best practices**
|
||||
- Use **TypeScript-style** prop definitions
|
||||
- Implement **responsive design** patterns
|
||||
|
||||
### Component Structure
|
||||
```vue
|
||||
<template>
|
||||
<!-- Template with semantic HTML -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Composition API with proper imports
|
||||
// Props, emits, lifecycle hooks
|
||||
// Computed properties and methods
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Scoped styles with CSS custom properties */
|
||||
/* Responsive breakpoints */
|
||||
</style>
|
||||
```
|
||||
|
||||
### Performance
|
||||
- Use **v-memo** for expensive renders
|
||||
- Implement **virtual scrolling** for large lists
|
||||
- **Lazy load** images and components
|
||||
- **Code split** routes and features
|
||||
|
||||
---
|
||||
|
||||
**ROA2WEB Frontend** - Vue.js 3 application for modern ERP reporting
|
||||
617
reports-app/frontend/RESPONSIVE_EXPORT_PLAN.md
Normal file
617
reports-app/frontend/RESPONSIVE_EXPORT_PLAN.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# ROA2WEB - Responsive Design & Export Functionality Implementation Plan
|
||||
|
||||
## Overview
|
||||
This plan addresses the following requirements:
|
||||
1. Make login form and dashboard fully responsive
|
||||
2. Fix text size reduction issue in tables on mobile (keep text readable)
|
||||
3. Implement consistent Export Excel/PDF buttons across all tables
|
||||
4. Use same button styling throughout the dashboard
|
||||
|
||||
## Current Issues Identified
|
||||
- Tables shrink text on mobile making numbers unreadable
|
||||
- Export buttons are inconsistent (only 2 tables have them)
|
||||
- Button styles vary across the dashboard
|
||||
- Login form needs mobile optimization
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### 1. Responsive Login Form Enhancement
|
||||
**File: `src/views/LoginView.vue`**
|
||||
|
||||
#### Changes needed:
|
||||
```css
|
||||
/* Add to <style scoped> section */
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
max-width: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 0 1rem 1.5rem 1rem;
|
||||
}
|
||||
|
||||
/* Ensure inputs are touch-friendly */
|
||||
.p-inputtext,
|
||||
.p-password input {
|
||||
min-height: 44px;
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Dashboard Table Responsiveness Fix
|
||||
**File: `src/views/DashboardView.vue`**
|
||||
|
||||
#### Key changes:
|
||||
1. **Prevent text shrinking in tables**
|
||||
2. **Add horizontal scroll on mobile**
|
||||
3. **Maintain readable font sizes**
|
||||
|
||||
```css
|
||||
/* Add to <style scoped> section */
|
||||
|
||||
/* Mobile Table Styles - Prevent text shrinking */
|
||||
@media (max-width: 768px) {
|
||||
/* Horizontal scroll for table containers */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Minimum table width to prevent compression */
|
||||
.summary-table,
|
||||
.breakdown-table,
|
||||
.dashboard-table {
|
||||
min-width: 600px !important;
|
||||
}
|
||||
|
||||
/* Maintain readable text size */
|
||||
.summary-table td,
|
||||
.summary-table th,
|
||||
.breakdown-table td,
|
||||
.breakdown-table th {
|
||||
font-size: 14px !important; /* Never go below 14px */
|
||||
padding: 0.5rem;
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Amount cells should never shrink */
|
||||
.amount-cell {
|
||||
font-size: 14px !important;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Stack controls vertically */
|
||||
.section-controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.section-controls > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Button groups on mobile */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra small devices */
|
||||
@media (max-width: 480px) {
|
||||
.summary-table,
|
||||
.breakdown-table {
|
||||
min-width: 500px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
/* Stack button groups vertically on very small screens */
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Export Utility Functions
|
||||
**New File: `src/utils/exportUtils.js`**
|
||||
|
||||
```javascript
|
||||
import * as XLSX from 'xlsx';
|
||||
import jsPDF from 'jspdf';
|
||||
import 'jspdf-autotable';
|
||||
|
||||
/**
|
||||
* Format currency values for export
|
||||
*/
|
||||
const formatCurrency = (value) => {
|
||||
if (value == null || value === '-') return '-';
|
||||
return new Intl.NumberFormat('ro-RO', {
|
||||
style: 'currency',
|
||||
currency: 'RON',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Export data to Excel
|
||||
* @param {Array} data - Array of objects to export
|
||||
* @param {String} filename - Name of the file (without extension)
|
||||
* @param {String} sheetName - Name of the Excel sheet
|
||||
*/
|
||||
export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
|
||||
try {
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, sheetName);
|
||||
XLSX.writeFile(wb, `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Excel export failed:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Export data to PDF
|
||||
* @param {Array} data - Array of objects to export
|
||||
* @param {Array} columns - Column definitions [{field: 'key', header: 'Display Name', type: 'currency'}]
|
||||
* @param {String} filename - Name of the file (without extension)
|
||||
* @param {String} title - Title for the PDF document
|
||||
*/
|
||||
export const exportToPDF = (data, columns, filename, title) => {
|
||||
try {
|
||||
const doc = new jsPDF('landscape', 'mm', 'a4');
|
||||
|
||||
// Add title
|
||||
doc.setFontSize(16);
|
||||
doc.text(title, 14, 15);
|
||||
|
||||
// Add company info
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Generat: ${new Date().toLocaleString('ro-RO')}`, 14, 25);
|
||||
|
||||
// Prepare table data
|
||||
const tableColumns = columns.map(col => col.header);
|
||||
const tableRows = data.map(row =>
|
||||
columns.map(col => {
|
||||
const value = row[col.field];
|
||||
if (col.type === 'currency') {
|
||||
return formatCurrency(value);
|
||||
}
|
||||
return value || '-';
|
||||
})
|
||||
);
|
||||
|
||||
// Add table
|
||||
doc.autoTable({
|
||||
head: [tableColumns],
|
||||
body: tableRows,
|
||||
startY: 30,
|
||||
styles: { fontSize: 9, cellPadding: 2 },
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
alternateRowStyles: { fillColor: [245, 245, 245] }
|
||||
});
|
||||
|
||||
// Save PDF
|
||||
doc.save(`${filename}_${new Date().toISOString().split('T')[0]}.pdf`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('PDF export failed:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Export General Totals table
|
||||
*/
|
||||
export const exportGeneralTotals = (summaryData) => {
|
||||
const data = [
|
||||
{
|
||||
Tip: 'Clienți',
|
||||
'Total Facturat': summaryData?.clienti_total_facturat || 0,
|
||||
'Total Încasat': summaryData?.clienti_total_incasat || 0,
|
||||
'Sold Net': summaryData?.clienti_sold_total || 0,
|
||||
'Sold În Termen': summaryData?.clienti_sold_in_termen || 0,
|
||||
'Sold Restant': summaryData?.clienti_sold_restant || 0
|
||||
},
|
||||
{
|
||||
Tip: 'Furnizori',
|
||||
'Total Facturat': summaryData?.furnizori_total_facturat || 0,
|
||||
'Total Achitat': summaryData?.furnizori_total_achitat || 0,
|
||||
'Sold Net': summaryData?.furnizori_sold_total || 0,
|
||||
'Sold În Termen': summaryData?.furnizori_sold_in_termen || 0,
|
||||
'Sold Restant': summaryData?.furnizori_sold_restant || 0
|
||||
},
|
||||
{
|
||||
Tip: 'Trezorerie',
|
||||
'Total Facturat': '-',
|
||||
'Total Încasat/Achitat': '-',
|
||||
'Sold Net': summaryData?.trezorerie_sold || 0,
|
||||
'Sold În Termen': '-',
|
||||
'Sold Restant': '-'
|
||||
}
|
||||
];
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Export Sold Net Breakdown table
|
||||
*/
|
||||
export const exportSoldNetBreakdown = (summaryData) => {
|
||||
const data = [
|
||||
{
|
||||
Categorie: 'Clienți - Restant',
|
||||
'TOTAL': summaryData?.clienti_sold_restant || 0,
|
||||
'7 zile': summaryData?.clienti_restant_7 || 0,
|
||||
'14 zile': summaryData?.clienti_restant_14 || 0,
|
||||
'30 zile': summaryData?.clienti_restant_30 || 0,
|
||||
'60 zile': summaryData?.clienti_restant_60 || 0,
|
||||
'90 zile': summaryData?.clienti_restant_90 || 0,
|
||||
'90+ zile': summaryData?.clienti_restant_over_90 || 0
|
||||
},
|
||||
{
|
||||
Categorie: 'Furnizori - Restant',
|
||||
'TOTAL': summaryData?.furnizori_sold_restant || 0,
|
||||
'7 zile': summaryData?.furnizori_restant_7 || 0,
|
||||
'14 zile': summaryData?.furnizori_restant_14 || 0,
|
||||
'30 zile': summaryData?.furnizori_restant_30 || 0,
|
||||
'60 zile': summaryData?.furnizori_restant_60 || 0,
|
||||
'90 zile': summaryData?.furnizori_restant_90 || 0,
|
||||
'90+ zile': summaryData?.furnizori_restant_over_90 || 0
|
||||
}
|
||||
];
|
||||
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Update Dashboard Export Methods
|
||||
**File: `src/views/DashboardView.vue`**
|
||||
|
||||
#### Add import:
|
||||
```javascript
|
||||
import {
|
||||
exportToExcel,
|
||||
exportToPDF,
|
||||
exportGeneralTotals as prepareGeneralTotals,
|
||||
exportSoldNetBreakdown as prepareSoldNetBreakdown
|
||||
} from '@/utils/exportUtils';
|
||||
```
|
||||
|
||||
#### Update export methods:
|
||||
```javascript
|
||||
// Export General Totals to Excel
|
||||
const exportGeneralTotalsExcel = () => {
|
||||
const data = prepareGeneralTotals(dashboardStore.summary);
|
||||
const result = exportToExcel(data, 'totaluri_generale', 'Totaluri Generale');
|
||||
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Export Reușit',
|
||||
detail: 'Fișier Excel generat cu succes',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare Export',
|
||||
detail: 'Nu s-a putut genera fișierul Excel',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Export General Totals to PDF
|
||||
const exportGeneralTotalsPDF = () => {
|
||||
const data = prepareGeneralTotals(dashboardStore.summary);
|
||||
const columns = [
|
||||
{ field: 'Tip', header: 'Tip', type: 'text' },
|
||||
{ field: 'Total Facturat', header: 'Total Facturat', type: 'currency' },
|
||||
{ field: 'Total Încasat', header: 'Total Încasat/Achitat', type: 'currency' },
|
||||
{ field: 'Sold Net', header: 'Sold Net', type: 'currency' },
|
||||
{ field: 'Sold În Termen', header: 'Sold În Termen', type: 'currency' },
|
||||
{ field: 'Sold Restant', header: 'Sold Restant', type: 'currency' }
|
||||
];
|
||||
|
||||
const result = exportToPDF(
|
||||
data,
|
||||
columns,
|
||||
'totaluri_generale',
|
||||
`Totaluri Generale - ${companyStore.selectedCompany?.name || 'ROA Reports'}`
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Export Reușit',
|
||||
detail: 'Fișier PDF generat cu succes',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Similar methods for Sold Net Breakdown
|
||||
const exportSoldNetExcel = () => {
|
||||
const data = prepareSoldNetBreakdown(dashboardStore.summary);
|
||||
exportToExcel(data, 'detaliere_sold_net', 'Detaliere Sold Net');
|
||||
};
|
||||
|
||||
const exportSoldNetPDF = () => {
|
||||
const data = prepareSoldNetBreakdown(dashboardStore.summary);
|
||||
const columns = [
|
||||
{ field: 'Categorie', header: 'Categorie', type: 'text' },
|
||||
{ field: 'TOTAL', header: 'TOTAL', type: 'currency' },
|
||||
{ field: '7 zile', header: '7 zile', type: 'currency' },
|
||||
{ field: '14 zile', header: '14 zile', type: 'currency' },
|
||||
{ field: '30 zile', header: '30 zile', type: 'currency' },
|
||||
{ field: '60 zile', header: '60 zile', type: 'currency' },
|
||||
{ field: '90 zile', header: '90 zile', type: 'currency' },
|
||||
{ field: '90+ zile', header: '90+ zile', type: 'currency' }
|
||||
];
|
||||
|
||||
exportToPDF(
|
||||
data,
|
||||
columns,
|
||||
'detaliere_sold_net',
|
||||
`Detaliere Sold Net - ${companyStore.selectedCompany?.name || 'ROA Reports'}`
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Update Table Templates with Consistent Buttons
|
||||
**File: `src/views/DashboardView.vue`**
|
||||
|
||||
#### Update all table headers with consistent button groups:
|
||||
|
||||
```html
|
||||
<!-- General Totals Table -->
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Totaluri Generale</h2>
|
||||
<div class="section-controls">
|
||||
<div class="button-group">
|
||||
<button class="btn btn-sm btn-primary" @click="exportGeneralTotalsExcel">
|
||||
<i class="pi pi-file-excel"></i>
|
||||
<span class="btn-text">Excel</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="exportGeneralTotalsPDF">
|
||||
<i class="pi pi-file-pdf"></i>
|
||||
<span class="btn-text">PDF</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" @click="refreshGeneralTotals">
|
||||
<i class="pi pi-refresh"></i>
|
||||
<span class="btn-text">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sold Net Breakdown Table -->
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">DETALIERE SOLD NET</h2>
|
||||
<div class="section-controls">
|
||||
<div class="button-group">
|
||||
<button class="btn btn-sm btn-primary" @click="exportSoldNetExcel">
|
||||
<i class="pi pi-file-excel"></i>
|
||||
<span class="btn-text">Excel</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="exportSoldNetPDF">
|
||||
<i class="pi pi-file-pdf"></i>
|
||||
<span class="btn-text">PDF</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" @click="refreshSoldNetBreakdown">
|
||||
<i class="pi pi-refresh"></i>
|
||||
<span class="btn-text">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trend Section -->
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Analize Trend</h2>
|
||||
<div class="section-controls">
|
||||
<div class="control-group">
|
||||
<label>Perioada:</label>
|
||||
<select :value="selectedPeriod" class="trend-select" @change="(e) => { selectedPeriod = e.target.value; handlePeriodChange(); }">
|
||||
<option value="ytd">An curent (YTD)</option>
|
||||
<option value="12m">Ultimele 12 luni</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Tip Grafic:</label>
|
||||
<select :value="selectedChartType" class="trend-select" @change="(e) => { selectedChartType = e.target.value; handleChartTypeChange(); }">
|
||||
<option value="line">Linie</option>
|
||||
<option value="bar">Bare</option>
|
||||
<option value="area">Arie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-sm btn-primary" @click="exportTrendExcel">
|
||||
<i class="pi pi-file-excel"></i>
|
||||
<span class="btn-text">Excel</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="exportTrendPDF">
|
||||
<i class="pi pi-file-pdf"></i>
|
||||
<span class="btn-text">PDF</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" @click="refreshTrendData">
|
||||
<i class="pi pi-refresh"></i>
|
||||
<span class="btn-text">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 6. Button Styling Updates
|
||||
**File: `src/assets/css/components/buttons.css`**
|
||||
|
||||
Add button group styles:
|
||||
```css
|
||||
/* Button Groups for Dashboard */
|
||||
.button-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Hide button text on small screens */
|
||||
@media (max-width: 640px) {
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stack buttons vertically on very small screens */
|
||||
@media (max-width: 480px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: inline; /* Show text again when stacked */
|
||||
}
|
||||
}
|
||||
|
||||
/* Primary button style for exports */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Mobile Testing (320px - 768px)
|
||||
- [ ] Login form is properly sized and centered
|
||||
- [ ] Input fields are touch-friendly (44px min height)
|
||||
- [ ] Tables scroll horizontally
|
||||
- [ ] Table text remains at 14px minimum
|
||||
- [ ] Numbers don't shrink or wrap
|
||||
- [ ] Export buttons are visible and functional
|
||||
- [ ] Buttons stack properly on small screens
|
||||
|
||||
### Tablet Testing (768px - 1024px)
|
||||
- [ ] Tables display properly with slight compression
|
||||
- [ ] All export buttons visible
|
||||
- [ ] Controls layout is optimal
|
||||
|
||||
### Desktop Testing (1024px+)
|
||||
- [ ] Full layout displays correctly
|
||||
- [ ] All features accessible
|
||||
- [ ] Export functions work properly
|
||||
|
||||
### Export Testing
|
||||
- [ ] Excel export generates valid .xlsx files
|
||||
- [ ] PDF export generates readable documents
|
||||
- [ ] All data is included in exports
|
||||
- [ ] Currency formatting is correct
|
||||
- [ ] Date/time stamps are included
|
||||
|
||||
## Dependencies Required
|
||||
```json
|
||||
{
|
||||
"xlsx": "^0.18.5",
|
||||
"jspdf": "^2.5.1",
|
||||
"jspdf-autotable": "^3.5.31"
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
1. Create export utility functions
|
||||
2. Update button styles in CSS
|
||||
3. Add responsive styles to LoginView
|
||||
4. Update DashboardView with new export methods
|
||||
5. Add consistent button groups to all tables
|
||||
6. Update mobile.css with table fixes
|
||||
7. Test on various devices
|
||||
|
||||
## Notes
|
||||
- Maintain 14px minimum font size for readability
|
||||
- Use horizontal scroll instead of text compression
|
||||
- Keep button styling consistent across all tables
|
||||
- Stack controls vertically on mobile for better UX
|
||||
- Test exports with real data to ensure formatting
|
||||
|
||||
## Success Criteria
|
||||
- ✅ Login form responsive on all devices
|
||||
- ✅ Dashboard tables readable on mobile (no text shrinking)
|
||||
- ✅ All tables have Excel and PDF export buttons
|
||||
- ✅ Buttons use consistent styling
|
||||
- ✅ Export functions work correctly
|
||||
- ✅ Touch-friendly interface on mobile devices
|
||||
249
reports-app/frontend/TEST_RUNNER_GUIDE.md
Normal file
249
reports-app/frontend/TEST_RUNNER_GUIDE.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# ROA2WEB Test Runner Guide
|
||||
|
||||
Ghid pentru rularea testelor Playwright folosind scripturile disponibile.
|
||||
|
||||
## 🚀 Scripturi Disponibile
|
||||
|
||||
### 1. Script Bash (Linux/macOS/WSL)
|
||||
```bash
|
||||
./run_tests.sh [test_type] [browser] [mode]
|
||||
```
|
||||
|
||||
### 2. Script Windows Batch
|
||||
```cmd
|
||||
run_tests.bat [test_type] [browser] [mode]
|
||||
```
|
||||
|
||||
### 3. Script Python (Cross-platform)
|
||||
```bash
|
||||
python run_tests.py [options]
|
||||
```
|
||||
|
||||
## 📋 Opțiuni Disponibile
|
||||
|
||||
### Tipuri de Teste
|
||||
- `all` - Toate testele (implicit)
|
||||
- `auth` - Doar testele de autentificare
|
||||
- `dashboard` - Doar testele dashboard
|
||||
- `invoices` - Doar testele facturi
|
||||
- `payments` - Doar testele încasări
|
||||
- `responsive` - Doar testele responsive
|
||||
- `smoke` - Testele critice (login + dashboard basic)
|
||||
|
||||
### Browsere
|
||||
- `chromium` - Doar pe Chromium
|
||||
- `firefox` - Doar pe Firefox
|
||||
- `webkit` - Doar pe WebKit (Safari)
|
||||
- `mobile` - Doar pe browsere mobile
|
||||
|
||||
### Moduri de rulare
|
||||
- `--headed` - Cu interfață browser vizibilă
|
||||
- `--debug` - Mod debug (pas cu pas)
|
||||
- `--ui` - Cu Playwright UI
|
||||
- `--report` - Afișează raportul
|
||||
|
||||
## 🛠️ Exemple de Utilizare
|
||||
|
||||
### Bash/Linux/macOS
|
||||
```bash
|
||||
# Toate testele
|
||||
./run_tests.sh
|
||||
|
||||
# Doar testele de autentificare
|
||||
./run_tests.sh auth
|
||||
|
||||
# Testele dashboard pe Firefox cu UI
|
||||
./run_tests.sh dashboard firefox --headed
|
||||
|
||||
# Teste smoke în mod debug
|
||||
./run_tests.sh smoke --debug
|
||||
|
||||
# Afișează help
|
||||
./run_tests.sh --help
|
||||
```
|
||||
|
||||
### Windows
|
||||
```cmd
|
||||
REM Toate testele
|
||||
run_tests.bat
|
||||
|
||||
REM Doar testele facturi
|
||||
run_tests.bat invoices
|
||||
|
||||
REM Testele responsive pe Chromium
|
||||
run_tests.bat responsive chromium
|
||||
|
||||
REM Teste cu Playwright UI
|
||||
run_tests.bat all --ui
|
||||
```
|
||||
|
||||
### Python (Cross-platform)
|
||||
```bash
|
||||
# Toate testele
|
||||
python run_tests.py
|
||||
|
||||
# Testele payments cu opțiuni avansate
|
||||
python run_tests.py payments --workers 2 --retries 1
|
||||
|
||||
# Lista toate testele disponibile
|
||||
python run_tests.py --list
|
||||
|
||||
# Testele care conțin "login" în nume
|
||||
python run_tests.py --grep "login"
|
||||
|
||||
# Afișează help complet
|
||||
python run_tests.py --help
|
||||
```
|
||||
|
||||
## 🔧 Opțiuni Avansate (Python)
|
||||
|
||||
Scriptul Python oferă opțiuni suplimentare:
|
||||
|
||||
```bash
|
||||
# Numărul de worker-i paraleli
|
||||
python run_tests.py all --workers 4
|
||||
|
||||
# Numărul de reîncercări pentru testele failed
|
||||
python run_tests.py auth --retries 2
|
||||
|
||||
# Timeout personalizat (în milisecunde)
|
||||
python run_tests.py invoices --timeout 60000
|
||||
|
||||
# Filtrare cu regex
|
||||
python run_tests.py --grep "should.*correctly"
|
||||
|
||||
# Combinații complexe
|
||||
python run_tests.py responsive webkit --headed --workers 1
|
||||
```
|
||||
|
||||
## 📊 Monitorizare și Raportare
|
||||
|
||||
### Afișarea Raportului
|
||||
```bash
|
||||
# Bash
|
||||
./run_tests.sh --report
|
||||
|
||||
# Windows
|
||||
run_tests.bat --report
|
||||
|
||||
# Python
|
||||
python run_tests.py --report
|
||||
```
|
||||
|
||||
### Listarea Testelor
|
||||
```bash
|
||||
# Doar cu Python
|
||||
python run_tests.py --list
|
||||
```
|
||||
|
||||
## 🎯 Workflow Recomandat
|
||||
|
||||
### 1. Dezvoltare Rapidă
|
||||
```bash
|
||||
# Teste smoke pentru verificare rapidă
|
||||
./run_tests.sh smoke
|
||||
|
||||
# Testele unei funcționalități specifice
|
||||
./run_tests.sh auth --headed
|
||||
```
|
||||
|
||||
### 2. Testing Complet
|
||||
```bash
|
||||
# Toate testele pe toate browserele
|
||||
./run_tests.sh all
|
||||
|
||||
# Testele responsive pe mobile
|
||||
./run_tests.sh responsive mobile
|
||||
```
|
||||
|
||||
### 3. Debugging
|
||||
```bash
|
||||
# Debug interactiv
|
||||
./run_tests.sh auth --debug
|
||||
|
||||
# Cu UI pentru investigare
|
||||
./run_tests.sh invoices --ui
|
||||
```
|
||||
|
||||
### 4. CI/CD
|
||||
```bash
|
||||
# Pentru integrare continuă
|
||||
python run_tests.py all --workers 2 --retries 1
|
||||
```
|
||||
|
||||
## 🔧 Setup Inițial
|
||||
|
||||
### 1. Asigură-te că dependențele sunt instalate
|
||||
```bash
|
||||
cd roa2web/reports-app/frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. (Opțional) Pornește serverul frontend
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
*Nota: Testele funcționează și cu API-uri mock-uite*
|
||||
|
||||
### 3. Rulează testele
|
||||
```bash
|
||||
./run_tests.sh
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Problema: "command not found"
|
||||
```bash
|
||||
# Bash - fă scriptul executabil
|
||||
chmod +x run_tests.sh
|
||||
|
||||
# Python - verifică că Python 3 este instalat
|
||||
python3 --version
|
||||
```
|
||||
|
||||
### Problema: "Frontend server not detected"
|
||||
```bash
|
||||
# Pornește serverul în alt terminal
|
||||
npm run dev
|
||||
|
||||
# Sau rulează testele cu mock-uri (implicit)
|
||||
./run_tests.sh auth
|
||||
```
|
||||
|
||||
### Problema: "Playwright not installed"
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
### Problema: Teste failed
|
||||
```bash
|
||||
# Verifică cu debug mode
|
||||
./run_tests.sh [test_type] --debug
|
||||
|
||||
# Sau cu UI pentru investigare
|
||||
./run_tests.sh [test_type] --ui
|
||||
|
||||
# Afișează raportul detaliat
|
||||
./run_tests.sh --report
|
||||
```
|
||||
|
||||
## 📈 Performance Tips
|
||||
|
||||
1. **Rulare Paralelă**: Folosește `--workers N` pentru teste mai rapide
|
||||
2. **Browser Specific**: Rulează pe un singur browser pentru dezvoltare
|
||||
3. **Teste Smoke**: Folosește `smoke` pentru verificări rapide
|
||||
4. **Mock APIs**: Testele sunt optimizate să ruleze fără server backend
|
||||
|
||||
## 🎭 Informații Despre Teste
|
||||
|
||||
- **Total teste**: 262 (peste 5 browsere)
|
||||
- **Coverage**: Login, Dashboard, Facturi, Încasări, Responsive
|
||||
- **Pattern**: Page Object Model pentru mentenabilitate
|
||||
- **Mock**: API responses pentru consistență
|
||||
- **Screenshots**: Automat la failure
|
||||
- **Videos**: Pentru testele failed
|
||||
|
||||
---
|
||||
|
||||
*Pentru documentație completă despre teste, vezi `tests/README.md`*
|
||||
153
reports-app/frontend/cleanup-tests.sh
Normal file
153
reports-app/frontend/cleanup-tests.sh
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ROA2WEB Test Cleanup Script
|
||||
# Removes all temporary test files, screenshots, videos, and reports
|
||||
# Created: 2025-08-04
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FRONTEND_DIR="$SCRIPT_DIR"
|
||||
|
||||
echo -e "${PURPLE}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ROA2WEB Test Cleanup Tool ║"
|
||||
echo "║ ║"
|
||||
echo "║ Removes all temporary test files, screenshots, and reports ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# Function to safely remove files/directories
|
||||
safe_remove() {
|
||||
local path="$1"
|
||||
local description="$2"
|
||||
|
||||
if [ -e "$path" ]; then
|
||||
if [ -d "$path" ]; then
|
||||
local count=$(find "$path" -type f | wc -l)
|
||||
rm -rf "$path"
|
||||
log_success "Removed $description ($count files)"
|
||||
else
|
||||
rm -f "$path"
|
||||
log_success "Removed $description"
|
||||
fi
|
||||
else
|
||||
log_info "$description - not found (already clean)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to count and remove files by pattern
|
||||
remove_by_pattern() {
|
||||
local pattern="$1"
|
||||
local description="$2"
|
||||
|
||||
local files=($(find . -name "$pattern" -type f 2>/dev/null || true))
|
||||
|
||||
if [ ${#files[@]} -gt 0 ]; then
|
||||
for file in "${files[@]}"; do
|
||||
rm -f "$file"
|
||||
done
|
||||
log_success "Removed $description (${#files[@]} files)"
|
||||
else
|
||||
log_info "$description - not found"
|
||||
fi
|
||||
}
|
||||
|
||||
log_info "Starting cleanup process..."
|
||||
echo ""
|
||||
|
||||
# 1. Playwright Reports and Results
|
||||
log_info "Cleaning Playwright reports and results..."
|
||||
safe_remove "playwright-report" "Main Playwright HTML report"
|
||||
safe_remove "playwright-report-integration" "Integration Playwright HTML report"
|
||||
safe_remove "test-results" "Test results directory"
|
||||
|
||||
# 2. Screenshots and Videos
|
||||
log_info "Cleaning screenshots and debug images..."
|
||||
remove_by_pattern "*.png" "Screenshot files"
|
||||
remove_by_pattern "*.jpg" "JPEG image files"
|
||||
remove_by_pattern "*.jpeg" "JPEG image files"
|
||||
remove_by_pattern "debug-*.png" "Debug screenshot files"
|
||||
remove_by_pattern "journey-*.png" "User journey screenshots"
|
||||
remove_by_pattern "button-debug.png" "Button debug screenshots"
|
||||
remove_by_pattern "responsive-*.png" "Responsive design screenshots"
|
||||
|
||||
# 3. Video files
|
||||
log_info "Cleaning video recordings..."
|
||||
remove_by_pattern "*.webm" "WebM video files"
|
||||
remove_by_pattern "*.mp4" "MP4 video files"
|
||||
|
||||
# 4. Test artifacts and temporary files
|
||||
log_info "Cleaning test artifacts..."
|
||||
safe_remove ".nyc_output" "Coverage output directory"
|
||||
safe_remove "coverage" "Coverage reports directory"
|
||||
remove_by_pattern "*.tmp" "Temporary files"
|
||||
remove_by_pattern "*.log" "Log files"
|
||||
|
||||
# 5. Node.js and build artifacts (optional cleanup)
|
||||
if [ "$1" = "--deep" ]; then
|
||||
log_info "Deep cleanup mode - removing build artifacts..."
|
||||
safe_remove "node_modules/.cache" "Node modules cache"
|
||||
safe_remove "dist" "Build output directory"
|
||||
safe_remove ".vite" "Vite cache directory"
|
||||
fi
|
||||
|
||||
# 6. Specific test debugging files
|
||||
log_info "Cleaning debugging artifacts..."
|
||||
remove_by_pattern "*-debug.json" "Debug JSON files"
|
||||
remove_by_pattern "*-report.json" "JSON report files"
|
||||
remove_by_pattern "*-report.xml" "XML report files"
|
||||
remove_by_pattern "*.har" "HAR network recording files"
|
||||
|
||||
# 7. Clean any leftover Playwright traces
|
||||
safe_remove "trace.zip" "Playwright trace files"
|
||||
remove_by_pattern "trace-*.zip" "Individual trace files"
|
||||
|
||||
echo ""
|
||||
log_success "🎉 Cleanup completed successfully!"
|
||||
echo ""
|
||||
|
||||
# Show current directory size after cleanup
|
||||
if command -v du &> /dev/null; then
|
||||
dir_size=$(du -sh . 2>/dev/null | cut -f1)
|
||||
log_info "Current directory size: $dir_size"
|
||||
fi
|
||||
|
||||
echo -e "${PURPLE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${PURPLE}║ Cleanup Summary ║${NC}"
|
||||
echo -e "${PURPLE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
log_info "All temporary test files have been cleaned"
|
||||
log_info "Run './cleanup-tests.sh --deep' for deep cleanup including build cache"
|
||||
echo ""
|
||||
|
||||
# Optional: Show what would be ignored by git
|
||||
if [ -f ".gitignore" ]; then
|
||||
echo -e "${YELLOW}💡 Tip: Check .gitignore to ensure test artifacts are properly ignored${NC}"
|
||||
fi
|
||||
20
reports-app/frontend/index.html
Normal file
20
reports-app/frontend/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ROA Reports - Rapoarte ERP</title>
|
||||
<meta name="description" content="Aplicație pentru rapoarte ERP - facturi și încasări">
|
||||
|
||||
<!-- Cache busting - forțează reîncărcare asset-uri noi -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<meta name="app-version" content="BUILD_TIMESTAMP" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
reports-app/frontend/nginx.conf
Normal file
54
reports-app/frontend/nginx.conf
Normal file
@@ -0,0 +1,54 @@
|
||||
server {
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' http://localhost:8000 ws://localhost:*" always;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache index.html for short time
|
||||
location = /index.html {
|
||||
expires 5m;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
}
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
56
reports-app/frontend/package.json
Normal file
56
reports-app/frontend/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "roa-reports-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "ROA2WEB Reports Frontend - Vue.js 3 + PrimeVue",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"serve": "vite preview --port 3000",
|
||||
"lint": "eslint tests/ --ext .js --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"chart.js": "^4.5.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"jspdf": "^3.0.1",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.46.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"prettier": "^3.1.1",
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"fastapi",
|
||||
"primevue",
|
||||
"reports",
|
||||
"oracle",
|
||||
"erp"
|
||||
],
|
||||
"author": "ROA2WEB Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
46
reports-app/frontend/playwright.config.js
Normal file
46
reports-app/frontend/playwright.config.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:3001',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'mobile-chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'mobile-safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3001',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
70
reports-app/frontend/playwright.real-api.config.js
Normal file
70
reports-app/frontend/playwright.real-api.config.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright configuration for real API integration tests
|
||||
* Uses actual Oracle database connections and real credentials
|
||||
* No API mocking - full end-to-end testing with ROMFAST data
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests/integration',
|
||||
fullyParallel: false, // Run sequentially for real data tests
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0, // Fewer retries for real API tests
|
||||
workers: 1, // Single worker to avoid conflicts with real data
|
||||
timeout: 60000, // Longer timeout for real API calls
|
||||
|
||||
reporter: [
|
||||
['html', { outputFolder: 'playwright-report-integration' }],
|
||||
['json', { outputFile: 'test-results/integration-results.json' }],
|
||||
['junit', { outputFile: 'test-results/integration-junit.xml' }]
|
||||
],
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:3001',
|
||||
trace: 'on',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
// Extended timeouts for real API interactions
|
||||
actionTimeout: 15000,
|
||||
navigationTimeout: 30000,
|
||||
// No API route interception - use real backend
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'real-api-chrome',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
// Additional Chrome flags for testing
|
||||
launchOptions: {
|
||||
args: [
|
||||
'--disable-web-security',
|
||||
'--disable-features=VizDisplayCompositor',
|
||||
'--no-sandbox'
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'real-api-firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
// Global setup and teardown for real API tests
|
||||
globalSetup: './tests/integration/global-setup.js',
|
||||
globalTeardown: './tests/integration/global-teardown.js',
|
||||
|
||||
// Environment-specific configurations
|
||||
metadata: {
|
||||
testType: 'integration',
|
||||
environment: 'development',
|
||||
backend: 'http://localhost:8000',
|
||||
frontend: 'http://localhost:3001',
|
||||
database: 'Oracle via SSH tunnel',
|
||||
credentials: 'Real Oracle credentials'
|
||||
}
|
||||
});
|
||||
514
reports-app/frontend/run-comprehensive-tests.sh
Normal file
514
reports-app/frontend/run-comprehensive-tests.sh
Normal file
@@ -0,0 +1,514 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ROA2WEB Comprehensive Test Execution and Reporting Script
|
||||
# Executes all tests, identifies errors, and generates detailed reports
|
||||
# Created: 2025-08-04
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FRONTEND_DIR="$SCRIPT_DIR"
|
||||
BACKEND_DIR="$SCRIPT_DIR/../backend"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
REPORT_DIR="test-reports-$TIMESTAMP"
|
||||
DETAILED_LOG="$REPORT_DIR/detailed-test-log.txt"
|
||||
|
||||
# Test execution flags
|
||||
RUN_CLEANUP=true
|
||||
RUN_BASIC_TESTS=true
|
||||
RUN_REAL_WORLD_TESTS=true
|
||||
RUN_DEBUGGING_TESTS=true
|
||||
GENERATE_SCREENSHOTS=true
|
||||
CHECK_SERVICES=true
|
||||
|
||||
# Results tracking
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
ISSUES_FOUND=()
|
||||
RECOMMENDATIONS=()
|
||||
|
||||
echo -e "${PURPLE}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ROA2WEB Comprehensive Test Execution Suite ║"
|
||||
echo "║ ║"
|
||||
echo "║ • Error Detection & Analysis ║"
|
||||
echo "║ • Performance Monitoring ║"
|
||||
echo "║ • Real-World Scenario Testing ║"
|
||||
echo "║ • Detailed Reporting & Recommendations ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $1" >> "$DETAILED_LOG"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SUCCESS: $1" >> "$DETAILED_LOG"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $1" >> "$DETAILED_LOG"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$DETAILED_LOG"
|
||||
ISSUES_FOUND+=("$1")
|
||||
}
|
||||
|
||||
log_test() {
|
||||
echo -e "${CYAN}🧪 $1${NC}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] TEST: $1" >> "$DETAILED_LOG"
|
||||
}
|
||||
|
||||
add_recommendation() {
|
||||
RECOMMENDATIONS+=("$1")
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] RECOMMENDATION: $1" >> "$DETAILED_LOG"
|
||||
}
|
||||
|
||||
# Create report directory
|
||||
mkdir -p "$REPORT_DIR"
|
||||
touch "$DETAILED_LOG"
|
||||
|
||||
# Initialize log
|
||||
echo "ROA2WEB Comprehensive Test Execution Log" > "$DETAILED_LOG"
|
||||
echo "Started: $(date)" >> "$DETAILED_LOG"
|
||||
echo "=========================================" >> "$DETAILED_LOG"
|
||||
echo "" >> "$DETAILED_LOG"
|
||||
|
||||
main() {
|
||||
local start_time=$(date +%s)
|
||||
|
||||
log_info "Starting comprehensive test execution..."
|
||||
log_info "Report directory: $REPORT_DIR"
|
||||
echo ""
|
||||
|
||||
# Step 1: Cleanup previous test artifacts
|
||||
if [ "$RUN_CLEANUP" = true ]; then
|
||||
log_test "Step 1: Cleaning up previous test artifacts"
|
||||
if [ -f "./cleanup-tests.sh" ]; then
|
||||
./cleanup-tests.sh 2>&1 | tee -a "$DETAILED_LOG"
|
||||
else
|
||||
log_warning "Cleanup script not found, skipping cleanup"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Step 2: Environment and Service Check
|
||||
if [ "$CHECK_SERVICES" = true ]; then
|
||||
log_test "Step 2: Checking service availability"
|
||||
check_services
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Step 3: Execute Basic Test Suite
|
||||
if [ "$RUN_BASIC_TESTS" = true ]; then
|
||||
log_test "Step 3: Executing basic test suite"
|
||||
execute_basic_tests
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Step 4: Execute Real-World Tests
|
||||
if [ "$RUN_REAL_WORLD_TESTS" = true ]; then
|
||||
log_test "Step 4: Executing real-world comprehensive tests"
|
||||
execute_real_world_tests
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Step 5: Execute Debugging Tests
|
||||
if [ "$RUN_DEBUGGING_TESTS" = true ]; then
|
||||
log_test "Step 5: Executing debugging and issue detection tests"
|
||||
execute_debugging_tests
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Step 6: Performance Analysis
|
||||
log_test "Step 6: Analyzing performance metrics"
|
||||
analyze_performance
|
||||
echo ""
|
||||
|
||||
# Step 7: Generate Comprehensive Report
|
||||
log_test "Step 7: Generating comprehensive report"
|
||||
generate_comprehensive_report
|
||||
|
||||
# Calculate execution time
|
||||
local end_time=$(date +%s)
|
||||
local execution_time=$((end_time - start_time))
|
||||
local minutes=$((execution_time / 60))
|
||||
local seconds=$((execution_time % 60))
|
||||
|
||||
echo ""
|
||||
echo -e "${PURPLE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${PURPLE}║ Execution Summary ║${NC}"
|
||||
echo -e "${PURPLE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
log_info "Total Execution Time: ${minutes}m ${seconds}s"
|
||||
log_info "Total Tests: $TOTAL_TESTS"
|
||||
log_success "Passed Tests: $PASSED_TESTS"
|
||||
if [ $FAILED_TESTS -gt 0 ]; then
|
||||
log_error "Failed Tests: $FAILED_TESTS"
|
||||
else
|
||||
log_success "Failed Tests: $FAILED_TESTS"
|
||||
fi
|
||||
log_info "Issues Found: ${#ISSUES_FOUND[@]}"
|
||||
log_info "Recommendations: ${#RECOMMENDATIONS[@]}"
|
||||
|
||||
echo ""
|
||||
log_success "📊 Comprehensive test report generated: $REPORT_DIR/comprehensive-test-report.md"
|
||||
log_info "📋 Detailed log available: $DETAILED_LOG"
|
||||
|
||||
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
log_warning "🚨 Issues found - check the detailed report for recommendations"
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
log_success "🎉 All tests completed successfully - no critical issues found!"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
check_services() {
|
||||
log_info "Checking required services..."
|
||||
|
||||
# Check backend
|
||||
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
||||
log_success "Backend service (port 8000) is available"
|
||||
else
|
||||
log_error "Backend service (port 8000) is not available"
|
||||
add_recommendation "Start backend service: cd ../backend && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
|
||||
fi
|
||||
|
||||
# Check frontend
|
||||
if curl -s http://localhost:3001 > /dev/null 2>&1; then
|
||||
log_success "Frontend service (port 3001) is available"
|
||||
else
|
||||
log_error "Frontend service (port 3001) is not available"
|
||||
add_recommendation "Start frontend service: npm run dev"
|
||||
fi
|
||||
|
||||
# Check SSH tunnel (Oracle database)
|
||||
if nc -z localhost 1521 2>/dev/null; then
|
||||
log_success "SSH tunnel (port 1521) appears to be active"
|
||||
else
|
||||
log_warning "SSH tunnel (port 1521) may not be active"
|
||||
add_recommendation "Start SSH tunnel: cd ../../../.. && ./ssh_tunnel.sh start"
|
||||
fi
|
||||
|
||||
# Check Node.js and npm
|
||||
if command -v node &> /dev/null && command -v npm &> /dev/null; then
|
||||
local node_version=$(node --version)
|
||||
local npm_version=$(npm --version)
|
||||
log_success "Node.js $node_version and npm $npm_version are available"
|
||||
else
|
||||
log_error "Node.js or npm not found"
|
||||
add_recommendation "Install Node.js and npm"
|
||||
fi
|
||||
}
|
||||
|
||||
execute_basic_tests() {
|
||||
log_info "Running basic authentication and UI tests..."
|
||||
|
||||
local basic_result=0
|
||||
|
||||
# Run login tests
|
||||
if npx playwright test auth/login.spec.js --project=chromium --reporter=json > "$REPORT_DIR/basic-tests-result.json" 2>&1; then
|
||||
log_success "Basic authentication tests passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 10))
|
||||
else
|
||||
log_error "Basic authentication tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 10))
|
||||
basic_result=1
|
||||
fi
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 10))
|
||||
|
||||
# Analyze basic test results
|
||||
if [ -f "$REPORT_DIR/basic-tests-result.json" ]; then
|
||||
analyze_test_results "$REPORT_DIR/basic-tests-result.json" "Basic Tests"
|
||||
fi
|
||||
|
||||
return $basic_result
|
||||
}
|
||||
|
||||
execute_real_world_tests() {
|
||||
log_info "Running real-world comprehensive tests..."
|
||||
|
||||
local real_world_result=0
|
||||
|
||||
if npx playwright test e2e/real-world-comprehensive.spec.js --project=chromium --reporter=json > "$REPORT_DIR/real-world-tests-result.json" 2>&1; then
|
||||
log_success "Real-world comprehensive tests passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 4))
|
||||
else
|
||||
log_error "Real-world comprehensive tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 4))
|
||||
real_world_result=1
|
||||
fi
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 4))
|
||||
|
||||
# Analyze real-world test results
|
||||
if [ -f "$REPORT_DIR/real-world-tests-result.json" ]; then
|
||||
analyze_test_results "$REPORT_DIR/real-world-tests-result.json" "Real-World Tests"
|
||||
fi
|
||||
|
||||
return $real_world_result
|
||||
}
|
||||
|
||||
execute_debugging_tests() {
|
||||
log_info "Running debugging and issue detection tests..."
|
||||
|
||||
local debug_result=0
|
||||
|
||||
if npx playwright test e2e/debugging-real-issues.spec.js --project=chromium --reporter=json > "$REPORT_DIR/debugging-tests-result.json" 2>&1; then
|
||||
log_success "Debugging tests passed"
|
||||
PASSED_TESTS=$((PASSED_TESTS + 4))
|
||||
else
|
||||
log_error "Debugging tests failed"
|
||||
FAILED_TESTS=$((FAILED_TESTS + 4))
|
||||
debug_result=1
|
||||
fi
|
||||
|
||||
TOTAL_TESTS=$((TOTAL_TESTS + 4))
|
||||
|
||||
# Analyze debugging test results
|
||||
if [ -f "$REPORT_DIR/debugging-tests-result.json" ]; then
|
||||
analyze_test_results "$REPORT_DIR/debugging-tests-result.json" "Debugging Tests"
|
||||
fi
|
||||
|
||||
return $debug_result
|
||||
}
|
||||
|
||||
analyze_test_results() {
|
||||
local result_file="$1"
|
||||
local test_category="$2"
|
||||
|
||||
if [ ! -f "$result_file" ]; then
|
||||
log_warning "Result file $result_file not found for $test_category"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Analyzing $test_category results..."
|
||||
|
||||
# Extract key information from JSON results
|
||||
if command -v jq &> /dev/null; then
|
||||
local total_tests=$(jq -r '.stats.total // 0' "$result_file")
|
||||
local passed_tests=$(jq -r '.stats.passed // 0' "$result_file")
|
||||
local failed_tests=$(jq -r '.stats.failed // 0' "$result_file")
|
||||
local duration=$(jq -r '.stats.duration // 0' "$result_file")
|
||||
|
||||
log_info "$test_category: $passed_tests/$total_tests passed (${duration}ms)"
|
||||
|
||||
# Extract failed test details
|
||||
local failed_test_titles=$(jq -r '.tests[] | select(.status == "failed") | .title' "$result_file" 2>/dev/null || echo "")
|
||||
if [ -n "$failed_test_titles" ]; then
|
||||
log_warning "Failed tests in $test_category:"
|
||||
echo "$failed_test_titles" | while read -r title; do
|
||||
log_error " - $title"
|
||||
done
|
||||
fi
|
||||
else
|
||||
log_warning "jq not available for detailed JSON analysis"
|
||||
add_recommendation "Install jq for better test result analysis: sudo apt-get install jq"
|
||||
fi
|
||||
}
|
||||
|
||||
analyze_performance() {
|
||||
log_info "Analyzing application performance..."
|
||||
|
||||
# Check if we have performance data from tests
|
||||
local perf_issues=()
|
||||
|
||||
# Look for performance-related issues in logs
|
||||
if grep -q "slow" "$DETAILED_LOG" 2>/dev/null; then
|
||||
perf_issues+=("Slow requests detected")
|
||||
fi
|
||||
|
||||
if grep -q "timeout" "$DETAILED_LOG" 2>/dev/null; then
|
||||
perf_issues+=("Timeout issues detected")
|
||||
fi
|
||||
|
||||
if [ ${#perf_issues[@]} -gt 0 ]; then
|
||||
log_warning "Performance issues found:"
|
||||
for issue in "${perf_issues[@]}"; do
|
||||
log_warning " - $issue"
|
||||
ISSUES_FOUND+=("Performance: $issue")
|
||||
done
|
||||
add_recommendation "Review performance bottlenecks and optimize slow endpoints"
|
||||
else
|
||||
log_success "No significant performance issues detected"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_comprehensive_report() {
|
||||
local report_file="$REPORT_DIR/comprehensive-test-report.md"
|
||||
|
||||
cat > "$report_file" << EOF
|
||||
# 🧪 ROA2WEB Comprehensive Test Report
|
||||
|
||||
**Generated:** $(date)
|
||||
**Execution Time:** Total execution logged in detailed log
|
||||
**Test Environment:** Development (localhost)
|
||||
|
||||
## 📊 Executive Summary
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total Tests | $TOTAL_TESTS |
|
||||
| Passed Tests | $PASSED_TESTS |
|
||||
| Failed Tests | $FAILED_TESTS |
|
||||
| Success Rate | $(( PASSED_TESTS * 100 / (TOTAL_TESTS > 0 ? TOTAL_TESTS : 1) ))% |
|
||||
| Issues Found | ${#ISSUES_FOUND[@]} |
|
||||
| Recommendations | ${#RECOMMENDATIONS[@]} |
|
||||
|
||||
## 🚨 Issues Identified
|
||||
|
||||
EOF
|
||||
|
||||
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
|
||||
echo "### Critical Issues:" >> "$report_file"
|
||||
for issue in "${ISSUES_FOUND[@]}"; do
|
||||
echo "- ❌ $issue" >> "$report_file"
|
||||
done
|
||||
echo "" >> "$report_file"
|
||||
else
|
||||
echo "✅ **No critical issues found!**" >> "$report_file"
|
||||
echo "" >> "$report_file"
|
||||
fi
|
||||
|
||||
cat >> "$report_file" << EOF
|
||||
## 💡 Recommendations
|
||||
|
||||
EOF
|
||||
|
||||
if [ ${#RECOMMENDATIONS[@]} -gt 0 ]; then
|
||||
for rec in "${RECOMMENDATIONS[@]}"; do
|
||||
echo "- 🔧 $rec" >> "$report_file"
|
||||
done
|
||||
else
|
||||
echo "✅ **No specific recommendations - application is performing well!**" >> "$report_file"
|
||||
fi
|
||||
|
||||
cat >> "$report_file" << EOF
|
||||
|
||||
## 📋 Test Categories Executed
|
||||
|
||||
### 1. 🔐 Authentication Tests
|
||||
- Login form validation
|
||||
- Authentication flow
|
||||
- Error handling
|
||||
- Session management
|
||||
|
||||
### 2. 🌍 Real-World Tests
|
||||
- Complete user journeys
|
||||
- Network performance analysis
|
||||
- Error handling stress tests
|
||||
- Responsive design validation
|
||||
|
||||
### 3. 🔍 Debugging Tests
|
||||
- API request format validation
|
||||
- Button state logic verification
|
||||
- Error message format checking
|
||||
- Network monitoring
|
||||
|
||||
## 📁 Report Files
|
||||
|
||||
- **Detailed Log:** \`$(basename "$DETAILED_LOG")\`
|
||||
- **Basic Tests:** \`basic-tests-result.json\`
|
||||
- **Real-World Tests:** \`real-world-tests-result.json\`
|
||||
- **Debugging Tests:** \`debugging-tests-result.json\`
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. **Address Critical Issues:** Review and fix any issues listed above
|
||||
2. **Implement Recommendations:** Follow the recommendations for optimal performance
|
||||
3. **Regular Testing:** Run this comprehensive test suite regularly
|
||||
4. **Monitor Performance:** Keep track of response times and error rates
|
||||
|
||||
---
|
||||
*Report generated by ROA2WEB Comprehensive Test Suite*
|
||||
EOF
|
||||
|
||||
log_success "Comprehensive report generated: $report_file"
|
||||
}
|
||||
|
||||
# Handle cleanup on exit
|
||||
cleanup_on_exit() {
|
||||
if [ -d "$REPORT_DIR" ]; then
|
||||
log_info "Test reports saved in: $REPORT_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup_on_exit EXIT
|
||||
|
||||
# Show usage if help requested
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "ROA2WEB Comprehensive Test Execution Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --no-cleanup Skip cleanup of previous test artifacts"
|
||||
echo " --no-basic Skip basic test suite"
|
||||
echo " --no-real-world Skip real-world tests"
|
||||
echo " --no-debugging Skip debugging tests"
|
||||
echo " --no-services Skip service availability check"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " - SSH tunnel active: ./ssh_tunnel.sh start"
|
||||
echo " - Backend running: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
|
||||
echo " - Frontend running: npm run dev"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--no-cleanup)
|
||||
RUN_CLEANUP=false
|
||||
shift
|
||||
;;
|
||||
--no-basic)
|
||||
RUN_BASIC_TESTS=false
|
||||
shift
|
||||
;;
|
||||
--no-real-world)
|
||||
RUN_REAL_WORLD_TESTS=false
|
||||
shift
|
||||
;;
|
||||
--no-debugging)
|
||||
RUN_DEBUGGING_TESTS=false
|
||||
shift
|
||||
;;
|
||||
--no-services)
|
||||
CHECK_SERVICES=false
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_warning "Unknown option: $1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Execute main function
|
||||
main "$@"
|
||||
210
reports-app/frontend/run-tests.sh
Normal file
210
reports-app/frontend/run-tests.sh
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Comprehensive Playwright Testing Script with Console Error Monitoring
|
||||
# Runs both mock-based e2e tests and real API integration tests
|
||||
# Includes SSH tunnel management and service orchestration
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
PURPLE='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
ROA2WEB_DIR="$PROJECT_ROOT/roa2web"
|
||||
FRONTEND_DIR="$SCRIPT_DIR"
|
||||
BACKEND_DIR="$SCRIPT_DIR/../backend"
|
||||
|
||||
# Test configuration
|
||||
RUN_MOCK_TESTS=true
|
||||
RUN_INTEGRATION_TESTS=true
|
||||
CLEANUP_ON_EXIT=true
|
||||
GENERATE_REPORTS=true
|
||||
CHECK_SERVICES=true
|
||||
|
||||
# Service PIDs for cleanup
|
||||
BACKEND_PID=""
|
||||
FRONTEND_PID=""
|
||||
TUNNEL_ACTIVE=false
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${PURPLE}🔧 $1${NC}"
|
||||
}
|
||||
|
||||
log_test() {
|
||||
echo -e "${CYAN}🧪 $1${NC}"
|
||||
}
|
||||
|
||||
# Simple version - just run the tests
|
||||
main() {
|
||||
echo -e "${PURPLE}"
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ROA2WEB Comprehensive Playwright Testing Suite ║"
|
||||
echo "║ ║"
|
||||
echo "║ • Console Error Monitoring ║"
|
||||
echo "║ • Real Oracle Data Integration ║"
|
||||
echo "║ • Performance Regression Testing ║"
|
||||
echo "║ • Mock-based E2E Testing ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo ""
|
||||
|
||||
local start_time=$(date +%s)
|
||||
|
||||
log_info "Starting ROA2WEB test execution..."
|
||||
echo ""
|
||||
|
||||
# Check if services are running
|
||||
log_step "Checking service availability..."
|
||||
|
||||
# Check backend
|
||||
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
||||
log_success "Backend service is available"
|
||||
else
|
||||
log_warning "Backend service not available - some tests may fail"
|
||||
fi
|
||||
|
||||
# Check frontend
|
||||
if curl -s http://localhost:3001 > /dev/null 2>&1; then
|
||||
log_success "Frontend service is available"
|
||||
else
|
||||
log_warning "Frontend service not available - tests may fail"
|
||||
fi
|
||||
|
||||
# Check SSH tunnel (check if Oracle port is accessible)
|
||||
if nc -z localhost 1521 2>/dev/null; then
|
||||
log_success "SSH tunnel appears to be active (port 1521 accessible)"
|
||||
else
|
||||
log_warning "SSH tunnel may not be active - integration tests may fail"
|
||||
log_info "Run: cd ../../../.. && ./ssh_tunnel.sh start"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Ensure test results directory exists
|
||||
mkdir -p test-results
|
||||
|
||||
# Track test results
|
||||
MOCK_TESTS_RESULT=0
|
||||
INTEGRATION_TESTS_RESULT=0
|
||||
|
||||
# Run mock-based tests if requested
|
||||
if [ "$1" != "--no-mock" ]; then
|
||||
log_test "Running mock-based E2E tests..."
|
||||
|
||||
if npx playwright test --config=playwright.config.js --reporter=html,json,junit; then
|
||||
log_success "Mock-based E2E tests completed successfully"
|
||||
else
|
||||
log_error "Mock-based E2E tests failed"
|
||||
MOCK_TESTS_RESULT=1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Run integration tests if requested
|
||||
if [ "$1" != "--no-integration" ]; then
|
||||
log_test "Running real API integration tests..."
|
||||
|
||||
if npx playwright test --config=playwright.real-api.config.js --reporter=html,json,junit; then
|
||||
log_success "Real API integration tests completed successfully"
|
||||
else
|
||||
log_error "Real API integration tests failed"
|
||||
INTEGRATION_TESTS_RESULT=1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Calculate execution time
|
||||
local end_time=$(date +%s)
|
||||
local execution_time=$((end_time - start_time))
|
||||
local minutes=$((execution_time / 60))
|
||||
local seconds=$((execution_time % 60))
|
||||
|
||||
echo ""
|
||||
echo -e "${PURPLE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${PURPLE}║ Test Execution Summary ║${NC}"
|
||||
echo -e "${PURPLE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $MOCK_TESTS_RESULT -eq 0 ]; then
|
||||
log_success "Mock-based E2E Tests: PASSED"
|
||||
else
|
||||
log_error "Mock-based E2E Tests: FAILED"
|
||||
fi
|
||||
|
||||
if [ $INTEGRATION_TESTS_RESULT -eq 0 ]; then
|
||||
log_success "Real API Integration Tests: PASSED"
|
||||
else
|
||||
log_error "Real API Integration Tests: FAILED"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Total Execution Time: ${minutes}m ${seconds}s"
|
||||
echo ""
|
||||
log_info "Test reports available at:"
|
||||
log_info " - HTML Reports: playwright-report/ and playwright-report-integration/"
|
||||
log_info " - JSON Results: test-results/*.json"
|
||||
log_info " - JUnit Results: test-results/*.xml"
|
||||
echo ""
|
||||
|
||||
# Final result
|
||||
if [ $MOCK_TESTS_RESULT -eq 0 ] && [ $INTEGRATION_TESTS_RESULT -eq 0 ]; then
|
||||
log_success "🎉 All tests completed successfully!"
|
||||
echo ""
|
||||
log_info "✨ Console error monitoring detected no critical issues"
|
||||
log_info "🚀 Performance baselines met for all operations"
|
||||
log_info "🔒 Real Oracle data integration working correctly"
|
||||
echo ""
|
||||
exit 0
|
||||
else
|
||||
log_error "❌ Some tests failed - check reports for details"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Show usage if help requested
|
||||
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
||||
echo "ROA2WEB Comprehensive Playwright Testing Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --no-mock Skip mock-based E2E tests"
|
||||
echo " --no-integration Skip real API integration tests"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " - SSH tunnel active: ./ssh_tunnel.sh start"
|
||||
echo " - Backend running: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
|
||||
echo " - Frontend running: npm run dev"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Execute main function with all arguments
|
||||
main "$@"
|
||||
236
reports-app/frontend/scripts/README_ANDROID.md
Normal file
236
reports-app/frontend/scripts/README_ANDROID.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# 📱 Android Testing Scripts
|
||||
|
||||
Scripturi pentru testarea aplicației ROA2WEB pe telefoane Android reale.
|
||||
|
||||
## 🎯 Scripturi Disponibile
|
||||
|
||||
| Script | Platform | Status | Descriere |
|
||||
|--------|----------|---------|-----------|
|
||||
| **android-test-setup.ps1** | Windows PowerShell | [OK] Functional | Setup complet Android testing |
|
||||
| **android-disconnect.sh** | Bash/WSL | [OK] Functional | Cleanup port forwarding |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Windows)
|
||||
|
||||
**In Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
|
||||
# Setup complet (prima data)
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
**Pentru cleanup:**
|
||||
```bash
|
||||
# In WSL (dupa testare)
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/reports-app/frontend
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
**Screenshot-uri (Claude Code):**
|
||||
|
||||
Nu mai este nevoie de script pentru salvare screenshot-uri! Claude Code poate face screenshot-uri direct prin MCP (chrome-devtools-android) si le primeste inline pentru analiza.
|
||||
|
||||
---
|
||||
|
||||
## 📜 Documentație Detaliată
|
||||
|
||||
### 1️⃣ `android-test-setup.ps1` (Windows PowerShell)
|
||||
|
||||
**Scop:** Configurare completă conexiune Android pentru testare
|
||||
|
||||
**Ce face:**
|
||||
- ✅ Verifică ADB este instalat
|
||||
- ✅ Verifică telefon conectat (WiFi sau USB)
|
||||
- ✅ Configurează port forwarding pentru Chrome DevTools (9222)
|
||||
- ✅ Configurează reverse port forwarding pentru acces aplicație (3000, 8001)
|
||||
- ✅ Testează conexiunea la Chrome pe telefon
|
||||
- ✅ Afișează informații rețea și configurare MCP
|
||||
- ✅ Comenzi utile pentru debugging
|
||||
|
||||
**Utilizare:**
|
||||
```powershell
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
**Când să rulezi:**
|
||||
- Prima dată când conectezi telefonul
|
||||
- După restart calculator/telefon
|
||||
- Când port forwarding nu mai funcționează
|
||||
- Pentru verificare setup
|
||||
|
||||
**Output exemplu:**
|
||||
```
|
||||
================================
|
||||
🚀 ROA2WEB - Android Testing Setup
|
||||
================================
|
||||
|
||||
✓ ADB este instalat
|
||||
Android Debug Bridge version 1.0.41
|
||||
|
||||
✓ Telefon Android conectat: 1 dispozitiv(e)
|
||||
10.0.20.114:38261 device
|
||||
|
||||
✓ Port forwarding configurat: localhost:9222 -> Chrome pe telefon
|
||||
10.0.20.114:38261 tcp:9222 localabstract:chrome_devtools_remote
|
||||
|
||||
✓ Reverse port forwarding configurat
|
||||
Frontend: http://localhost:3000
|
||||
Backend: http://localhost:8001/api
|
||||
|
||||
ℹ IP-ul calculatorului: 10.0.20.144
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2 `android-disconnect.sh` (Bash/WSL)
|
||||
|
||||
**Scop:** Cleanup port forwarding când termini testarea
|
||||
|
||||
**Ce face:**
|
||||
- ✅ Șterge toate port forwarding-urile (9222, 3000, 8001)
|
||||
- ✅ Șterge reverse port forwarding
|
||||
- ✅ Cleanup complet pentru deconectare sigură
|
||||
|
||||
**Utilizare:**
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/reports-app/frontend
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
**Când să rulezi:**
|
||||
- După finalizarea sesiunii de testare
|
||||
- Înainte de a deconecta telefonul
|
||||
- Pentru cleanup general
|
||||
|
||||
**Output exemplu:**
|
||||
```
|
||||
🔌 Deconectare telefon Android și cleanup...
|
||||
✓ Port forwarding șters
|
||||
✓ Reverse port forwarding șters
|
||||
✅ Deconectare completă!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Workflow Complet de Testare
|
||||
|
||||
### Setup Inițial (o dată):
|
||||
|
||||
**1. Instalează ADB pe Windows:**
|
||||
```powershell
|
||||
winget install Google.PlatformTools
|
||||
```
|
||||
|
||||
**2. Configurează telefonul Android:**
|
||||
```
|
||||
Setări → Despre telefon → Apasă 7x "Build number"
|
||||
Setări → Developer options → Activează "USB debugging"
|
||||
Setări → Developer options → Activează "Wireless debugging"
|
||||
```
|
||||
|
||||
**3. Conectează telefonul:**
|
||||
- **WiFi:** `adb pair IP:PORT` apoi `adb connect IP:PORT`
|
||||
- **USB:** Conectează cablu, acceptă "Allow USB debugging"
|
||||
|
||||
### Workflow Zilnic:
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
|
||||
# 1. Setup conexiune
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
|
||||
# 2. Pornește aplicația (în WSL)
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
|
||||
# 3. Pe telefon Chrome: http://localhost:3000
|
||||
|
||||
# 4. In Claude Code: "fa screenshot de pe telefon" (MCP inline)
|
||||
|
||||
# 5. La final, cleanup (WSL)
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "ADB not found"
|
||||
```powershell
|
||||
winget install Google.PlatformTools
|
||||
# Sau download manual: https://developer.android.com/tools/releases/platform-tools
|
||||
```
|
||||
|
||||
### "No Android device connected"
|
||||
**WiFi:**
|
||||
```powershell
|
||||
adb pair 10.0.20.114:PORT # Portul din "Pair device"
|
||||
adb connect 10.0.20.114:PORT # Portul wireless debugging
|
||||
adb devices # Verifică
|
||||
```
|
||||
|
||||
**USB:**
|
||||
- Verifică cablul (unele sunt doar pentru încărcare)
|
||||
- Deblochează telefonul
|
||||
- Acceptă "Allow USB debugging"
|
||||
|
||||
### "Port forwarding nu funcționează"
|
||||
```powershell
|
||||
# Re-setup complet
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Note Importante
|
||||
|
||||
### De ce `android-test-setup.sh` nu funcționează în WSL?
|
||||
|
||||
ADB în WSL2 **nu poate vedea** dispozitivele USB conectate la Windows. Chiar și cu ADB wireless, există probleme de networking între WSL2 și Android device.
|
||||
|
||||
**Soluție:** Folosește scripturile **PowerShell** care rulează ADB direct în Windows!
|
||||
|
||||
### Chrome DevTools MCP
|
||||
|
||||
Pentru ca Chrome DevTools MCP să funcționeze din WSL (Claude Code), trebuie:
|
||||
1. Port forwarding activ: `adb forward tcp:9222 ...`
|
||||
2. Windows port proxy: `netsh interface portproxy add v4tov4 ...`
|
||||
3. Configurare MCP cu IP-ul fizic Windows: `http://10.0.20.144:9222`
|
||||
|
||||
Vezi `tests/ANDROID_TESTING_GUIDE.md` pentru setup complet.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentație Suplimentară
|
||||
|
||||
- **ANDROID_QUICK_START.md** - Ghid rapid 5 minute
|
||||
- **tests/ANDROID_TESTING_GUIDE.md** - Ghid complet cu troubleshooting
|
||||
- **frontend/README.md** - Secțiunea "Testing on Real Android Devices"
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Scripturi functionale:**
|
||||
- [OK] `android-test-setup.ps1` (Windows PowerShell) - Setup complet
|
||||
- [OK] `android-disconnect.sh` (WSL) - Cleanup
|
||||
|
||||
**Screenshot-uri:**
|
||||
- Nu mai este nevoie de script dedicat
|
||||
- Claude Code face screenshot-uri prin MCP (chrome-devtools-android) inline
|
||||
|
||||
**Testare optima:**
|
||||
- Ruleaza android-test-setup.ps1 din Windows PowerShell
|
||||
- Claude Code controleaza Chrome pe telefon prin MCP
|
||||
|
||||
---
|
||||
|
||||
**Autor:** ROA2WEB Development Team
|
||||
**Data:** 2025-10-20
|
||||
**Versiune:** 3.0 (Final cleanup - doar scripturi esentiale)
|
||||
39
reports-app/frontend/scripts/android-disconnect.sh
Normal file
39
reports-app/frontend/scripts/android-disconnect.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ROA2WEB - Android Disconnect Script
|
||||
# Opreste conexiunea si curata port forwarding
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_success() { echo -e "${GREEN}[OK] $1${NC}"; }
|
||||
print_info() { echo -e "${BLUE}[INFO] $1${NC}"; }
|
||||
|
||||
echo
|
||||
print_info " Deconectare telefon Android si cleanup..."
|
||||
echo
|
||||
|
||||
# Remove all port forwarding
|
||||
print_info "Stergere port forwarding..."
|
||||
adb forward --remove-all 2>/dev/null || true
|
||||
print_success "Port forwarding sters"
|
||||
|
||||
# Remove all reverse port forwarding
|
||||
print_info "Stergere reverse port forwarding..."
|
||||
adb reverse --remove-all 2>/dev/null || true
|
||||
print_success "Reverse port forwarding sters"
|
||||
|
||||
echo
|
||||
print_success "[OK] Deconectare completa!"
|
||||
echo
|
||||
print_info "Poti deconecta telefonul de la calculator in siguranta."
|
||||
echo
|
||||
print_info "Pentru a reconecta, ruleaza (Windows PowerShell):"
|
||||
echo " .\\android-test-setup.ps1"
|
||||
echo
|
||||
266
reports-app/frontend/scripts/android-test-setup.ps1
Normal file
266
reports-app/frontend/scripts/android-test-setup.ps1
Normal file
@@ -0,0 +1,266 @@
|
||||
# ROA2WEB - Android Testing Setup Script (PowerShell)
|
||||
# Configureaza conexiunea la telefon Android pentru testare
|
||||
|
||||
param(
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
ROA2WEB - Android Testing Setup
|
||||
|
||||
Acest script configureaza conexiunea ADB Wireless pentru testare pe Android.
|
||||
|
||||
Usage:
|
||||
.\android-test-setup.ps1
|
||||
|
||||
Prerequisites:
|
||||
- ADB instalat (winget install Google.PlatformTools)
|
||||
- Telefon Android cu Wireless Debugging activat
|
||||
- Telefon si calculator in aceeasi retea WiFi
|
||||
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Colors
|
||||
function Write-Header($message) {
|
||||
Write-Host "`n================================" -ForegroundColor Blue
|
||||
Write-Host $message -ForegroundColor Blue
|
||||
Write-Host "================================`n" -ForegroundColor Blue
|
||||
}
|
||||
|
||||
function Write-Success($message) {
|
||||
Write-Host "[OK] $message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error($message) {
|
||||
Write-Host "[ERROR] $message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning($message) {
|
||||
Write-Host "[WARN] $message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Info($message) {
|
||||
Write-Host "[INFO] $message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Check ADB is installed
|
||||
function Check-ADB {
|
||||
Write-Header "Verificare ADB (Android Debug Bridge)"
|
||||
|
||||
$adb = Get-Command adb -ErrorAction SilentlyContinue
|
||||
if (-not $adb) {
|
||||
Write-Error "ADB nu este instalat sau nu este in PATH!"
|
||||
Write-Host ""
|
||||
Write-Info "Pentru a instala ADB:"
|
||||
Write-Host " winget install Google.PlatformTools"
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "ADB este instalat"
|
||||
adb version | Select-Object -First 1
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Check device connection
|
||||
function Check-Device {
|
||||
Write-Header "Verificare Conexiune Telefon"
|
||||
|
||||
$devices = adb devices | Select-String "device$" | Measure-Object | Select-Object -ExpandProperty Count
|
||||
|
||||
if ($devices -eq 0) {
|
||||
Write-Error "Niciun telefon Android conectat!"
|
||||
Write-Host ""
|
||||
Write-Info "Pasi de conectare ADB Wireless:"
|
||||
Write-Host " 1. Pe telefon: Setari → Developer options → Wireless debugging → ON"
|
||||
Write-Host " 2. Apasa pe 'Wireless debugging' → 'Pair device with pairing code'"
|
||||
Write-Host " 3. In PowerShell: adb pair IP:PORT"
|
||||
Write-Host " 4. Introdu codul de pe telefon"
|
||||
Write-Host " 5. In PowerShell: adb connect IP:PORT_WIRELESS"
|
||||
Write-Host ""
|
||||
Write-Info "Dispozitive detectate:"
|
||||
adb devices
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Telefon Android conectat: $devices dispozitiv(e)"
|
||||
adb devices | Select-String "device$"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Setup port forwarding for Chrome DevTools
|
||||
function Setup-ChromeDevTools {
|
||||
Write-Header "Configurare Chrome DevTools Port Forwarding"
|
||||
|
||||
# Remove existing forwarding
|
||||
adb forward --remove-all | Out-Null
|
||||
|
||||
# Setup new forwarding
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Port forwarding configurat: localhost:9222 -> Chrome pe telefon"
|
||||
} else {
|
||||
Write-Error "Eroare la configurarea port forwarding!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify forwarding
|
||||
Write-Info "Port forwarding activ:"
|
||||
adb forward --list
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Setup reverse port forwarding for app access
|
||||
function Setup-AppAccess {
|
||||
Write-Header "Configurare Acces la Aplicatie de pe Telefon"
|
||||
|
||||
# Remove existing reverse forwarding
|
||||
adb reverse --remove-all 2>$null | Out-Null
|
||||
|
||||
# Setup reverse forwarding for frontend and backend
|
||||
adb reverse tcp:3000 tcp:3000 | Out-Null
|
||||
adb reverse tcp:8001 tcp:8001 | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Reverse port forwarding configurat"
|
||||
Write-Info "Pe telefon poti accesa:"
|
||||
Write-Host " Frontend: http://localhost:3000"
|
||||
Write-Host " Backend: http://localhost:8001/api"
|
||||
} else {
|
||||
Write-Warning "Reverse port forwarding a esuat (optional)"
|
||||
Write-Info "Alternativ, foloseste IP-ul calculatorului:"
|
||||
$localIP = (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "Wi-Fi*","Ethernet*" | Select-Object -First 1).IPAddress
|
||||
Write-Host " Frontend: http://${localIP}:3000"
|
||||
Write-Host " Backend: http://${localIP}:8001/api"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Test Chrome DevTools connection
|
||||
function Test-ChromeConnection {
|
||||
Write-Header "Testare Conexiune Chrome DevTools"
|
||||
|
||||
Write-Info "Asigura-te ca Chrome este deschis pe telefon!"
|
||||
Write-Host ""
|
||||
Read-Host "Apasa Enter cand Chrome este deschis pe telefon"
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:9222/json/version" -ErrorAction Stop
|
||||
Write-Success "Conexiune reusita la Chrome pe telefon!"
|
||||
Write-Host ""
|
||||
Write-Info "Detalii Chrome:"
|
||||
$response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 10
|
||||
} catch {
|
||||
Write-Error "Nu se poate conecta la Chrome pe telefon!"
|
||||
Write-Host ""
|
||||
Write-Info "Verifica ca:"
|
||||
Write-Host " 1. Chrome este deschis pe telefon"
|
||||
Write-Host " 2. Port forwarding este activ: adb forward --list"
|
||||
Write-Host " 3. Wireless debugging este activat"
|
||||
Write-Host ""
|
||||
Write-Info "Pentru debugging manual:"
|
||||
Write-Host " Invoke-WebRequest http://localhost:9222/json/version"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display network info
|
||||
function Display-NetworkInfo {
|
||||
Write-Header "Informatii Retea pentru Acces Remote"
|
||||
|
||||
$localIP = (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "Wi-Fi*","Ethernet*" | Select-Object -First 1).IPAddress
|
||||
|
||||
Write-Info "IP-ul calculatorului: $localIP"
|
||||
Write-Host ""
|
||||
Write-Info "Pentru acces de pe telefon (daca reverse port forwarding nu functioneaza):"
|
||||
Write-Host " Frontend: http://${localIP}:3000"
|
||||
Write-Host " Backend: http://${localIP}:8001/api"
|
||||
Write-Host ""
|
||||
Write-Warning "Asigura-te ca telefonul si calculatorul sunt in aceeasi retea WiFi!"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display MCP configuration
|
||||
function Display-MCPConfig {
|
||||
Write-Header "Configurare Chrome DevTools MCP pentru Claude Code"
|
||||
|
||||
Write-Info "Instalare chrome-devtools-mcp:"
|
||||
Write-Host " npm install -g chrome-devtools-mcp"
|
||||
Write-Host ""
|
||||
|
||||
Write-Info "Configurare in Claude Desktop (claude_desktop_config.json):"
|
||||
Write-Host ""
|
||||
Write-Host @'
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"chrome-devtools-mcp",
|
||||
"--browser-url",
|
||||
"http://localhost:9222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
Write-Host ""
|
||||
Write-Warning "Dupa configurare, restart Claude Desktop!"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display useful commands
|
||||
function Display-UsefulCommands {
|
||||
Write-Header "Comenzi Utile"
|
||||
|
||||
Write-Host "Verificare dispozitive:"
|
||||
Write-Host " adb devices"
|
||||
Write-Host ""
|
||||
Write-Host "Restart ADB server:"
|
||||
Write-Host " adb kill-server; adb start-server"
|
||||
Write-Host ""
|
||||
Write-Host "Screenshot de pe telefon:"
|
||||
Write-Host " adb shell screencap -p /sdcard/screenshot.png"
|
||||
Write-Host " adb pull /sdcard/screenshot.png .\screenshot.png"
|
||||
Write-Host ""
|
||||
Write-Host "Verificare Chrome DevTools:"
|
||||
Write-Host " Invoke-WebRequest http://localhost:9222/json/version"
|
||||
Write-Host ""
|
||||
Write-Host "Deschide Chrome DevTools in browser:"
|
||||
Write-Host " Start-Process chrome://inspect#devices"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Main
|
||||
function Main {
|
||||
Clear-Host
|
||||
Write-Header " ROA2WEB - Android Testing Setup"
|
||||
|
||||
Check-ADB
|
||||
Check-Device
|
||||
Setup-ChromeDevTools
|
||||
Setup-AppAccess
|
||||
Test-ChromeConnection
|
||||
Display-NetworkInfo
|
||||
Display-MCPConfig
|
||||
Display-UsefulCommands
|
||||
|
||||
Write-Header "[OK] Setup Complet!"
|
||||
|
||||
Write-Success "Telefonul Android este configurat pentru testare!"
|
||||
Write-Host ""
|
||||
Write-Info "Next steps:"
|
||||
Write-Host " 1. Deschide Chrome pe telefon"
|
||||
Write-Host " 2. Navigheaza la aplicatia ROA2WEB (vezi IP-ul de mai sus)"
|
||||
Write-Host " 3. In Claude Code, cere: 'Fa un screenshot al aplicatiei de pe telefonul meu Android'"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Run main
|
||||
Main
|
||||
221
reports-app/frontend/src/App.vue
Normal file
221
reports-app/frontend/src/App.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- Navigation Bar -->
|
||||
<Menubar
|
||||
v-if="authStore.isAuthenticated"
|
||||
:model="menuItems"
|
||||
class="app-menubar"
|
||||
>
|
||||
<template #start>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-chart-bar text-primary text-2xl"></i>
|
||||
<span class="font-bold text-xl">ROA Reports</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="flex align-items-center gap-3">
|
||||
<Badge
|
||||
:value="selectedCompany?.name || 'Selectați firmă'"
|
||||
:severity="selectedCompany ? 'info' : 'warning'"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-sign-out"
|
||||
label="Deconectare"
|
||||
text
|
||||
@click="logout"
|
||||
class="p-button-text"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Menubar>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
class="main-content"
|
||||
:class="{ 'with-navbar': authStore.isAuthenticated }"
|
||||
>
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- Global Toast Messages - positioned below header to avoid covering company selector -->
|
||||
<Toast position="top-center" :style="{ top: '80px' }" />
|
||||
|
||||
<!-- Global Confirmation Dialog -->
|
||||
<ConfirmDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "./stores/auth";
|
||||
import { useCompanyStore } from "./stores/companies";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const companyStore = useCompanyStore();
|
||||
|
||||
// Dashboard options
|
||||
const dashboardOptions = [
|
||||
{ label: 'Main Dashboard', value: '/dashboard' },
|
||||
{ label: 'New Dashboard', value: '/dashboard-new' },
|
||||
{ label: 'Ultra Minimal', value: '/dashboard-v1' },
|
||||
{ label: 'Compact Grid', value: '/dashboard-v2' },
|
||||
{ label: 'Data Tables', value: '/dashboard-v3' },
|
||||
{ label: 'Action Center', value: '/dashboard-v4' }
|
||||
];
|
||||
|
||||
// Menu items for navigation
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
label: "Dashboard",
|
||||
icon: "pi pi-home",
|
||||
items: dashboardOptions.map(option => ({
|
||||
label: option.label,
|
||||
command: () => router.push(option.value)
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: "Facturi",
|
||||
icon: "pi pi-file-text",
|
||||
command: () => router.push("/invoices"),
|
||||
},
|
||||
{
|
||||
label: "Registru Casa si Banca",
|
||||
icon: "pi pi-wallet",
|
||||
command: () => router.push("/bank-cash-register"),
|
||||
},
|
||||
]);
|
||||
|
||||
// Get selected company
|
||||
const selectedCompany = computed(() => companyStore.selectedCompany);
|
||||
|
||||
// Logout function
|
||||
const logout = () => {
|
||||
authStore.logout();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
// Initialize app
|
||||
onMounted(async () => {
|
||||
// Check authentication on app start
|
||||
if (authStore.isAuthenticated) {
|
||||
try {
|
||||
// Load companies if authenticated
|
||||
await companyStore.loadCompanies();
|
||||
} catch (error) {
|
||||
console.error("Failed to load companies:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
background-color: var(--surface-ground);
|
||||
}
|
||||
|
||||
.app-menubar {
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.main-content.with-navbar {
|
||||
margin-top: 0;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.main-content:not(.with-navbar) {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Global styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
"Inter",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
sans-serif;
|
||||
background-color: var(--surface-ground);
|
||||
}
|
||||
|
||||
.app-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
padding: 0.25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-menubar .p-menubar-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.main-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom PrimeVue overrides */
|
||||
.p-button {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr.invoice-paid {
|
||||
background-color: var(--green-50);
|
||||
color: var(--green-900);
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr.invoice-overdue {
|
||||
background-color: var(--red-50);
|
||||
color: var(--red-900);
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-paid {
|
||||
background-color: var(--green-100);
|
||||
color: var(--green-900);
|
||||
}
|
||||
|
||||
.status-overdue {
|
||||
background-color: var(--red-100);
|
||||
color: var(--red-900);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: var(--yellow-100);
|
||||
color: var(--yellow-900);
|
||||
}
|
||||
</style>
|
||||
430
reports-app/frontend/src/assets/css/components/buttons.css
Normal file
430
reports-app/frontend/src/assets/css/components/buttons.css
Normal file
@@ -0,0 +1,430 @@
|
||||
/* Button Components - ROA2WEB */
|
||||
|
||||
/* Base Button Styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
line-height: var(--leading-normal);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Button Sizes */
|
||||
.btn-xs {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: var(--text-xs);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.btn-xl {
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* Button Variants */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-border-dark);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Status Button Variants */
|
||||
.btn-success {
|
||||
background: var(--color-success);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #047857;
|
||||
border-color: #047857;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--color-warning);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #b45309;
|
||||
border-color: #b45309;
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
background: var(--color-error);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
.btn-error:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
/* Button Shapes */
|
||||
.btn-rounded {
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.btn-square {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn-circle {
|
||||
border-radius: var(--radius-full);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-circle.btn-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-circle.btn-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Icon Buttons */
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-icon-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Button Groups */
|
||||
.btn-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
border-radius: 0;
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.btn-group .btn:first-child {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.btn-group .btn:hover {
|
||||
z-index: 1;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
/* Action Buttons for Dashboard V4 */
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-xl);
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.action-btn:hover .action-btn-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.action-btn-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Toggle Buttons */
|
||||
.btn-toggle {
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-toggle.active,
|
||||
.btn-toggle:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Pill Buttons */
|
||||
.btn-pill {
|
||||
border-radius: var(--radius-full);
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn-loading {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-loading::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: var(--space-xs);
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: var(--radius-full);
|
||||
animation: btn-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes btn-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Button Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.btn {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
font-size: var(--text-base);
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--text-sm);
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
border-radius: 0;
|
||||
border-right-width: 1px;
|
||||
border-bottom-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group .btn:first-child {
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: var(--space-lg);
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.action-btn {
|
||||
min-height: 80px;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.action-btn-label {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus States for Accessibility */
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button Groups for Dashboard */
|
||||
.button-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Hide button text on small screens */
|
||||
@media (max-width: 640px) {
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stack buttons vertically on very small screens */
|
||||
@media (max-width: 480px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: inline; /* Show text again when stacked */
|
||||
}
|
||||
}
|
||||
|
||||
/* Primary button style for exports */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
border-color: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.btn-full-width {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-auto-width {
|
||||
width: auto;
|
||||
}
|
||||
360
reports-app/frontend/src/assets/css/components/cards.css
Normal file
360
reports-app/frontend/src/assets/css/components/cards.css
Normal file
@@ -0,0 +1,360 @@
|
||||
/* Card Components - ROA2WEB */
|
||||
|
||||
/* Base Card Styles */
|
||||
.card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--transition-fast);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-border-dark);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
/* Card Variants */
|
||||
.card-compact {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.card-compact .card-header,
|
||||
.card-compact .card-body,
|
||||
.card-compact .card-footer {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.card-minimal {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-elevated:hover {
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Stats Cards */
|
||||
.stats-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-lg);
|
||||
text-align: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stats-card-mini {
|
||||
padding: var(--space-md);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stats-card-large {
|
||||
padding: var(--space-xl);
|
||||
}
|
||||
|
||||
/* Stats Card Content */
|
||||
.stats-value {
|
||||
display: block;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.stats-value-large {
|
||||
font-size: var(--text-4xl);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stats-change {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.stats-change.positive {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.stats-change.negative {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* KPI Cards */
|
||||
.kpi-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.kpi-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kpi-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
/* Action Cards */
|
||||
.action-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-lg);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto var(--space-md);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.action-description {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Status Cards */
|
||||
.status-card {
|
||||
background: var(--color-bg);
|
||||
border-left: 4px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.status-card.success {
|
||||
border-left-color: var(--color-success);
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.status-card.warning {
|
||||
border-left-color: var(--color-warning);
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.status-card.error {
|
||||
border-left-color: var(--color-error);
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.status-card.info {
|
||||
border-left-color: var(--color-info);
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
/* Company Banner Card */
|
||||
.company-banner {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||
color: var(--color-text-inverse);
|
||||
border: none;
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.company-info {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Dashboard V2 Mini Cards */
|
||||
.mini-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-sm);
|
||||
text-align: center;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mini-card-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: var(--space-xs);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mini-card-value {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.mini-card-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
/* Heatmap Colors for Mini Cards */
|
||||
.mini-card.heat-low {
|
||||
background: #f0fdf4;
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.mini-card.heat-medium {
|
||||
background: #fffbeb;
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.mini-card.heat-high {
|
||||
background: #fef2f2;
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Mobile Card Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.card-header,
|
||||
.card-body,
|
||||
.card-footer {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.stats-card,
|
||||
.kpi-card,
|
||||
.action-card {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
.stats-value-large {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
.company-banner {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card-header,
|
||||
.card-body,
|
||||
.card-footer,
|
||||
.stats-card,
|
||||
.kpi-card,
|
||||
.action-card {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
.mini-card-value {
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
}
|
||||
460
reports-app/frontend/src/assets/css/components/forms.css
Normal file
460
reports-app/frontend/src/assets/css/components/forms.css
Normal file
@@ -0,0 +1,460 @@
|
||||
/* Form Components - ROA2WEB */
|
||||
|
||||
/* Base Form Styles */
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.form-col {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Input Base Styles */
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-base);
|
||||
font-family: inherit;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
transition: all var(--transition-fast);
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-input:disabled,
|
||||
.form-select:disabled,
|
||||
.form-textarea:disabled {
|
||||
background: var(--color-bg-muted);
|
||||
color: var(--color-text-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Input Variants */
|
||||
.form-input-sm {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: var(--text-sm);
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.form-input-lg {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
font-size: var(--text-lg);
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
/* Textarea */
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.form-textarea-sm {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.form-textarea-lg {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* Select */
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
|
||||
background-position: right var(--space-sm) center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
padding-right: var(--space-xl);
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* Input Groups */
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group .form-input {
|
||||
border-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.input-group .form-input:first-child {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.input-group .form-input:last-child {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-group-addon:first-child {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.input-group-addon:last-child {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
/* Floating Labels */
|
||||
.form-floating {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-floating .form-input,
|
||||
.form-floating .form-textarea {
|
||||
padding-top: var(--space-lg);
|
||||
padding-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.form-floating .form-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: var(--space-md);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--transition-fast);
|
||||
pointer-events: none;
|
||||
transform-origin: left center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.form-floating .form-input:focus + .form-label,
|
||||
.form-floating .form-input:not(:placeholder-shown) + .form-label,
|
||||
.form-floating .form-textarea:focus + .form-label,
|
||||
.form-floating .form-textarea:not(:placeholder-shown) + .form-label {
|
||||
transform: translateY(-50%) scale(0.85);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Validation States */
|
||||
.form-input.valid,
|
||||
.form-select.valid,
|
||||
.form-textarea.valid {
|
||||
border-color: var(--color-success);
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%2316a34a' viewBox='0 0 16 16'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z'/%3E%3C/svg%3E");
|
||||
background-position: right var(--space-sm) center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.form-input.invalid,
|
||||
.form-select.invalid,
|
||||
.form-textarea.invalid {
|
||||
border-color: var(--color-error);
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc2626' viewBox='0 0 16 16'%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
|
||||
background-position: right var(--space-sm) center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
/* Select with validation needs different padding */
|
||||
.form-select.valid,
|
||||
.form-select.invalid {
|
||||
padding-right: calc(var(--space-xl) + var(--space-lg));
|
||||
}
|
||||
|
||||
/* Help Text */
|
||||
.form-help {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-error);
|
||||
margin-top: var(--space-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.form-success {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-success);
|
||||
margin-top: var(--space-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
/* Checkboxes and Radios */
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-check-input[type="checkbox"] {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.form-check-input[type="radio"] {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E%3Cpath d='M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z'/%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
.form-check-input[type="radio"]:checked {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='4'/%3E%3C/svg%3E");
|
||||
background-size: 8px 8px;
|
||||
}
|
||||
|
||||
.form-check-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--space-xl);
|
||||
padding-top: var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.form-actions-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-actions-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.form-actions-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Search Form */
|
||||
.search-form {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: end;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-input .form-input {
|
||||
padding-right: var(--space-3xl);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: var(--space-md);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-lg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Inline Forms */
|
||||
.form-inline {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-inline .form-group {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* File Upload */
|
||||
.file-upload {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-upload-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-upload-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-lg);
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
min-height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-upload:hover .file-upload-label,
|
||||
.file-upload-label.drag-over {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(37, 99, 235, 0.05);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Mobile Form Styles */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-inline .form-group {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-actions-between {
|
||||
justify-content: center;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Ensure mobile-friendly touch targets */
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
min-height: 44px;
|
||||
font-size: 16px; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.form-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
border: none;
|
||||
border-bottom: 1px solid #000;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: var(--space-xs) 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
448
reports-app/frontend/src/assets/css/components/stats.css
Normal file
448
reports-app/frontend/src/assets/css/components/stats.css
Normal file
@@ -0,0 +1,448 @@
|
||||
/* Stats Components - ROA2WEB Dashboard */
|
||||
|
||||
/* Stats Grid Layout */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
/* Stats Cards */
|
||||
.stats-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-border-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Stats Card Header */
|
||||
.stats-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
padding-bottom: var(--space-sm);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.stats-card-header i {
|
||||
font-size: var(--text-xl);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.stats-card-header h3 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Stats Details */
|
||||
.stats-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
padding: var(--space-xs) 0;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.stat-row span:first-child {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.stat-row span:last-child {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-medium);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.stat-highlight {
|
||||
background: var(--color-bg-secondary);
|
||||
padding: var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
margin: var(--space-sm) 0;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.stat-warning {
|
||||
color: var(--color-error);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.stat-warning span:first-child {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.stat-success {
|
||||
color: var(--color-success);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.stat-success span:first-child {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
/* Treasury Specific Styling */
|
||||
.treasury-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.treasury-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.treasury-section-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--space-xs);
|
||||
padding-bottom: var(--space-xs);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-xs) 0;
|
||||
}
|
||||
|
||||
.account-name {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-balance {
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-semibold);
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--space-sm);
|
||||
}
|
||||
|
||||
.treasury-totals {
|
||||
margin-top: var(--space-sm);
|
||||
padding-top: var(--space-sm);
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-muted);
|
||||
margin-left: calc(-1 * var(--space-lg));
|
||||
margin-right: calc(-1 * var(--space-lg));
|
||||
margin-bottom: calc(-1 * var(--space-lg));
|
||||
padding-left: var(--space-lg);
|
||||
padding-right: var(--space-lg);
|
||||
padding-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.total-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
padding: var(--space-xs) 0;
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
/* KPI Large Display */
|
||||
.kpi-large-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-xl);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.kpi-large-card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.kpi-large-value {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.kpi-large-label {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.kpi-large-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.kpi-large-change.positive {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.kpi-large-change.negative {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Mini Stats for V2 Dashboard */
|
||||
.mini-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.mini-stat-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-sm);
|
||||
text-align: center;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-stat-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-primary);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.mini-stat-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: var(--space-xs);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mini-stat-value {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.mini-stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
/* Heat Map Colors for Mini Cards */
|
||||
.mini-stat-card.heat-low {
|
||||
background: #f0fdf4;
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.mini-stat-card.heat-low .mini-stat-value {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.mini-stat-card.heat-medium {
|
||||
background: #fffbeb;
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.mini-stat-card.heat-medium .mini-stat-value {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.mini-stat-card.heat-high {
|
||||
background: #fef2f2;
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
.mini-stat-card.heat-high .mini-stat-value {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Quick Actions Grid */
|
||||
.quick-actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
/* Loading Spinner for Stats */
|
||||
.stats-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-xl);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.stats-loading-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Stats Card Variants */
|
||||
.stats-card.clients {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
.stats-card.clients .stats-card-header i {
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.stats-card.suppliers {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.stats-card.suppliers .stats-card-header i {
|
||||
color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.stats-card.treasury {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.stats-card.treasury .stats-card-header i {
|
||||
color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.mini-stats-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.treasury-totals {
|
||||
margin-left: calc(-1 * var(--space-md));
|
||||
margin-right: calc(-1 * var(--space-md));
|
||||
margin-bottom: calc(-1 * var(--space-md));
|
||||
padding-left: var(--space-md);
|
||||
padding-right: var(--space-md);
|
||||
padding-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.mini-stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.kpi-large-value {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
.quick-actions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mini-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(12, auto);
|
||||
}
|
||||
|
||||
.mini-stat-card {
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
.mini-stat-value {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.stats-card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.stat-row span:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.stats-card {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
876
reports-app/frontend/src/assets/css/components/tables.css
Normal file
876
reports-app/frontend/src/assets/css/components/tables.css
Normal file
@@ -0,0 +1,876 @@
|
||||
/* Table Components - ROA2WEB */
|
||||
|
||||
/* Base Table Styles */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--card-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: var(--color-bg-muted);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
text-align: left;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Table Variants */
|
||||
.table-striped tbody tr:nth-child(even) {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-child(even):hover {
|
||||
background: var(--color-bg-muted);
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.table-bordered th,
|
||||
.table-bordered td {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.table-borderless th,
|
||||
.table-borderless td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table-sm th,
|
||||
.table-sm td {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
.table-lg th,
|
||||
.table-lg td {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
}
|
||||
|
||||
/* Responsive Table */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.table-responsive .table {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
/* Sortable Headers */
|
||||
.table th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
padding-right: var(--space-xl);
|
||||
}
|
||||
|
||||
.table th.sortable:hover {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.table th.sortable::after {
|
||||
content: '↕';
|
||||
position: absolute;
|
||||
right: var(--space-sm);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.5;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.table th.sortable.sorted-asc::after {
|
||||
content: '↑';
|
||||
opacity: 1;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.table th.sortable.sorted-desc::after {
|
||||
content: '↓';
|
||||
opacity: 1;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Table Status Colors */
|
||||
.table .cell-success,
|
||||
.table .text-success {
|
||||
color: var(--color-success);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.table .cell-warning,
|
||||
.table .text-warning {
|
||||
color: var(--color-warning);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.table .cell-error,
|
||||
.table .text-error {
|
||||
color: var(--color-error);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.table .cell-info,
|
||||
.table .text-info {
|
||||
color: var(--color-info);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.table .cell-muted,
|
||||
.table .text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Table Row States */
|
||||
.table .row-selected {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.table .row-active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.table .row-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.table .row-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* Editable Cells */
|
||||
.table .cell-editable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table .cell-editable:hover {
|
||||
background: rgba(37, 99, 235, 0.05);
|
||||
}
|
||||
|
||||
.table .cell-input {
|
||||
width: 100%;
|
||||
padding: var(--space-xs);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.table .cell-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* Action Buttons in Tables */
|
||||
.table .table-actions {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table .table-action-btn {
|
||||
padding: var(--space-xs);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.table .table-action-btn:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Table Pagination */
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-current {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Table Search and Filters */
|
||||
.table-filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-search {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.table-filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.table-filter-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Data Table Stats */
|
||||
.table-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
background: var(--color-bg-muted);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.table-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-stat-value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-text);
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.table-stat-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.table-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-3xl) var(--space-xl);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.table-empty-icon {
|
||||
font-size: var(--text-4xl);
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.table-empty-message {
|
||||
font-size: var(--text-lg);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.table-empty-description {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Trends Section Styling */
|
||||
.trends-container {
|
||||
padding: var(--space-xl);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trend-placeholder {
|
||||
text-align: center;
|
||||
padding: var(--space-xl);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-text-secondary);
|
||||
border: 2px dashed var(--color-border);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: var(--space-lg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.trend-placeholder h3 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
}
|
||||
|
||||
.trend-placeholder p {
|
||||
font-size: var(--text-base);
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.trend-placeholder ul {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding-left: var(--space-lg);
|
||||
}
|
||||
|
||||
.trend-placeholder li {
|
||||
margin-bottom: var(--space-xs);
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Chart Container for future charts */
|
||||
.chart-container {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-lg);
|
||||
margin: var(--space-md) 0;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3xl);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Responsive Table Container */
|
||||
.table-container {
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-dark);
|
||||
}
|
||||
|
||||
/* Mobile Table Styles */
|
||||
@media (max-width: 768px) {
|
||||
.table-mobile-stack {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-mobile-stack thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-mobile-stack tbody,
|
||||
.table-mobile-stack tr,
|
||||
.table-mobile-stack td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-mobile-stack tr {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
background: var(--color-bg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.table-mobile-stack td {
|
||||
border: none;
|
||||
position: relative;
|
||||
padding: var(--space-sm) 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-mobile-stack td::before {
|
||||
content: attr(data-label) ': ';
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text-secondary);
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin-right: var(--space-sm);
|
||||
}
|
||||
|
||||
.table-filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.table-filter-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.table-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-mobile-stack td::before {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Dashboard-specific mobile styles */
|
||||
.dashboard-table {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.dashboard-table th,
|
||||
.dashboard-table td {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trends-container {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.trend-placeholder h3 {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Professional Dashboard Table Styles */
|
||||
.dashboard-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--card-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.dashboard-table th {
|
||||
background: var(--color-bg-muted);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
text-align: left;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.dashboard-table th.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dashboard-table td {
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
vertical-align: middle;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.dashboard-table td.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dashboard-table tbody tr:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.dashboard-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Enhanced Table Cell Types */
|
||||
.dashboard-table .category-cell {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.dashboard-table .name-cell {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text);
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-table .amount-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: var(--font-medium);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-table .status-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Enhanced Status Badge */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-badge.activ {
|
||||
background: var(--color-success-bg);
|
||||
color: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-badge.restant {
|
||||
background: var(--color-warning-bg);
|
||||
color: var(--color-warning);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.status-badge.inactiv {
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error);
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Balance Color Classes */
|
||||
.positive {
|
||||
color: var(--color-success) !important;
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: var(--color-error) !important;
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.neutral {
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
/* Grand Total Row Enhancement */
|
||||
.grand-total-row {
|
||||
background: var(--color-bg-muted);
|
||||
font-weight: var(--font-semibold);
|
||||
border-top: 2px solid var(--color-border);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.grand-total-row td {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.grand-total-row:hover {
|
||||
background: var(--color-bg-muted) !important;
|
||||
}
|
||||
|
||||
/* Section Styling */
|
||||
.dashboard-section {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--card-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.section-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Control Groups */
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-select,
|
||||
.detail-input,
|
||||
.trend-select {
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-sm);
|
||||
min-width: 120px;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.detail-select:focus,
|
||||
.detail-input:focus,
|
||||
.trend-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.2);
|
||||
}
|
||||
|
||||
/* Enhanced Pagination */
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-md) var(--space-xl);
|
||||
background: var(--color-bg-secondary);
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-medium);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--color-bg-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-filters,
|
||||
.table-pagination,
|
||||
.table-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
font-size: 10px;
|
||||
box-shadow: none;
|
||||
border: 1px solid #000 !important;
|
||||
}
|
||||
|
||||
.dashboard-table th {
|
||||
background: #f5f5f5 !important;
|
||||
color: #000 !important;
|
||||
border: 1px solid #000 !important;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.dashboard-table td {
|
||||
border: 1px solid #000 !important;
|
||||
padding: 4px 6px;
|
||||
background: white !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.grand-total-row td {
|
||||
background: #f0f0f0 !important;
|
||||
font-weight: bold;
|
||||
border: 2px solid #000 !important;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
126
reports-app/frontend/src/assets/css/core/reset.css
Normal file
126
reports-app/frontend/src/assets/css/core/reset.css
Normal file
@@ -0,0 +1,126 @@
|
||||
/* Modern CSS Reset - ROA2WEB */
|
||||
|
||||
/* Box sizing rules */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Remove default margin and padding */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Remove list styles on ul, ol elements with a list role */
|
||||
ul[role='list'],
|
||||
ol[role='list'] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Set core root defaults */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* Set core body defaults */
|
||||
body {
|
||||
min-height: 100vh;
|
||||
line-height: var(--leading-normal);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Remove default styling from common elements */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
/* A elements that don't have a class get default styles */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img,
|
||||
picture,
|
||||
svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Inherit fonts for inputs and buttons */
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Remove default button styles */
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Make sure textareas without a rows attribute are not tiny */
|
||||
textarea:not([rows]) {
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
/* Remove default styling from fieldsets */
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Remove default styling from legends */
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Remove default outline on focused elements for better accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Remove all animations and transitions for people that prefer not to see them */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure minimum font size on iOS to prevent zoom */
|
||||
@media screen and (max-width: 480px) {
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
155
reports-app/frontend/src/assets/css/core/typography.css
Normal file
155
reports-app/frontend/src/assets/css/core/typography.css
Normal file
@@ -0,0 +1,155 @@
|
||||
/* Typography System - ROA2WEB */
|
||||
|
||||
/* Heading Styles */
|
||||
.text-4xl, .h1 {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.text-3xl, .h2 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.text-2xl, .h3 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.text-xl, .h4 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.text-lg, .h5 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-medium);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.text-base, .h6 {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
/* Body Text Sizes */
|
||||
.text-sm {
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: var(--text-xs);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
/* Font Weights */
|
||||
.font-light { font-weight: var(--font-light); }
|
||||
.font-normal { font-weight: var(--font-normal); }
|
||||
.font-medium { font-weight: var(--font-medium); }
|
||||
.font-semibold { font-weight: var(--font-semibold); }
|
||||
.font-bold { font-weight: var(--font-bold); }
|
||||
|
||||
/* Text Colors */
|
||||
.text-primary { color: var(--color-primary); }
|
||||
.text-secondary { color: var(--color-text-secondary); }
|
||||
.text-muted { color: var(--color-text-muted); }
|
||||
.text-inverse { color: var(--color-text-inverse); }
|
||||
.text-success { color: var(--color-success); }
|
||||
.text-warning { color: var(--color-warning); }
|
||||
.text-error { color: var(--color-error); }
|
||||
.text-info { color: var(--color-info); }
|
||||
|
||||
/* Text Alignment */
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
/* Line Heights */
|
||||
.leading-tight { line-height: var(--leading-tight); }
|
||||
.leading-normal { line-height: var(--leading-normal); }
|
||||
.leading-loose { line-height: var(--leading-loose); }
|
||||
|
||||
/* Letter Spacing */
|
||||
.tracking-tight { letter-spacing: -0.025em; }
|
||||
.tracking-normal { letter-spacing: 0; }
|
||||
.tracking-wide { letter-spacing: 0.025em; }
|
||||
|
||||
/* Text Transform */
|
||||
.uppercase { text-transform: uppercase; }
|
||||
.lowercase { text-transform: lowercase; }
|
||||
.capitalize { text-transform: capitalize; }
|
||||
|
||||
/* Text Decoration */
|
||||
.underline { text-decoration: underline; }
|
||||
.no-underline { text-decoration: none; }
|
||||
|
||||
/* Page Title Styles */
|
||||
.page-title {
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-normal);
|
||||
line-height: var(--leading-normal);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
/* Section Title Styles */
|
||||
.section-title {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-medium);
|
||||
line-height: var(--leading-tight);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
/* KPI Display Typography */
|
||||
.kpi-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.kpi-large {
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Mobile Typography Adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.text-4xl, .h1 { font-size: var(--text-3xl); }
|
||||
.text-3xl, .h2 { font-size: var(--text-2xl); }
|
||||
.text-2xl, .h3 { font-size: var(--text-xl); }
|
||||
|
||||
.page-title {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
.kpi-large {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
}
|
||||
181
reports-app/frontend/src/assets/css/core/variables.css
Normal file
181
reports-app/frontend/src/assets/css/core/variables.css
Normal file
@@ -0,0 +1,181 @@
|
||||
/* CSS Variables - ROA2WEB Design System */
|
||||
|
||||
:root {
|
||||
/* Spacing System */
|
||||
--space-xs: 0.25rem; /* 4px */
|
||||
--space-sm: 0.5rem; /* 8px */
|
||||
--space-md: 1rem; /* 16px */
|
||||
--space-lg: 1.5rem; /* 24px */
|
||||
--space-xl: 2rem; /* 32px */
|
||||
--space-2xl: 3rem; /* 48px */
|
||||
--space-3xl: 4rem; /* 64px */
|
||||
|
||||
/* Typography Scale */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 2rem; /* 32px */
|
||||
--text-4xl: 2.5rem; /* 40px */
|
||||
|
||||
/* Font Weights */
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Line Heights */
|
||||
--leading-tight: 1.2;
|
||||
--leading-normal: 1.5;
|
||||
--leading-loose: 1.75;
|
||||
|
||||
/* Colors - Minimal Professional Palette */
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-dark: #1d4ed8;
|
||||
--color-primary-light: #3b82f6;
|
||||
|
||||
--color-secondary: #64748b;
|
||||
--color-secondary-dark: #475569;
|
||||
--color-secondary-light: #94a3b8;
|
||||
|
||||
--color-success: #059669;
|
||||
--color-warning: #d97706;
|
||||
--color-error: #dc2626;
|
||||
--color-info: #0891b2;
|
||||
|
||||
--color-text: #111827;
|
||||
--color-text-secondary: #6b7280;
|
||||
--color-text-muted: #9ca3af;
|
||||
--color-text-inverse: #ffffff;
|
||||
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-secondary: #f9fafb;
|
||||
--color-bg-muted: #f3f4f6;
|
||||
--color-bg-dark: #111827;
|
||||
|
||||
--color-border: #e5e7eb;
|
||||
--color-border-light: #f3f4f6;
|
||||
--color-border-dark: #d1d5db;
|
||||
|
||||
/* Surface colors for PrimeVue compatibility */
|
||||
--surface-0: #ffffff;
|
||||
--surface-50: #f8fafc;
|
||||
--surface-100: #f1f5f9;
|
||||
--surface-200: #e2e8f0;
|
||||
--surface-300: #cbd5e1;
|
||||
--surface-400: #94a3b8;
|
||||
--surface-500: #64748b;
|
||||
--surface-600: #475569;
|
||||
--surface-700: #334155;
|
||||
--surface-800: #1e293b;
|
||||
--surface-900: #0f172a;
|
||||
--surface-950: #020617;
|
||||
|
||||
/* Red color palette for errors */
|
||||
--red-50: #fef2f2;
|
||||
--red-100: #fee2e2;
|
||||
--red-200: #fecaca;
|
||||
--red-300: #fca5a5;
|
||||
--red-400: #f87171;
|
||||
--red-500: #ef4444;
|
||||
--red-600: #dc2626;
|
||||
--red-700: #b91c1c;
|
||||
--red-800: #991b1b;
|
||||
--red-900: #7f1d1d;
|
||||
--red-950: #450a0a;
|
||||
|
||||
/* Compatibility aliases for old variable names */
|
||||
--primary-color: var(--color-primary);
|
||||
--primary-color-dark: var(--color-primary-dark);
|
||||
--primary-color-light: var(--color-primary-light);
|
||||
--text-color: var(--color-text);
|
||||
--text-color-secondary: var(--color-text-secondary);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.25rem; /* 4px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Layout Specific */
|
||||
--header-height: 56px;
|
||||
--sidebar-width: 240px;
|
||||
--card-radius: var(--radius-md);
|
||||
--container-max-width: 1400px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 250ms ease;
|
||||
--transition-slow: 350ms ease;
|
||||
|
||||
/* Additional Status Colors */
|
||||
--color-success-bg: rgba(5, 150, 105, 0.1);
|
||||
--color-warning-bg: rgba(217, 119, 6, 0.1);
|
||||
--color-error-bg: rgba(220, 38, 38, 0.1);
|
||||
--color-info-bg: rgba(8, 145, 178, 0.1);
|
||||
|
||||
/* Color RGB values for opacity usage */
|
||||
--color-primary-rgb: 37, 99, 235;
|
||||
--color-success-rgb: 5, 150, 105;
|
||||
--color-warning-rgb: 217, 119, 6;
|
||||
--color-error-rgb: 220, 38, 38;
|
||||
|
||||
/* Monospace font for numbers */
|
||||
--font-mono: 'SF Mono', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
|
||||
/* Z-Index Scale */
|
||||
--z-dropdown: 1200;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
|
||||
/* Breakpoints (for reference in media queries) */
|
||||
--breakpoint-mobile: 480px;
|
||||
--breakpoint-tablet: 768px;
|
||||
--breakpoint-desktop: 1024px;
|
||||
--breakpoint-wide: 1400px;
|
||||
}
|
||||
|
||||
/* Dark mode support (for future enhancement) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-text: #f9fafb;
|
||||
--color-text-secondary: #d1d5db;
|
||||
--color-text-muted: #9ca3af;
|
||||
--color-bg: #111827;
|
||||
--color-bg-secondary: #1f2937;
|
||||
--color-bg-muted: #374151;
|
||||
--color-border: #374151;
|
||||
--color-border-light: #4b5563;
|
||||
--color-border-dark: #6b7280;
|
||||
|
||||
/* Surface colors for dark mode */
|
||||
--surface-0: #ffffff;
|
||||
--surface-50: #020617;
|
||||
--surface-100: #0f172a;
|
||||
--surface-200: #1e293b;
|
||||
--surface-300: #334155;
|
||||
--surface-400: #475569;
|
||||
--surface-500: #64748b;
|
||||
--surface-600: #94a3b8;
|
||||
--surface-700: #cbd5e1;
|
||||
--surface-800: #e2e8f0;
|
||||
--surface-900: #f1f5f9;
|
||||
--surface-950: #f8fafc;
|
||||
|
||||
/* Red colors remain the same in dark mode */
|
||||
}
|
||||
}
|
||||
686
reports-app/frontend/src/assets/css/global.css
Normal file
686
reports-app/frontend/src/assets/css/global.css
Normal file
@@ -0,0 +1,686 @@
|
||||
/* Global CSS for ROA Reports */
|
||||
|
||||
/* CSS Custom Properties for consistent theming */
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--roa-primary: #2563eb;
|
||||
--roa-primary-hover: #1d4ed8;
|
||||
--roa-primary-light: #dbeafe;
|
||||
|
||||
/* Success Colors */
|
||||
--roa-success: #16a34a;
|
||||
--roa-success-light: #dcfce7;
|
||||
|
||||
/* Warning Colors */
|
||||
--roa-warning: #ca8a04;
|
||||
--roa-warning-light: #fef3c7;
|
||||
|
||||
/* Danger Colors */
|
||||
--roa-danger: #dc2626;
|
||||
--roa-danger-light: #fee2e2;
|
||||
|
||||
/* Neutral Colors */
|
||||
--roa-gray-50: #f9fafb;
|
||||
--roa-gray-100: #f3f4f6;
|
||||
--roa-gray-200: #e5e7eb;
|
||||
--roa-gray-300: #d1d5db;
|
||||
--roa-gray-400: #9ca3af;
|
||||
--roa-gray-500: #6b7280;
|
||||
--roa-gray-600: #4b5563;
|
||||
--roa-gray-700: #374151;
|
||||
--roa-gray-800: #1f2937;
|
||||
--roa-gray-900: #111827;
|
||||
|
||||
/* Spacing */
|
||||
--roa-spacing-xs: 0.25rem;
|
||||
--roa-spacing-sm: 0.5rem;
|
||||
--roa-spacing-md: 1rem;
|
||||
--roa-spacing-lg: 1.5rem;
|
||||
--roa-spacing-xl: 2rem;
|
||||
--roa-spacing-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--roa-radius-sm: 0.375rem;
|
||||
--roa-radius-md: 0.5rem;
|
||||
--roa-radius-lg: 0.75rem;
|
||||
--roa-radius-xl: 1rem;
|
||||
|
||||
/* Shadows */
|
||||
--roa-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--roa-shadow-md:
|
||||
0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--roa-shadow-lg:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--roa-shadow-xl:
|
||||
0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--roa-transition-fast: 150ms ease-in-out;
|
||||
--roa-transition-normal: 300ms ease-in-out;
|
||||
--roa-transition-slow: 500ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Arial,
|
||||
"Noto Sans",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--surface-ground, var(--roa-gray-50));
|
||||
color: var(--text-color, var(--roa-gray-900));
|
||||
font-feature-settings: "kern" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
|
||||
/* Spacing Utilities */
|
||||
.m-0 {
|
||||
margin: 0;
|
||||
}
|
||||
.m-1 {
|
||||
margin: var(--roa-spacing-xs);
|
||||
}
|
||||
.m-2 {
|
||||
margin: var(--roa-spacing-sm);
|
||||
}
|
||||
.m-3 {
|
||||
margin: var(--roa-spacing-md);
|
||||
}
|
||||
.m-4 {
|
||||
margin: var(--roa-spacing-lg);
|
||||
}
|
||||
.m-5 {
|
||||
margin: var(--roa-spacing-xl);
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0;
|
||||
}
|
||||
.p-1 {
|
||||
padding: var(--roa-spacing-xs);
|
||||
}
|
||||
.p-2 {
|
||||
padding: var(--roa-spacing-sm);
|
||||
}
|
||||
.p-3 {
|
||||
padding: var(--roa-spacing-md);
|
||||
}
|
||||
.p-4 {
|
||||
padding: var(--roa-spacing-lg);
|
||||
}
|
||||
.p-5 {
|
||||
padding: var(--roa-spacing-xl);
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: var(--roa-spacing-xs);
|
||||
}
|
||||
.mt-2 {
|
||||
margin-top: var(--roa-spacing-sm);
|
||||
}
|
||||
.mt-3 {
|
||||
margin-top: var(--roa-spacing-md);
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: var(--roa-spacing-lg);
|
||||
}
|
||||
.mt-5 {
|
||||
margin-top: var(--roa-spacing-xl);
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.mb-1 {
|
||||
margin-bottom: var(--roa-spacing-xs);
|
||||
}
|
||||
.mb-2 {
|
||||
margin-bottom: var(--roa-spacing-sm);
|
||||
}
|
||||
.mb-3 {
|
||||
margin-bottom: var(--roa-spacing-md);
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: var(--roa-spacing-lg);
|
||||
}
|
||||
.mb-5 {
|
||||
margin-bottom: var(--roa-spacing-xl);
|
||||
}
|
||||
|
||||
/* Text Utilities */
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
|
||||
.font-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color, var(--roa-primary));
|
||||
}
|
||||
.text-success {
|
||||
color: var(--green-600, var(--roa-success));
|
||||
}
|
||||
.text-warning {
|
||||
color: var(--yellow-600, var(--roa-warning));
|
||||
}
|
||||
.text-danger {
|
||||
color: var(--red-600, var(--roa-danger));
|
||||
}
|
||||
.text-muted {
|
||||
color: var(--text-color-secondary, var(--roa-gray-500));
|
||||
}
|
||||
|
||||
/* Display Utilities */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* Flexbox Utilities */
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.flex-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.flex-none {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.gap-1 {
|
||||
gap: var(--roa-spacing-xs);
|
||||
}
|
||||
.gap-2 {
|
||||
gap: var(--roa-spacing-sm);
|
||||
}
|
||||
.gap-3 {
|
||||
gap: var(--roa-spacing-md);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: var(--roa-spacing-lg);
|
||||
}
|
||||
.gap-5 {
|
||||
gap: var(--roa-spacing-xl);
|
||||
}
|
||||
|
||||
/* Width and Height Utilities */
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.w-auto {
|
||||
width: auto;
|
||||
}
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
.h-auto {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Border Utilities */
|
||||
.border {
|
||||
border: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
}
|
||||
.border-0 {
|
||||
border: 0;
|
||||
}
|
||||
.border-t {
|
||||
border-top: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
}
|
||||
.border-b {
|
||||
border-bottom: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
}
|
||||
.border-l {
|
||||
border-left: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
}
|
||||
.border-r {
|
||||
border-right: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: var(--roa-radius-md);
|
||||
}
|
||||
.rounded-sm {
|
||||
border-radius: var(--roa-radius-sm);
|
||||
}
|
||||
.rounded-lg {
|
||||
border-radius: var(--roa-radius-lg);
|
||||
}
|
||||
.rounded-xl {
|
||||
border-radius: var(--roa-radius-xl);
|
||||
}
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
/* Shadow Utilities */
|
||||
.shadow-sm {
|
||||
box-shadow: var(--roa-shadow-sm);
|
||||
}
|
||||
.shadow-md {
|
||||
box-shadow: var(--roa-shadow-md);
|
||||
}
|
||||
.shadow-lg {
|
||||
box-shadow: var(--roa-shadow-lg);
|
||||
}
|
||||
.shadow-xl {
|
||||
box-shadow: var(--roa-shadow-xl);
|
||||
}
|
||||
.shadow-none {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Background Utilities */
|
||||
.bg-white {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.bg-gray-50 {
|
||||
background-color: var(--roa-gray-50);
|
||||
}
|
||||
.bg-gray-100 {
|
||||
background-color: var(--roa-gray-100);
|
||||
}
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color, var(--roa-primary));
|
||||
}
|
||||
.bg-success {
|
||||
background-color: var(--green-100, var(--roa-success-light));
|
||||
}
|
||||
.bg-warning {
|
||||
background-color: var(--yellow-100, var(--roa-warning-light));
|
||||
}
|
||||
.bg-danger {
|
||||
background-color: var(--red-100, var(--roa-danger-light));
|
||||
}
|
||||
|
||||
/* Hover Effects */
|
||||
.hover-lift {
|
||||
transition:
|
||||
transform var(--roa-transition-fast),
|
||||
box-shadow var(--roa-transition-fast);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--roa-shadow-lg);
|
||||
}
|
||||
|
||||
/* Focus States */
|
||||
.focus-ring:focus {
|
||||
outline: 2px solid var(--primary-color, var(--roa-primary));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Animation Utilities */
|
||||
.transition-all {
|
||||
transition: all var(--roa-transition-normal);
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition:
|
||||
color var(--roa-transition-normal),
|
||||
background-color var(--roa-transition-normal),
|
||||
border-color var(--roa-transition-normal);
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition: opacity var(--roa-transition-normal);
|
||||
}
|
||||
|
||||
.transition-transform {
|
||||
transition: transform var(--roa-transition-normal);
|
||||
}
|
||||
|
||||
/* Custom ROA Classes */
|
||||
.roa-card {
|
||||
background: var(--surface-card, #ffffff);
|
||||
border: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
border-radius: var(--roa-radius-lg);
|
||||
box-shadow: var(--roa-shadow-sm);
|
||||
padding: var(--roa-spacing-lg);
|
||||
}
|
||||
|
||||
.roa-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--roa-spacing-sm);
|
||||
padding: var(--roa-spacing-sm) var(--roa-spacing-lg);
|
||||
border: none;
|
||||
border-radius: var(--roa-radius-md);
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--roa-transition-fast);
|
||||
background-color: var(--primary-color, var(--roa-primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roa-button:hover {
|
||||
background-color: var(--primary-color-dark, var(--roa-primary-hover));
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--roa-shadow-md);
|
||||
}
|
||||
|
||||
.roa-input {
|
||||
width: 100%;
|
||||
padding: var(--roa-spacing-sm) var(--roa-spacing-md);
|
||||
border: 1px solid var(--surface-border, var(--roa-gray-200));
|
||||
border-radius: var(--roa-radius-md);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
transition:
|
||||
border-color var(--roa-transition-fast),
|
||||
box-shadow var(--roa-transition-fast);
|
||||
}
|
||||
|
||||
.roa-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, var(--roa-primary));
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
/* Invoice Status Classes */
|
||||
.status-paid {
|
||||
background-color: var(--green-100, var(--roa-success-light));
|
||||
color: var(--green-800, var(--roa-success));
|
||||
padding: var(--roa-spacing-xs) var(--roa-spacing-sm);
|
||||
border-radius: var(--roa-radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-overdue {
|
||||
background-color: var(--red-100, var(--roa-danger-light));
|
||||
color: var(--red-800, var(--roa-danger));
|
||||
padding: var(--roa-spacing-xs) var(--roa-spacing-sm);
|
||||
border-radius: var(--roa-radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: var(--yellow-100, var(--roa-warning-light));
|
||||
color: var(--yellow-800, var(--roa-warning));
|
||||
padding: var(--roa-spacing-xs) var(--roa-spacing-sm);
|
||||
border-radius: var(--roa-radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 640px) {
|
||||
.sm\:hidden {
|
||||
display: none;
|
||||
}
|
||||
.sm\:block {
|
||||
display: block;
|
||||
}
|
||||
.sm\:flex {
|
||||
display: flex;
|
||||
}
|
||||
.sm\:grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.sm\:flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.sm\:items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.sm\:justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sm\:text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.sm\:text-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.sm\:p-2 {
|
||||
padding: var(--roa-spacing-sm);
|
||||
}
|
||||
.sm\:m-2 {
|
||||
margin: var(--roa-spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.md\:hidden {
|
||||
display: none;
|
||||
}
|
||||
.md\:block {
|
||||
display: block;
|
||||
}
|
||||
.md\:flex {
|
||||
display: flex;
|
||||
}
|
||||
.md\:grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.md\:flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.md\:items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.md\:justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.md\:text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.md\:text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.md\:p-3 {
|
||||
padding: var(--roa-spacing-md);
|
||||
}
|
||||
.md\:m-3 {
|
||||
margin: var(--roa-spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.lg\:hidden {
|
||||
display: none;
|
||||
}
|
||||
.lg\:block {
|
||||
display: block;
|
||||
}
|
||||
.lg\:flex {
|
||||
display: flex;
|
||||
}
|
||||
.lg\:grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.lg\:items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.lg\:justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.lg\:text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.lg\:text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.lg\:p-4 {
|
||||
padding: var(--roa-spacing-lg);
|
||||
}
|
||||
.lg\:m-4 {
|
||||
margin: var(--roa-spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.print\:hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print\:block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
* {
|
||||
color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support (if implemented in the future) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-gray-800 {
|
||||
background-color: var(--roa-gray-800);
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dark\:border-gray-600 {
|
||||
border-color: var(--roa-gray-600);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user