BOLO-KESARI commited on
Commit ·
d2426db
0
Parent(s):
Initial commit for deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .agent/rules/check.md +6 -0
- .agent/workflows/pre-commit-check.md +72 -0
- .agent/workflows/quality-check.md +134 -0
- .agent/workflows/weekly-audit.md +105 -0
- .github/workflows/deploy_and_keep_alive.yml +34 -0
- .gitignore +37 -0
- AGENT_USAGE.md +196 -0
- DEPLOYMENT_GUIDE.md +70 -0
- Dockerfile +35 -0
- README.md +50 -0
- Ticker_List_NSE_India.csv +0 -0
- agent_config.yaml +94 -0
- app.py +38 -0
- backend/.env.example +11 -0
- backend/Ticker_List_NSE_India.csv +0 -0
- backend/app/__init__.py +1 -0
- backend/app/core/__init__.py +1 -0
- backend/app/core/config.py +36 -0
- backend/app/core/database.py +32 -0
- backend/app/core/security.py +157 -0
- backend/app/main.py +51 -0
- backend/app/models/__init__.py +3 -0
- backend/app/models/portfolio.py +30 -0
- backend/app/models/user.py +33 -0
- backend/app/routers/__init__.py +1 -0
- backend/app/routers/auth.py +116 -0
- backend/app/routers/portfolio.py +136 -0
- backend/app/routers/stocks.py +158 -0
- backend/app/schemas/__init__.py +1 -0
- backend/app/schemas/auth.py +45 -0
- backend/app/schemas/portfolio.py +43 -0
- backend/app/services/__init__.py +3 -0
- backend/app/services/stock_service.py +150 -0
- backend/create_account.py +40 -0
- backend/database_schema.sql +36 -0
- backend/init_db.py +14 -0
- backend/requirements.txt +9 -0
- backend/stock_analysis.db +0 -0
- backend/test_api.py +70 -0
- backend/test_stocks.py +41 -0
- code_quality_agent.py +293 -0
- frontend/README.md +260 -0
- frontend/css/animations.css +177 -0
- frontend/css/auth.css +402 -0
- frontend/css/base.css +127 -0
- frontend/css/variables.css +58 -0
- frontend/dashboard.html +624 -0
- frontend/dashboard_direct.html +437 -0
- frontend/index.html +776 -0
- frontend/js/auth.js +237 -0
.agent/rules/check.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
trigger: always_on
|
| 3 |
+
glob:
|
| 4 |
+
description:
|
| 5 |
+
---
|
| 6 |
+
|
.agent/workflows/pre-commit-check.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Pre-commit quality checks before pushing code
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
# Pre-Commit Quality Check Workflow
|
| 6 |
+
|
| 7 |
+
Run this workflow before committing code to ensure quality and catch issues early.
|
| 8 |
+
|
| 9 |
+
// turbo-all
|
| 10 |
+
|
| 11 |
+
## Steps
|
| 12 |
+
|
| 13 |
+
### 1. Format Code
|
| 14 |
+
|
| 15 |
+
Clean and format all Python files:
|
| 16 |
+
```bash
|
| 17 |
+
python code_quality_agent.py --module clean
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
### 2. Security Scan
|
| 21 |
+
|
| 22 |
+
Check for security vulnerabilities:
|
| 23 |
+
```bash
|
| 24 |
+
python code_quality_agent.py --module security
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 3. Run Tests
|
| 28 |
+
|
| 29 |
+
Execute tests with coverage:
|
| 30 |
+
```bash
|
| 31 |
+
python code_quality_agent.py --module test
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### 4. Review Report
|
| 35 |
+
|
| 36 |
+
Open the quality report:
|
| 37 |
+
```bash
|
| 38 |
+
start quality_reports\latest_report.html
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 5. Check for Critical Issues
|
| 42 |
+
|
| 43 |
+
Review the console output for any CRITICAL or HIGH severity issues.
|
| 44 |
+
|
| 45 |
+
**If any CRITICAL issues found**: Fix them before committing!
|
| 46 |
+
|
| 47 |
+
**If any HIGH issues found**: Consider fixing before committing.
|
| 48 |
+
|
| 49 |
+
**If tests fail**: Fix failing tests before committing.
|
| 50 |
+
|
| 51 |
+
### 6. Stage and Commit
|
| 52 |
+
|
| 53 |
+
Once all checks pass:
|
| 54 |
+
```bash
|
| 55 |
+
git add .
|
| 56 |
+
git commit -m "Your commit message"
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Quick Pre-Commit
|
| 60 |
+
|
| 61 |
+
For a fast check, run:
|
| 62 |
+
```bash
|
| 63 |
+
python code_quality_agent.py --module clean
|
| 64 |
+
python code_quality_agent.py --module security
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## Full Pre-Commit
|
| 68 |
+
|
| 69 |
+
For comprehensive check:
|
| 70 |
+
```bash
|
| 71 |
+
python code_quality_agent.py --all
|
| 72 |
+
```
|
.agent/workflows/quality-check.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Run code quality checks with the automated agent
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
# Code Quality Check Workflow
|
| 6 |
+
|
| 7 |
+
This workflow runs the automated code quality agent to check your code for issues, vulnerabilities, and test coverage.
|
| 8 |
+
|
| 9 |
+
## Prerequisites
|
| 10 |
+
|
| 11 |
+
Ensure dependencies are installed:
|
| 12 |
+
```bash
|
| 13 |
+
pip install -r requirements-dev.txt
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
## Steps
|
| 17 |
+
|
| 18 |
+
### 1. Quick Security Scan
|
| 19 |
+
|
| 20 |
+
Run a fast security check to find vulnerabilities:
|
| 21 |
+
```bash
|
| 22 |
+
python code_quality_agent.py --module security
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
This checks for:
|
| 26 |
+
- Security vulnerabilities with Bandit
|
| 27 |
+
- Vulnerable dependencies with Safety
|
| 28 |
+
- CORS configuration issues
|
| 29 |
+
- DEBUG mode in production
|
| 30 |
+
- Weak secret keys
|
| 31 |
+
|
| 32 |
+
### 2. Format and Clean Code
|
| 33 |
+
|
| 34 |
+
// turbo
|
| 35 |
+
Clean and format all Python files:
|
| 36 |
+
```bash
|
| 37 |
+
python code_quality_agent.py --module clean
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
This will:
|
| 41 |
+
- Format code with Black (PEP 8)
|
| 42 |
+
- Sort imports with isort
|
| 43 |
+
- Remove unused imports and variables
|
| 44 |
+
|
| 45 |
+
### 3. Test Database and API Connections
|
| 46 |
+
|
| 47 |
+
// turbo
|
| 48 |
+
Verify all connections are working:
|
| 49 |
+
```bash
|
| 50 |
+
python code_quality_agent.py --module connections
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**Note**: Make sure your backend is running first:
|
| 54 |
+
```bash
|
| 55 |
+
cd backend
|
| 56 |
+
uvicorn app.main:app --reload
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### 4. Run All Tests with Coverage
|
| 60 |
+
|
| 61 |
+
// turbo
|
| 62 |
+
Execute all tests and generate coverage reports:
|
| 63 |
+
```bash
|
| 64 |
+
python code_quality_agent.py --module test
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 5. Full Quality Check (All Modules)
|
| 68 |
+
|
| 69 |
+
Run everything at once:
|
| 70 |
+
```bash
|
| 71 |
+
python code_quality_agent.py --all
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
### 6. View Reports
|
| 75 |
+
|
| 76 |
+
// turbo
|
| 77 |
+
Open the HTML report in your browser:
|
| 78 |
+
```bash
|
| 79 |
+
start quality_reports\latest_report.html
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## Quick Commands
|
| 83 |
+
|
| 84 |
+
**Dry Run (Preview Only)**:
|
| 85 |
+
```bash
|
| 86 |
+
python code_quality_agent.py --dry-run
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
**Before Committing**:
|
| 90 |
+
```bash
|
| 91 |
+
python code_quality_agent.py --module clean
|
| 92 |
+
python code_quality_agent.py --module security
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
**CI/CD Integration**:
|
| 96 |
+
```bash
|
| 97 |
+
python code_quality_agent.py --all
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Understanding Results
|
| 101 |
+
|
| 102 |
+
- **Green/PASSED**: Everything is good ✅
|
| 103 |
+
- **Yellow/WARNING**: Minor issues to review ⚠️
|
| 104 |
+
- **Red/FAILED**: Critical issues to fix ❌
|
| 105 |
+
|
| 106 |
+
### Security Severity Levels
|
| 107 |
+
|
| 108 |
+
- **CRITICAL**: Fix immediately (e.g., hardcoded secrets)
|
| 109 |
+
- **HIGH**: Fix soon (e.g., SQL injection risks)
|
| 110 |
+
- **MEDIUM**: Should fix (e.g., DEBUG mode enabled)
|
| 111 |
+
- **LOW**: Nice to fix (e.g., minor best practices)
|
| 112 |
+
|
| 113 |
+
## Customization
|
| 114 |
+
|
| 115 |
+
Edit `agent_config.yaml` to customize:
|
| 116 |
+
- Enable/disable specific modules
|
| 117 |
+
- Set coverage thresholds
|
| 118 |
+
- Configure excluded directories
|
| 119 |
+
- Adjust severity levels
|
| 120 |
+
|
| 121 |
+
## Troubleshooting
|
| 122 |
+
|
| 123 |
+
**"Module not found" errors**:
|
| 124 |
+
```bash
|
| 125 |
+
pip install -r requirements-dev.txt
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**Connection tests failing**:
|
| 129 |
+
- Ensure backend server is running on port 8000
|
| 130 |
+
- Check if database file exists
|
| 131 |
+
|
| 132 |
+
**No files found**:
|
| 133 |
+
- Verify you're in the project root directory
|
| 134 |
+
- Check `exclude_dirs` in `agent_config.yaml`
|
.agent/workflows/weekly-audit.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Weekly comprehensive code quality audit
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
# Weekly Code Quality Audit Workflow
|
| 6 |
+
|
| 7 |
+
Run this workflow weekly to maintain code quality and track improvements over time.
|
| 8 |
+
|
| 9 |
+
## Steps
|
| 10 |
+
|
| 11 |
+
### 1. Update Dependencies
|
| 12 |
+
|
| 13 |
+
Check for outdated packages:
|
| 14 |
+
```bash
|
| 15 |
+
pip list --outdated
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### 2. Run Full Quality Check
|
| 19 |
+
|
| 20 |
+
Execute all quality modules:
|
| 21 |
+
```bash
|
| 22 |
+
python code_quality_agent.py --all
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### 3. Start Backend Server
|
| 26 |
+
|
| 27 |
+
For connection tests:
|
| 28 |
+
```bash
|
| 29 |
+
cd backend
|
| 30 |
+
uvicorn app.main:app --reload
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 4. Run Connection Tests
|
| 34 |
+
|
| 35 |
+
In a new terminal:
|
| 36 |
+
```bash
|
| 37 |
+
python code_quality_agent.py --module connections
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### 5. Review Security Issues
|
| 41 |
+
|
| 42 |
+
Open the HTML report and focus on security section:
|
| 43 |
+
```bash
|
| 44 |
+
start quality_reports\latest_report.html
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
**Review**:
|
| 48 |
+
- Any new CRITICAL or HIGH severity issues?
|
| 49 |
+
- Are there vulnerable dependencies?
|
| 50 |
+
- Is CORS configuration appropriate?
|
| 51 |
+
- Is DEBUG mode disabled for production?
|
| 52 |
+
|
| 53 |
+
### 6. Check Code Coverage
|
| 54 |
+
|
| 55 |
+
Review test coverage percentage:
|
| 56 |
+
- **Target**: >70% coverage
|
| 57 |
+
- **Good**: >80% coverage
|
| 58 |
+
- **Excellent**: >90% coverage
|
| 59 |
+
|
| 60 |
+
If coverage is low, identify untested modules and add tests.
|
| 61 |
+
|
| 62 |
+
### 7. Review Code Quality Scores
|
| 63 |
+
|
| 64 |
+
Check Pylint score:
|
| 65 |
+
- **Target**: >7.0/10
|
| 66 |
+
- **Good**: >8.0/10
|
| 67 |
+
- **Excellent**: >9.0/10
|
| 68 |
+
|
| 69 |
+
### 8. Compare with Previous Week
|
| 70 |
+
|
| 71 |
+
Check historical reports:
|
| 72 |
+
```bash
|
| 73 |
+
explorer quality_reports\history
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**Track trends**:
|
| 77 |
+
- Is coverage improving?
|
| 78 |
+
- Are security issues decreasing?
|
| 79 |
+
- Is code quality score stable or improving?
|
| 80 |
+
|
| 81 |
+
### 9. Create Action Items
|
| 82 |
+
|
| 83 |
+
Based on the report, create tasks for:
|
| 84 |
+
- Fixing CRITICAL/HIGH security issues
|
| 85 |
+
- Improving test coverage
|
| 86 |
+
- Addressing code quality issues
|
| 87 |
+
- Updating vulnerable dependencies
|
| 88 |
+
|
| 89 |
+
### 10. Document Findings
|
| 90 |
+
|
| 91 |
+
Create a summary of:
|
| 92 |
+
- Issues found and fixed
|
| 93 |
+
- Coverage improvements
|
| 94 |
+
- Quality score changes
|
| 95 |
+
- Action items for next week
|
| 96 |
+
|
| 97 |
+
## Quick Weekly Check
|
| 98 |
+
|
| 99 |
+
For a faster audit:
|
| 100 |
+
```bash
|
| 101 |
+
python code_quality_agent.py --all
|
| 102 |
+
start quality_reports\latest_report.html
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
Then review the summary dashboard for key metrics.
|
.github/workflows/deploy_and_keep_alive.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Keep Alive & Deploy
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
# Run every 12 hours
|
| 6 |
+
- cron: '0 */12 * * *'
|
| 7 |
+
push:
|
| 8 |
+
branches: [ main ]
|
| 9 |
+
workflow_dispatch:
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
ping_server:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
steps:
|
| 15 |
+
- name: Ping Hugging Face Space
|
| 16 |
+
run: |
|
| 17 |
+
# Replace with your actual Space URL
|
| 18 |
+
curl -s https://huggingface.co/spaces/${{ secrets.HF_USERNAME }}/${{ secrets.HF_SPACE_NAME }}/health || echo "Ping complete"
|
| 19 |
+
|
| 20 |
+
sync_to_hub:
|
| 21 |
+
runs-on: ubuntu-latest
|
| 22 |
+
needs: ping_server
|
| 23 |
+
if: github.event_name == 'push'
|
| 24 |
+
steps:
|
| 25 |
+
- uses: actions/checkout@v3
|
| 26 |
+
with:
|
| 27 |
+
fetch-depth: 0
|
| 28 |
+
- name: Push to Hugging Face Hub
|
| 29 |
+
env:
|
| 30 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 31 |
+
HF_USERNAME: ${{ secrets.HF_USERNAME }}
|
| 32 |
+
HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }}
|
| 33 |
+
run: |
|
| 34 |
+
git push https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/$HF_USERNAME/$HF_SPACE_NAME main
|
.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment variables
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
*.so
|
| 9 |
+
.Python
|
| 10 |
+
env/
|
| 11 |
+
venv/
|
| 12 |
+
ENV/
|
| 13 |
+
.venv
|
| 14 |
+
|
| 15 |
+
# IDE
|
| 16 |
+
.vscode/
|
| 17 |
+
.idea/
|
| 18 |
+
*.swp
|
| 19 |
+
*.swo
|
| 20 |
+
*~
|
| 21 |
+
|
| 22 |
+
# OS
|
| 23 |
+
.DS_Store
|
| 24 |
+
Thumbs.db
|
| 25 |
+
|
| 26 |
+
# Testing
|
| 27 |
+
.pytest_cache/
|
| 28 |
+
.coverage
|
| 29 |
+
htmlcov/
|
| 30 |
+
|
| 31 |
+
# Distribution
|
| 32 |
+
build/
|
| 33 |
+
dist/
|
| 34 |
+
*.egg-info/
|
| 35 |
+
|
| 36 |
+
# Code Quality Reports
|
| 37 |
+
quality_reports/
|
AGENT_USAGE.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code Quality Agent - Usage Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Code Quality Agent is an automated tool that performs comprehensive code quality checks including:
|
| 6 |
+
- **Code Cleaning**: Format code with Black, sort imports, remove unused code
|
| 7 |
+
- **Security Scanning**: Check for vulnerabilities with Bandit and Safety
|
| 8 |
+
- **Connection Testing**: Verify database and API connections
|
| 9 |
+
- **Code Testing**: Run tests with coverage reporting
|
| 10 |
+
|
| 11 |
+
## Installation
|
| 12 |
+
|
| 13 |
+
1. **Install Development Dependencies**:
|
| 14 |
+
```bash
|
| 15 |
+
pip install -r requirements-dev.txt
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
2. **Verify Installation**:
|
| 19 |
+
```bash
|
| 20 |
+
python code_quality_agent.py --help
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Quick Start
|
| 24 |
+
|
| 25 |
+
### Run All Checks
|
| 26 |
+
```bash
|
| 27 |
+
python code_quality_agent.py --all
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Preview Changes (Dry Run)
|
| 31 |
+
```bash
|
| 32 |
+
python code_quality_agent.py --dry-run
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Run Specific Module
|
| 36 |
+
```bash
|
| 37 |
+
# Code cleaning only
|
| 38 |
+
python code_quality_agent.py --module clean
|
| 39 |
+
|
| 40 |
+
# Security checks only
|
| 41 |
+
python code_quality_agent.py --module security
|
| 42 |
+
|
| 43 |
+
# Connection tests only
|
| 44 |
+
python code_quality_agent.py --module connections
|
| 45 |
+
|
| 46 |
+
# Code tests only
|
| 47 |
+
python code_quality_agent.py --module test
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## Configuration
|
| 51 |
+
|
| 52 |
+
Edit `agent_config.yaml` to customize behavior:
|
| 53 |
+
|
| 54 |
+
```yaml
|
| 55 |
+
# Enable/disable modules
|
| 56 |
+
modules:
|
| 57 |
+
code_cleaner: true
|
| 58 |
+
vulnerability_checker: true
|
| 59 |
+
connection_tester: true
|
| 60 |
+
code_tester: true
|
| 61 |
+
|
| 62 |
+
# Customize thresholds
|
| 63 |
+
code_tester:
|
| 64 |
+
coverage_threshold: 70 # minimum percentage
|
| 65 |
+
pylint_threshold: 7.0 # minimum score
|
| 66 |
+
|
| 67 |
+
# Exclude directories
|
| 68 |
+
code_cleaner:
|
| 69 |
+
exclude_dirs:
|
| 70 |
+
- "venv"
|
| 71 |
+
- "__pycache__"
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## Understanding Reports
|
| 75 |
+
|
| 76 |
+
### HTML Report
|
| 77 |
+
- Open `quality_reports/latest_report.html` in your browser
|
| 78 |
+
- Interactive dashboard with charts and metrics
|
| 79 |
+
- Color-coded severity levels
|
| 80 |
+
|
| 81 |
+
### JSON Report
|
| 82 |
+
- Located at `quality_reports/latest_report.json`
|
| 83 |
+
- Machine-readable format for CI/CD integration
|
| 84 |
+
- Contains all raw data
|
| 85 |
+
|
| 86 |
+
### Console Summary
|
| 87 |
+
- Quick overview printed to terminal
|
| 88 |
+
- Shows key metrics and pass/fail status
|
| 89 |
+
|
| 90 |
+
## Common Workflows
|
| 91 |
+
|
| 92 |
+
### Before Committing Code
|
| 93 |
+
```bash
|
| 94 |
+
# Check everything
|
| 95 |
+
python code_quality_agent.py --all
|
| 96 |
+
|
| 97 |
+
# Review the HTML report
|
| 98 |
+
# Fix any issues found
|
| 99 |
+
# Commit your changes
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### CI/CD Integration
|
| 103 |
+
```bash
|
| 104 |
+
# In your CI pipeline
|
| 105 |
+
python code_quality_agent.py --all
|
| 106 |
+
|
| 107 |
+
# Check exit code
|
| 108 |
+
if [ $? -ne 0 ]; then
|
| 109 |
+
echo "Quality checks failed"
|
| 110 |
+
exit 1
|
| 111 |
+
fi
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Code Review Preparation
|
| 115 |
+
```bash
|
| 116 |
+
# Clean code first
|
| 117 |
+
python code_quality_agent.py --module clean
|
| 118 |
+
|
| 119 |
+
# Then run all checks
|
| 120 |
+
python code_quality_agent.py --all
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## Interpreting Results
|
| 124 |
+
|
| 125 |
+
### Code Cleaning
|
| 126 |
+
- **Files Processed**: Total Python files scanned
|
| 127 |
+
- **Formatted**: Files modified by Black
|
| 128 |
+
- **Errors**: Issues encountered during cleaning
|
| 129 |
+
|
| 130 |
+
### Security
|
| 131 |
+
- **CRITICAL**: Immediate attention required (e.g., hardcoded secrets)
|
| 132 |
+
- **HIGH**: Serious issues (e.g., SQL injection risks)
|
| 133 |
+
- **MEDIUM**: Important to fix (e.g., DEBUG mode enabled)
|
| 134 |
+
- **LOW**: Minor issues or best practices
|
| 135 |
+
|
| 136 |
+
### Connections
|
| 137 |
+
- **PASSED**: Connection successful
|
| 138 |
+
- **FAILED**: Connection failed (check if server is running)
|
| 139 |
+
- **SKIPPED**: Test was not applicable
|
| 140 |
+
|
| 141 |
+
### Code Tests
|
| 142 |
+
- **Coverage**: Percentage of code covered by tests (aim for >70%)
|
| 143 |
+
- **Pylint Score**: Code quality score out of 10 (aim for >7.0)
|
| 144 |
+
- **Flake8**: Style issues found
|
| 145 |
+
|
| 146 |
+
## Troubleshooting
|
| 147 |
+
|
| 148 |
+
### "Module not found" errors
|
| 149 |
+
```bash
|
| 150 |
+
# Reinstall dependencies
|
| 151 |
+
pip install -r requirements-dev.txt
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Connection tests failing
|
| 155 |
+
```bash
|
| 156 |
+
# Make sure your backend is running
|
| 157 |
+
cd backend
|
| 158 |
+
uvicorn app.main:app --reload
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
### No Python files found
|
| 162 |
+
- Check `exclude_dirs` in config
|
| 163 |
+
- Verify you're running from project root
|
| 164 |
+
|
| 165 |
+
## Advanced Usage
|
| 166 |
+
|
| 167 |
+
### Custom Config File
|
| 168 |
+
```bash
|
| 169 |
+
python code_quality_agent.py --config my_config.yaml
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### Dry Run with Specific Module
|
| 173 |
+
```bash
|
| 174 |
+
python code_quality_agent.py --module clean --dry-run
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### View Historical Reports
|
| 178 |
+
```bash
|
| 179 |
+
# Reports are saved in quality_reports/history/
|
| 180 |
+
ls quality_reports/history/
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
## Tips
|
| 184 |
+
|
| 185 |
+
1. **Run dry-run first** to preview changes before applying
|
| 186 |
+
2. **Fix CRITICAL issues immediately** - they're security risks
|
| 187 |
+
3. **Aim for >70% coverage** for good test coverage
|
| 188 |
+
4. **Run before every commit** to maintain code quality
|
| 189 |
+
5. **Review HTML report** for detailed insights
|
| 190 |
+
|
| 191 |
+
## Support
|
| 192 |
+
|
| 193 |
+
For issues or questions:
|
| 194 |
+
1. Check the logs in `quality_reports/agent.log`
|
| 195 |
+
2. Review the configuration in `agent_config.yaml`
|
| 196 |
+
3. Ensure all dependencies are installed
|
DEPLOYMENT_GUIDE.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide covers how to deploy the Stock Analysis Application to Hugging Face Spaces, Vercel, and GitHub.
|
| 4 |
+
|
| 5 |
+
## 1. Hugging Face Spaces Deployment (Recommended for Full Features)
|
| 6 |
+
|
| 7 |
+
Hugging Face Spaces supports Docker, which is perfect for this app.
|
| 8 |
+
|
| 9 |
+
1. Create a new Space on [Hugging Face](https://huggingface.co/spaces).
|
| 10 |
+
2. Select **Docker** as the SDK.
|
| 11 |
+
3. Choose a name (e.g., `stock-analysis`).
|
| 12 |
+
4. In your local project, initialize Git if you haven't already:
|
| 13 |
+
```bash
|
| 14 |
+
git init
|
| 15 |
+
git add .
|
| 16 |
+
git commit -m "Initial commit"
|
| 17 |
+
```
|
| 18 |
+
5. Add the Hugging Face remote (replace `USERNAME` and `SPACE_NAME`):
|
| 19 |
+
```bash
|
| 20 |
+
git remote add hf https://huggingface.co/spaces/USERNAME/SPACE_NAME
|
| 21 |
+
```
|
| 22 |
+
6. Push to Hugging Face:
|
| 23 |
+
```bash
|
| 24 |
+
git push hf main
|
| 25 |
+
```
|
| 26 |
+
*Note: If you have large files (like `stock_analysis.db`), you might need to use `git-lfs` or exclude them via `.gitignore`.*
|
| 27 |
+
|
| 28 |
+
## 2. GitHub Actions Automation & Keep Alive
|
| 29 |
+
|
| 30 |
+
We've included a GitHub Action that:
|
| 31 |
+
- Automatically syncs your code to Hugging Face when you push to GitHub.
|
| 32 |
+
- Pings your app every 12 hours to prevent it from sleeping (if on a free tier).
|
| 33 |
+
|
| 34 |
+
### Setup:
|
| 35 |
+
1. Push your code to a GitHub repository.
|
| 36 |
+
2. Go to **Settings** > **Secrets and variables** > **Actions**.
|
| 37 |
+
3. Add the following Repository Secrets:
|
| 38 |
+
- `HF_TOKEN`: Your Hugging Face Access Token (Write permissions).
|
| 39 |
+
- `HF_USERNAME`: Your Hugging Face username.
|
| 40 |
+
- `HF_SPACE_NAME`: The name of your Space.
|
| 41 |
+
|
| 42 |
+
The workflow file is located at `.github/workflows/deploy_and_keep_alive.yml`.
|
| 43 |
+
|
| 44 |
+
## 3. Vercel Deployment
|
| 45 |
+
|
| 46 |
+
Vercel is great for the frontend, but requires specific configuration for Python backends (serverless).
|
| 47 |
+
|
| 48 |
+
1. Install Vercel CLI: `npm i -g vercel`
|
| 49 |
+
2. Login: `vercel login`
|
| 50 |
+
3. Deploy:
|
| 51 |
+
```bash
|
| 52 |
+
vercel
|
| 53 |
+
```
|
| 54 |
+
4. The `vercel.json` configuration file handles the routing to the Python backend.
|
| 55 |
+
|
| 56 |
+
## 4. Local "Keep Alive" Script
|
| 57 |
+
|
| 58 |
+
If you prefer to run a keep-alive script from your own machine:
|
| 59 |
+
|
| 60 |
+
1. Edit `keep_alive.py` and set your URL.
|
| 61 |
+
2. Run it:
|
| 62 |
+
```bash
|
| 63 |
+
python keep_alive.py
|
| 64 |
+
```
|
| 65 |
+
It will ping your server every 12 hours.
|
| 66 |
+
|
| 67 |
+
## Important Notes
|
| 68 |
+
|
| 69 |
+
- **Database**: The SQLite database (`stock_analysis.db`) is file-based. On Hugging Face Spaces (Docker), it will reset if the container restarts unless you set up persistent storage (Hugging Face "Persistent Storage" dataset or upgrade the space). For production, consider using an external database (PostgreSQL/Supabase).
|
| 70 |
+
- **Environment Variables**: Make sure to set any secrets (like database URLs or API keys) in the Settings tab of your deployment platform.
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.11
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set up a new user named "user" with user ID 1000
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
|
| 7 |
+
# Switch to the "user" user
|
| 8 |
+
USER user
|
| 9 |
+
|
| 10 |
+
# Set home to the user's home directory
|
| 11 |
+
ENV HOME=/home/user \
|
| 12 |
+
PATH=/home/user/.local/bin:$PATH
|
| 13 |
+
|
| 14 |
+
# Set the working directory to the user's home directory
|
| 15 |
+
WORKDIR $HOME/app
|
| 16 |
+
|
| 17 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 18 |
+
COPY --chown=user . $HOME/app
|
| 19 |
+
|
| 20 |
+
# Install requirements
|
| 21 |
+
# Note: We use the backend/requirements.txt
|
| 22 |
+
RUN pip install --no-cache-dir --upgrade -r backend/requirements.txt
|
| 23 |
+
|
| 24 |
+
# Create necessary directories within the user's space
|
| 25 |
+
RUN mkdir -p $HOME/app/data
|
| 26 |
+
|
| 27 |
+
# Exposure port (Hugging Face Spaces default)
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Health Check
|
| 31 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 32 |
+
CMD python -c "import requests; requests.get('http://localhost:7860/health')"
|
| 33 |
+
|
| 34 |
+
# Run the application using the app.py entry point
|
| 35 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📈 Stock Analysis Application
|
| 2 |
+
|
| 3 |
+
Complete stock analysis platform with authentication and real-time market data.
|
| 4 |
+
|
| 5 |
+
## 🚀 Quick Start
|
| 6 |
+
|
| 7 |
+
### 1. Start Backend
|
| 8 |
+
```bash
|
| 9 |
+
cd backend
|
| 10 |
+
python -m uvicorn app.main:app --reload
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### 2. Open Application
|
| 14 |
+
Open `frontend/index.html` in your browser
|
| 15 |
+
|
| 16 |
+
### 3. Login
|
| 17 |
+
**Test Account:**
|
| 18 |
+
- Email: `admin@test.com`
|
| 19 |
+
- Password: `SecurePass123!`
|
| 20 |
+
|
| 21 |
+
### 4. View Dashboard
|
| 22 |
+
Stocks display instantly after login!
|
| 23 |
+
|
| 24 |
+
## ✨ Features
|
| 25 |
+
|
| 26 |
+
- ✅ Secure authentication (JWT)
|
| 27 |
+
- ✅ Real-time stock data (Yahoo Finance)
|
| 28 |
+
- ✅ 5 popular stocks: AAPL, GOOGL, MSFT, TSLA, AMZN
|
| 29 |
+
- ✅ Instant loading dashboard
|
| 30 |
+
- ✅ Color-coded price changes
|
| 31 |
+
- ✅ Auto-refresh capability
|
| 32 |
+
- ✅ Beautiful cyberpunk UI
|
| 33 |
+
|
| 34 |
+
## 📊 Stock Data
|
| 35 |
+
|
| 36 |
+
Each stock card shows:
|
| 37 |
+
- Current price
|
| 38 |
+
- Change % (color-coded)
|
| 39 |
+
- Dollar change
|
| 40 |
+
- Trading volume
|
| 41 |
+
|
| 42 |
+
## 🔧 Tech Stack
|
| 43 |
+
|
| 44 |
+
**Backend:** FastAPI, SQLAlchemy, yfinance, JWT
|
| 45 |
+
**Frontend:** HTML, CSS, JavaScript
|
| 46 |
+
**Database:** SQLite (local development)
|
| 47 |
+
|
| 48 |
+
## ✅ Status
|
| 49 |
+
|
| 50 |
+
**Production Ready** - All features working!
|
Ticker_List_NSE_India.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
agent_config.yaml
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code Quality Agent Configuration
|
| 2 |
+
|
| 3 |
+
# General Settings
|
| 4 |
+
general:
|
| 5 |
+
project_name: "Stock Analysis"
|
| 6 |
+
project_root: "."
|
| 7 |
+
output_dir: "quality_reports"
|
| 8 |
+
log_level: "INFO" # DEBUG, INFO, WARNING, ERROR
|
| 9 |
+
|
| 10 |
+
# Modules to Enable/Disable
|
| 11 |
+
modules:
|
| 12 |
+
code_cleaner: true
|
| 13 |
+
vulnerability_checker: true
|
| 14 |
+
connection_tester: true
|
| 15 |
+
code_tester: true
|
| 16 |
+
|
| 17 |
+
# Code Cleaner Settings
|
| 18 |
+
code_cleaner:
|
| 19 |
+
enabled: true
|
| 20 |
+
format_with_black: true
|
| 21 |
+
sort_imports: true
|
| 22 |
+
remove_unused_imports: true
|
| 23 |
+
line_length: 100
|
| 24 |
+
exclude_dirs:
|
| 25 |
+
- "venv"
|
| 26 |
+
- ".venv"
|
| 27 |
+
- "env"
|
| 28 |
+
- "__pycache__"
|
| 29 |
+
- ".git"
|
| 30 |
+
- "node_modules"
|
| 31 |
+
- "quality_reports"
|
| 32 |
+
exclude_files:
|
| 33 |
+
- "*.pyc"
|
| 34 |
+
- "*.pyo"
|
| 35 |
+
|
| 36 |
+
# Vulnerability Checker Settings
|
| 37 |
+
vulnerability_checker:
|
| 38 |
+
enabled: true
|
| 39 |
+
run_bandit: true
|
| 40 |
+
run_safety: true
|
| 41 |
+
custom_security_checks: true
|
| 42 |
+
severity_threshold: "LOW" # LOW, MEDIUM, HIGH, CRITICAL
|
| 43 |
+
exclude_dirs:
|
| 44 |
+
- "venv"
|
| 45 |
+
- ".venv"
|
| 46 |
+
- "tests"
|
| 47 |
+
bandit_config:
|
| 48 |
+
confidence_level: "LOW"
|
| 49 |
+
severity_level: "LOW"
|
| 50 |
+
|
| 51 |
+
# Connection Tester Settings
|
| 52 |
+
connection_tester:
|
| 53 |
+
enabled: true
|
| 54 |
+
test_database: true
|
| 55 |
+
test_api_endpoints: true
|
| 56 |
+
test_health_endpoints: true
|
| 57 |
+
test_authentication: true
|
| 58 |
+
api_base_url: "http://localhost:8000"
|
| 59 |
+
timeout: 10 # seconds
|
| 60 |
+
endpoints_to_test:
|
| 61 |
+
- path: "/"
|
| 62 |
+
method: "GET"
|
| 63 |
+
expected_status: 200
|
| 64 |
+
- path: "/health"
|
| 65 |
+
method: "GET"
|
| 66 |
+
expected_status: 200
|
| 67 |
+
- path: "/docs"
|
| 68 |
+
method: "GET"
|
| 69 |
+
expected_status: 200
|
| 70 |
+
|
| 71 |
+
# Code Tester Settings
|
| 72 |
+
code_tester:
|
| 73 |
+
enabled: true
|
| 74 |
+
run_pytest: true
|
| 75 |
+
run_pylint: true
|
| 76 |
+
run_flake8: true
|
| 77 |
+
generate_coverage: true
|
| 78 |
+
coverage_threshold: 70 # minimum percentage
|
| 79 |
+
test_directories:
|
| 80 |
+
- "backend"
|
| 81 |
+
pytest_args:
|
| 82 |
+
- "-v"
|
| 83 |
+
- "--tb=short"
|
| 84 |
+
pylint_threshold: 7.0 # minimum score out of 10
|
| 85 |
+
|
| 86 |
+
# Report Settings
|
| 87 |
+
reporting:
|
| 88 |
+
generate_html: true
|
| 89 |
+
generate_json: true
|
| 90 |
+
generate_console_summary: true
|
| 91 |
+
include_charts: true
|
| 92 |
+
open_html_after_run: true
|
| 93 |
+
save_history: true
|
| 94 |
+
max_history_reports: 10
|
app.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Spaces entry point for Stock Analysis Application.
|
| 3 |
+
This file serves both the FastAPI backend and static frontend files.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import uvicorn
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
+
from fastapi.responses import FileResponse
|
| 10 |
+
from backend.app.main import app as backend_app
|
| 11 |
+
|
| 12 |
+
# Mount static files
|
| 13 |
+
backend_app.mount("/static", StaticFiles(directory="frontend"), name="static")
|
| 14 |
+
|
| 15 |
+
# Serve index.html at root
|
| 16 |
+
@backend_app.get("/app")
|
| 17 |
+
async def serve_app():
|
| 18 |
+
"""Serve the main application page."""
|
| 19 |
+
return FileResponse("frontend/index.html")
|
| 20 |
+
|
| 21 |
+
@backend_app.get("/dashboard")
|
| 22 |
+
async def serve_dashboard():
|
| 23 |
+
"""Serve the dashboard page."""
|
| 24 |
+
return FileResponse("frontend/dashboard.html")
|
| 25 |
+
|
| 26 |
+
@backend_app.get("/portfolio")
|
| 27 |
+
async def serve_portfolio():
|
| 28 |
+
"""Serve the portfolio page."""
|
| 29 |
+
return FileResponse("frontend/portfolio.html")
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
port = int(os.getenv("PORT", 7860))
|
| 33 |
+
uvicorn.run(
|
| 34 |
+
backend_app,
|
| 35 |
+
host="0.0.0.0",
|
| 36 |
+
port=port,
|
| 37 |
+
log_level="info"
|
| 38 |
+
)
|
backend/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Database Configuration
|
| 2 |
+
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.zbnydktylyfpyzmldotn.supabase.co:5432/postgres
|
| 3 |
+
|
| 4 |
+
# JWT Configuration
|
| 5 |
+
SECRET_KEY=your-secret-key-here-change-this-in-production
|
| 6 |
+
ALGORITHM=HS256
|
| 7 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
| 8 |
+
|
| 9 |
+
# Application Settings
|
| 10 |
+
PROJECT_NAME=Stock Analysis API
|
| 11 |
+
DEBUG=True
|
backend/Ticker_List_NSE_India.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Stock Analysis Backend Application
|
backend/app/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Core utilities and configuration
|
backend/app/core/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Application configuration using Pydantic Settings.
|
| 3 |
+
Loads environment variables from .env file.
|
| 4 |
+
"""
|
| 5 |
+
from pydantic_settings import BaseSettings
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Settings(BaseSettings):
|
| 10 |
+
"""Application settings loaded from environment variables."""
|
| 11 |
+
|
| 12 |
+
# Database
|
| 13 |
+
DATABASE_URL: str
|
| 14 |
+
|
| 15 |
+
# JWT Settings
|
| 16 |
+
SECRET_KEY: str
|
| 17 |
+
ALGORITHM: str = "HS256"
|
| 18 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 43200 # 30 days
|
| 19 |
+
|
| 20 |
+
# Application
|
| 21 |
+
PROJECT_NAME: str = "Stock Analysis API"
|
| 22 |
+
DEBUG: bool = True
|
| 23 |
+
|
| 24 |
+
# CORS - Allow all origins for development (including file://)
|
| 25 |
+
BACKEND_CORS_ORIGINS: list = ["*"]
|
| 26 |
+
|
| 27 |
+
# Groq API
|
| 28 |
+
GROQ_API_KEY: str = ""
|
| 29 |
+
|
| 30 |
+
class Config:
|
| 31 |
+
env_file = ".env"
|
| 32 |
+
case_sensitive = True
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# Global settings instance
|
| 36 |
+
settings = Settings()
|
backend/app/core/database.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Database connection and session management using SQLAlchemy.
|
| 3 |
+
"""
|
| 4 |
+
from sqlalchemy import create_engine
|
| 5 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 6 |
+
from sqlalchemy.orm import sessionmaker
|
| 7 |
+
from .config import settings
|
| 8 |
+
|
| 9 |
+
# Create database engine
|
| 10 |
+
engine = create_engine(
|
| 11 |
+
settings.DATABASE_URL,
|
| 12 |
+
pool_pre_ping=True, # Verify connections before using
|
| 13 |
+
echo=settings.DEBUG # Log SQL queries in debug mode
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Session factory
|
| 17 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 18 |
+
|
| 19 |
+
# Base class for models
|
| 20 |
+
Base = declarative_base()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def get_db():
|
| 24 |
+
"""
|
| 25 |
+
Dependency function to get database session.
|
| 26 |
+
Yields a session and ensures it's closed after use.
|
| 27 |
+
"""
|
| 28 |
+
db = SessionLocal()
|
| 29 |
+
try:
|
| 30 |
+
yield db
|
| 31 |
+
finally:
|
| 32 |
+
db.close()
|
backend/app/core/security.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Security utilities for password hashing and JWT token management.
|
| 3 |
+
"""
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from jose import JWTError, jwt
|
| 7 |
+
from passlib.context import CryptContext
|
| 8 |
+
from fastapi import Depends, HTTPException, status
|
| 9 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 10 |
+
from sqlalchemy.orm import Session
|
| 11 |
+
|
| 12 |
+
from .config import settings
|
| 13 |
+
from .database import get_db
|
| 14 |
+
from ..models.user import User
|
| 15 |
+
|
| 16 |
+
# Password hashing context
|
| 17 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 18 |
+
|
| 19 |
+
# HTTP Bearer token scheme
|
| 20 |
+
security = HTTPBearer()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def hash_password(password: str) -> str:
|
| 24 |
+
"""
|
| 25 |
+
Hash a plain text password using bcrypt.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
password: Plain text password
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
Hashed password string
|
| 32 |
+
"""
|
| 33 |
+
return pwd_context.hash(password)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 37 |
+
"""
|
| 38 |
+
Verify a plain text password against a hashed password.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
plain_password: Plain text password to verify
|
| 42 |
+
hashed_password: Hashed password from database
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
True if password matches, False otherwise
|
| 46 |
+
"""
|
| 47 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
| 51 |
+
"""
|
| 52 |
+
Create a JWT access token.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
data: Dictionary of data to encode in the token
|
| 56 |
+
expires_delta: Optional custom expiration time
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Encoded JWT token string
|
| 60 |
+
"""
|
| 61 |
+
to_encode = data.copy()
|
| 62 |
+
|
| 63 |
+
if expires_delta:
|
| 64 |
+
expire = datetime.utcnow() + expires_delta
|
| 65 |
+
else:
|
| 66 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 67 |
+
|
| 68 |
+
to_encode.update({"exp": expire})
|
| 69 |
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 70 |
+
|
| 71 |
+
return encoded_jwt
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def decode_access_token(token: str) -> Optional[dict]:
|
| 75 |
+
"""
|
| 76 |
+
Decode and validate a JWT access token.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
token: JWT token string
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
Decoded token payload or None if invalid
|
| 83 |
+
"""
|
| 84 |
+
try:
|
| 85 |
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 86 |
+
return payload
|
| 87 |
+
except JWTError:
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
async def get_current_user(
|
| 92 |
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 93 |
+
db: Session = Depends(get_db)
|
| 94 |
+
) -> User:
|
| 95 |
+
"""
|
| 96 |
+
FastAPI dependency to get the current authenticated user from JWT token.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
credentials: HTTP Bearer credentials from request header
|
| 100 |
+
db: Database session
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
User object
|
| 104 |
+
|
| 105 |
+
Raises:
|
| 106 |
+
HTTPException: If token is invalid or user not found
|
| 107 |
+
"""
|
| 108 |
+
credentials_exception = HTTPException(
|
| 109 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 110 |
+
detail="Could not validate credentials",
|
| 111 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
token = credentials.credentials
|
| 115 |
+
payload = decode_access_token(token)
|
| 116 |
+
|
| 117 |
+
if payload is None:
|
| 118 |
+
raise credentials_exception
|
| 119 |
+
|
| 120 |
+
user_id: str = payload.get("sub")
|
| 121 |
+
if user_id is None:
|
| 122 |
+
raise credentials_exception
|
| 123 |
+
|
| 124 |
+
# Get user from database
|
| 125 |
+
user = db.query(User).filter(User.id == user_id).first()
|
| 126 |
+
|
| 127 |
+
if user is None:
|
| 128 |
+
raise credentials_exception
|
| 129 |
+
|
| 130 |
+
if not user.is_active:
|
| 131 |
+
raise HTTPException(
|
| 132 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 133 |
+
detail="Inactive user"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
return user
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def require_role(required_role: str):
|
| 140 |
+
"""
|
| 141 |
+
Dependency factory to require a specific user role.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
required_role: Role required (e.g., "ADMIN")
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
Dependency function that checks user role
|
| 148 |
+
"""
|
| 149 |
+
async def role_checker(current_user: User = Depends(get_current_user)) -> User:
|
| 150 |
+
if current_user.role != required_role:
|
| 151 |
+
raise HTTPException(
|
| 152 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 153 |
+
detail=f"Access denied. {required_role} role required."
|
| 154 |
+
)
|
| 155 |
+
return current_user
|
| 156 |
+
|
| 157 |
+
return role_checker
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main FastAPI application entry point.
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from .core.database import engine, Base
|
| 7 |
+
from .routers import auth, stocks, portfolio
|
| 8 |
+
from .models import User, Portfolio # Import models to ensure tables are created
|
| 9 |
+
|
| 10 |
+
# Create database tables
|
| 11 |
+
Base.metadata.create_all(bind=engine)
|
| 12 |
+
|
| 13 |
+
app = FastAPI(
|
| 14 |
+
title="Stock Analysis API",
|
| 15 |
+
description="Real-time stock market data and analysis API",
|
| 16 |
+
version="1.0.0"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# CORS middleware
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"], # In production, specify exact origins
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Include routers
|
| 29 |
+
app.include_router(auth.router)
|
| 30 |
+
app.include_router(stocks.router)
|
| 31 |
+
app.include_router(portfolio.router)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@app.get("/")
|
| 35 |
+
async def root():
|
| 36 |
+
"""Root endpoint - API health check."""
|
| 37 |
+
return {
|
| 38 |
+
"message": "Stock Analysis API",
|
| 39 |
+
"version": "1.0.0",
|
| 40 |
+
"status": "active"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.get("/health")
|
| 45 |
+
async def health_check():
|
| 46 |
+
"""Health check endpoint."""
|
| 47 |
+
return {
|
| 48 |
+
"status": "healthy",
|
| 49 |
+
"api": "operational",
|
| 50 |
+
"database": "connected"
|
| 51 |
+
}
|
backend/app/models/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Database models
|
| 2 |
+
from .user import User, UserRole
|
| 3 |
+
from .portfolio import Portfolio
|
backend/app/models/portfolio.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Portfolio database model for tracking user stock holdings.
|
| 3 |
+
"""
|
| 4 |
+
from sqlalchemy import Column, String, Float, Date, DateTime, ForeignKey
|
| 5 |
+
from sqlalchemy.orm import relationship
|
| 6 |
+
from datetime import datetime, date
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
from ..core.database import Base
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Portfolio(Base):
|
| 13 |
+
"""Portfolio model for tracking user stock holdings."""
|
| 14 |
+
|
| 15 |
+
__tablename__ = "portfolio"
|
| 16 |
+
|
| 17 |
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 18 |
+
user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True)
|
| 19 |
+
symbol = Column(String(50), nullable=False, index=True)
|
| 20 |
+
shares = Column(Float, nullable=False)
|
| 21 |
+
buying_date = Column(Date, nullable=False)
|
| 22 |
+
buying_price = Column(Float, nullable=True) # Optional: price at purchase
|
| 23 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 24 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 25 |
+
|
| 26 |
+
# Relationship to user
|
| 27 |
+
# user = relationship("User", back_populates="portfolio")
|
| 28 |
+
|
| 29 |
+
def __repr__(self):
|
| 30 |
+
return f"<Portfolio {self.symbol} - {self.shares} shares>"
|
backend/app/models/user.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
User database model using SQLAlchemy ORM.
|
| 3 |
+
"""
|
| 4 |
+
import enum
|
| 5 |
+
from sqlalchemy import Column, String, Boolean, Enum, DateTime
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
from ..core.database import Base
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class UserRole(str, enum.Enum):
|
| 13 |
+
"""User role enumeration."""
|
| 14 |
+
ADMIN = "ADMIN"
|
| 15 |
+
STAFF = "STAFF"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class User(Base):
|
| 19 |
+
"""User model for authentication and authorization."""
|
| 20 |
+
|
| 21 |
+
__tablename__ = "users"
|
| 22 |
+
|
| 23 |
+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 24 |
+
email = Column(String(255), unique=True, nullable=False, index=True)
|
| 25 |
+
name = Column(String(255), nullable=False)
|
| 26 |
+
password_hash = Column(String(255), nullable=False)
|
| 27 |
+
role = Column(Enum(UserRole), nullable=False, default=UserRole.STAFF)
|
| 28 |
+
is_active = Column(Boolean, default=True, nullable=False)
|
| 29 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 30 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
| 31 |
+
|
| 32 |
+
def __repr__(self):
|
| 33 |
+
return f"<User {self.email}>"
|
backend/app/routers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API routers
|
backend/app/routers/auth.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication routes: signup, login, and user management.
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 5 |
+
from sqlalchemy.orm import Session
|
| 6 |
+
|
| 7 |
+
from ..core.database import get_db
|
| 8 |
+
from ..core.security import (
|
| 9 |
+
hash_password,
|
| 10 |
+
verify_password,
|
| 11 |
+
create_access_token,
|
| 12 |
+
get_current_user,
|
| 13 |
+
require_role
|
| 14 |
+
)
|
| 15 |
+
from ..models.user import User, UserRole
|
| 16 |
+
from ..schemas.auth import (
|
| 17 |
+
UserSignup,
|
| 18 |
+
UserLogin,
|
| 19 |
+
Token,
|
| 20 |
+
UserResponse,
|
| 21 |
+
MessageResponse
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@router.post("/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
| 28 |
+
async def signup(user_data: UserSignup, db: Session = Depends(get_db)):
|
| 29 |
+
"""
|
| 30 |
+
Register a new user.
|
| 31 |
+
|
| 32 |
+
- **email**: Valid email address (must be unique)
|
| 33 |
+
- **name**: User's full name
|
| 34 |
+
- **password**: Password (minimum 8 characters)
|
| 35 |
+
- **role**: User role (ADMIN or STAFF, defaults to STAFF)
|
| 36 |
+
"""
|
| 37 |
+
# Check if email already exists
|
| 38 |
+
existing_user = db.query(User).filter(User.email == user_data.email).first()
|
| 39 |
+
if existing_user:
|
| 40 |
+
raise HTTPException(
|
| 41 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 42 |
+
detail="Email already registered"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Create new user
|
| 46 |
+
new_user = User(
|
| 47 |
+
email=user_data.email,
|
| 48 |
+
name=user_data.name,
|
| 49 |
+
password_hash=hash_password(user_data.password),
|
| 50 |
+
role=UserRole(user_data.role)
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
db.add(new_user)
|
| 54 |
+
db.commit()
|
| 55 |
+
db.refresh(new_user)
|
| 56 |
+
|
| 57 |
+
return new_user
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@router.post("/login", response_model=Token)
|
| 61 |
+
async def login(credentials: UserLogin, db: Session = Depends(get_db)):
|
| 62 |
+
"""
|
| 63 |
+
Authenticate user and return JWT access token.
|
| 64 |
+
|
| 65 |
+
- **email**: User's email address
|
| 66 |
+
- **password**: User's password
|
| 67 |
+
"""
|
| 68 |
+
# Find user by email
|
| 69 |
+
user = db.query(User).filter(User.email == credentials.email).first()
|
| 70 |
+
|
| 71 |
+
if not user:
|
| 72 |
+
raise HTTPException(
|
| 73 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 74 |
+
detail="Incorrect email or password",
|
| 75 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Verify password
|
| 79 |
+
if not verify_password(credentials.password, user.password_hash):
|
| 80 |
+
raise HTTPException(
|
| 81 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 82 |
+
detail="Incorrect email or password",
|
| 83 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Check if user is active
|
| 87 |
+
if not user.is_active:
|
| 88 |
+
raise HTTPException(
|
| 89 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 90 |
+
detail="Account is inactive"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# Create access token
|
| 94 |
+
access_token = create_access_token(data={"sub": str(user.id)})
|
| 95 |
+
|
| 96 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@router.get("/me", response_model=UserResponse)
|
| 100 |
+
async def get_current_user_info(current_user: User = Depends(get_current_user)):
|
| 101 |
+
"""
|
| 102 |
+
Get current authenticated user's information.
|
| 103 |
+
|
| 104 |
+
Requires valid JWT token in Authorization header.
|
| 105 |
+
"""
|
| 106 |
+
return current_user
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@router.get("/admin-only", response_model=MessageResponse)
|
| 110 |
+
async def admin_only_route(current_user: User = Depends(require_role("ADMIN"))):
|
| 111 |
+
"""
|
| 112 |
+
Demo endpoint that requires ADMIN role.
|
| 113 |
+
|
| 114 |
+
This demonstrates role-based authorization.
|
| 115 |
+
"""
|
| 116 |
+
return {"message": f"Welcome, Admin {current_user.name}! You have access to this protected route."}
|
backend/app/routers/portfolio.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Portfolio API endpoints for managing user stock holdings.
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 5 |
+
from sqlalchemy.orm import Session
|
| 6 |
+
from typing import List
|
| 7 |
+
from datetime import date
|
| 8 |
+
|
| 9 |
+
from ..core.database import get_db
|
| 10 |
+
from ..core.security import get_current_user
|
| 11 |
+
from ..models.user import User
|
| 12 |
+
from ..models.portfolio import Portfolio
|
| 13 |
+
from ..schemas.portfolio import PortfolioAdd, PortfolioUpdate, PortfolioResponse
|
| 14 |
+
|
| 15 |
+
router = APIRouter(prefix="/portfolio", tags=["Portfolio"])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@router.post("/add", response_model=PortfolioResponse)
|
| 19 |
+
async def add_to_portfolio(
|
| 20 |
+
portfolio_data: PortfolioAdd,
|
| 21 |
+
current_user: User = Depends(get_current_user),
|
| 22 |
+
db: Session = Depends(get_db)
|
| 23 |
+
):
|
| 24 |
+
"""
|
| 25 |
+
Add a stock to user's portfolio.
|
| 26 |
+
|
| 27 |
+
**Requires authentication.**
|
| 28 |
+
"""
|
| 29 |
+
# Check if stock already exists in portfolio
|
| 30 |
+
existing = db.query(Portfolio).filter(
|
| 31 |
+
Portfolio.user_id == current_user.id,
|
| 32 |
+
Portfolio.symbol == portfolio_data.symbol.upper()
|
| 33 |
+
).first()
|
| 34 |
+
|
| 35 |
+
if existing:
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=400,
|
| 38 |
+
detail=f"Stock {portfolio_data.symbol} already exists in your portfolio"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Create new portfolio entry
|
| 42 |
+
portfolio_entry = Portfolio(
|
| 43 |
+
user_id=current_user.id,
|
| 44 |
+
symbol=portfolio_data.symbol.upper(),
|
| 45 |
+
shares=portfolio_data.shares,
|
| 46 |
+
buying_date=portfolio_data.buying_date,
|
| 47 |
+
buying_price=portfolio_data.buying_price
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
db.add(portfolio_entry)
|
| 51 |
+
db.commit()
|
| 52 |
+
db.refresh(portfolio_entry)
|
| 53 |
+
|
| 54 |
+
return portfolio_entry
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@router.get("/", response_model=List[PortfolioResponse])
|
| 58 |
+
async def get_portfolio(
|
| 59 |
+
current_user: User = Depends(get_current_user),
|
| 60 |
+
db: Session = Depends(get_db)
|
| 61 |
+
):
|
| 62 |
+
"""
|
| 63 |
+
Get user's complete portfolio.
|
| 64 |
+
|
| 65 |
+
**Requires authentication.**
|
| 66 |
+
"""
|
| 67 |
+
portfolio = db.query(Portfolio).filter(
|
| 68 |
+
Portfolio.user_id == current_user.id
|
| 69 |
+
).order_by(Portfolio.created_at.desc()).all()
|
| 70 |
+
|
| 71 |
+
return portfolio
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@router.delete("/{portfolio_id}")
|
| 75 |
+
async def remove_from_portfolio(
|
| 76 |
+
portfolio_id: str,
|
| 77 |
+
current_user: User = Depends(get_current_user),
|
| 78 |
+
db: Session = Depends(get_db)
|
| 79 |
+
):
|
| 80 |
+
"""
|
| 81 |
+
Remove a stock from portfolio.
|
| 82 |
+
|
| 83 |
+
**Requires authentication.**
|
| 84 |
+
"""
|
| 85 |
+
portfolio_entry = db.query(Portfolio).filter(
|
| 86 |
+
Portfolio.id == portfolio_id,
|
| 87 |
+
Portfolio.user_id == current_user.id
|
| 88 |
+
).first()
|
| 89 |
+
|
| 90 |
+
if not portfolio_entry:
|
| 91 |
+
raise HTTPException(
|
| 92 |
+
status_code=404,
|
| 93 |
+
detail="Portfolio entry not found"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
db.delete(portfolio_entry)
|
| 97 |
+
db.commit()
|
| 98 |
+
|
| 99 |
+
return {"message": f"Removed {portfolio_entry.symbol} from portfolio"}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@router.put("/{portfolio_id}", response_model=PortfolioResponse)
|
| 103 |
+
async def update_portfolio_entry(
|
| 104 |
+
portfolio_id: str,
|
| 105 |
+
update_data: PortfolioUpdate,
|
| 106 |
+
current_user: User = Depends(get_current_user),
|
| 107 |
+
db: Session = Depends(get_db)
|
| 108 |
+
):
|
| 109 |
+
"""
|
| 110 |
+
Update a portfolio entry.
|
| 111 |
+
|
| 112 |
+
**Requires authentication.**
|
| 113 |
+
"""
|
| 114 |
+
portfolio_entry = db.query(Portfolio).filter(
|
| 115 |
+
Portfolio.id == portfolio_id,
|
| 116 |
+
Portfolio.user_id == current_user.id
|
| 117 |
+
).first()
|
| 118 |
+
|
| 119 |
+
if not portfolio_entry:
|
| 120 |
+
raise HTTPException(
|
| 121 |
+
status_code=404,
|
| 122 |
+
detail="Portfolio entry not found"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Update fields if provided
|
| 126 |
+
if update_data.shares is not None:
|
| 127 |
+
portfolio_entry.shares = update_data.shares
|
| 128 |
+
if update_data.buying_date is not None:
|
| 129 |
+
portfolio_entry.buying_date = update_data.buying_date
|
| 130 |
+
if update_data.buying_price is not None:
|
| 131 |
+
portfolio_entry.buying_price = update_data.buying_price
|
| 132 |
+
|
| 133 |
+
db.commit()
|
| 134 |
+
db.refresh(portfolio_entry)
|
| 135 |
+
|
| 136 |
+
return portfolio_entry
|
backend/app/routers/stocks.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stock API endpoints.
|
| 3 |
+
Provides real-time stock quotes and market data.
|
| 4 |
+
"""
|
| 5 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 6 |
+
from typing import List
|
| 7 |
+
import csv
|
| 8 |
+
import os
|
| 9 |
+
from ..services.stock_service import stock_service
|
| 10 |
+
from ..core.security import get_current_user
|
| 11 |
+
from ..models.user import User
|
| 12 |
+
from ..schemas.portfolio import NSEStock
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/stocks", tags=["Stocks"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.get("/quote/{symbol}")
|
| 18 |
+
async def get_stock_quote(
|
| 19 |
+
symbol: str,
|
| 20 |
+
current_user: User = Depends(get_current_user)
|
| 21 |
+
):
|
| 22 |
+
"""
|
| 23 |
+
Get current quote for a stock symbol.
|
| 24 |
+
|
| 25 |
+
**Requires authentication.**
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
symbol: Stock ticker symbol (e.g., AAPL, GOOGL)
|
| 29 |
+
|
| 30 |
+
Returns:
|
| 31 |
+
Stock quote with price, change, volume, etc.
|
| 32 |
+
"""
|
| 33 |
+
quote = stock_service.get_stock_quote(symbol)
|
| 34 |
+
|
| 35 |
+
if not quote:
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=404,
|
| 38 |
+
detail=f"Stock symbol '{symbol}' not found or data unavailable"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
return quote
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@router.get("/popular")
|
| 45 |
+
async def get_popular_stocks(
|
| 46 |
+
current_user: User = Depends(get_current_user)
|
| 47 |
+
):
|
| 48 |
+
"""
|
| 49 |
+
Get quotes for popular stocks (AAPL, GOOGL, MSFT, TSLA, AMZN).
|
| 50 |
+
|
| 51 |
+
**Requires authentication.**
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
List of stock quotes
|
| 55 |
+
"""
|
| 56 |
+
stocks = stock_service.get_popular_stocks()
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
"stocks": stocks,
|
| 60 |
+
"count": len(stocks)
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@router.get("/search")
|
| 65 |
+
async def search_stocks(
|
| 66 |
+
q: str,
|
| 67 |
+
current_user: User = Depends(get_current_user)
|
| 68 |
+
):
|
| 69 |
+
"""
|
| 70 |
+
Search for stocks by symbol or name.
|
| 71 |
+
|
| 72 |
+
**Requires authentication.**
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
q: Search query (symbol or company name)
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
List of matching stocks
|
| 79 |
+
"""
|
| 80 |
+
if not q or len(q) < 1:
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=400,
|
| 83 |
+
detail="Search query must be at least 1 character"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
results = stock_service.search_stocks(q)
|
| 87 |
+
|
| 88 |
+
return {
|
| 89 |
+
"results": results,
|
| 90 |
+
"count": len(results),
|
| 91 |
+
"query": q
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@router.get("/nse-symbols", response_model=List[NSEStock])
|
| 96 |
+
async def get_nse_symbols(
|
| 97 |
+
q: str = None,
|
| 98 |
+
limit: int = 100,
|
| 99 |
+
current_user: User = Depends(get_current_user)
|
| 100 |
+
):
|
| 101 |
+
"""
|
| 102 |
+
Get NSE India stock symbols from CSV file.
|
| 103 |
+
|
| 104 |
+
**Requires authentication.**
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
q: Optional search query to filter symbols
|
| 108 |
+
limit: Maximum number of results (default: 100)
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
List of NSE stock symbols with company names
|
| 112 |
+
"""
|
| 113 |
+
# Path to CSV file
|
| 114 |
+
csv_path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "Ticker_List_NSE_India.csv")
|
| 115 |
+
|
| 116 |
+
if not os.path.exists(csv_path):
|
| 117 |
+
raise HTTPException(
|
| 118 |
+
status_code=500,
|
| 119 |
+
detail="NSE stock symbols file not found"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
stocks = []
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
with open(csv_path, 'r', encoding='utf-8') as file:
|
| 126 |
+
csv_reader = csv.DictReader(file)
|
| 127 |
+
|
| 128 |
+
for row in csv_reader:
|
| 129 |
+
symbol = row.get('SYMBOL', '').strip()
|
| 130 |
+
name = row.get('NAME OF COMPANY', '').strip()
|
| 131 |
+
series = row.get(' SERIES', '').strip()
|
| 132 |
+
|
| 133 |
+
if not symbol:
|
| 134 |
+
continue
|
| 135 |
+
|
| 136 |
+
# Filter by search query if provided
|
| 137 |
+
if q:
|
| 138 |
+
q_lower = q.lower()
|
| 139 |
+
if q_lower not in symbol.lower() and q_lower not in name.lower():
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
stocks.append(NSEStock(
|
| 143 |
+
symbol=symbol,
|
| 144 |
+
name=name,
|
| 145 |
+
series=series if series else None
|
| 146 |
+
))
|
| 147 |
+
|
| 148 |
+
# Limit results
|
| 149 |
+
if len(stocks) >= limit:
|
| 150 |
+
break
|
| 151 |
+
|
| 152 |
+
return stocks
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
raise HTTPException(
|
| 156 |
+
status_code=500,
|
| 157 |
+
detail=f"Error reading NSE symbols: {str(e)}"
|
| 158 |
+
)
|
backend/app/schemas/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Pydantic schemas for request/response validation
|
backend/app/schemas/auth.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic schemas for authentication requests and responses.
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from uuid import UUID
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class UserSignup(BaseModel):
|
| 11 |
+
"""Schema for user signup request."""
|
| 12 |
+
email: EmailStr
|
| 13 |
+
name: str = Field(..., min_length=1, max_length=255)
|
| 14 |
+
password: str = Field(..., min_length=8, max_length=100)
|
| 15 |
+
role: str = Field(default="STAFF", pattern="^(ADMIN|STAFF)$")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class UserLogin(BaseModel):
|
| 19 |
+
"""Schema for user login request."""
|
| 20 |
+
email: EmailStr
|
| 21 |
+
password: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Token(BaseModel):
|
| 25 |
+
"""Schema for JWT token response."""
|
| 26 |
+
access_token: str
|
| 27 |
+
token_type: str = "bearer"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class UserResponse(BaseModel):
|
| 31 |
+
"""Schema for user data response."""
|
| 32 |
+
id: UUID
|
| 33 |
+
email: str
|
| 34 |
+
name: str
|
| 35 |
+
role: str
|
| 36 |
+
is_active: bool
|
| 37 |
+
created_at: datetime
|
| 38 |
+
|
| 39 |
+
class Config:
|
| 40 |
+
from_attributes = True # Allows creation from ORM models
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class MessageResponse(BaseModel):
|
| 44 |
+
"""Generic message response schema."""
|
| 45 |
+
message: str
|
backend/app/schemas/portfolio.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Portfolio schemas for request/response validation.
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from datetime import date, datetime
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PortfolioAdd(BaseModel):
|
| 10 |
+
"""Schema for adding a stock to portfolio."""
|
| 11 |
+
symbol: str = Field(..., min_length=1, max_length=50, description="Stock symbol")
|
| 12 |
+
shares: float = Field(..., gt=0, description="Number of shares")
|
| 13 |
+
buying_date: date = Field(..., description="Date when stock was purchased")
|
| 14 |
+
buying_price: Optional[float] = Field(None, gt=0, description="Price at purchase (optional)")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PortfolioUpdate(BaseModel):
|
| 18 |
+
"""Schema for updating portfolio entry."""
|
| 19 |
+
shares: Optional[float] = Field(None, gt=0)
|
| 20 |
+
buying_date: Optional[date] = None
|
| 21 |
+
buying_price: Optional[float] = Field(None, gt=0)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class PortfolioResponse(BaseModel):
|
| 25 |
+
"""Schema for portfolio response."""
|
| 26 |
+
id: str
|
| 27 |
+
user_id: str
|
| 28 |
+
symbol: str
|
| 29 |
+
shares: float
|
| 30 |
+
buying_date: date
|
| 31 |
+
buying_price: Optional[float]
|
| 32 |
+
created_at: datetime
|
| 33 |
+
updated_at: datetime
|
| 34 |
+
|
| 35 |
+
class Config:
|
| 36 |
+
from_attributes = True
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class NSEStock(BaseModel):
|
| 40 |
+
"""Schema for NSE stock symbol."""
|
| 41 |
+
symbol: str
|
| 42 |
+
name: str
|
| 43 |
+
series: Optional[str] = None
|
backend/app/services/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Services package initialization.
|
| 3 |
+
"""
|
backend/app/services/stock_service.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stock data service using Yahoo Finance.
|
| 3 |
+
Fetches real-time stock quotes and company information.
|
| 4 |
+
"""
|
| 5 |
+
import yfinance as yf
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from typing import Optional, Dict, List
|
| 8 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class StockService:
|
| 15 |
+
"""Service for fetching stock data from Yahoo Finance."""
|
| 16 |
+
|
| 17 |
+
# Popular stocks to display by default
|
| 18 |
+
POPULAR_SYMBOLS = ["AAPL", "GOOGL", "MSFT", "TSLA", "AMZN"]
|
| 19 |
+
|
| 20 |
+
# Simple cache
|
| 21 |
+
_cache = {}
|
| 22 |
+
_cache_duration = timedelta(minutes=1) # Cache for 1 minute
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def get_stock_quote(symbol: str) -> Optional[Dict]:
|
| 26 |
+
"""
|
| 27 |
+
Get current stock quote for a symbol.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
symbol: Stock ticker symbol (e.g., 'AAPL')
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary with stock data or None if error
|
| 34 |
+
"""
|
| 35 |
+
# Check cache first
|
| 36 |
+
cache_key = f"quote_{symbol}"
|
| 37 |
+
if cache_key in StockService._cache:
|
| 38 |
+
cached_data, cached_time = StockService._cache[cache_key]
|
| 39 |
+
if datetime.now() - cached_time < StockService._cache_duration:
|
| 40 |
+
logger.info(f"Using cached data for {symbol}")
|
| 41 |
+
return cached_data
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
logger.info(f"Fetching fresh data for {symbol}")
|
| 45 |
+
stock = yf.Ticker(symbol)
|
| 46 |
+
|
| 47 |
+
# Use fast_info for quicker response
|
| 48 |
+
try:
|
| 49 |
+
fast_info = stock.fast_info
|
| 50 |
+
current_price = fast_info.get('lastPrice') or fast_info.get('regularMarketPrice')
|
| 51 |
+
previous_close = fast_info.get('previousClose')
|
| 52 |
+
except:
|
| 53 |
+
# Fallback to regular info if fast_info fails
|
| 54 |
+
info = stock.info
|
| 55 |
+
current_price = info.get('currentPrice') or info.get('regularMarketPrice')
|
| 56 |
+
previous_close = info.get('previousClose')
|
| 57 |
+
|
| 58 |
+
if not current_price or not previous_close:
|
| 59 |
+
logger.warning(f"Missing price data for {symbol}")
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
# Calculate change
|
| 63 |
+
change = current_price - previous_close
|
| 64 |
+
change_percent = (change / previous_close) * 100
|
| 65 |
+
|
| 66 |
+
# Get company name (use fast method)
|
| 67 |
+
try:
|
| 68 |
+
name = stock.info.get('longName') or stock.info.get('shortName') or symbol
|
| 69 |
+
except:
|
| 70 |
+
name = symbol
|
| 71 |
+
|
| 72 |
+
result = {
|
| 73 |
+
"symbol": symbol.upper(),
|
| 74 |
+
"name": name,
|
| 75 |
+
"price": round(float(current_price), 2),
|
| 76 |
+
"change": round(float(change), 2),
|
| 77 |
+
"change_percent": round(float(change_percent), 2),
|
| 78 |
+
"volume": int(fast_info.get('volume', 0)) if 'fast_info' in locals() else 0,
|
| 79 |
+
"market_cap": None,
|
| 80 |
+
"pe_ratio": None,
|
| 81 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# Cache the result
|
| 85 |
+
StockService._cache[cache_key] = (result, datetime.now())
|
| 86 |
+
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.error(f"Error fetching stock data for {symbol}: {e}")
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def get_popular_stocks() -> List[Dict]:
|
| 95 |
+
"""
|
| 96 |
+
Get quotes for popular stocks (fetched in parallel for speed).
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
List of stock quote dictionaries
|
| 100 |
+
"""
|
| 101 |
+
logger.info("Fetching popular stocks in parallel...")
|
| 102 |
+
stocks = []
|
| 103 |
+
|
| 104 |
+
# Use ThreadPoolExecutor to fetch stocks in parallel
|
| 105 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
| 106 |
+
# Submit all tasks
|
| 107 |
+
future_to_symbol = {
|
| 108 |
+
executor.submit(StockService.get_stock_quote, symbol): symbol
|
| 109 |
+
for symbol in StockService.POPULAR_SYMBOLS
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# Collect results as they complete
|
| 113 |
+
for future in as_completed(future_to_symbol):
|
| 114 |
+
symbol = future_to_symbol[future]
|
| 115 |
+
try:
|
| 116 |
+
quote = future.result(timeout=10) # 10 second timeout per stock
|
| 117 |
+
if quote:
|
| 118 |
+
stocks.append(quote)
|
| 119 |
+
logger.info(f"✅ Got quote for {symbol}")
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.error(f"❌ Failed to get quote for {symbol}: {e}")
|
| 122 |
+
|
| 123 |
+
logger.info(f"Fetched {len(stocks)} stocks successfully")
|
| 124 |
+
return stocks
|
| 125 |
+
|
| 126 |
+
@staticmethod
|
| 127 |
+
def search_stocks(query: str, limit: int = 10) -> List[Dict]:
|
| 128 |
+
"""
|
| 129 |
+
Search for stocks by symbol or name.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
query: Search query
|
| 133 |
+
limit: Maximum number of results
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
List of matching stocks
|
| 137 |
+
"""
|
| 138 |
+
# For now, just try to get the quote for the query as a symbol
|
| 139 |
+
# In production, you'd use a proper search API
|
| 140 |
+
query = query.upper().strip()
|
| 141 |
+
quote = StockService.get_stock_quote(query)
|
| 142 |
+
|
| 143 |
+
if quote:
|
| 144 |
+
return [quote]
|
| 145 |
+
return []
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# Create singleton instance
|
| 149 |
+
stock_service = StockService()
|
| 150 |
+
|
backend/create_account.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quick test to create a user via API
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
API_BASE_URL = "http://localhost:8000"
|
| 8 |
+
|
| 9 |
+
print("=" * 50)
|
| 10 |
+
print("Creating test account...")
|
| 11 |
+
print("=" * 50)
|
| 12 |
+
|
| 13 |
+
payload = {
|
| 14 |
+
"email": "admin@test.com",
|
| 15 |
+
"name": "Admin User",
|
| 16 |
+
"password": "SecurePass123!",
|
| 17 |
+
"role": "ADMIN"
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
response = requests.post(
|
| 22 |
+
f"{API_BASE_URL}/auth/signup",
|
| 23 |
+
json=payload
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
print(f"\nStatus Code: {response.status_code}")
|
| 27 |
+
print(f"\nResponse:")
|
| 28 |
+
print(json.dumps(response.json(), indent=2))
|
| 29 |
+
|
| 30 |
+
if response.status_code == 201:
|
| 31 |
+
print("\n✅ Account created successfully!")
|
| 32 |
+
print("\nYou can now login with:")
|
| 33 |
+
print(f"Email: {payload['email']}")
|
| 34 |
+
print(f"Password: {payload['password']}")
|
| 35 |
+
else:
|
| 36 |
+
print("\n❌ Failed to create account!")
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"\n❌ Error: {e}")
|
| 40 |
+
print("\nMake sure the backend is running on http://localhost:8000")
|
backend/database_schema.sql
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Stock Analysis Database Schema
|
| 2 |
+
-- Run this SQL in your Supabase SQL Editor
|
| 3 |
+
|
| 4 |
+
-- Create user role enum
|
| 5 |
+
CREATE TYPE user_role AS ENUM ('ADMIN', 'STAFF');
|
| 6 |
+
|
| 7 |
+
-- Create users table
|
| 8 |
+
CREATE TABLE users (
|
| 9 |
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 10 |
+
email VARCHAR(255) UNIQUE NOT NULL,
|
| 11 |
+
name VARCHAR(255) NOT NULL,
|
| 12 |
+
password_hash VARCHAR(255) NOT NULL,
|
| 13 |
+
role user_role NOT NULL DEFAULT 'STAFF',
|
| 14 |
+
is_active BOOLEAN DEFAULT TRUE,
|
| 15 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 16 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 17 |
+
);
|
| 18 |
+
|
| 19 |
+
-- Create index on email for faster lookups
|
| 20 |
+
CREATE INDEX idx_users_email ON users(email);
|
| 21 |
+
|
| 22 |
+
-- Create function to automatically update updated_at timestamp
|
| 23 |
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
| 24 |
+
RETURNS TRIGGER AS $$
|
| 25 |
+
BEGIN
|
| 26 |
+
NEW.updated_at = NOW();
|
| 27 |
+
RETURN NEW;
|
| 28 |
+
END;
|
| 29 |
+
$$ language 'plpgsql';
|
| 30 |
+
|
| 31 |
+
-- Create trigger to call the function before update
|
| 32 |
+
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
| 33 |
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
| 34 |
+
|
| 35 |
+
-- Verify table creation
|
| 36 |
+
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';
|
backend/init_db.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Create database tables for SQLite.
|
| 3 |
+
Run this once to initialize the database.
|
| 4 |
+
"""
|
| 5 |
+
from app.core.database import engine, Base
|
| 6 |
+
from app.models.user import User
|
| 7 |
+
|
| 8 |
+
print("Creating database tables...")
|
| 9 |
+
|
| 10 |
+
# Create all tables
|
| 11 |
+
Base.metadata.create_all(bind=engine)
|
| 12 |
+
|
| 13 |
+
print("✅ Database tables created successfully!")
|
| 14 |
+
print("You can now run the application.")
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn[standard]==0.27.0
|
| 3 |
+
sqlalchemy==2.0.25
|
| 4 |
+
psycopg2-binary==2.9.9
|
| 5 |
+
pydantic==2.5.3
|
| 6 |
+
pydantic-settings==2.1.0
|
| 7 |
+
python-jose[cryptography]==3.3.0
|
| 8 |
+
passlib[bcrypt]==1.7.4
|
| 9 |
+
python-multipart==0.0.6
|
backend/stock_analysis.db
ADDED
|
Binary file (32.8 kB). View file
|
|
|
backend/test_api.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script to verify the signup endpoint works.
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
API_BASE_URL = "http://localhost:8000"
|
| 8 |
+
|
| 9 |
+
def test_signup():
|
| 10 |
+
"""Test user signup."""
|
| 11 |
+
print("Testing signup endpoint...")
|
| 12 |
+
|
| 13 |
+
payload = {
|
| 14 |
+
"email": "testuser@example.com",
|
| 15 |
+
"name": "Test User",
|
| 16 |
+
"password": "SecurePass123!",
|
| 17 |
+
"role": "STAFF"
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
response = requests.post(
|
| 22 |
+
f"{API_BASE_URL}/auth/signup",
|
| 23 |
+
json=payload
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
print(f"Status Code: {response.status_code}")
|
| 27 |
+
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 28 |
+
|
| 29 |
+
if response.status_code == 201:
|
| 30 |
+
print("✅ Signup successful!")
|
| 31 |
+
return True
|
| 32 |
+
else:
|
| 33 |
+
print("❌ Signup failed!")
|
| 34 |
+
return False
|
| 35 |
+
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"❌ Error: {e}")
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
def test_health():
|
| 41 |
+
"""Test health endpoint."""
|
| 42 |
+
print("\nTesting health endpoint...")
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
response = requests.get(f"{API_BASE_URL}/health")
|
| 46 |
+
print(f"Status Code: {response.status_code}")
|
| 47 |
+
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 48 |
+
|
| 49 |
+
if response.status_code == 200:
|
| 50 |
+
print("✅ Backend is healthy!")
|
| 51 |
+
return True
|
| 52 |
+
else:
|
| 53 |
+
print("❌ Backend health check failed!")
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"❌ Error: {e}")
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
print("=" * 50)
|
| 62 |
+
print("API Test Script")
|
| 63 |
+
print("=" * 50)
|
| 64 |
+
|
| 65 |
+
# Test health first
|
| 66 |
+
if test_health():
|
| 67 |
+
# Then test signup
|
| 68 |
+
test_signup()
|
| 69 |
+
else:
|
| 70 |
+
print("\n❌ Backend is not responding. Make sure it's running on port 8000.")
|
backend/test_stocks.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test stock API endpoint
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
|
| 6 |
+
API_BASE_URL = "http://localhost:8000"
|
| 7 |
+
|
| 8 |
+
# First, login to get token
|
| 9 |
+
print("=" * 50)
|
| 10 |
+
print("Testing Stock API")
|
| 11 |
+
print("=" * 50)
|
| 12 |
+
|
| 13 |
+
# Login
|
| 14 |
+
print("\n1. Logging in...")
|
| 15 |
+
login_response = requests.post(
|
| 16 |
+
f"{API_BASE_URL}/auth/login",
|
| 17 |
+
json={"email": "admin@test.com", "password": "SecurePass123!"}
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
if login_response.status_code == 200:
|
| 21 |
+
token = login_response.json()["access_token"]
|
| 22 |
+
print("✅ Login successful!")
|
| 23 |
+
|
| 24 |
+
# Test stock endpoint
|
| 25 |
+
print("\n2. Fetching popular stocks...")
|
| 26 |
+
stock_response = requests.get(
|
| 27 |
+
f"{API_BASE_URL}/stocks/popular",
|
| 28 |
+
headers={"Authorization": f"Bearer {token}"}
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
print(f"Status Code: {stock_response.status_code}")
|
| 32 |
+
|
| 33 |
+
if stock_response.status_code == 200:
|
| 34 |
+
data = stock_response.json()
|
| 35 |
+
print(f"✅ Got {data['count']} stocks!")
|
| 36 |
+
for stock in data['stocks']:
|
| 37 |
+
print(f"\n{stock['symbol']}: ${stock['price']} ({stock['change_percent']:+.2f}%)")
|
| 38 |
+
else:
|
| 39 |
+
print(f"❌ Error: {stock_response.text}")
|
| 40 |
+
else:
|
| 41 |
+
print(f"❌ Login failed: {login_response.text}")
|
code_quality_agent.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Code Quality Agent - Main orchestration script.
|
| 3 |
+
|
| 4 |
+
This is the main entry point for the automated code quality agent.
|
| 5 |
+
It coordinates all modules and generates comprehensive quality reports.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python code_quality_agent.py [options]
|
| 9 |
+
|
| 10 |
+
Options:
|
| 11 |
+
--dry-run Preview changes without applying them
|
| 12 |
+
--module <name> Run specific module only (clean, security, connections, test)
|
| 13 |
+
--all Run all modules (default)
|
| 14 |
+
--config <path> Path to custom config file
|
| 15 |
+
--help Show this help message
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import sys
|
| 19 |
+
import argparse
|
| 20 |
+
import logging
|
| 21 |
+
import yaml
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from typing import Dict, Any
|
| 24 |
+
|
| 25 |
+
from modules import (
|
| 26 |
+
CodeCleaner,
|
| 27 |
+
VulnerabilityChecker,
|
| 28 |
+
ConnectionTester,
|
| 29 |
+
CodeTester,
|
| 30 |
+
ReportGenerator
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class CodeQualityAgent:
|
| 35 |
+
"""Main code quality agent orchestrator."""
|
| 36 |
+
|
| 37 |
+
def __init__(self, config_path: str = "agent_config.yaml"):
|
| 38 |
+
"""
|
| 39 |
+
Initialize the code quality agent.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
config_path: Path to configuration file
|
| 43 |
+
"""
|
| 44 |
+
self.project_root = Path(__file__).parent
|
| 45 |
+
self.config = self._load_config(config_path)
|
| 46 |
+
self._setup_logging()
|
| 47 |
+
|
| 48 |
+
self.results = {}
|
| 49 |
+
|
| 50 |
+
def _load_config(self, config_path: str) -> Dict[str, Any]:
|
| 51 |
+
"""Load configuration from YAML file."""
|
| 52 |
+
config_file = self.project_root / config_path
|
| 53 |
+
|
| 54 |
+
if not config_file.exists():
|
| 55 |
+
print(f"Warning: Config file {config_path} not found. Using defaults.")
|
| 56 |
+
return self._get_default_config()
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
with open(config_file, 'r') as f:
|
| 60 |
+
config = yaml.safe_load(f)
|
| 61 |
+
return config
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print(f"Error loading config: {e}. Using defaults.")
|
| 64 |
+
return self._get_default_config()
|
| 65 |
+
|
| 66 |
+
def _get_default_config(self) -> Dict[str, Any]:
|
| 67 |
+
"""Get default configuration."""
|
| 68 |
+
return {
|
| 69 |
+
"general": {
|
| 70 |
+
"project_name": "Project",
|
| 71 |
+
"project_root": ".",
|
| 72 |
+
"output_dir": "quality_reports",
|
| 73 |
+
"log_level": "INFO"
|
| 74 |
+
},
|
| 75 |
+
"modules": {
|
| 76 |
+
"code_cleaner": True,
|
| 77 |
+
"vulnerability_checker": True,
|
| 78 |
+
"connection_tester": True,
|
| 79 |
+
"code_tester": True
|
| 80 |
+
},
|
| 81 |
+
"code_cleaner": {"enabled": True},
|
| 82 |
+
"vulnerability_checker": {"enabled": True},
|
| 83 |
+
"connection_tester": {"enabled": True},
|
| 84 |
+
"code_tester": {"enabled": True},
|
| 85 |
+
"reporting": {
|
| 86 |
+
"generate_html": True,
|
| 87 |
+
"generate_json": True,
|
| 88 |
+
"generate_console_summary": True
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
def _setup_logging(self):
|
| 93 |
+
"""Setup logging configuration."""
|
| 94 |
+
log_level = self.config.get("general", {}).get("log_level", "INFO")
|
| 95 |
+
output_dir = self.project_root / self.config.get("general", {}).get("output_dir", "quality_reports")
|
| 96 |
+
|
| 97 |
+
# Create output directory if it doesn't exist
|
| 98 |
+
output_dir.mkdir(exist_ok=True)
|
| 99 |
+
|
| 100 |
+
logging.basicConfig(
|
| 101 |
+
level=getattr(logging, log_level),
|
| 102 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 103 |
+
handlers=[
|
| 104 |
+
logging.StreamHandler(sys.stdout),
|
| 105 |
+
logging.FileHandler(output_dir / "agent.log")
|
| 106 |
+
]
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
self.logger = logging.getLogger(__name__)
|
| 110 |
+
|
| 111 |
+
def run_all(self, dry_run: bool = False):
|
| 112 |
+
"""
|
| 113 |
+
Run all enabled modules.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
dry_run: If True, preview changes without applying them
|
| 117 |
+
"""
|
| 118 |
+
self.logger.info("="*70)
|
| 119 |
+
self.logger.info("CODE QUALITY AGENT STARTED")
|
| 120 |
+
self.logger.info("="*70)
|
| 121 |
+
|
| 122 |
+
project_name = self.config.get("general", {}).get("project_name", "Project")
|
| 123 |
+
self.logger.info(f"Project: {project_name}")
|
| 124 |
+
self.logger.info(f"Dry run: {dry_run}")
|
| 125 |
+
|
| 126 |
+
# Run Code Cleaner
|
| 127 |
+
if self.config.get("modules", {}).get("code_cleaner", True):
|
| 128 |
+
self.logger.info("\n" + "-"*70)
|
| 129 |
+
self.logger.info("MODULE: Code Cleaner")
|
| 130 |
+
self.logger.info("-"*70)
|
| 131 |
+
cleaner = CodeCleaner(
|
| 132 |
+
self.config.get("code_cleaner", {}),
|
| 133 |
+
str(self.project_root)
|
| 134 |
+
)
|
| 135 |
+
self.results["code_cleaner"] = cleaner.run(dry_run=dry_run)
|
| 136 |
+
self.logger.info(cleaner.get_summary())
|
| 137 |
+
|
| 138 |
+
# Run Vulnerability Checker
|
| 139 |
+
if self.config.get("modules", {}).get("vulnerability_checker", True):
|
| 140 |
+
self.logger.info("\n" + "-"*70)
|
| 141 |
+
self.logger.info("MODULE: Vulnerability Checker")
|
| 142 |
+
self.logger.info("-"*70)
|
| 143 |
+
vuln_checker = VulnerabilityChecker(
|
| 144 |
+
self.config.get("vulnerability_checker", {}),
|
| 145 |
+
str(self.project_root)
|
| 146 |
+
)
|
| 147 |
+
self.results["vulnerability_checker"] = vuln_checker.run()
|
| 148 |
+
self.logger.info(vuln_checker.get_summary())
|
| 149 |
+
|
| 150 |
+
# Run Connection Tester
|
| 151 |
+
if self.config.get("modules", {}).get("connection_tester", True):
|
| 152 |
+
self.logger.info("\n" + "-"*70)
|
| 153 |
+
self.logger.info("MODULE: Connection Tester")
|
| 154 |
+
self.logger.info("-"*70)
|
| 155 |
+
conn_tester = ConnectionTester(
|
| 156 |
+
self.config.get("connection_tester", {}),
|
| 157 |
+
str(self.project_root)
|
| 158 |
+
)
|
| 159 |
+
self.results["connection_tester"] = conn_tester.run()
|
| 160 |
+
self.logger.info(conn_tester.get_summary())
|
| 161 |
+
|
| 162 |
+
# Run Code Tester
|
| 163 |
+
if self.config.get("modules", {}).get("code_tester", True):
|
| 164 |
+
self.logger.info("\n" + "-"*70)
|
| 165 |
+
self.logger.info("MODULE: Code Tester")
|
| 166 |
+
self.logger.info("-"*70)
|
| 167 |
+
code_tester = CodeTester(
|
| 168 |
+
self.config.get("code_tester", {}),
|
| 169 |
+
str(self.project_root)
|
| 170 |
+
)
|
| 171 |
+
self.results["code_tester"] = code_tester.run()
|
| 172 |
+
self.logger.info(code_tester.get_summary())
|
| 173 |
+
|
| 174 |
+
# Generate Reports
|
| 175 |
+
self.logger.info("\n" + "-"*70)
|
| 176 |
+
self.logger.info("Generating Reports")
|
| 177 |
+
self.logger.info("-"*70)
|
| 178 |
+
report_gen = ReportGenerator(
|
| 179 |
+
self.config.get("reporting", {}),
|
| 180 |
+
str(self.project_root)
|
| 181 |
+
)
|
| 182 |
+
report_gen.generate_reports(self.results)
|
| 183 |
+
|
| 184 |
+
self.logger.info("\n" + "="*70)
|
| 185 |
+
self.logger.info("CODE QUALITY AGENT COMPLETED")
|
| 186 |
+
self.logger.info("="*70)
|
| 187 |
+
|
| 188 |
+
def run_module(self, module_name: str, dry_run: bool = False):
|
| 189 |
+
"""
|
| 190 |
+
Run a specific module.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
module_name: Name of module to run (clean, security, connections, test)
|
| 194 |
+
dry_run: If True, preview changes without applying them
|
| 195 |
+
"""
|
| 196 |
+
self.logger.info(f"Running module: {module_name}")
|
| 197 |
+
|
| 198 |
+
if module_name == "clean":
|
| 199 |
+
cleaner = CodeCleaner(
|
| 200 |
+
self.config.get("code_cleaner", {}),
|
| 201 |
+
str(self.project_root)
|
| 202 |
+
)
|
| 203 |
+
self.results["code_cleaner"] = cleaner.run(dry_run=dry_run)
|
| 204 |
+
self.logger.info(cleaner.get_summary())
|
| 205 |
+
|
| 206 |
+
elif module_name == "security":
|
| 207 |
+
vuln_checker = VulnerabilityChecker(
|
| 208 |
+
self.config.get("vulnerability_checker", {}),
|
| 209 |
+
str(self.project_root)
|
| 210 |
+
)
|
| 211 |
+
self.results["vulnerability_checker"] = vuln_checker.run()
|
| 212 |
+
self.logger.info(vuln_checker.get_summary())
|
| 213 |
+
|
| 214 |
+
elif module_name == "connections":
|
| 215 |
+
conn_tester = ConnectionTester(
|
| 216 |
+
self.config.get("connection_tester", {}),
|
| 217 |
+
str(self.project_root)
|
| 218 |
+
)
|
| 219 |
+
self.results["connection_tester"] = conn_tester.run()
|
| 220 |
+
self.logger.info(conn_tester.get_summary())
|
| 221 |
+
|
| 222 |
+
elif module_name == "test":
|
| 223 |
+
code_tester = CodeTester(
|
| 224 |
+
self.config.get("code_tester", {}),
|
| 225 |
+
str(self.project_root)
|
| 226 |
+
)
|
| 227 |
+
self.results["code_tester"] = code_tester.run()
|
| 228 |
+
self.logger.info(code_tester.get_summary())
|
| 229 |
+
|
| 230 |
+
else:
|
| 231 |
+
self.logger.error(f"Unknown module: {module_name}")
|
| 232 |
+
return
|
| 233 |
+
|
| 234 |
+
# Generate report for single module
|
| 235 |
+
report_gen = ReportGenerator(
|
| 236 |
+
self.config.get("reporting", {}),
|
| 237 |
+
str(self.project_root)
|
| 238 |
+
)
|
| 239 |
+
report_gen.generate_reports(self.results)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def main():
|
| 243 |
+
"""Main entry point."""
|
| 244 |
+
parser = argparse.ArgumentParser(
|
| 245 |
+
description="Automated Code Quality Agent",
|
| 246 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 247 |
+
epilog="""
|
| 248 |
+
Examples:
|
| 249 |
+
python code_quality_agent.py --all
|
| 250 |
+
python code_quality_agent.py --dry-run
|
| 251 |
+
python code_quality_agent.py --module clean
|
| 252 |
+
python code_quality_agent.py --module security
|
| 253 |
+
"""
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
parser.add_argument(
|
| 257 |
+
"--dry-run",
|
| 258 |
+
action="store_true",
|
| 259 |
+
help="Preview changes without applying them"
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
parser.add_argument(
|
| 263 |
+
"--module",
|
| 264 |
+
choices=["clean", "security", "connections", "test"],
|
| 265 |
+
help="Run specific module only"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
parser.add_argument(
|
| 269 |
+
"--all",
|
| 270 |
+
action="store_true",
|
| 271 |
+
help="Run all modules (default)"
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
parser.add_argument(
|
| 275 |
+
"--config",
|
| 276 |
+
default="agent_config.yaml",
|
| 277 |
+
help="Path to custom config file"
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
args = parser.parse_args()
|
| 281 |
+
|
| 282 |
+
# Create agent
|
| 283 |
+
agent = CodeQualityAgent(config_path=args.config)
|
| 284 |
+
|
| 285 |
+
# Run agent
|
| 286 |
+
if args.module:
|
| 287 |
+
agent.run_module(args.module, dry_run=args.dry_run)
|
| 288 |
+
else:
|
| 289 |
+
agent.run_all(dry_run=args.dry_run)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
if __name__ == "__main__":
|
| 293 |
+
main()
|
frontend/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend - Stock Analysis Authentication UI
|
| 2 |
+
|
| 3 |
+
A **visually stunning authentication interface** with dark cyberpunk aesthetics.
|
| 4 |
+
|
| 5 |
+
## 🎨 Design Features
|
| 6 |
+
|
| 7 |
+
- **Dark Cyberpunk Theme** - Deep space blue with neon cyan/green accents
|
| 8 |
+
- **Unique Typography** - Syne, JetBrains Mono, Orbitron (NO generic fonts!)
|
| 9 |
+
- **Animated Gradient Background** - Smooth color shifting
|
| 10 |
+
- **Floating Particles** - CSS-only geometric animations
|
| 11 |
+
- **Glassmorphism** - Blurred card effects
|
| 12 |
+
- **Glowing Inputs** - Neon border effects on focus
|
| 13 |
+
- **Smooth Micro-interactions** - 60fps animations
|
| 14 |
+
- **Fully Responsive** - Mobile, tablet, desktop
|
| 15 |
+
|
| 16 |
+
## 📁 Structure
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
frontend/
|
| 20 |
+
├── index.html # Login/Signup page
|
| 21 |
+
├── dashboard.html # Protected dashboard
|
| 22 |
+
├── css/
|
| 23 |
+
│ ├── variables.css # Design tokens
|
| 24 |
+
│ ├── base.css # Reset & typography
|
| 25 |
+
│ ├── animations.css # Keyframe animations
|
| 26 |
+
│ └── auth.css # Auth page styles
|
| 27 |
+
└── js/
|
| 28 |
+
├── config.js # API configuration
|
| 29 |
+
├── utils.js # Helper functions
|
| 30 |
+
└── auth.js # Authentication logic
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## 🚀 Quick Start
|
| 34 |
+
|
| 35 |
+
### 1. Make sure backend is running
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
cd ../backend
|
| 39 |
+
uvicorn app.main:app --reload
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Backend should be running on `http://localhost:8000`
|
| 43 |
+
|
| 44 |
+
### 2. Open the frontend
|
| 45 |
+
|
| 46 |
+
**Option A: Using Live Server (VS Code)**
|
| 47 |
+
1. Install "Live Server" extension
|
| 48 |
+
2. Right-click `index.html`
|
| 49 |
+
3. Select "Open with Live Server"
|
| 50 |
+
|
| 51 |
+
**Option B: Direct File**
|
| 52 |
+
1. Simply open `index.html` in your browser
|
| 53 |
+
2. File path: `E:\VsCode\New_PProject\Stock_anaylsis\frontend\index.html`
|
| 54 |
+
|
| 55 |
+
## 🎯 Features
|
| 56 |
+
|
| 57 |
+
### Authentication Pages
|
| 58 |
+
|
| 59 |
+
**Login Mode** (Default)
|
| 60 |
+
- Email + Password
|
| 61 |
+
- Smooth form validation
|
| 62 |
+
- Loading states
|
| 63 |
+
- Error messages with shake animation
|
| 64 |
+
|
| 65 |
+
**Signup Mode** (Toggle)
|
| 66 |
+
- Email + Name + Password + Role
|
| 67 |
+
- Role selector (Staff/Admin)
|
| 68 |
+
- Auto-login after signup
|
| 69 |
+
- Animated form expansion
|
| 70 |
+
|
| 71 |
+
### Protected Dashboard
|
| 72 |
+
|
| 73 |
+
- Requires valid JWT token
|
| 74 |
+
- Auto-redirects if not authenticated
|
| 75 |
+
- Displays user info
|
| 76 |
+
- Logout functionality
|
| 77 |
+
- Placeholder for portfolio features
|
| 78 |
+
|
| 79 |
+
## 🔐 How It Works
|
| 80 |
+
|
| 81 |
+
### Authentication Flow
|
| 82 |
+
|
| 83 |
+
1. **User enters credentials** → Form validation
|
| 84 |
+
2. **Submit** → API call to backend
|
| 85 |
+
3. **Success** → JWT token saved to localStorage
|
| 86 |
+
4. **Redirect** → Dashboard page
|
| 87 |
+
5. **Dashboard loads** → Verifies token with `/auth/me`
|
| 88 |
+
6. **Token valid** → Display user info
|
| 89 |
+
7. **Token invalid** → Redirect to login
|
| 90 |
+
|
| 91 |
+
### Token Management
|
| 92 |
+
|
| 93 |
+
```javascript
|
| 94 |
+
// Token stored in localStorage
|
| 95 |
+
localStorage.setItem('stock_analysis_token', token);
|
| 96 |
+
|
| 97 |
+
// Used in API calls
|
| 98 |
+
fetch(`${API_BASE_URL}/auth/me`, {
|
| 99 |
+
headers: {
|
| 100 |
+
'Authorization': `Bearer ${token}`
|
| 101 |
+
}
|
| 102 |
+
});
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## 🎨 Design System
|
| 106 |
+
|
| 107 |
+
### Colors
|
| 108 |
+
|
| 109 |
+
```css
|
| 110 |
+
--bg-primary: #0a0e1a; /* Deep space blue */
|
| 111 |
+
--accent-cyan: #00f0ff; /* Primary accent */
|
| 112 |
+
--accent-green: #39ff14; /* Success */
|
| 113 |
+
--accent-purple: #b026ff; /* Hover */
|
| 114 |
+
--accent-red: #ff0055; /* Errors */
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### Typography
|
| 118 |
+
|
| 119 |
+
```css
|
| 120 |
+
--font-heading: 'Syne' /* Bold geometric */
|
| 121 |
+
--font-body: 'JetBrains Mono' /* Monospace */
|
| 122 |
+
--font-accent: 'Orbitron' /* Futuristic */
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Animations
|
| 126 |
+
|
| 127 |
+
- **Page Load**: Staggered fadeInUp (0.1s delay each)
|
| 128 |
+
- **Input Focus**: Glow + lift effect
|
| 129 |
+
- **Button Hover**: Glow expand + lift
|
| 130 |
+
- **Form Toggle**: Smooth opacity transition
|
| 131 |
+
- **Error**: Shake animation
|
| 132 |
+
- **Background**: Infinite gradient shift
|
| 133 |
+
|
| 134 |
+
## 📱 Responsive Breakpoints
|
| 135 |
+
|
| 136 |
+
- **Desktop** (1024px+): Side-by-side layout
|
| 137 |
+
- **Tablet** (768px-1023px): Stacked layout
|
| 138 |
+
- **Mobile** (< 768px): Full-screen card
|
| 139 |
+
|
| 140 |
+
## 🧪 Testing
|
| 141 |
+
|
| 142 |
+
### Test Login
|
| 143 |
+
|
| 144 |
+
1. Open `index.html`
|
| 145 |
+
2. Use credentials from backend signup
|
| 146 |
+
3. Click "Sign In"
|
| 147 |
+
4. Should redirect to dashboard
|
| 148 |
+
|
| 149 |
+
### Test Signup
|
| 150 |
+
|
| 151 |
+
1. Click "Create one"
|
| 152 |
+
2. Fill all fields
|
| 153 |
+
3. Select role (Staff/Admin)
|
| 154 |
+
4. Click "Sign Up"
|
| 155 |
+
5. Should auto-login and redirect
|
| 156 |
+
|
| 157 |
+
### Test Protected Route
|
| 158 |
+
|
| 159 |
+
1. Open `dashboard.html` directly (no login)
|
| 160 |
+
2. Should redirect to `index.html`
|
| 161 |
+
3. Login first
|
| 162 |
+
4. Then access dashboard
|
| 163 |
+
5. Should show user info
|
| 164 |
+
|
| 165 |
+
### Test Token Persistence
|
| 166 |
+
|
| 167 |
+
1. Login successfully
|
| 168 |
+
2. Close browser
|
| 169 |
+
3. Reopen `index.html`
|
| 170 |
+
4. Should auto-redirect to dashboard (token still valid)
|
| 171 |
+
|
| 172 |
+
### Test Logout
|
| 173 |
+
|
| 174 |
+
1. On dashboard, click "Logout"
|
| 175 |
+
2. Should clear token
|
| 176 |
+
3. Should redirect to login
|
| 177 |
+
|
| 178 |
+
## 🎭 Animation Showcase
|
| 179 |
+
|
| 180 |
+
### Page Load Sequence
|
| 181 |
+
|
| 182 |
+
1. Background gradient fades in (0s)
|
| 183 |
+
2. Floating particles appear (0.2s)
|
| 184 |
+
3. Auth card scales in (0.8s)
|
| 185 |
+
4. Title fades in (1s)
|
| 186 |
+
5. Form fields stagger in (1.1s - 1.5s)
|
| 187 |
+
|
| 188 |
+
### Micro-interactions
|
| 189 |
+
|
| 190 |
+
- **Input focus**: Cyan glow + 2px lift
|
| 191 |
+
- **Button hover**: Shadow expand + 3px lift
|
| 192 |
+
- **Form submit**: Button → spinner
|
| 193 |
+
- **Error**: Red glow + horizontal shake
|
| 194 |
+
- **Success**: Green flash
|
| 195 |
+
|
| 196 |
+
## 🔧 Configuration
|
| 197 |
+
|
| 198 |
+
### Change API URL
|
| 199 |
+
|
| 200 |
+
Edit `js/config.js`:
|
| 201 |
+
|
| 202 |
+
```javascript
|
| 203 |
+
const API_BASE_URL = 'http://localhost:8000';
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
### Customize Colors
|
| 207 |
+
|
| 208 |
+
Edit `css/variables.css`:
|
| 209 |
+
|
| 210 |
+
```css
|
| 211 |
+
:root {
|
| 212 |
+
--accent-cyan: #00f0ff; /* Change to your color */
|
| 213 |
+
}
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
## 🌟 Highlights
|
| 217 |
+
|
| 218 |
+
### What Makes This Unique
|
| 219 |
+
|
| 220 |
+
✅ **NOT generic AI design** - Custom cyberpunk aesthetic
|
| 221 |
+
✅ **Distinctive fonts** - Syne, JetBrains Mono, Orbitron
|
| 222 |
+
✅ **Bold color choices** - Neon accents on dark backgrounds
|
| 223 |
+
✅ **Atmospheric backgrounds** - Animated gradients + particles
|
| 224 |
+
✅ **Smooth 60fps animations** - CSS-only, performant
|
| 225 |
+
✅ **Production-ready** - Full error handling, validation
|
| 226 |
+
|
| 227 |
+
### Portfolio Talking Points
|
| 228 |
+
|
| 229 |
+
> "I designed and built a custom authentication UI with a dark cyberpunk aesthetic, featuring animated gradient backgrounds, glassmorphism effects, and smooth micro-interactions. The interface uses vanilla JavaScript for JWT token management and API integration, with a fully responsive design that works across all devices."
|
| 230 |
+
|
| 231 |
+
## 📝 Next Steps
|
| 232 |
+
|
| 233 |
+
- [ ] Add password strength indicator
|
| 234 |
+
- [ ] Add "Remember me" checkbox
|
| 235 |
+
- [ ] Add "Forgot password" flow
|
| 236 |
+
- [ ] Add social login buttons
|
| 237 |
+
- [ ] Add email verification
|
| 238 |
+
- [ ] Build portfolio management features
|
| 239 |
+
|
| 240 |
+
## 🐛 Troubleshooting
|
| 241 |
+
|
| 242 |
+
**Styles not loading?**
|
| 243 |
+
- Check file paths in HTML
|
| 244 |
+
- Ensure all CSS files exist
|
| 245 |
+
|
| 246 |
+
**API calls failing?**
|
| 247 |
+
- Verify backend is running on port 8000
|
| 248 |
+
- Check CORS settings in backend
|
| 249 |
+
|
| 250 |
+
**Animations not smooth?**
|
| 251 |
+
- Use modern browser (Chrome, Firefox, Edge)
|
| 252 |
+
- Check hardware acceleration enabled
|
| 253 |
+
|
| 254 |
+
**Token not persisting?**
|
| 255 |
+
- Check browser localStorage is enabled
|
| 256 |
+
- Check for private/incognito mode
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
**Built with ❤️ and attention to detail**
|
frontend/css/animations.css
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Keyframe Animations */
|
| 2 |
+
|
| 3 |
+
/* Fade In Up - Staggered element reveals */
|
| 4 |
+
@keyframes fadeInUp {
|
| 5 |
+
from {
|
| 6 |
+
opacity: 0;
|
| 7 |
+
transform: translateY(30px);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
to {
|
| 11 |
+
opacity: 1;
|
| 12 |
+
transform: translateY(0);
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Glow Pulse - Pulsing glow effect */
|
| 17 |
+
@keyframes glowPulse {
|
| 18 |
+
|
| 19 |
+
0%,
|
| 20 |
+
100% {
|
| 21 |
+
box-shadow: var(--glow-cyan);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
50% {
|
| 25 |
+
box-shadow: var(--glow-cyan-strong);
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Gradient Shift - Animated background */
|
| 30 |
+
@keyframes gradientShift {
|
| 31 |
+
0% {
|
| 32 |
+
background-position: 0% 50%;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
50% {
|
| 36 |
+
background-position: 100% 50%;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
100% {
|
| 40 |
+
background-position: 0% 50%;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Float Particle - Floating geometric shapes */
|
| 45 |
+
@keyframes floatParticle {
|
| 46 |
+
|
| 47 |
+
0%,
|
| 48 |
+
100% {
|
| 49 |
+
transform: translate(0, 0) rotate(0deg);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
25% {
|
| 53 |
+
transform: translate(10px, -10px) rotate(90deg);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
50% {
|
| 57 |
+
transform: translate(0, -20px) rotate(180deg);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
75% {
|
| 61 |
+
transform: translate(-10px, -10px) rotate(270deg);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Shake - Error animation */
|
| 66 |
+
@keyframes shake {
|
| 67 |
+
|
| 68 |
+
0%,
|
| 69 |
+
100% {
|
| 70 |
+
transform: translateX(0);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
10%,
|
| 74 |
+
30%,
|
| 75 |
+
50%,
|
| 76 |
+
70%,
|
| 77 |
+
90% {
|
| 78 |
+
transform: translateX(-5px);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
20%,
|
| 82 |
+
40%,
|
| 83 |
+
60%,
|
| 84 |
+
80% {
|
| 85 |
+
transform: translateX(5px);
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Spin - Loading spinner */
|
| 90 |
+
@keyframes spin {
|
| 91 |
+
from {
|
| 92 |
+
transform: rotate(0deg);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
to {
|
| 96 |
+
transform: rotate(360deg);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Slide In Right */
|
| 101 |
+
@keyframes slideInRight {
|
| 102 |
+
from {
|
| 103 |
+
opacity: 0;
|
| 104 |
+
transform: translateX(20px);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
to {
|
| 108 |
+
opacity: 1;
|
| 109 |
+
transform: translateX(0);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Slide In Left */
|
| 114 |
+
@keyframes slideInLeft {
|
| 115 |
+
from {
|
| 116 |
+
opacity: 0;
|
| 117 |
+
transform: translateX(-20px);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
to {
|
| 121 |
+
opacity: 1;
|
| 122 |
+
transform: translateX(0);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Scale In */
|
| 127 |
+
@keyframes scaleIn {
|
| 128 |
+
from {
|
| 129 |
+
opacity: 0;
|
| 130 |
+
transform: scale(0.9);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
to {
|
| 134 |
+
opacity: 1;
|
| 135 |
+
transform: scale(1);
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Glow Expand - Button hover effect */
|
| 140 |
+
@keyframes glowExpand {
|
| 141 |
+
from {
|
| 142 |
+
box-shadow: 0 0 10px rgba(0, 240, 255, 0.3);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
to {
|
| 146 |
+
box-shadow: 0 0 25px rgba(0, 240, 255, 0.6), 0 0 50px rgba(0, 240, 255, 0.3);
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* Utility Animation Classes */
|
| 151 |
+
.animate-fadeInUp {
|
| 152 |
+
animation: fadeInUp var(--transition-smooth) ease-out forwards;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.animate-delay-1 {
|
| 156 |
+
animation-delay: 0.1s;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.animate-delay-2 {
|
| 160 |
+
animation-delay: 0.2s;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.animate-delay-3 {
|
| 164 |
+
animation-delay: 0.3s;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.animate-delay-4 {
|
| 168 |
+
animation-delay: 0.4s;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.animate-delay-5 {
|
| 172 |
+
animation-delay: 0.5s;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.animate-delay-6 {
|
| 176 |
+
animation-delay: 0.6s;
|
| 177 |
+
}
|
frontend/css/auth.css
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Authentication Page Styles */
|
| 2 |
+
|
| 3 |
+
/* Main Container */
|
| 4 |
+
.auth-container {
|
| 5 |
+
min-height: 100vh;
|
| 6 |
+
display: grid;
|
| 7 |
+
grid-template-columns: 1fr 1fr;
|
| 8 |
+
position: relative;
|
| 9 |
+
overflow: hidden;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* Left Side - Animated Background */
|
| 13 |
+
.auth-bg {
|
| 14 |
+
position: relative;
|
| 15 |
+
background: var(--gradient-bg);
|
| 16 |
+
background-size: 400% 400%;
|
| 17 |
+
animation: gradientShift 15s ease infinite;
|
| 18 |
+
display: flex;
|
| 19 |
+
align-items: center;
|
| 20 |
+
justify-content: center;
|
| 21 |
+
overflow: hidden;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Grid Overlay */
|
| 25 |
+
.auth-bg::before {
|
| 26 |
+
content: '';
|
| 27 |
+
position: absolute;
|
| 28 |
+
inset: 0;
|
| 29 |
+
background-image:
|
| 30 |
+
linear-gradient(var(--accent-cyan) 1px, transparent 1px),
|
| 31 |
+
linear-gradient(90deg, var(--accent-cyan) 1px, transparent 1px);
|
| 32 |
+
background-size: 50px 50px;
|
| 33 |
+
opacity: 0.03;
|
| 34 |
+
animation: fadeInUp 1s ease-out;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Floating Particles */
|
| 38 |
+
.particle {
|
| 39 |
+
position: absolute;
|
| 40 |
+
width: 4px;
|
| 41 |
+
height: 4px;
|
| 42 |
+
background: var(--accent-cyan);
|
| 43 |
+
border-radius: 50%;
|
| 44 |
+
box-shadow: var(--glow-cyan);
|
| 45 |
+
animation: floatParticle 20s infinite ease-in-out;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.particle:nth-child(1) {
|
| 49 |
+
top: 20%;
|
| 50 |
+
left: 20%;
|
| 51 |
+
animation-delay: 0s;
|
| 52 |
+
animation-duration: 15s;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.particle:nth-child(2) {
|
| 56 |
+
top: 60%;
|
| 57 |
+
left: 30%;
|
| 58 |
+
animation-delay: 2s;
|
| 59 |
+
animation-duration: 18s;
|
| 60 |
+
background: var(--accent-green);
|
| 61 |
+
box-shadow: var(--glow-green);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.particle:nth-child(3) {
|
| 65 |
+
top: 40%;
|
| 66 |
+
left: 70%;
|
| 67 |
+
animation-delay: 4s;
|
| 68 |
+
animation-duration: 22s;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.particle:nth-child(4) {
|
| 72 |
+
top: 80%;
|
| 73 |
+
left: 50%;
|
| 74 |
+
animation-delay: 1s;
|
| 75 |
+
animation-duration: 20s;
|
| 76 |
+
background: var(--accent-purple);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.particle:nth-child(5) {
|
| 80 |
+
top: 30%;
|
| 81 |
+
left: 80%;
|
| 82 |
+
animation-delay: 3s;
|
| 83 |
+
animation-duration: 16s;
|
| 84 |
+
background: var(--accent-green);
|
| 85 |
+
box-shadow: var(--glow-green);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/* Brand Section */
|
| 89 |
+
.brand-section {
|
| 90 |
+
z-index: 10;
|
| 91 |
+
text-align: center;
|
| 92 |
+
padding: var(--space-xl);
|
| 93 |
+
opacity: 0;
|
| 94 |
+
animation: fadeInUp 0.8s ease-out 0.4s forwards;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.brand-logo {
|
| 98 |
+
font-family: var(--font-accent);
|
| 99 |
+
font-size: 4rem;
|
| 100 |
+
font-weight: 900;
|
| 101 |
+
background: var(--gradient-primary);
|
| 102 |
+
-webkit-background-clip: text;
|
| 103 |
+
-webkit-text-fill-color: transparent;
|
| 104 |
+
background-clip: text;
|
| 105 |
+
margin-bottom: var(--space-md);
|
| 106 |
+
letter-spacing: 0.05em;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.brand-tagline {
|
| 110 |
+
font-size: 1.1rem;
|
| 111 |
+
color: var(--text-secondary);
|
| 112 |
+
font-weight: 500;
|
| 113 |
+
letter-spacing: 0.1em;
|
| 114 |
+
text-transform: uppercase;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* Right Side - Auth Form */
|
| 118 |
+
.auth-form-container {
|
| 119 |
+
display: flex;
|
| 120 |
+
align-items: center;
|
| 121 |
+
justify-content: center;
|
| 122 |
+
padding: var(--space-xl);
|
| 123 |
+
background: var(--bg-secondary);
|
| 124 |
+
position: relative;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* Decorative Elements */
|
| 128 |
+
.auth-form-container::before {
|
| 129 |
+
content: '';
|
| 130 |
+
position: absolute;
|
| 131 |
+
top: 0;
|
| 132 |
+
left: 0;
|
| 133 |
+
width: 100%;
|
| 134 |
+
height: 2px;
|
| 135 |
+
background: var(--gradient-primary);
|
| 136 |
+
opacity: 0;
|
| 137 |
+
animation: slideInRight 0.6s ease-out 0.6s forwards;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Auth Card */
|
| 141 |
+
.auth-card {
|
| 142 |
+
width: 100%;
|
| 143 |
+
max-width: 480px;
|
| 144 |
+
background: var(--bg-overlay);
|
| 145 |
+
backdrop-filter: blur(20px);
|
| 146 |
+
border-radius: var(--radius-xl);
|
| 147 |
+
padding: var(--space-xl);
|
| 148 |
+
box-shadow: var(--shadow-lg);
|
| 149 |
+
border: 1px solid rgba(0, 240, 255, 0.1);
|
| 150 |
+
opacity: 0;
|
| 151 |
+
animation: scaleIn 0.6s ease-out 0.8s forwards;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* Auth Header */
|
| 155 |
+
.auth-header {
|
| 156 |
+
margin-bottom: var(--space-lg);
|
| 157 |
+
opacity: 0;
|
| 158 |
+
animation: fadeInUp 0.6s ease-out 1s forwards;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.auth-title {
|
| 162 |
+
font-size: 2.5rem;
|
| 163 |
+
margin-bottom: var(--space-xs);
|
| 164 |
+
background: var(--gradient-primary);
|
| 165 |
+
-webkit-background-clip: text;
|
| 166 |
+
-webkit-text-fill-color: transparent;
|
| 167 |
+
background-clip: text;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.auth-subtitle {
|
| 171 |
+
color: var(--text-secondary);
|
| 172 |
+
font-size: 0.95rem;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Form Groups */
|
| 176 |
+
.form-group {
|
| 177 |
+
margin-bottom: var(--space-md);
|
| 178 |
+
opacity: 0;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.form-group:nth-child(1) {
|
| 182 |
+
animation: fadeInUp 0.5s ease-out 1.1s forwards;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.form-group:nth-child(2) {
|
| 186 |
+
animation: fadeInUp 0.5s ease-out 1.2s forwards;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.form-group:nth-child(3) {
|
| 190 |
+
animation: fadeInUp 0.5s ease-out 1.3s forwards;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.form-group:nth-child(4) {
|
| 194 |
+
animation: fadeInUp 0.5s ease-out 1.4s forwards;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.form-label {
|
| 198 |
+
display: block;
|
| 199 |
+
margin-bottom: var(--space-xs);
|
| 200 |
+
color: var(--text-secondary);
|
| 201 |
+
font-size: 0.85rem;
|
| 202 |
+
font-weight: 500;
|
| 203 |
+
text-transform: uppercase;
|
| 204 |
+
letter-spacing: 0.05em;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.form-input {
|
| 208 |
+
width: 100%;
|
| 209 |
+
padding: var(--space-sm) var(--space-md);
|
| 210 |
+
background: var(--bg-tertiary);
|
| 211 |
+
border: 2px solid transparent;
|
| 212 |
+
border-radius: var(--radius-md);
|
| 213 |
+
color: var(--text-primary);
|
| 214 |
+
font-size: 0.95rem;
|
| 215 |
+
transition: all var(--transition-smooth);
|
| 216 |
+
outline: none;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.form-input::placeholder {
|
| 220 |
+
color: var(--text-muted);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.form-input:focus {
|
| 224 |
+
border-color: var(--accent-cyan);
|
| 225 |
+
box-shadow: var(--glow-cyan);
|
| 226 |
+
transform: translateY(-2px);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.form-input:invalid:not(:placeholder-shown) {
|
| 230 |
+
border-color: var(--accent-red);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* Role Selector */
|
| 234 |
+
.role-selector {
|
| 235 |
+
display: grid;
|
| 236 |
+
grid-template-columns: 1fr 1fr;
|
| 237 |
+
gap: var(--space-sm);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.role-option {
|
| 241 |
+
position: relative;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.role-option input[type="radio"] {
|
| 245 |
+
position: absolute;
|
| 246 |
+
opacity: 0;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.role-label {
|
| 250 |
+
display: block;
|
| 251 |
+
padding: var(--space-sm);
|
| 252 |
+
background: var(--bg-tertiary);
|
| 253 |
+
border: 2px solid transparent;
|
| 254 |
+
border-radius: var(--radius-md);
|
| 255 |
+
text-align: center;
|
| 256 |
+
cursor: pointer;
|
| 257 |
+
transition: all var(--transition-smooth);
|
| 258 |
+
font-weight: 500;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.role-option input[type="radio"]:checked+.role-label {
|
| 262 |
+
border-color: var(--accent-cyan);
|
| 263 |
+
background: rgba(0, 240, 255, 0.1);
|
| 264 |
+
box-shadow: var(--glow-cyan);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.role-label:hover {
|
| 268 |
+
border-color: var(--accent-purple);
|
| 269 |
+
transform: translateY(-2px);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Submit Button */
|
| 273 |
+
.btn-submit {
|
| 274 |
+
width: 100%;
|
| 275 |
+
padding: var(--space-md);
|
| 276 |
+
background: var(--gradient-primary);
|
| 277 |
+
color: var(--text-primary);
|
| 278 |
+
font-weight: 700;
|
| 279 |
+
font-size: 1rem;
|
| 280 |
+
text-transform: uppercase;
|
| 281 |
+
letter-spacing: 0.1em;
|
| 282 |
+
border-radius: var(--radius-md);
|
| 283 |
+
transition: all var(--transition-smooth);
|
| 284 |
+
position: relative;
|
| 285 |
+
overflow: hidden;
|
| 286 |
+
opacity: 0;
|
| 287 |
+
animation: fadeInUp 0.5s ease-out 1.5s forwards;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.btn-submit::before {
|
| 291 |
+
content: '';
|
| 292 |
+
position: absolute;
|
| 293 |
+
inset: 0;
|
| 294 |
+
background: linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100%);
|
| 295 |
+
transform: translateX(-100%);
|
| 296 |
+
transition: transform 0.6s;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.btn-submit:hover {
|
| 300 |
+
transform: translateY(-3px);
|
| 301 |
+
box-shadow: 0 8px 24px rgba(0, 240, 255, 0.4);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.btn-submit:hover::before {
|
| 305 |
+
transform: translateX(100%);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.btn-submit:active {
|
| 309 |
+
transform: translateY(-1px);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.btn-submit:disabled {
|
| 313 |
+
opacity: 0.6;
|
| 314 |
+
cursor: not-allowed;
|
| 315 |
+
transform: none;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* Loading Spinner */
|
| 319 |
+
.spinner {
|
| 320 |
+
display: inline-block;
|
| 321 |
+
width: 16px;
|
| 322 |
+
height: 16px;
|
| 323 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 324 |
+
border-top-color: white;
|
| 325 |
+
border-radius: 50%;
|
| 326 |
+
animation: spin 0.8s linear infinite;
|
| 327 |
+
margin-right: var(--space-xs);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/* Toggle Auth Mode */
|
| 331 |
+
.auth-toggle {
|
| 332 |
+
text-align: center;
|
| 333 |
+
margin-top: var(--space-lg);
|
| 334 |
+
color: var(--text-secondary);
|
| 335 |
+
font-size: 0.9rem;
|
| 336 |
+
opacity: 0;
|
| 337 |
+
animation: fadeInUp 0.5s ease-out 1.6s forwards;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.auth-toggle-link {
|
| 341 |
+
color: var(--accent-cyan);
|
| 342 |
+
font-weight: 600;
|
| 343 |
+
cursor: pointer;
|
| 344 |
+
transition: color var(--transition-fast);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.auth-toggle-link:hover {
|
| 348 |
+
color: var(--accent-purple);
|
| 349 |
+
text-decoration: underline;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* Alert Messages */
|
| 353 |
+
.alert {
|
| 354 |
+
padding: var(--space-sm) var(--space-md);
|
| 355 |
+
border-radius: var(--radius-md);
|
| 356 |
+
margin-bottom: var(--space-md);
|
| 357 |
+
font-size: 0.9rem;
|
| 358 |
+
font-weight: 500;
|
| 359 |
+
animation: slideInRight 0.3s ease-out;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.alert-error {
|
| 363 |
+
background: rgba(255, 0, 85, 0.1);
|
| 364 |
+
border: 1px solid var(--accent-red);
|
| 365 |
+
color: var(--accent-red);
|
| 366 |
+
animation: shake 0.5s ease-out, slideInRight 0.3s ease-out;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.alert-success {
|
| 370 |
+
background: rgba(57, 255, 20, 0.1);
|
| 371 |
+
border: 1px solid var(--accent-green);
|
| 372 |
+
color: var(--accent-green);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* Responsive Design */
|
| 376 |
+
@media (max-width: 1024px) {
|
| 377 |
+
.auth-container {
|
| 378 |
+
grid-template-columns: 1fr;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.auth-bg {
|
| 382 |
+
display: none;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.auth-form-container {
|
| 386 |
+
min-height: 100vh;
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
@media (max-width: 640px) {
|
| 391 |
+
.auth-card {
|
| 392 |
+
padding: var(--space-lg);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.auth-title {
|
| 396 |
+
font-size: 2rem;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.brand-logo {
|
| 400 |
+
font-size: 3rem;
|
| 401 |
+
}
|
| 402 |
+
}
|
frontend/css/base.css
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* CSS Reset & Base Styles */
|
| 2 |
+
|
| 3 |
+
* {
|
| 4 |
+
margin: 0;
|
| 5 |
+
padding: 0;
|
| 6 |
+
box-sizing: border-box;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
html {
|
| 10 |
+
font-size: 16px;
|
| 11 |
+
scroll-behavior: smooth;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: var(--font-body);
|
| 16 |
+
font-size: 0.9rem;
|
| 17 |
+
line-height: 1.6;
|
| 18 |
+
color: var(--text-primary);
|
| 19 |
+
background: var(--bg-primary);
|
| 20 |
+
overflow-x: hidden;
|
| 21 |
+
-webkit-font-smoothing: antialiased;
|
| 22 |
+
-moz-osx-font-smoothing: grayscale;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* Typography */
|
| 26 |
+
h1,
|
| 27 |
+
h2,
|
| 28 |
+
h3,
|
| 29 |
+
h4,
|
| 30 |
+
h5,
|
| 31 |
+
h6 {
|
| 32 |
+
font-family: var(--font-heading);
|
| 33 |
+
font-weight: 800;
|
| 34 |
+
line-height: 1.2;
|
| 35 |
+
margin-bottom: var(--space-sm);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1 {
|
| 39 |
+
font-size: 3rem;
|
| 40 |
+
letter-spacing: -0.02em;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
h2 {
|
| 44 |
+
font-size: 2rem;
|
| 45 |
+
letter-spacing: -0.01em;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
h3 {
|
| 49 |
+
font-size: 1.5rem;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
p {
|
| 53 |
+
margin-bottom: var(--space-sm);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
a {
|
| 57 |
+
color: var(--accent-cyan);
|
| 58 |
+
text-decoration: none;
|
| 59 |
+
transition: color var(--transition-fast);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
a:hover {
|
| 63 |
+
color: var(--accent-purple);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Form Elements */
|
| 67 |
+
input,
|
| 68 |
+
button,
|
| 69 |
+
select,
|
| 70 |
+
textarea {
|
| 71 |
+
font-family: var(--font-body);
|
| 72 |
+
font-size: 0.9rem;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
button {
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
border: none;
|
| 78 |
+
outline: none;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Utility Classes */
|
| 82 |
+
.text-center {
|
| 83 |
+
text-align: center;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.text-muted {
|
| 87 |
+
color: var(--text-muted);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.text-secondary {
|
| 91 |
+
color: var(--text-secondary);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.mb-sm {
|
| 95 |
+
margin-bottom: var(--space-sm);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.mb-md {
|
| 99 |
+
margin-bottom: var(--space-md);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.mb-lg {
|
| 103 |
+
margin-bottom: var(--space-lg);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.hidden {
|
| 107 |
+
display: none !important;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Scrollbar Styling */
|
| 111 |
+
::-webkit-scrollbar {
|
| 112 |
+
width: 8px;
|
| 113 |
+
height: 8px;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
::-webkit-scrollbar-track {
|
| 117 |
+
background: var(--bg-secondary);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
::-webkit-scrollbar-thumb {
|
| 121 |
+
background: var(--accent-cyan);
|
| 122 |
+
border-radius: var(--radius-sm);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
::-webkit-scrollbar-thumb:hover {
|
| 126 |
+
background: var(--accent-purple);
|
| 127 |
+
}
|
frontend/css/variables.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
/* Color Palette - Dark Cyberpunk Theme */
|
| 3 |
+
--bg-primary: #0a0e1a;
|
| 4 |
+
--bg-secondary: #141b2d;
|
| 5 |
+
--bg-tertiary: #1a2332;
|
| 6 |
+
--bg-overlay: rgba(20, 27, 45, 0.85);
|
| 7 |
+
|
| 8 |
+
/* Accent Colors */
|
| 9 |
+
--accent-cyan: #00f0ff;
|
| 10 |
+
--accent-green: #39ff14;
|
| 11 |
+
--accent-purple: #b026ff;
|
| 12 |
+
--accent-red: #ff0055;
|
| 13 |
+
--accent-orange: #ff6b35;
|
| 14 |
+
|
| 15 |
+
/* Text Colors */
|
| 16 |
+
--text-primary: #e8edf5;
|
| 17 |
+
--text-secondary: #8b95a8;
|
| 18 |
+
--text-muted: #4a5568;
|
| 19 |
+
|
| 20 |
+
/* Glow Effects */
|
| 21 |
+
--glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5);
|
| 22 |
+
--glow-cyan-strong: 0 0 30px rgba(0, 240, 255, 0.8);
|
| 23 |
+
--glow-green: 0 0 20px rgba(57, 255, 20, 0.5);
|
| 24 |
+
--glow-red: 0 0 20px rgba(255, 0, 85, 0.5);
|
| 25 |
+
|
| 26 |
+
/* Typography */
|
| 27 |
+
--font-heading: 'Syne', sans-serif;
|
| 28 |
+
--font-body: 'JetBrains Mono', monospace;
|
| 29 |
+
--font-accent: 'Orbitron', sans-serif;
|
| 30 |
+
|
| 31 |
+
/* Spacing */
|
| 32 |
+
--space-xs: 0.5rem;
|
| 33 |
+
--space-sm: 1rem;
|
| 34 |
+
--space-md: 1.5rem;
|
| 35 |
+
--space-lg: 2rem;
|
| 36 |
+
--space-xl: 3rem;
|
| 37 |
+
|
| 38 |
+
/* Border Radius */
|
| 39 |
+
--radius-sm: 4px;
|
| 40 |
+
--radius-md: 8px;
|
| 41 |
+
--radius-lg: 16px;
|
| 42 |
+
--radius-xl: 24px;
|
| 43 |
+
|
| 44 |
+
/* Transitions */
|
| 45 |
+
--transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 46 |
+
--transition-smooth: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 47 |
+
--transition-slow: 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
| 48 |
+
|
| 49 |
+
/* Shadows */
|
| 50 |
+
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 51 |
+
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
| 52 |
+
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
|
| 53 |
+
|
| 54 |
+
/* Gradients */
|
| 55 |
+
--gradient-primary: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 100%);
|
| 56 |
+
--gradient-secondary: linear-gradient(135deg, var(--accent-green) 0%, var(--accent-cyan) 100%);
|
| 57 |
+
--gradient-bg: linear-gradient(135deg, #0a0e1a 0%, #141b2d 50%, #1a2332 100%);
|
| 58 |
+
}
|
frontend/dashboard.html
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>STOCKLYZE - Dashboard</title>
|
| 7 |
+
|
| 8 |
+
<!-- Google Fonts -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<style>
|
| 14 |
+
* {
|
| 15 |
+
margin: 0;
|
| 16 |
+
padding: 0;
|
| 17 |
+
box-sizing: border-box;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Inter', sans-serif;
|
| 22 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
| 23 |
+
min-height: 100vh;
|
| 24 |
+
position: relative;
|
| 25 |
+
overflow-x: hidden;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Animated Gradient Blobs */
|
| 29 |
+
.gradient-blob {
|
| 30 |
+
position: fixed;
|
| 31 |
+
width: 500px;
|
| 32 |
+
height: 500px;
|
| 33 |
+
background: radial-gradient(circle, rgba(0, 200, 180, 0.3) 0%, rgba(0, 150, 255, 0.2) 50%, transparent 70%);
|
| 34 |
+
border-radius: 50%;
|
| 35 |
+
filter: blur(80px);
|
| 36 |
+
animation: float 10s ease-in-out infinite;
|
| 37 |
+
z-index: 0;
|
| 38 |
+
pointer-events: none;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.gradient-blob:nth-child(1) {
|
| 42 |
+
top: -150px;
|
| 43 |
+
right: -150px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.gradient-blob:nth-child(2) {
|
| 47 |
+
bottom: -150px;
|
| 48 |
+
left: -150px;
|
| 49 |
+
animation-delay: 5s;
|
| 50 |
+
background: radial-gradient(circle, rgba(100, 200, 255, 0.3) 0%, rgba(0, 180, 200, 0.2) 50%, transparent 70%);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
@keyframes float {
|
| 54 |
+
0%, 100% {
|
| 55 |
+
transform: translate(0, 0) scale(1);
|
| 56 |
+
}
|
| 57 |
+
50% {
|
| 58 |
+
transform: translate(30px, 30px) scale(1.05);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Header */
|
| 63 |
+
.header {
|
| 64 |
+
position: relative;
|
| 65 |
+
z-index: 10;
|
| 66 |
+
background: rgba(255, 255, 255, 0.95);
|
| 67 |
+
backdrop-filter: blur(10px);
|
| 68 |
+
padding: 20px 40px;
|
| 69 |
+
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
|
| 70 |
+
display: flex;
|
| 71 |
+
justify-content: space-between;
|
| 72 |
+
align-items: center;
|
| 73 |
+
margin-bottom: 40px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.logo-section {
|
| 77 |
+
display: flex;
|
| 78 |
+
align-items: center;
|
| 79 |
+
gap: 15px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.logo-icon {
|
| 83 |
+
width: 45px;
|
| 84 |
+
height: 45px;
|
| 85 |
+
background: linear-gradient(135deg, #00c896 0%, #00a8cc 100%);
|
| 86 |
+
border-radius: 10px;
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
justify-content: center;
|
| 90 |
+
font-size: 24px;
|
| 91 |
+
box-shadow: 0 4px 12px rgba(0, 200, 150, 0.3);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.logo-text {
|
| 95 |
+
font-size: 28px;
|
| 96 |
+
font-weight: 800;
|
| 97 |
+
color: #1a1a2e;
|
| 98 |
+
font-family: 'Poppins', sans-serif;
|
| 99 |
+
letter-spacing: -1px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.header-actions {
|
| 103 |
+
display: flex;
|
| 104 |
+
align-items: center;
|
| 105 |
+
gap: 20px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.refresh-btn {
|
| 109 |
+
padding: 12px 24px;
|
| 110 |
+
background: linear-gradient(135deg, #00c896 0%, #00a8cc 100%);
|
| 111 |
+
color: white;
|
| 112 |
+
border: none;
|
| 113 |
+
border-radius: 50px;
|
| 114 |
+
font-weight: 600;
|
| 115 |
+
cursor: pointer;
|
| 116 |
+
transition: all 0.3s ease;
|
| 117 |
+
box-shadow: 0 4px 15px rgba(0, 200, 150, 0.3);
|
| 118 |
+
font-size: 14px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.refresh-btn:hover {
|
| 122 |
+
transform: translateY(-2px);
|
| 123 |
+
box-shadow: 0 6px 20px rgba(0, 200, 150, 0.5);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.refresh-btn:active {
|
| 127 |
+
transform: translateY(0);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Profile Menu */
|
| 131 |
+
.profile-container {
|
| 132 |
+
position: relative;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.profile-btn {
|
| 136 |
+
width: 45px;
|
| 137 |
+
height: 45px;
|
| 138 |
+
border-radius: 50%;
|
| 139 |
+
background: linear-gradient(135deg, #00c896 0%, #00a8cc 100%);
|
| 140 |
+
border: 3px solid white;
|
| 141 |
+
color: white;
|
| 142 |
+
font-size: 20px;
|
| 143 |
+
cursor: pointer;
|
| 144 |
+
display: flex;
|
| 145 |
+
align-items: center;
|
| 146 |
+
justify-content: center;
|
| 147 |
+
transition: all 0.3s ease;
|
| 148 |
+
box-shadow: 0 4px 12px rgba(0, 200, 150, 0.3);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.profile-btn:hover {
|
| 152 |
+
transform: scale(1.05);
|
| 153 |
+
box-shadow: 0 6px 16px rgba(0, 200, 150, 0.5);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.profile-menu {
|
| 157 |
+
display: none;
|
| 158 |
+
position: absolute;
|
| 159 |
+
top: 60px;
|
| 160 |
+
right: 0;
|
| 161 |
+
background: white;
|
| 162 |
+
border-radius: 16px;
|
| 163 |
+
min-width: 200px;
|
| 164 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
| 165 |
+
overflow: hidden;
|
| 166 |
+
z-index: 1000;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.profile-menu.active {
|
| 170 |
+
display: block;
|
| 171 |
+
animation: slideDown 0.3s ease;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
@keyframes slideDown {
|
| 175 |
+
from {
|
| 176 |
+
opacity: 0;
|
| 177 |
+
transform: translateY(-10px);
|
| 178 |
+
}
|
| 179 |
+
to {
|
| 180 |
+
opacity: 1;
|
| 181 |
+
transform: translateY(0);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.profile-header {
|
| 186 |
+
padding: 15px 20px;
|
| 187 |
+
background: linear-gradient(135deg, #00c896 0%, #00a8cc 100%);
|
| 188 |
+
color: white;
|
| 189 |
+
font-weight: 600;
|
| 190 |
+
font-size: 14px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.menu-item {
|
| 194 |
+
width: 100%;
|
| 195 |
+
padding: 14px 20px;
|
| 196 |
+
background: transparent;
|
| 197 |
+
border: none;
|
| 198 |
+
color: #2c3e50;
|
| 199 |
+
text-align: left;
|
| 200 |
+
cursor: pointer;
|
| 201 |
+
font-weight: 500;
|
| 202 |
+
transition: background 0.2s ease;
|
| 203 |
+
display: flex;
|
| 204 |
+
align-items: center;
|
| 205 |
+
gap: 10px;
|
| 206 |
+
font-size: 14px;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.menu-item:hover {
|
| 210 |
+
background: #f5f7fa;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.menu-item.logout {
|
| 214 |
+
color: #e74c3c;
|
| 215 |
+
border-top: 1px solid #e8ecf1;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.menu-item.logout:hover {
|
| 219 |
+
background: #fee;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Main Content */
|
| 223 |
+
.container {
|
| 224 |
+
position: relative;
|
| 225 |
+
z-index: 1;
|
| 226 |
+
max-width: 1400px;
|
| 227 |
+
margin: 0 auto;
|
| 228 |
+
padding: 0 40px 40px;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.dashboard-header {
|
| 232 |
+
margin-bottom: 30px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.page-title {
|
| 236 |
+
font-size: 36px;
|
| 237 |
+
font-weight: 800;
|
| 238 |
+
color: #1a1a2e;
|
| 239 |
+
margin-bottom: 10px;
|
| 240 |
+
font-family: 'Poppins', sans-serif;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.status-badge {
|
| 244 |
+
display: inline-flex;
|
| 245 |
+
align-items: center;
|
| 246 |
+
gap: 8px;
|
| 247 |
+
padding: 8px 16px;
|
| 248 |
+
background: rgba(0, 200, 150, 0.1);
|
| 249 |
+
border: 2px solid #00c896;
|
| 250 |
+
border-radius: 50px;
|
| 251 |
+
color: #00c896;
|
| 252 |
+
font-size: 13px;
|
| 253 |
+
font-weight: 600;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.status-dot {
|
| 257 |
+
width: 8px;
|
| 258 |
+
height: 8px;
|
| 259 |
+
background: #00c896;
|
| 260 |
+
border-radius: 50%;
|
| 261 |
+
animation: pulse 2s ease-in-out infinite;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
@keyframes pulse {
|
| 265 |
+
0%, 100% {
|
| 266 |
+
opacity: 1;
|
| 267 |
+
}
|
| 268 |
+
50% {
|
| 269 |
+
opacity: 0.5;
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* Stock Cards Grid */
|
| 274 |
+
.stocks-grid {
|
| 275 |
+
display: grid;
|
| 276 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 277 |
+
gap: 24px;
|
| 278 |
+
margin-top: 30px;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.stock-card {
|
| 282 |
+
background: rgba(255, 255, 255, 0.95);
|
| 283 |
+
border-radius: 20px;
|
| 284 |
+
padding: 28px;
|
| 285 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
| 286 |
+
transition: all 0.3s ease;
|
| 287 |
+
cursor: pointer;
|
| 288 |
+
position: relative;
|
| 289 |
+
overflow: hidden;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.stock-card::before {
|
| 293 |
+
content: '';
|
| 294 |
+
position: absolute;
|
| 295 |
+
top: 0;
|
| 296 |
+
left: 0;
|
| 297 |
+
right: 0;
|
| 298 |
+
height: 4px;
|
| 299 |
+
background: linear-gradient(90deg, #00c896 0%, #00a8cc 100%);
|
| 300 |
+
transform: scaleX(0);
|
| 301 |
+
transition: transform 0.3s ease;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.stock-card:hover {
|
| 305 |
+
transform: translateY(-8px);
|
| 306 |
+
box-shadow: 0 12px 40px rgba(0, 200, 150, 0.2);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.stock-card:hover::before {
|
| 310 |
+
transform: scaleX(1);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.stock-header {
|
| 314 |
+
display: flex;
|
| 315 |
+
justify-content: space-between;
|
| 316 |
+
align-items: flex-start;
|
| 317 |
+
margin-bottom: 20px;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.stock-symbol {
|
| 321 |
+
font-size: 28px;
|
| 322 |
+
font-weight: 800;
|
| 323 |
+
color: #1a1a2e;
|
| 324 |
+
font-family: 'Poppins', sans-serif;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.stock-trend-icon {
|
| 328 |
+
font-size: 24px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.stock-name {
|
| 332 |
+
font-size: 13px;
|
| 333 |
+
color: #7f8c8d;
|
| 334 |
+
margin-bottom: 20px;
|
| 335 |
+
font-weight: 500;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.stock-price {
|
| 339 |
+
font-size: 40px;
|
| 340 |
+
font-weight: 800;
|
| 341 |
+
color: #1a1a2e;
|
| 342 |
+
margin-bottom: 12px;
|
| 343 |
+
font-family: 'Poppins', sans-serif;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.stock-change {
|
| 347 |
+
display: inline-flex;
|
| 348 |
+
align-items: center;
|
| 349 |
+
gap: 6px;
|
| 350 |
+
padding: 8px 16px;
|
| 351 |
+
border-radius: 50px;
|
| 352 |
+
font-size: 14px;
|
| 353 |
+
font-weight: 700;
|
| 354 |
+
margin-bottom: 16px;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.stock-change.positive {
|
| 358 |
+
background: rgba(0, 200, 150, 0.15);
|
| 359 |
+
color: #00c896;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.stock-change.negative {
|
| 363 |
+
background: rgba(231, 76, 60, 0.15);
|
| 364 |
+
color: #e74c3c;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.stock-meta {
|
| 368 |
+
display: flex;
|
| 369 |
+
justify-content: space-between;
|
| 370 |
+
padding-top: 16px;
|
| 371 |
+
border-top: 2px solid #f5f7fa;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.meta-item {
|
| 375 |
+
display: flex;
|
| 376 |
+
flex-direction: column;
|
| 377 |
+
gap: 4px;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.meta-label {
|
| 381 |
+
font-size: 11px;
|
| 382 |
+
color: #95a5a6;
|
| 383 |
+
text-transform: uppercase;
|
| 384 |
+
letter-spacing: 0.5px;
|
| 385 |
+
font-weight: 600;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.meta-value {
|
| 389 |
+
font-size: 14px;
|
| 390 |
+
color: #2c3e50;
|
| 391 |
+
font-weight: 600;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
/* Loading State */
|
| 395 |
+
.loading {
|
| 396 |
+
text-align: center;
|
| 397 |
+
padding: 60px 20px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.loading-spinner {
|
| 401 |
+
width: 50px;
|
| 402 |
+
height: 50px;
|
| 403 |
+
border: 4px solid #e8ecf1;
|
| 404 |
+
border-top-color: #00c896;
|
| 405 |
+
border-radius: 50%;
|
| 406 |
+
animation: spin 1s linear infinite;
|
| 407 |
+
margin: 0 auto 20px;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
@keyframes spin {
|
| 411 |
+
to {
|
| 412 |
+
transform: rotate(360deg);
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
/* Responsive Design */
|
| 417 |
+
@media (max-width: 768px) {
|
| 418 |
+
.header {
|
| 419 |
+
padding: 15px 20px;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.container {
|
| 423 |
+
padding: 0 20px 20px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.logo-text {
|
| 427 |
+
font-size: 22px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.page-title {
|
| 431 |
+
font-size: 28px;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.stocks-grid {
|
| 435 |
+
grid-template-columns: 1fr;
|
| 436 |
+
gap: 16px;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.refresh-btn {
|
| 440 |
+
padding: 10px 18px;
|
| 441 |
+
font-size: 13px;
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
</style>
|
| 445 |
+
</head>
|
| 446 |
+
<body>
|
| 447 |
+
<!-- Animated Gradient Blobs -->
|
| 448 |
+
<div class="gradient-blob"></div>
|
| 449 |
+
<div class="gradient-blob"></div>
|
| 450 |
+
|
| 451 |
+
<!-- Header -->
|
| 452 |
+
<header class="header">
|
| 453 |
+
<div class="logo-section">
|
| 454 |
+
<div class="logo-icon">📈</div>
|
| 455 |
+
<div class="logo-text">STOCKLYZE</div>
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
<div class="header-actions">
|
| 459 |
+
<button class="refresh-btn" onclick="loadStocks()">
|
| 460 |
+
🔄 Refresh Data
|
| 461 |
+
</button>
|
| 462 |
+
|
| 463 |
+
<div class="profile-container">
|
| 464 |
+
<button class="profile-btn" onclick="toggleProfileMenu()" id="profile-btn">
|
| 465 |
+
👤
|
| 466 |
+
</button>
|
| 467 |
+
|
| 468 |
+
<div class="profile-menu" id="profile-menu">
|
| 469 |
+
<div class="profile-header">
|
| 470 |
+
Admin User
|
| 471 |
+
</div>
|
| 472 |
+
<button class="menu-item" onclick="window.location.href='portfolio.html'">
|
| 473 |
+
💼 My Portfolio
|
| 474 |
+
</button>
|
| 475 |
+
<button class="menu-item logout" onclick="logout()">
|
| 476 |
+
🚪 Logout
|
| 477 |
+
</button>
|
| 478 |
+
</div>
|
| 479 |
+
</div>
|
| 480 |
+
</div>
|
| 481 |
+
</header>
|
| 482 |
+
|
| 483 |
+
<!-- Main Content -->
|
| 484 |
+
<main class="container">
|
| 485 |
+
<div class="dashboard-header">
|
| 486 |
+
<h1 class="page-title">Stock Dashboard</h1>
|
| 487 |
+
<div class="status-badge" id="status-badge">
|
| 488 |
+
<span class="status-dot"></span>
|
| 489 |
+
<span id="status">Initializing...</span>
|
| 490 |
+
</div>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div class="stocks-grid" id="stocks-grid">
|
| 494 |
+
<!-- Stock cards will be inserted here -->
|
| 495 |
+
</div>
|
| 496 |
+
</main>
|
| 497 |
+
|
| 498 |
+
<script>
|
| 499 |
+
console.log('🚀 Dashboard loaded!');
|
| 500 |
+
|
| 501 |
+
const API_BASE_URL = 'http://localhost:8000';
|
| 502 |
+
const TOKEN_KEY = 'stock_analysis_token';
|
| 503 |
+
|
| 504 |
+
const MOCK_STOCKS = [
|
| 505 |
+
{ symbol: 'AAPL', name: 'Apple Inc.', price: 259.48, change: 1.19, change_percent: 0.46, volume: 52000000, market_cap: 3980000000000 },
|
| 506 |
+
{ symbol: 'GOOGL', name: 'Alphabet Inc.', price: 338.0, change: -0.24, change_percent: -0.07, volume: 28000000, market_cap: 2100000000000 },
|
| 507 |
+
{ symbol: 'MSFT', name: 'Microsoft Corporation', price: 430.29, change: -3.21, change_percent: -0.74, volume: 31000000, market_cap: 3200000000000 },
|
| 508 |
+
{ symbol: 'TSLA', name: 'Tesla, Inc.', price: 430.41, change: 13.84, change_percent: 3.32, volume: 95000000, market_cap: 1370000000000 },
|
| 509 |
+
{ symbol: 'AMZN', name: 'Amazon.com, Inc.', price: 239.3, change: -2.44, change_percent: -1.01, volume: 42000000, market_cap: 2480000000000 },
|
| 510 |
+
{ symbol: 'META', name: 'Meta Platforms, Inc.', price: 612.75, change: 8.42, change_percent: 1.39, volume: 18500000, market_cap: 1560000000000 }
|
| 511 |
+
];
|
| 512 |
+
|
| 513 |
+
// Check authentication
|
| 514 |
+
if (!localStorage.getItem(TOKEN_KEY)) {
|
| 515 |
+
window.location.href = 'index.html';
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
function displayStocks(stocks) {
|
| 519 |
+
console.log('📊 Displaying stocks:', stocks);
|
| 520 |
+
const grid = document.getElementById('stocks-grid');
|
| 521 |
+
|
| 522 |
+
grid.innerHTML = stocks.map(stock => {
|
| 523 |
+
const isPositive = stock.change >= 0;
|
| 524 |
+
const changeClass = isPositive ? 'positive' : 'negative';
|
| 525 |
+
const changeSymbol = isPositive ? '+' : '';
|
| 526 |
+
const trendIcon = isPositive ? '📈' : '📉';
|
| 527 |
+
|
| 528 |
+
return `
|
| 529 |
+
<div class="stock-card">
|
| 530 |
+
<div class="stock-header">
|
| 531 |
+
<div class="stock-symbol">${stock.symbol}</div>
|
| 532 |
+
<div class="stock-trend-icon">${trendIcon}</div>
|
| 533 |
+
</div>
|
| 534 |
+
<div class="stock-name">${stock.name}</div>
|
| 535 |
+
<div class="stock-price">$${stock.price.toFixed(2)}</div>
|
| 536 |
+
<div class="stock-change ${changeClass}">
|
| 537 |
+
${changeSymbol}${stock.change_percent.toFixed(2)}%
|
| 538 |
+
(${changeSymbol}$${Math.abs(stock.change).toFixed(2)})
|
| 539 |
+
</div>
|
| 540 |
+
<div class="stock-meta">
|
| 541 |
+
<div class="meta-item">
|
| 542 |
+
<span class="meta-label">Volume</span>
|
| 543 |
+
<span class="meta-value">${formatVolume(stock.volume)}</span>
|
| 544 |
+
</div>
|
| 545 |
+
<div class="meta-item">
|
| 546 |
+
<span class="meta-label">Market Cap</span>
|
| 547 |
+
<span class="meta-value">${formatMarketCap(stock.market_cap || 0)}</span>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
`;
|
| 552 |
+
}).join('');
|
| 553 |
+
|
| 554 |
+
console.log('✅ Cards rendered!');
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
function formatVolume(volume) {
|
| 558 |
+
if (volume >= 1000000000) return (volume / 1000000000).toFixed(2) + 'B';
|
| 559 |
+
if (volume >= 1000000) return (volume / 1000000).toFixed(2) + 'M';
|
| 560 |
+
if (volume >= 1000) return (volume / 1000).toFixed(2) + 'K';
|
| 561 |
+
return volume.toString();
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
function formatMarketCap(cap) {
|
| 565 |
+
if (cap >= 1000000000000) return '$' + (cap / 1000000000000).toFixed(2) + 'T';
|
| 566 |
+
if (cap >= 1000000000) return '$' + (cap / 1000000000).toFixed(2) + 'B';
|
| 567 |
+
if (cap >= 1000000) return '$' + (cap / 1000000).toFixed(2) + 'M';
|
| 568 |
+
return '$' + cap.toString();
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
async function loadStocks() {
|
| 572 |
+
const statusEl = document.getElementById('status');
|
| 573 |
+
statusEl.textContent = 'Fetching live data...';
|
| 574 |
+
|
| 575 |
+
try {
|
| 576 |
+
const token = localStorage.getItem(TOKEN_KEY);
|
| 577 |
+
|
| 578 |
+
const stocksRes = await fetch(`${API_BASE_URL}/stocks/popular`, {
|
| 579 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
if (!stocksRes.ok) {
|
| 583 |
+
throw new Error('Failed to fetch stocks');
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
const data = await stocksRes.json();
|
| 587 |
+
displayStocks(data.stocks);
|
| 588 |
+
statusEl.textContent = `Live data - Updated at ${new Date().toLocaleTimeString()}`;
|
| 589 |
+
} catch (error) {
|
| 590 |
+
console.error('Error:', error);
|
| 591 |
+
statusEl.textContent = 'Error loading live data - showing cached';
|
| 592 |
+
displayStocks(MOCK_STOCKS);
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
function toggleProfileMenu() {
|
| 597 |
+
const menu = document.getElementById('profile-menu');
|
| 598 |
+
menu.classList.toggle('active');
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
// Close menu when clicking outside
|
| 602 |
+
document.addEventListener('click', function(event) {
|
| 603 |
+
const menu = document.getElementById('profile-menu');
|
| 604 |
+
const btn = document.getElementById('profile-btn');
|
| 605 |
+
if (!btn.contains(event.target) && !menu.contains(event.target)) {
|
| 606 |
+
menu.classList.remove('active');
|
| 607 |
+
}
|
| 608 |
+
});
|
| 609 |
+
|
| 610 |
+
function logout() {
|
| 611 |
+
localStorage.clear();
|
| 612 |
+
window.location.href = 'index.html';
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// Initialize
|
| 616 |
+
console.log('📊 Showing mock data...');
|
| 617 |
+
displayStocks(MOCK_STOCKS);
|
| 618 |
+
document.getElementById('status').textContent = 'Cached data - Click refresh for live prices';
|
| 619 |
+
|
| 620 |
+
// Load real data after 1 second
|
| 621 |
+
setTimeout(loadStocks, 1000);
|
| 622 |
+
</script>
|
| 623 |
+
</body>
|
| 624 |
+
</html>
|
frontend/dashboard_direct.html
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Dashboard - Stock Analysis</title>
|
| 7 |
+
|
| 8 |
+
<!-- Google Fonts -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=JetBrains+Mono:wght@400;500;700&family=Orbitron:wght@700;900&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<!-- Stylesheets -->
|
| 14 |
+
<link rel="stylesheet" href="css/variables.css">
|
| 15 |
+
<link rel="stylesheet" href="css/base.css">
|
| 16 |
+
<link rel="stylesheet" href="css/animations.css">
|
| 17 |
+
|
| 18 |
+
<style>
|
| 19 |
+
/* Dashboard Styles */
|
| 20 |
+
.dashboard-container {
|
| 21 |
+
min-height: 100vh;
|
| 22 |
+
background: var(--bg-primary);
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Header */
|
| 28 |
+
.dashboard-header {
|
| 29 |
+
background: var(--bg-secondary);
|
| 30 |
+
border-bottom: 2px solid var(--accent-cyan);
|
| 31 |
+
padding: var(--space-md) var(--space-xl);
|
| 32 |
+
display: flex;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: center;
|
| 35 |
+
animation: slideInRight 0.6s ease-out;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.dashboard-logo {
|
| 39 |
+
font-family: var(--font-accent);
|
| 40 |
+
font-size: 1.5rem;
|
| 41 |
+
font-weight: 900;
|
| 42 |
+
background: var(--gradient-primary);
|
| 43 |
+
-webkit-background-clip: text;
|
| 44 |
+
-webkit-text-fill-color: transparent;
|
| 45 |
+
background-clip: text;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.btn-logout {
|
| 49 |
+
padding: var(--space-xs) var(--space-md);
|
| 50 |
+
background: transparent;
|
| 51 |
+
border: 2px solid var(--accent-red);
|
| 52 |
+
color: var(--accent-red);
|
| 53 |
+
font-weight: 600;
|
| 54 |
+
font-size: 0.85rem;
|
| 55 |
+
border-radius: var(--radius-md);
|
| 56 |
+
transition: all var(--transition-smooth);
|
| 57 |
+
text-transform: uppercase;
|
| 58 |
+
letter-spacing: 0.05em;
|
| 59 |
+
cursor: pointer;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.btn-logout:hover {
|
| 63 |
+
background: var(--accent-red);
|
| 64 |
+
color: var(--text-primary);
|
| 65 |
+
box-shadow: var(--glow-red);
|
| 66 |
+
transform: translateY(-2px);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Main Content */
|
| 70 |
+
.dashboard-main {
|
| 71 |
+
flex: 1;
|
| 72 |
+
padding: var(--space-xl);
|
| 73 |
+
max-width: 1400px;
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
width: 100%;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.welcome-section {
|
| 79 |
+
margin-bottom: var(--space-xl);
|
| 80 |
+
opacity: 0;
|
| 81 |
+
animation: fadeInUp 0.6s ease-out 0.2s forwards;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.welcome-title {
|
| 85 |
+
font-size: 2.5rem;
|
| 86 |
+
margin-bottom: var(--space-sm);
|
| 87 |
+
background: var(--gradient-primary);
|
| 88 |
+
-webkit-background-clip: text;
|
| 89 |
+
-webkit-text-fill-color: transparent;
|
| 90 |
+
background-clip: text;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Stock Cards Section */
|
| 94 |
+
.stocks-section {
|
| 95 |
+
opacity: 0;
|
| 96 |
+
animation: fadeInUp 0.6s ease-out 0.4s forwards;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.section-header {
|
| 100 |
+
display: flex;
|
| 101 |
+
justify-content: space-between;
|
| 102 |
+
align-items: center;
|
| 103 |
+
margin-bottom: var(--space-lg);
|
| 104 |
+
flex-wrap: wrap;
|
| 105 |
+
gap: var(--space-md);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.section-title {
|
| 109 |
+
font-size: 1.8rem;
|
| 110 |
+
color: var(--accent-cyan);
|
| 111 |
+
font-family: var(--font-accent);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.section-actions {
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
gap: var(--space-md);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.last-updated {
|
| 121 |
+
font-size: 0.85rem;
|
| 122 |
+
color: var(--text-secondary);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.btn-refresh {
|
| 126 |
+
padding: var(--space-xs) var(--space-md);
|
| 127 |
+
background: transparent;
|
| 128 |
+
border: 2px solid var(--accent-cyan);
|
| 129 |
+
color: var(--accent-cyan);
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
font-size: 0.85rem;
|
| 132 |
+
border-radius: var(--radius-md);
|
| 133 |
+
transition: all var(--transition-smooth);
|
| 134 |
+
display: flex;
|
| 135 |
+
align-items: center;
|
| 136 |
+
gap: var(--space-xs);
|
| 137 |
+
cursor: pointer;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.btn-refresh:hover:not(:disabled) {
|
| 141 |
+
background: var(--accent-cyan);
|
| 142 |
+
color: var(--bg-primary);
|
| 143 |
+
box-shadow: var(--glow-cyan);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.btn-refresh:disabled {
|
| 147 |
+
opacity: 0.5;
|
| 148 |
+
cursor: not-allowed;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.refresh-icon {
|
| 152 |
+
display: inline-block;
|
| 153 |
+
transition: transform 0.3s;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.btn-refresh:hover:not(:disabled) .refresh-icon {
|
| 157 |
+
transform: rotate(180deg);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.loading-state {
|
| 161 |
+
display: flex;
|
| 162 |
+
flex-direction: column;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: center;
|
| 165 |
+
padding: var(--space-xl);
|
| 166 |
+
color: var(--text-secondary);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.spinner {
|
| 170 |
+
width: 40px;
|
| 171 |
+
height: 40px;
|
| 172 |
+
border: 3px solid rgba(0, 240, 255, 0.2);
|
| 173 |
+
border-top-color: var(--accent-cyan);
|
| 174 |
+
border-radius: 50%;
|
| 175 |
+
animation: spin 1s linear infinite;
|
| 176 |
+
margin-bottom: var(--space-md);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
@keyframes spin {
|
| 180 |
+
to { transform: rotate(360deg); }
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.stocks-grid {
|
| 184 |
+
display: grid;
|
| 185 |
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 186 |
+
gap: var(--space-lg);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.stock-card {
|
| 190 |
+
background: var(--bg-secondary);
|
| 191 |
+
border: 1px solid rgba(0, 240, 255, 0.2);
|
| 192 |
+
border-radius: var(--radius-lg);
|
| 193 |
+
padding: var(--space-lg);
|
| 194 |
+
transition: all var(--transition-smooth);
|
| 195 |
+
cursor: pointer;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.stock-card:hover {
|
| 199 |
+
border-color: var(--accent-cyan);
|
| 200 |
+
box-shadow: var(--glow-cyan);
|
| 201 |
+
transform: translateY(-5px);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.stock-header {
|
| 205 |
+
display: flex;
|
| 206 |
+
justify-content: space-between;
|
| 207 |
+
align-items: center;
|
| 208 |
+
margin-bottom: var(--space-sm);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.stock-symbol {
|
| 212 |
+
font-family: var(--font-accent);
|
| 213 |
+
font-size: 1.2rem;
|
| 214 |
+
font-weight: 900;
|
| 215 |
+
color: var(--text-primary);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.stock-change {
|
| 219 |
+
font-weight: 700;
|
| 220 |
+
font-size: 0.9rem;
|
| 221 |
+
padding: 4px 8px;
|
| 222 |
+
border-radius: var(--radius-sm);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.stock-change.positive {
|
| 226 |
+
color: var(--accent-green);
|
| 227 |
+
background: rgba(57, 255, 20, 0.1);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.stock-change.negative {
|
| 231 |
+
color: var(--accent-red);
|
| 232 |
+
background: rgba(255, 0, 85, 0.1);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.stock-name {
|
| 236 |
+
font-size: 0.85rem;
|
| 237 |
+
color: var(--text-secondary);
|
| 238 |
+
margin-bottom: var(--space-md);
|
| 239 |
+
overflow: hidden;
|
| 240 |
+
text-overflow: ellipsis;
|
| 241 |
+
white-space: nowrap;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.stock-price {
|
| 245 |
+
font-family: var(--font-accent);
|
| 246 |
+
font-size: 2rem;
|
| 247 |
+
font-weight: 900;
|
| 248 |
+
color: var(--accent-cyan);
|
| 249 |
+
margin-bottom: var(--space-md);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.stock-details {
|
| 253 |
+
display: flex;
|
| 254 |
+
flex-direction: column;
|
| 255 |
+
gap: var(--space-xs);
|
| 256 |
+
padding-top: var(--space-sm);
|
| 257 |
+
border-top: 1px solid rgba(0, 240, 255, 0.1);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.stock-detail {
|
| 261 |
+
display: flex;
|
| 262 |
+
justify-content: space-between;
|
| 263 |
+
font-size: 0.85rem;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.detail-label {
|
| 267 |
+
color: var(--text-secondary);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.detail-value {
|
| 271 |
+
font-weight: 600;
|
| 272 |
+
color: var(--text-primary);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.detail-value.positive {
|
| 276 |
+
color: var(--accent-green);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.detail-value.negative {
|
| 280 |
+
color: var(--accent-red);
|
| 281 |
+
}
|
| 282 |
+
</style>
|
| 283 |
+
</head>
|
| 284 |
+
<body>
|
| 285 |
+
<div class="dashboard-container">
|
| 286 |
+
<!-- Header -->
|
| 287 |
+
<header class="dashboard-header">
|
| 288 |
+
<div class="dashboard-logo">STOCKLYZE</div>
|
| 289 |
+
<button class="btn-logout" onclick="handleLogout()">Logout</button>
|
| 290 |
+
</header>
|
| 291 |
+
|
| 292 |
+
<!-- Main Content -->
|
| 293 |
+
<main class="dashboard-main">
|
| 294 |
+
<!-- Welcome Section -->
|
| 295 |
+
<section class="welcome-section">
|
| 296 |
+
<h1 class="welcome-title">Stock Dashboard</h1>
|
| 297 |
+
<p style="color: var(--text-secondary); font-size: 1.1rem;">Real-time market data</p>
|
| 298 |
+
</section>
|
| 299 |
+
|
| 300 |
+
<!-- Stock Cards Section -->
|
| 301 |
+
<section class="stocks-section">
|
| 302 |
+
<div class="section-header">
|
| 303 |
+
<h2 class="section-title">📈 Popular Stocks</h2>
|
| 304 |
+
<div class="section-actions">
|
| 305 |
+
<span class="last-updated" id="last-updated">Loading...</span>
|
| 306 |
+
<button class="btn-refresh" onclick="loadStocks()" id="refresh-btn">
|
| 307 |
+
<span class="refresh-icon">↻</span> Refresh
|
| 308 |
+
</button>
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<div id="stocks-loading" class="loading-state">
|
| 313 |
+
<div class="spinner"></div>
|
| 314 |
+
<p>Fetching stock data...</p>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<div class="stocks-grid" id="stocks-grid"></div>
|
| 318 |
+
</section>
|
| 319 |
+
</main>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<!-- Scripts -->
|
| 323 |
+
<script src="js/config.js"></script>
|
| 324 |
+
<script>
|
| 325 |
+
const { API_BASE_URL, TOKEN_KEY } = window.CONFIG;
|
| 326 |
+
|
| 327 |
+
// Auto-load stocks on page load
|
| 328 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 329 |
+
console.log('🚀 Page loaded, starting stock fetch...');
|
| 330 |
+
loadStocks();
|
| 331 |
+
// Auto-refresh every 60 seconds
|
| 332 |
+
setInterval(loadStocks, 60000);
|
| 333 |
+
});
|
| 334 |
+
|
| 335 |
+
async function loadStocks() {
|
| 336 |
+
console.log('🔄 loadStocks() called');
|
| 337 |
+
const loadingEl = document.getElementById('stocks-loading');
|
| 338 |
+
const gridEl = document.getElementById('stocks-grid');
|
| 339 |
+
const refreshBtn = document.getElementById('refresh-btn');
|
| 340 |
+
|
| 341 |
+
loadingEl.style.display = 'flex';
|
| 342 |
+
if (refreshBtn) refreshBtn.disabled = true;
|
| 343 |
+
|
| 344 |
+
try {
|
| 345 |
+
// Step 1: Login
|
| 346 |
+
console.log('🔐 Logging in...');
|
| 347 |
+
const loginRes = await fetch(`${API_BASE_URL}/auth/login`, {
|
| 348 |
+
method: 'POST',
|
| 349 |
+
headers: { 'Content-Type': 'application/json' },
|
| 350 |
+
body: JSON.stringify({
|
| 351 |
+
email: 'admin@test.com',
|
| 352 |
+
password: 'SecurePass123!'
|
| 353 |
+
})
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
if (!loginRes.ok) throw new Error('Login failed');
|
| 357 |
+
|
| 358 |
+
const { access_token } = await loginRes.json();
|
| 359 |
+
console.log('✅ Login successful');
|
| 360 |
+
|
| 361 |
+
// Step 2: Fetch stocks
|
| 362 |
+
console.log('📡 Fetching stocks...');
|
| 363 |
+
const stocksRes = await fetch(`${API_BASE_URL}/stocks/popular`, {
|
| 364 |
+
headers: { 'Authorization': `Bearer ${access_token}` }
|
| 365 |
+
});
|
| 366 |
+
|
| 367 |
+
if (!stocksRes.ok) throw new Error('Failed to fetch stocks');
|
| 368 |
+
|
| 369 |
+
const data = await stocksRes.json();
|
| 370 |
+
console.log('✅ Got stocks:', data);
|
| 371 |
+
|
| 372 |
+
// Step 3: Display
|
| 373 |
+
displayStocks(data.stocks);
|
| 374 |
+
updateLastUpdated();
|
| 375 |
+
loadingEl.style.display = 'none';
|
| 376 |
+
|
| 377 |
+
} catch (error) {
|
| 378 |
+
console.error('❌ Error:', error);
|
| 379 |
+
loadingEl.style.display = 'none';
|
| 380 |
+
gridEl.innerHTML = `<p style="grid-column: 1/-1; text-align: center; color: var(--accent-red);">Error: ${error.message}</p>`;
|
| 381 |
+
} finally {
|
| 382 |
+
if (refreshBtn) refreshBtn.disabled = false;
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
function displayStocks(stocks) {
|
| 387 |
+
const gridEl = document.getElementById('stocks-grid');
|
| 388 |
+
|
| 389 |
+
gridEl.innerHTML = stocks.map(stock => {
|
| 390 |
+
const isPositive = stock.change >= 0;
|
| 391 |
+
const changeClass = isPositive ? 'positive' : 'negative';
|
| 392 |
+
const changeSymbol = isPositive ? '+' : '';
|
| 393 |
+
|
| 394 |
+
return `
|
| 395 |
+
<div class="stock-card">
|
| 396 |
+
<div class="stock-header">
|
| 397 |
+
<div class="stock-symbol">${stock.symbol}</div>
|
| 398 |
+
<div class="stock-change ${changeClass}">
|
| 399 |
+
${changeSymbol}${stock.change_percent.toFixed(2)}%
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
<div class="stock-name">${stock.name}</div>
|
| 403 |
+
<div class="stock-price">$${stock.price.toFixed(2)}</div>
|
| 404 |
+
<div class="stock-details">
|
| 405 |
+
<div class="stock-detail">
|
| 406 |
+
<span class="detail-label">Change:</span>
|
| 407 |
+
<span class="detail-value ${changeClass}">${changeSymbol}$${Math.abs(stock.change).toFixed(2)}</span>
|
| 408 |
+
</div>
|
| 409 |
+
<div class="stock-detail">
|
| 410 |
+
<span class="detail-label">Volume:</span>
|
| 411 |
+
<span class="detail-value">${formatVolume(stock.volume)}</span>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
`;
|
| 416 |
+
}).join('');
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
function formatVolume(volume) {
|
| 420 |
+
if (volume >= 1000000000) return (volume / 1000000000).toFixed(2) + 'B';
|
| 421 |
+
if (volume >= 1000000) return (volume / 1000000).toFixed(2) + 'M';
|
| 422 |
+
if (volume >= 1000) return (volume / 1000).toFixed(2) + 'K';
|
| 423 |
+
return volume.toString();
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function updateLastUpdated() {
|
| 427 |
+
const now = new Date();
|
| 428 |
+
document.getElementById('last-updated').textContent = `Last updated: ${now.toLocaleTimeString()}`;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
function handleLogout() {
|
| 432 |
+
localStorage.clear();
|
| 433 |
+
window.location.href = 'index.html';
|
| 434 |
+
}
|
| 435 |
+
</script>
|
| 436 |
+
</body>
|
| 437 |
+
</html>
|
frontend/index.html
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>STOCKLYZE - Road to Financial Freedom</title>
|
| 7 |
+
|
| 8 |
+
<!-- Google Fonts - Inter for Professional Fintech Look -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<style>
|
| 14 |
+
/* ============================================
|
| 15 |
+
DESIGN SYSTEM - STOCKLYZE
|
| 16 |
+
Visual Personality: Clean • Trustworthy • Data-Smart • Premium
|
| 17 |
+
============================================ */
|
| 18 |
+
|
| 19 |
+
:root {
|
| 20 |
+
/* Color System - Professional Fintech Palette */
|
| 21 |
+
--primary-teal: #00b4a6;
|
| 22 |
+
--primary-teal-dark: #008c82;
|
| 23 |
+
--primary-teal-light: #00d4c3;
|
| 24 |
+
|
| 25 |
+
/* Backgrounds */
|
| 26 |
+
--bg-primary: #fafbfc;
|
| 27 |
+
--bg-secondary: #f5f7fa;
|
| 28 |
+
--bg-white: #ffffff;
|
| 29 |
+
|
| 30 |
+
/* Text Colors */
|
| 31 |
+
--text-primary: #1a2332;
|
| 32 |
+
--text-secondary: #6b7280;
|
| 33 |
+
--text-muted: #9ca3af;
|
| 34 |
+
|
| 35 |
+
/* Supporting Colors */
|
| 36 |
+
--success: #10b981;
|
| 37 |
+
--success-light: #d1fae5;
|
| 38 |
+
--warning: #f59e0b;
|
| 39 |
+
--warning-light: #fef3c7;
|
| 40 |
+
--error: #ef4444;
|
| 41 |
+
--error-light: #fee2e2;
|
| 42 |
+
|
| 43 |
+
/* Shadows - Subtle & Professional */
|
| 44 |
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
|
| 45 |
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 46 |
+
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.12);
|
| 47 |
+
--shadow-glow: 0 0 0 3px rgba(0, 180, 166, 0.1);
|
| 48 |
+
|
| 49 |
+
/* Spacing System - 8px Base */
|
| 50 |
+
--space-1: 8px;
|
| 51 |
+
--space-2: 16px;
|
| 52 |
+
--space-3: 24px;
|
| 53 |
+
--space-4: 32px;
|
| 54 |
+
--space-6: 48px;
|
| 55 |
+
--space-8: 64px;
|
| 56 |
+
|
| 57 |
+
/* Border Radius */
|
| 58 |
+
--radius-sm: 8px;
|
| 59 |
+
--radius-md: 16px;
|
| 60 |
+
--radius-lg: 20px;
|
| 61 |
+
--radius-full: 9999px;
|
| 62 |
+
|
| 63 |
+
/* Typography */
|
| 64 |
+
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
* {
|
| 68 |
+
margin: 0;
|
| 69 |
+
padding: 0;
|
| 70 |
+
box-sizing: border-box;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
body {
|
| 74 |
+
font-family: var(--font-family);
|
| 75 |
+
background: var(--bg-primary);
|
| 76 |
+
min-height: 100vh;
|
| 77 |
+
display: flex;
|
| 78 |
+
align-items: center;
|
| 79 |
+
justify-content: center;
|
| 80 |
+
overflow: hidden;
|
| 81 |
+
position: relative;
|
| 82 |
+
-webkit-font-smoothing: antialiased;
|
| 83 |
+
-moz-osx-font-smoothing: grayscale;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Subtle Gradient Blobs - Calm, Not Loud */
|
| 87 |
+
.gradient-blob {
|
| 88 |
+
position: absolute;
|
| 89 |
+
width: 600px;
|
| 90 |
+
height: 600px;
|
| 91 |
+
background: radial-gradient(circle, rgba(0, 180, 166, 0.08) 0%, rgba(0, 180, 166, 0.04) 50%, transparent 70%);
|
| 92 |
+
border-radius: 50%;
|
| 93 |
+
filter: blur(60px);
|
| 94 |
+
animation: float 12s ease-in-out infinite;
|
| 95 |
+
z-index: 0;
|
| 96 |
+
pointer-events: none;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.gradient-blob:nth-child(1) {
|
| 100 |
+
top: -200px;
|
| 101 |
+
left: -200px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.gradient-blob:nth-child(2) {
|
| 105 |
+
bottom: -200px;
|
| 106 |
+
right: -200px;
|
| 107 |
+
animation-delay: 6s;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
@keyframes float {
|
| 111 |
+
0%, 100% {
|
| 112 |
+
transform: translate(0, 0) scale(1);
|
| 113 |
+
}
|
| 114 |
+
50% {
|
| 115 |
+
transform: translate(40px, 40px) scale(1.05);
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Main Container */
|
| 120 |
+
.container {
|
| 121 |
+
position: relative;
|
| 122 |
+
z-index: 1;
|
| 123 |
+
width: 100%;
|
| 124 |
+
max-width: 1200px;
|
| 125 |
+
padding: var(--space-4);
|
| 126 |
+
display: flex;
|
| 127 |
+
align-items: center;
|
| 128 |
+
justify-content: space-between;
|
| 129 |
+
gap: var(--space-8);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Left Side - Branding */
|
| 133 |
+
.brand-section {
|
| 134 |
+
flex: 1;
|
| 135 |
+
max-width: 520px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.brand-header {
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: center;
|
| 141 |
+
gap: 12px;
|
| 142 |
+
margin-bottom: var(--space-6);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.brand-icon {
|
| 146 |
+
width: 48px;
|
| 147 |
+
height: 48px;
|
| 148 |
+
background: linear-gradient(135deg, var(--primary-teal) 0%, var(--primary-teal-dark) 100%);
|
| 149 |
+
border-radius: 12px;
|
| 150 |
+
display: flex;
|
| 151 |
+
align-items: center;
|
| 152 |
+
justify-content: center;
|
| 153 |
+
font-size: 24px;
|
| 154 |
+
box-shadow: var(--shadow-md);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.brand-title {
|
| 158 |
+
font-size: 13px;
|
| 159 |
+
font-weight: 600;
|
| 160 |
+
color: var(--text-secondary);
|
| 161 |
+
text-transform: uppercase;
|
| 162 |
+
letter-spacing: 0.8px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.main-logo {
|
| 166 |
+
font-size: 64px;
|
| 167 |
+
font-weight: 800;
|
| 168 |
+
color: var(--text-primary);
|
| 169 |
+
margin-bottom: var(--space-2);
|
| 170 |
+
letter-spacing: -2px;
|
| 171 |
+
line-height: 1;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.main-tagline {
|
| 175 |
+
font-size: 24px;
|
| 176 |
+
color: var(--text-secondary);
|
| 177 |
+
font-weight: 500;
|
| 178 |
+
line-height: 1.4;
|
| 179 |
+
margin-bottom: var(--space-3);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.tagline-highlight {
|
| 183 |
+
color: var(--primary-teal);
|
| 184 |
+
font-weight: 700;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.security-badge {
|
| 188 |
+
display: inline-flex;
|
| 189 |
+
align-items: center;
|
| 190 |
+
gap: var(--space-1);
|
| 191 |
+
padding: var(--space-1) var(--space-2);
|
| 192 |
+
background: var(--success-light);
|
| 193 |
+
border-radius: var(--radius-full);
|
| 194 |
+
color: var(--success);
|
| 195 |
+
font-size: 13px;
|
| 196 |
+
font-weight: 600;
|
| 197 |
+
margin-top: var(--space-3);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* Right Side - Login Form */
|
| 201 |
+
.login-container {
|
| 202 |
+
flex: 0 0 440px;
|
| 203 |
+
background: var(--bg-white);
|
| 204 |
+
border-radius: var(--radius-lg);
|
| 205 |
+
padding: var(--space-6) var(--space-4);
|
| 206 |
+
box-shadow: var(--shadow-lg);
|
| 207 |
+
backdrop-filter: blur(10px);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.login-header {
|
| 211 |
+
margin-bottom: var(--space-4);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.login-title {
|
| 215 |
+
font-size: 28px;
|
| 216 |
+
font-weight: 700;
|
| 217 |
+
color: var(--text-primary);
|
| 218 |
+
margin-bottom: var(--space-1);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.login-subtitle {
|
| 222 |
+
font-size: 14px;
|
| 223 |
+
color: var(--text-secondary);
|
| 224 |
+
font-weight: 500;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
/* Form Styles */
|
| 228 |
+
.form-group {
|
| 229 |
+
margin-bottom: var(--space-3);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.form-label {
|
| 233 |
+
display: block;
|
| 234 |
+
font-size: 14px;
|
| 235 |
+
font-weight: 600;
|
| 236 |
+
color: var(--text-primary);
|
| 237 |
+
margin-bottom: var(--space-1);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.form-input {
|
| 241 |
+
width: 100%;
|
| 242 |
+
padding: 14px var(--space-2);
|
| 243 |
+
font-size: 15px;
|
| 244 |
+
font-weight: 500;
|
| 245 |
+
border: 2px solid #e5e7eb;
|
| 246 |
+
background: var(--bg-secondary);
|
| 247 |
+
border-radius: var(--radius-sm);
|
| 248 |
+
color: var(--text-primary);
|
| 249 |
+
outline: none;
|
| 250 |
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 251 |
+
font-family: var(--font-family);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.form-input::placeholder {
|
| 255 |
+
color: var(--text-muted);
|
| 256 |
+
font-weight: 400;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.form-input:focus {
|
| 260 |
+
border-color: var(--primary-teal);
|
| 261 |
+
background: var(--bg-white);
|
| 262 |
+
box-shadow: var(--shadow-glow);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.form-input:hover:not(:focus) {
|
| 266 |
+
border-color: #d1d5db;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/* Input States */
|
| 270 |
+
.form-input.error {
|
| 271 |
+
border-color: var(--error);
|
| 272 |
+
background: var(--error-light);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.form-input.success {
|
| 276 |
+
border-color: var(--success);
|
| 277 |
+
background: var(--success-light);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* Password Toggle */
|
| 281 |
+
.password-wrapper {
|
| 282 |
+
position: relative;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.password-toggle {
|
| 286 |
+
position: absolute;
|
| 287 |
+
right: var(--space-2);
|
| 288 |
+
top: 50%;
|
| 289 |
+
transform: translateY(-50%);
|
| 290 |
+
background: none;
|
| 291 |
+
border: none;
|
| 292 |
+
color: var(--text-muted);
|
| 293 |
+
cursor: pointer;
|
| 294 |
+
font-size: 18px;
|
| 295 |
+
padding: var(--space-1);
|
| 296 |
+
transition: color 0.2s;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.password-toggle:hover {
|
| 300 |
+
color: var(--text-secondary);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Submit Button */
|
| 304 |
+
.btn-submit {
|
| 305 |
+
width: 100%;
|
| 306 |
+
padding: 14px var(--space-3);
|
| 307 |
+
font-size: 15px;
|
| 308 |
+
font-weight: 700;
|
| 309 |
+
color: white;
|
| 310 |
+
background: linear-gradient(135deg, var(--primary-teal) 0%, var(--primary-teal-dark) 100%);
|
| 311 |
+
border: none;
|
| 312 |
+
border-radius: var(--radius-full);
|
| 313 |
+
cursor: pointer;
|
| 314 |
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 315 |
+
box-shadow: var(--shadow-md);
|
| 316 |
+
margin-top: var(--space-2);
|
| 317 |
+
font-family: var(--font-family);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.btn-submit:hover {
|
| 321 |
+
transform: translateY(-2px);
|
| 322 |
+
box-shadow: 0 8px 20px rgba(0, 180, 166, 0.25);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.btn-submit:active {
|
| 326 |
+
transform: translateY(0);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.btn-submit:disabled {
|
| 330 |
+
opacity: 0.6;
|
| 331 |
+
cursor: not-allowed;
|
| 332 |
+
transform: none;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
/* Role Selector */
|
| 336 |
+
.role-selector {
|
| 337 |
+
display: flex;
|
| 338 |
+
gap: var(--space-2);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.role-option {
|
| 342 |
+
flex: 1;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.role-option input[type="radio"] {
|
| 346 |
+
display: none;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.role-label {
|
| 350 |
+
display: block;
|
| 351 |
+
padding: 12px var(--space-2);
|
| 352 |
+
text-align: center;
|
| 353 |
+
border: 2px solid #e5e7eb;
|
| 354 |
+
border-radius: var(--radius-sm);
|
| 355 |
+
cursor: pointer;
|
| 356 |
+
font-weight: 600;
|
| 357 |
+
color: var(--text-secondary);
|
| 358 |
+
transition: all 0.2s;
|
| 359 |
+
background: var(--bg-secondary);
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.role-option input[type="radio"]:checked + .role-label {
|
| 363 |
+
border-color: var(--primary-teal);
|
| 364 |
+
background: rgba(0, 180, 166, 0.08);
|
| 365 |
+
color: var(--primary-teal);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.role-label:hover {
|
| 369 |
+
border-color: var(--primary-teal);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/* Toggle Auth Mode */
|
| 373 |
+
.auth-toggle {
|
| 374 |
+
text-align: center;
|
| 375 |
+
margin-top: var(--space-3);
|
| 376 |
+
font-size: 14px;
|
| 377 |
+
color: var(--text-secondary);
|
| 378 |
+
font-weight: 500;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.auth-toggle-link {
|
| 382 |
+
color: var(--primary-teal);
|
| 383 |
+
font-weight: 700;
|
| 384 |
+
text-decoration: none;
|
| 385 |
+
transition: color 0.2s;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.auth-toggle-link:hover {
|
| 389 |
+
color: var(--primary-teal-dark);
|
| 390 |
+
text-decoration: underline;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/* Alert Messages */
|
| 394 |
+
.alert {
|
| 395 |
+
padding: 12px var(--space-2);
|
| 396 |
+
border-radius: var(--radius-sm);
|
| 397 |
+
margin-bottom: var(--space-3);
|
| 398 |
+
font-size: 14px;
|
| 399 |
+
font-weight: 600;
|
| 400 |
+
animation: slideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 401 |
+
display: flex;
|
| 402 |
+
align-items: center;
|
| 403 |
+
gap: var(--space-1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.alert-error {
|
| 407 |
+
background: var(--error-light);
|
| 408 |
+
color: var(--error);
|
| 409 |
+
border: 2px solid var(--error);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.alert-success {
|
| 413 |
+
background: var(--success-light);
|
| 414 |
+
color: var(--success);
|
| 415 |
+
border: 2px solid var(--success);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
@keyframes slideDown {
|
| 419 |
+
from {
|
| 420 |
+
opacity: 0;
|
| 421 |
+
transform: translateY(-8px);
|
| 422 |
+
}
|
| 423 |
+
to {
|
| 424 |
+
opacity: 1;
|
| 425 |
+
transform: translateY(0);
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.hidden {
|
| 430 |
+
display: none !important;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
/* Responsive Design */
|
| 434 |
+
@media (max-width: 968px) {
|
| 435 |
+
.container {
|
| 436 |
+
flex-direction: column;
|
| 437 |
+
gap: var(--space-6);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.brand-section {
|
| 441 |
+
text-align: center;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.main-logo {
|
| 445 |
+
font-size: 48px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.main-tagline {
|
| 449 |
+
font-size: 20px;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.login-container {
|
| 453 |
+
flex: 1;
|
| 454 |
+
width: 100%;
|
| 455 |
+
max-width: 440px;
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
@media (max-width: 480px) {
|
| 460 |
+
.container {
|
| 461 |
+
padding: var(--space-2);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.login-container {
|
| 465 |
+
padding: var(--space-4) var(--space-3);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.main-logo {
|
| 469 |
+
font-size: 40px;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.main-tagline {
|
| 473 |
+
font-size: 18px;
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
</style>
|
| 477 |
+
</head>
|
| 478 |
+
<body>
|
| 479 |
+
<!-- Subtle Gradient Blobs -->
|
| 480 |
+
<div class="gradient-blob"></div>
|
| 481 |
+
<div class="gradient-blob"></div>
|
| 482 |
+
|
| 483 |
+
<div class="container">
|
| 484 |
+
<!-- Left Side - Branding -->
|
| 485 |
+
<div class="brand-section">
|
| 486 |
+
<div class="brand-header">
|
| 487 |
+
<div class="brand-icon">✦</div>
|
| 488 |
+
<div class="brand-title">Road to Financial Freedom</div>
|
| 489 |
+
</div>
|
| 490 |
+
|
| 491 |
+
<h1 class="main-logo">STOCKLYZE</h1>
|
| 492 |
+
<p class="main-tagline">
|
| 493 |
+
Advanced <span class="tagline-highlight">Portfolio Analytics</span>
|
| 494 |
+
</p>
|
| 495 |
+
|
| 496 |
+
<div class="security-badge">
|
| 497 |
+
🔒 Bank-level security
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<!-- Right Side - Login Form -->
|
| 502 |
+
<div class="login-container">
|
| 503 |
+
<div class="login-header">
|
| 504 |
+
<h2 class="login-title" id="auth-title">Welcome back</h2>
|
| 505 |
+
<p class="login-subtitle" id="auth-subtitle">Sign in to access your portfolio</p>
|
| 506 |
+
</div>
|
| 507 |
+
|
| 508 |
+
<!-- Alert Container -->
|
| 509 |
+
<div id="alert-container"></div>
|
| 510 |
+
|
| 511 |
+
<!-- Auth Form -->
|
| 512 |
+
<form id="auth-form">
|
| 513 |
+
<!-- Name (Signup only) -->
|
| 514 |
+
<div class="form-group hidden" id="name-group">
|
| 515 |
+
<label for="name" class="form-label">Full Name</label>
|
| 516 |
+
<input
|
| 517 |
+
type="text"
|
| 518 |
+
id="name"
|
| 519 |
+
class="form-input"
|
| 520 |
+
placeholder="Enter your full name"
|
| 521 |
+
autocomplete="name"
|
| 522 |
+
>
|
| 523 |
+
</div>
|
| 524 |
+
|
| 525 |
+
<!-- Email -->
|
| 526 |
+
<div class="form-group">
|
| 527 |
+
<label for="email" class="form-label">Email Address</label>
|
| 528 |
+
<input
|
| 529 |
+
type="email"
|
| 530 |
+
id="email"
|
| 531 |
+
class="form-input"
|
| 532 |
+
placeholder="you@example.com"
|
| 533 |
+
required
|
| 534 |
+
autocomplete="email"
|
| 535 |
+
>
|
| 536 |
+
</div>
|
| 537 |
+
|
| 538 |
+
<!-- Password -->
|
| 539 |
+
<div class="form-group">
|
| 540 |
+
<label for="password" class="form-label">Password</label>
|
| 541 |
+
<div class="password-wrapper">
|
| 542 |
+
<input
|
| 543 |
+
type="password"
|
| 544 |
+
id="password"
|
| 545 |
+
class="form-input"
|
| 546 |
+
placeholder="Enter your password"
|
| 547 |
+
required
|
| 548 |
+
autocomplete="current-password"
|
| 549 |
+
minlength="8"
|
| 550 |
+
>
|
| 551 |
+
<button type="button" class="password-toggle" onclick="togglePassword()">
|
| 552 |
+
👁️
|
| 553 |
+
</button>
|
| 554 |
+
</div>
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<!-- Role Selector (Signup only) -->
|
| 558 |
+
<div class="form-group hidden" id="role-group">
|
| 559 |
+
<label class="form-label">Account Type</label>
|
| 560 |
+
<div class="role-selector">
|
| 561 |
+
<div class="role-option">
|
| 562 |
+
<input type="radio" id="role-staff" name="role" value="STAFF" checked>
|
| 563 |
+
<label for="role-staff" class="role-label">Staff</label>
|
| 564 |
+
</div>
|
| 565 |
+
<div class="role-option">
|
| 566 |
+
<input type="radio" id="role-admin" name="role" value="ADMIN">
|
| 567 |
+
<label for="role-admin" class="role-label">Admin</label>
|
| 568 |
+
</div>
|
| 569 |
+
</div>
|
| 570 |
+
</div>
|
| 571 |
+
|
| 572 |
+
<!-- Submit Button -->
|
| 573 |
+
<button type="submit" class="btn-submit" id="submit-btn">
|
| 574 |
+
Sign In
|
| 575 |
+
</button>
|
| 576 |
+
</form>
|
| 577 |
+
|
| 578 |
+
<!-- Toggle Auth Mode -->
|
| 579 |
+
<div class="auth-toggle">
|
| 580 |
+
<span id="toggle-text">Don't have an account? </span>
|
| 581 |
+
<a href="#" class="auth-toggle-link" id="toggle-auth">Create one</a>
|
| 582 |
+
</div>
|
| 583 |
+
</div>
|
| 584 |
+
</div>
|
| 585 |
+
|
| 586 |
+
<script>
|
| 587 |
+
const API_BASE_URL = 'http://localhost:8000';
|
| 588 |
+
const TOKEN_KEY = 'stock_analysis_token';
|
| 589 |
+
const USER_KEY = 'stock_analysis_user';
|
| 590 |
+
let authMode = 'login';
|
| 591 |
+
|
| 592 |
+
// Initialize on page load
|
| 593 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 594 |
+
// Check if already authenticated
|
| 595 |
+
if (localStorage.getItem(TOKEN_KEY)) {
|
| 596 |
+
window.location.href = 'dashboard.html';
|
| 597 |
+
return;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
// Set up event listeners
|
| 601 |
+
const form = document.getElementById('auth-form');
|
| 602 |
+
const toggleLink = document.getElementById('toggle-auth');
|
| 603 |
+
|
| 604 |
+
form.addEventListener('submit', handleSubmit);
|
| 605 |
+
toggleLink.addEventListener('click', toggleAuthMode);
|
| 606 |
+
});
|
| 607 |
+
|
| 608 |
+
// Toggle password visibility
|
| 609 |
+
function togglePassword() {
|
| 610 |
+
const passwordInput = document.getElementById('password');
|
| 611 |
+
const toggleBtn = document.querySelector('.password-toggle');
|
| 612 |
+
|
| 613 |
+
if (passwordInput.type === 'password') {
|
| 614 |
+
passwordInput.type = 'text';
|
| 615 |
+
toggleBtn.textContent = '🙈';
|
| 616 |
+
} else {
|
| 617 |
+
passwordInput.type = 'password';
|
| 618 |
+
toggleBtn.textContent = '👁️';
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
// Toggle between login and signup
|
| 623 |
+
function toggleAuthMode(e) {
|
| 624 |
+
e.preventDefault();
|
| 625 |
+
authMode = authMode === 'login' ? 'signup' : 'login';
|
| 626 |
+
updateUI();
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
// Update UI based on mode
|
| 630 |
+
function updateUI() {
|
| 631 |
+
const title = document.getElementById('auth-title');
|
| 632 |
+
const subtitle = document.getElementById('auth-subtitle');
|
| 633 |
+
const nameGroup = document.getElementById('name-group');
|
| 634 |
+
const roleGroup = document.getElementById('role-group');
|
| 635 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 636 |
+
const toggleText = document.getElementById('toggle-text');
|
| 637 |
+
const toggleLink = document.getElementById('toggle-auth');
|
| 638 |
+
|
| 639 |
+
if (authMode === 'login') {
|
| 640 |
+
title.textContent = 'Welcome back';
|
| 641 |
+
subtitle.textContent = 'Sign in to access your portfolio';
|
| 642 |
+
nameGroup.classList.add('hidden');
|
| 643 |
+
roleGroup.classList.add('hidden');
|
| 644 |
+
submitBtn.textContent = 'Sign In';
|
| 645 |
+
toggleText.textContent = "Don't have an account? ";
|
| 646 |
+
toggleLink.textContent = 'Create one';
|
| 647 |
+
} else {
|
| 648 |
+
title.textContent = 'Create your account';
|
| 649 |
+
subtitle.textContent = 'Start your investment journey today';
|
| 650 |
+
nameGroup.classList.remove('hidden');
|
| 651 |
+
roleGroup.classList.remove('hidden');
|
| 652 |
+
submitBtn.textContent = 'Create Account';
|
| 653 |
+
toggleText.textContent = 'Already have an account? ';
|
| 654 |
+
toggleLink.textContent = 'Sign in';
|
| 655 |
+
}
|
| 656 |
+
clearAlert();
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
// Handle form submission
|
| 660 |
+
async function handleSubmit(e) {
|
| 661 |
+
e.preventDefault();
|
| 662 |
+
|
| 663 |
+
const email = document.getElementById('email').value.trim();
|
| 664 |
+
const password = document.getElementById('password').value;
|
| 665 |
+
|
| 666 |
+
// Validation
|
| 667 |
+
if (!email || !password) {
|
| 668 |
+
showError('Please fill in all fields');
|
| 669 |
+
return;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
if (authMode === 'login') {
|
| 673 |
+
await handleLogin(email, password);
|
| 674 |
+
} else {
|
| 675 |
+
const name = document.getElementById('name').value.trim();
|
| 676 |
+
const role = document.querySelector('input[name="role"]:checked')?.value || 'STAFF';
|
| 677 |
+
|
| 678 |
+
if (!name) {
|
| 679 |
+
showError('Please enter your name');
|
| 680 |
+
return;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
await handleSignup(email, name, password, role);
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Handle login
|
| 688 |
+
async function handleLogin(email, password) {
|
| 689 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 690 |
+
submitBtn.textContent = 'Signing in...';
|
| 691 |
+
submitBtn.disabled = true;
|
| 692 |
+
|
| 693 |
+
try {
|
| 694 |
+
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
| 695 |
+
method: 'POST',
|
| 696 |
+
headers: { 'Content-Type': 'application/json' },
|
| 697 |
+
body: JSON.stringify({ email, password })
|
| 698 |
+
});
|
| 699 |
+
|
| 700 |
+
const data = await response.json();
|
| 701 |
+
|
| 702 |
+
if (response.ok) {
|
| 703 |
+
localStorage.setItem(TOKEN_KEY, data.access_token);
|
| 704 |
+
showSuccess('Login successful! Redirecting...');
|
| 705 |
+
setTimeout(() => window.location.href = 'dashboard.html', 1000);
|
| 706 |
+
} else {
|
| 707 |
+
showError(data.detail || 'Invalid email or password');
|
| 708 |
+
submitBtn.textContent = 'Sign In';
|
| 709 |
+
submitBtn.disabled = false;
|
| 710 |
+
}
|
| 711 |
+
} catch (error) {
|
| 712 |
+
console.error('Login error:', error);
|
| 713 |
+
showError('Unable to connect to server. Please try again.');
|
| 714 |
+
submitBtn.textContent = 'Sign In';
|
| 715 |
+
submitBtn.disabled = false;
|
| 716 |
+
}
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
// Handle signup
|
| 720 |
+
async function handleSignup(email, name, password, role) {
|
| 721 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 722 |
+
submitBtn.textContent = 'Creating account...';
|
| 723 |
+
submitBtn.disabled = true;
|
| 724 |
+
|
| 725 |
+
try {
|
| 726 |
+
const response = await fetch(`${API_BASE_URL}/auth/signup`, {
|
| 727 |
+
method: 'POST',
|
| 728 |
+
headers: { 'Content-Type': 'application/json' },
|
| 729 |
+
body: JSON.stringify({ email, name, password, role })
|
| 730 |
+
});
|
| 731 |
+
|
| 732 |
+
const data = await response.json();
|
| 733 |
+
|
| 734 |
+
if (response.ok) {
|
| 735 |
+
showSuccess('Account created! Logging you in...');
|
| 736 |
+
setTimeout(() => handleLogin(email, password), 1000);
|
| 737 |
+
} else {
|
| 738 |
+
showError(data.detail || 'Unable to create account');
|
| 739 |
+
submitBtn.textContent = 'Create Account';
|
| 740 |
+
submitBtn.disabled = false;
|
| 741 |
+
}
|
| 742 |
+
} catch (error) {
|
| 743 |
+
console.error('Signup error:', error);
|
| 744 |
+
showError('Unable to connect to server. Please try again.');
|
| 745 |
+
submitBtn.textContent = 'Create Account';
|
| 746 |
+
submitBtn.disabled = false;
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
// Show error message
|
| 751 |
+
function showError(message) {
|
| 752 |
+
const container = document.getElementById('alert-container');
|
| 753 |
+
container.innerHTML = `
|
| 754 |
+
<div class="alert alert-error">
|
| 755 |
+
⚠️ ${message}
|
| 756 |
+
</div>
|
| 757 |
+
`;
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// Show success message
|
| 761 |
+
function showSuccess(message) {
|
| 762 |
+
const container = document.getElementById('alert-container');
|
| 763 |
+
container.innerHTML = `
|
| 764 |
+
<div class="alert alert-success">
|
| 765 |
+
✓ ${message}
|
| 766 |
+
</div>
|
| 767 |
+
`;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
// Clear alert
|
| 771 |
+
function clearAlert() {
|
| 772 |
+
document.getElementById('alert-container').innerHTML = '';
|
| 773 |
+
}
|
| 774 |
+
</script>
|
| 775 |
+
</body>
|
| 776 |
+
</html>
|
frontend/js/auth.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Authentication Logic
|
| 2 |
+
|
| 3 |
+
const { API_BASE_URL, TOKEN_KEY, USER_KEY } = window.CONFIG;
|
| 4 |
+
const { showError, showSuccess, validateEmail, validatePassword, setLoading, animateFormSwitch } = window.utils;
|
| 5 |
+
|
| 6 |
+
// Current auth mode
|
| 7 |
+
let authMode = 'login'; // 'login' or 'signup'
|
| 8 |
+
|
| 9 |
+
// Initialize on page load
|
| 10 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 11 |
+
// Check if already authenticated
|
| 12 |
+
if (getToken()) {
|
| 13 |
+
redirectToDashboard();
|
| 14 |
+
return;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Set up event listeners
|
| 18 |
+
setupEventListeners();
|
| 19 |
+
|
| 20 |
+
// Set initial mode
|
| 21 |
+
setAuthMode('login');
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// Set up event listeners
|
| 25 |
+
function setupEventListeners() {
|
| 26 |
+
const form = document.getElementById('auth-form');
|
| 27 |
+
const toggleLink = document.getElementById('toggle-auth');
|
| 28 |
+
|
| 29 |
+
form.addEventListener('submit', handleSubmit);
|
| 30 |
+
toggleLink.addEventListener('click', toggleAuthMode);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Toggle between login and signup
|
| 34 |
+
function toggleAuthMode(e) {
|
| 35 |
+
e.preventDefault();
|
| 36 |
+
authMode = authMode === 'login' ? 'signup' : 'login';
|
| 37 |
+
setAuthMode(authMode);
|
| 38 |
+
animateFormSwitch();
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Set auth mode UI
|
| 42 |
+
function setAuthMode(mode) {
|
| 43 |
+
const title = document.getElementById('auth-title');
|
| 44 |
+
const subtitle = document.getElementById('auth-subtitle');
|
| 45 |
+
const nameGroup = document.getElementById('name-group');
|
| 46 |
+
const roleGroup = document.getElementById('role-group');
|
| 47 |
+
const submitBtn = document.getElementById('submit-btn');
|
| 48 |
+
const toggleText = document.getElementById('toggle-text');
|
| 49 |
+
const toggleLink = document.getElementById('toggle-auth');
|
| 50 |
+
|
| 51 |
+
if (mode === 'login') {
|
| 52 |
+
title.textContent = 'Welcome Back';
|
| 53 |
+
subtitle.textContent = 'Sign in to access your portfolio';
|
| 54 |
+
nameGroup.classList.add('hidden');
|
| 55 |
+
roleGroup.classList.add('hidden');
|
| 56 |
+
submitBtn.textContent = 'Sign In';
|
| 57 |
+
toggleText.textContent = "Don't have an account? ";
|
| 58 |
+
toggleLink.textContent = 'Create one';
|
| 59 |
+
} else {
|
| 60 |
+
title.textContent = 'Create Account';
|
| 61 |
+
subtitle.textContent = 'Join the stock analysis platform';
|
| 62 |
+
nameGroup.classList.remove('hidden');
|
| 63 |
+
roleGroup.classList.remove('hidden');
|
| 64 |
+
submitBtn.textContent = 'Sign Up';
|
| 65 |
+
toggleText.textContent = 'Already have an account? ';
|
| 66 |
+
toggleLink.textContent = 'Sign in';
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Handle form submission
|
| 71 |
+
async function handleSubmit(e) {
|
| 72 |
+
e.preventDefault();
|
| 73 |
+
|
| 74 |
+
const email = document.getElementById('email').value.trim();
|
| 75 |
+
const password = document.getElementById('password').value;
|
| 76 |
+
|
| 77 |
+
// Validation
|
| 78 |
+
if (!validateEmail(email)) {
|
| 79 |
+
showError('Please enter a valid email address');
|
| 80 |
+
return;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const passwordCheck = validatePassword(password);
|
| 84 |
+
if (!passwordCheck.valid) {
|
| 85 |
+
showError(passwordCheck.message);
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if (authMode === 'login') {
|
| 90 |
+
await handleLogin(email, password);
|
| 91 |
+
} else {
|
| 92 |
+
const name = document.getElementById('name').value.trim();
|
| 93 |
+
const role = document.querySelector('input[name="role"]:checked')?.value || 'STAFF';
|
| 94 |
+
|
| 95 |
+
if (!name) {
|
| 96 |
+
showError('Please enter your name');
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
await handleSignup(email, name, password, role);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Handle login
|
| 105 |
+
async function handleLogin(email, password) {
|
| 106 |
+
setLoading(true);
|
| 107 |
+
|
| 108 |
+
try {
|
| 109 |
+
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
| 110 |
+
method: 'POST',
|
| 111 |
+
headers: {
|
| 112 |
+
'Content-Type': 'application/json'
|
| 113 |
+
},
|
| 114 |
+
body: JSON.stringify({ email, password })
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
const data = await response.json();
|
| 118 |
+
|
| 119 |
+
if (response.ok) {
|
| 120 |
+
saveToken(data.access_token);
|
| 121 |
+
await fetchUserData();
|
| 122 |
+
showSuccess('Login successful! Redirecting...');
|
| 123 |
+
setTimeout(() => redirectToDashboard(), 1000);
|
| 124 |
+
} else {
|
| 125 |
+
showError(data.detail || 'Invalid email or password');
|
| 126 |
+
setLoading(false);
|
| 127 |
+
}
|
| 128 |
+
} catch (error) {
|
| 129 |
+
console.error('Login error:', error);
|
| 130 |
+
showError('Unable to connect to server. Please try again.');
|
| 131 |
+
setLoading(false);
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Handle signup
|
| 136 |
+
async function handleSignup(email, name, password, role) {
|
| 137 |
+
setLoading(true);
|
| 138 |
+
|
| 139 |
+
try {
|
| 140 |
+
const response = await fetch(`${API_BASE_URL}/auth/signup`, {
|
| 141 |
+
method: 'POST',
|
| 142 |
+
headers: {
|
| 143 |
+
'Content-Type': 'application/json'
|
| 144 |
+
},
|
| 145 |
+
body: JSON.stringify({ email, name, password, role })
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
const data = await response.json();
|
| 149 |
+
|
| 150 |
+
if (response.ok) {
|
| 151 |
+
showSuccess('Account created! Logging you in...');
|
| 152 |
+
// Auto-login after signup
|
| 153 |
+
setTimeout(() => handleLogin(email, password), 1000);
|
| 154 |
+
} else {
|
| 155 |
+
showError(data.detail || 'Unable to create account');
|
| 156 |
+
setLoading(false);
|
| 157 |
+
}
|
| 158 |
+
} catch (error) {
|
| 159 |
+
console.error('Signup error:', error);
|
| 160 |
+
showError('Unable to connect to server. Please try again.');
|
| 161 |
+
setLoading(false);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Fetch user data
|
| 166 |
+
async function fetchUserData() {
|
| 167 |
+
const token = getToken();
|
| 168 |
+
if (!token) return null;
|
| 169 |
+
|
| 170 |
+
try {
|
| 171 |
+
const response = await fetch(`${API_BASE_URL}/auth/me`, {
|
| 172 |
+
headers: {
|
| 173 |
+
'Authorization': `Bearer ${token}`
|
| 174 |
+
}
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
if (response.ok) {
|
| 178 |
+
const userData = await response.json();
|
| 179 |
+
saveUser(userData);
|
| 180 |
+
return userData;
|
| 181 |
+
} else {
|
| 182 |
+
// Token invalid, clear it
|
| 183 |
+
clearAuth();
|
| 184 |
+
return null;
|
| 185 |
+
}
|
| 186 |
+
} catch (error) {
|
| 187 |
+
console.error('Fetch user error:', error);
|
| 188 |
+
return null;
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Save token to localStorage
|
| 193 |
+
function saveToken(token) {
|
| 194 |
+
localStorage.setItem(TOKEN_KEY, token);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Get token from localStorage
|
| 198 |
+
function getToken() {
|
| 199 |
+
return localStorage.getItem(TOKEN_KEY);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Save user data
|
| 203 |
+
function saveUser(userData) {
|
| 204 |
+
localStorage.setItem(USER_KEY, JSON.stringify(userData));
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// Get user data
|
| 208 |
+
function getUser() {
|
| 209 |
+
const userData = localStorage.getItem(USER_KEY);
|
| 210 |
+
return userData ? JSON.parse(userData) : null;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// Clear authentication
|
| 214 |
+
function clearAuth() {
|
| 215 |
+
localStorage.removeItem(TOKEN_KEY);
|
| 216 |
+
localStorage.removeItem(USER_KEY);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// Logout
|
| 220 |
+
function logout() {
|
| 221 |
+
clearAuth();
|
| 222 |
+
window.location.href = 'index.html';
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Redirect to dashboard
|
| 226 |
+
function redirectToDashboard() {
|
| 227 |
+
window.location.href = 'dashboard.html';
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Export functions
|
| 231 |
+
window.auth = {
|
| 232 |
+
getToken,
|
| 233 |
+
getUser,
|
| 234 |
+
logout,
|
| 235 |
+
fetchUserData,
|
| 236 |
+
clearAuth
|
| 237 |
+
};
|