BOLO-KESARI commited on
Commit
d2426db
·
0 Parent(s):

Initial commit for deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .agent/rules/check.md +6 -0
  2. .agent/workflows/pre-commit-check.md +72 -0
  3. .agent/workflows/quality-check.md +134 -0
  4. .agent/workflows/weekly-audit.md +105 -0
  5. .github/workflows/deploy_and_keep_alive.yml +34 -0
  6. .gitignore +37 -0
  7. AGENT_USAGE.md +196 -0
  8. DEPLOYMENT_GUIDE.md +70 -0
  9. Dockerfile +35 -0
  10. README.md +50 -0
  11. Ticker_List_NSE_India.csv +0 -0
  12. agent_config.yaml +94 -0
  13. app.py +38 -0
  14. backend/.env.example +11 -0
  15. backend/Ticker_List_NSE_India.csv +0 -0
  16. backend/app/__init__.py +1 -0
  17. backend/app/core/__init__.py +1 -0
  18. backend/app/core/config.py +36 -0
  19. backend/app/core/database.py +32 -0
  20. backend/app/core/security.py +157 -0
  21. backend/app/main.py +51 -0
  22. backend/app/models/__init__.py +3 -0
  23. backend/app/models/portfolio.py +30 -0
  24. backend/app/models/user.py +33 -0
  25. backend/app/routers/__init__.py +1 -0
  26. backend/app/routers/auth.py +116 -0
  27. backend/app/routers/portfolio.py +136 -0
  28. backend/app/routers/stocks.py +158 -0
  29. backend/app/schemas/__init__.py +1 -0
  30. backend/app/schemas/auth.py +45 -0
  31. backend/app/schemas/portfolio.py +43 -0
  32. backend/app/services/__init__.py +3 -0
  33. backend/app/services/stock_service.py +150 -0
  34. backend/create_account.py +40 -0
  35. backend/database_schema.sql +36 -0
  36. backend/init_db.py +14 -0
  37. backend/requirements.txt +9 -0
  38. backend/stock_analysis.db +0 -0
  39. backend/test_api.py +70 -0
  40. backend/test_stocks.py +41 -0
  41. code_quality_agent.py +293 -0
  42. frontend/README.md +260 -0
  43. frontend/css/animations.css +177 -0
  44. frontend/css/auth.css +402 -0
  45. frontend/css/base.css +127 -0
  46. frontend/css/variables.css +58 -0
  47. frontend/dashboard.html +624 -0
  48. frontend/dashboard_direct.html +437 -0
  49. frontend/index.html +776 -0
  50. 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
+ };